ICU-Pluralformen und Platzhalter: was gen-l10n durchlässt

Aktualisiert am · LocalePilot

flutter gen-l10n lehnt eine fehlerhafte ICU-Nachricht ab, akzeptiert aber stillschweigend eine Übersetzung, die ihren Platzhalter verloren hat, oder eine Pluralform, die bei null „1 Artikel“ anzeigt. Was geprüft wird, was nicht, und ein Skript, das die Lücke schließt.

Alle Beispiele unten wurden mit Flutter 3.47.5 und flutter gen-l10n getestet, mit einem Template app_en.arb und einer Übersetzung app_fr.arb. Für eine deutsche Datei app_de.arb gilt dasselbe, bis auf die Besonderheit der Kategorie one weiter unten.

Wenn es schnell gehen soll: Erzeugen Sie den Plural-Block jeder Sprache direkt im Browser, kostenlos, ohne Anmeldung und ohne Upload.

Was gen-l10n tatsächlich blockiert

Die ICU-Syntax wird in jeder Datei geprüft, nicht nur im Template. Eine Pluralform ohne Zweig other bricht die Generierung ab, mit Dateiname und Position des Fehlers:

[app_fr.arb:itemsOne] ICU Syntax Error: Plural expressions must have an "other" case.
    {count, plural, one{{count} article}}
                    ^

Dasselbe gilt für eine nicht geschlossene Klammer. Diese Fehler sehen Sie sofort, die folgenden nicht.

Ein verlorener Platzhalter geht kommentarlos durch

Das englische Template deklariert einen Parameter:

"welcomeUser": "Welcome back, {name}!",
"@welcomeUser": { "placeholders": { "name": { "type": "String" } } }

Die französische Übersetzung hat ihn verloren: "welcomeUser": "Bon retour parmi nous !". Die Generierung gelingt, der Analyzer schweigt, und die generierte Methode ignoriert ihr Argument einfach:

@override
String welcomeUser(String name) {
  return 'Bon retour parmi nous !';
}

Nichts stürzt ab. Französischsprachige Nutzer sehen nur nie ihren Namen. Manchmal ist das gewollt (der Satz klingt ohne besser), meistens ist es ein Fehler, und kein Standardwerkzeug unterscheidet beides. Sie müssen die Platzhalter jeder Übersetzung mit denen des Templates vergleichen; ein Skript dafür finden Sie weiter unten.

Ein umbenannter Platzhalter ändert die API jeder Locale

Dieser Fall ist seltsamer. Die Übersetzung verwendet einen Namen, den das Template nicht hat, etwa {nombre} statt {count}:

"reminder": "Il reste {nombre} articles dans {listName}."

gen-l10n meldet weiterhin nichts. Es fügt nombre als zusätzlichen Parameter hinzu, in der abstrakten Klasse, also in jeder Locale:

String reminder(String listName, int count, Object nombre);

Der Fehler zeigt sich später, wenn die bestehenden Aufrufe kompilieren: Too few positional arguments: 3 required, 2 given. Er zeigt auf Ihren Dart-Code, nicht auf die ARB-Datei, die ihn verursacht hat. Wenn eine Lokalisierungsmethode plötzlich einen Object-Parameter bekommt, suchen Sie diesen Namen in Ihren Übersetzungen.

Im Französischen umfasst one auch die Null

Die Kategorien one, few, many… folgen den CLDR-Regeln jeder Sprache. Im Englischen und im Deutschen bedeutet one nur 1. Im Französischen bedeutet es 0 und 1. Mit dieser Nachricht:

"itemsOne": "{count, plural, one{{count} article} other{{count} articles}}"
countEnglischFranzösisch
00 items0 article
11 item1 article
22 items2 articles

„0 article“ ist korrektes Französisch, hier also kein Problem. Die Falle liegt woanders: gen-l10n wandelt =0 und =1 in die Argumente zero und one von Intl.pluralLogic um. Ein Zweig =1 wird also zur Kategorie one, und im Französischen gilt diese Kategorie auch für null:

"exactOne": "{count, plural, =1{1 article restant} other{{count} articles restants}}"

Mit count = 0 zeigt Englisch „0 items left“ und Französisch „1 article restant“ (1 Artikel übrig). Der Text ist falsch, und nichts meldet es. Die Lösung: Fügen Sie einen Zweig =0 hinzu, oder schreiben Sie one{{count} article restant}, damit die angezeigte Zahl immer stimmt. Jede Sprache, in der one mehr als nur 1 abdeckt, hat dasselbe Problem; im Deutschen tritt es nicht auf.

Apostrophe und use-escaping

Standardmäßig ist ein gerader Apostroph einfach ein Zeichen: "Don't forget the milk" funktioniert. Wenn Sie use-escaping: true in l10n.yaml aktivieren (um literale geschweifte Klammern zu schreiben), wird er zum ICU-Escape-Zeichen, und ein einzelner Apostroph bricht die Generierung ab:

[app_en.arb:apostrophe] ICU Lexing Error: Unmatched single quotes.
    Don't forget the milk
       ^

Dann müssen Sie ihn verdoppeln (Don''t) oder den typografischen Apostroph ’ verwenden, der nicht betroffen ist. In einem bestehenden Projekt heißt das Umschalten dieser Option, jede Sprache noch einmal durchzugehen; englische Kurzformen, französische Elisionen und deutsche Formen wie „geht's“ halten Sie auf Trab.

Eine Prüfung, die in die CI gehört

Am unauffälligsten ist der verlorene Platzhalter. Dieses Skript liest die im Template deklarierten Platzhalter und prüft, dass jede Übersetzung sie noch verwendet:

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>;

Für das Beispiel dieser Seite gibt es lib/l10n/app_fr.arb · welcomeUser: {name} missing aus und endet mit einem Fehler. Seine Grenzen: Es betrachtet nur die in @key.placeholders deklarierten Platzhalter, und bei einer Pluralform prüft es, dass die Variable irgendwo in der Nachricht vorkommt, nicht in jedem Zweig. Die beiden Plural-Fallen oben fangen Sie am besten mit einem Test ab, der Ihre Nachrichten mit 0, 1 und 2 aufruft.

Häufige Fragen

Prüft flutter gen-l10n die Übersetzungen oder nur das Template?

Es prüft die ICU-Syntax jeder ARB-Datei. Es vergleicht aber nicht die Platzhalter einer Übersetzung mit denen des Templates: Ein verlorener Platzhalter geht ohne Warnung durch.

Warum hat meine Lokalisierungsmethode einen zusätzlichen Object-Parameter?

Eine Übersetzung verwendet einen Platzhalternamen, den es im Template nicht gibt, zum Beispiel {nombre} statt {count}. gen-l10n fügt ihn in jeder Locale als Parameter hinzu, und bestehende Aufrufe kompilieren nicht mehr.

=1 oder one in einer Pluralform?

In ICU trifft =1 genau den Wert 1, aber gen-l10n gibt ihn als Kategorie one weiter, die im Französischen auch 0 abdeckt. Fügen Sie einen Zweig =0 hinzu oder zeigen Sie {count} im Zweig one an.