Tutti gli esempi qui sotto sono stati testati con Flutter 3.47.5 e flutter gen-l10n, con un template app_en.arb e una traduzione app_fr.arb.
Hai fretta? Genera il blocco plurale di ogni lingua direttamente nel browser: gratis, senza registrazione, niente viene caricato.
Cosa blocca gen-l10n
La sintassi ICU viene controllata in ogni file, non solo nel template. Un plurale senza ramo other interrompe la generazione, con il nome del file e la posizione dell'errore:
[app_fr.arb:itemsOne] ICU Syntax Error: Plural expressions must have an "other" case.
{count, plural, one{{count} article}}
^
Lo stesso vale per una parentesi graffa non chiusa. Questi errori li vedi subito; quelli che seguono passano senza avvisi.
Un placeholder perso passa in silenzio
Il template inglese dichiara un parametro:
"welcomeUser": "Welcome back, {name}!",
"@welcomeUser": { "placeholders": { "name": { "type": "String" } } }
La traduzione francese l'ha perso: "welcomeUser": "Bon retour parmi nous !". La generazione riesce, l'analyzer non dice nulla, e il metodo generato ignora semplicemente il suo argomento:
@override
String welcomeUser(String name) {
return 'Bon retour parmi nous !';
}
Niente va in crash, ma gli utenti francofoni non vedono mai il proprio nome. A volte è voluto (la frase suona meglio senza), di solito è un errore, e nessuno strumento standard distingue i due casi. Bisogna confrontare i placeholder di ogni traduzione con quelli del template; più in basso c'è uno script per farlo.
Un placeholder rinominato cambia l'API di tutte le locale
Il caso seguente è più strano: la traduzione usa un nome che il template non ha, per esempio {nombre} invece di {count}:
"reminder": "Il reste {nombre} articles dans {listName}."
gen-l10n continua a non segnalare nulla. Aggiunge nombre come parametro in più, sulla classe astratta, quindi in tutte le locale:
String reminder(String listName, int count, Object nombre);
L'errore compare dopo, quando compila ogni chiamata esistente: Too few positional arguments: 3 required, 2 given. Punta al tuo codice Dart, non al file ARB che l'ha causato. Se un metodo di localizzazione si ritrova all'improvviso un parametro Object, cerca quel nome nelle traduzioni.
In francese, one copre anche lo zero
Le categorie one, few, many… seguono le regole CLDR di ogni lingua. In inglese, come in italiano, one indica solo 1. In francese indica 0 e 1. Con questo messaggio:
"itemsOne": "{count, plural, one{{count} article} other{{count} articles}}"
| count | Inglese | Francese |
|---|---|---|
| 0 | 0 items | 0 article |
| 1 | 1 item | 1 article |
| 2 | 2 items | 2 articles |
"0 article" è francese corretto, quindi nessun problema. La trappola è altrove: gen-l10n trasforma =0 e =1 negli argomenti zero e one di Intl.pluralLogic. Un ramo =1 diventa quindi la categoria one, e in francese quella categoria si applica anche allo zero:
"exactOne": "{count, plural, =1{1 article restant} other{{count} articles restants}}"
Con count = 0, l'inglese mostra "0 items left" e il francese "1 article restant" (un elemento rimasto). Il testo è sbagliato e nessuno strumento lo segnala. La soluzione: aggiungi un ramo =0, oppure scrivi one{{count} article restant} così il numero mostrato è sempre giusto. Ogni lingua in cui one copre più del solo 1 ha lo stesso problema.
Apostrofi e use-escaping
Per impostazione predefinita, un apostrofo dritto è solo un carattere: "Don't forget the milk" funziona. Se attivi use-escaping: true in l10n.yaml (per scrivere parentesi graffe letterali), diventa il carattere di escape ICU e un apostrofo isolato interrompe la generazione:
[app_en.arb:apostrophe] ICU Lexing Error: Unmatched single quotes.
Don't forget the milk
^
A quel punto devi raddoppiarlo (Don''t) o usare l'apostrofo tipografico ’, che non è interessato. Su un progetto esistente, attivare quell'opzione significa ripassare tutte le lingue; le contrazioni inglesi e le elisioni italiane e francesi ti terranno occupato.
Un controllo da aggiungere alla CI
Il caso silenzioso è il placeholder perso. Questo script legge i placeholder dichiarati nel template e verifica che ogni traduzione li usi ancora:
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; // missing key: see 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} missing');
errors++;
}
}
}
}
if (errors > 0) exit(1);
}
Map<String, dynamic> _read(String path) => jsonDecode(File(path).readAsStringSync()) as Map<String, dynamic>;
Sull'esempio di questa pagina stampa lib/l10n/app_fr.arb · welcomeUser: {name} missing ed esce con un errore. I suoi limiti: guarda solo i placeholder dichiarati in @key.placeholders, e per un plurale verifica che la variabile compaia da qualche parte nel messaggio, non in ogni ramo. Le due trappole dei plurali viste sopra si intercettano meglio con un test che chiama i messaggi con 0, 1 e 2.