chore: initial commit
This commit is contained in:
441
Docs/Design/22_LocalizationSystem.md
Normal file
441
Docs/Design/22_LocalizationSystem.md
Normal file
@@ -0,0 +1,441 @@
|
||||
# 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";
|
||||
|
||||
// 格式化辅助
|
||||
/// <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 下生效,用于对话策划的快速校验。
|
||||
Reference in New Issue
Block a user