# 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 下生效,用于对话策划的快速校验。