Claude CodeでFlutterのARBを多言語化 — gen-l10nで翻訳を検証する
Flutterのgen-l10nで文字列をARBに移し、翻訳をClaude Codeに埋めさせる手順です。プレースホルダと複数形の破損をPostToolUse hookで検出する設定例つき。
FlutterアプリをClaude Codeで多言語化するときの分担は、訳文を書くのがClaude Code、壊れていないかを判定するのが flutter gen-l10n と小さな検査スクリプトです。ARBファイルは中身がJSONなので、{userName} のようなプレースホルダが消えたり、複数形の構文が崩れたりしても、翻訳文だけ眺めていると気づきにくくなります。
この記事では、l10n.yaml とテンプレートARBの用意、文字列のARB化、プレースホルダ・複数形・selectの書き方、翻訳の依頼方法、PostToolUse hookによる自動検証を順に示します。
gen-l10nが必要とする3つの設定
flutter gen-l10n を動かすための土台は、pubspec.yaml、l10n.yaml、テンプレートARBの3つです。Claude Codeに「多言語化して」と丸ごと頼む前に、この3つが揃っているかを先に見ておくと、生成が失敗したときの切り分けが楽になります。
まず依存関係です。flutter_localizations と intl を追加します。
flutter pub add flutter_localizations:"{sdk: flutter}" intl:anypubspec.yaml の flutter セクションには generate: true を足します。
flutter:
generate: true次にプロジェクト直下の l10n.yaml です。Flutterのドキュメントにある例は次の3行です。
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dartarb-dir がARBの置き場所、template-arb-file が生成の基準になるテンプレート、output-localization-file が出力されるDartファイル名です。arb-dir の既定値は lib/l10n、template-arb-file の既定値は app_en.arb です。
最後に lib/l10n/app_en.arb と、翻訳先の lib/l10n/app_ja.arb を置きます。最小例は次のとおりです。
{
"helloWorld": "Hello World!",
"@helloWorld": {
"description": "The conventional newborn programmer greeting"
}
}ドキュメントの例では、翻訳側のARBに同じキーと訳文だけを書き、@ で始まる説明のブロックはテンプレート側にあります。
生成は flutter pub get か flutter run で自動的に走ります。アプリを起動せずに同じファイルを作りたいときは flutter gen-l10n を直接実行します。
flutter gen-l10nMaterialApp には AppLocalizations.delegate と ...GlobalMaterialLocalizations.delegates を localizationsDelegates に入れ、supportedLocales には AppLocalizations.supportedLocales を渡せます。ロケールを手で並べる必要はありません。
既存の文字列をARBへ移す依頼の出し方
ハードコードされた Text('ようこそ') をARBに移す作業は、Claude Codeが得意な機械的な置換です。ただし依頼文に条件を書かないと、キー名がばらけたり、@キー の説明が抜けたりします。
CLAUDE.mdに規約を置いておくと、毎回の依頼で繰り返さずに済みます。次は書き方の一例です。
## ローカライズ規約(Flutter)
- UI文字列は lib/l10n/app_en.arb に追加し、Textに直書きしない
- キーは lowerCamelCase で、画面名から始める(例: loginTitle)
- 追加するキーには必ず @キー を付け、description を英語で書く
- 動的な値は placeholders に型を明示する(String / int / num / DateTime)
- 文字列を追加したら flutter gen-l10n を実行し、エラーが出ないことを確認する
- app_ja.arb は編集後に必ず同じキー・同じプレースホルダ名を保つ最後の行が、翻訳工程で効いてきます。
依頼は「lib/screens/login_page.dart の直書き文字列をARBに移して」のように、範囲を1ファイルか1画面に絞ります。Claude Codeはテンプレートを編集し、Dartのコードを AppLocalizations.of(context)!.loginTitle の形に書き換えます。公式の説明では、ARBのキーが生成クラスのgetter名になり、値がローカライズされたメッセージになります。
nullable-getter を false にすると AppLocalizations.of(context) の戻り値のnullチェックが生成側に入り、呼び出し側の ! が不要になります。既定は true です。この設定を変えるなら、既存の ! を外す作業も同じ依頼に含めます。
プレースホルダ・複数形・selectをARBに書く
翻訳の破損が起きるのは、ほとんどが動的な値を含むメッセージです。Flutterのgen-l10nがARBに認める構文は3種類あります。
単純なプレースホルダ
メッセージ中の {userName} のように、波括弧で囲んだ名前がプレースホルダです。名前は有効なDartの識別子でなければなりません。テンプレート側で、placeholders オブジェクトに型を定義します。
"hello": "Hello {userName}",
"@hello": {
"description": "A message with a single parameter",
"placeholders": {
"userName": {
"type": "String",
"example": "Bob"
}
}
}これで AppLocalizations.of(context)!.hello('John') のように、String 引数を取るメソッドが生成されます。日本語側は "hello": "{userName}さん、こんにちは" のように、同じ {userName} を訳文の好きな位置に置けます。語順が違う言語で位置を動かせるのが、文字列連結をやめる理由です。
複数形
複数形は plural の書式で書きます。
"nWombats": "{count, plural, =0{no wombats} =1{1 wombat} other{{count} wombats}}",
"@nWombats": {
"description": "A plural message",
"placeholders": {
"count": {
"type": "num",
"format": "compact"
}
}
}複数形のメッセージには、どう複数化するかを決める num 型のパラメータが必要です。分岐に使えるのは =0、=1、=2、few、many、other で、必須なのは other だけです。言語によって複数形の作り方が違うため、英語では不要な few や many が翻訳先では必要になる場合があります。翻訳をClaude Codeに任せるときは、この分岐の追加が起こりうることを依頼文に書いておきます。
select
文字列の値で訳を切り替えるのが select です。性別のある言語への対応で最もよく使われる書式です。
"pronoun": "{gender, select, male{he} female{she} other{they}}",
"@pronoun": {
"description": "A gendered message",
"placeholders": {
"gender": { "type": "String" }
}
}引数と分岐名の比較は大文字小文字を区別します。"Male" を渡すと other の分岐に落ちます。
波括弧をそのまま表示したいとき
メッセージに { や } を文字として入れたいときは、l10n.yaml に use-escaping: true を足します。有効にすると、単一引用符で囲んだ範囲はパースされません。引用符そのものを出すには、引用符を2つ続けます。
落とし穴
訳文のプレースホルダ名が1文字違うだけで生成が失敗する
翻訳先のARBでは、{userName} を訳文中にそのまま残す必要があります。{ユーザー名} のように訳してしまうと、テンプレートで宣言したプレースホルダと食い違います。波括弧の内側は翻訳しないこと。この一文をCLAUDE.mdに入れておくと、Claude Codeの翻訳でこの種の破損を減らせます。
翻訳を埋める依頼と、日本語側の注意
テンプレートが固まったら、翻訳先ARBの作成を依頼します。依頼文には次の3点を入れます。
- 対象ファイルを
lib/l10n/app_ja.arbに限定する - テンプレートにあって翻訳先にないキーだけを追加する
- 波括弧の内側と、
plural/selectの分岐名(otherなど)は変更しない
「ヘッダや @ ブロックを触らない」と書くのも有効です。説明をテンプレート側だけに置く運用なら、翻訳先に @キー が増えると二重管理になります。
同じ発想で、gettextの.poを扱う手順をClaude Codeでpoファイルを翻訳する手順にまとめています。ARBは検証に使う道具が msgfmt から flutter gen-l10n に変わるだけで、分担の考え方は共通です。ゲームエンジン側の例はGodotの翻訳対応にあります。
未翻訳キーの確認
どのキーがまだ訳されていないかは、gen-l10nが出してくれます。untranslated-messages-file を指定しない場合、未翻訳のメッセージの要約がコマンドラインに表示されます。指定すると、次の形式のJSONが書き出されます。
{ "ja": ["loginTitle", "loginButton"] }l10n.yaml にファイルの位置を足す例です。
untranslated-messages-file: build/untranslated.jsonこの形式なら、Claude Codeに「build/untranslated.json の ja に並んだキーだけ訳して」と頼めます。全文を毎回渡すより、対象が明確になります。
PostToolUse hookでARBの破損を検出する
翻訳が終わるたびに人が flutter gen-l10n を打つのは忘れやすい作業です。Claude CodeのPostToolUse hookに任せると、ARBを編集した直後に検証が走ります。
仕組みは次の3点です。
hookで押さえる3つの仕様
- PostToolUseは、ツール呼び出しが成功した直後に動き、
tool_inputにファイルのパスが入る - 終了コード0のとき、hookのstderrはClaudeに届かない。警告を伝えるには終了コード2で終える
- PostToolUseの終了コード2はツールを取り消さない。すでに実行済みで、stderrがClaudeに表示される
hookの対象は、settings.json の matcher でツール名として Edit|Write を指定します。Claude Codeの if フィールドは、1つのhookに1つの権限ルールしか書けず、&& や || では組み合わせられません。そこでパスの判定はスクリプトの中で行います。
.claude/settings.json の設定例です。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-arb.sh"
}
]
}
]
}
}.claude/hooks/check-arb.sh は、編集されたファイルが .arb のときだけ検査します。
#!/bin/bash
f=$(jq -r '.tool_input.file_path // empty')
case "$f" in *.arb) ;; *) exit 0 ;; esac
cd "$CLAUDE_PROJECT_DIR" || exit 0
python3 tools/check_arb.py lib/l10n || exit 2
flutter gen-l10n >&2 || exit 2最初に自作の tools/check_arb.py を動かし、次に flutter gen-l10n を動かします。どちらかが失敗すると終了コード2になり、stderrの内容がClaude Codeに返ります。Claude Codeはその内容を読んで、ARBを直しにいきます。
tools/check_arb.py は、テンプレートで宣言したプレースホルダが翻訳先の訳文に残っているかを調べます。
import json, pathlib, sys
d = pathlib.Path(sys.argv[1])
tpl = json.loads((d / "app_en.arb").read_text("utf-8"))
bad = []
for arb in sorted(d.glob("app_*.arb")):
if arb.name == "app_en.arb":
continue
tr = json.loads(arb.read_text("utf-8"))
for key, meta in tpl.items():
if not key.startswith("@") or key.startswith("@@"):
continue
name = key[1:]
if name not in tr:
continue
for ph in meta.get("placeholders", {}):
if "{" + ph not in tr[name]:
bad.append(f"{arb.name}: {name} に {{{ph}}} がない")
if bad:
print("\n".join(bad), file=sys.stderr)
sys.exit(1)"{" + ph で調べているのは、{count, plural, ...} のように名前の直後がカンマになる書式も拾うためです。このスクリプトが見るのは、プレースホルダ名の欠落だけです。plural の分岐や other の有無の検査は flutter gen-l10n の側が受け持ちます。
なお、hookが動くのはClaude CodeのEditやWriteでARBが書き換わったときです。Bash コマンドや外部のプロセスが同じファイルを書き換えた場合は、Edit|Write にマッチするPostToolUse hookは動きません。翻訳をスクリプトで一括生成する運用では、そのスクリプトの最後で check_arb.py を呼ぶ必要があります。
hookの全体像と、Dart側のコマンドをhookに載せる別の例は、Flutterのpub依存衝突をanalyzeで直す記事で扱っています。Flutter向けの公式プラグインが提供するスキルはDart and Flutterプラグインの解説にあります。
検証を通す運用で見ておく点
hookを入れたあとも、次の点は人が判断する領域です。
- 訳の自然さは、
flutter gen-l10nでは判定されない。構文が通ることと、読んで違和感がないことは別 required-resource-attributesを有効にすると、すべてのメッセージIDに対応する@属性が必須になる。既定では単純なメッセージにメタデータは不要だが、意味の文脈を読者に伝えるため付けることが強く勧められている。複数形のメッセージは、常に属性が必要- VS Codeでは
arb-editor拡張を入れると、ARBテンプレートの診断とクイックフィックスが使える - 中国語のように、言語コードだけでは字体や地域を区別できない言語は、
supportedLocalesにzh_Hansやzh_Hantのようなスクリプトコードまで並べる必要がある
iOSのApp Storeに対応言語を表示させたい場合は、Flutter側のローカライズとは別に、Xcodeのプロジェクトで対応言語を追加する作業が要ります。Claude Codeに頼めるのはARBとDartまでで、Xcodeの設定は画面操作です。
まとめ
ARBの多言語化は、訳文の生成をClaude Codeに任せ、構文とプレースホルダの整合を機械に任せる分担にすると、手戻りが減ります。CLAUDE.mdに「波括弧の内側は訳さない」と書き、PostToolUse hookで flutter gen-l10n の失敗をClaude Codeへ返す構成が、その最小形です。次に足すなら、untranslated-messages-file のJSONを依頼の入力にすると、翻訳の対象範囲を毎回正確に絞れます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。