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.
Un sitio multilingüe educado no obliga al visitante a elegir idioma: lo adivina. El navegador ya anuncia, en cada petición, qué lenguas entiende su dueño y en qué orden las prefiere; solo hay que escucharlo. Astro pone esa información al alcance de la mano con un puñado de propiedades —currentLocale, preferredLocale, preferredLocaleList— que responden a dos preguntas distintas: en qué idioma está esta página y en qué idioma querría leerla quien la pide. Cruzar ambas respuestas en un middleware es lo que convierte una raíz muda en una portada que recibe a cada quien en su lengua, y este es el capítulo donde el i18n deja de ser rutas y empieza a ser conversación.
- Leer el idioma de la ruta actual con
Astro.currentLocaley saber cuándo esundefined. - Negociar el idioma del visitante con
Astro.preferredLocaleyAstro.preferredLocaleList. - Entender por qué la negociación por cabecera solo existe bajo demanda, no en estático.
- Escribir un middleware que redirija la raíz al idioma preferido y conocer el
routingmanual.
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 soportas
const 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.
currentLocale
El idioma de la ruta, deducido de la URL. Existe en estatico y bajo demanda. Undefined si no casa.
preferredLocale
El idioma que el visitante prefiere, de Accept-Language cruzado con tus locales. Solo bajo demanda.
preferredLocaleList
Todos los idiomas aceptados que soportas, en orden. Para estrategias mas finas que uno solo.
Middleware
El lugar donde cruzar ambos: leer el preferido y redirigir la raiz al idioma del visitante.
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.ts
import { 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.ts
import { 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.ts
import { 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.
flowchart TD REQ[peticion entra al middleware] --> ROOT[la ruta es la raiz] ROOT -->|no| PASS[next deja pasar la ruta] ROOT -->|si| PREF[lee context preferredLocale] PREF --> HAS[hay idioma preferido] HAS -->|si| GO[redirige al idioma del visitante] HAS -->|no| DEF[redirige al defaultLocale] style REQ fill:#89b4fa,color:#11111b style PREF fill:#f9e2af,color:#11111b style GO fill:#a6e3a1,color:#11111b
Si construyes tu sitio como estático y esperas leer Astro.preferredLocale, siempre obtendrás undefined, porque no hubo petición cuando se horneó la página. La negociación por cabecera exige un servidor que atienda la petición en vivo: un adaptador SSR y, típicamente, la ruta marcada como bajo demanda. Intentar redirigir por idioma preferido desde una página prerenderizada es una contradicción de fondo, no un bug: no hay nadie a quien preguntar cuando la página se genera en el build.
La redirección por idioma preferido es cortesía, no imposición. Si un visitante navega deliberadamente a /en/ estando su navegador en francés, respeta su elección: no lo devuelvas a /fr/ en la siguiente página. Limita la negociación automática a la raíz o a la primera visita, y guarda su decisión —una cookie basta— para no pelearte con quien ya te dijo qué quiere. Un sitio que reescribe el idioma en cada clic es más molesto que uno que nunca lo adivina.
Detrás de la distinción entre currentLocale y preferredLocale hay una de las dualidades más fértiles del diseño de sistemas web: la diferencia entre el estado de un recurso y las preferencias de quien lo pide. currentLocale describe un hecho objetivo e inscrito en la dirección: esta URL es la versión francesa, y lo será para cualquiera que la abra, hoy y dentro de un año, la comparta quien la comparta. preferredLocale describe algo radicalmente distinto: un deseo subjetivo, efímero y privado de un visitante concreto, que viaja en una cabecera y desaparece con la petición. Confundir ambos planos es el origen de casi todos los sitios multilingües que se comportan mal. Cuando el idioma vive en la URL, es compartible: puedes enviar el enlace de la versión francesa a un amigo y le llegará en francés, porque el idioma es parte de la identidad del recurso, no de tu sesión. Cuando el idioma vive solo en una cookie o en la cabecera, se vuelve incompartible: el mismo enlace le llega a cada quien en un idioma distinto, y el recurso pierde una identidad estable. Por eso el buen diseño i18n hace que la cabecera solo sugiera y que la URL decida: la negociación se usa una vez, en la puerta, para adivinar a dónde llevar a quien no ha elegido, y a partir de ahí el idioma queda anclado en la dirección, donde es visible, enlazable e indexable. Es la misma sabiduría que separa una petición idempotente de una que muta estado, o una redirección permanente de una temporal: reconocer qué pertenece a la identidad duradera de un recurso y qué a la circunstancia pasajera de una petición. Quien interioriza esa frontera escribe sitios que se pueden compartir; quien la ignora, sitios que solo funcionan bien para quien los construyó.
- Muestra
Astro.currentLocaleen varias rutas y comprueba que sigue el prefijo de la URL, con reserva para cuando seaundefined. - Con un adaptador SSR, imprime
Astro.preferredLocaleyAstro.preferredLocaleListy cambia el idioma de tu navegador para verlos moverse. - Escribe un middleware que redirija
/al idioma preferido, con caída aldefaultLocalecuando no haya cabecera. - Guarda la elección del usuario en una cookie y evita redirigirlo de nuevo si ya navegó a un idioma distinto del preferido.