Claude Media
Claude CodeでFlutterのARBを多言語化 — gen-l10nで翻訳を検証する

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:any

pubspec.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.dart

arb-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-l10n

MaterialApp には 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を依頼の入力にすると、翻訳の対象範囲を毎回正確に絞れます。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/hooks
  2. docs.flutter.dev/ui/internationalization
この記事を共有:XはてブLinkedIn