Aller au contenu
Blog
Méthode8 octobre 2026

Quatre langues, 884 clés, un seul dev

Grimoire Culinaire déclare quatre langues, mais seules deux sont complètes et la locale est verrouillée en français dans le code. Ce n'est pas une négligence : c'est un pattern de livraison assumé, la feature d'abord, la traduction quand elle paie.

L'illusion du multilingue gratuit

Quand j'ai démarré Grimoire Culinaire, mon app de recettes Flutter, j'ai créé quatre fichiers de traduction dès le premier jour : app_fr.arb, app_en.arb, app_es.arb, app_it.arb. Quatre langues déclarées, quatre marchés potentiels, pour le prix de quatre fichiers. Flutter rend ça tellement simple que ça ressemble à un cadeau.

Six mois plus tard, j'ai recompté les clés, script à l'appui, le jour même où j'écris ces lignes : FR 884 sur 884, EN 884 sur 884, ES 101 sur 884, IT 101 sur 884. Soit 783 chaînes vides par langue latine. Le fichier espagnol commence comme ça :

brigit@7sailjson
"appTitle": "",
"shoppingListTitle": "",
"addItem": "",
"editItem": "",

Le template qui impose la discipline

Le titre de l'app lui-même n'est pas traduit en espagnol. Voilà ce que vaut une langue déclarée sans plan pour la tenir. Le seul choix de départ que je défends sans réserve, c'est d'avoir fait du français la langue source :

  • Chaque nouvelle feature crée ses clés en français, le template fait autorité.
  • L'anglais est traduit dans la foulée, tant que le contexte est frais.
  • ES et IT restent quasi vides : la dette n'est pas cachée dans un backlog, elle est dans le repo, mesurable en une commande.
brigit@7sailyaml
arb-dir: lib/shared/l10n
template-arb-file: app_fr.arb

Le garde-fou brutal

Une app à moitié traduite est pire qu'une app pas traduite. Un utilisateur espagnol qui voit un écran en espagnol et des libellés français partout ailleurs ne conclut pas que la traduction est en cours : il conclut que l'app est cassée. Donc j'ai verrouillé, dans app.dart, ligne 450 :

Quatre langues déclarées, une seule servie. Et le backend raconte une troisième histoire : dans le seed Supabase, app.supported_locales vaut ["fr", "en"]. Trois niveaux de vérité, donc : quatre langues dans le client, deux promises par le serveur, une seule affichée. C'est moche sur le papier. En pratique, c'est le seul état honnête tant que la couverture n'est pas complète.

brigit@7saildart
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('fr'),
Plutôt une app 100 % française qu'une app espagnole à moitié traduite.

La feature d'abord, la traduction quand elle paie

Le détail révélateur, c'est quelles clés sont remplies en espagnol. Pas un socle : appTitle est vide, addItem et editItem sont vides. Les 101 clés remplies couvrent les flows récents : la capture vocale ("Toca para dictar tu receta"), l'OCR photo, la liste de courses, le parrainage, les codes promo.

L'explication est bête. Mes features récentes sont écrites avec une discipline que les anciennes n'avaient pas : les quatre langues d'un coup, au moment où le contexte est frais. Traduire une clé pendant qu'on écrit la feature coûte trente secondes. Revenir traduire 783 clés hors contexte, c'est un chantier qu'il faut planifier, relire et tester. La traduction suit la même loi que les tests ou la doc : marginale au fil de l'eau, monumentale en rattrapage.

Est-ce que l'espagnol arrivera naturellement, feature après feature ? Non. À ce rythme, les 783 clés historiques ne se rempliront jamais toutes seules. Le rattrapage sera un lot dédié, déclenché le jour où une raison business le justifiera. Je ne promets pas de date, et c'est assumé.

Ce que je retiens

  • Déclarer une langue coûte une ligne. La tenir coûte 884 clés. Le vrai livrable i18n n'est pas le fichier de traduction, c'est la décision de quand l'activer.
  • Une locale verrouillée en dur vaut mieux qu'une expérience à moitié traduite qui a l'air cassée.
  • Une dette visible et comptable (783 chaînes vides dans le repo) vaut mieux qu'une dette planquée dans un ticket.
  • Traduire au fil de l'eau coûte trente secondes par clé. En rattrapage, c'est un projet à part entière.