El archivo translation.json empieza pequeño. Después, cada pantalla añade sus propias claves, dos componentes usan title para cosas distintas y nadie sabe si checkout.payment.failed todavía pertenece a checkout o a una regla compartida.
Cuando ocurre, el problema va más allá de react-i18next. La aplicación no tiene una frontera clara de propiedad para los mensajes. En una app grande, organiza las traducciones por funcionalidad, mantén estables los nombres y deja que TypeScript y CI comprueben el contrato.
Este artículo amplía la guía general para internacionalizar aplicaciones React y servicios backend. Aquí la pregunta es más concreta: ¿dónde debe vivir una clave y cómo puedes descubrir pronto que un cambio rompió el catálogo?
La respuesta corta
- Usa un namespace por funcionalidad o área del producto, no un archivo enorme por idioma.
- Prefiere claves semánticas y estables como
checkout.payment.faileden lugar de la frase en inglés.- Deriva los tipos del locale fuente y usa
keyPrefixpara reducir rutas repetidas en los componentes.- Comprueba claves faltantes, textos hardcoded y tipos generados antes del merge.
¿Por qué un único archivo de traducción empieza a fallar?
El problema de un catálogo único aparece en la propiedad, la carga y la revisión. La documentación de react-i18next recomienda separar los archivos de traducción en namespaces y cargarlos cuando una pantalla los necesita (react-i18next, "Multiple Translation Files").
Un archivo por idioma parece sencillo mientras el producto tiene pocas pantallas. Cuando crece, navegación, facturación, onboarding y mensajes de error terminan juntos. El nombre de la clave deja de mostrar quién puede cambiarla, y una revisión pequeña toca un archivo editado por varios equipos.
Los namespaces también son una frontera de carga. El hook useTranslation puede recibir un namespace, y el runtime puede cargar solo ese conjunto cuando se renderiza una ruta. Eso no significa que todos los proyectos necesiten lazy loading, pero hace explícita la decisión en lugar de atar cada idioma a un paquete monolítico.
El objetivo no es crear decenas de archivos porque la carpeta se vea ordenada. Un namespace debe representar un área con responsable, ciclo de cambios y contexto propio. Si dos pantallas siempre cambian juntas y comparten vocabulario, quizá pertenezcan al mismo namespace.
¿Cómo elegir namespaces por funcionalidad?
Empieza por la pantalla o el flujo que reconoce el usuario. checkout, account y reports son fronteras más útiles que buttons, labels y messages, porque conectan el catálogo con un equipo y con la parte del producto que la persona está usando.
Una estructura sencilla puede ser esta:
src/locales/
├── en-US/
│ ├── common.json
│ ├── checkout.json
│ └── account.json
├── pt-BR/
│ ├── common.json
│ ├── checkout.json
│ └── account.json
└── es/
├── common.json
├── checkout.json
└── account.json
Mantén common pequeño. Coloca ahí solo términos realmente compartidos, como acciones del shell o estados usados por varias áreas. Si cada pantalla nueva añade texto a common, solo has cambiado un archivo enorme por un namespace enorme.
Dentro de checkout.json, agrupa los mensajes por contexto de uso:
{
"summary": {
"title": "Revisa tu pedido",
"total": "Total",
"submit": "Confirmar pedido"
},
"payment": {
"failed": "No pudimos confirmar el pago",
"retry": "Intentar de nuevo"
}
}
El namespace indica qué funcionalidad es responsable de la clave. La ruta interna explica la pantalla o el estado. Es más fácil de buscar que un catálogo donde error, title y button aparecen decenas de veces.
¿Claves semánticas o frases como claves?
Elige un estilo y escríbelo. i18next usa por defecto una notación basada en claves y también documenta las claves en lenguaje natural como alternativa (i18next, "Getting started").
En una aplicación mantenida por varias personas, las claves semánticas separan el texto visible de la identidad que usa el código. payment.failed puede seguir apuntando al mismo concepto cuando cambia la frase, y una persona traductora puede recibir un texto nuevo sin tener que buscar la frase anterior.
Eso no significa convertir cada frase en una taxonomía profunda. Evita rutas que describan todo el árbol de componentes, como pages.checkout.components.payment.form.card.error. La estructura debe sobrevivir a una refactorización visual.
Evita también las claves vagas. title dentro de checkout.summary se entiende. title dentro de common casi nunca. Si un mensaje tiene interpolación, plural o contexto, deja esa información explícita en el recurso y prueba los casos que cambian la frase entre locales.
¿Cómo tipar las claves con TypeScript?
i18next puede inferir claves y valores desde un objeto de recursos y exponer esa forma mediante module augmentation. La documentación oficial muestra cómo configurar CustomTypeOptions, resources, defaultNS y keyPrefix (i18next, "TypeScript").
Usa un locale como fuente del contrato. Los otros idiomas deben mantenerse al día con sus claves, pero no deberían redefinir la forma que conoce el código. Este ejemplo es ilustrativo:
// src/i18n/resources.ts
export const resources = {
"en-US": {
common: {
cancel: "Cancel",
},
checkout: {
summary: {
title: "Review your order",
submit: "Place order",
},
},
},
} as const;
export default resources;
// src/@types/i18next.d.ts
import "i18next";
import resources from "../i18n/resources";
declare module "i18next" {
interface CustomTypeOptions {
defaultNS: "common";
resources: (typeof resources)["en-US"];
}
}
En el componente, el namespace y el prefijo reducen la ruta que cada llamada repite:
import { useTranslation } from "react-i18next";
export function OrderSummary() {
const { t } = useTranslation("checkout", { keyPrefix: "summary" });
return (
<section aria-labelledby="order-summary-title">
<h2 id="order-summary-title">{t("title")}</h2>
<button type="submit">{t("submit")}</button>
</section>
);
}
Este ejemplo es ilustrativo. El nombre del locale fuente, la carga de recursos y la configuración del bundler dependen de la aplicación. Ejecuta tsc --noEmit en el proyecto y confirma que una clave desconocida falla donde el código se revisaría antes de adoptar este patrón de tipado.
La documentación actual de i18next también describe enableSelector: "optimize" para consultas con selector y conjuntos grandes de traducciones. Esa opción cambia la API que usa el equipo, así que trátala como una decisión de migración, no como una línea para copiar sin medir su impacto.
¿Cómo mantener sincronizados los locales?
Tipar el locale fuente no demuestra que pt-BR y es contengan todas las claves. La verificación debe comparar los recursos y también encontrar textos que escaparon del flujo de traducción.
El i18next-cli, mantenido por el mismo proyecto, combina extracción de claves, generación de tipos, sincronización de locales y linting (i18next, "i18next-cli"). Un pipeline inicial puede ser:
npx i18next-cli status
npx i18next-cli extract --ci
npx i18next-cli types --ci
npx tsc --noEmit
status ayuda a ver la cobertura por idioma y namespace. extract --ci puede fallar cuando los archivos no reflejan las claves encontradas en el código. types --ci evita que las definiciones generadas queden desactualizadas. tsc cierra la parte que depende del uso tipado en el componente.
Hay un límite importante: el análisis estático no entiende todas las claves construidas dinámicamente, como t(`error.${code}`). El README del CLI incluye este caso entre las limitaciones del informe de claves no usadas (i18next, "i18next-cli"). Para esos flujos, usa un mapa explícito de claves permitidas o una prueba que recorra los códigos válidos.
No bloquees un merge porque una traducción editorial todavía esté pendiente si el producto tiene un fallback aceptable. Bloquea cuando la aplicación perdió la clave, el namespace no carga, la interpolación no coincide o una pantalla introdujo texto visible fuera del contrato. Son fallos distintos y merecen mensajes distintos en CI.
¿Qué debes verificar antes de aceptar una clave nueva?
Una revisión de traducción debe responder dos preguntas: ¿la clave está en el lugar correcto y el comportamiento sigue siendo correcto en cada locale? i18next describe los namespaces como agrupaciones lógicas y explica el orden de resolución entre idioma, namespace y fallback (i18next, "Translation Resolution").
Usa esta lista para un cambio de producto:
- Propiedad: ¿la clave pertenece a una funcionalidad concreta? Si la usan dos áreas, ¿hay una razón para compartirla?
- Nombre: ¿el nombre describe el concepto y sigue siendo válido si el componente cambia de sitio?
- Parámetros: ¿la interpolación, el plural y el contexto tienen pruebas para los valores que cambian la frase?
- Carga: ¿la pantalla pide el namespace correcto y maneja el estado mientras todavía está llegando?
- Paridad: ¿todos los locales esperados tienen la misma clave o una excepción documentada?
- Diseño: ¿una traducción más larga cabe en el botón, el menú y el layout móvil?
- Eliminación: cuando desaparece la funcionalidad, ¿se elimina la clave antigua o se registra como deuda?
La lista es corta a propósito. Conecta la revisión del JSON con el comportamiento que ve el usuario. Para los flujos de pantalla, una prueba de navegador también ayuda: separa el mock de API de la prueba de contrato en Playwright y usa navegación real para comprobar idioma, fallback y contenido visible.
¿Cuándo debe cambiar la estructura?
No reorganices todos los archivos solo porque el producto haya crecido. Cambia la estructura cuando un equipo no puede encontrar quién es responsable de una clave, cuando la carga obliga a enviar catálogos de pantallas que no se usan o cuando la comprobación de paridad tarda más que el cambio.
Una migración segura empieza con un namespace nuevo y una frontera clara. Mueve una funcionalidad cada vez, conserva un alias temporal solo cuando la compatibilidad lo exija y elimina la ruta antigua después de actualizar los consumidores. i18next documenta keyPrefix y getFixedT para reducir repeticiones sin ocultar de dónde viene una clave (i18next, "API").
Si el equipo no puede explicar la diferencia entre common, shared y global, no crees otro nombre de la misma familia. Da un ejemplo concreto, registra al responsable y haz que la próxima revisión use la regla. Una convención pequeña que la gente sigue vale más que un árbol perfecto que nadie respeta.
Preguntas frecuentes
¿Debo crear un namespace para cada componente React?
No por defecto. Un componente reutilizable puede tener texto propio, pero el namespace debe seguir una unidad de propiedad y carga. Un archivo por componente aumenta la coordinación y puede separar una frase que debería entenderse como un mismo flujo. Empieza por funcionalidad y divide después cuando el tamaño o la carga lo justifique.
¿Las claves semánticas son mejores que usar la frase en inglés?
No hay una respuesta universal. Las claves semánticas separan el texto de su identidad, lo que ayuda cuando cambia el copy y cuando varias personas mantienen el catálogo. Las claves naturales pueden ser más rápidas en un proyecto pequeño. El riesgo aparece al mezclar estilos o tratar una frase como identificador estable.
¿TypeScript garantiza que todos los locales tengan la traducción?
No por sí solo. El tipado puede comprobar el recurso que conoce el código, pero la paridad entre archivos de locale necesita extracción, status, una prueba propia u otro paso del pipeline. Mantén las dos verificaciones separadas para saber si falló el uso de la clave o si no se entregó una traducción.
Conclusión
En una aplicación grande, las claves de traducción forman parte de la arquitectura del producto. Da a cada funcionalidad un namespace claro, usa nombres que sobrevivan a las refactorizaciones y elige un locale fuente para alimentar el contrato de TypeScript.
Después automatiza lo que la gente olvida: extracción, paridad, tipos y textos hardcoded. El objetivo no es producir más archivos. Es permitir que alguien encuentre un mensaje, entienda quién lo mantiene y descubra una rotura antes que el usuario.
Fuentes consultadas
- i18next: TypeScript, consultado el 17/08/2026.
- i18next: Translation Resolution, consultado el 17/08/2026.
- i18next: API, consultado el 17/08/2026.
- i18next: Extracting translations, consultado el 17/08/2026.
- i18next: i18next-cli, consultado el 17/08/2026.
- react-i18next: Multiple Translation Files, consultado el 17/08/2026.
- i18next: Getting started, consultado el 17/08/2026.