wandres.dev
I18N · internacionalización

Helpers de astro:i18n: construir URLs por idioma

Dejar de teclear a mano las direcciones localizadas y derivarlas de la configuración con las funciones del módulo virtual astro:i18n. getRelativeLocaleUrl y getAbsoluteLocaleUrl para una URL por idioma, sus variantes en lista para recorrer todos los idiomas de golpe, y el par getPathByLocale y getLocaleByPath que traduce entre el código de un idioma y el segmento que se ve en la URL cuando usas path y codes personalizados.

⏱ 16 min

Una vez que las URLs de tu sitio dependen del idioma, escribirlas a mano se vuelve una trampa. Un enlace /en/blog/ teclado literalmente ignora tu trailingSlash, tu base, tu prefixDefaultLocale; y el día que cambies cualquiera de esas claves, se romperá en silencio, disperso por cientos de plantillas. El módulo virtual astro:i18n existe para que nunca vuelvas a escribir una URL localizada a mano: sus funciones derivan cada dirección de tu configuración, de modo que respetan todas tus políticas de rutas a la vez y se rehacen solas cuando la configuración cambia. Son el puente entre el mapa de idiomas que declaraste y los enlaces concretos que tu HTML necesita.

🎯 Al terminar esta lección sabrás
  • Construir una URL relativa por idioma con getRelativeLocaleUrl respetando tu configuración de rutas.
  • Producir direcciones absolutas con getAbsoluteLocaleUrl a partir de site.
  • Recorrer todos los idiomas de golpe con las variantes en lista para selectores y hreflang.
  • Traducir entre código y segmento de URL con getPathByLocale y getLocaleByPath.

getRelativeLocaleUrl: la dirección derivada

La función central del módulo es getRelativeLocaleUrl. Recibe un código de idioma y, opcionalmente, una ruta interna, y devuelve la dirección relativa correcta para ese idioma. Lo importante no es que concatene cadenas —eso lo haría cualquiera— sino que aplica toda tu configuración de rutas: el prefijo o su ausencia según prefixDefaultLocale, la barra final de trailingSlash, el subdirectorio de base, la forma de build.format. Una sola llamada carga con todas esas decisiones.

---
import { getRelativeLocaleUrl } from 'astro:i18n';

const inicioEn = getRelativeLocaleUrl('en');          // '/en/'
const blogEn = getRelativeLocaleUrl('en', 'blog');    // '/en/blog/'
const blogEs = getRelativeLocaleUrl('es', 'blog');    // '/blog/' si es es el default sin prefijo
---
<a href={blogEn}>Read in English</a>

Fíjate en la última línea del comentario: si es es el idioma por defecto y prefixDefaultLocale es false, getRelativeLocaleUrl('es', 'blog') no devuelve /es/blog/ sino /blog/, porque el default no lleva prefijo. Esa es exactamente la lógica que no querrías reimplementar a mano en cada plantilla, y la razón de que la función exista: encapsula la regla completa de tu enrutado en un único punto, y tú solo dices qué idioma y qué ruta.

El tercer argumento, opcional, es un objeto de opciones. El más útil es normalizeLocale, activo por defecto, que pasa el código a minúsculas y forma canónica; puedes desactivarlo si necesitas preservar un código tal cual lo escribiste. La regla mental es simple: para cualquier enlace interno que dependa del idioma, esta función es la fuente, y el literal es el error.

getAbsoluteLocaleUrl y las variantes en lista

Cuando una dirección tiene que salir de tu sitio —una etiqueta canónica, un hreflang, una URL en un feed o en datos estructurados— no basta con la ruta relativa: hace falta el dominio completo. Para eso está getAbsoluteLocaleUrl, que hace lo mismo que su hermana relativa pero antepone el site de tu configuración. Sin site declarado no puede trabajar, del mismo modo que el sitemap o las canónicas tampoco pueden.

---
import { getAbsoluteLocaleUrl } from 'astro:i18n';

// con site: 'https://misitio.com'
const canon = getAbsoluteLocaleUrl('fr', 'blog');
// 'https://misitio.com/fr/blog/'
---
<link rel="canonical" href={canon} />

