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

14 KiB
Raw Permalink Blame History

22 · 本地化系统Localization System

命名空间 BaseGames.Localization
所属文档集 ← 返回索引 · 总览
Package 依赖 com.unity.localization (≥ 1.4)
依赖系统 BaseGames.UI · BaseGames.Dialogue


目录

  1. 系统总览
  2. 语言包配置
  3. StringTable 结构
  4. LanguageManager — 语言切换管理器
  5. UI 组件本地化
  6. 对话系统本地化桥接
  7. 字体与排版规范
  8. 数字 / 货币 / 时间格式
  9. SaveData 扩展
  10. 编辑器工作流

1. 系统总览

本地化系统基于 Unity Localization 包构建,提供运行时语言切换零硬编码字符串的文本管理方案。

本地化系统架构:
  ├─ LocalizationSettingsUnity 包)
  │   ├─ StringTableCollection    → 按功能域划分的字符串表集合
  │   ├─ AssetTableCollection     → 本地化资产(字体、图片)
  │   └─ Locale 列表             → zh-CN / en / ja初始三语言
  │
  ├─ LanguageManagerSO            → SO 单例,封装语言切换、持久化
  ├─ LocalizedStringComponent     → TextMeshPro 文本组件的本地化桥接
  ├─ DialogueLocalizationBridge   → DialogueSequenceSO → 本地化键 映射
  └─ LocalizationKeys             → 所有键名静态常量(编译期检查)

设计原则

  • UI 文本 → LocalizedStringReferenceUnity 内置组件,在 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 创建规范

// 编辑器脚本:自动创建 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 常量类(编译期保障)

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 操作语言切换:

[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 上添加 LocalizeStringEventUnity Localization 包内置组件):

Inspector:
  LocalizeStringEvent
  ├─ String Reference: UI_Table / "ui.menu.start"
  └─ Update String: TextMeshProUGUI.SetText自动连接

语言切换时 Unity 自动更新所有绑定了 LocalizeStringEvent 的 TMP 组件。

5.2 运行时动态文本

对于需要在代码中动态设置的文本:

// 物品获取提示(动态拼接物品名)
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. 对话系统本地化桥接

DialogueLocalizationBridgeDialogueSequenceSO 加载时,将文本内容替换为本地化版本:

[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 字体资产:

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.lineSpacingCJK 字体建议 +5%(行高较高)
  • 中文不允许行首标点(句号/逗号/右括号)→ TMP 开启 Line Breaking RuleChinese Simplified
  • 日文全角字符宽度 = 中文字符宽度,可复用布局
  • 对话框固定宽度,文本内容通过 Unity Localization 中的 Smart String 处理变量替换

8. 数字 / 货币 / 时间格式

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 扩展

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