La localisation de Flutter repose sur les fichiers ARB et le package intl, et elle utilise ICU MessageFormat pour les pluriels et la sélection. Ce dernier point compte : cela signifie que les catégories de pluriel sont des catégories CLDR nommées plutôt qu’une notation maison, ce qui place Flutter en avance sur plusieurs frameworks web sur la partie la plus difficile à corriger après coup.
Mise en place
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
intl: any
flutter:
generate: true
# l10n.yaml
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
nullable-getter: false
nullable-getter: false vaut la peine d’être défini. Sans cela, chaque lookup s’écrit AppLocalizations.of(context)! avec une assertion de non-nullité, ce qui devient du bruit sur chaque site d’appel.
Puis :
flutter gen-l10n
Câblage
import "package:flutter_gen/gen_l10n/app_localizations.dart";
MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const HomePage(),
);
final l10n = AppLocalizations.of(context);
Text(l10n.dashboardTitle);
Les lookups sont des méthodes Dart générées, pas des clés de chaîne. C’est la meilleure propriété de ce système : une faute de frappe devient une erreur de compilation plutôt qu’une chaîne manquante à l’exécution, et renommer une clé est un refactor que l’analyzer peut vérifier.
Fichiers ARB
{
"@@locale": "en",
"dashboardTitle": "Dashboard",
"@dashboardTitle": {
"description": "Title of the main dashboard screen"
},
"welcome": "Welcome back, {name}",
"@welcome": {
"description": "Greeting shown on the dashboard",
"placeholders": {
"name": { "type": "String" }
}
}
}
Chaque message peut être suivi d’une entrée de métadonnées préfixée par @. La description est le champ le plus précieux du fichier — c’est ce que voit un traducteur, et c’est le mécanisme qui résout le problème d’ambiguïté où une chaîne comme Open pourrait être un verbe ou un adjectif. Remplissez-la. Voir le contexte visuel pour les traducteurs.
Pluriels
{
"messageCount": "{count, plural, =0{No messages} one{1 message} other{{count} messages}}",
"@messageCount": {
"placeholders": {
"count": { "type": "int" }
}
}
}
Text(l10n.messageCount(unread.length));
C’est du véritable ICU, donc les catégories sont les noms CLDR et la langue cible fournit l’ensemble dont elle a besoin — une traduction polonaise a one, few, many, other ; l’arabe en a six. Voir les règles de pluriel par langue.
La correspondance exacte =0 vaut la peine d’être utilisée là où l’état vide a besoin d’une formulation différente plutôt que simplement d’une forme de pluriel différente.
Select et genre
{
"profileUpdated": "{gender, select, female{She updated her profile} male{He updated his profile} other{They updated their profile}}",
"@profileUpdated": {
"placeholders": {
"gender": { "type": "String" }
}
}
}
other est obligatoire et doit se lire correctement pour des valeurs manquantes ou inattendues, pas seulement comme un filet de sécurité que personne n’a vérifié.
Placeholders formatés
Déclarez le type et le format plutôt que de pré-formater dans Dart :
{
"lastSync": "Last synced {date}",
"@lastSync": {
"placeholders": {
"date": { "type": "DateTime", "format": "yMMMd" }
}
},
"total": "Total: {amount}",
"@total": {
"placeholders": {
"amount": {
"type": "double",
"format": "compactCurrency",
"optionalParameters": { "symbol": "€" }
}
}
}
}
Le code généré appelle les formateurs d’intl avec la locale active, donc les séparateurs, le placement du symbole et les conventions de calendrier sortent corrects pour chaque locale. Formater dans Dart puis interpoler la chaîne obtenue fige au contraire les conventions d’une seule locale — voir qu’est-ce qu’une locale pour ce qui varie réellement.
Échappement
ICU traite les accolades comme de la syntaxe, donc une accolade littérale doit être échappée. Réglez :
# l10n.yaml
use-escaping: true
Ensuite '{' produit une accolade littérale. Sans ce flag, un message contenant une accolade littérale échoue à l’analyse d’une façon qui ne fait pas immédiatement penser à un problème d’échappement.
Le piège lié est l’apostrophe. En ICU, une apostrophe simple démarre une section entre guillemets, donc des traductions françaises ou italiennes pleines de contractions peuvent silencieusement désactiver tout ce qui suit la première. Doublez-la : L''utilisateur.
Résolution de locale
Flutter choisit dans supportedLocales en fonction de la locale de l’appareil. Vous pouvez surcharger la correspondance :
MaterialApp(
localeResolutionCallback: (deviceLocale, supported) {
if (deviceLocale == null) return supported.first;
for (final locale in supported) {
if (locale.languageCode == deviceLocale.languageCode) return locale;
}
return supported.first;
},
);
La résolution par défaut fait correspondre d’abord la langue puis le pays, ce qui est généralement correct. Cela vaut la peine d’être surchargé quand vous prenez en charge des variantes d’écriture — un lecteur en zh-Hant ne devrait pas silencieusement retomber sur zh-Hans, car les deux ne sont pas confortablement interchangeables.
Pour laisser les utilisateurs choisir une langue dans l’application, conservez la locale choisie dans votre gestion d’état et passez-la comme MaterialApp.locale, en surchargeant le réglage de l’appareil.
Ce qu’on oublie
supportedLocalesnon mis à jour quand une langue est ajoutée, si bien que l’ARB existe mais n’est jamais sélectionné.- Des pluriels utilisés pour des langues dont l’ARB n’a que
oneetother, parce que les fichiers cibles ont été générés en copiant la structure anglaise. - Les chaînes de plateforme — le nom de l’app iOS, les invites de permission et le texte de notification vivent dans
Info.plistet les ressources Android, entièrement hors de l’ARB. - Les messages d’erreur construits par concaténation de chaînes en Dart plutôt que comme des messages.
- Le formatage des dates et des nombres fait manuellement.
Droite-à-gauche et mise en page
Flutter gère le RTL mieux que la plupart des frameworks, à condition d’utiliser les widgets directionnels plutôt que les widgets physiques.
Directionality est réglé automatiquement à partir de la locale, et la mise en page suit — mais seulement pour les widgets qui la respectent :
// ✗ Physique : reste à gauche en arabe
padding: EdgeInsets.only(left: 16)
Align(alignment: Alignment.centerLeft)
// ✓ Directionnel : s'inverse correctement
padding: EdgeInsetsDirectional.only(start: 16)
Align(alignment: AlignmentDirectional.centerStart)
La règle est d’utiliser start et end plutôt que left et right partout, y compris dans BorderRadiusDirectional et PositionedDirectional. Une base de code écrite avec un alignement physique semble correcte jusqu’à la première locale RTL, moment auquel chaque écran doit être revu.
Les icônes qui indiquent une direction doivent aussi être mises en miroir — flèches de retour, chevrons de progression, annuler. Les icônes qui n’indiquent pas de direction — lecture multimédia, coches, logos — doivent rester inchangées. Transform.flip piloté par Directionality.of(context) gère le premier groupe.
Testez-le sans traduire quoi que ce soit : forcez Locale("ar") avec une surcharge Directionality et regardez les écrans. C’est de la pseudo-localisation appliquée à la mise en page, et elle révèle les problèmes avant même qu’aucun arabe n’existe.
Tests
Montez les widgets avec une locale explicite et les vrais delegates :
await tester.pumpWidget(MaterialApp(
locale: const Locale("fr"),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const Dashboard(),
));
Comme les lookups sont des méthodes générées, une clé manquante ne peut pas atteindre un test — elle échoue à la compilation. Ce que les tests doivent couvrir à la place, c’est que les formes de pluriel se résolvent correctement pour les nombres qui comptent (0, 1, 2, 5, 11, 21 couvrent la plupart des frontières de catégorie) et que la mise en page survit à des traductions longues.
Synchroniser les traductions
ARB est du JSON, donc ça se synchronise directement :
wti push # lib/l10n/app_en.arb envoyé
wti pull # fichiers ARB traduits reçus en retour
wti diff # ce que changerait un push
Puis flutter gen-l10n dans votre build pour que le Dart généré corresponde à l’ARB. Comme les lookups sont des méthodes générées, un fichier de traduction qui a dérivé par rapport au code produit une erreur de compilation plutôt qu’une surprise à l’exécution — ce qui est le bon mode d’échec et vaut la peine d’être exploité.
Push au merge sur votre branche désignée, pull chaque nuit dans une pull request ; voir les flux de travail de localisation basés sur Git.
Un détail spécifique à Flutter : les entrées de métadonnées préfixées par @ n’ont leur place que dans l’ARB modèle, pas dans les fichiers traduits. flutter gen-l10n lit les types, les formats et les descriptions depuis le fichier modèle, donc les dupliquer dans chaque locale crée plusieurs endroits où la même information peut diverger. Les fichiers ARB traduits ne doivent porter que les messages et l’en-tête @@locale, rien d’autre.
Frequently asked questions
- Qu’est-ce qu’un fichier ARB dans Flutter ?
- ARB (Application Resource Bundle) est un format basé sur JSON où chaque clé correspond à une chaîne de message, accompagnée en option d’une entrée @clé contenant des métadonnées — une description pour les traducteurs, ainsi que le type et le format de chaque placeholder.
- Comment ajouter la localisation à une application Flutter ?
- Ajoutez flutter_localizations et intl à pubspec.yaml, réglez generate : true sous la section flutter, créez l10n.yaml pointant vers votre répertoire ARB, puis exécutez flutter gen-l10n. La classe AppLocalizations générée est connectée à MaterialApp et lue avec AppLocalizations.of(context).
- Flutter prend-il en charge ICU MessageFormat ?
- Oui. Les messages ARB utilisent la syntaxe ICU pour le pluriel, le select et le genre, donc les catégories de pluriel sont des catégories CLDR nommées plutôt que des formes positionnelles. C’est un avantage important par rapport aux frameworks qui ont inventé leur propre notation de pluriel.
- Comment formater les nombres et les dates dans Flutter ?
- Déclarez le type du placeholder comme int, double ou DateTime dans les métadonnées @clé et donnez-lui un format tel que compactCurrency ou yMMMd. Le code généré appelle les formateurs du package intl avec la locale active, ce qui donne les bons séparateurs et le bon comportement de calendrier par locale.
Keep reading
-
ICU MessageFormat : un guide pratique
La syntaxe des pluriels, de la sélection par genre, du formatage des nombres et des dates dans une seule chaîne de message — avec les règles d’échappement et les erreurs que font systématiquement les traducteurs.
-
Règles de pluriel par langue : le guide CLDR complet
Un tableau de référence des catégories de pluriel CLDR pour 163 langues, plus pourquoi one ne veut pas dire 1 et comment chaque framework i18n attend que vous écriviez les règles.
-
React i18next : configuration et flux de travail
Configurer i18next dans une application React : namespaces, pluriels, interpolation, Trans pour le balisage imbriqué, chargement différé, et synchronisation des fichiers de traduction.
-
Traduire des fichiers JSON et .arb (documentation)
Comment WebTranslateIt analyse les structures JSON, y compris les fichiers .arb de Flutter.
Translate your app without the spreadsheet round-trip
WebTranslateIt reads the file formats and placeholder syntax described on this page, validates them as translators work, and syncs the results straight back into your repository.