Comment charger les traductions depuis des fichiers dans TanStack Start v1

Séparer le contenu traduisible du code

Problème

Coder en dur les chaînes destinées aux utilisateurs directement dans les composants crée un couplage étroit entre le contenu et le code. Chaque fois qu'une chaîne change ou qu'une nouvelle langue est ajoutée, les développeurs doivent localiser et modifier les fichiers source, puis redéployer l'application. Cette approche rend les workflows de traduction dépendants des cycles d'ingénierie et empêche les membres non techniques de l'équipe de mettre à jour le contenu. À mesure que le nombre de langues prises en charge augmente, la logique conditionnelle pour sélectionner la bonne chaîne devient lourde et sujette aux erreurs. Le résultat est une itération plus lente, des coûts de maintenance plus élevés et une base de code encombrée de contenu traduisible qui devrait se trouver ailleurs.

Solution

Séparez toutes les chaînes traduisibles du code de l'application en les stockant dans des fichiers JSON externes, un par locale. Définissez une clé stable pour chaque message et référencez ces clés dans les composants au lieu de texte littéral. Au moment de l'exécution, chargez le fichier de traduction approprié en fonction de la locale de l'utilisateur et fournissez ces messages au IntlProvider de react-intl. Cela découple le contenu du code : les traducteurs peuvent travailler directement avec les fichiers JSON, les modifications de contenu ne nécessitent aucune modification du code, et l'ajout d'une nouvelle langue signifie simplement ajouter un nouveau fichier sans toucher aux composants.

Étapes

1. Créer des fichiers de traduction pour chaque locale

Organisez les traductions sous forme de fichiers JSON dans un répertoire dédié, avec un fichier par locale contenant des paires clé-valeur pour tous les messages.

{
"welcome": "Welcome back",
"greeting": "Hello, {name}",
"itemCount": "{count, plural, =0 {No items} one {One item} other {# items}}"
}

Enregistrez ceci sous app/translations/en.json. Créez des fichiers parallèles pour d'autres locales, tels que app/translations/es.json et app/translations/fr.json, avec les mêmes clés mais des valeurs traduites.

2. Charger les traductions à l'aide d'une fonction serveur

Utilisez une fonction serveur pour lire le fichier de traduction depuis le disque en fonction de la locale demandée, garantissant que les traductions sont chargées côté serveur pendant le SSR et récupérées côté client lors de la navigation.

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);
  },
);

Cette fonction lit le fichier JSON pour la locale donnée et retourne l'objet messages analysé. Elle s'exécute uniquement sur le serveur, préservant la sécurité de l'accès au système de fichiers.

3. Créer une fonction utilitaire pour déterminer la locale de l'utilisateur

Définissez un petit utilitaire qui extrait la locale de la requête ou utilise une valeur par défaut, le rendant réutilisable dans toutes les routes.

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";
}

Cette fonction vérifie d'abord les paramètres de requête, puis l'en-tête Accept-Language, et utilise l'anglais par défaut. Elle fournit une source unique de vérité pour la détection de la locale.

4. Charger les messages dans un loader de route

Utilisez le loader de route pour récupérer les messages de la locale actuelle avant le rendu, les rendant disponibles pour l'arborescence de composants.

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>
  );
}

Le loader appelle la fonction serveur pour récupérer les messages, et le composant y accède via useLoaderData. Ce pattern fonctionne à la fois pour le SSR et la navigation côté client.

5. Fournir les messages à react-intl

Enveloppez votre arborescence de composants avec IntlProvider, en passant la locale et les messages chargés afin que tous les composants descendants puissent accéder aux traductions.

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 rend la locale et les messages disponibles pour tous les composants et hooks react-intl. Les composants peuvent maintenant référencer les messages par clé en utilisant FormattedMessage ou useIntl, et la traduction correcte est rendue en fonction de la locale chargée.

6. Référencer les messages par clé dans les composants

Utilisez le composant FormattedMessage ou le hook useIntl de react-intl pour afficher les chaînes traduites, en référençant les clés définies dans vos fichiers 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 affiche la chaîne traduite en ligne, tandis que useIntl().formatMessage retourne une chaîne pour une utilisation dans les attributs ou la logique JavaScript. Les deux acceptent values pour l'interpolation et prennent en charge la syntaxe de message ICU pour les pluriels et le formatage.