注意
この記事の内容は、開発途中のものです。 今後、変更などを行う可能性が高いのでご注意ください。
はじめに
Steamで配信されているゲーム「Indigo Park」(Chapter 1)を日本語で遊ぶための、非公式の日本語化MODを作りました。
- リポジトリ:223n/indigo-park-localization
- ライセンス:Apache License 2.0(このリポジトリで作ったものが対象。移植したCityHashの部分はMIT License)
2026年9月17日の夜にリポジトリを作り、翌18日のうちにv0.6.1まで進みました。 この記事では、ゲームの中身の調査、文章とフォントの差し替え、原文の収集、訳し方の決めごと、リリースの自動化までを順に記録します。
このMODは開発元や販売元とは関わりがなく、承認も受けていません。 ゲームから取り出したファイルそのものは、リポジトリと配布物のどちらにも入れていません。 ゲームに含まれる文や画像、音声などの権利は権利者にあります。
導入のしかた
Releasesからzipをダウンロードして展開し、install.batを実行します。
ゲームの場所はSteamの設定から自動で探します。
Windowsの表示言語が日本語なら、ゲームを起動するだけで日本語になります。
英語のままのときは、ゲーム内の「OPTIONS」から「GAMEPLAY」の「LANGUAGE」を開き、日本語を選びます。
取り外すときはuninstall.batを実行します。
ゲームの中身を調べる
最初に、ゲームのファイルがどう組み立てられているかを調べました。
| 項目 | 内容 |
|---|---|
| エンジン | Unreal Engine 5.2 |
| アセット | IoStore形式(.utocと.ucas) |
.pak |
.locresや.ufontなど、アセット以外のファイルだけを収録 |
| 暗号化 | なし |
| 署名 | なし |
| MODの読み込み | Content/Paks/~mods/に置いたpakを読み込む |
このゲームでは、cooked済みのアセットはすべてIoStore形式のファイルに入っていました。
従来の.pakに残っているのは、翻訳データやフォントなどの一部だけです。
暗号化と署名はどちらもないため、~modsフォルダーに置いたpakで中身を上書きできることを実機で確かめました。
展開や組み立てには、trumank氏のretoc(IoStore用)とrepak(pak用)を使いました。
retoc to-legacyで、18,395個のアセットをすべて従来の形式に展開できました(約17GB)。
文章を差し替える
どのファイルが読まれているか
Unreal Engineのゲームは、画面の文字を「FText」という形で持ち、言語ごとの訳を.locresファイルから引きます。
このゲームの設定ファイルには多言語化の設定の節がなく、ゲーム本体の訳の読み込み先はエンジン既定のContent/Localization/Game/<言語>/Game.locresだけでした。
ゲーム自体はこのファイルを同梱しておらず、代わりにMenuSystemPro.locresとInspectionSystemPro.locresという別の.locresが入っています。
ところが、これらはマーケットプレイスで買えるプラグインの付属品で、実際には読み込まれていませんでした。
画面の文字も、字幕も、同じGame.locresで差し替えられます。
字幕は音声のアセットの中にFTextとして入っているためです。
locresを自分で書き出す
.locresを書き出す処理はPythonで自作し、外部のパッケージは使っていません。
ただし、キーのハッシュに使うCityHash64の計算は、Googleが公開しているCityHash(MIT License)をPythonへ移植したものです。
書き出したファイルはUnrealLocresで読み直して検証しました。
形式そのものは単純ですが、2か所でつまずきました。
1つ目はファイルの先頭の印(マジック)です。 バイト順を誤ると、エンジンが古い形式のファイルと誤認してクラッシュします。
2つ目はキーのハッシュです。
キーの文字列をUTF-16LEにしてCityHash64を掛け、さらに「下位32bit + 上位32bit × 23」で32bitに畳みます。
下位32bitをそのまま使うのではありません。
この計算で、ゲームに入っている.locresのハッシュ300件がすべて一致しました。
なお、原文の変化を検出するためのハッシュ(SourceStringHash)はこれとは別の計算(CRC32の一種)です。
言語の選択肢に日本語を足す
.locresを置いただけでは、日本語を選べませんでした。
ゲームの言語の一覧は1つのデータアセットにあり、中身は英語(en)とドイツ語(de)だけです。
言語を切り替える処理は、選ばれた値がこの一覧にあるかを確かめてから切り替えます。
一覧にないjaを指定しても、英語に戻されてしまいます。
そこで、訳の入っていないdeを、同じ長さのjaに書き換えました。
長さが同じなのでファイルのサイズは変わりません。
これをretoc to-zenでIoStore形式のMODに組み立てて~modsに置くと、日本語を選んで保存できるようになりました。
ただし、書き換えたアセットを配布すると、ゲームのデータの再配布にあたります。
そのため、インストーラーが利用者自身のゲームからそのアセットを取り出し、その場で書き換えてMODを組み立てる作りにしました。
この処理に失敗した場合は、ドイツ語を選べば日本語で表示されると案内します。
ドイツ語の枠にも同じ日本語のGame.locresを入れてあるためです。
その後、Windowsの表示言語が日本語なら、pakを置くだけで日本語になることも分かりました。
エンジンがOSの言語からjaを選ぶためです。
フォントを差し替える
.locresだけを入れると、日本語の部分が表示されずに消えました。
いわゆる豆腐(□)すら出ません。
ゲームが使うフォントに日本語の字形がないためと見ています。
.ufontファイルの中身はそのままのTrueTypeで、pakで上書きすれば差し替えられます。
配布版ではM PLUS 1p(SIL Open Font License 1.1)の4つのウェイトを使い、ゲームの18個の.ufontを置き換えています。
フォントはリポジトリに含めず、手元にないときだけGoogle FontsのGitHubリポジトリから取得します。
使う前には毎回SHA256で照合します。
タイトルロゴ専用の装飾書体は英字しか使わないため、置き換えの対象から外しました。
原文を集める
集めた原文と翻訳の対象
展開したアセットを走査し、32桁の16進数のキーを手がかりにFTextを拾う道具を作りました。
キーが16進数ではない文字列表(ST_Menuなど)の356件と合わせて、集めた原文は全部で1,230件です。
このうち、画面に出る608件を翻訳の対象にしました。
内訳は、メニューが291件、本編の字幕が212件、その他のUIが105件です。
残る622件は訳しません。
内部の識別子やコンソールコマンド、仮置きの文言、プラグインの見本の文章、FXAAやRec.709のような規格名などです。
訳すかどうかは、ファイルの置き場所ではなく、画面に出るかどうかで決めます。 この決まりは、後で述べるIssue #16を受けて翻訳の指針に明記しました。
Blueprintのバイトコード内のFText(v0.5.0)
v0.4.0で全件を訳したあとに実機で遊ぶと、目標の表示と操作の案内が英語のままでした。
原因は、FTextの保存のされ方が2通りあることでした。
- プロパティとして持つもの:長さ付きの文字列で、名前空間、キー、原文の順で並ぶ
- Blueprintのバイトコード内のもの:
0x1Fの後ろのヌル終端の文字列で、原文、キー、名前空間の順で並ぶ
最初の収集は前者しか見ていませんでした。 後者も拾うようにすると、新たに45件が見つかりました。 目標の表示、操作の案内、「はじめから遊ぶか」の確認ダイアログ、収集品の解説などです。 このうち38件を翻訳の対象に加え、既存の原文の仕分けも見直したため、対象は572件から608件になりました。
収集品の解説は、主人公が子どものころを振り返る独白でした。 それまで主人公を話者として扱っていなかったので、翻訳の指針に主人公の口調を足しました。
訳し方を先に決める
人物ごとの口調
機械翻訳のような直訳を避けるため、訳す前に人物ごとの一人称と口調を決めました。
| 人物 | 一人称 | 口調の方針 |
|---|---|---|
| ランブリー | ボク | 明るく早口。丁寧な案内口調と友達口調を行き来する |
| モリー | アタシ | 気風のいい飛行機乗り |
| ロイドフォード | ワガハイ | 芝居がかった古風で尊大な口調 |
| フィンリー | ぼく | 小声で、ひらがなを主体にする |
| 主人公 | オレ | 常体の独白。少し皮肉まじり |
ロイドフォードの一人称は、最初は「ワタクシ」にしていました。 自分で見直して男性キャラクターには合わないと感じたため、Issue #18に記録し、v0.6.0で「ワガハイ」に改めました。 「ワタクシ」はへりくだる側の言葉で、尊大な人物像とは向きが逆です。 候補には「オレサマ」もありましたが、採りませんでした。 「オレサマ」は尊大な自称ですが、くだけた「オレ」をもとにしているので、庶民扱いを嫌う人物像とは合わないと考えました。 また、「オレ」は主人公などがすでに使っています。 字幕に話者の名前が出ない作品なので、一人称が重なると誰の台詞かが分かりにくくなります。
固有名詞は原則としてカタカナにし、ロゴや看板の文字は原語のままにします。 UIは、ボタンと項目名を体言止め、説明文をですます調にそろえています。
訳はPO形式で管理する
訳のデータはPO形式(data/ja.po)で管理しています。
改行やリッチテキストのタグ、引用符をそのまま扱えて、Poeditなどの翻訳ツールでそのまま開けるためです。
英文が同じでキーの違う項目があるので、文脈の欄(msgctxt)に名前空間とキーを入れて区別しています。
訳を検査する道具も作り、CIで毎回走らせています。 件数の整合、訳の抜け、未知のID、訳さない項目の混入、タグの一致、改行の数の一致の6点を見ます。
リリースを自動で公開する
ワークフローの流れ
リリースはGitHub Actionsのワークフローで進めます。
- 「リリース」を手動で実行すると、
developからrelease/vX.Y.Zを切り、版を上げてmainへのPull Requestを開く - 人がそのPull Requestをマージすると、「リリースを公開する」ワークフローがタグを打ち、ドラフトのReleaseを作って配布物のzipを添付する
mainをdevelopへ戻し、リリースのブランチを消す- 配布物を検証し、問題がなければReleaseを公開する
配布物の検証を後始末より後ろに置いているのには理由があります。
検証が先にあると、検証が落ちたときに後始末まで飛ばされ、release/*のブランチが残って次のリリースを始められなくなるためです。
配布物の中身を確かめる
人の目で確かめる代わりに、添付したzipそのものを落として中身を確かめる道具を作りました。
- 必要なファイルがそろっているか
- 同梱の説明書きの版が、リポジトリの版と合っているか
- pakの中の
.locresに訳文が入っているか - 訳文のはずの項目が、英語の原文のまま入っていないか
4つ目は、Issue #16と同じ形の崩れを捕まえるための検査です。 Issue #16は、設定画面の説明文が2件だけ英語のまま出ていたという報告でした。 その2件をプラグインの見本の文章と見なして翻訳の対象から外していましたが、実際にはゲームの設定画面がその見本の画面を使っていました。
.locresは原文を持たず、訳文だけを持つファイルです。
そのため、.locresの中に英語の原文が見えたら、訳文の代わりに原文が書き込まれたと判断できます。
最初はバイト列を探すだけの作りで、31件の誤検知が出ました。
別の項目の訳として同じ英語が書かれている場合と区別できなかったためです。
.locresを項目ごとに読み解いてPOと突き合わせる作りに変えると、誤検知はなくなりました。
v0.6.1は、この仕組みで公開した最初の版です。 タグを打ってから18秒後に、Releaseが公開されていました。
ワークフローが開いたPull RequestではCIが動かない
v0.4.0のリリースで、ワークフローが開いたPull RequestでCIが1つも動かない、という問題に当たりました。
GitHubは、GITHUB_TOKENで作られたPull Requestのワークフローを「承認待ち」で作り、ジョブを1つも動かしません。
ワークフローが自分自身を呼び続けるのを防ぐための仕様です。
mainにはCode scanningの結果を必須にする規則をかけているため、チェックが埋まらずにマージできなくなります。
いまは、Pull Requestを一度閉じて開き直すことで対処しています。 人の操作として記録される「開き直し」でワークフローが起動し、ブランチとコミットは変わりません。 個人アクセストークンを使えば手間はなくなりますが、秘密情報の管理が増えるため見送りました。
ラベラーをフォーク用と自分用に分ける
Pull Requestに自動でラベルを付けるワークフローでも、1つ調整が要りました。
pull_request_targetで動くラベラーは、設定を既定のブランチ(main)から読みます。
そのため、developで足したラベルの規則が、developへのPull Requestに効きませんでした。
develop側の設定を取り出すようにすると、今度はワークフローの安全性を検査するzizmorが「危険なトリガー」として指摘しました。
pull_request_targetは書き込みのできるトークンで動くので、そこでPull Requestのコードを取り出して動かすのは危ういためです。
取り出し方をgh apiに変えても、同じ指摘が出ました。
最終的には、ワークフローを2つに分けました。
自分のリポジトリから出たPull Requestはpull_requestで動かし、マージ先のブランチの設定を使います。
フォークからのPull Requestだけをpull_request_targetで受け持ち、設定はmainから読まれる遅れを受け入れました。
指摘を無視する設定で黙らせる方法もありましたが、将来だれかが危険な変更を足したときに気付けなくなるため避けました。
今週出した版
| 版 | 主な内容 |
|---|---|
| v0.4.0 | 調査、locresの道具、原文の収集、PO形式、翻訳の指針、572件の全訳、インストーラー |
| v0.5.0 | Blueprintのバイトコード内のFTextを収集し、翻訳の対象を608件に拡大 |
| v0.6.0 | 訳の検査とCI、翻訳の訂正を受け付けるIssueフォーム、一人称の変更、ラベラーの分割 |
| v0.6.1 | 自動公開と配布物の検証、誤ってコミットしたフォントファイルの削除 |
v0.4.0の時点で、導入から取り外しまでを実機で一通り確かめています。
残っていること
いちばん大きく残っているのは、本編を通しで遊んで字幕を確かめる作業です。 字幕は1つの台詞が複数の項目に分かれているので、前後をつなげて訳していますが、実際の画面で読んだときの自然さはまだ確かめ切れていません。
また、ゲームの更新によって、言語の一覧を書き換える処理が失敗するおそれもあります。 その場合でもドイツ語を選べば日本語で遊べるようにしてありますが、更新のたびに確かめる必要があります。
おわりに
Unreal Engine 5.2のゲームでも、暗号化と署名がなければ、~modsに置くpakで文章とフォントを差し替えられました。
手間がかかったのは、読まれていない.locresや、Blueprintに埋め込まれたFTextのように、見えているものと実際に使われているものの食い違いを1つずつ確かめるところでした。
翻訳の訂正や提案は、リポジトリのIssueのフォームから受け付けています。