Nous developpons votre site vitrine SPA À PARTIR DE 300€. Oui, c'est dingue. Web SPA À PARTIR DE 300€ — Oui, c'est dingue. Parlons-en →

Comment traduire une application web sans inventer une clé pour chaque texte

Comment traduire une application web sans inventer une clé pour chaque texte

Toute application multilingue se heurte tôt ou tard au même dilemme, et le tranche presque toujours sans se rendre compte qu'elle faisait un choix. Quand les mille premiers textes sont écrits, il est déjà trop tard pour changer d'avis à bon compte. Vous verrez ici pourquoi ce dilemme existe, comment chaque technologie le résout et ce qui se passe quand votre application atteint dix mille textes. Les exemples sont en Laravel, l'un des stacks que nous pratiquons chez AndorraDev, mais le problème est identique avec Symfony, Rails, Django ou Next.js.

Le problème n'est pas de traduire, c'est d'identifier chaque texte

Tout système d'internationalisation demande la même chose : identifiez le texte d'une façon ou d'une autre. Et il n'y a que deux façons, toutes deux avec un coût qui se paie plus tard.

La première est d'utiliser une clé. Vous écrivez dashboard.empty_state_title et vous définissez ailleurs ce que cela signifie. La seconde est d'utiliser le texte d'origine comme identifiant : vous écrivez "Vous n'avez encore aucun bien" et ce littéral devient la clé.

Aucune n'est gratuite, et la plupart des projets découvrent laquelle ils ont mal choisie quand il y a déjà des milliers de textes.

Ce que coûte chaque option

Avec des clés, le code ne vous raconte plus rien. Vous ouvrez un template et vous voyez @lang('dashboard.empty_state_title'). Pour savoir ce qui s'affiche, il faut aller dans le fichier de langue, chercher la clé et revenir. Multipliez par chaque relecture, chaque changement de copy et chaque personne qui arrive sur le projet.

Et il y a un effet pire sur la durée : personne ne supprime de clés. Comme vous ne savez pas si dashboard.empty_state_title sert encore quelque part, elle reste. Un fichier de langue de trois ans contient toujours un tiers de déchets que personne n'ose toucher.

Avec le texte littéral, la moindre retouche casse la traduction. C'est le modèle de gettext, utilisé par PHP et aussi par WordPress : le msgid est le texte anglais. Cela fonctionne très bien jusqu'à ce que quelqu'un retire un point final. À cet instant l'identifiant a changé, la traduction existante n'est plus trouvée et l'utilisateur voit l'anglais sans que rien ne prévienne.

Sur des projets au copy vivant, c'est à dire presque tous, ce modèle signifie retraduire par accident plusieurs fois par an.

Ce que fait chaque technologie

gettext mise sur le texte littéral, et traîne depuis des décennies ce problème de fragilité. WordPress en hérite exactement avec son __('Texte', 'textdomain').

ICU MessageFormat, le standard qui se cache sous la moitié du web, fonctionne par clés et ajoute une syntaxe très puissante pour les pluriels et les genres. Il règle la grammaire, pas la lisibilité.

i18next en JavaScript, et par extension next-intl sur Next.js, fonctionnent aussi par clés avec des fichiers JSON imbriqués. L'expérience est la même : le code ne dit pas ce qui s'affiche.

Rails a une astuce intéressante avec les clés paresseuses, où t('.title') se résout selon la vue où vous êtes. Cela économise de la frappe mais ne vous dit pas le contenu.

Et Laravel vous laisse faire les deux : __('cle') ou __('Texte littéral'). Ce qui revient à vous demander lequel des deux problèmes vous préférez.

Écrire la clé et le texte dans le même appel

La sortie consiste à ne plus choisir. Vous écrivez la clé et le texte dans le même appel, et le système utilise la clé pour identifier et le texte comme valeur par défaut.

C'est ce que fait laratext, un paquet que nous maintenons et que nous utilisons en production :

@text('dashboard.empty_state_title', 'Vous n\'avez encore aucun bien')

Vous lisez le template et vous savez ce qui s'affiche. Vous cherchez la clé et vous la trouvez. Vous changez une virgule dans le texte et la traduction ne casse pas, parce que la clé n'a pas bougé. Le coût est d'écrire un peu plus à chaque appel, et en échange les deux problèmes disparaissent.

Les marqueurs et les pluriels suivent la syntaxe que vous connaissez déjà de Laravel :

text('items.count', 'Un bien|Vous avez :count biens', ['count' => $n]);
Andie recommande

source_locale est ce qui vous permet d'avoir les textes du code dans une langue et de faire tourner l'application dans une autre, le cas typique d'un projet écrit en anglais qui tourne en espagnol. En le déclarant, le scanner compare au bon original et ne voit que les textes qui ont réellement changé : chaque passage traduit le nouveau et rien d'autre. Une ligne de configuration, et c'est elle qui maintient la maintenance bon marché.

Ce qui change quand il y a dix mille textes

