Godotの翻訳CSV運用 — 5回目のアップデートでも壊れない進め方
Godotで最初の翻訳を動かすのは、たいてい半日で終わります。CSVを追加してインポートさせ、いくつかの文字列をtr()で包めば、メニューの言語が切り替わる。ここはエンジン側がよくできていて、実際に簡単な部類です。
壊れるのはその後です。シナリオ担当がセリフを書き直した2回目。翻訳者から列の並びが変わったファイルが返ってきた1回目。フランス語で遊んでみたらアイテム名の3割がそもそも表に載っていなかった日。どれもエンジンの問題ではなく、その周りに組んだ運用の問題です。
この記事はその運用の話です。CSVとgettextのどちらを選ぶか、原本をどこに置くか、Godotが自動で訳してくれる範囲と黙って素通りする範囲、テキスト以外のアセットの扱い、そして更新のたびに既存の翻訳を失わないための手順を扱います。
CSVかgettext(PO)か — 判断基準は「誰がファイルを開くか」
GodotはCSV形式の翻訳テーブルもgettextのPOファイルも読み込めます。エンジン側はどちらでも構わないので、判断は一点に絞れます。そのファイルを開いてテキストを直す人は誰か、です。
CSVは全言語が1枚のグリッドに収まります。1行=1キー、1列=1ロケール。自分と少人数の共同編集者が直接テキストを触る間は、この形が圧倒的に扱いやすい。空のセルがそのまま未翻訳を意味するので網羅状況が一目で分かり、差分も「変わった行のリスト」として読めます。弱いのはグリッドで表現しにくいもの、つまり翻訳者向けの補足メモ、改行を含むテキスト、言語ごとに形の異なる複数形です。
POファイルは1言語1ファイルですが、CSVにない構造を持っています。翻訳者コメント、参照元の位置情報、同じ原文を文脈によって訳し分けるためのコンテキスト、言語ごとの複数形ルール、原文が変わったときに「要確認」を立てるフラグ。そして業務用の翻訳ソフトはほぼすべてPOをそのまま開けるので、翻訳を仕事にしている人に依頼するなら先方から指定されるのはこちらです。代償として全言語を横並びで見られなくなり、管理するファイルはロケールの数だけ増えます。
現実的な既定路線は、自分だけがテキストを触っているうちはCSV、外部の翻訳者にファイルを渡す段階、あるいは複数形の扱いが必要になった段階でPOへ、という組み立てです。GodotはプロジェクトからPOTテンプレートを生成できるので、乗り換えは書き直しにはなりません。生成の設定はプロジェクト設定のローカライズの項目にあります。手順は使っているバージョンの公式ドキュメントで確認してください。
key,en,ja,fr ui.menu.new_game,New game,はじめから,Nouvelle partie ui.menu.continue,Continue,つづきから,Continuer dlg.inn.greet,"Welcome, traveller.","ようこそ、旅の方。","Bienvenue, voyageur."
原本は表、インポート結果はビルド生成物 — そして列ヘッダーがロケール一覧
Godotが翻訳CSVをインポートすると、ロケール列ごとに翻訳リソースが生成されます。この生成物は、コンパイル済みシェーダーやインポート済みテクスチャとまったく同じ意味で派生物です。人間が編集するファイルはCSVのほうだけ。リポジトリ上でもその区別が見えるようにしておくと、誰かが誤字を間違った場所で直す事故が起きません。
表は1枚の巨大ファイルにせず、領域で分けます。UI文字列、会話、アイテム・スキル名、クレジット。Godotは複数の翻訳ファイルを同時に読み込めますし、分割の効果は2つあります。差分が小さく読めるものになること、そして翻訳者にゲーム全体ではなく必要なファイルだけを渡せることです。守るべき制約は1つ、キーの検索はグローバルなので、ファイルをまたいでキーが一意である必要があります。キーに領域のプレフィックスを付けておけば自動的に満たせます。
CSVインポータ固有の罠が1つあります。表計算ソフトの感覚で作業していると必ず踏むもので、キー列より後ろの列はすべてロケールとして扱われる、という挙動です。親切のつもりで「状態」「メモ」列を足すと、statusという名前の言語の翻訳セットが生まれます。作業用のメタ情報はインポート対象のファイルの外に置いてください。別シート、別ファイル、あるいはPOに移行済みならコメント欄が置き場所になります。
つまり列ヘッダーは飾りではありません。それぞれが実行時に設定したロケールと突き合わされるロケール名で、エンジンが認識できない綴りのヘッダーはエラーになりません。誰も選べない言語ができあがり、テスターから「切り替えても何も変わらない」と報告が来ます。表計算ソフトや翻訳者が使った綴りではなく、Godot自身が使う綴りに合わせること。そしてデバッグビルドの起動時に、読み込まれたロケールの一覧を1回だけ出力すること。TranslationServerは現在のロケールと実際に読み込まれたロケールの集合の両方を返すので、出荷したつもりの言語リストと突き合わせれば、綴り間違いは1行のコードで即座に見つかります。
ついでに、未翻訳の文字列がどう見えるべきかも明示的に決めておきます。フォールバックを設定しない場合、見つからないキーはキー文字列そのものが表示されます。プレイヤーの前に出ると見苦しいものの、開発中は抜けを見逃しようがないという利点があります。フォールバックロケールを設定すると、抜けは静かに原文の言語で埋まります。出荷時には安全ですが、気づくのは格段に難しくなります。開発ビルドではキー表示、リリースビルドではフォールバック、と使い分けると、それぞれが役に立つ場面で両方の利点を取れます。
Godotが自動で訳す範囲と、黙って素通りする範囲
Controlノードはロケールが変わると自分のテキストを自動的に翻訳します。シーンエディタでLabelのテキストにキーを入れておくだけで動くのはこのためです。この便利さを前提に、意識して身につけておきたい習慣が2つあります。
1つは処理の順番です。部品を組み立てて作る文は、必ずテンプレートの状態で翻訳してから中身を埋める。組み立ててからtr()に渡してはいけません。コード上で組み立てられた文は表に一度も存在したことがないので、検索しても見つからず、そのまま素通りします。つまり全ロケールで原文のまま出続けます。原文の言語でテストしている限り、この不具合は目に見えません。
もう1つは、自動翻訳を切るべき場所を知っておくことです。プレイヤーが入力した名前、セーブデータのタイトル、チャットの発言を表示するLabelは、任意のプレイヤー入力をキー検索に通していることになります。ほとんどの場合は何も起きません。見つからないキーはそのまま素通りするからです。だからこそ、たまたま実在のキーと一致した一度だけ、誰も気づけません。ユーザー入力や実行時データを表示するノードでは自動翻訳を無効にしてください。該当プロパティの名前はGodotのバージョンによって変わっているので、使っているバージョンのドキュメントで確認してください。
そして、そもそも仕組みに届いていないテキストがあります。自作のデータファイルから読み込む文字列、リソースやJSONに定義してあってインポータが見ていない名前、ツールスクリプト内のテキスト、アドオンが持っている文言、キー化を忘れてシーンに直書きのまま残った仮テキスト。これらはエラーも警告も出しません。見つける方法は、完全に翻訳したはずの言語で実際にゲームを開き、原文のまま残っている箇所を目で拾うことだけです。
# 悪い例: 先に文を組み立てているので、このキーは存在しない
label.text = tr("%sを見つけた!" % item_name)
# 良い例: テンプレートを翻訳してから中身を埋める
label.text = tr("msg.found_item").format({"item": item_display_name})リマップ — 言語を持っているのはテキストだけではない
言語によって差し替えが必要なのは表の中のテキストだけではありません。タイトルロゴに描き込まれたゲーム名、看板のテクスチャ、ボタン名を書き込んだチュートリアル画像、収録済みのボイス、既定のフォントが持っていない文字を含む言語のためのフォント。どれも言語依存のアセットですが、どれも文字列ではありません。
Godotにはローカライズリマップという仕組みがあります。あるリソースパスに対してロケールごとの差し替え先を対応づけておくと、読み込み時に自動的に切り替わります。設定はプロジェクト設定のローカライズの項目で行い、元のパスを読み込んでいるコードは条件分岐なしで正しいほうを受け取ります。
運用上の注意が2つ。リマップの設定は翻訳ファイルの隣ではなくプロジェクト設定側にあるため、半年後に言語を追加するときに真っ先に忘れられます。リマップ対象アセットの一覧を翻訳テーブルと同じフォルダに置いておくと、片方を更新するときにもう片方も目に入ります。もう1つ、存在しないリソースを指したリマップは、そのロケールでだけ読み込み失敗になります。普段のテストでは絶対に踏みません。
より安いのは、そもそもリマップを減らす設計です。画像に文字を焼き込まず実行時に上から描くようにすれば、Label1つで言語別アセットという作業カテゴリごと消えます。これは翻訳が届いてからではなく、アートを作る時点で決まってしまう話なので、早い段階で共有しておく価値があります。
更新のたびに翻訳を失わないために
ローカライズで高くつくのは1回目ではありません。表が1000行になり、3言語が完成、1言語が半分、そこにシナリオ担当が会話40行を書き直してきた5回目の更新です。
まず行順を固定します。書き出し側と翻訳者のエディタで並び順が食い違うと、往復のたびに「全行が変わったように見える差分」が生成され、本当の変更がその中に埋もれます。並び順を1つ決めて — キーの昇順が最も揉めません — 双方で守ってください。
原文が変わったときの扱いは、明示的に決めておく必要があります。これが症状の出ない失敗だからです。日本語の原文だけが変わってキーが同じなら、既存の訳文はそのまま残り、セルは埋まったままです。つまりファイルは網羅率100%を報告しながら、原文と一致しないテキストを出荷します。どの版の原文に対して訳されたのかを、どこかが記録している必要があります。翻訳時点の原文を控えた併走ファイル、行に立てるマーカー、POに移行済みなら要確認フラグ。手段は何でも構いませんが、要件は同じです。原文の変更が、どこかで目に見える信号にならなければいけません。
削除は慎重に。どのコードからも参照されていない行が残っていても、数バイトの無駄でしかなく、誰も困りません。まだ参照されているキーを消すと、プレイヤーの画面に生のキー文字列が出ます。行を消す前にプロジェクト全体を検索し、迷ったら残してください。
その言語を「完了」と呼ぶ前に
列が埋まっていることは、表についての主張であって、ゲームについての主張ではありません。意味があるのは、その言語でビルドを起動して目で見る確認だけです。次の観点で一周すると、表計算ソフトでは見えないものの大半が拾えます。
- プレイヤーが到達しうる全画面。特に確認ダイアログ、エラーメッセージ、ポーズメニュー — 最後にキー化され、最もテストされない場所
- 特定の状態でしか出ない文字列。所持枠が満杯のとき、ゲームオーバー、通信エラー、実績解除の瞬間
- 変数から組み立てている文字列。翻訳の前に組み立ててしまっている可能性が最も高い箇所
- グリフの網羅。フォントが持っていない文字はエラーではなく空白の四角として描画されるため、失敗が静かに進行する
- レイアウト。訳文が原文と同じ長さになることはまずなく、固定幅のボタンが最初の犠牲になる
- 読み込まれたロケールの一覧。出荷したつもりの言語が、エンジンに実際に登録されているかの最終確認