Claude CodeでUnityのテキストをローカライズする — String Table移行の手順
C#に直書きした文字列を洗い出し、UnityのLocalizationパッケージのString Tableへ移し、CSVで翻訳と未翻訳の検査を回す手順です。Claude Codeに任せる範囲と規約の書き方も示します。
ハードコード文字列をString Tableへ移す流れ
Unityのゲームで画面に出す文字列をLocalizationパッケージのString Tableへ移し、言語ごとに翻訳を持たせるまでの作業は、Claude Codeに任せやすい部分と、Unity Editorで人が操作する部分に分かれます。C#の書き換え、CSVの編集、翻訳漏れの検査はClaude Codeの得意分野です。Localization Settingsの作成やロケールの追加、Addressablesのビルドは、Editorの画面操作が中心になります。
| 工程 | 担当 | 成果物 |
|---|---|---|
| 直書き文字列の洗い出し | 担当Claude Code | 成果物一覧とキー名の案 |
| String Tableの作成・ロケール追加 | 担当Unity Editor(人) | 成果物String Table Collection |
| C#の置き換え | 担当Claude Code | 成果物LocalizedStringを使うスクリプト |
| 翻訳の入力 | 担当Claude Code + CSV | 成果物翻訳済みCSV |
| 未翻訳・崩れの検査 | 担当スクリプト | 成果物要確認キーの一覧 |
| 見た目の確認 | 担当Unity Editor(人) | 成果物疑似ローカライズの画面 |
Unity Editor全般のCLI連携はClaude CodeでUnityゲーム開発を進めるにまとめています。Editor側の操作もClaude Codeから行いたいときは、UnityのMCPサーバーの接続手順が参考になります。
作業の順番
- 1
直書きを洗い出す
C#のgrepで文字列リテラルを拾い、キー名の案を付けます。
- 2
String Tableを用意する
Editorでパッケージ導入、ロケール生成、Table Collection作成を済ませます。
- 3
C#を置き換える
文字列リテラルを
LocalizedStringに差し替えます。 - 4
CSVで翻訳する
書き出し、翻訳、Merge取り込みを往復します。
- 5
検査する
未翻訳とプレースホルダーのずれをスクリプトで拾います。
前提とパッケージの導入
Localizationパッケージ(com.unity.localization)の1.5系は、Unity Editor 2019.4以降に対応しています。Package Managerに表示されるのは2021.2以降で、それより前のバージョンでは「Add package from git URL」にcom.unity.localizationを入力します。
導入後の初期設定はEditorで行います。
Edit > Project Settings > LocalizationでCreateを押し、Localization Settingsアセットを作る- 同じ画面の
Locale Generatorで、使う言語(例: English、Japanese)にチェックを入れてGenerate Localesを押す - Specific Locale Selectorで既定のロケールを選び、Project Locale Identifierも開発に使っている言語に合わせる
Window > Asset Management > Localization TablesでNew Table Collectionを作り、種類にString Table Collectionを選ぶ
Project Localeは、ローカライズ作業の基準になる言語です。Quick Start Guideでは、プロジェクトを開発している言語を指定すると説明されています。
String Table Collectionを作ると、ロケールごとのString Tableアセットと、キー名とIDを持つShared Table Dataが1つ生成されます。以降の作業で触るのはこのCollectionです。
直書き文字列を洗い出す
最初の作業は、画面に出ている文字列がどこにあるかの一覧作りです。C#側はgrepで拾えます。
grep -rnE '\.text\s*=\s*"|SetText\("|\.text\s*=\s*\$"' \
Assets --include='*.cs' > /tmp/hardcoded.txt
wc -l /tmp/hardcoded.txtこの正規表現が拾うのは、label.text = "..."とSetText("...")、補間文字列の代入の3種類です。new GUIContent("...")や独自のUIラッパー経由の文字列は拾えないので、プロジェクトの流儀に合わせてパターンを足します。Claude Codeに「このリポジトリでUIに文字列を渡している書き方を全部挙げて、grepパターンに加えて」と頼むと、リポジトリを読んで抜けを補います。
シーンやプレハブのインスペクターに直接入力したテキストは、C#のgrepに現れません。こちらは疑似ローカライズ(Pseudo-Localization)が見つけてくれます。ローカライズの仕組みを通っていない直書き文字列は、疑似ローカライズが拾う問題の1つです。手順は見た目の確認の節に置きました。
洗い出しの結果は、そのまま置き換えに入らず、キー名を付けた表にします。Claude Codeには次のように頼みます。
/tmp/hardcoded.txt の各行について、ファイル名・行・元の文字列・
キー名の案を表にして。キー名は CLAUDE.md の命名規約に従うこと。
同じ文字列が複数箇所にあるときは同じキーにまとめて。
補間している文字列は、変数部分を {名前} のプレースホルダーにした形も書いて。CLAUDE.mdにキーと文字列の規約を置く
キー名の付け方や置き換えの形は、CLAUDE.mdに書いておくと毎回の指示が短くなります。Claude Codeのメモリのドキュメントは、「コードを整形して」より「インデントは2スペース」のように、検証できる具体的な書き方を勧め、1ファイル200行未満を目安にしています。
## ローカライズの規約
- 画面に出す文字列は直書きしない。String Table Collection「GameText」にキーを追加し、`LocalizedString`で参照する
- キー名は `画面_用途` の大文字スネークケース(例: `HUD_SCORE`, `TITLE_START`)
- 一度公開したキー名は変えない。変えるときはCSVのId列を残したまま行う
- 数値や名前を埋め込む文は、文字列連結をやめてSmart Stringの名前付きプレースホルダーにする(例: `{Score}`)
- 翻訳の編集はCSVで行い、.assetファイルを直接編集しない
- 変更後は `python3 tools/check_loc.py` を実行し、要確認が0件であることを確認するキー名の形式は公式の決まりではなく、この例で置いた規約です。Unity側の制約は次の点です。Key Nameは後から変えられますがKey Idは変えられず、Unityはアセットやコンポーネントからの参照にKey Idを使います。
1行目の「String Table Collection『GameText』」は例です。実際のCollection名に合わせて書き換えます。
C#をLocalizedStringに置き換える
置き換えの核は、文字列リテラルをLocalizedStringのフィールドに変えることです。LocalizedStringはString Tableの1エントリを指し、ロケールが切り替わるたびにStringChangedイベントを呼びます。
スコア表示を置き換える
直書きの文字列連結
public class ScoreView : MonoBehaviour
{
[SerializeField] private TMP_Text label;
public void Show(int score)
{
label.text = "Score: " + score;
}
}LocalizedStringとSmart String
using TMPro;
using UnityEngine;
using UnityEngine.Localization;
public class ScoreView : MonoBehaviour
{
[SerializeField] private TMP_Text label;
[SerializeField] private LocalizedString scoreText; // HUD_SCORE: "Score: {Score}"
public int Score { get; private set; }
void OnEnable()
{
scoreText.Arguments = new object[] { this };
scoreText.StringChanged += OnChanged;
}
void OnDisable() => scoreText.StringChanged -= OnChanged;
void OnChanged(string s) => label.text = s;
public void Show(int score)
{
Score = score;
scoreText.RefreshString();
}
}右側の形は、公式のスクリプト解説にある「{TimeNow}というプレースホルダーに、スクリプト自身をArgumentsで渡す」例と同じ構成です。プレースホルダーの{Score}は、渡したオブジェクトのプロパティScoreを参照します。StringChangedの登録と解除をペアにする点も公式の例に沿っています。
Smart Stringとして扱うには、String Tableのエントリ側でSmartを有効にします。Localization Tablesウィンドウの各エントリ右下の縦3点メニューからSmart Formatを選ぶか、EditorスクリプトでIsSmartをtrueにします。有効にしないと{Score}は展開されません。
動的な値がなく、固定の文言だけなら、Editor上でTextコンポーネントを右クリックしてLocalizeを選ぶ方法もあります。Localize String Eventコンポーネントが追加され、スクリプトを書かずに済みます。
初回表示の遅れに注意する
String Tableは既定では事前読み込みされません。最初にテーブルへアクセスした瞬間は、非同期の読み込みが終わっておらず、文字列がすぐ得られない場合があります。起動直後の画面で空文字が一瞬見えるなら、Localization TablesウィンドウのPreloadを有効にします。StringChangedを使う書き方は、テーブルが用意できた時点で呼ばれるため、この遅れを自然に吸収します。
CSVで翻訳を往復する
エントリの追加と翻訳の入力は、CSVを経由するとClaude Codeが扱いやすくなります。Localization Tablesウィンドウの縦3点メニューからExport > CSVで書き出すと、1行が1文字列で、Key、Id、ロケールごとの列が並びます。
Key,Id,English(en),Japanese(ja)
HUD_SCORE,0,Score: {Score},
TITLE_START,0,Start,
TITLE_EXIT,0,Exit Game,新しい行を足すときは、Id列を空欄か0にします。Unityが新しいIDを割り当てます。取り込みにはKeyかIdのどちらかの列が必須で、他の列は省略できます。
Claude Codeには、英語の列は触らず、日本語の列だけを埋めるよう頼みます。
loc/GameText.csv の Japanese(ja) 列を埋めて。
- English(en) 列と Key, Id 列は変更しない
- {Score} のような波括弧のプレースホルダーは英語と同じものを残す
- ゲーム画面の文言なので簡潔に。丁寧語は使わず体言止めを基本にする取り込みはImportから2種類を選べます。
CSV: String Tableの内容をCSVで置き換えるCSV(Merge): CSVに含まれるエントリだけを更新する
翻訳列を埋めただけならCSV(Merge)で足ります。置き換えのCSVは、CSVに無いエントリの扱いが変わるので、他の人が別のキーを追加している状況では避けます。
落とし穴
キー名を変えたCSVを取り込むと別のキーが増える
キー名だけを書き換えてId列を消した状態で取り込むと、Unityは新しいキーとして登録します。CSV拡張の説明も、キー名を変えたときはIdを含めて書き出すのがよいとしています。既存キーの名前を変えるなら、Id列を残したまま行います。規約に「Id列を残す」と書いておく理由はここにあります。
未翻訳と崩れを検査するスクリプト
CSVに書き出せれば、未翻訳の検出は単純な文字列処理です。次のスクリプトは、基準言語と対象言語の列を比べて、3種類の異常を報告します。
- 対象言語の列が空欄
- 対象言語の文字列が原文と同一(未翻訳のまま残っている疑い)
- 波括弧のプレースホルダー名が基準言語と一致しない
#!/usr/bin/env python3
"""String TableのCSV書き出しを検査する。使い方: check_loc.py export.csv en ja"""
import csv, re, sys
PLACEHOLDER = re.compile(r"\{([\w\-.]+)")
def find_col(header, code):
for i, name in enumerate(header):
if name.endswith(f"({code})"):
return i
sys.exit(f"列が見つかりません: ({code})")
def main(path, base, target):
with open(path, newline="", encoding="utf-8-sig") as f:
rows = list(csv.reader(f))
header, body = rows[0], rows[1:]
b, t = find_col(header, base), find_col(header, target)
problems = []
for row in body:
key = row[0]
src, dst = row[b].strip(), row[t].strip()
if not dst:
problems.append((key, "未翻訳(空欄)"))
continue
if dst == src:
problems.append((key, "原文と同一(未翻訳の疑い)"))
if set(PLACEHOLDER.findall(src)) != set(PLACEHOLDER.findall(dst)):
problems.append((key, "プレースホルダーが不一致"))
for key, msg in problems:
print(f"{key}: {msg}")
print(f"{len(body)}件中{len({k for k, _ in problems})}件に要確認")
sys.exit(1 if problems else 0)
if __name__ == "__main__":
if len(sys.argv) != 4:
sys.exit(__doc__)
main(*sys.argv[1:])列名の判定は、CSVの見出しがEnglish(en)、Japanese(ja)の形であることを前提にしています。CSV解説の書き出し例と同じ形式です。
意図的に壊した5行のCSVでこのスクリプトを動かすと、次の出力になりました(Python 3.12)。
python3 -I check_loc.py sample.csv en ja
echo "exit=$?"UI_HP: 未翻訳(空欄)
UI_SCORE: プレースホルダーが不一致
UI_OK: 原文と同一(未翻訳の疑い)
5件中3件に要確認
exit=1UI_OKのように、「OK」が英語のままで正しいケースもあります。同一文字列は「疑い」止まりにして、人が判断する前提です。プレースホルダー名の比較は、このスクリプト独自の検査です。Unityが取り込み時に確認するわけではないので、{Score}を{Point}と書き間違えた翻訳は、取り込み後に展開されずにそのまま画面に出ます。
複数形の分岐({count:plural:...})は、言語ごとに分岐の数が異なります。Quick Start Guideでも、英語・フランス語・スペイン語は2つの複数形を持ち、日本語は複数形を使わず値を差し込むだけの例になっています。このスクリプトは分岐の中身を比べず、{の直後の名前だけを見ているので、言語ごとに分岐の数が違っても誤検知しません。
編集のたびに自動で検査する
CSVを編集するたびに検査を走らせたいときは、PostToolUseフックでEdit|Writeを対象にします。フックが終了コード2で終わると、ツールの実行後でもstderrの内容がClaudeに渡ります。検査スクリプトが要確認キーを標準エラーに出して終了コード2を返す形にすれば、Claudeは翻訳漏れを見て自分で直せます。matcherにEdit|Writeを指定する設定の構造は、Unityのコンパイル検証をフックに載せる例と同じです。
疑似ローカライズで見た目の崩れを確かめる
翻訳が入っても、文字が枠に収まるかは別問題です。疑似ローカライズは、本物の翻訳を入れる前に、文字数が増えたときの崩れを見つけるための仕組みです。次のような問題を見つけられます。
疑似ローカライズが見つける問題
スペース不足
翻訳後の文字列が枠に収まらず、折り返しや切れが起きる箇所です。
ローカライズされていない文字列
直書きのままで、ローカライズの仕組みを通っていない文字列です。
フォントの対応文字
必要な特殊文字がフォントに含まれているかの確認です。
文字を含む画像
テクスチャに文字が焼き込まれていて、翻訳できないアートです。
作り方はAssets > Create > Localization > Pseudo-Localeです。Source Localeに英語を選ぶと、英語のString Tableを読み込んで疑似変換します。既定のメソッドはAccenter、Expander、Encapsulatorの3つです。適用は、Game ViewのLocaleメニューか、スクリプトでLocalizationSettings.SelectedLocaleを疑似ロケールに変えて行います。
疑似ロケールに切り替えて画面を見ると、変換されずに英語のまま残っている文字列は、String Tableを通っていません。ここが、C#のgrepで拾えなかった直書きの検出になります。スクリーンショットをClaude Codeに見せて「変換されていないラベルを挙げて」と頼む使い方もできますが、見た目の判断は最終的に人が確認します。
日本語フォントの文字セットを書き出す
日本語対応でもう1つ要るのが、フォントアトラスに入れる文字の選定です。中国語は5万字を超える文字集合を持つ一方、多くのゲームで実際に使うのは多くても1,000字程度で、日本語・中国語を完全対応するゲームがその近辺になるとされています。
Unityは、String Table内で使われている文字を調べて、Character Setとして書き出せます。Smart Stringの場合は、プレースホルダー部分を除いた文字だけが対象です。書き出し先は、String TableウィンドウやString Table Collectionウィンドウのメニュー、GenerateCharacterSetの呼び出しなどです。出力したファイルは、TextMeshProのFont Asset Creatorに渡してフォントアトラスを作るのに使います。翻訳を追加するたびに文字セットを作り直す流れにしておくと、「表示されない文字」が出にくくなります。
日本語にしても英語のままのときの切り分け
実機で言語を切り替えても表示が変わらないときは、上から順に確かめます。
言語が切り替わらないときの確認順
- QLocaleの一覧に日本語が入っているか?答えがいいえのとき答えがはい
Localization Settingsの
Locale Generatorで日本語を生成し、Available Localesに入れます。 - QString Tableの日本語列にその文字列があるか?答えがいいえのとき答えがはい
CSVの検査スクリプトで空欄を拾い、翻訳を入れて
CSV(Merge)で取り込みます。 - Qプレイヤー用にAddressablesをビルドしたか?答えがいいえのとき答えがはい
Addressablesウィンドウで
Build > New Build > Default Build Scriptを実行します。
それでも変わらないとき
その文字列がC#の直書きのままか、LocalizedStringが別のTable Collectionを指していないかを見ます。
Quick Start Guideには、プレイヤーでローカライズされたアセットを使うにはAddressablesのプレイヤーコンテンツをビルドする必要がある、と書かれています。Editorで動いていてビルドで動かない症状は、まずここが原因になります。
実際の作業の割り振り
最後に、Claude Codeへ渡す依頼を1回分にまとめます。1つの画面や1つのシーンを単位に区切ると、差分が小さく確認しやすくなります。
タイトル画面(Assets/Scripts/Title/)の直書き文字列をローカライズして。
1. grepで文字列を洗い出し、キー名の案を表にして見せて(置き換えはまだしない)
2. 承認後、LocalizedStringに置き換える
3. loc/GameText.csv に新しい行を追加(Id は 0、Japanese(ja) も埋める)
4. python3 tools/check_loc.py loc/GameText.csv en ja を実行し、0件になるまで直すステップ1と2を分けるのは、キー名が後から変えにくいからです。名前を変えるたびにCSVのId列の扱いを気にすることになります。
Editorで人が行う操作は、String Table Collectionの作成、CSVの取り込み、疑似ロケールでの確認、Addressablesのビルドの4つです。それ以外のC#とCSVと検査は、規約とスクリプトを置いておけばClaude Codeが一巡させられます。この分担にすると、人の仕事は文字列を探すことから、要確認の一覧を見ることに移ります。国別に通貨や日付表記まで差し替えるWebページの場合は、海外向けLPのローカライズ手順が別の切り口で扱っています。