Internationalisation Flutter

L’i18n Flutter avec les fichiers ARB et gen_l10n : mise en place, pluriels ICU et select, placeholders avec formatage, résolution de locale et synchronisation des fichiers ARB.

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

yaml
# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: any

flutter:
  generate: true
yaml
# 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 :

bash
flutter gen-l10n

Câblage

dart
import "package:flutter_gen/gen_l10n/app_localizations.dart";

MaterialApp(
  localizationsDelegates: AppLocalizations.localizationsDelegates,
  supportedLocales: AppLocalizations.supportedLocales,
  home: const HomePage(),
);
dart
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

json
{
  "@@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

json
{
  "messageCount": "{count, plural, =0{No messages} one{1 message} other{{count} messages}}",
  "@messageCount": {
    "placeholders": {
      "count": { "type": "int" }
    }
  }
}
dart
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

json
{
  "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 :

json
{
  "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 :

yaml
# 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 :

dart
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

  • supportedLocales non 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 one et other, 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.plist et 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 :

dart
// ✗ 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 :

dart
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 :

bash
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

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.