これは、KMC Advent Calender 2025の11日目の記事です。
こんにちは。KMC48代のcreeamsodaです。前回は、sashiさんのFortran と C++ を Visual Studio で連携する でした。研究室に入るとレガシーと戦う必要もあるのかと思うと少し怖くなります。研究室のことを何も考えていない現状はちょっとまずいかも。
次回(!?)は、noraさんのコード大紹介 - アドヴェントカレンダァァァァァァァ(2025) でした。DTM良いですよね。今まではチャンバラの活動日と被っていたこともあって触れてこなかったのですが、来年度からはDTM練習会が土曜日に移る計画があり、来年はDTM元年になるかもしれません。
さあ、12月11日に公開するはずだったこの記事ですが、課題に追われたり、部内の例会講座の準備をしたり、(学マスの石集めに苦しんだり、)しているうちにあっという間にクリスマスになっていました。師走とはまさにこのこと。
前置きはこのぐらいにして、今回はUnityのLocalizationについての話です。僕も最近まで知らなかったのですが、Unityには標準でローカライズ機能が提供されていて、テキストを言語ごとに切り替えたり、アセットを差し替えたりできるようになっています。
最初からローカライズを意識して進められればベストですが、なかなかそういうわけにはいかず、もう作り始めたプロジェクトに後付けでローカライズ機能を実装することになる場面も多いのではないでしょうか。今回は、そんな場面でいい感じにローカライズを追加する方法をまとめました。
パッケージのインストール
まずは、プロジェクトにLocalizationパッケージを導入します。Unity6では、Window>Package Management>Package ManagerでPackage Managerを開き、Unity Registryのタブ内で検索すると出てくるので、installボタンを押します。

使い方
基本的な使い方としては、ネット上にたくさん記事があるので簡単に。
- Project SettingsでLocalize Settingを設定
- String Tableを追加
- TextまたはTextMeshProコンポーネントが付いたオブジェクトにLocalize String Eventコンポーネントを追加
- コンポーネントにString Tableとそのテキストのkeyを登録
という感じの流れです。Project Settingsをいじったり、String Tableを追加したりするのは簡単なのですが、問題はLocalize String Eventコンポーネントを追加する操作です。Text/TextMeshProコンポーネントを持つオブジェクト全てに追加する必要があるため、手作業で行うのはなかなか大変です。そこで今回は、Editor Scriptを使ってScene上のオブジェクトとPrefabにコンポーネントの追加の追加を行います。
Textコンポーネントを検索してローカライズを追加するEditor Script
スクリプトは以下の通りです。GetComponentsInChildrenでTextとTextMeshProを取り出し、Localize String Eventコンポーネントを追加しています。
using System;
using UnityEngine.Events;
using UnityEditor;
using UnityEditor.Events;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using TMPro;
using UnityEngine.Localization.Components;
namespace Editor
{
public static class SceneLocalizeAutoSetup
{
[MenuItem("Tools/Localization/Setup LocalizeStringEvent In Scene")]
static void SetupLocalizeStringEvent()
{
int count = 0;
var scene = SceneManager.GetActiveScene();
var roots = scene.GetRootGameObjects();
foreach (var root in roots)
{
var texts = root.GetComponentsInChildren<Text>(true);
Debug.Log("texts cout = "+texts.Length);
foreach (var t in texts)
{
var go = t.gameObject;
// Prefabのインスタンスだった場合は無視する(Prefab側でもLocalizeStringEventを追加した場合、二重に追加してしまうから
if (PrefabUtility.IsPartOfAnyPrefab(go)) continue;
// --- UGUI Text ---
SetupForText(go, t);
count++;
}
var tmpTexts = root.GetComponentsInChildren<TextMeshProUGUI>(true);
Debug.Log("tmpText count = "+tmpTexts.Length);
foreach (var t in tmpTexts)
{
// --- TMP ---
var go = t.gameObject;
// Prefabのインスタンスだった場合は無視する(Prefab側でもLocalizeStringEventを追加した場合、二重に追加してしまうから)
if (PrefabUtility.IsPartOfAnyPrefab(go)) continue;
SetupForTMP(go, t);
count++;
}
}
Debug.Log($"LocalizeStringEvent 設定完了: {count} オブジェクト");
}
// =========================
// UGUI Text
// =========================
static void SetupForText(GameObject go, Text text)
{
Undo.RecordObject(go, "Add LocalizeStringEvent");
var localize = go.GetComponent<LocalizeStringEvent>();
if (localize == null)
{
localize = Undo.AddComponent<LocalizeStringEvent>(go);
}
// 既存イベントをクリア
int persistentEventCount = localize.OnUpdateString.GetPersistentEventCount();
for (int i = persistentEventCount - 1; 0<=i; i--)
{
UnityEventTools.RemovePersistentListener(localize.OnUpdateString, i);
}
var setMethod = typeof(Text).GetProperty("text")?.GetSetMethod();
if (setMethod != null)
{
// Setterを呼ぶUnityActionを作成
var methodDelegate =
(UnityAction<string>)Delegate.CreateDelegate(typeof(UnityAction<string>), text, setMethod);
// Text.text を永続イベントとして登録
UnityEventTools.AddPersistentListener(
localize.OnUpdateString,
methodDelegate
);
}
EditorUtility.SetDirty(localize);
EditorUtility.SetDirty(text);
}
// =========================
// TMP
// =========================
static void SetupForTMP(GameObject go, TextMeshProUGUI tmp)
{
Undo.RecordObject(go, "Add LocalizeStringEvent");
var localize = go.GetComponent<LocalizeStringEvent>();
if (localize == null)
{
localize = Undo.AddComponent<LocalizeStringEvent>(go);
}
// 既存イベントをクリア
int persistentEventCount = localize.OnUpdateString.GetPersistentEventCount();
for (int i = persistentEventCount - 1; 0<=i; i--)
{
UnityEventTools.RemovePersistentListener(localize.OnUpdateString, i);
}
// TMP_Text.SetText(string) を永続イベントとして登録
UnityEventTools.AddPersistentListener(
localize.OnUpdateString,
tmp.SetText
);
EditorUtility.SetDirty(localize);
EditorUtility.SetDirty(tmp);
}
}
}
ポイントは、UnityEventTools.AddPersistentListenerです。Localizationでは、言語の切り替わりのタイミングなどでテキストの更新イベントが走るのですが、コンポーネントを追加しただけではそのイベントを監視する設定ができていない状態です。Editor Script側で自身のテキスト更新メソッドをイベントに追加する必要があります。UnityEventTools.AddPersistentListenerを使うことで、Inspectorウィンドウでよく見るこの部分にメソッドを登録できます。

