Si tu app de Next.js está traducida pero la pestaña del navegador, la página 404, los errores de formulario y la vista previa del enlace siguen en inglés, la causa casi siempre es la misma: esas cadenas se generan fuera de un componente, así que una llamada a useTranslations nunca las tocó. globalize.now es infraestructura de localización impulsada por IA, y esta es la capa que vemos a medio hacer cuando un agente de IA añade i18n a un proyecto con App Router: la página visible se convierte y el resto de la superficie no.
A continuación verás dónde vive cada una de esas cadenas y la llamada de next-intl que se encarga de ella. Todos los ejemplos siguen la documentación actual de next-intl, consultada el 28/10/2026.
¿Dónde se esconden las cadenas sin traducir en una app con App Router?
Se esconden en seis sitios que no son componentes de React que renderizan texto. Un coding agent que añade i18n recorriendo tus componentes convertirá la UI visible y pasará de largo por estos:
- Títulos y descripciones de página en
generateMetadata. - Errores de validación y mensajes de resultado en Server Actions.
not-found.tsx, la página 404.error.tsx, la página de errores en runtime.- Archivos generados como
opengraph-image.tsx,manifest.tsysitemap.ts. - Route Handlers que devuelven texto.
El hilo común es que ninguno se renderiza dentro del árbol de layouts de [locale] de la forma habitual, así que hay que pasarles el locale o leerlo de otra manera. El post sobre el routing de Next.js 16 cubre la configuración del routing; este trata de lo que esa configuración no alcanza.
¿Cómo se traducen los títulos y las descripciones de página?
Llama a getTranslations de next-intl/server dentro de generateMetadata. La función se puede exportar desde una página o un layout y es asíncrona, así que la API con await es la adecuada:
// app/[locale]/page.tsx
import {getTranslations} from 'next-intl/server';
export async function generateMetadata() {
const t = await getTranslations('Metadata');
return {title: t('title'), description: t('description')};
}
La trampa es la forma estática. Los agentes suelen escribir export const metadata = {title: 'Dashboard'} porque es más corta, y una constante no puede llamar a una función de traducción. Conviértela a la forma de función y añade las mismas claves al catálogo de cada locale. Este es además el texto que ve el buscador, por eso traducir la app sin traducir sus metadatos apenas mejora el posicionamiento.
¿Cómo se traducen los mensajes de Server Actions y de validación?
Pasa el locale a la acción y llama a getTranslations con un namespace y ese locale. Una Server Action no sabe en qué idioma está la página, así que la documentación de next-intl indica que se lo proporciones, por ejemplo como argumento:
'use server';
import {getTranslations} from 'next-intl/server';
export async function subscribe(locale: string, formData: FormData) {
const t = await getTranslations({locale, namespace: 'Subscribe'});
// ...validate, then return t('success') or t('invalidEmail')
}
Una advertencia de la misma documentación: piensa qué pasa si un visitante puede cambiar de idioma mientras hay en pantalla el mensaje de una acción. Con un segmento [locale] en la URL esto se resuelve solo, porque cambia la ruta. Sin él, quizá te convenga limpiar el mensaje tú mismo, por ejemplo dándole al componente key={locale} para que se reinicie su estado.
Para validar formularios con Zod, el enfoque documentado es un error map. Zod acepta uno por cada llamada a safeParse, y el mapa puede llamar a getTranslations. Ponlo en un único helper asíncrono y pasa su resultado como opción error en cada parse. Así tus schemas contienen reglas, no frases en inglés, y los formularios generados con IA dejan de mostrar "Invalid email" en una página en letón.
¿Cómo se traducen las páginas 404 y de error?
Pon not-found.tsx dentro de app/[locale]/ y añade un catch-all para que se dispare de verdad. Next.js renderiza ese archivo cuando un segmento llama a notFound(), y una URL desconocida no lo llama por sí sola:
// app/[locale]/[...rest]/page.tsx
import {notFound} from 'next/navigation';
export default function CatchAllPage() {
notFound();
}
Las peticiones que el middleware de next-intl no captura no llevan locale alguno, así que el 404 de [locale] nunca se les aplica. La respuesta de la documentación es app/global-not-found.tsx, que Next.js documenta como experimental y que renderiza sus propios html y body. Para un valor de [locale] desconocido, la documentación indica llamar a notFound(), normalmente en i18n/request.ts.
error.tsx es distinto. Next.js exige que sea un Client Component y se renderiza bajo tu layout, así que necesita un NextIntlClientProvider con al menos el namespace Error. Pasa solo esos mensajes en lugar de todo el catálogo:
// app/[locale]/layout.tsx (excerpt)
const messages = await getMessages();
// ...
<NextIntlClientProvider locale={locale} messages={pick(messages, 'Error')}>
Entonces useTranslations('Error') funciona dentro de error.tsx. La página no cubre global-error, así que revisa ese archivo a mano si lo tienes.
¿Y las imágenes para compartir, el manifest y el sitemap?
Todos necesitan que les pases el locale, porque viven fuera del árbol de layouts. Para opengraph-image.tsx, lee locale de params y llama a getTranslations({locale, namespace}). Con un localePrefix personalizado, la documentación avisa de que la ruta generada puede quedar inaccesible, porque Next.js no ve las reescrituras del proxy (middleware); la solución es hacer que el matcher ignore las rutas que terminan en opengraph-image.
app/manifest.ts está fuera de [locale], así que next-intl no puede inferir un locale a partir de la ruta. Elige uno representativo, normalmente el predeterminado, y pásalo de forma explícita. Para sitemap.ts, añade entradas alternates.languages y construye cada URL con el getPathname({locale, href}) asíncrono de tu módulo de navegación, con el host como prefijo. next-intl además envía por defecto una cabecera de respuesta link que anuncia las versiones por idioma; el sitemap es la opción cuando necesitas controlar slugs que vienen de un CMS.
¿Cómo gestionas los Route Handlers?
Toma el locale de un search param, de un segmento o de la cabecera accept-language, valídalo con hasLocale(routing.locales, locale) y devuelve un 400 si falla. Después llama a getTranslations({locale, namespace}). Si te saltas la validación, una cadena arbitraria llega a la consulta de tu catálogo.
¿Cómo encuentras el inglés que ha dejado atrás tu agente?
Ejecútalos desde la raíz del proyecto. Son toscos a propósito: el objetivo es obtener una lista que leer, no una puntuación.
# 1. Static metadata that cannot be translated
grep -rn "export const metadata" app
# 2. Titles and descriptions already in generateMetadata
grep -rn "title:" app --include=page.tsx --include=layout.tsx
# 3. Server Actions with literal return messages
grep -rln "use server" app src 2>/dev/null | xargs grep -n "message:\|error:"
# 4. Zod schemas with inline messages
grep -rn "message:\s*['\"]" app src 2>/dev/null
# 5. 404 and error files, and whether a catch-all exists
ls app/\[locale\]/not-found.tsx app/\[locale\]/error.tsx "app/[locale]/[...rest]/page.tsx" 2>&1
Todo lo que aparezca en los cuatro primeros resultados y sea un literal en inglés sin más pertenece al catálogo. Por qué los agentes siguen añadiendo cadenas escritas directamente en el código explica por qué esto se repite: el agente sigue el patrón visible y el patrón visible es el componente. Si usas Copilot, los límites de los archivos de instrucciones de Copilot también se aplican aquí.
¿Qué cambia esto en tu catálogo?
Añade claves, y las claves son donde empieza el desfase. Cada punto anterior mete entradas nuevas en el catálogo, como Metadata, Error, Subscribe y los mensajes de validación, y cada una necesita traducción en todos los idiomas que lanzas. Un proyecto que traduce sus componentes una vez y luego añade una Server Action o un título de página nuevo es el mismo proyecto que describe por qué los archivos de traducción se desincronizan.
next-intl sirve las traducciones en runtime y no lo sustituimos. globalize.now trabaja una capa por encima: conectas el repo en la app, el codebase se convierte una vez en un catálogo y los push jobs traducen después las unidades nuevas del catálogo a medida que aparecen, entregadas como cambios que revisas. La integración con Next.js describe la configuración, y qué tocamos y qué no en una configuración de i18n existente cubre el caso en que next-intl ya está integrado a mano.
Por dónde empezar
Ejecuta los cinco greps, convierte cualquier export estático de metadata, añade la ruta catch-all y dale su provider a error.tsx. Después comprueba que el catálogo tiene los namespaces nuevos en todos los locales. Si lanzaste una app creada con IA y prefieres que la conversión y la traducción se hagan por ti en lugar de mantenerlas tú, la página de Vibe Coders es el resumen, la referencia para desarrolladores cubre las configuraciones de agentes y los precios tienen 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