Cómo cargar traducciones desde archivos en TanStack Start v1
Separa el contenido traducible del código
Problema
Codificar cadenas de texto orientadas al usuario directamente en los componentes crea un acoplamiento estrecho entre contenido y código. Cada vez que una cadena cambia o se añade un nuevo idioma, los desarrolladores deben localizar y modificar archivos fuente, y luego redesplegar la aplicación. Este enfoque hace que los flujos de trabajo de traducción dependan de ciclos de ingeniería e impide que miembros del equipo no técnicos actualicen el contenido. A medida que crece el número de idiomas soportados, la lógica condicional para seleccionar la cadena correcta se vuelve difícil de manejar 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 locale. 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 según el locale 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 contenido no requieren modificaciones de código, y añadir un nuevo idioma significa añadir un nuevo archivo sin tocar los componentes.
Pasos
1. Crea archivos de traducción para cada locale
Organiza las traducciones como archivos JSON en un directorio dedicado, con un archivo por locale 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 otros locales, como app/translations/es.json y app/translations/fr.json, con las mismas claves pero valores traducidos.
2. Carga las traducciones usando una función de servidor
Utiliza una función de servidor para leer el archivo de traducción desde el disco según la configuración regional solicitada, asegurando que las traducciones se carguen en el servidor durante el 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. Crea una utilidad para determinar la configuración regional del usuario
Define una pequeña utilidad que extraiga la configuración regional de la solicitud o recurra 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 usa inglés. Proporciona una única fuente de verdad para la detección de configuración regional.
4. Carga 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 de servidor para recuperar mensajes, y el componente accede a ellos mediante useLoaderData. Este patrón funciona tanto para SSR como para navegación del lado del cliente.
5. Proporciona mensajes a react-intl
Envuelve tu árbol de componentes con IntlProvider, pasando la configuración regional y los mensajes cargados 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 hacer referencia a mensajes por clave usando FormattedMessage o useIntl, y se renderiza la traducción correcta según la configuración regional cargada.
6. Haz referencia a mensajes por clave en componentes
Utiliza el componente FormattedMessage de react-intl o el hook useIntl para mostrar cadenas traducidas, haciendo referencia a 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 de JavaScript. Ambos aceptan values para interpolación y admiten la sintaxis de mensajes ICU para plurales y formato.