このスクリプトをAssets/Editorフォルダに追加した上で、UnityEditorのツールバーのTools>Localization>Setup LocalizeStringEvent In Sceneを押すと現在開いているシーン上の、TextかTextMeshProコンポーネントがついている全てのGameObjectにLocalize String Eventコンポーネントを追加し、イベントの登録を行います。
注意が必要なのが、実行するたびにLocalize String EventコンポーネントのOnUpdateStringを全てクリアしていることです。もし、すでにこのイベントに何かを追加していた場合、それが上書きされてしまいます。
Prefabのインスタンスなどは無視するようにしています。次のPrefabにコンポーネントを追加するスクリプトを実行した際に、コンポーネントが二重に追加されるのを防ぐためです。エディタ上で青字で表示されるインスタンスを無視する(実際にはPrefabUtility.IsPartOfAnyPrefabを使って判定しています)ので、Prefabのインスタンスに個別に追加して、Prefab本体には含まれていない子オブジェクトなどにはコンポーネントが追加されます。
PrefabにLocalize String Eventコンポーネントを追加するEditor Scriptは以下の通りです。
using System;
using UnityEngine.Events;
using UnityEditor;
using UnityEditor.Events;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using UnityEngine.Localization.Components;
namespace Editor
{
public static class PrefabLocalizeAutoSetup
{
[MenuItem("Tools/Localization/Setup LocalizeStringEvent In Prefabs")]
static void SetupPrefabs()
{
string[] guids = AssetDatabase.FindAssets(
"t:Prefab",
new[] { "Assets/Prefabs" }
);
int modifiedPrefabCount = 0;
int targetCount = 0;
foreach (var guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
GameObject root = PrefabUtility.LoadPrefabContents(path);
bool modified = false;
var texts = root.GetComponentsInChildren<Text>(true);
foreach (var text in texts)
{
SetupForText(text.gameObject, text);
targetCount++;
modified = true;
}
var tmps = root.GetComponentsInChildren<TextMeshProUGUI>(true);
foreach (var tmp in tmps)
{
SetupForTMP(tmp.gameObject, tmp);
targetCount++;
modified = true;
}
if (modified)
{
PrefabUtility.SaveAsPrefabAsset(root, path);
modifiedPrefabCount++;
}
PrefabUtility.UnloadPrefabContents(root);
}
Debug.Log(
$"完了: Prefab {modifiedPrefabCount} 個 / 対象 Text・TMP {targetCount} 個"
);
}
// =========================
// UGUI Text
// =========================
static void SetupForText(GameObject go, Text text)
{
var localize = go.GetComponent<LocalizeStringEvent>();
if (localize == null)
{
localize = go.AddComponent<LocalizeStringEvent>();
}
// 既存イベントをクリア
int persistentEventCount = localize.OnUpdateString.GetPersistentEventCount();
for (int i = persistentEventCount - 1; 0<=i; i--)
{
UnityEventTools.RemovePersistentListener(localize.OnUpdateString, i);
}
var setMethod = typeof(Text).GetProperty("text")?.GetSetMethod();
if (setMethod != null)
{
// Setterを呼ぶUnityActionを作成
var methodDelegate =
(UnityAction<string>)Delegate.CreateDelegate(typeof(UnityAction<string>), text, setMethod);
// Text.text を永続イベントとして登録
UnityEventTools.AddPersistentListener(
localize.OnUpdateString,
methodDelegate
);
}
EditorUtility.SetDirty(localize);
EditorUtility.SetDirty(text);
}
// =========================
// TMP
// =========================
static void SetupForTMP(GameObject go, TextMeshProUGUI tmp)
{
var localize = go.GetComponent<LocalizeStringEvent>();
if (localize == null)
{
localize = go.AddComponent<LocalizeStringEvent>();
}
// 既存イベントをクリア
int persistentEventCount = localize.OnUpdateString.GetPersistentEventCount();
for (int i = persistentEventCount - 1; 0<=i; i--)
{
UnityEventTools.RemovePersistentListener(localize.OnUpdateString, i);
}
// TMP 用永続イベント
UnityEventTools.AddPersistentListener(
localize.OnUpdateString,
tmp.SetText
);
EditorUtility.SetDirty(localize);
EditorUtility.SetDirty(tmp);
}
}
}
AssetsDatabaseを起点にPrefabを取得し、あとはコンポーネントをチェックしつつ必要に応じてLocalizeStringEventコンポーネントを追加するという同じ流れを踏んでいます。異なるのは、コンポーネントを追加した後に、PrefabUtility.SaveAsPrefabAssetメソッドを呼んで変更を保存していることぐらいです。
これを実行すると、Assets/Prefabsフォルダ以下に配置されているPrefabで、TextまたはTextMeshProコンポーネントがついたオブジェクトにLocalize String Eventコンポーネントを追加して、テキスト更新イベントへの自身の登録を行います。こちらもすでに登録されていたテキスト更新イベントの登録状況を上書きすることに注意が必要です。
String Tableへの自動追加を行うEditor Script
前の章ではコンポーネントの追加を行いましたが、これだけでは設定は終わっていません。追加したコンポーネントに対して、翻訳テキストのデータが入ったString Tableを渡した上で、そのTable上でのどのテキストを当てはめるのかを示すkeyを登録する必要があります。

