Comment charger des traductions à partir de fichiers dans TanStack Start v1

Séparer le contenu traduisible du code

Problème

Le codage en dur des 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 sources, puis redéployer l'application. Cette approche rend les flux de travail de traduction dépendants des cycles d'ingénierie et empêche les membres non techniques de l'équipe de mettre à jour le texte. À 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 du texte littéral. À 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 texte ne nécessitent aucune modification de 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": "Bienvenue",
"greeting": "Bonjour, {name}",
"itemCount": "{count, plural, =0 {Aucun élément} one {Un élément} other {# éléments}}"
}

Enregistrez ceci comme app/translations/en.json. Créez des fichiers parallèles pour d'autres locales, comme 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 pendant 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 renvoie l'objet messages analysé. Elle s'exécute uniquement sur le serveur, maintenant l'accès au système de fichiers sécurisé.

3. Créer un 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 à travers 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 chargeur de route

Utilisez le chargeur de route pour récupérer les messages pour la locale actuelle avant le rendu, les rendant disponibles pour l'arborescence des 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 chargeur appelle la fonction serveur pour récupérer les messages, et le composant y accède via useLoaderData. Ce modèle fonctionne à la fois pour le SSR et la navigation côté client.

5. Fournir des messages à react-intl

Enveloppez votre arborescence de composants avec IntlProvider, en transmettant la locale chargée et les messages 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 de 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 de react-intl ou le hook useIntl pour afficher des 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 renvoie une chaîne à utiliser dans les attributs ou la logique JavaScript. Les deux acceptent des values pour l'interpolation et prennent en charge la syntaxe de message ICU pour les pluriels et le formatage.