Claude CodeでGodotを翻訳対応にする — CSVとgettextの使い分け
Godotの翻訳はCSVとgettextの2通り。入口ごとにClaude Codeへ渡す指示と検証コマンドが変わる点を、CLAUDE.mdの書き方まで含めて示します。
翻訳データの入口は2つあり、検証の道具が違う
Godotが読み込める翻訳データは、スプレッドシート由来のCSVと、GNU gettextのPO/MOファイルの2種類です。どちらを選んでも、ゲーム側で文字列を呼ぶ関数は同じtr()です。違いが出るのは、翻訳データを作って更新する工程と、その結果を機械的に確かめる方法です。
Claude Codeに任せるときは、この違いが指示文の中身になります。CSVでは「キー表に行と列を足す」仕事が中心で、検証は列数・重複キー・文字コードです。gettextでは「ソース文字列を抽出して差分を取り込む」仕事が中心で、検証はmsgfmtとmsgmergeです。
入口で変わるClaude Codeの仕事
CSV
1つのファイルに、キーと全言語の列が並びます。Claude Codeが触るのは表の行と、キーを呼ぶ側のtr()です。重複キーや列の欠けは、自作の小さなスクリプトで拾います。
gettext
言語ごとに.poファイルを持ちます。Claude Codeが触るのはmsgstrと、ソース側の英語文字列です。構文と更新漏れは、gettextの標準コマンドで拾えます。
公式の説明で、2つの入口の長所と短所は次のように書き分けられています。
| 観点 | CSV | gettext |
|---|---|---|
| 翻訳者の道具 | CSV表計算ソフト | gettextPoeditなどの専用エディタ、WeblateやTransifex |
| Gitでの差分 | CSV全言語が1ファイルに入る | gettext言語ごとにファイルが分かれ、差分を追いやすい |
| 複数行の文字列 | CSVセル内の改行を引用符で囲む必要がある | gettextPO形式のほうが編集しやすい |
| 文字列の抽出 | CSVキーを手で表に書く | gettextエディタがシーンとスクリプトからPOTを生成する |
| 追加で要るもの | CSVなし(UTF-8のCSVを用意する) | gettextgettextコマンドのインストール |
翻訳会社からスプレッドシートで納品されるなら、CSVがそのまま入ります。翻訳支援のWebサービスを使う、言語ごとの差分をレビューしたい、長い台詞が多いといった条件が重なるなら、gettextのほうが扱いやすい構成です。Godotの公式は、ゲームでは文字列ごとに一意のIDを振る方式が一般的で、英語をキーにする方式は複数言語を同時に進めると大変になる、とも説明しています。キーをGREETのようなIDにするか、英語の原文にするかは、入口を決める前に固定しておく項目です。
どちらの入口でも先に固定するtr()の書き方
翻訳データの形式にかかわらず、ゲーム側のコードが崩れていると翻訳は成立しません。Claude Codeに画面やセリフを書かせる前に、次の4点を指示文に入れておくと手戻りが減ります。
プレースホルダーは名前付きのformatにする
tr("%s picked up the %s") % ["Ogre", "Sword"]の形は、語句の位置は変えられても順序は変えられません。語順が違う言語では足りなくなります。名前付きの形なら、翻訳者が順序まで選べます。
message.text = tr("{character} picked up the {weapon}") \
.format({character = "Ogre", weapon = "Sword"})Claude Codeは、文字列を+でつなぐ書き方に流れがちです。つないだ文は翻訳単位として分断されるので、「文は1つのtr()にまとめ、可変部分は{名前}にする」と書いておきます。
複数形と文脈は専用の引数を使う
数を含む文はtr_n()、同じ原文で意味が違う語はtr()の第2引数に文脈を渡します。
label.text = tr_n("There is %d apple", "There are %d apples", n) % n
button.text = tr("Close", "Actions")最初は日本語だけの対応でも、他言語へ広げるときにコードを直さずに済みます。この2つは、後述するCSVの?plural・?context列、gettextのmsgid_plural・msgctxtとそれぞれ対応します。
翻訳したくないラベルは自動翻訳を切る
LabelやButtonは、テキストが翻訳キーと一致すると自動で翻訳されます。プレイヤー名を表示するラベルがたまたまキーと同じ文字列だと、名前が翻訳されてしまいます。対策はインスペクターのAuto Translate > ModeをDisabledにすることで、シーンファイル(.tscn)を編集するClaude Codeには、この設定を残す対象のノードを指定します。
動的に組み立てた文字列をtr()に渡さない
gettextの入口では、特に重要です。エディタのPOT生成は、引数が定数の文字列でないとその式を評価できず、無視します。tr("GAME_STATUS_%d" % index)のような書き方は、公式のコード例に出てくる形ですが、POTには載らない書き方に当たります。CSVの入口なら、キーを手で表に書くので動きます。入口とコードの書き方は、セットで決める必要があります。
CSVの入口 — Claude Codeに渡す指示と検証
形式の決まりをそのまま指示にする
CSVの1行目はkeysと言語コードの列です。言語コードは対応ロケールか、先頭が_のコメント列でなければなりません。キーは一意で、大文字小文字を区別します。左上のセルは無視されます。
keys,en,ja
GREET,"Hello, friend!",こんにちは
QUOTE,"""Hello"" said the man.",「こんにちは」男は言いましたカンマ・改行・二重引用符を含む文は二重引用符で囲み、文中の二重引用符は2つ重ねます。Claude Codeに行を足させるとき、この規則が崩れる場面が検証の主対象です。文字コードは、BOMなしのUTF-8である必要があります。ExcelはCSVを既定でANSIで保存するため、公式はLibreOfficeかGoogle Sheetsを勧めています。翻訳者からExcel経由で戻ってきたファイルを、Claude Codeに取り込ませる前に確認する場面が現実的です。
Godot 4.6からは、CSVに?pluralと?contextの列を足せます。ただし、この2つを使う行ではLabelなどの自動翻訳が効かず、tr()かtr_n()で手動翻訳する必要があります。キー表を広げる前に、バージョンと自動翻訳の可否を指示へ入れます。
公式の検証手段は無いので、小さな検査を作らせる
CSVにはmsgfmtに相当するコマンドがありません。そこで、列数・重複キー・BOMを見る検査スクリプトをClaude Codeに書かせて、編集のたびに実行させます。次は例示用の最小形です。
import csv, collections, sys
path = sys.argv[1]
raw = open(path, "rb").read()
if raw.startswith(b"\xef\xbb\xbf"):
print("BOM付きUTF-8です")
rows = list(csv.reader(open(path, encoding="utf-8")))
head = rows[0]
count = collections.Counter(r[0] for r in rows[1:] if r)
print("重複キー:", [k for k, n in count.items() if n > 1])
print("列数不一致:", [r[0] for r in rows[1:] if r and len(r) != len(head)])意図的に壊した3行のCSV(GREETを2回書き、BYEの行だけ列を1つ欠かした)に流すと、重複キーにGREET、列数不一致にBYEが出ました。列が欠けた行は、Godot側でも空の翻訳になりうる箇所なので、取り込み前に止める意味があります。
取り込んだ後に、登録を確かめる
CSVを.csvのまま置くと、Godotが翻訳として取り込み、隣に圧縮した翻訳リソースを作ります。ここで、公式のCSVのページ内で書き方がずれています。そのページには、取り込みで翻訳がproject.godotの読み込み一覧に追加されるという説明と、取り込んだだけではプロジェクトの翻訳元として登録されないという説明が、同じページに並んでいます。後者の説明には、Project > Project Settings > Localization > Translationsで登録する手順への案内が付いています。
どちらの挙動になるかを推測で決めず、結果を見て確かめる方針が安全です。キーが登録されていないと、tr()は変換せず、キー文字列がそのまま表示されます。画面にGREETと出るだけなので、気づきにくい失敗です。ヘッドレスで次のようなスクリプトを走らせ、言語を切り替えて出力を読ませます。
extends SceneTree
func _init():
for loc in ["en", "ja"]:
TranslationServer.set_locale(loc)
print(loc, ": ", tr("GREET"))
quit()godot --headless --path . -s res://tests/i18n_check.gd出力がja: GREETのようにキーのままなら、登録漏れかキーの綴り違いです。ja: こんにちはなら通っています。Godotをヘッドレスで動かす基本はClaude CodeでGodotゲーム開発を始める手順にあります。
gettextの入口 — POTの生成だけは人の操作が要る
流れと、Claude Codeが担当できる範囲
gettextの流れは次の順です。
gettextで翻訳を回す順序
- 1
POTを生成する
Project > Project Settings > Localization > Template Generationにシーンとスクリプトを追加し、Generateを押します。公式の手順はエディタの操作として書かれているため、ここは人が行います。 - 2
言語ごとのPOを作る
localeディレクトリでmsginit --no-translator --input=messages.pot --locale=jaを実行すると、ja.poができます。 - 3
msgstrを埋める
Claude Codeに翻訳案を書かせる工程です。翻訳の質は、人が見て確認します。
- 4
構文を検査し、Godotに登録する
msgfmt ja.po --checkで構文を確かめ、Localization > Translationsに.poを追加します。ロケールはファイル内のLanguageヘッダーから決まります。 - 5
文字列を増やしたらPOTを作り直し、msgmergeで取り込む
POTを再生成しないと、追加した文字列は翻訳対象になりません。
POTの再生成をClaude Codeが忘れるのが、この入口の典型的な抜けです。CLAUDE.mdに「tr()の文字列を増やしたら、POT再生成が必要なので作業の最後に知らせる」と書いておきます。
gettextの標準コマンドで出た結果
GNU gettext-tools 1.0で、小さなPOTから流れを試しました。msginitで作ったja.poには、Plural-Forms: nplurals=1; plural=0;が日本語向けに入りました。翻訳済みのStart gameをStart the gameに変えたPOTでmsgmerge --update --backup=none ja.po messages.potを実行すると、翻訳が残ったまま#, fuzzyが付きました。
#, fuzzy
msgid "Start the game"
msgstr "ゲーム開始"Godotの公式は、fuzzyの付いた文字列は、翻訳を直してfuzzyの行を消すまで読み込まれないと説明しています。Claude Codeに更新を任せるとき、fuzzyを翻訳の確認なしに消すと、古い訳が画面に出ます。「fuzzyの行は、msgstrを原文に合わせて直した後に限って消す」と指示します。作業後にgrep -c fuzzy locale/*.poを実行させ、残数を報告させる方法もあります。
msgfmt --checkは、同じmsgidを2回書いた.poでduplicate message definitionの致命的エラーを出し、終了コードは1でした。一方、PO-Revision-Dateヘッダーが無いだけの.poには警告を出しますが、終了コードは0でした。検査の合否を終了コードだけで読ませると、警告を見落とします。出力全体を読ませる指示が要ります。
抽出されるコードの書き方と、翻訳者向けコメント
エディタのPOT生成は、tr()・tr_n()の呼び出しに加え、textやtooltip_textへの代入なども拾います。引数が定数の文字列であることが条件です。不要な文字列は# NO_TRANSLATE、翻訳者への注意は# TRANSLATORS:のコメントで書きます。
$CharacterName.text = "???" # NO_TRANSLATE
item.text = "Tool" # TRANSLATORS: Up to 10 characters.文字数制限のような制約をTRANSLATORS:に書いておけば、Claude Codeが訳を書くときの条件にもなります。
CLAUDE.mdには入口ごとの検証コマンドを書く
どちらの入口でも、書く内容は「触ってよい場所」「使う関数の形」「終わる前に走らせるコマンド」です。CLAUDE.mdは、1ファイル200行未満が目安です。i18nだけで長くなるなら、pathsを指定したルールファイルに分けると、翻訳ファイルを触るときだけ読み込まれます。
## 翻訳(i18n)
- 画面に出す文字列は必ず tr() か tr_n() を通す。+ で連結しない
- プレースホルダーは {name} 形式と .format() を使う
- tr() の引数は定数の文字列だけにする(動的な組み立て禁止)
- 翻訳ファイルを編集したら、次を実行して出力を読む
- CSV: python3 tools/check_csv.py locale/game.csv
- PO: msgfmt locale/ja.po --check
- 登録確認: godot --headless --path . -s res://tests/i18n_check.gd
- fuzzy の行は、msgstr を直した後にだけ消す
- Project Settings の Locale > Test は空のままにするmsgmerge --updateは.poを直接書き換えるので、コマンドの許可ルールを足す場合も、Bash(msgfmt *)のように検査用だけを許可するほうが範囲を狭くできます。
出荷前の確認と、人がエディタで決める設定
翻訳の確認方法は、公式が3つ挙げています。
Project Settings > General > Internationalization > LocaleのTestにロケールコードを入れる(詳細設定の表示が必要)- エディタの
View > Preview Translationで言語を選ぶ - コマンドラインで
godot --language frのように起動する
Claude Codeが使いやすいのは、3つ目です。ただし、ヘッドレスでは描画された文字は見えないので、前節のtr()の出力で文字列を、画面は人の目で確かめる分担になります。
落とし穴
Testプロパティを入れたままコミットすると、全員の起動言語が固定される
Locale > Testはプロジェクト設定なので、空でない値のままだとバージョン管理に出ます。Claude Codeに動作確認を任せた後は、この値が空に戻っているかをコミット前の差分で見ます。
文字列が長くなって枠に収まらないかは、詳細設定の疑似ローカライズで確かめられます。ローカライズ対象の文字列が長くなり、一部の文字がアクセント付きに置き換わります(Hello world, this is %s!が[Ĥéłłô ŵôŕłd́, ŧh̀íš íš %s!]になる)。tr()を通していない文字列は変換されないので、翻訳漏れの発見にも使えます。
フォントの確認も要ります。Godotの既定のフォントはLatin-1の一部しか持たず、公式はロシア語や中国語を例に、文字が出ないときはフォントを替えるよう案内しています。日本語を出荷するなら、同じ確認が必要です。
コードでは完結しない設定もあります。次はエディタで人が決める項目です。
エディタ側で決める翻訳まわりの設定
リソースの差し替え
Remapsタブで、ロケールごとに画像や音声を差し替えます。ダイナミックフォントは対象外で、フォールバックの仕組みを使います。プロジェクト名の翻訳
Application > Configのローカライズ可能な文字列として追加します。書き出し後のアプリ名になります。分かち書きしない言語の改行
分かち書きしない言語向けの改行データは、書き出しに含まれません。
Include Text Server Dataをオンにする設定があり、データは約4MBです。起動時の言語
OS.get_locale_language()で端末の言語を取り、対応していなければFallbackかenになります。ゲーム内で言語を変えられる設定も用意します。
まとめ
入口の選択は、翻訳をどこから受け取るかで決まります。スプレッドシートで受け取るならCSV、翻訳支援のサービスとGitを軸にするならgettextです。どちらの場合も、先に固定するのはtr()の書き方で、Claude Codeには検証コマンドと一緒に渡します。キーの設計と自動翻訳の挙動は、CLAUDE.mdに書いた規約を検証コマンドで繰り返す進め方と同じ型で回せます。Docusaurusで多言語サイトを作る場合の考え方は、Claude CodeでDocusaurusサイトを作る手順にまとめています。