Claude Media
Claude CodeでUnityのテキストをローカライズする — String Table移行の手順

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. 1

    直書きを洗い出す

    C#のgrepで文字列リテラルを拾い、キー名の案を付けます。

  2. 2

    String Tableを用意する

    Editorでパッケージ導入、ロケール生成、Table Collection作成を済ませます。

  3. 3

    C#を置き換える

    文字列リテラルをLocalizedStringに差し替えます。

  4. 4

    CSVで翻訳する

    書き出し、翻訳、Merge取り込みを往復します。

  5. 5

    検査する

    未翻訳とプレースホルダーのずれをスクリプトで拾います。

前提とパッケージの導入

Localizationパッケージ(com.unity.localization)の1.5系は、Unity Editor 2019.4以降に対応しています。Package Managerに表示されるのは2021.2以降で、それより前のバージョンでは「Add package from git URL」にcom.unity.localizationを入力します。

導入後の初期設定はEditorで行います。

  1. Edit > Project Settings > LocalizationでCreateを押し、Localization Settingsアセットを作る
  2. 同じ画面のLocale Generatorで、使う言語(例: English、Japanese)にチェックを入れてGenerate Localesを押す
  3. Specific Locale Selectorで既定のロケールを選び、Project Locale Identifierも開発に使っている言語に合わせる
  4. 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イベントを呼びます。

前と後

スコア表示を置き換える

BEFORE

直書きの文字列連結

public class ScoreView : MonoBehaviour
{
    [SerializeField] private TMP_Text label;
 
    public void Show(int score)
    {
        label.text = "Score: " + score;
    }
}
AFTER

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=1

UI_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に渡してフォントアトラスを作るのに使います。翻訳を追加するたびに文字セットを作り直す流れにしておくと、「表示されない文字」が出にくくなります。

日本語にしても英語のままのときの切り分け

実機で言語を切り替えても表示が変わらないときは、上から順に確かめます。

切り分け

言語が切り替わらないときの確認順

  1. QLocaleの一覧に日本語が入っているか?
    答えがいいえのとき

    Localization SettingsのLocale Generatorで日本語を生成し、Available Localesに入れます。

    答えがはい
  2. QString Tableの日本語列にその文字列があるか?
    答えがいいえのとき

    CSVの検査スクリプトで空欄を拾い、翻訳を入れてCSV(Merge)で取り込みます。

    答えがはい
  3. 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のローカライズ手順が別の切り口で扱っています。

この記事を共有:XはてブLinkedIn