Respuesta rápida
Si tus traducciones en Next.js no funcionan, revisa esto en orden: (1) La clave de traducción existe en el archivo JSON con el anidamiento correcto, (2) El archivo JSON es válido (sin comas finales), (3) El locale se detecta correctamente en el middleware, (4) El Provider/contexto envuelve tus componentes, (5) Usas el hook correcto (Server vs Client Component). El 90% de los problemas son errores tipográficos en las claves o entradas faltantes en el JSON.
He depurado cientos de configuraciones i18n en proyectos React y Next.js. La buena noticia: los errores de traducción casi siempre son uno de unos 10 problemas predecibles. La mala noticia: pueden ser increíblemente frustrantes de diagnosticar cuando no sabes qué buscar.
Esta guía cubre todos los problemas de traducción que he encontrado en producción, organizados de más común a menos común. Me enfoco en Next.js 14/15 con App Router, pero la mayoría de estos aplican también para Pages Router.
Problema #1: Se muestra la clave en lugar de la traducción
Síntoma: Ves common.buttons.submit en pantalla en lugar de "Enviar"
Este es el problema #1, y generalmente significa una de estas cosas:
La clave no existe en tu JSON
JSON1// ❌ Tu código usa: t('common.buttons.submit') 2// Pero tu en.json tiene: 3{ 4 "common": { 5 "button": { // Nota: "button" no "buttons" 6 "submit": "Submit" 7 } 8 } 9}
Solución: Revisa la estructura de tu JSON cuidadosamente. Usa un IDE con vista previa de rutas JSON o una herramienta como IntlPull que valida las claves.
Estás usando el namespace incorrecto
TSX1// ❌ Incorrecto: busca en el namespace por defecto 2const t = useTranslations(); 3t('checkout.title'); 4 5// ✅ Correcto: especifica el namespace 6const t = useTranslations('checkout'); 7t('title');
El archivo JSON no se está cargando
Revisa tu configuración de i18n:
TypeScript1// i18n/request.ts para next-intl 2import { getRequestConfig } from 'next-intl/server'; 3 4export default getRequestConfig(async ({ locale }) => ({ 5 messages: (await import(`../messages/${locale}.json`)).default 6}));
Verifica que la ruta del archivo sea correcta. Un error común es poner los mensajes en /public/locales cuando tu configuración espera /messages.
Problema #2: Sintaxis JSON inválida
Síntoma: La app crashea con "Unexpected token" o las traducciones fallan silenciosamente
JSON es estricto. Esto romperá todo:
JSON1// ❌ Coma final (lo más común) 2{ 3 "welcome": "Hola", 4 "goodbye": "Adiós", // <-- Esta coma lo rompe 5} 6 7// ❌ Comillas simples 8{ 9 'welcome': 'Hola' // Debe usar comillas dobles 10} 11 12// ❌ Comillas sin escapar en los valores 13{ 14 "message": "Haz clic "aquí" para continuar" // Necesita escapar 15} 16 17// ✅ Correcto 18{ 19 "welcome": "Hola", 20 "goodbye": "Adiós", 21 "message": "Haz clic \"aquí\" para continuar" 22}
Solución: Usa un validador de JSON. VS Code resalta errores de sintaxis. Ejecuta cat en.json | python -m json.tool para validar desde CLI.
Problema #3: Traducción faltante para un locale específico
Síntoma: El inglés funciona, pero español/alemán/etc. muestra las claves
Has agregado la clave a en.json pero olvidaste los otros locales:
/messages
en.json ✅ Tiene "checkout.newFeature": "Try our new feature"
es.json ❌ Falta esta clave completamente
de.json ❌ Falta esta clave completamente
Solución: Usa una herramienta de gestión de traducciones como IntlPull que rastrea traducciones faltantes en todos los locales. O configura un fallback:
TypeScript1// Configuración next-intl con fallback 2export default getRequestConfig(async ({ locale }) => ({ 3 messages: { 4 ...(await import(`../messages/en.json`)).default, // Fallback 5 ...(await import(`../messages/${locale}.json`)).default 6 } 7}));
Problema #4: El middleware no detecta el locale
Síntoma: Siempre muestra el idioma por defecto, el locale en la URL se ignora
TypeScript1// ❌ El middleware no se ejecuta en tus rutas 2export const config = { 3 matcher: ['/api/:path*'] // ¡Solo hace match con rutas de API! 4}; 5 6// ✅ Matcher correcto para i18n 7export const config = { 8 matcher: ['/((?!api|_next|.*\\..*).*)'] 9};
También verifica la ubicación de tu archivo de middleware - debe estar en middleware.ts en la raíz del proyecto, no dentro de /app o /src/app.
Revisa la lógica de detección del locale
TypeScript1// middleware.ts 2import createMiddleware from 'next-intl/middleware'; 3 4export default createMiddleware({ 5 locales: ['en', 'es', 'de', 'fr'], 6 defaultLocale: 'en', 7 localePrefix: 'always' // o 'as-needed' 8});
Tip de depuración: Agrega console.log al middleware para ver qué locale se está detectando:
TypeScript1export default function middleware(request: NextRequest) { 2 console.log('Locale detectado:', request.nextUrl.pathname); 3 // ... resto del middleware 4}
Problema #5: Errores de desajuste de hidratación
Síntoma: La consola muestra "Text content does not match server-rendered HTML"
Esto sucede cuando el servidor y el cliente renderizan traducciones diferentes:
Causa 1: Usar un hook de cliente en un Server Component
TSX1// ❌ Server Component usando hook de cliente 2// app/[locale]/page.tsx (Server Component por defecto) 3import { useTranslations } from 'next-intl'; // Esto está bien en realidad 4 5export default function Page() { 6 const t = useTranslations('home'); 7 return <h1>{t('title')}</h1>; // ¡Funciona en next-intl! 8}
Espera, esto realmente funciona en next-intl porque detecta el contexto. Pero con react-i18next:
TSX1// ❌ Con react-i18next en Server Component 2'use server'; 3import { useTranslation } from 'react-i18next'; // No funcionará 4 5// ✅ Usa función del lado del servidor 6import { getTranslations } from 'next-intl/server'; 7 8export default async function Page() { 9 const t = await getTranslations('home'); 10 return <h1>{t('title')}</h1>; 11}
Causa 2: Formato de fecha/hora sin zona horaria
TSX1// ❌ El servidor puede estar en UTC, el cliente en zona horaria local 2{formatDate(new Date())} 3 4// ✅ Siempre especifica la zona horaria 5import { format } from 'date-fns-tz'; 6{format(new Date(), 'PPP', { timeZone: userTimezone })}
Problema #6: El Provider no envuelve los componentes
Síntoma: "Could not find IntlProvider" u otros errores de contexto similares
TSX1// ❌ Falta el provider en el layout 2// app/[locale]/layout.tsx 3export default function Layout({ children }) { 4 return <html><body>{children}</body></html>; 5} 6 7// ✅ Con provider 8import { NextIntlClientProvider } from 'next-intl'; 9import { getMessages } from 'next-intl/server'; 10 11export default async function Layout({ children, params: { locale } }) { 12 const messages = await getMessages(); 13 return ( 14 <html lang={locale}> 15 <body> 16 <NextIntlClientProvider messages={messages}> 17 {children} 18 </NextIntlClientProvider> 19 </body> 20 </html> 21 ); 22}
Problema #7: Las claves dinámicas no funcionan
Síntoma: t(status.${value}) devuelve la clave, no la traducción
TSX1// ❌ Algunas configuraciones no soportan claves dinámicas 2const key = `status.${order.status}`; 3t(key); // Algunos bundlers no pueden optimizar esto 4 5// ✅ Usa un mapeo explícito 6const statusMessages = { 7 pending: t('status.pending'), 8 shipped: t('status.shipped'), 9 delivered: t('status.delivered') 10}; 11return statusMessages[order.status]; 12 13// ✅ O usa t.raw() para claves dinámicas en next-intl 14t(`status.${order.status}`); // Esto realmente funciona en next-intl
Problema #8: La pluralización no funciona
Síntoma: Muestra "{count, plural, one {# item} other {# items}}" literalmente
Estás usando formato ICU pero la librería no lo está parseando:
JSON1// Tu JSON 2{ 3 "items": "{count, plural, one {# artículo} other {# artículos}}" 4}
TSX1// ❌ No pasas la variable 2t('items'); 3 4// ✅ Pasa la variable count 5t('items', { count: 5 }); // "5 artículos"
Verifica que tu librería soporte ICU
- next-intl: Soporte completo de ICU ✅
- react-i18next: Necesita el plugin
i18next-icu - next-translate: Solo pluralización básica
Problema #9: Problemas de entorno/build
Las claves funcionan en dev, fallan en producción
TypeScript1// ❌ Los imports dinámicos pueden fallar en tiempo de build 2const messages = await import(`@/messages/${locale}.json`); 3 4// ✅ Asegúrate de que todos los locales sean conocidos estáticamente 5import en from '@/messages/en.json'; 6import es from '@/messages/es.json'; 7 8const messages = { en, es }; 9export const getMessages = (locale: string) => messages[locale];
"Module not found" después de agregar un nuevo locale
Después de agregar un nuevo archivo de locale, reinicia tu servidor de desarrollo. Next.js cachea la resolución de módulos.
Terminalrm -rf .next && npm run dev
Problema #10: Los enlaces/navegación pierden el locale
Síntoma: Al hacer clic en enlaces internos se resetea al idioma por defecto
TSX1// ❌ El Link normal pierde el locale 2import Link from 'next/link'; 3<Link href="/about">Acerca de</Link> 4 5// ✅ Usa la navegación de next-intl 6import { Link } from '@/i18n/navigation'; // Tu navegación configurada 7<Link href="/about">Acerca de</Link> // Preserva el locale automáticamente 8 9// ✅ O incluye el locale manualmente 10import { useLocale } from 'next-intl'; 11const locale = useLocale(); 12<Link href={`/${locale}/about`}>Acerca de</Link>
Lista de verificación para depuración
Cuando las traducciones fallen, pasa por esta lista:
| Verificación | Comando/Acción |
|---|---|
| Sintaxis JSON válida | `cat messages/en.json |
| La clave existe | Busca la clave exacta en el archivo JSON |
| El archivo del locale existe | ls messages/ |
| El middleware se ejecuta | Agrega console.log, revisa la salida del servidor |
| El Provider está en su lugar | Revisa layout.tsx buscando IntlProvider |
| Import correcto | Server: getTranslations, Client: useTranslations |
| Caché limpio | rm -rf .next && npm run dev |
Mensajes de error comunes decodificados
| Error | Significado | Solución |
|---|---|---|
Missing message: "key" | La clave no está en el JSON | Agrega la clave a todos los archivos de locale |
Unable to find next-intl locale | El middleware no está configurando el locale | Revisa la ubicación y configuración de middleware.ts |
Hydration failed | Desajuste servidor/cliente | Usa el hook correcto para el tipo de componente |
Cannot read property 't' of undefined | Falta el provider | Envuelve con IntlClientProvider |
ENOENT: no such file | Ruta del archivo incorrecta | Revisa la ruta de la carpeta messages en la configuración |
Prevención: Evita estos problemas por completo
1. Usa TypeScript para claves type-safe
TypeScript1// Genera tipos desde tu JSON 2// Con next-intl, crea un archivo de tipos: 3type Messages = typeof import('./messages/en.json'); 4declare global { 5 interface IntlMessages extends Messages {} 6}
Ahora TypeScript mostrará errores en claves inválidas.
2. Usa un sistema de gestión de traducciones
Herramientas como IntlPull automáticamente:
- Validan la sintaxis JSON
- Rastrean traducciones faltantes por locale
- Previenen errores tipográficos con autocompletado
- Sincronizan claves en todos los locales
3. Agrega verificaciones en CI
YAML1# .github/workflows/i18n-check.yml 2- name: Validar archivos JSON 3 run: | 4 for f in messages/*.json; do 5 python -m json.tool "$f" > /dev/null || exit 1 6 done 7 8- name: Verificar claves faltantes 9 run: npx intlpull check --config intlpull.config.json
4. Tests de integración para rutas críticas
TypeScript1// e2e/i18n.spec.ts 2test('la página de checkout renderiza en todos los locales', async ({ page }) => { 3 for (const locale of ['en', 'es', 'de']) { 4 await page.goto(`/${locale}/checkout`); 5 // No debería contener claves de traducción en crudo 6 await expect(page.locator('body')).not.toContainText('checkout.'); 7 } 8});
Cuándo usar gestión de traducciones
Si experimentas estos problemas repetidamente, considera un TMS:
| Escenario | Archivos JSON DIY | Gestión de traducciones |
|---|---|---|
| < 50 claves, 1-2 devs | ✅ Funciona bien | Excesivo |
| > 200 claves, múltiples devs | ❌ Conflictos de merge | ✅ Única fuente de verdad |
| > 3 idiomas | ❌ Difícil sincronizar | ✅ Detección de claves faltantes |
| Traductores externos | ❌ Pesadilla de handoff de JSON | ✅ Flujo de trabajo integrado |
IntlPull (plug descarado) está construido específicamente para esto. Se integra con tu flujo de trabajo de Git, detecta traducciones faltantes en CI, y soporta actualizaciones OTA para que no necesites redesplegar para corregir traducciones.
Preguntas frecuentes
¿Por qué mi función t() devuelve la clave en lugar de la traducción?
Tu clave de traducción no existe en el archivo JSON o namespace. Revisa errores tipográficos en la clave, verifica que la estructura JSON coincida con la ruta esperada en tu código, asegúrate de usar el namespace correcto, y valida que el archivo JSON se esté cargando. Este es el problema de i18n más común.
¿Por qué mis traducciones funcionan en inglés pero no en otros idiomas?
La clave de traducción falta en los otros archivos de locale. Cuando agregas una nueva clave a en.json, también debes agregarla a es.json, de.json, etc. Usa un TMS como IntlPull para detectar automáticamente traducciones faltantes, o configura locales de fallback en tu configuración.
¿Cómo soluciono los errores de desajuste de hidratación en Next.js i18n?
El servidor y el cliente están renderizando contenido diferente. Esto generalmente ocurre con formato de fecha/hora (el servidor está en UTC, el cliente en hora local), usando hooks de cliente en Server Components, o desajuste de locale entre servidor y cliente. Especifica zonas horarias explícitamente, usa los hooks correctos para el tipo de componente, y asegúrate de que el locale se pase correctamente.
¿Por qué mi middleware de Next.js no detecta el locale?
Tu patrón matcher del middleware no coincide con tus rutas. Asegúrate de que middleware.ts esté en la raíz del proyecto (no en /app), tu matcher incluya las rutas que necesitas, y el array de locales coincida con tus idiomas soportados. Agrega console.log al middleware para depurar qué se está detectando.
¿Cómo depuro errores de "Could not find IntlProvider"?
Tu componente no está envuelto con el provider de i18n. Verifica que NextIntlClientProvider (o el provider de tu librería) esté en tu app/[locale]/layout.tsx y envuelva todos los componentes hijos. El provider debe recibir los messages y el locale actual.
¿Por qué mi archivo JSON causa errores de "Unexpected token"?
Tu sintaxis JSON es inválida. Problemas comunes: comas finales, comillas simples en lugar de dobles, comillas sin escapar en los valores, o comas faltantes. Pasa tu JSON por un validador como python -m json.tool para encontrar la ubicación exacta del error.
¿Cómo hago que las traducciones funcionen en Server Components de Next.js?
Usa funciones de traducción del lado del servidor. En next-intl, usa getTranslations() de next-intl/server en Server Components. El hook regular useTranslations() funciona en next-intl gracias a la detección de contexto, pero para react-i18next necesitas funciones explícitas del lado del servidor.
¿Por qué mis traducciones funcionan en desarrollo pero fallan en producción?
Los imports dinámicos pueden fallar en tiempo de build. Asegúrate de que todos los archivos de locale existan antes del build, usa imports estáticos o imports dinámicos verificados, y limpia el caché .next antes de construir. Los builds de producción optimizan los imports de manera diferente que el modo dev.
¿Cómo evito que se pierda el locale al navegar?
Usa el componente Link de tu librería i18n en lugar del Link de Next.js. En next-intl, configura las exportaciones de navegación en i18n/navigation.ts e importa Link desde ahí. Alternativamente, incluye manualmente el locale en href: /${locale}/about.
¿Cómo pruebo que todas las traducciones existan?
Agrega verificaciones de CI que validen el JSON y detecten claves faltantes. Valida la sintaxis JSON con un linter, compara claves entre archivos de locale, y ejecuta tests de integración que verifiquen que las páginas no muestren claves de traducción en crudo. El CLI de IntlPull proporciona detección automatizada de traducciones faltantes.
Resumen
La mayoría de los problemas de i18n en Next.js caen en categorías predecibles:
- Errores tipográficos en claves → Usa tipos TypeScript y autocompletado
- JSON inválido → Valida en CI
- Datos de locale faltantes → Usa un TMS con verificación de sincronización
- Hook/contexto incorrecto → Sigue las reglas de Server vs Client Component
- Problemas de middleware → Revisa la ubicación del archivo y configuración del matcher
El ecosistema ha mejorado mucho. Con App Router y librerías modernas como next-intl, i18n es significativamente más confiable que con Pages Router. Pero sigue siendo código, y el código tiene bugs.
Construye defensivamente: claves type-safe, validación en CI, y buenas herramientas te ahorrarán horas de depuración.
¿Necesitas ayuda gestionando traducciones a escala? Comienza gratis con IntlPull — detección automática de traducciones faltantes, traducción con IA e integración perfecta con next-intl.
