wandres.dev
I18N · internacionalización

El routing i18n: locales, defaultLocale y prefijos

Activar el enrutado internacional declarando la clave i18n en la configuración: enumerar los idiomas con locales, elegir el principal con defaultLocale y gobernar la forma de las URLs con routing. El papel decisivo de prefixDefaultLocale, la disposición de carpetas que cada modo exige, los locales con path y codes personalizados, y qué hace exactamente redirectToDefaultLocale con la raíz del sitio.

⏱ 16 min

Un sitio en varios idiomas no es un sitio traducido: es un sitio que aprende a multiplicar sus rutas. La misma página tiene que existir en español, en inglés y en francés, cada versión bajo una dirección que la distinga, y alguien debe decidir cómo se dibuja esa dirección: si el idioma principal lleva prefijo o vive en la raíz, qué pasa cuando alguien pide / a secas, cómo casan las carpetas con las URLs. La clave i18n de astro.config.mjs es donde se toman esas decisiones de una vez y para todo el proyecto. No traduce una sola palabra —eso vendrá después—, pero fija la gramática de las URLs sobre la que todo lo demás se apoya.

🎯 Al terminar esta lección sabrás
  • Activar el enrutado internacional declarando la clave i18n en astro.config.mjs.
  • Enumerar los idiomas con locales y elegir el principal con defaultLocale.
  • Gobernar la forma de las URLs con prefixDefaultLocale y su disposición de carpetas.
  • Decidir el destino de la raíz con redirectToDefaultLocale y conocer los locales con path y codes.

locales y defaultLocale: declarar el mapa de idiomas

Todo empieza con dos datos: qué idiomas hablas y cuál es el principal. locales es un array con los códigos que tu sitio soporta; defaultLocale señala uno de ellos como el idioma de referencia, el que se sirve cuando nada indica otra cosa. Con esas dos líneas Astro ya sabe distinguir una ruta localizada de una que no lo está, y activa todo el aparato de helpers y propiedades que veremos en las lecciones siguientes.

// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({
  i18n: {
    locales: ['es', 'en', 'fr'],
    defaultLocale: 'es',
  },
});

Conviene entender qué hace y qué no hace esta declaración. Activa el reconocimiento de idiomas en las URLs y habilita el módulo virtual astro:i18n; pero no genera ni una sola página por su cuenta. Las carpetas siguen siendo tuyas: eres tú quien crea src/pages/en/ para el inglés y src/pages/fr/ para el francés. Astro no traduce ni duplica contenido, solo interpreta la forma de las direcciones y te da las herramientas para construirlas. La internacionalización de Astro es una capa de enrutado, no un motor de traducción.

El defaultLocale debe ser uno de los valores presentes en locales, o Astro protesta en el arranque. Esa redundancia aparente es deliberada: obliga a que el idioma por defecto sea siempre un idioma declarado, nunca un huérfano. A partir de aquí, la pregunta que lo cambia todo es cómo se ve en la URL ese idioma principal, y esa la responde routing.

prefixDefaultLocale: dos mundos de URLs

Dentro de routing, la opción prefixDefaultLocale es la decisión estructural del nivel entero, porque define dos disposiciones de carpetas incompatibles. Su valor por defecto es false, y significa que solo los idiomas secundarios llevan prefijo. El idioma por defecto vive en la raíz de src/pages, sin marca alguna en su dirección.

i18n: {
  locales: ['es', 'en', 'fr'],
  defaultLocale: 'es',
  routing: {
    prefixDefaultLocale: false,
  },
},

Con false, la disposición es asimétrica y suele ser la más natural para un sitio que nace en un idioma y se abre a otros. El fichero src/pages/index.astro sirve / en español; src/pages/en/index.astro sirve /en/ en inglés; src/pages/fr/blog.astro sirve /fr/blog. El español no tiene carpeta propia porque es la raíz.

Cuando pones prefixDefaultLocale: true, el mundo se vuelve simétrico: todos los idiomas llevan prefijo, sin excepción. Ya no hay contenido en la raíz de src/pages; cada página vive bajo la carpeta de su idioma, incluido el principal.

routing: {
  prefixDefaultLocale: true,
},

Ahora src/pages/es/index.astro sirve /es/, src/pages/en/index.astro sirve /en/, y la raíz / deja de tener un dueño natural: nadie vive ahí. Esa simetría tiene una virtud —ninguna URL es ambigua sobre su idioma— y un coste —hay que decidir qué ocurre cuando alguien escribe / sin más—, y ese coste lo paga la siguiente opción.

🌍

prefixDefaultLocale false

Asimetrico. El idioma por defecto vive en la raiz de src pages sin prefijo. Los demas en su carpeta.

🏳️

prefixDefaultLocale true

Simetrico. Todos los idiomas llevan prefijo y viven en su carpeta. La raiz queda vacia.

🧭

redirectToDefaultLocale

Con prefijo en todos, decide si la raiz redirige al idioma por defecto o responde con un 404.

🔤

path y codes

Un locale puede ser un objeto: el path que se ve en la URL y los codes que casan en la negociacion.

redirectToDefaultLocale y los locales con forma de objeto

Cuando prefixDefaultLocale es true, la raíz queda sin contenido, y redirectToDefaultLocale decide su suerte. Con su valor por defecto —true—, quien pide / recibe una redirección a /es/, el idioma principal, de modo que la portada nunca queda muerta. Si lo pones en false, la raíz responde con un 404 y te toca a ti resolverla, típicamente con un middleware que negocie el idioma del visitante, como veremos en la cuarta lección.

routing: {
  prefixDefaultLocale: true,
  redirectToDefaultLocale: false,
},