Pero el patrón que de verdad brilla en un sitio multilingüe es el de las listas. getRelativeLocaleUrlList y getAbsoluteLocaleUrlList reciben solo la ruta —no un idioma— y devuelven un array con esa misma página en todos los idiomas configurados. Es el material exacto con el que se construye un selector de idioma o el bloque de etiquetas hreflang, sin que tengas que enumerar los locales a mano.

---
import { getRelativeLocaleUrlList } from 'astro:i18n';

const versiones = getRelativeLocaleUrlList('blog');
// ['/blog/', '/en/blog/', '/fr/blog/']
---
<ul>
  {versiones.map((url) => <li><a href={url}>{url}</a></li>)}
</ul>

La ventaja no es ahorrar tres líneas: es que el selector se deriva de locales. Añade mañana un cuarto idioma a la configuración y el selector crece solo, sin que toques la plantilla. Lo mismo vale para las etiquetas hreflang de la lección de SEO: nacen de la misma lista, de modo que el conjunto de alternativas nunca queda desincronizado con los idiomas reales del sitio.

🔗

getRelativeLocaleUrl

Idioma mas ruta a URL relativa. Aplica prefijo, trailingSlash y base. Para enlaces internos.

🌐

getAbsoluteLocaleUrl

Igual pero con el dominio de site delante. Para canonicas, hreflang y feeds.

📋

Las variantes List

Reciben solo la ruta y devuelven la pagina en todos los idiomas. Para selectores y alternativas.

🔁

getPathByLocale y getLocaleByPath

Traducen entre el codigo de un idioma y el segmento que aparece en la URL.

getPathByLocale y getLocaleByPath: código contra segmento

El último par de funciones cobra sentido cuando usas los locales con forma de objeto que vimos en la lección anterior: aquellos donde el path visible en la URL difiere de los codes de idioma. En cuanto la URL dice /espanol/ pero el código es es, necesitas traducir en ambas direcciones, y para eso están estas dos funciones inversas.

getPathByLocale recibe un código de idioma y devuelve el segmento de URL asociado; getLocaleByPath hace el viaje contrario, del segmento al código. La primera la usas cuando tienes un código —de una cabecera, de una preferencia— y quieres construir la URL; la segunda, cuando lees un segmento de la URL y quieres saber a qué idioma pertenece.

import { getPathByLocale, getLocaleByPath } from 'astro:i18n';

// con locales: [{ path: 'espanol', codes: ['es', 'es-ES'] }, 'en']
getPathByLocale('es');        // 'espanol'
getPathByLocale('es-ES');     // 'espanol' (ambos codes apuntan al mismo path)
getLocaleByPath('espanol');   // 'es' (el primer code asociado)

La asimetría es reveladora: varios codes pueden apuntar al mismo pathes y es-ES comparten /espanol/—, pero un path devuelve un solo código, el primero de su lista. Es la misma relación de muchos a uno que hay entre las variantes regionales de un idioma y su carpeta única en el sitio. Cuando tus locales son cadenas simples, path y code coinciden y estas funciones son triviales; su valor aparece justo cuando decides separar lo que el usuario ve de lo que el navegador anuncia.

Conviene subrayar que estas dos funciones operan sobre la configuración, no sobre la petición: no saben nada del visitante ni de la URL en curso, solo consultan el mapa de locales que declaraste. Eso las hace utilizables en cualquier contexto —una página estática, un endpoint, un script de build— sin depender de que exista una petición viva, a diferencia de la negociación por cabecera que veremos más adelante. Son traducciones puras entre dos representaciones del mismo idioma, y por serlo, están siempre disponibles.

flowchart LR
CFG[configuracion i18n] --> REL[getRelativeLocaleUrl]
CFG --> ABS[getAbsoluteLocaleUrl]
CFG --> LIST[variantes List]
CFG --> PB[getPathByLocale]
CFG --> LB[getLocaleByPath]
REL --> LINK[enlace interno]
ABS --> CAN[canonica y hreflang]
LIST --> SEL[selector de idioma]
PB --> SEG[segmento de la URL]
LB --> CODE[codigo de idioma]
style CFG fill:#89b4fa,color:#11111b
style LIST fill:#f9e2af,color:#11111b
style SEL fill:#a6e3a1,color:#11111b
💡
Centraliza el enlazado en un helper propio

