UnrealのString TableとCSV往復 — 実際に回るローカライズ運用
Unrealは、ローカライズの仕組みが他エンジンよりかなり完備されています。翻訳を前提としたテキスト型があり、翻訳対象文字列を保持するアセット型があり、プロジェクトからテキストを収集して業界標準の翻訳ファイルを書き出すダッシュボードがあり、パッケージ版が実行時に読み込むカルチャー別データを生成するコンパイル工程まで用意されています。足りないものはほとんどありません。
問題は、そのすべてが「使うと決めたものだけが有効になる」設計で、使われなかった部分が静かに失敗することです。間違った型で持ったテキストは収集されません。文字列を連結して作った文も収集されません。アセットを複製したときに一緒に複製されたキーは、収集ログを読まない限り気づけない衝突を起こします。そしてどのケースでもパイプラインは成功を報告し、その文字列は英語のまま出荷されます。
この記事では、一度きりの初期設定ではなく継続的な運用としてこのループを追います。何より先に成立していなければならないテキストの前提、String Tableの正体とそのCSVの形、往復の流れ、そこから漏れるもの、そして原文の英語が変わったときに翻訳がどうなるかです。
ローカライズできるテキストはFTextだけ
Unrealには初心者が同じものとして扱いがちで、ローカライズ機構はまったく別物として扱う2つのテキスト型があります。FStringは可変の文字列で、ファイルパス、識別子、通信データ、そしてプレイヤーが読まないものすべてに使う型です。FTextは表示用のテキストで、名前空間とキーを持ち、実行時に翻訳データから引くことができ、収集工程が拾うのはこちらだけです。
そこから導かれるルールは単純で、最初から徹底する価値があります。プレイヤーが読むものはFText、それ以外は使わない。FStringにバインドされたウィジェットはローカライズ対象になりません。書き出しにも現れず、警告も出ません。この事故が起きる原因は知識不足であることのほうが少なく、たいていは利便性です。文の中に数値を入れたい人が、1行で済むという理由で文字列連結に手を伸ばし、その文は永久にローカライズ機構の外に出ます。
もう1つ、意図せず使ってしまいやすい抜け道があります。実行時の文字列から直接FTextを構築すると、意図的にカルチャー非依存のテキストになります。有効なFTextで、コンパイルも通り、表示もされ、そして決して翻訳されません。プレイヤーが入力した名前やスコアの数値にはこれが正しい挙動です。それ以外に使えば不具合ですが、コード上の見た目は翻訳されるテキストとまったく同じです。
C++では、リテラルの表示テキストはローカライズ用マクロを通すことで、コンパイル時に名前空間とキーが結び付きます。ブループリントでは、ノードに直接打ち込んだテキストリテラルはアセットから収集されますが、これは収集設定にパッケージが含まれている場合に限られます。既定では含まれており、そして収集を速くしようとして真っ先に外されがちな設定でもあります。
String Tableとは何か、その裏側のCSV
String Tableは、キーと原文の対応表に名前を付けたものです。FTextは自前のリテラルを持つ代わりに、この表の行を参照できます。同じ警告文を4つのウィジェットに個別に打ち込んで4つの独立したエントリにする代わりに、4つとも1行を指す。効果はすぐ出ます。文字列は一度書かれ、一度翻訳され、そこを直せば出現箇所すべてが直ります。
形式は意図的に素朴です。String Tableはエディタ上のアセットとして作ることもできますし、キー列・原文列・任意のコメント列を持つCSVファイルを裏側に置くこともできます。そのCSVはリポジトリ上の普通のファイルです。プルリクエストでレビューでき、差分が読め、エディタを開いていない人でも編集できます。
コメント列は空のままにせず、きちんと使う価値があります。このパイプラインの中で、翻訳者に対して「この2語はボタンで見出しではない」「この名前は地名ではなく人名」「周りのUIに入る余裕は30文字ぶん」といった情報を構造的に渡せる唯一の場所だからです。ここに書いた内容は翻訳ファイルまで届き、つまり実際に作業する人の手元まで届きます。
String Tableをリテラル直書きより優先すべきもう1つの理由は、キーの読みやすさです。ブループリントやアセットに直接入力したテキストには自動生成のキーが付き、自動生成のキーは書き出したファイルの中では機械的な識別子にしか見えません。その羅列を開いた翻訳者には各エントリが何なのか分かりませんし、半年後に翻訳をレビューする人にも分かりません。表のキーは、自分で決めたキーです。
Key,SourceString,Comment UI_NEW_GAME,New game,メインメニューのボタン。入るのは16文字程度。 UI_CONTINUE,Continue,メインメニューのボタン。セーブがない場合は非活性。 MSG_LOW_HEALTH,Health critical,HUDの警告。ゲームプレイ中に赤字で重ねて表示。 NPC_SMITH_NAME,Halvor,キャラクター名。男性の鍛冶屋。翻訳不要。
往復の流れ — 収集・書き出し・翻訳・取り込み・コンパイル
ローカライズダッシュボードは作業をターゲット単位で管理し、各ターゲットは同じ5工程を回します。それぞれが何を生成するのかを把握しておくことが、ゲームに反映されないときの切り分けを可能にします。
収集は、設定したソース — C++のソースファイル、パッケージやアセット、アセットのメタデータ — を走査して、ローカライズ対象のテキストをマニフェストに集めます。マニフェストは「このプロジェクトが何を翻訳対象と見なしているか」に対する唯一の答えです。ここに載っていない文字列は下流のどの工程でも救えないので、迂回策を考えるのではなく、いったん止めて収集側を直してください。
書き出しは、カルチャーごとに1つずつ、標準的なgettext(PO)形式の翻訳ファイルを生成します。外部に渡すのはこれです。翻訳者も翻訳ソフトもこの形式をすでに理解していること、そしてコメントや文脈情報を失わずに往復できることから、受け渡し物として正しい選択になります。
翻訳作業はUnrealの外で行われます。取り込みは、返ってきたファイルをカルチャー別のアーカイブに反映します。アーカイブは、各文字列がどう翻訳されるかの記録そのものです。コンパイルはそのアーカイブを、実行中のゲームが実際に読むバイナリのカルチャー別データに変換します。
コンパイルについて2点、よく引っかかります。1つは独立した工程であることで、取り込んだばかりの翻訳はコンパイルを実行するまでゲームに現れません。「翻訳が反映されない」という報告のかなりの割合が、まさにこれです。もう1つは、コンパイル結果をパッケージに含める必要があることです。どのローカライズを同梱するかを指定する設定があり、そこから漏れたカルチャーはエディタ上では完璧に動作し、ストアに提出したビルドからは消えています。
# パッケージ版を特定のカルチャーで起動して確認する MyGame.exe -culture=ja # エディタとパッケージ版ではフォント解決やレイアウトの # 挙動が異なるため、必ず両方で確認する。
収集が見ていないテキスト
どのプロジェクトにも必ずあります。見つけるのは意識的な作業であって、パイプラインが報告してくれるものではありません。そして種類はプロジェクトをまたいで同じです。
- 実行時に部品から組み立てている文。名前付き引数を持つ書式文字列を1つのFTextエントリにするのが正解で、そうすれば翻訳者は文全体を受け取り、自分の言語の語順に合わせて要素を並べ替えられる
- データテーブル、データアセット、設定ファイルにFStringとして入っているプレイヤー向けテキスト。型からは表示用テキストであることが読み取れない
- プラグインやサードパーティコンテンツ内のテキスト。独自のローカライズターゲットを持つため、ゲーム側のターゲットではカバーされない
- Enumの表示名、アセットのメタデータ、エディタ用ラベル。結局プレイヤーの目に触れていた、という形で発覚しがち
- テクスチャやマテリアルに焼き込まれた文字列。看板、ロゴ、ボタン名入りのチュートリアル画像など、テキストファイルがどこにも存在しないローカライズ作業
- そもそもプロジェクト内に存在しないプラットフォーム側のテキスト。ストアの説明文、実績名と説明、リッチプレゼンスは、いずれもパブリッシャー向けポータルで別途入力する
名前空間とキー、そして原文が変わったとき
FTextのエントリは、名前空間とキーの組で識別されます。この組がマニフェストに記録され、アーカイブはこの組に対して訳文を保持し、実行時のルックアップもこの組で行われます。翻訳がどれだけ長持ちするかは、この組の安定性で決まります。
実務上、結果は2つの形で現れます。1つ目、キーを変えると古いキーに紐づいていた訳文は捨てられます。アーカイブから見ればそのエントリは消滅し、新しいキーは未翻訳として現れます。2つ目はより気づきにくいもので、訳文と一緒に原文も記録されているという点です。キーを変えずに英語を書き直すと、パイプラインはそのエントリを「新たに作業が必要」として扱います。別の文言のために書かれた訳文を黙って流用しないという意味で、この挙動は正しい。同時にそれは、英語の何気ない言い回し変更が、出荷しているすべての言語で有償の翻訳作業を発生させることを意味します。英語のコピー修正は翻訳ラウンドの前にまとめて済ませ、後から小出しにしないでください。
衝突のケースも押さえておく価値があります。警告として出るぶん、慣れて無視されるようになるからです。ブループリントやウィジェットを複製すると、中のテキストキーごと複製されることがあり、異なる原文が同じ名前空間とキーを主張する状態になります。収集はこれを衝突として報告し、これは実害のある問題です。ルックアップで勝てるのは一方だけなので、ゲームのどこかで、別の訳文が表示されるという症状だけが残ります。収集のたびにログを読み、衝突をノイズではなくエラーとして扱ってください。
そのカルチャーを「出荷可」と呼ぶ前に
エディタは親切な環境で、パッケージ版はそうではありません。フォントの解決が異なり、ローカライズデータが欠けたときのフォールバックの挙動も異なり、エディタ解像度のプレビューに収まったテキストがプレイヤーの使う解像度で収まる保証もありません。検証とは、対象カルチャーでパッケージ版を起動して実際に遊ぶことです。
効果が高いのは、画面ではなく状態を網羅する確認です。メニューは誰でも最初に開くのでテストされます。壊れたまま出荷されるのは、戦闘中の警告表示、通信切断のダイアログ、所持枠が満杯のときだけ出るツールチップ、リザルト画面。いずれも短時間だけ、狭い枠に、ざっと触るだけでは到達しない条件で表示される場所です。
プロジェクトのファイル外にあるものも忘れずに確認してください。既定では持ち主が決まっていない領域だからです。実績名と説明、ストアページのテキスト、プラットフォーム側の文言はゲームのビルドとは別に入力するもので、2言語目・3言語目でしばしば忘れられ、しかもゲームを起動する前にストアページを開くすべてのプレイヤーの目に触れます。