i18n sin archivos de traducción
Cualquiera que haya internacionalizado una aplicación JavaScript conoce el flujo de trabajo. Instalas una biblioteca de i18n, creas un archivo en.json, extraes de tus componentes todas las cadenas visibles para el usuario, asignas una clave a cada una y usas esa clave donde antes estaba la cadena. Luego duplicas ese archivo JSON para cada idioma que admites. es.json, fr.json, ja.json.
Al principio, no pasa nada. Treinta cadenas, tres idiomas, 90 entradas.
Pero tu aplicación crece. Seis meses después tienes 400 cadenas y 12 idiomas. 4.800 entradas repartidas en una docena de archivos. Un desarrollador añade una nueva funcionalidad, escribe cinco cadenas nuevas y se olvida de actualizar tres de los archivos de traducción. Nadie se da cuenta hasta que un usuario en Tokio ve contenido alternativo en inglés en una interfaz en japonés. Alguien propone comprar un sistema de gestión de traducciones.
El peaje de gestionar archivos
Los archivos de traducción generan una carga de mantenimiento que aumenta con la cantidad de idiomas que admites. Cambia un string y tienes que actualizar una docena de archivos. Esos archivos acaban desincronizándose. Mantenerlos alineados requiere herramientas, automatización o disciplina.
El ecosistema de apaños lo deja claro: extensiones de VS Code que generan automáticamente claves de traducción, generadores de tipos que validan tu JSON frente a interfaces de TypeScript, linters para claves sin usar, comprobaciones de CI que verifican que todos los locales estén en sync. No creas este tipo de herramientas para un flujo de trabajo que funciona bien.
Tu interfaz dice una cosa, tus archivos de traducción dicen otra, y una asociación basada en claves mantiene ambas unidas:
function CheckoutSummary({ itemCount, discount }) {
const { t } = useTranslation('checkout');
return (
<div>
<h2>{t('summary.title')}</h2>
<p>{t('summary.item_count', { count: itemCount })}</p>
{discount && <p>{t('summary.discount_applied', { percent: discount })}</p>}
<p>{t('summary.total_label')}</p>
<small>{t('summary.tax_notice')}</small>
</div>
);
}Para entender qué renderiza este componente, tendrías que abrir checkout.json, buscar el espacio de nombres summary y cotejar cinco claves. A lo largo de cientos de componentes, la revisión de código se convierte en un ejercicio de ir saltando de archivo en archivo.
La sintaxis de mensajes ICU lo empeora aún más. El formato que usan bibliotecas como i18next, react-intl y next-intl para plurales, género e interpolación es su propio minilenguaje incrustado en cadenas JSON. Una llave mal colocada en {count, plural, one {# item} other {# items}} no se detectará hasta tiempo de ejecución. Existen linters para esto, pero la mayoría de los equipos se los saltan. El código inline, en cambio, lo validan tu compilador de TypeScript y tu IDE en cuanto lo escribes.
Traducir código directamente
¿Y si no extrajeras ninguna cadena?
import { T } from 'gt-react';
function CheckoutSummary({ itemCount, discount }) {
return (
<T>
<div>
<h2>Order Summary</h2>
<p>You have {itemCount} items in your cart.</p>
{discount && <p>{discount}% discount applied!</p>}
<p>Total (before tax):</p>
<small>Tax calculated at checkout.</small>
</div>
</T>
);
}El componente <T> en gt-react marca un bloque de JSX para traducirlo. El inglés se mantiene en tu componente. Si un usuario accede en español, el contenido dentro de <T> se sustituye por su equivalente en español. La estructura y el formato se conservan.
No hay t('checkout.summary.title'), ni en.json, ni archivos JSON por cada configuración regional que debas mantener en sync.
Traducciones como resultado de la compilación
Se generan en el momento del despliegue. La GT CLI analiza tu codebase en busca de todo lo que esté dentro de componentes <T> y genera traducciones para cada idioma de destino. El resultado se guarda en un directorio ignorado por git, como el CSS compilado o el JavaScript empaquetado.
{
"defaultLocale": "en",
"locales": ["es", "fr", "ja", "de", "ko", "zh"],
"files": {
"gt": {
"output": "public/_gt/[locale].json"
}
}
}public/_gt/En desarrollo, las traducciones se realizan on-demand. Cambia una string, recarga y la verás en japonés de inmediato. En producción, todo se genera por adelantado y se sirve desde una CDN. Los archivos de traducción siguen existiendo en disco, pero son artefactos de salida, no algo que escribas o mantengas.
Qué cambia en la práctica
La diferencia se nota más en el trabajo diario que en los diagramas de arquitectura.
Un PR que añade una nueva sección a una página se lee justamente así: como un PR que añade una nueva sección a una página. Quien revisa ve las palabras reales, no checkout.summary.discount_applied_notice. La revisión de código deja de requerir una comparación en paralelo con un archivo JSON.
Refactorizar duele menos. Renombra un componente, muévelo, divídelo en partes. Las traducciones siguen al contenido porque no hay claves que reasignar. Añadir un idioma es un cambio de configuración de una sola línea. Al eliminar un componente, sus traducciones simplemente dejan de generarse, en vez de dejar claves huérfanas repartidas en 15 archivos que nadie quiere limpiar.
Hacia dónde se dirigen las bibliotecas de i18n
La tendencia en todo el ecosistema apunta a las cadenas inline. Las primeras bibliotecas de i18n, como i18next y react-intl, surgieron cuando la traducción automática no era viable y cada cadena tenía que pasarse a un traductor humano. Los diccionarios tenían sentido como formato de intercambio. Esa limitación ya no existe, y el coste en experiencia de desarrollo de mantener archivos de cadenas en paralelo es cada vez más difícil de justificar.
next-intl añadió llamadas a t() sin diccionario junto con su modo de diccionario. El Compiler de Lingui extrae mensajes en tiempo de compilación a partir de tagged templates inline. Paraglide sigue una ruta distinta: compila archivos de mensajes en funciones optimizables mediante tree-shaking para cada configuración regional. Los enfoques difieren, pero en todos los casos el contenido se acerca más al componente. GT lleva esto hasta el final: tu JSX es la fuente de referencia, y la traducción es un paso de compilación.
La contrapartida
Cuando escribes contenido inline, lo haces en tu idioma nativo. La estructura de tus componentes, la forma en que construyes las frases y el flujo de tu interfaz reflejan cómo piensas en inglés (o en cualquier otro idioma de origen). Un enfoque basado en diccionarios como next-intl es, por diseño, más independiente del idioma, porque el componente nunca contiene una frase real en ningún idioma, solo una clave que apunta a otro lugar.
Pero la mayoría de los desarrolladores ya piensan en un solo idioma cuando crean una interfaz. El diseño, los textos y las etiquetas de los botones se conciben primero en inglés. Ese sesgo ya está presente en el diseño, tanto si las cadenas están inline como si están en un archivo JSON. Creemos que el framework de i18n debe adaptarse a cómo trabajas realmente. Crea la aplicación de forma natural y deja que el framework se encargue de la traducción, en lugar de abstraer el contenido en claves por el bien de la neutralidad lingüística.
Primeros pasos
npx gt@latest initEl asistente de configuración prepara tu proyecto, instala las dependencias y habilita la recarga en caliente de las traducciones durante el desarrollo. Consulta el proceso completo en la guía de Quickstart.
gt-react es de código abierto. Para Next.js App Router, está disponible gt-next. Para React Native, está disponible gt-react-native.