Tous les exemples ci-dessous ont été testés avec Flutter 3.47.5 et flutter gen-l10n, avec un fichier modèle app_en.arb et une traduction app_fr.arb.
Générez le bloc plural de chaque langue directement dans le navigateur avec un outil gratuit, sans compte et sans envoi de données.
Ce que gen-l10n bloque
La syntaxe ICU est vérifiée dans tous les fichiers, pas seulement dans le modèle. Un pluriel sans branche other arrête la génération, avec le nom du fichier et la position de l'erreur :
[app_fr.arb:itemsOne] ICU Syntax Error: Plural expressions must have an "other" case.
{count, plural, one{{count} article}}
^
Une accolade non fermée arrête la génération de la même façon. Ces erreurs se voient tout de suite ; les cas suivants sont plus difficiles à repérer.
Un placeholder oublié passe sans un mot
Le modèle anglais déclare un paramètre :
"welcomeUser": "Welcome back, {name}!",
"@welcomeUser": { "placeholders": { "name": { "type": "String" } } }
La traduction française l'a perdu : "welcomeUser": "Bon retour parmi nous !". La génération réussit, l'analyseur ne dit rien, et la méthode générée ignore tout simplement son argument :
@override
String welcomeUser(String name) {
return 'Bon retour parmi nous !';
}
L'application ne plante pas, mais l'utilisateur francophone ne voit jamais son prénom. Parfois c'est voulu (une tournure plus naturelle sans le nom), le plus souvent c'est un oubli, et aucun outil standard ne fait la différence. Il faut comparer les placeholders de chaque traduction avec ceux du modèle : un script pour ça plus bas.
Un placeholder renommé modifie l'API de toutes les langues
Autre cas : la traduction utilise un nom qui n'existe pas dans le modèle, par exemple {nombre} au lieu de {count}.
"reminder": "Il reste {nombre} articles dans {listName}."
gen-l10n ne signale toujours rien. Il ajoute nombre comme paramètre supplémentaire, dans la classe abstraite et donc dans toutes les langues :
String reminder(String listName, int count, Object nombre);
L'erreur arrive plus loin, à la compilation de chaque appel existant : Too few positional arguments: 3 required, 2 given. Le message pointe vers votre code Dart, pas vers le fichier ARB fautif. Si une méthode de localisation gagne soudain un paramètre de type Object, cherchez le nom en question dans les traductions.
En français, one couvre aussi zéro
Les catégories one, few, many… suivent les règles CLDR de chaque langue. En anglais, one ne vaut que pour 1. En français, il vaut pour 0 et 1. Avec ce message :
"itemsOne": "{count, plural, one{{count} article} other{{count} articles}}"
| count | Anglais | Français |
|---|---|---|
| 0 | 0 items | 0 article |
| 1 | 1 item | 1 article |
| 2 | 2 items | 2 articles |
« 0 article » est correct en français. Le problème vient des valeurs exactes : gen-l10n transforme =0 et =1 en arguments zero et one de Intl.pluralLogic. Une branche =1 devient donc la catégorie one, et en français elle s'applique aussi à zéro :
"exactOne": "{count, plural, =1{1 article restant} other{{count} articles restants}}"
Avec count = 0, l'anglais affiche « 0 items left » et le français « 1 article restant ». Le texte est faux et rien ne le signale. Pour corriger, ajoutez une branche =0 ou écrivez one{{count} article restant} pour que le nombre affiché soit toujours le bon.
Les apostrophes et use-escaping
Par défaut, l'apostrophe droite est un caractère comme un autre : "N'oubliez pas le lait" fonctionne. Si vous activez use-escaping: true dans l10n.yaml (pour écrire des accolades littérales), elle devient un caractère d'échappement ICU, et une apostrophe seule arrête la génération :
[app_fr.arb:apostrophe] ICU Lexing Error: Unmatched single quotes.
N'oubliez pas le lait
^
Il faut alors la doubler (N''oubliez) ou utiliser l'apostrophe typographique ’, qui n'est pas concernée. Sur un projet existant, activer l'option oblige à repasser sur toutes les langues ; le français sera le plus touché.
Un contrôle à ajouter en CI
Le cas silencieux est le placeholder oublié. Ce script lit les placeholders déclarés dans le modèle et vérifie qu'ils apparaissent dans chaque traduction :
import 'dart:convert';
import 'dart:io';
void main() {
final template = _read('lib/l10n/app_en.arb');
var errors = 0;
for (final file in Directory('lib/l10n').listSync().whereType<File>()) {
if (!file.path.endsWith('.arb') || file.path.endsWith('app_en.arb')) continue;
final translation = _read(file.path);
for (final key in template.keys.where((k) => !k.startsWith('@'))) {
final text = translation[key];
if (text is! String) continue; // clé absente : voir untranslated-messages-file
final meta = template['@$key'] as Map<String, dynamic>?;
final declared = (meta?['placeholders'] as Map<String, dynamic>?)?.keys ?? const <String>[];
for (final name in declared) {
if (!RegExp(r'\{\s*' + name + r'\s*[,}]').hasMatch(text)) {
stderr.writeln('${file.path} · $key : {$name} absent');
errors++;
}
}
}
}
if (errors > 0) exit(1);
}
Map<String, dynamic> _read(String path) => jsonDecode(File(path).readAsStringSync()) as Map<String, dynamic>;
Sur l'exemple de cette page, il affiche lib/l10n/app_fr.arb · welcomeUser : {name} absent et sort en erreur. Ses limites : il ne regarde que les placeholders déclarés dans @clé.placeholders, et pour un pluriel il vérifie que la variable apparaît quelque part dans le message, pas dans chaque branche. Pour le reste, les deux pièges de pluriel ci-dessus se repèrent à la relecture, avec un test qui appelle vos messages pour 0, 1 et 2.