Unity 漢化パイプライン(自己監査機能付き)
「抽出 → リスク判定 → 翻訳 → 書き込み → 自己監査」を再利用可能な 4 段階パイプラインとして構成します。 核となるのは翻訳そのものではなく、どの文字列を翻訳してはいけないかを事前に判断することです。これこそが、ゲームを壊す唯一の原因です。
使用するタイミング
使用前に以下の 3 つを確認し、1 つでも満たさない場合は使用を控えてください。
- テキストの所在:
Mono/IL2CPPの Unity ゲームで、テキストがマネージドアセンブリ(*_Data/Managed/Assembly-CSharp.dll)またはresources.assetsにハードコードされており、Localizationテーブル、.po、.csvなどのローカライゼーションリソースが存在しないこと。 既存のローカライゼーションテーブルがある場合は、それを使用し、アセンブリを変更しないでください。 - オリジナルファイルを入手可能: 実行ファイルを変更するため、変更されていないオリジナルファイルをベースライン(バックアップディレクトリ、Steam 検証後の再ダウンロードなど)として用意できること。ベースラインがない場合は、その場での変更を行わないでください。
- 中国語が表示可能: プロジェクトに動的フォント(TMP または Legacy
Font)が埋め込まれていること。静的フォント/スプライトフォントの中国語化にはテクスチャの差し替えが必要であり、このパイプラインは動的フォントのみを処理します。
IL2CPP(Assembly-CSharp.dll がなく、GameAssembly.dll のみ)はサポート対象外です。Il2CppDumper + 再コンパイルが必要な場合は、別途対応します。
クイックスタート
# 0) プロジェクトの骨格を作成(l10n.json を生成)
python <skill>/scripts/zhl10n.py init -C あなたのプロジェクトディレクトリ
# 1) すべての ldstr サイトをリストアップ(約 10 秒 / 5000 件)
python <skill>/scripts/zhl10n.py extract
# 2) リスクレベルを確認:翻訳可能、限定翻訳のみ、絶対に翻訳不可
python <skill>/scripts/zhl10n.py risk
# 3) trans_map.json / site_map.json を編集し、書き込みを実行(オリジナルは変更せず、out/build/ に出力)
python <skill>/scripts/zhl10n.py apply
# 4) 自己監査(デフォルトで out/build の成果物を読み込む)
python <skill>/scripts/zhl10n.py audit --fail-on P1
ワンストップ実行:zhl10n.py all(extract → apply → audit)。
依存関係のインストール:pip install dnfile dncil UnityPy fonttools。audit のみの場合は UnityPy がなくても使用できます。
各フェーズの責務
| フェーズ | エントリポイント | 入力 | 出力 | 責任範囲 |
|---|---|---|---|---|
| init | zhl10n.py init | ターゲットゲームのパス | l10n.json、空のテーブル | 設定を生成するのみで、ゲームファイルには触れない |
| extract | extract.py | オリジナル DLL(+ オプションの assets) | out/extract/sites.jsonsites.json の各項目に str/type/method/token/offset を含む | 読み取り専用。パッチ未適用のオリジナルを読み取る必要がある。そうしないと、既に漢化された文字列が抽出される |
| risk | risk.py | sites.json | コンソールへのレベル別統計出力 | 判定のみ。テーブルは一切変更しない |
| apply | apply.py | オリジナル DLL + 翻訳テーブル + フォントサブセット | out/build/Assembly-CSharp.dllout/build/resources.assets | 新しいファイルに書き込み、オリジナルは変更しない(--inplace でその場で変更する場合は、事前に .orig を保存)。ブロックルールに一致する翻訳は書き込みを拒否し、エラーを報告する |
| audit | audit.py | 成果物 + 翻訳テーブル + 用語集 + ログ | out/audit/audit.{json,md,html}fixlist.csv | 読み取り専用。任意のバージョンのファイルに対して実行でき、新旧比較に使用できる |
3 層の翻訳テーブル。それぞれの責務は異なります。
trans_map.json—— グローバルテーブル。文字列で置換し、すべての出現箇所を置き換えます。'中国語に翻訳してもロジックを壊さない' 文字列のみを配置します。site_map.json—— サイト固有テーブル。キーは型::メソッドで、その場所のldstrのみを変更します。'同じ単語でも、メニューでは翻訳すべきだが、入力システムでは翻訳すべきでない' 場合に使用します。enum_map.json—— 列挙型名前テーブル。#Stringsヒープ内のフィールド名を変更します。ToString()から出力される列挙型の表示名に使用します。
判定順序: サイト固有 → グローバル。サイト固有でヒットした場合、グローバルは適用されません。
自己監査
自己監査は、このパイプラインの中核です。書き込み後に成果物を独立して読み取り、翻訳テーブル自身の主張を信用しません。
出力
| ファイル | 内容 |
|---|---|
audit.json | 完全な構造化結果。各 finding に check/severity/count/items[]/hint を含む |
audit.md | 人間向けの要約。P0→P2 の順にソート |
audit.html | 折りたたみ可能な単一ファイルレポート |
fixlist.csv | 修正が必要な項目のリスト。1 行 1 項目で、TODO リストとして直接使用可能 |
チェック項目(31 項目、6 カテゴリ)
CV 完全性——漏れがないか
| 番号 | レベル | チェック | 判定基準 |
|---|---|---|---|
| CV1 | P1 | 未翻訳テキスト | サイトが表示可能 + 両方のテーブルに翻訳がない + リスクルールで許可されていない |
| CV1b | P2 | UI テキストのように見えるがブロックされている | エンジンの制約によるもので、翻訳漏れではないことを示す。サイト固有の翻訳と実機テストが必要 |
| CV2 | P1 | 空の翻訳 / プレースホルダー翻訳 | 翻訳が空、または TODO/未翻訳 を含む |
| CV3 | P1 | 長い文の翻訳が原文と完全に一致 | 翻訳されていない可能性が高い |
| CV4 | P2 | 短い単語/記号の翻訳が原文と一致 | アイコンフォント、キー名などの原文保持は通常正しい |
| CV5 | P2 | サイト固有テーブルのデッドエントリ | 宣言されているが、オリジナルの IL 内に見つからない。メソッド名が変更されたか、グローバルテーブルで上書きされている |
CS 一貫性——用語が一致しているか
| 番号 | レベル | チェック |
|---|---|---|
| CS1 | P2 | 同一原文がグローバルテーブルとサイト固有テーブルで異なる翻訳 |
| CS2 | P1 | 用語集の強制翻訳が遵守されていない |
| CS3 | P1 | 翻訳が用語集の禁止語にヒット |
| CS4 | P2 | 重複キー(大文字/小文字、空白のみが異なる) |
| CS5 | P2 | 中国語の句読点が半角に偏っている |
FM フォーマット——連結が壊れていないか
| 番号 | レベル | チェック | 結果 |
|---|---|---|---|
| FM1 | P1 | プレースホルダーの不整合 | String.Format が例外をスローするか、数字が消失する |
| FM1b | P2 | 英語の複数形プレースホルダーを削除 | 正しい方法。実際のパラメータを誤って削除していないことを確認するためのヒントのみ |
| FM2 | P2 | 改行数の不一致 | レイアウトのずれ |
| FM3 | P1 | 単位/結合フラグメントの先頭/末尾スペースが欠落 | 1000coins のようにくっつく |
| FM4 | P2 | その他の先頭/末尾スペースの変更 | 中国語のテキスト間では通常問題なし |
| FM5/FM6 | P2 | 半角句読点 / 連続する空白 | 見た目の問題 |
| FM7 | P2 | 翻訳後の長さが過度に増加 | 多くの場合、説明文がラベルに書き込まれている |
RK リスク——ゲームが壊れる可能性があるか
| 番号 | レベル | チェック |
|---|---|---|
| RK1 | P0 | 高リスク文字列が翻訳テーブルに含まれている(パス、エンジンキー、正規表現、入力アクション名など) |
| RK2 | P2 | グローバルテーブルに単一識別子が含まれている。サイト固有テーブルへの変更を推奨 |
| RK3 | P1 | 翻訳テーブルのキーに中国語が含まれている。2 回目の適用でエラーが発生する |
| RK4 | P2 | 翻訳に英語の断片が残っている |
IG 構造——ファイル自体がまだ有効か
| 番号 | レベル | チェック |
|---|---|---|
| IG0/IG1/IG2/IG4 | P0 | ターゲットファイルの欠落、PE/メタデータの異常、#US の解析不能、ldstr トークンが不正、メタデータの行数がオリジナルと一致しない |
| IG3 | P2 | パッチ後も英語のままのサイト(想定内のホワイトリスト。確認用) |
| IG5/IG6 | P1 | フォントの文字欠け / フォントファイルの解析不能 |
RT 実行時——ゲームが自ら問題を報告していないか
| 番号 | レベル | チェック |
|---|---|---|
| RT1 | P1 | Player.log に例外スタックトレースが出現(source.logs 設定が必要) |
トリガー方法
3 つの方法からシナリオに応じて選択します。
# 1. 書き込み直後に自己チェック(開発ループで最も一般的)
zhl10n.py audit
# 2. ゲートとして使用:CI またはリリース前に、基準を満たさない場合は非ゼロで終了
zhl10n.py audit --fail-on P0 # ブロッキングレベルがあれば失敗
zhl10n.py audit --fail-on P1 # デフォルトの厳格さ。リリースにはこれを使用推奨
zhl10n.py audit --fail-on P2 # ヒントもクリアする必要あり
# 3. 回帰比較:今回の変更で前回より増えた問題を確認
zhl10n.py audit --baseline
apply の終了時にも自動的に audit が 1 回実行され、結果が出力されます。書き込みフェーズで、知らないうちに不良パッケージが生成されることはありません。
監査結果の活用方法
- まず
P0を確認します。P0 が 1 つでもあるとリリース不可です。'ゲームが起動しない可能性がある' ことを意味します。 - 次に
P1を確認します。CV1は翻訳漏れリスト、FM1はクラッシュ必須リスト、RT1はすでにクラッシュしているという証拠です。 P2はチェックリストとして使用します。CV1bはエンジンの制約によるもので、あなたの漏れではないことを示します。CV5/CS4は直接削除可能な不要エントリです。fixlist.csvを 1 行ずつ修正し、修正後再実行して、P0=0 かつ P1=0になるまで繰り返します。
設定(l10n.json)
{
"project": "ゲーム名", "version": "1.0",
"source": {
"dll": "…/Managed/Assembly-CSharp.dll", // 位置特定のみに使用。extract は backup を読み取る
"backup": "…/_Backup/Assembly-CSharp.dll", // 変更されていないオリジナルである必要がある
"assets": ["…/resources.assets"],
"logs": ["…/Player.log"] // オプション。RT1 用
},
"output": { "dir": "out" },
"text": { "map": "trans_map.json", "site_map": "site_map.json", "enum_map": "enum_map.json" },
"glossary": "glossary.csv",
"font": { "subset": "font/zh-subset.ttf", "source": "font/NotoSansSC-Bold.otf", "targets": {} },
"audit": { "severity_fail": "P1", "max_expansion_ratio": 2.5, "max_expansion_abs": 40 },
"risk": { "extra_patterns": [], "extra_allow": [] }
}
2 つのエスケープハッチ。エラーが発生した場合は、まずこれらを検討します。
risk.extra_patterns—— プロジェクト内で翻訳できないことがわかっている文字列(正規表現)。例:^Levels$risk.extra_allow—— 翻訳する必要がないことが確認されている特定の文字列。これに追加すると、翻訳漏れとしてカウントされなくなります
用語集 glossary.csv:en,zh,forbidden の 3 列。forbidden は | で同義語を区切ります。CS2/CS3 はこれに依存します。存在しない場合は、これらの項目はスキップされます。
判定ルール:絶対に翻訳してはいけない文字列
一言で言えば:コード内で「キー」として使用される文字列は、翻訳すると見つからなくなります。
| カテゴリ | 例 | 翻訳した場合の影響 |
|---|---|---|
| リソースパス | "Levels"、"UI/Fonts/X" | レベルテーブルが空 → セーブデータ復元時に NRE → ローディング画面でフリーズ |
| 入力アクション/フィールド名 | Move/Up/Button | キーバインドが機能しなくなる |
| セーブデータフィールド | legacySkillPointCredit | セーブデータが読み込めなくなる |
| 列挙型名 | Deposit(ToString() 以外に Enum.Parse もある) | パースに失敗する |
| 正規表現パターン | ([0-9.]+)% more coins | ヒントテキストの書き換えが機能せず、英語に戻る |
| オブジェクト/リソース名 | new GameObject("Display glass") | 通常はクラッシュしないが、リソース名を汚染する |
| フォーマット文字列 | {0:N0} / {1:N0} | String.Format が例外をスローする |
同じ形でも異なる運命:Pause は設定メニューではボタンテキスト(翻訳すべき)だが、GameInput ではアクション名(翻訳すべきでない)。
テキストの見た目だけでは決して区別できません。「どの型、どのメソッドに出現するか」に依存します。これが risk.py の _code_shape() の役割です。
- オブジェクトを作成するメソッド(
.ctor/Build/Initialize/Create/Ensure…)内のリテラル → 名前、翻訳しない - UI を描画するメソッド(
Draw/OnGUI/Format/get_…)内 → テキスト、翻訳する - 比較/検索を行うメソッド(
Is/Has/Matches/Find…)内 → マッチングに使用されるキー、翻訳しない
新しいルールを追加した後は、ネガティブテストで検証する必要があります。翻訳テーブルに意図的に "Levels": "等级" を追加し、監査で P0 が報告されることを確認します。
フォントの注意点
埋め込み動的フォントは等長インプレース置換のみ可能です。resources.assets 全体を書き換えると、Texture2D の m_StreamData オフセットが変更され、テクスチャがすべてずれます。
フロー:font フェーズでは、「実際に使用される漢字のセット」に基づいてサブセット化し、TTF(Unity は CFF/OTF のサポートが不安定)に変換し、m_FontData に書き戻します。グリフ数が足りない場合は、フォントサイズを小さくするか、ウェイトを圧縮します。元のサイズを超えることはできません。
Bangers-Regular のような数十 KB しかないフォントには中国語を収容できません。フォントパスを別の中華フォントにリダイレクトする(l10n.json の font.targets で設定)方が、無理に詰め込むより信頼性が高くなります。
納品前チェックリスト
[ ] audit --fail-on P1 に合格
[ ] 実際にゲームを起動し、画面ごとに確認:メインメニュー / 設定 / セーブデータ一覧 / ゲーム内 HUD / ポップアップ / エラー表示
[ ] Player.log に新しい例外がない(RT1 が 0)
[ ] バックアップオリジナルの場所をリリースノートに記載(復元はこれに依存)
[ ] 成果物の MD5 を記録し、バージョンリリースノートに記載
破損箇所の特定方法:まず DLL だけをオリジナルに戻し、他はそのままにする。正常に動作すれば、問題はアセンブリパッチにあることが確定します。次に DLL だけをパッチ済みのものに戻し、他はそのままにして再現するか確認します。二分法はコードを読むより高速です。
参考資料
references/flow.md—— 各フェーズの詳細な実装とデータ構造references/audit-spec.md—— 31 項目のチェックの完全な定義、レベル、修正手順references/risk-rules.md—— リスクルールテーブルとその背後にある実際のインシデントassets/l10n.example.json/assets/glossary.example.csv—— 設定テンプレート