La otra pieza fina es que un elemento de locales no tiene por qué ser una cadena. Puede ser un objeto con dos campos: path, el segmento que de verdad aparece en la URL, y codes, la lista de códigos de idioma que ese segmento representa a efectos de negociación por cabecera. Así separas lo que el usuario ve de lo que el navegador anuncia.

i18n: {
  defaultLocale: 'es',
  locales: [
    'en',
    { path: 'espanol', codes: ['es', 'es-ES', 'es-MX'] },
  ],
},

Aquí la URL usa /espanol/, más legible que un código seco, mientras que codes mantiene el vínculo con es, es-ES y es-MX para que la detección de preferencia del visitante siga funcionando. Esta dualidad pathcodes es la que las funciones getPathByLocale y getLocaleByPath traducen en un sentido y en otro, protagonistas de la lección siguiente.

Queda una tercera vía que conviene al menos nombrar: routing no solo admite un objeto de opciones, también acepta el valor 'manual'. Con él renuncias al enrutado i18n automático de Astro y asumes el control total desde un middleware, apoyándote en las utilidades que exporta astro:i18n. Es la puerta de escape para sitios con reglas de idioma que no encajan en prefixDefaultLocale ni redirectToDefaultLocale, y la abriremos en la lección de negociación por cabecera; por ahora basta saber que existe y que el modo declarativo de este capítulo cubre la inmensa mayoría de los casos reales.

flowchart TD
REQ[peticion a una URL] --> ROUTE[el enrutado i18n mira el prefijo]
ROUTE -->|con prefijo en o fr| LOC[locale de esa ruta]
ROUTE -->|sin prefijo y default en raiz| DEF[defaultLocale]
ROUTE -->|raiz vacia y prefijo en todos| RED[redirectToDefaultLocale decide]
LOC --> PAGE[renderiza la pagina del idioma]
DEF --> PAGE
style REQ fill:#89b4fa,color:#11111b
style ROUTE fill:#f9e2af,color:#11111b
style PAGE fill:#a6e3a1,color:#11111b
ℹ️
i18n activa el reconocimiento, no crea páginas

La clave i18n no genera rutas: reconoce idiomas en las direcciones y habilita astro:i18n. Las carpetas por idioma dentro de src/pages las creas tú. Si declaras locales: ['es', 'en'] pero no existe ninguna carpeta en, no habrá páginas en inglés; solo estará el andamiaje listo para cuando las escribas. Separar el enrutado de la traducción es justo lo que te deja elegir después cómo organizas el contenido.

⚠️
Cambiar prefixDefaultLocale mueve todas tus carpetas

Pasar de false a true no es tocar un interruptor: es reorganizar src/pages. Con false, el idioma por defecto está en la raíz; con true, tiene que mudarse a su propia carpeta —src/pages/es/— y la raíz queda vacía. Si cambias el valor sin mover los ficheros, obtendrás 404 en masa. Decide la disposición al principio del proyecto, cuando mover cuatro carpetas cuesta poco, y no a mitad de camino.

📝
El idioma por defecto es una convención, no una jerarquía

Que un idioma sea el defaultLocale no lo hace superior ni más importante: solo lo designa como el caso de referencia, el que se sirve cuando la URL no especifica otro. Puedes cambiar cuál es el principal editando una línea, y con prefixDefaultLocale: false decidir además que ese papel se traduzca en vivir sin prefijo. La elección suele seguir a tu audiencia mayoritaria o al idioma en que nació el proyecto, pero es reversible y no ata al resto de la configuración: es una convención de referencia, nunca una jerarquía de valor entre lenguas.

El enrutado i18n separa la identidad de una página de su dirección

La idea profunda que esconde esta configuración es que una página deja de identificarse con una sola URL. Hasta ahora, en el enrutado por ficheros, un archivo era una dirección: src/pages/blog.astro era /blog, y punto. La internacionalización rompe esa identidad de uno a uno e introduce una distinción que la ingeniería de sistemas conoce bien: la diferencia entre la identidad de un recurso y sus representaciones. La página del blog es ahora una sola entidad conceptual —un contenido, un propósito— que se manifiesta en varias representaciones concretas, una por idioma, cada una con su dirección. Y el trabajo de i18n es precisamente gobernar cómo se derivan esas direcciones a partir de dos coordenadas ortogonales: qué contenido y en qué idioma. prefixDefaultLocale es la política que decide si esas coordenadas se ven siempre en la URL o solo cuando difieren del caso por defecto, exactamente como un sistema de codificación decide si el caso más frecuente se marca o se deja implícito para ahorrar. Por eso la elección no es cosmética: al escoger si el idioma principal lleva prefijo estás escogiendo qué consideras el caso base de tu sitio, el estado que no necesita anunciarse. Y al declararlo una vez en la configuración, en lugar de repartir esa decisión por cientos de enlaces, consigues que toda la lógica de direcciones se derive de una fuente única. Cambiar la política de idiomas de tu sitio entero pasa a ser editar cuatro líneas, no reescribir el árbol de rutas. Esa es la marca de una buena abstracción: convierte una decisión que estaba dispersa e implícita en un parámetro explícito y central.

⚔️ Levanta el esqueleto multilingüe de un sitio
  1. Declara i18n con locales: ['es', 'en', 'fr'] y defaultLocale: 'es', y crea src/pages/index.astro más src/pages/en/index.astro.
  2. Con prefixDefaultLocale: false, comprueba que el español sale en / y el inglés en /en/.
  3. Cambia a prefixDefaultLocale: true, mueve el español a src/pages/es/ y observa cómo / redirige a /es/.
  4. Pon redirectToDefaultLocale: false y verifica que ahora / responde con un 404, dejando el hueco que un middleware deberá resolver.