Cómo cargar traducciones desde archivos en TanStack Start v1
Separar el contenido traducible del código
Problema
Codificar directamente las cadenas de texto destinadas al usuario en los componentes crea un acoplamiento estrecho entre el contenido y el código. Cada vez que una cadena cambia o se añade un nuevo idioma, los desarrolladores deben localizar y modificar los archivos fuente, y luego volver a desplegar la aplicación. Este enfoque hace que los flujos de trabajo de traducción dependan de los ciclos de ingeniería e impide que los miembros del equipo no técnicos actualicen el texto. A medida que crece el número de idiomas soportados, la lógica condicional para seleccionar la cadena correcta se vuelve complicada y propensa a errores. El resultado es una iteración más lenta, mayores costos de mantenimiento y una base de código saturada con contenido traducible que debería estar en otro lugar.
Solución
Separa todas las cadenas traducibles del código de la aplicación almacenándolas en archivos JSON externos, uno por localización. Define una clave estable para cada mensaje y referencia esas claves en los componentes en lugar de texto literal. En tiempo de ejecución, carga el archivo de traducción apropiado basado en la localización del usuario y proporciona esos mensajes al IntlProvider de react-intl. Esto desacopla el contenido del código: los traductores pueden trabajar directamente con archivos JSON, los cambios de texto no requieren modificaciones de código, y añadir un nuevo idioma significa agregar un nuevo archivo sin tocar los componentes.
Pasos
1. Crear archivos de traducción para cada localización
Organiza las traducciones como archivos JSON en un directorio dedicado, con un archivo por localización que contenga pares clave-valor para todos los mensajes.
{
"welcome": "Welcome back",
"greeting": "Hello, {name}",
"itemCount": "{count, plural, =0 {No items} one {One item} other {# items}}"
}
Guarda esto como app/translations/en.json. Crea archivos paralelos para otras localizaciones, como app/translations/es.json y app/translations/fr.json, con las mismas claves pero con valores traducidos.
2. Cargar traducciones usando una función de servidor
Utiliza una función de servidor para leer el archivo de traducción desde el disco basado en la configuración regional solicitada, asegurando que las traducciones se carguen del lado del servidor durante SSR y se obtengan en el cliente durante la navegación.
import { createServerFn } from "@tanstack/react-start";
import * as fs from "node:fs";
export const getMessages = createServerFn({ method: "GET" }).handler(
async ({ request }) => {
const url = new URL(request.url);
const locale = url.searchParams.get("locale") || "en";
const filePath = `app/translations/${locale}.json`;
const content = await fs.promises.readFile(filePath, "utf-8");
return JSON.parse(content);
},
);
Esta función lee el archivo JSON para la configuración regional dada y devuelve el objeto de mensajes analizado. Se ejecuta solo en el servidor, manteniendo seguro el acceso al sistema de archivos.
3. Crear un helper para determinar la configuración regional del usuario
Define una pequeña utilidad que extrae la configuración regional de la solicitud o recurre a un valor predeterminado, haciéndola reutilizable en todas las rutas.
export function getLocaleFromRequest(request: Request): string {
const url = new URL(request.url);
const localeParam = url.searchParams.get("locale");
if (localeParam) return localeParam;
const acceptLanguage = request.headers.get("accept-language");
if (acceptLanguage) {
const match = acceptLanguage.split(",")[0].split("-")[0];
return match || "en";
}
return "en";
}
Esta función verifica primero los parámetros de consulta, luego el encabezado Accept-Language, y por defecto utiliza inglés. Proporciona una única fuente de verdad para la detección de la configuración regional.
4. Cargar mensajes en un cargador de ruta
Utiliza el cargador de ruta para obtener mensajes para la configuración regional actual antes de renderizar, haciéndolos disponibles para el árbol de componentes.
import { createFileRoute } from "@tanstack/react-router";
import { getMessages, getLocaleFromRequest } from "../lib/i18n";
export const Route = createFileRoute("/")({
loader: async ({ context }) => {
const locale = getLocaleFromRequest(context.request);
const messages = await getMessages({ data: { locale } });
return { locale, messages };
},
component: HomePage,
});
function HomePage() {
const { locale, messages } = Route.useLoaderData();
return (
<div>
<p>{messages.welcome}</p>
</div>
);
}
El cargador llama a la función del servidor para recuperar mensajes, y el componente accede a ellos a través de useLoaderData. Este patrón funciona tanto para SSR como para navegación del lado del cliente.
5. Proporcionar mensajes a react-intl
Envuelve tu árbol de componentes con IntlProvider, pasando la configuración regional cargada y los mensajes para que todos los componentes descendientes puedan acceder a las traducciones.
import { IntlProvider } from "react-intl";
function HomePage() {
const { locale, messages } = Route.useLoaderData();
return (
<IntlProvider locale={locale} messages={messages}>
<AppContent />
</IntlProvider>
);
}
function AppContent() {
return (
<div>
<FormattedMessage id="welcome" />
</div>
);
}
IntlProvider hace que la configuración regional y los mensajes estén disponibles para todos los componentes y hooks de react-intl. Los componentes ahora pueden referenciar mensajes por clave usando FormattedMessage o useIntl, y la traducción correcta se renderiza basada en la configuración regional cargada.
6. Referenciar mensajes por clave en componentes
Usa el componente FormattedMessage de react-intl o el hook useIntl para mostrar cadenas traducidas, referenciando las claves definidas en tus archivos JSON.
import { FormattedMessage, useIntl } from "react-intl";
function UserGreeting({ name }: { name: string }) {
const intl = useIntl();
const title = intl.formatMessage({ id: "greeting" }, { name });
return (
<div>
<h1 title={title}>
<FormattedMessage id="greeting" values={{ name }} />
</h1>
<p>
<FormattedMessage id="itemCount" values={{ count: 5 }} />
</p>
</div>
);
}
FormattedMessage renderiza la cadena traducida en línea, mientras que useIntl().formatMessage devuelve una cadena para usar en atributos o lógica JavaScript. Ambos aceptan values para interpolación y soportan la sintaxis de mensajes ICU para plurales y formateo.