Alle voorbeelden hieronder zijn getest met Flutter 3.47.5 en flutter gen-l10n, met een template app_en.arb en een vertaling app_fr.arb.
Maak het meervoudsblok voor elke taal, direct in je browser: gratis, zonder account, er wordt niets geüpload.
Wat gen-l10n wel tegenhoudt
De ICU-syntaxis wordt in elk bestand gecontroleerd, niet alleen in de template. Een plural zonder tak other stopt de generatie, met de bestandsnaam en de positie van de fout:
[app_fr.arb:itemsOne] ICU Syntax Error: Plural expressions must have an "other" case.
{count, plural, one{{count} article}}
^
Hetzelfde geldt voor een niet-gesloten accolade. Zulke fouten zie je meteen; de gevallen hieronder vallen niet op.
Een weggevallen placeholder gaat er stilletjes door
De Engelse template declareert een parameter:
"welcomeUser": "Welcome back, {name}!",
"@welcomeUser": { "placeholders": { "name": { "type": "String" } } }
De Franse vertaling is hem kwijt: "welcomeUser": "Bon retour parmi nous !". De generatie slaagt, de analyzer zwijgt, en de gegenereerde methode negeert gewoon haar argument:
@override
String welcomeUser(String name) {
return 'Bon retour parmi nous !';
}
Er crasht niets. Franstalige gebruikers zien alleen nooit hun naam. Soms is dat bewust (de zin loopt beter zonder), meestal is het een fout, en geen standaardtool maakt onderscheid tussen die twee. Je moet de placeholders van elke vertaling met die van de template vergelijken; verderop staat een script daarvoor.
Een hernoemde placeholder verandert de API van elke locale
Deze is vreemder. De vertaling gebruikt een naam die de template niet kent, bijvoorbeeld {nombre} in plaats van {count}:
"reminder": "Il reste {nombre} articles dans {listName}."
gen-l10n meldt nog steeds niets. Het voegt nombre toe als extra parameter, op de abstracte klasse, dus in elke locale:
String reminder(String listName, int count, Object nombre);
De fout duikt later op, wanneer elke bestaande aanroep compileert: Too few positional arguments: 3 required, 2 given. Ze wijst naar je Dart-code, niet naar het ARB-bestand dat haar veroorzaakte. Krijgt een lokalisatiemethode plotseling een Object-parameter, zoek die naam dan op in je vertalingen.
In het Frans dekt one ook nul
De categorieën one, few, many… volgen de CLDR-regels van elke taal. In het Engels (en het Nederlands) betekent one alleen 1. In het Frans betekent het 0 en 1. Met dit bericht:
"itemsOne": "{count, plural, one{{count} article} other{{count} articles}}"
| count | Engels | Frans |
|---|---|---|
| 0 | 0 items | 0 article |
| 1 | 1 item | 1 article |
| 2 | 2 items | 2 articles |
"0 article" is correct Frans, dus daar is geen probleem. De valkuil zit ergens anders: gen-l10n zet =0 en =1 om naar de argumenten zero en one van Intl.pluralLogic. Een tak =1 wordt dus de categorie one, en in het Frans geldt die categorie ook voor nul:
"exactOne": "{count, plural, =1{1 article restant} other{{count} articles restants}}"
Met count = 0 toont het Engels "0 items left" en het Frans "1 article restant" (nog één item). De tekst klopt niet, en niets meldt het. De oplossing: voeg een tak =0 toe, of schrijf one{{count} article restant}, zodat het getoonde getal altijd klopt. Elke taal waarin one meer dan 1 dekt, heeft hetzelfde probleem.
Apostroffen en use-escaping
Standaard is een rechte apostrof gewoon een teken: "Don't forget the milk" werkt. Zet je use-escaping: true aan in l10n.yaml (om letterlijke accolades te schrijven), dan wordt hij het ICU-escapeteken en stopt een losse apostrof de generatie:
[app_en.arb:apostrophe] ICU Lexing Error: Unmatched single quotes.
Don't forget the milk
^
Je moet hem dan verdubbelen (Don''t) of de typografische apostrof ’ gebruiken, die er geen last van heeft. In een bestaand project betekent het omzetten van die optie dat je elke taal opnieuw moet nalopen; Engelse samentrekkingen, Franse elisies en Nederlandse meervouden als "foto's" houden je wel bezig.
Een controle die in CI thuishoort
Het stille geval is de weggevallen placeholder. Dit script leest de placeholders die in de template zijn gedeclareerd en controleert of elke vertaling ze nog gebruikt:
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>;
Voor het voorbeeld op deze pagina print het lib/l10n/app_fr.arb · welcomeUser: {name} missing en stopt het met een fout. De beperkingen: het kijkt alleen naar placeholders die in @key.placeholders zijn gedeclareerd, en voor een meervoud controleert het of de variabele ergens in het bericht voorkomt, niet in elke tak. De twee meervoudsvalkuilen hierboven vang je het best met een test die je berichten aanroept met 0, 1 en 2.