# 22 · 本地化系统(Localization System)
> **命名空间** `BaseGames.Localization`
> **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md)
> **Package 依赖** `com.unity.localization` (≥ 1.4)
> **依赖系统** `BaseGames.UI` · `BaseGames.Dialogue`
---
## 目录
1. [系统总览](#1-系统总览)
2. [语言包配置](#2-语言包配置)
3. [StringTable 结构](#3-stringtable-结构)
4. [LanguageManager — 语言切换管理器](#4-languagemanager--语言切换管理器)
5. [UI 组件本地化](#5-ui-组件本地化)
6. [对话系统本地化桥接](#6-对话系统本地化桥接)
7. [字体与排版规范](#7-字体与排版规范)
8. [数字 / 货币 / 时间格式](#8-数字--货币--时间格式)
9. [SaveData 扩展](#9-savedata-扩展)
10. [编辑器工作流](#10-编辑器工作流)
---
## 1. 系统总览
本地化系统基于 **Unity Localization** 包构建,提供**运行时语言切换**和**零硬编码字符串**的文本管理方案。
```
本地化系统架构:
├─ LocalizationSettings(Unity 包)
│ ├─ StringTableCollection → 按功能域划分的字符串表集合
│ ├─ AssetTableCollection → 本地化资产(字体、图片)
│ └─ Locale 列表 → zh-CN / en / ja(初始三语言)
│
├─ LanguageManagerSO → SO 单例,封装语言切换、持久化
├─ LocalizedStringComponent → TextMeshPro 文本组件的本地化桥接
├─ DialogueLocalizationBridge → DialogueSequenceSO → 本地化键 映射
└─ LocalizationKeys → 所有键名静态常量(编译期检查)
```
**设计原则**:
- UI 文本 → `LocalizedStringReference`(Unity 内置组件,在 Inspector 中选键名)
- 对话文本 → `DialogueLocalizationBridge`(键 = `"dialogue.{sequenceId}.{lineIndex}"`)
- 代码内不直接写任何面向玩家的字符串,统一走 `LocalizationKeys`
---
## 2. 语言包配置
在 `Assets/Settings/Localization/` 下配置 Unity Localization:
```
Localization/
├── LocalizationSettings.asset → LocalizationSettings(项目唯一)
├── Locales/
│ ├── zh-CN.asset → SimpleChinese(简体中文,默认语言)
│ ├── en.asset → English
│ └── ja.asset → Japanese
└── Tables/
├── UI_Table/ → UI 通用文本
├── Dialogue_Table/ → NPC / 系统对话
├── Items_Table/ → 物品名称与描述
├── Combat_Table/ → 战斗相关文本(HP 不足、BOSS 名等)
└── Settings_Table/ → 设置页面文本
```
### Locale 创建规范
```csharp
// 编辑器脚本:自动创建 Locale
// Tools > Zeling > Localization > Create Locales
[MenuItem("Tools/Zeling/Localization/Create Locales")]
static void CreateLocales()
{
var codes = new[] { ("zh-CN", "简体中文"), ("en", "English"), ("ja", "日本語") };
foreach (var (code, name) in codes)
{
var locale = Locale.CreateLocale(new SystemLanguage());
locale.LocaleName = name;
// ... 创建资产
}
}
```
---
## 3. StringTable 结构
### 键名规范
```
{域}.{子域}.{词条}
UI:
ui.menu.start = "开始游戏"
ui.menu.continue = "继续"
ui.menu.settings = "设置"
ui.menu.quit = "退出"
ui.hud.soul_label = "灵魂"
ui.hud.geo_label = "Geo"
ui.map.fast_travel = "快速旅行"
ui.map.undiscovered = "???"
ui.item.found = "获得物品"
ui.ability.unlocked = "获得能力"
ui.boss.defeated = "BOSS 已击败"
对话:
dialogue.{sequenceId}.{lineIndex}
dialogue.town_elderbug_01.0 = "又来了,年轻的旅者。"
dialogue.town_elderbug_01.1 = "王国啊……它已不再是从前的样子。"
物品:
item.simple_key.name = "简单的钥匙"
item.simple_key.desc = "一把普通的钥匙,不知锁的是什么门。"
item.pale_ore.name = "苍白矿石"
item.pale_ore.desc = "稀有的矿石,蕴含奇特的力量。"
战斗:
combat.parry_success = "弹反!"
combat.boss.false_knight = "伪骑士"
设置:
settings.language = "语言"
settings.sfx_volume = "音效音量"
settings.bgm_volume = "音乐音量"
settings.fullscreen = "全屏"
settings.vibration = "手柄震动"
```
### LocalizationKeys 常量类(编译期保障)
```csharp
public static class LocalizationKeys
{
// UI
public const string UI_MENU_START = "ui.menu.start";
public const string UI_MENU_CONTINUE = "ui.menu.continue";
public const string UI_MAP_FAST_TRAVEL = "ui.map.fast_travel";
public const string UI_ITEM_FOUND = "ui.item.found";
public const string UI_BOSS_DEFEATED = "ui.boss.defeated";
// 物品
public const string ITEM_SIMPLE_KEY_NAME = "item.simple_key.name";
// 战斗
public const string COMBAT_PARRY_SUCCESS = "combat.parry_success";
// 格式化辅助
/// 返回对话行键名 "dialogue.{id}.{idx}"
public static string DialogueLine(string sequenceId, int lineIndex)
=> $"dialogue.{sequenceId}.{lineIndex}";
/// 返回物品名称键名
public static string ItemName(string itemId) => $"item.{itemId}.name";
/// 返回物品描述键名
public static string ItemDesc(string itemId) => $"item.{itemId}.desc";
}
```
---
## 4. LanguageManager — 语言切换管理器
`LanguageManagerSO` 是项目全局语言管理的 SO 单例,通过 `LocalizationSettings` 操作语言切换:
```csharp
[CreateAssetMenu(menuName = "Localization/LanguageManager")]
public class LanguageManagerSO : ScriptableObject
{
[Header("支持的语言代码")]
public string[] supportedLocales = { "zh-CN", "en", "ja" };
[Header("默认语言")]
public string defaultLocale = "zh-CN";
[Header("事件频道")]
[SerializeField] StringEventChannelSO _onLanguageChanged;
/// 当前语言代码(如 "zh-CN")
public string CurrentLocale
=> LocalizationSettings.SelectedLocale?.Identifier.Code ?? defaultLocale;
/// 设置语言并持久化
public void SetLanguage(string localeCode)
{
var locale = LocalizationSettings.AvailableLocales
.Locales.Find(l => l.Identifier.Code == localeCode);
if (locale == null) return;
LocalizationSettings.SelectedLocale = locale;
PlayerPrefs.SetString("SelectedLocale", localeCode);
PlayerPrefs.Save();
_onLanguageChanged?.Raise(localeCode);
}
/// 游戏启动时恢复上次语言
public void RestoreSavedLanguage()
{
string saved = PlayerPrefs.GetString("SelectedLocale", defaultLocale);
SetLanguage(saved);
}
/// 同步获取本地化字符串(非异步,需提前加载表)
public string Get(string key, string tableCollection = "UI_Table")
{
var result = LocalizationSettings.StringDatabase
.GetLocalizedString(tableCollection, key);
return string.IsNullOrEmpty(result) ? $"[{key}]" : result;
}
/// 带参数格式化
public string GetFormatted(string key, string tableCollection, params object[] args)
{
string template = Get(key, tableCollection);
return string.Format(template, args);
}
}
```
---
## 5. UI 组件本地化
### 5.1 TextMeshPro 文本自动本地化
**推荐方式**:在 TextMeshProUGUI 同 GameObject 上添加 `LocalizeStringEvent`(Unity Localization 包内置组件):
```
Inspector:
LocalizeStringEvent
├─ String Reference: UI_Table / "ui.menu.start"
└─ Update String: TextMeshProUGUI.SetText(自动连接)
```
语言切换时 Unity 自动更新所有绑定了 `LocalizeStringEvent` 的 TMP 组件。
### 5.2 运行时动态文本
对于需要在代码中动态设置的文本:
```csharp
// 物品获取提示(动态拼接物品名)
public class ItemPickupNotification : MonoBehaviour
{
[SerializeField] TMP_Text _label;
[SerializeField] LanguageManagerSO _langMgr;
public void Show(string itemId)
{
// 正确:通过 LocalizationKeys 获取键名,再查询
string itemName = _langMgr.Get(LocalizationKeys.ItemName(itemId), "Items_Table");
string template = _langMgr.Get(LocalizationKeys.UI_ITEM_FOUND, "UI_Table");
_label.text = string.Format(template, itemName); // "{0} 已获取"
}
}
```
---
## 6. 对话系统本地化桥接
`DialogueLocalizationBridge` 在 `DialogueSequenceSO` 加载时,将文本内容替换为本地化版本:
```csharp
[RequireComponent(typeof(DialoguePlayer))]
public class DialogueLocalizationBridge : MonoBehaviour
{
[SerializeField] LanguageManagerSO _langMgr;
public DialogueLine[] Localize(string sequenceId, DialogueLine[] rawLines)
{
var localized = new DialogueLine[rawLines.Length];
for (int i = 0; i < rawLines.Length; i++)
{
localized[i] = rawLines[i]; // 值类型浅拷贝
string key = LocalizationKeys.DialogueLine(sequenceId, i);
string text = _langMgr.Get(key, "Dialogue_Table");
// 如果找到本地化键,覆盖原始文本;否则保留 DialogueSequenceSO 中的备用文本
if (!text.StartsWith("[")) // "[key]" 表示未找到
localized[i].text = text;
}
return localized;
}
}
```
**对话文本存储规范**:
- `DialogueSequenceSO.lines[i].text` 存储**备用文本**(英文或开发者备注)
- 正式文本统一在 **Dialogue_Table** 中维护
- 新增 NPC/对话时,同步在所有语言的表中添加条目(即使暂时只有一种语言文本)
---
## 7. 字体与排版规范
### 字体资产布局
```
Assets/Fonts/
├── zh-CN/
│ ├── NotoSerifSC-Regular_SDF.asset → 正文(宋体风格)
│ ├── NotoSerifSC-Bold_SDF.asset → 标题/加重
│ └── PixelFont_CJK_SDF.asset → 像素风(HUD 标签,伤害数字)
├── en/
│ ├── TrajanPro3-Regular_SDF.asset → 正文
│ └── TrajanPro3-Bold_SDF.asset → 标题
└── ja/
├── NotoSerifJP-Regular_SDF.asset
└── NotoSerifJP-Bold_SDF.asset
```
### 本地化字体切换(AssetTable)
通过 Unity Localization 的 **AssetTableCollection** 实现运行时字体切换:
```
AssetTableCollection: Fonts_Table
键名: zh-CN en ja
"font.body" → NotoSerifSC → TrajanPro3-Regular → NotoSerifJP
"font.title" → NotoSerifSC-Bold → TrajanPro3-Bold → NotoSerifJP-Bold
"font.hud" → PixelFont_CJK → PixelFont_EN → PixelFont_JP
```
`LocalizeFontGroup` 组件(在 Canvas 根节点)统一更新所有 TMP 字体资产:
```csharp
public class LocalizeFontGroup : MonoBehaviour
{
[Serializable]
struct FontEntry { public string key; public TMP_Text[] targets; }
[SerializeField] FontEntry[] _entries;
[SerializeField] LanguageManagerSO _langMgr;
void OnEnable()
=> LocalizationSettings.SelectedLocaleChanged += OnLocaleChanged;
void OnDisable()
=> LocalizationSettings.SelectedLocaleChanged -= OnLocaleChanged;
void OnLocaleChanged(Locale locale)
{
foreach (var entry in _entries)
{
var fontOp = LocalizationSettings.AssetDatabase
.GetLocalizedAssetAsync("Fonts_Table", entry.key);
fontOp.Completed += op =>
{
foreach (var t in entry.targets) t.font = op.Result;
};
}
}
}
```
### CJK 排版注意事项
- `TMP_Text.lineSpacing`:CJK 字体建议 +5%(行高较高)
- 中文不允许行首标点(句号/逗号/右括号)→ TMP 开启 `Line Breaking Rule` 选 `Chinese Simplified`
- 日文全角字符宽度 = 中文字符宽度,可复用布局
- 对话框固定宽度,文本内容通过 Unity Localization 中的 **Smart String** 处理变量替换
---
## 8. 数字 / 货币 / 时间格式
```csharp
public static class LocalizationFormat
{
/// 伤害数字显示(统一不使用千位分隔符,保持像素风格简洁)
public static string Damage(int value) => value.ToString();
/// Geo 货币显示
public static string Geo(int amount)
{
string locale = LocalizationSettings.SelectedLocale?.Identifier.Code ?? "zh-CN";
return locale switch
{
"zh-CN" => $"{amount} Geo",
"ja" => $"{amount}ジオ",
_ => $"{amount} Geo",
};
}
/// 游戏内时间(存档时长)
public static string PlayTime(float seconds)
{
int h = (int)(seconds / 3600);
int m = (int)(seconds % 3600 / 60);
int s = (int)(seconds % 60);
return $"{h:D2}:{m:D2}:{s:D2}";
}
}
```
---
## 9. SaveData 扩展
```json
"localization": {
"selectedLocale": "zh-CN"
}
```
> 语言设置存储于 `PlayerPrefs` 而非 SaveData,以便在读取存档前即可恢复语言(主菜单阶段)。
---
## 10. 编辑器工作流
### 本地化键扫描器
`Tools > Zeling > Localization > Scan Missing Keys`:
1. 扫描所有 `DialogueSequenceSO`,为每行对话生成期望键名
2. 扫描所有 `ItemDataSO`,生成期望 `item.{id}.name/desc` 键
3. 与 StringTable 实际条目对比,输出**缺失清单**
4. 提供 \[一键创建缺失条目\] 按钮(默认文本 = SO 中的备用文本)
### 翻译导入 / 导出工具
`Tools > Zeling > Localization > Export CSV`:将所有 StringTable 导出为 CSV,每行格式:
```
Key,zh-CN,en,ja
ui.menu.start,开始游戏,Start Game,スタート
dialogue.town_elderbug_01.0,又来了年轻的旅者。,Back again...,また来たな…
```
`Tools > Zeling > Localization > Import CSV`:导入翻译后的 CSV,自动更新对应 StringTable。
### 语言预览模式
Inspector 顶部下拉菜单切换预览语言(不修改 PlayerPrefs),仅在 Editor Play Mode 下生效,用于对话策划的快速校验。