i18n en Next.js
Guía estructural para poner una aplicación Next.js en varios idiomas: cómo debe ser el diseño de carpetas, qué falla cuando está mal y qué evitar.
Elija la biblioteca primero
Por defecto, use next-intl para cualquier proyecto nuevo con App Router. No necesita complemento de compilación, ni paso de macros, ni gestión de instancias por locale; los mensajes son JSON simple tipados a partir de su locale de origen.
| Biblioteca | Úsela cuando | Costo |
|---|---|---|
| next-intl (predeterminada) | Nuevo trabajo con App Router, cualquier tamaño de equipo | Ninguno: JSON simple, sin generación de código |
| @lingui/react | Quiere extracción en tiempo de compilación y un runtime pequeño, o una capa de i18n común para React/Vue/Astro | Complemento de macro SWC o Babel, versionado exacto |
| next-i18next | El equipo ya conoce i18next, o migra código de Pages Router | Instancia por solicitud, división getT/useT |
i18n integrado de Next.js | Solo Pages Router. Nunca App Router | No aplicable |
La configuración i18n: { locales, defaultLocale } de Next.js es solo para Pages Router. En App Router no hace nada: el enrutamiento por locale allí implica un segmento [locale] más un proxy. No permita que el agente "active" el i18n integrado para un proyecto de App Router.
Comparación completa, incluido el costo de bundle y las compensaciones por biblioteca: references/library-comparison.md.
Arquitectura canónica
Mantenga los diccionarios estáticos fuera de src/ para que el empaquetado y las rutas de importación sean obvias, y mantenga toda la maquinaria de locale en una carpeta.
messages/ # diccionarios estáticos puros — mantener fuera de /src
├── en.json # fuente de verdad; cada otro locale lo refleja
├── de.json
└── ja.json
src/
├── i18n/
│ ├── routing.ts # defineRouting: locales + defaultLocale
│ ├── request.ts # getRequestConfig: resolver locale, cargar mensajes
│ └── navigation.ts # createNavigation: Link/redirect consciente del locale
├── proxy.ts # detección de locale + redirección (Next 16; antes era middleware.ts)
└── app/
├── [locale]/ # envuelve toda la aplicación localizada
│ ├── layout.tsx # validar locale, <html lang>, generateStaticParams
│ └── page.tsx
└── not-found.tsx # 404 global vive FUERA de [locale]
Reglas que hacen que esta estructura funcione:
[locale]es un segmento dinámico raíz. Todo lo localizado vive debajo de él. Las rutas raíz (not-found.tsx,error.tsx,api/) permanecen fuera y no deben llamar a hooks de i18n de forma incondicional, o fallarán en tiempo de ejecución.- Un locale de origen.
messages/en.jsones el maestro. Cada otro archivo debe reflejar exactamente su estructura. - Todo el conocimiento de locale en
src/i18n/. Los componentes importanLinkdesde@/i18n/navigation, nunca desdenext/link. Un solo lugar para agregar un idioma.
Configuración (proyecto nuevo)
Ejecute en este orden. El orden importa: proxy.ts depende de routing.ts, y el layout depende de request.ts.
- Instale
next-intl, envuelvanext.config.tsconcreateNextIntlPlugin(). - Cree
src/i18n/routing.tscondefineRouting. - Cree
src/proxy.tsexportandocreateMiddleware(routing)más el matcher predeterminado. - Cree
src/i18n/request.tscongetRequestConfigresolviendo el locale. - Mueva
app/**bajoapp/[locale]/**; agreguegenerateStaticParamsy valide el parámetro. - Cree
src/i18n/navigation.tsconcreateNavigation(routing). - Cree
messages/en.jsony reemplace las cadenas fijas conuseTranslations. - Establezca
<html lang>y agregue metadatos localizados + hreflang.
Contenido exacto de los archivos para cada paso: references/next-intl-setup.md.
Problemas comunes
Lea esta sección antes de escribir cualquier código de i18n. Estos son los fallos que cuestan horas.
setRequestLocalees legado. En Next.js 16.3+, lea el locale connext/root-paramsdentro degetRequestConfig. La mayoría de las guías, tutoriales y datos de entrenamiento de LLM aún enseñansetRequestLocale(params.locale). Todavía funciona y está documentado como retrocompatible, pero es la API antigua. Úselo solo en Next < 16.3.generateStaticParamssigue siendo necesario de cualquier manera.paramses una Promesa en Next 15+.const {locale} = await params. Trateparams.localecomo un error de tipo, no de ejecución.- Valide el locale o los valores desconocidos se renderizan. Use
hasLocale(routing.locales, locale)ynotFound(). Sin esto,/xx/aboutrenderiza su página en inglés en una URL falsa. proxy.ts, nomiddleware.ts, en Next 16.middleware.tsestá obsoleto. El proxy se ejecuta solo en Node.js: el runtime Edge no es compatible. Lingui y next-i18next promocionan proxies compatibles con Edge; si necesita Edge, tiene una compensación real, no una victoria gratuita.- Nunca concatene cadenas traducidas.
"Tienes " + count + " elementos"es agramatical en japonés y árabe. Use ICU:"{count, plural, =1 {Un elemento} other {# elementos}}". - Nunca interpole con
+o plantillas literales dentro de un mensaje. Use marcadores de posición con nombre —{name}— para que el orden de las palabras pueda cambiar. - Las fechas y números causan discrepancias de hidratación.
new Date()en el servidor y en el navegador difieren por zona horaria y reloj. EstablezcatimeZoneengetRequestConfig, o renderice solo en el cliente para tiempos relativos. Síntoma: "El contenido del texto no coincide con el HTML renderizado por el servidor". - El formato depende del locale.
toLocaleString()sin un locale explícito formatea según el locale del runtime, no el locale solicitado. UseuseFormatter()/getFormatter(). getRequestConfigse ejecuta por solicitud y es almacenado en caché por React. No coloque estado variable por solicitud (carrito, preferencias de usuario) allí. Solo configuración variable por locale.- Las Server Actions y Route Handlers no tienen parámetros raíz.
await locale()funciona solo en Server Components. Pase el locale explícitamente como argumento en acciones y handlers.
Modernización de una aplicación existente
Trate esto como una migración con impacto en las URL, no como un cambio de configuración. Cada URL existente cambia de forma cuando agrega un prefijo [locale].
Planifique en este orden:
- Inventario de cadenas — ejecute
scripts/find-untranslated.shpara encontrar texto JSX fijo. - Elija la estrategia de enrutamiento antes de codificar. Prefijo de locale siempre (
/en/about,/de/about) es el predeterminado; es inequívoco y limpio para hreflang.localePrefix: 'as-needed'sirve el locale predeterminado en la ruta simple — mejores URL, señal SEO más débil. Decida ahora; cambiar después es una migración completa. - Configure el enrutamiento antes de tocar cualquier componente. Haga que
proxy.ts+[locale]funcionen solo con mensajes en inglés. - Mueva las rutas bajo
[locale]/— esto es un grangit mvmecánico. Hágalo en un solo commit para quegit log --followsiga siendo útil. - Reemplace cadenas por espacio de nombres, no por archivo. Traduzca un espacio de nombres de principio a fin para que la forma de la clave esté probada antes de que se multiplique.
- Agregue redirecciones para cada URL que cambió.
/about→/en/about(o 307 al locale detectado). Sin esto, pierde todos los rankings existentes. No negociable. - Agregue hreflang + metadatos localizados solo después de que cada locale tenga la ruta, o emitirá alternativas que apuntan a 404.
Secuencia completa de migración con notas sobre redirecciones y reversión: references/migration.md.
Qué evitar
- No ponga
messages/dentro desrc/. Los diccionarios solo del servidor en el árbol fuente desdibujan lo que se envía al cliente. - No importe
Linkdesdenext/linken rutas localizadas. Elimina el prefijo de locale y enlaza silenciosamente al idioma incorrecto. - No escriba manualmente prefijos de locale en los hrefs (
href={/${locale}/about}). UseLinkdesde@/i18n/navigation, que maneja enrutamiento con prefijo, según necesidad y por dominio. - No use un espacio de nombres de claves plano. Agrupe por funcionalidad/página. Las claves planas chocan y no se pueden buscar.
- No traduzca automáticamente en tiempo de compilación como fuente de verdad. La salida de la máquina es un borrador; comprometerla sin revisión hace que los errores sean permanentes y no rastreables.
- No agregue un locale a
routing.localesy lo dé por terminado. Sin unmessages/xx.jsontraducido, cada página renderiza claves o retrocede silenciosamente. - No mezcle estrategias de enrutamiento.
as-neededmás unhreflangque apunta a URLs con prefijo es un error SEO autoinfligido.
Referencias
Léalas bajo demanda, no por adelantado:
references/library-comparison.md— elegir entre next-intl, Lingui, next-i18next; costo de bundle, desencadenantes de migración, forma de configuración por biblioteca.references/next-intl-setup.md— cada archivo en la configuración canónica, listo para copiar y pegar.references/messages-and-keys.md— estructura de espacios de nombres, nomenclatura de claves, sintaxis ICU, formato de fecha/número/lista, mensajes tipados.references/seo.md— hreflang,<html lang>,generateMetadatalocalizado, canónico, alternativas de sitemap, reglas SEO para selector de locale.references/migration.md— modernización de una aplicación existente, redirecciones, reversión.
Scripts
scripts/find-untranslated.sh— marca cadenas probablemente fijas dirigidas al usuario en un proyecto para que pueda dimensionar el trabajo antes de comenzar. Ejecútelo como paso 1 de una modernización.scripts/run-trigger-eval.sh— mide si ladescriptionde esta habilidad se activa con los prompts correctos. Úselo al cambiar la descripción.
Prueba de la descripción
La description es lo único que el agente ve antes de decidir cargar esta habilidad. Si no se activa, nada de lo siguiente importa.
bash scripts/run-trigger-eval.sh --dry-run # mostrar el plan, sin llamadas a la API
bash scripts/run-trigger-eval.sh --split train # 12 consultas x 3 ejecuciones
bash scripts/run-trigger-eval.sh --split validation --out report.json
Conjunto de consultas: evals/trigger-queries.json — 12 deben activarse, 8 no deben activarse, divididas 60/40 en un conjunto fijo de entrenamiento y validación.
Al editar la descripción:
- Corrija fallos usando solo el conjunto de entrenamiento. Toque el conjunto de validación una vez al final para verificar que el cambio se generaliza: si el entrenamiento mejora y la validación empeora, se sobreajustó.
- Los fallos de "debe activarse" significan que la descripción es demasiado estrecha: agregue el concepto que representa la consulta, nunca las palabras clave de la consulta.
- Los fallos de "no debe activarse" significan que es demasiado amplia: diga lo que la habilidad no cubre.
- Vuelva a ejecutar antes de editar. La activación es no determinista, y una ejecución no prueba nada.
No agregue palabras clave de una consulta fallida: así es como las descripciones terminan pareciendo una lista de palabras clave y dejan de coincidir con prompts reales.