currentLocale, preferredLocale y negociación por header
Las propiedades que Astro expone sobre el idioma en cada petición y cómo usarlas para servir a cada visitante en su lengua. Astro.currentLocale derivado de la URL, Astro.preferredLocale y preferredLocaleList negociados desde la cabecera Accept-Language, la diferencia crucial entre estático y bajo demanda, y un middleware que redirige la raíz al idioma preferido, con la alternativa del routing manual y el middleware de astro:i18n.
currentLocale: el idioma de la ruta
La primera propiedad responde a la pregunta más básica: ¿en qué idioma está la página que se está renderizando? Astro.currentLocale lo calcula a partir de la URL, confrontando su prefijo con tu lista de locales. En /en/blog/ vale 'en'; en /blog/, si el español es el default sin prefijo, vale 'es'. Es información derivada de la dirección, así que existe tanto en páginas estáticas como bajo demanda: siempre hay una URL de la que deducirlo.
---const idioma = Astro.currentLocale; // 'en' en /en/..., 'es' en la raiz---<html lang={idioma}>El detalle importante es cuándo vale undefined: cuando la URL no casa con ningún locale conocido. Una ruta que vive fuera del esquema i18n —un /admin/ sin prefijo de idioma en un sitio donde el default lo lleva— no tiene idioma que Astro pueda inferir, y currentLocale queda indefinido. Por eso, al usarlo para algo crítico como el atributo lang del documento, conviene dar un valor de reserva. Frente a los param crudos, currentLocale tiene la ventaja de respetar los locales con path y codes personalizados: donde el segmento es espanol pero el código es es, es es lo que devuelve.
preferredLocale: escuchar la cabecera del navegador
La segunda pregunta es más interesante: ¿qué idioma querría el visitante? Eso no está en la URL, está en la cabecera Accept-Language que el navegador envía en cada petición, donde el usuario ha declarado sus idiomas por orden de preferencia. Astro.preferredLocale toma esa cabecera, la cruza con tus locales y devuelve el mejor emparejamiento: el idioma que tú soportas y que el visitante prefiere más.
---const quiere = Astro.preferredLocale; // 'fr' si el navegador lo pide y tu lo soportasconst todos = Astro.preferredLocaleList; // ['fr', 'en'] interseccion ordenada---preferredLocaleList da el cuadro completo: todos los idiomas que el visitante acepta y tú soportas, en su orden de preferencia. Si el navegador pide francés, luego inglés, luego alemán, y tú soportas inglés y francés, la lista es ['fr', 'en'] —el alemán cae porque no lo tienes—. Con esa lista puedes construir estrategias más ricas que un solo idioma: ofrecer el preferido pero sugerir el segundo, o elegir el primero que tenga la página concreta que se pide.
La cabecera Accept-Language no es una simple lista: cada idioma puede venir con un factor de calidad —un peso entre cero y uno— que ordena las preferencias del visitante. Astro lee ese orden y lo cruza con tus locales, así que no tienes que analizar la cabecera a mano: preferredLocale ya te entrega el ganador y preferredLocaleList, la clasificación completa con los idiomas que no soportas ya descartados. Delegar ese análisis es esquivar una de las partes más tediosas y propensas a error de toda la internacionalización.
Aquí llega la advertencia que ahorra horas de desconcierto: la negociación por cabecera solo existe bajo demanda. Una página estática se horneó en el build, cuando no había ningún visitante ni ninguna cabecera; por eso, en una ruta prerenderizada, preferredLocale y preferredLocaleList son undefined. Solo cobran valor en rutas servidas por un adaptador SSR, donde existe una petición real con su Accept-Language. La regla es tajante: currentLocale va con la URL y funciona siempre; preferredLocale va con la petición y solo vive donde hay una.
Un middleware que redirige la raíz al idioma preferido
Recuerda el hueco que dejamos en la primera lección: con prefixDefaultLocale: true y redirectToDefaultLocale: false, la raíz / responde con un 404. El momento de cerrarlo es ahora. Un middleware que corra en cada petición puede interceptar la raíz, leer el idioma preferido del visitante y redirigir a su versión, de modo que quien llega a / acaba en /fr/ o en /en/ según lo que su navegador pidió.
// src/middleware.tsimport { defineMiddleware } from 'astro:middleware';export const onRequest = defineMiddleware((context, next) => { if (context.url.pathname === '/') { const idioma = context.preferredLocale ?? 'es'; return context.redirect(`/${idioma}/`); } return next();});El context del middleware trae las mismas propiedades de idioma que Astro: context.currentLocale, context.preferredLocale, context.preferredLocaleList. Aquí leemos el preferido, caemos al español si no hay ninguno —el caso del rastreador sin cabecera— y redirigimos solo la raíz, dejando pasar todo lo demás con next. Es el patrón de portero de la lección de middleware aplicado al idioma: una decisión transversal, escrita una vez, que cubre cada entrada al sitio.
Astro ofrece además una alternativa integrada para quien no quiere escribir esta lógica a mano: el routing: 'manual'. Al fijarlo, desactivas el enrutado i18n automático y tomas el control con el middleware que exporta astro:i18n, una función que genera un onRequest con el comportamiento estándar —prefijos, redirección al default— configurable por parámetros, que puedes encadenar con sequence junto a tu propia lógica.
// src/middleware.tsimport { sequence } from 'astro:middleware';import { middleware } from 'astro:i18n';export const onRequest = sequence( middleware({ prefixDefaultLocale: true, redirectToDefaultLocale: false, }),);Guardar la elección para no repetir la negociación
Adivinar el idioma está bien la primera vez; insistir en adivinarlo en cada visita es una impertinencia. La solución es recordar la decisión: cuando el visitante aterriza en un idioma —porque lo negociaste o porque lo eligió a mano en el selector—, guarda ese idioma en una cookie y, en adelante, respétalo por encima de la cabecera. La cookie convierte una preferencia efímera del navegador en una elección persistente de la persona.
// src/middleware.tsimport { defineMiddleware } from 'astro:middleware';export const onRequest = defineMiddleware((context, next) => { if (context.url.pathname === '/') { const guardado = context.cookies.get('idioma')?.value; const idioma = guardado ?? context.preferredLocale ?? 'es'; return context.redirect(`/${idioma}/`); } return next();});El orden de precedencia lo dice todo sobre tus prioridades: primero la cookie —lo que el usuario decidió—, luego la cabecera —lo que su navegador sugiere— y por último el defaultLocale, la red de seguridad para quien no trae ninguna de las dos. Escribir esa cookie desde el propio selector de idioma cierra el círculo: cada vez que alguien cambia de lengua queda anotado, y la próxima vez que pise la raíz irá directo a donde quiso estar, sin negociación que valga.