Plurali ICU e placeholder: cosa lascia passare gen-l10n

Aggiornato il · LocalePilot

flutter gen-l10n rifiuta un messaggio ICU malformato, ma accetta senza dire nulla una traduzione che ha perso il suo placeholder, o un plurale che dirà "1 elemento" per zero. Ecco cosa controlla, cosa no, e uno script per colmare la lacuna.

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}}"
countIngleseFrancese
00 items0 article
11 item1 article
22 items2 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.

Domande frequenti

flutter gen-l10n controlla le traduzioni o solo il template?

Controlla la sintassi ICU di ogni file ARB. Non confronta i placeholder di una traduzione con quelli del template: un placeholder perso passa senza avvisi.

Perché il mio metodo di localizzazione ha un parametro Object in più?

Una traduzione usa un nome di placeholder che non esiste nel template, per esempio {nombre} invece di {count}. gen-l10n lo aggiunge come parametro in tutte le locale, e le chiamate esistenti smettono di compilare.

=1 o one in un plurale?

In ICU, =1 indica esattamente il valore 1, ma gen-l10n lo passa come categoria one, che in francese copre anche lo 0. Aggiungi un ramo =0 o mostra {count} nel ramo one.