Każdy przykład poniżej został przetestowany na Flutter 3.47.5 i flutter gen-l10n, z szablonem app_en.arb i tłumaczeniem app_fr.arb.
Spieszysz się? Wygeneruj blok liczby mnogiej dla każdego języka od razu w przeglądarce: za darmo, bez rejestracji, nic nie jest wysyłane.
Co gen-l10n faktycznie blokuje
Składnia ICU jest sprawdzana w każdym pliku, nie tylko w szablonie. Liczba mnoga bez gałęzi other przerywa generowanie, z nazwą pliku i pozycją błędu:
[app_fr.arb:itemsOne] ICU Syntax Error: Plural expressions must have an "other" case.
{count, plural, one{{count} article}}
^
Tak samo przy niezamkniętym nawiasie klamrowym. Takie błędy widać od razu, w przeciwieństwie do opisanych niżej.
Zgubiony placeholder przechodzi bez słowa
Angielski szablon deklaruje parametr:
"welcomeUser": "Welcome back, {name}!",
"@welcomeUser": { "placeholders": { "name": { "type": "String" } } }
Francuskie tłumaczenie go zgubiło: "welcomeUser": "Bon retour parmi nous !". Generowanie się udaje, analizator milczy, a wygenerowana metoda po prostu ignoruje swój argument:
@override
String welcomeUser(String name) {
return 'Bon retour parmi nous !';
}
Aplikacja działa, tylko użytkownicy francuskojęzyczni nigdy nie zobaczą swojego imienia. Czasem to celowe (zdanie brzmi lepiej bez niego), zwykle to błąd, a żadne standardowe narzędzie nie odróżni jednego od drugiego. Trzeba porównać placeholdery każdego tłumaczenia z placeholderami szablonu; skrypt do tego znajdziesz niżej.
Przemianowany placeholder zmienia API wszystkich lokalizacji
Ten przypadek jest dziwniejszy. Tłumaczenie używa nazwy, której szablon nie ma, na przykład {nombre} zamiast {count}:
"reminder": "Il reste {nombre} articles dans {listName}."
gen-l10n nadal niczego nie zgłasza. Dodaje nombre jako dodatkowy parametr w klasie abstrakcyjnej, czyli we wszystkich lokalizacjach:
String reminder(String listName, int count, Object nombre);
Błąd pojawia się później, przy kompilacji każdego istniejącego wywołania: Too few positional arguments: 3 required, 2 given. Wskazuje na kod w Dart, a nie na plik ARB, który go spowodował. Jeśli metoda lokalizacji nagle dostaje parametr Object, poszukaj tej nazwy w tłumaczeniach.
Po francusku one obejmuje też zero
Kategorie one, few, many… wynikają z reguł CLDR każdego języka. Po angielsku one oznacza tylko 1. Po francusku oznacza 0 i 1. Po polsku one to tylko 1, ale potrzebujesz też few (2–4, 22–24…) i many (0, 5–21…). Z takim komunikatem:
"itemsOne": "{count, plural, one{{count} article} other{{count} articles}}"
| count | Angielski | Francuski |
|---|---|---|
| 0 | 0 items | 0 article |
| 1 | 1 item | 1 article |
| 2 | 2 items | 2 articles |
„0 article” to poprawny francuski, więc tu nie ma problemu. Pułapka jest gdzie indziej: gen-l10n zamienia =0 i =1 na argumenty zero i one funkcji Intl.pluralLogic. Gałąź =1 staje się więc kategorią one, a po francusku ta kategoria dotyczy też zera:
"exactOne": "{count, plural, =1{1 article restant} other{{count} articles restants}}"
Przy count = 0 angielski pokazuje „0 items left”, a francuski „1 article restant” (został jeden element). Tekst jest błędny, a nic tego nie zgłasza. Rozwiązanie: dodaj gałąź =0 albo napisz one{{count} article restant}, żeby pokazywana liczba zawsze była prawidłowa. Ten sam problem ma każdy język, w którym one obejmuje więcej niż 1.
Apostrofy i use-escaping
Domyślnie prosty apostrof to zwykły znak: "Don't forget the milk" działa. Jeśli włączysz use-escaping: true w l10n.yaml (żeby pisać dosłowne nawiasy klamrowe), staje się on znakiem escape ICU, a samotny apostrof przerywa generowanie:
[app_en.arb:apostrophe] ICU Lexing Error: Unmatched single quotes.
Don't forget the milk
^
Trzeba go wtedy podwoić (Don''t) albo użyć typograficznego apostrofu ’, którego to nie dotyczy. W istniejącym projekcie przełączenie tej opcji oznacza przejrzenie wszystkich języków; angielskie skróty i francuskie elizje przysporzą sporo pracy.
Kontrola, którą warto dodać do CI
Bez ostrzeżenia przechodzi zgubiony placeholder. Ten skrypt odczytuje placeholdery zadeklarowane w szablonie i sprawdza, czy każde tłumaczenie nadal ich używa:
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>;
Na przykładzie z tej strony wypisuje lib/l10n/app_fr.arb · welcomeUser: {name} missing i kończy się błędem. Jego ograniczenia: patrzy tylko na placeholdery zadeklarowane w @key.placeholders, a przy liczbie mnogiej sprawdza, czy zmienna występuje gdzieś w komunikacie, nie w każdej gałęzi. Obie pułapki liczby mnogiej opisane wyżej najlepiej wyłapać testem, który wywołuje komunikaty z wartościami 0, 1 i 2 (a dla polskiego także 5 i 22).