Retour

l’i18n sans fichiers de traduction

Jackie Chen avatarJackie Chen
guideinternationalizationnextjsi18ngt-nexttranslation-filesdeveloper-experience

Quiconque a déjà internationalisé une application JavaScript connaît le processus. Vous installez une bibliothèque d’i18n, créez un fichier en.json, extrayez de vos composants chaque texte visible par l’utilisateur, attribuez une clé à chacun, puis remplacez le texte par cette clé là où il se trouvait. Ensuite, vous dupliquez ce fichier JSON pour chaque langue prise en charge. es.json, fr.json, ja.json.

Au début, ça va. Trente chaînes, trois langues, 90 entrées.

Puis votre application grandit. Six mois plus tard, vous avez 400 chaînes et 12 langues. 4 800 entrées réparties dans une douzaine de fichiers. Un développeur ajoute une nouvelle fonctionnalité, écrit cinq nouvelles chaînes, oublie de mettre à jour trois fichiers de traduction. Personne ne s’en rend compte jusqu’à ce qu’un utilisateur à Tokyo voie du contenu de secours en anglais dans une interface japonaise. Quelqu’un suggère d’acheter un système de gestion de la traduction.

Le fardeau de la gestion des fichiers

Les fichiers de traduction créent une charge de maintenance qui augmente avec le nombre de langues prises en charge. Modifiez une chaîne et vous devez mettre à jour une douzaine de fichiers. Ces fichiers finissent par se désynchroniser. Les garder alignés demande des outils, de l’automatisation ou de la rigueur.

L’écosystème des solutions de contournement en dit long : des extensions VS Code qui génèrent automatiquement des clés de traduction, des générateurs de types qui valident votre JSON par rapport à des interfaces TypeScript, des linters pour repérer les clés inutilisées, des vérifications CI qui s’assurent que tous les paramètres régionaux sont synchronisés. Vous ne mettez pas en place ce genre d’outillage pour un workflow qui fonctionne bien.

Votre interface dit une chose, vos fichiers de traduction en disent une autre, et une correspondance fondée sur des clés fait le lien entre les deux :

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

Pour comprendre ce que ce composant affiche, il faudrait ouvrir checkout.json, trouver l’espace de noms summary et faire le rapprochement entre cinq clés. Avec des centaines de composants, la revue de code se transforme en exercice de va-et-vient entre les fichiers.

La syntaxe des messages ICU aggrave encore les choses. Le format utilisé par des bibliothèques comme i18next, react-intl et next-intl pour les pluriels, le genre et l’interpolation est en soi un mini-langage intégré dans des chaînes JSON. Une accolade mal placée dans {count, plural, one {# item} other {# items}} n’apparaîtra qu’au Runtime. Il existe des linters pour ça, mais la plupart des équipes s’en passent. Le code inline, lui, est vérifié par votre compilateur TypeScript et votre IDE dès que vous le saisissez.

Traduire directement dans le code

Et si vous n’extrayiez pas du tout les chaînes de caractères ?

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

Le composant <T> de gt-react marque un bloc de JSX à traduire. L’anglais reste dans votre composant. Lorsqu’un utilisateur accède au site en espagnol, le contenu à l’intérieur de <T> est remplacé par son équivalent en espagnol. La structure et la mise en forme sont conservées.

Il n’y a ni t('checkout.summary.title'), ni en.json, ni fichiers JSON par paramètre régional à garder en sync.

Les traductions en sortie de build

Elles sont générées au moment du déploiement. Le GT CLI analyse votre codebase pour tout ce qui se trouve dans les composants <T> et génère des traductions pour chaque langue cible. Le résultat est écrit dans un répertoire ignoré par Git, comme pour du CSS compilé ou du JavaScript bundle.

{
  "defaultLocale": "en",
  "locales": ["es", "fr", "ja", "de", "ko", "zh"],
  "files": {
    "gt": {
      "output": "public/_gt/[locale].json"
    }
  }
}
public/_gt/

En développement, les traductions se font on-demand. Modifiez une chaîne, actualisez la page, et voyez-la immédiatement en japonais. En production, tout est pré-généré et distribué via un CDN. Les fichiers de traduction existent toujours sur le disque, mais ce sont des artefacts générés, pas quelque chose que vous rédigez ou maintenez.

Ce qui change en pratique

La différence se voit davantage dans le travail au quotidien que dans les schémas d’architecture.

Une PR qui ajoute une nouvelle section à une page se lit comme telle. Le relecteur voit les vrais mots, pas checkout.summary.discount_applied_notice. La revue de code n’exige plus de comparer en parallèle avec un fichier JSON.

Le refactoring est moins pénible. Renommez un composant, déplacez-le, découpez-le en plusieurs parties. Les traductions suivent le contenu, puisqu’il n’y a pas de clés à remapper. Ajouter une langue revient à modifier une seule ligne de configuration. Supprimer un composant signifie que ses traductions cessent simplement d’être générées, au lieu de laisser des clés orphelines dans 15 fichiers que personne n’a envie de nettoyer.

Vers quoi évoluent les bibliothèques i18n

Dans l’ensemble de l’écosystème, la tendance va vers des chaînes inline. Les premières bibliothèques i18n comme i18next et react-intl ont été conçues à une époque où la traduction automatique n’était pas viable et où chaque chaîne devait être confiée à un traducteur humain. Les dictionnaires avaient alors du sens comme format d’échange. Cette contrainte a disparu, et le coût, en expérience développeur, de la maintenance de fichiers de chaînes séparés est de plus en plus difficile à justifier.

next-intl a ajouté des appels t() sans dictionnaire en plus de son mode dictionnaire. Le compilateur de Lingui extrait les messages à la build à partir de tagged templates inline. Paraglide suit une autre approche, en compilant des fichiers de messages en fonctions éliminables par tree-shaking pour chaque paramètre régional. Les approches diffèrent, mais dans tous les cas, le contenu se rapproche du composant. GT pousse cette logique jusqu’au bout : votre JSX est la source de vérité, et la traduction est une étape de compilation.

Le compromis

Lorsque vous écrivez du contenu inline, vous l’écrivez dans votre langue maternelle. La structure de vos composants, la tournure de vos phrases, le flux de votre interface reflètent tous votre façon de penser en anglais (ou dans votre langue source, quelle qu’elle soit). Une approche fondée sur un dictionnaire comme next-intl est, par conception, plus indépendante de la langue, car le composant ne contient jamais de véritable phrase dans quelque langue que ce soit, seulement une clé qui renvoie ailleurs.

Mais la plupart des développeurs pensent déjà dans une langue lorsqu’ils construisent une interface. La mise en page, les textes, les libellés des boutons sont d’abord conçus en anglais. Ce biais est présent dès la conception, que les chaînes soient inline ou dans un fichier JSON. Nous pensons que le framework d’i18n doit s’adapter à votre façon réelle de travailler. Construisez l’application naturellement et laissez le framework gérer la traduction, plutôt que d’abstraire le contenu en clés au nom de la neutralité linguistique.

Pour commencer

npx gt@latest init

L’assistant de configuration configure votre projet, installe les dépendances et met en place le rechargement à chaud des traductions en développement. Procédure complète dans le guide Quickstart.

gt-react est open source. Pour l’App Router de Next.js, il y a gt-next. Pour React Native, il y a gt-react-native.