Unreal Engineのローカライズ入門 — FTextと収集・書き出しの流れ
Unreal Engine(UE5を含む)で翻訳できる文字列は、名前空間(namespace)とキーを持つFTextとしてエンジンに渡ったものだけです。ここから先のすべてがこの一点で決まります。テキストがFTextになっていれば、あとはカルチャー(言語・地域の設定)ごとに、収集(Gather)から書き出し・翻訳・取り込み・コンパイル・パッケージ設定までの同じループを回すだけです。
エディタのパネル名やコンソールコマンド、APIのシグネチャはエンジンのバージョンによって変わります。そこは公式ドキュメントのローカライズ関連の章を一次情報として確認してください。逆にバージョンで変わらないのは設計の判断です。どの文字列をFTextにするか、キーをどう名付けるか、どのカルチャーを出荷するか、文をどう組み立てるか。このうち2つはUnrealを起動する前に手元で検証できるので、確認のしかたも含めて書きます。
収集されるのはFTextだけ — FText・FString・FNameの使い分け
FTextは、自分の身元を持った表示用テキストです。名前空間、その中でのキー、そして原文をセットで持ちます。C++ではLOCTEXTマクロがファイルのLOCTEXT_NAMESPACEを使って宣言し、NSLOCTEXTは名前空間を引数で明示します。BlueprintノードのテキストピンやアセットのテキストプロパティもFTextなので、対象のパスを設定してあればエディタ上で書いたテキストもそのまま収集されます。
FStringは身元を持たない可変の文字列バッファで、収集ステップが記録できるものが何もありません。FNameはアセット名や行名の検索に使う軽量な識別子で、表示用ではありません。最初のプロジェクトがほぼ必ず踏むのがFText::FromStringです。コンパイルも通り画面にも出て、実行時にもFTextですが、素の文字列から作られているため名前空間もキーも持たず、収集ステップには書き留めるものがありません。この方法で組み立てた文はどのカルチャーでも原文のまま出荷されます。デバッグ表示や開発者向け画面を除けば、コードレビューで指摘すべき対象です。
名前空間とキーの組は、原文を編集しても翻訳を引き継ぐための仕組みでもあります。1つの名前空間の中では、キーは永久に1つの意味だけを指す必要があります。別の意味で使い回した時点で、1つの訳文で2つの文を兼ねろと頼んだことになります。ゲームロジックの中にプレイヤー向けの文字列が直書きされたままなら、直書き文字列の外部化が先です。
Localization Dashboardの流れと、各ステップが書き出すもの
Localization Dashboardは下に挙げた順番で処理を進め、各ステップがそれぞれの成果物を作ります。原文はマニフェスト、翻訳はカルチャーごとのアーカイブに置かれ、最後にランタイムが読むバイナリのリソース(LocResファイル)になります。ファイル名や各ステップの設定項目はバージョン依存なので、公式ドキュメントのLocalization Dashboardの章で確認してください。
計画上いちばん効いてくるのは、コンパイル済みのローカライズデータがビルド成果物だということです。POを直接編集しても動いているビルドには反映されず、修正の確認には取り込みとコンパイルを通す必要があります。
String Tableアセットを使う書き方も同じパイプラインに載ります。キーと原文の行を持ち、CSVから入力でき、ウィジェットやデータアセットから参照できます。中身はやはり名前空間とキーを持つFTextなので、収集・書き出し・コンパイルは同じ経路を通ります。シナリオライターやデザイナーがテキストを持ち、エディタの外で編集したい場合はこちらを選び、表示するロジックのすぐ隣に置きたい文字列はコード側のLOCTEXTのままにします。手順はUnrealのString TableとCSVの運用にまとめています。
- Gather — 設定したコード・アセットのパスを走査し、マニフェストとアーカイブを書く
- Export — カルチャーごとにPOファイルを書き出し、翻訳者に渡す
- Import — 返ってきたPOをアーカイブへ取り込む
- Compile — ランタイムが読むカルチャーごとのリソースを作る
- Package — 出荷するカルチャーをパッケージに含め、パッケージ版でテストする
POファイルはエンジンに渡す前に検証できる
POファイルはgettext形式のテキストなので、エディタを開かずに構造を検証できます。下の形は、Unrealの書き出しと、gettext系の翻訳ツールから返ってくるPOを合わせた構造です。msgctxtが名前空間とキーを持ち、msgidが原文、msgstrが訳文です。Unreal自身の書き出しはコメントのラベルをタブ区切りで書き、原文の出どころも出しますが、フラグのコメントは書きません。Unrealは名前空間とキーを1つのコンテキスト文字列にまとめて書きますが、その正確な書式はプロジェクトのPOフォーマット設定に左右されます。ツールを書く前に、公式ドキュメントの書き出し・取り込み設定の章で確認してください。形式自体の読み方はgettextのPOファイル形式で説明しています。
- 同じコンテキストで原文も同じエントリが2つあると致命的エラーになる。gettextのmsgfmtにチェックオプションを付けて実行すると重複定義として報告され、終了コードが0以外になる
- 同じキーで原文が違う場合はエラーにならない。別々のエントリとして通るので、キーの使い回しは自分のレビューでしか見つからない
- 訳文からプレースホルダーを削っても通る。例のダメージの行から片方の引数を消してもエラーは出ず、3件翻訳済みのまま終了コード0になる。Unrealの名前付き引数はgettextが知っている書式ではないため
- fuzzyはgettext系の翻訳ツールが付ける印で、Unrealの書き出し自体は書かない。原文が変わった訳の扱いは、Unreal側ではstaleという別の仕組みになる
msgid ""
msgstr ""
"Project-Id-Version: MyGame\n"
"PO-Revision-Date: 2026-09-12 10:00+0900\n"
"Last-Translator: \n"
"Language-Team: ja\n"
"Language: ja\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#. Key: MainMenu_Start
#: /Game/UI/WBP_MainMenu.WBP_MainMenu
msgctxt "MainMenu,MainMenu_Start"
msgid "Start"
msgstr "はじめる"
#. Key: Tutorial_Start
msgctxt "Tutorial,Tutorial_Start"
msgid "Start"
msgstr "開始"
#. Key: Hud_Damage
msgctxt "Hud,Hud_Damage"
msgid "{Attacker} dealt {Damage} damage"
msgstr "{Attacker}が{Damage}のダメージをあたえた"
#. Key: Hud_Pickup
#, fuzzy
msgctxt "Hud,Hud_Pickup"
msgid "You picked up {Count} coins"
msgstr "コインを{Count}枚ひろった"カルチャー名の正規形とフォールバックの連鎖
Unrealはカルチャーを言語タグで識別します。BCP 47の言語タグと同じ体系で、大文字小文字は慣習として決まっています。言語のサブタグは小文字、文字体系(スクリプト)は先頭だけ大文字、地域は大文字です。正規形はNodeのIntl.getCanonicalLocalesで確認できます(下の対応表)。カルチャーのフォルダ名や設定値はこの表記に合わせてください。
解決は具体的なタグから順に下りていきます。zh-Hans-CNの要求はzh-Hans、さらにzhへとフォールバックします。だからこそ、素のzhではなくzh-Hansとzh-Hantを出荷すべきです。zhだけでは文字体系が指定されておらず、標準の推定データは中国本土の簡体字として解決してしまいます。
zh-hans-cn -> zh-Hans-CN maximize("zh") -> zh-Hans-CN
pt-br -> pt-BR minimize("zh-Hans-CN") -> zh
fr-ca -> fr-CA maximize("pt-BR") -> pt-Latn-BR
PT -> pt maximize("ja") -> ja-Jpan-JP
ja -> ja フォールバック: zh-Hans-CN -> zh-Hans -> zh書式・複数形と、FString連結という罠
パーツを組み合わせて作る文は、名前付き引数を持つ1つのFTextにして、FText::Formatと引数マップで組み立てます。名前付きであることが重要なのは、翻訳者が引数を文中のどこへでも動かせるからです。日本語は名詞のあとに助詞が来て、ドイツ語は動詞が文末に行きます。失敗の形はFStringの連結です。固定の断片の間に名詞を挟むと英語の語順が固定され、しかも断片はFTextではないので収集もされず、その文は翻訳者のもとに届きません。
数を含む文は置換だけでは足りません。NodeのIntl.PluralRulesで複数形のカテゴリ数を調べると、下表のとおり言語差が大きいことが分かります。数によって形が変わる文は、カテゴリが2つ以上ある言語ではカテゴリごとの形が必要になり、数値を埋め込むだけの1本の文字列では表現できません。Unrealの書式機能はテキストの中で複数形や性別の分岐を書けるようになっていて、記法は業界標準のメッセージ書式(ICU MessageFormatの書き方)に近いものの同一ではありません。記法はバージョンで変わってきたので、正確な書き方は公式ドキュメントのテキストの書式設定の章から取ってください。バージョンに依存しない判断は1つで、分岐は翻訳可能な文字列の内側に置き、2本の文字列をif文で切り替える形にはしないことです。
良い例: "{Attacker} dealt {Damage} damage" 1つのFText。引数の順序を変えられる
悪い例: "You found " + ItemName + "!" 語順が固定され、収集もされない
複数形カテゴリ数: 日本語1・英語2・ロシア語4・ポーランド語4・アラビア語6最初のローカライズで止まりやすい箇所と確認リスト
初回の作業で時間を失う原因は、見るべきビルドを見るまで気づけない短いリストに集中しています。ループが一周したことは、テキストがパイプラインを通ったという意味しかありません。結果が正しいかどうかはローカライズQA(LQA)のような実機での確認が必要です。完了と判断する前の最終確認は、エディタではなくパッケージ版で行います。
- 収集対象のパスの設定漏れ — 抜けた文字列は警告を出さない。Gatherのエントリ数を前回と比べる
- パッケージ設定にカルチャーを含め忘れ — エディタでは動くのに製品版から言語が消える
- アセットのローカライズはテキストとは別作業 — 音声や文字を焼き込んだ画像はカルチャーごとのアセット規約に従い、独立した工程として日程を取る
- フォントは持っているグリフしか描けない — 欧文フォントのままでは日本語や中国語が空の四角になる(豆腐(空白の四角)が出る原因と直し方)。合成フォントでフォールバックを用意する
- 原文を直すと既存の訳はstale(翻訳した時点の原文と食い違う状態)になる — アーカイブが翻訳時点の原文を保持して検出し、staleな訳を出荷に含めるかはコンパイルの設定で決まる
- 実行時にカルチャーを切り替えても、FTextをToString()した結果を保持している画面は古い文字列のまま残る
- 訳文は原文より長くなる言語が多い — 実際の訳文を実際のUIに入れて確認する
カルチャーを完了と判断する前に、パッケージ版で確認する: 返ってきたPOが構造検証を通り、コンテキストの重複がない 訳文のプレースホルダー名が原文と一致している(順序の入れ替えは可) Gatherのエントリ数が前回と一致している 対象カルチャーが選択でき、すべての画面が翻訳されている