Avec vingt textes, n'importe quelle méthode fonctionne. Le problème apparaît à l'échelle, et c'est là que les coutures se voient.

Sur Abodara, une plateforme que nous avons construite, il y a 9 950 appels de texte répartis en trois langues. Sur Hominer, l'approche est la même. À ce volume, personne ne maintient les fichiers de langue à la main.

Ce qu'il faut, c'est un scanner qui lit le code et le compare à ce qui est traduit. Avec cela vous pouvez :

  • Voir ce qui manque avant de dépenser un centime, avec un passage à blanc qui n'appelle pas le traducteur
  • Traduire seulement le nouveau et signaler les textes qui ont changé, au lieu de tout refaire
  • Supprimer les clés qui ne servent plus, ce qui règle les déchets accumulés du début
  • Figer une clé précise quand la traduction automatique s'est trompée et que vous l'avez corrigée à la main

Une clé construite à la volée est invisible

Il y a un piège présent dans tous les systèmes à scanner, et il vaut mieux le connaître avant qu'après.

Une clé construite à la volée est invisible. Le scanner lit le code de façon statique, donc il ne voit pas ceci :

return text("activity.{$this->type->value}", $default);

Le problème n'est pas seulement qu'elle ne soit pas traduite : c'est qu'il ne peut pas non plus vous signaler qu'elle manque. La langue qui ne l'a pas affichera la clé brute à l'utilisateur et rien ne vous alertera. La version qui fonctionne est ennuyeuse et explicite :

return match ($this->type->value) {
    'created' => text('activity.created', ':actor a créé :item'),
    'deleted' => text('activity.deleted', ':actor a supprimé :item'),
};

Ce qui existe sur le marché et en quoi cela diffère

Il vaut mieux séparer trois problèmes que l'on confond en permanence, car chaque outil n'en résout qu'un.

Les textes de l'interface sont ceux de cet article. Laravel propose déjà les deux formes, fichiers PHP avec clé et fichiers JSON avec le texte original comme clé : le choix existe donc dès le premier jour, même si personne ne le fait consciemment. Hors PHP, gettext est le standard historique et a tranché pour la seconde il y a des décennies.

Le contenu de la base de données est un autre problème, et la référence y est spatie/laravel-translatable, qui stocke les traductions d'un attribut dans une colonne JSON. Il n'entre pas en concurrence avec le précédent : le nom d'un produit ne vit pas dans un fichier de langue.

La gestion du processus de traduction est couverte par des plateformes comme Crowdin, Lokalise ou Weblate, qui deviennent indispensables dès que la personne qui traduit ne touche pas au code. Elles fonctionnent avec l'une ou l'autre façon d'identifier, à condition d'avoir choisi avant d'avoir dix mille textes.

Laratext, celui que nous maintenons, s'attaque précisément à la couture du premier problème : écrire la clé et le texte dans le même appel, pour ne pas avoir à choisir entre une clé illisible et un littéral qui casse dès qu'on déplace une virgule. Si votre application compte deux cents textes, ce que fournit le framework suffit largement.

Et si vous ne travaillez pas avec Laravel

Le principe se transpose, même si les outils changent.

  • Sur Next.js, next-intl et i18next fonctionnent par clés. Vous pouvez récupérer de la lisibilité en utilisant le texte d'origine dans la clé, ou en gardant le fichier de la langue source à côté du code. Et il existe des scanners comme i18next-parser qui détectent ce qui manque.
  • Sur WordPress, vous êtes lié à gettext et à sa fragilité. La vraie parade est la discipline : figez les textes avant de traduire et traitez tout changement de copy pour ce qu'il est, une retraduction.
  • Sur n'importe quelle stack, le critère qui compte reste le même : que le code reste lisible, qu'un changement mineur ne casse rien et que vous puissiez savoir quelles clés sont en trop. Si votre système ne vous donne pas ces trois choses, vous le paierez dans deux ans.

Où est le code

Le paquet est publié sur Packagist, le code sur GitHub et la documentation du paquet couvre toutes les options. Il est gratuit et il est né de ce problème précis sur un projet client.

Si vous montez une application multilingue et que vous voulez vous épargner cette douleur dès le départ, sur applications web sur mesure et développement Laravel nous expliquons comment nous l'abordons. Et si vous préférez en parler directement, écrivez-nous.

Écrit par
Edu Lazaro
Edu Lazaro
Founder & Lead Developer en AndorraDev

Développeur full-stack avec plus de 15 ans d'expérience en Laravel, React, Node.js et architectures cloud. J'aide les entreprises en Andorre à construire leur présence digitale.

Partner de diseño · ionospace.
Besoin d'aide? ×
Andie by AndorraDev
Assistant IA + équipe humaine
Assistant IA d'AndorraDev
Andie
Bonjour! Je suis Andie, l'assistant IA d'AndorraDev. Comment puis-je vous aider? Si vous souhaitez parler avec Edu, il suffit de le demander.
17:27