以下の例はすべて、Flutter 3.47.5 と flutter gen-l10n で、テンプレート app_en.arb とフランス語訳 app_fr.arb を使って検証しています。
お急ぎですか?言語ごとの複数形ブロックをブラウザでそのまま生成できます。無料・登録不要で、何もアップロードされません。
gen-l10n が確実に止めるもの
ICU 構文は翻訳ファイルも含め、すべてのファイルでチェックされます。other の分岐がない複数形は、ファイル名とエラーの位置を示して生成を止めます。
[app_fr.arb:itemsOne] ICU Syntax Error: Plural expressions must have an "other" case.
{count, plural, one{{count} article}}
^
閉じていない波括弧も同様です。ここまではすぐに気づけますが、厄介なのは次のケースです。
抜けたプレースホルダーは何も言われずに通る
英語のテンプレートはパラメータを宣言しています。
"welcomeUser": "Welcome back, {name}!",
"@welcomeUser": { "placeholders": { "name": { "type": "String" } } }
フランス語訳ではそれが失われています:"welcomeUser": "Bon retour parmi nous !"。生成は成功し、アナライザーも何も言わず、生成されたメソッドは引数を無視するだけです。
@override
String welcomeUser(String name) {
return 'Bon retour parmi nous !';
}
クラッシュはしません。フランス語のユーザーに名前が表示されないだけです。意図的な場合(名前がないほうが自然な文になる)もありますが、たいていはミスであり、標準のツールではその 2 つを区別できません。各翻訳のプレースホルダーをテンプレートと比較する必要があります。そのためのスクリプトをこの後で紹介します。
名前を変えたプレースホルダーは全ロケールの API を変える
こちらはもっと奇妙です。翻訳がテンプレートにない名前、たとえば {count} の代わりに {nombre} を使っているとします。
"reminder": "Il reste {nombre} articles dans {listName}."
gen-l10n はやはり何も報告しません。nombre を余分なパラメータとして抽象クラスに追加するので、すべてのロケールに影響します。
String reminder(String listName, int count, Object nombre);
エラーが出るのは後になって、既存の呼び出し箇所をコンパイルするときです:Too few positional arguments: 3 required, 2 given. このエラーは原因となった ARB ファイルではなく、Dart のコードを指します。ローカライズメソッドに突然 Object パラメータが増えたら、翻訳の中からその名前を探してください。
フランス語では one がゼロも含む
one、few、many などのカテゴリは各言語の CLDR ルールに従います。英語では one は 1 だけを意味しますが、フランス語では 0 と 1 を意味します(日本語は other だけです)。次のメッセージの場合:
"itemsOne": "{count, plural, one{{count} article} other{{count} articles}}"
| count | 英語 | フランス語 |
|---|---|---|
| 0 | 0 items | 0 article |
| 1 | 1 item | 1 article |
| 2 | 2 items | 2 articles |
「0 article」はフランス語として正しいので、ここは問題ありません。落とし穴は別のところにあります。gen-l10n は =0 と =1 を Intl.pluralLogic の zero と one 引数に変換します。そのため =1 の分岐は one カテゴリになり、フランス語ではこのカテゴリがゼロにも適用されます。
"exactOne": "{count, plural, =1{1 article restant} other{{count} articles restants}}"
count = 0 のとき、英語は「0 items left」と表示しますが、フランス語は「1 article restant」(残り 1 件)と表示します。テキストが間違っているのに、何も検出されません。対策は、=0 の分岐を追加するか、one{{count} article restant} と書いて表示される数値が常に正しくなるようにすることです。one が 1 以外も含む言語はすべて同じ問題を抱えています。
アポストロフィと use-escaping
デフォルトでは、半角のアポストロフィはただの文字です:"Don't forget the milk" は問題なく動きます。l10n.yaml で use-escaping: true を有効にすると(波括弧をそのまま書くため)、アポストロフィは ICU のエスケープ文字になり、単独のアポストロフィで生成が止まります。
[app_en.arb:apostrophe] ICU Lexing Error: Unmatched single quotes.
Don't forget the milk
^
その場合は二重にする(Don''t)か、エスケープの影響を受けないタイポグラフィ用のアポストロフィ ’ を使います。既存のプロジェクトでこのオプションを切り替えると、すべての言語を見直すことになります。英語の短縮形やフランス語のエリジオンで手間がかかるでしょう。
CI に追加する価値のあるチェック
警告が出ないのは、抜けたプレースホルダーのケースです。このスクリプトはテンプレートで宣言されたプレースホルダーを読み込み、各翻訳がそれを使い続けているかをチェックします。
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>;
このページの例では、lib/l10n/app_fr.arb · welcomeUser: {name} missing と出力してエラーで終了します。制限:@key.placeholders で宣言されたプレースホルダーしか見ず、複数形では変数がメッセージのどこかに現れるかをチェックするだけで、すべての分岐にあるかは確認しません。上で紹介した複数形の 2 つの落とし穴は、メッセージを 0、1、2 で呼び出すテストで検出するのがいちばんです。