このコンポーネントでいう、String ReferenceのTable CollectionにString Tableの名前(ここでは"StringTable")を入れて、Entry Nameに翻訳テキストのkey(ここでは”りんご”)を入れる必要があります。設定できると、String Table内の翻訳文が読み込まれて、写真でいうEnglishやJapaneseのところにappleやりんごが表示されていることがわかります。
String Tableはこんな感じになっています。ツールバーのWindow>Asset Management>Localization Tablesから見れます。

左端の列がkeyで、縦列が言語ごとの列、横列がそのkeyに対する翻訳テキストのバリエーションです。ローカライズを実装する際には、いい感じのkeyを設定した上で、翻訳文を手動でこのString Tableに追加することになります。
本来は、keyに"NextButton"や"TutorialText"などいい感じのものをつけたいところですが、後付けずぼらローカライズにはそんな余裕はないので、現在オブジェクトに設定されている日本語のテキストをそのままkeyとしてStringTableに新規登録することにします。
Scene上のオブジェクト用のEditor Scriptはこちらです。
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
using TMPro;
using UnityEditor.Localization;
using UnityEngine.Localization;
using UnityEngine.Localization.Components;
namespace Editor
{
public static class SceneLocalizeStringTableAutoSetup
{
// string table collectionのパス
private const string STRING_TABLE_COLLECTION_PATH = "Assets/Localization/StringTable.asset";
[MenuItem("Tools/Localization/Setup And Add New Entry To String Table In Scene")]
static void SetupLocalizeStringEvent()
{
int count = 0;
var scene = SceneManager.GetActiveScene();
var roots = scene.GetRootGameObjects();
var stringTableCollection = AssetDatabase.LoadAssetAtPath<StringTableCollection>(STRING_TABLE_COLLECTION_PATH);
if (stringTableCollection == null)
{
Debug.Log("String Tableが見つかりません");
}
foreach (var root in roots)
{
var localizeStringEvents = root.GetComponentsInChildren<LocalizeStringEvent>(true);
foreach (var localize in localizeStringEvents)
{
var go = localize.gameObject;
// Prefabのインスタンスだった場合は無視する(Prefab側でもLocalizeStringEventを追加した場合、二重に追加してしまうから
if (PrefabUtility.IsPartOfAnyPrefab(go)) continue;
Setup(go, localize, stringTableCollection);
count++;
}
}
Debug.Log($"LocalizeStringEvent 設定完了: {count} オブジェクト");
}
static void Setup(GameObject go, LocalizeStringEvent localizeStringEvent, StringTableCollection stringTableCollection)
{
Undo.RecordObject(go, "Add Settings to LocalizeStringEvent");
Text text = go.GetComponentInChildren<Text>();
TextMeshProUGUI tmp = go.GetComponentInChildren<TextMeshProUGUI>();
if (text == null && tmp == null) return;
if(text != null)
{
SetupForText(text, localizeStringEvent, stringTableCollection);
}else if (tmp != null)
{
SetupForTMP(tmp, localizeStringEvent, stringTableCollection);
}
}
// =========================
// Text(Legacy)
// =========================
static void SetupForText(Text text, LocalizeStringEvent localizeStringEvent, StringTableCollection stringTableCollection)
{
// 新しいエントリーとして、現在の登録テキストをそのままkeyにして追加する
string key = text.text;
string EntryInJapanese = text.text;
AddEntryFromText(stringTableCollection, key, EntryInJapanese);
localizeStringEvent.StringReference = new LocalizedString(
stringTableCollection.TableCollectionName, key
);
EditorUtility.SetDirty(localizeStringEvent);
EditorUtility.SetDirty(text);
}
// =========================
// TMP
// =========================
static void SetupForTMP(TextMeshProUGUI tmp, LocalizeStringEvent localizeStringEvent, StringTableCollection stringTableCollection)
{
// 新しいエントリーとして、現在の登録テキストをそのままkeyにして追加する
string key = tmp.text;
string EntryInJapanese = tmp.text;
AddEntryFromText(stringTableCollection, key, EntryInJapanese);
localizeStringEvent.StringReference = new LocalizedString(
stringTableCollection.TableCollectionName, key
);
EditorUtility.SetDirty(localizeStringEvent);
EditorUtility.SetDirty(tmp);
}
static void AddEntryFromText(
StringTableCollection tables,
string key,
string entryInJapanese
)
{
foreach (var table in tables.StringTables)
{
if (table.GetEntry(key) != null)
continue;
// 日本語テーブルだけ初期文字列を入れる
string value =
table.LocaleIdentifier.Code == "ja-JP" ? entryInJapanese : "";
table.AddEntry(key, value);
EditorUtility.SetDirty(table);
}
AssetDatabase.SaveAssets();
}
}
}
パスを元にAssetDatabaseから手動で作成したStringTableCollectionを取得します。これは、言語ごとの翻訳テキストが入っているStringTableが、その言語で実装されている言語分まとめられたコレクションです。流れとしては、TextまたはTextMeshProコンポーネントに現在入れられているテキストをkeyとしてStringTableを検索し、まだ追加されていなければ新規追加して、日本語の欄にとりあえず現在コンポーネントに設定されているテキストを入れる形です。これをTools>Localization>Setup And Add New Entry To String Table In Sceneから実行すると、現在のテキストをkeyとしつつ、StringTableの日本語のところに現在のテキストが入れられます。他の言語のところは空になります。後から手動で翻訳してStringTableに追加してください。
注意点としては、前半のコンポーネントの追加部分と同様に、Prefabのインスタンスに関しては無視されることと、同じテキストに対しては同じ翻訳文を当てはめることになることです。例えば、タイトル画面の「進む」は"Start Game"に翻訳したいけど、リザルト画面の「進む」は”Back To Title”にする、といったことはできません。手動で頑張ってください。
こちらはPrefab用のEditor Scriptになります。
namespace Editor
{
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using UnityEditor.Localization;
using UnityEngine.Localization;
using UnityEngine.Localization.Components;
public static class PrefabLocalizeStringTableAutoSetup
{
// string table collectionのパス
private const string STRING_TABLE_COLLECTION_PATH = "Assets/Localization/StringTable.asset";
[MenuItem("Tools/Localization/Setup And Add New Entry To String Table In Prefabs")]
static void SetupLocalizeStringEvent()
{
var stringTableCollection = AssetDatabase.LoadAssetAtPath<StringTableCollection>(STRING_TABLE_COLLECTION_PATH);
if (stringTableCollection == null)
{
Debug.Log("String Tableが見つかりません");
}
string[] guids = AssetDatabase.FindAssets(
"t:Prefab",
new[] { "Assets/Prefabs" }
);
int modifiedPrefabCount = 0;
int targetCount = 0;
foreach (var guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
GameObject root = PrefabUtility.LoadPrefabContents(path);
bool modified = false;
var localizeStringEvents = root.GetComponentsInChildren<LocalizeStringEvent>(true);
foreach (var localize in localizeStringEvents)
{
var go = localize.gameObject;
Setup(go, localize, stringTableCollection);
targetCount++;
modified = true;
}
if (modified)
{
PrefabUtility.SaveAsPrefabAsset(root, path);
modifiedPrefabCount++;
}
PrefabUtility.UnloadPrefabContents(root);
}
Debug.Log($"完了: Prefab {modifiedPrefabCount} 個 / 対象 Text・TMP {targetCount} 個");
}
static void Setup(GameObject go, LocalizeStringEvent localizeStringEvent, StringTableCollection stringTableCollection)
{
Undo.RecordObject(go, "Add Settings to LocalizeStringEvent");
Text text = go.GetComponentInChildren<Text>();
TextMeshProUGUI tmp = go.GetComponentInChildren<TextMeshProUGUI>();
if (text == null && tmp == null) return;
if(text != null)
{
SetupForText(text, localizeStringEvent, stringTableCollection);
}else if (tmp != null)
{
SetupForTMP(tmp, localizeStringEvent, stringTableCollection);
}
}
// =========================
// Text(Legacy)
// =========================
static void SetupForText(Text text, LocalizeStringEvent localizeStringEvent, StringTableCollection stringTableCollection)
{
// 新しいエントリーとして、現在の登録テキストをそのままkeyにして追加する
string key = text.text;
string EntryInJapanese = text.text;
AddEntryFromText(stringTableCollection, key, EntryInJapanese);
localizeStringEvent.StringReference = new LocalizedString(
stringTableCollection.TableCollectionName, key
);
EditorUtility.SetDirty(localizeStringEvent);
EditorUtility.SetDirty(text);
}
// =========================
// TMP
// =========================
static void SetupForTMP(TextMeshProUGUI tmp, LocalizeStringEvent localizeStringEvent, StringTableCollection stringTableCollection)
{
// 新しいエントリーとして、現在の登録テキストをそのままkeyにして追加する
string key = tmp.text;
string EntryInJapanese = tmp.text;
AddEntryFromText(stringTableCollection, key, EntryInJapanese);
localizeStringEvent.StringReference = new LocalizedString(
stringTableCollection.TableCollectionName, key
);
EditorUtility.SetDirty(localizeStringEvent);
EditorUtility.SetDirty(tmp);
}
static void AddEntryFromText(
StringTableCollection tables,
string key,
string entryInJapanese
)
{
foreach (var table in tables.StringTables)
{
if (table.GetEntry(key) != null)
continue;
// 日本語テーブルだけ初期文字列を入れる
string value =
table.LocaleIdentifier.Code == "ja-JP" ? entryInJapanese : "";
table.AddEntry(key, value);
EditorUtility.SetDirty(table);
}
AssetDatabase.SaveAssets();
}
}
}
これはオブジェクトの取得方法以外はScene上のオブジェクト用のスクリプトと同じなので特に言うことはありません。
おわりに
さてさて、今回はすでにたくさんTextコンポーネントが追加されている状態を想定して、自動でコンポーネントの追加やString Tableの設定を行うEditor Scriptを作成しました。僕は制作したゲームにローカライズを追加したことはない(!?)のですが、ゲームをリリースすることがあったらこのスクリプトを使おうと思います。
それでは、メリークリスマス。










