Pídele hoy a Cursor o a Claude Code que añada i18n a una app de Next.js y muy probablemente obtendrás middleware.ts, setRequestLocale en cada layout, y useTranslations llamado desde una página async — una configuración que era correcta para Next.js 15 y que va una generación por detrás en Next.js 16. globalize.now es infraestructura de localización impulsada por IA, así que esta es la capa que vigilamos: el cableado de enrutamiento cambió, el catálogo que sirve no. A continuación, lo que se movió, cómo saber de qué era viene tu código generado, y los tres arreglos que llevan una tarde en lugar de una reescritura.
Nada de esto es un breaking change. Precisamente por eso es fácil pasarlo por alto.
¿Qué cambió en Next.js 16 para i18n?
El archivo que negocia tu locale cambió de nombre. Next.js documenta la convención del archivo middleware como obsoleta y renombrada a proxy, a partir de la v16.0.0, y su propia explicación es cuestión de vocabulario: «middleware» se seguía interpretando como middleware de Express, así que la función se renombró para describir el límite de red que en realidad es.
Dos notas de comportamiento acompañan al cambio de nombre en la v16. Proxy usa por defecto el runtime de Node.js, y la opción de configuración por archivo runtime ya no está disponible ahí — la defines y Next.js lanza un error. Proxy tampoco es compatible con la exportación estática, lo cual importa si tu negociación de locale es la única razón por la que no estás exportando.
En concreto para el enrutamiento de locale, la guía de configuración de next-intl ahora muestra el archivo como src/proxy.ts y señala claramente que se llamaba middleware.ts hasta Next.js 16. El import dentro de él no ha cambiado:
// src/proxy.ts
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';
export default createMiddleware(routing);
export const config = {
matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
};
Fíjate en la asimetría, porque confunde a la gente: el archivo ahora es proxy.ts, mientras que la ruta de importación sigue siendo next-intl/middleware. Renombrar el import es una sobrecorrección habitual.
¿Por qué el código i18n generado por IA sale una versión por detrás?
Porque un coding agent predice el patrón más representado, y el patrón más representado es el que ha tenido años para acumularse. Cada entrada de blog, respuesta de Stack Overflow y ejemplo de GitHub sobre el enrutamiento de locale en App Router escrito antes de finales de 2026 dice middleware.ts. Un modelo que sopesa ese corpus frente a unas pocas semanas de documentación de la v16 recurrirá a la forma antigua, con confianza, sin ningún aviso de que hubo un cambio de nombre.
Es el mismo mecanismo detrás de un problema del que ya hemos hablado, donde Cursor sigue añadiendo cadenas codificadas después de configurar i18n: el agente reproduce el codebase estadísticamente normal, no el tuyo. Es también la razón por la que los archivos de instrucciones solo lo arreglan a medias para GitHub Copilot: las reglas que lee la superficie de chat no las lee necesariamente el autocompletado en línea.
La consecuencia práctica es acotada pero real. Tu configuración generada funciona, así que nada falla de forma ruidosa. Luego te topas con un bug, lo buscas, y cada respuesta actual describe archivos que no tienes.
¿Cómo sé de qué era viene mi configuración generada?
Cuatro greps. Ejecútalos desde la raíz del proyecto y lo sabrás en aproximadamente un minuto.
# 1. Pre-16 locale negotiation file
ls middleware.ts src/middleware.ts 2>/dev/null
# 2. Legacy static-rendering API
grep -rn "setRequestLocale" app src 2>/dev/null
# 3. Hooks called inside async components
grep -rn -B3 "useTranslations" app | grep -n "async function"
# 4. Which next-intl era the config reads from
grep -rn "root-params\|await params" src/i18n/request.ts 2>/dev/null
Coincidencias en 1 y 2 significan andamiaje anterior a la v16. Una coincidencia en 3 es un error de runtime real esperando a la primera request que renderice ese componente. Ninguna coincidencia en 4 significa que tu configuración de request lee el locale a la manera antigua.
¿Tengo que renombrar middleware.ts a proxy.ts?
No con urgencia, y no deberías hacerlo a mano. Next.js incluye un codemod que renombra tanto el archivo como la función exportada:
npx @next/codemod@canary middleware-to-proxy .
El cambio de nombre es una deprecación, no una eliminación, así que un middleware.ts existente sigue funcionando. La razón para ejecutarlo de todos modos es el coste de mantenimiento: en cuanto tus nombres de archivo coinciden con la documentación actual, cada resultado de búsqueda futuro se aplica a tu repo. Déjalo desactualizado y pagas un pequeño impuesto en cada sesión de depuración, para siempre.
¿Está setRequestLocale obsoleto en next-intl?
Está marcado como legacy, que es una afirmación más suave que obsoleto y vale la pena leer con precisión. La documentación de next-intl describe setRequestLocale como una API que existió hasta que se introdujo next/root-params, dice que sigue siendo compatible por retrocompatibilidad, y recomienda next/root-params en su lugar.
La forma más reciente lee el locale detectado dentro de tu configuración de request en lugar de pasarlo manualmente por cada layout y página:
// src/i18n/request.ts
import * as rootParams from 'next/root-params';
import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';
import {hasLocale} from 'next-intl';
import {routing} from './routing';
export default getRequestConfig(async ({locale}) => {
if (!locale) {
const paramValue = await rootParams.locale();
if (hasLocale(routing.locales, paramValue)) {
locale = paramValue;
} else {
notFound();
}
}
return {locale};
});
next/root-params está disponible por defecto en Next.js 16.3 y posteriores; en versiones anteriores hay que habilitarlo mediante experimental.rootParams. Sigue la configuración de esta manera y el renderizado estático viene incluido, siempre que sigas exportando generateStaticParams para el segmento [locale].
El enfoque antiguo exigía llamar a setRequestLocale en cada página y layout que quisieras renderizar de forma estática, antes de cualquier otra llamada de next-intl, porque Next.js renderiza layouts y páginas de forma independiente. Es una regla que un agente de IA olvida en el quinto archivo que escribe. Eliminar el requisito elimina toda esa clase de bug.
¿Por qué falla useTranslations en mi Server Component asíncrono?
Porque los hooks no se pueden llamar desde componentes async, y useTranslations es un hook. Es una restricción de React Server Components, no una peculiaridad de next-intl, y la respuesta de next-intl es un conjunto paralelo de funciones esperables (awaitable):
// Async component — await the server API
import {getTranslations} from 'next-intl/server';
export default async function ProfilePage() {
const user = await fetchUser();
const t = await getTranslations('ProfilePage');
return <h1>{t('title', {username: user.name})}</h1>;
}
// Non-async component — the hook is correct here
import {useTranslations} from 'next-intl';
export default function UserDetails({user}) {
const t = useTranslations('UserProfile');
return <h2>{t('title')}</h2>;
}
getFormatter, getNow, getTimeZone, getMessages y getLocale siguen el mismo patrón. El segundo ejemplo merece detenerse: un componente no asíncrono sin funciones interactivas es un componente compartido, y next-intl resuelve la implementación correcta según se renderice en el servidor o en el cliente. Así que useTranslations en un Server Component no es un error — llamarlo desde uno async sí lo es.
¿Por qué me da el error de contexto de NextIntlClientProvider?
Las notas de resolución de problemas de next-intl dan dos causas, y quieren soluciones opuestas. O bien el componente realmente se está ejecutando en el cliente sin un provider por encima, en cuyo caso lo envuelves y le pasas los mensajes que necesita, o bien terminó en un grafo de módulos de cliente cuando esperabas renderizado en el servidor, en cuyo caso lo pasas a través de children desde un Server Component en lugar de importarlo dentro de uno.
El segundo caso es el que sufren las apps generadas por IA, porque los agentes aplican 'use client' con generosidad para que funcione la interactividad, y la directiva es contagiosa a lo largo del grafo de imports. El patrón preferido es traducir en el servidor y pasar las cadenas ya resueltas a través del límite:
import {useTranslations} from 'next-intl';
import Expandable from './Expandable'; // 'use client'
export default function FAQEntry() {
const t = useTranslations('FAQEntry');
return <Expandable title={t('title')}>{t('description')}</Expandable>;
}
Si algún componente realmente necesita mensajes en el cliente, puedes acotar un provider solo a ese subárbol en lugar de enviar todos los mensajes al navegador — messages={null} en el provider raíz no pasa ninguno en absoluto.
¿Qué es lo que nada de esto cambia?
Tus archivos de idioma. Cada arreglo anterior es cableado de enrutamiento y renderizado; el JSON que carga tu app queda intacto en todo esto. Y ahí está lo importante, porque el cableado es un trabajo de una tarde que cambia una vez al año, mientras que el catálogo es lo que se pudre cada semana a medida que sale nueva interfaz.
Esa es la división para la que construimos. next-intl y sus vecinos sirven traducciones en tiempo de ejecución — no competimos con ellos, y si todavía estás eligiendo entre ellos, next-intl vs react-i18next vs Lingui expone las ventajas e inconvenientes de cada uno. globalize.now se sitúa una capa por encima, produciendo las claves y los archivos de idioma que esos runtimes leen, por eso la integración con Next.js es indiferente a si tu negociación vive en middleware.ts o en proxy.ts.
La conversión es única y ocurre en la app: conectas el repositorio, globalize.now convierte el codebase una vez y abre una pull request con el catálogo. Después de eso, los push jobs traducen las nuevas unidades de catálogo a medida que aparecen — que es el modo de fallo descrito en por qué los archivos de traducción se desincronizan constantemente, y la razón por la que vale la pena cerrar esa brecha antes de tu tercer idioma, no después.
Si tu proyecto ya tiene una configuración de next-intl escrita a mano, hemos explicado exactamente qué tocamos y qué no. Si no la tiene, el recorrido con Cursor es el camino más corto — con la salvedad de que su archivo generado lleva el nombre de la convención antigua, así que ejecuta el codemod después.
Por dónde empezar
Ejecuta los cuatro greps. Si obtienes coincidencias en los dos primeros, ejecuta el codemod, mueve tu configuración de request a next/root-params, y estarás al día. Después mira los archivos de idioma, porque esa es la parte que seguirá desincronizándose el mes que viene. Si estás lanzando una app generada por IA y quieres que el catálogo esté gestionado en lugar de mantenido a mano, la página de vibe coders es la visión general y los precios están en su propia página.
globalize.now convierte el texto codificado de tu app en archivos de traducción listos para localizar y los mantiene actualizados en cada despliegue.
Prueba globalize.now gratis