Aunque estas funciones ya derivan las URLs de tu configuración, conviene envolverlas en un pequeño helper de tu proyecto —un t() de enlaces, por ejemplo— que fije convenciones comunes: el idioma actual por defecto, prefijos de ruta que repites, el class de un enlace activo. Así tus plantillas llaman a tu función y no a la de Astro directamente, y el día que cambie algo lo cambias en un sitio. Es el mismo principio de la lección de base: concentra la construcción de rutas en un puñado de puntos, nunca dispersa por el HTML.

⚠️
getAbsoluteLocaleUrl exige site, o falla

Las funciones absolutas necesitan un site en astro.config.mjs para anteponer el dominio; sin él no tienen base sobre la que construir y lanzan un error en el build. Es el mismo requisito del sitemap y las canónicas. Si tus hreflang o tus URLs de feed aparecen incompletas o el build se cae al generarlas, lo primero que hay que comprobar es que site esté declarado con la URL de producción, no una de desarrollo.

📝
Las listas son el cimiento del SEO multilingüe

Las variantes getRelativeLocaleUrlList y getAbsoluteLocaleUrlList no solo alimentan selectores de idioma visibles: son la materia prima de las etiquetas hreflang que declaran a los buscadores qué páginas son equivalentes entre idiomas. En la lección de SEO verás que ese bloque de alternativas se genera exactamente de aquí, de modo que la lista de idiomas que ve el usuario y la que ve el buscador nacen del mismo cálculo y no pueden discrepar. Vale la pena fijar el patrón desde ahora: una ruta entra, todas sus versiones salen.

Derivar en lugar de escribir: la URL como función de la configuración

Estas funciones encarnan un principio que atraviesa toda la ingeniería seria y que rara vez se enuncia con claridad: cuando un dato puede computarse a partir de otro, computarlo es siempre más fiable que escribirlo, porque el cómputo se repite idéntico y la escritura se pudre en cuanto cambia su fuente. Una URL localizada no es un dato primitivo: es una función de varias entradas —el idioma, la ruta lógica, y las políticas de prefixDefaultLocale, trailingSlash, base, site— que se combinan según reglas fijas. Teclearla a mano es congelar el resultado de esa función en un literal, y ese literal empieza a mentir en el instante en que cualquiera de sus entradas cambia. getRelativeLocaleUrl y sus compañeras te devuelven al mundo correcto, donde la URL vuelve a ser lo que conceptualmente siempre fue: no un texto que guardas, sino un cálculo que ejecutas. Esta distinción entre valor almacenado y valor derivado es una de las más productivas que existen, y reaparece por todas partes: en las hojas de cálculo que separan celdas de entrada de celdas con fórmula, en las bases de datos que prefieren vistas a copias, en los hreflang y sitemaps de la última lección que se generan del mismo manantial. La internacionalización es solo un dominio donde el coste de equivocarse es especialmente visible, porque un enlace roto entre idiomas se nota enseguida. Pero la lección trasciende el i18n: cada vez que sientas la tentación de teclear un valor que podría derivarse de otro, estás eligiendo entre una verdad que se recalcula y una copia que se corrompe. Elige siempre la función sobre el literal.

⚔️ Sustituye tus URLs a mano por URLs derivadas
  1. Reemplaza un enlace /en/blog/ escrito a mano por una llamada a getRelativeLocaleUrl('en', 'blog') y comprueba que la salida coincide.
  2. Cambia trailingSlash a never y observa cómo la URL derivada se ajusta sola mientras el literal no lo habría hecho.
  3. Construye un selector de idioma con getRelativeLocaleUrlList y añade un cuarto idioma a locales para verlo crecer sin tocar la plantilla.
  4. Define un locale con path: 'espanol' y codes, y usa getPathByLocale y getLocaleByPath para ir del código al segmento y de vuelta.