Files
zeling_v2/Docs/Design/22_LocalizationSystem.md
2026-05-08 11:04:00 +08:00

442 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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** 包构建,提供**运行时语言切换**和**零硬编码字符串**的文本管理方案。
```
本地化系统架构:
├─ LocalizationSettingsUnity 包)
│ ├─ 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";
// 格式化辅助
/// <summary> 返回对话行键名 "dialogue.{id}.{idx}" </summary>
public static string DialogueLine(string sequenceId, int lineIndex)
=> $"dialogue.{sequenceId}.{lineIndex}";
/// <summary> 返回物品名称键名 </summary>
public static string ItemName(string itemId) => $"item.{itemId}.name";
/// <summary> 返回物品描述键名 </summary>
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;
/// <summary> 当前语言代码(如 "zh-CN" </summary>
public string CurrentLocale
=> LocalizationSettings.SelectedLocale?.Identifier.Code ?? defaultLocale;
/// <summary> 设置语言并持久化 </summary>
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);
}
/// <summary> 游戏启动时恢复上次语言 </summary>
public void RestoreSavedLanguage()
{
string saved = PlayerPrefs.GetString("SelectedLocale", defaultLocale);
SetLanguage(saved);
}
/// <summary> 同步获取本地化字符串(非异步,需提前加载表) </summary>
public string Get(string key, string tableCollection = "UI_Table")
{
var result = LocalizationSettings.StringDatabase
.GetLocalizedString(tableCollection, key);
return string.IsNullOrEmpty(result) ? $"[{key}]" : result;
}
/// <summary> 带参数格式化 </summary>
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<TMP_FontAsset>("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
{
/// <summary> 伤害数字显示(统一不使用千位分隔符,保持像素风格简洁) </summary>
public static string Damage(int value) => value.ToString();
/// <summary> Geo 货币显示 </summary>
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",
};
}
/// <summary> 游戏内时间(存档时长) </summary>
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 下生效,用于对话策划的快速校验。