移动/游戏
Unity 项目需要多语言本地化(含 CJK…
在 Unity 项目中添加多种语言、翻译 UI 文本、用 TMP 支持中日韩字体,以及处理 Addressables 资源表时使用。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
Unity 项目需要多语言本地化(含 C特定
解决思路
- 先验证 Localization 包是否安装;不要只问 Package Manager,检查 `Packages/packages-lock.json` 中是否有 `com.unity.localization`(lock 文件记录实际解析结果,是纯 JSON,无需 Editor 和异步调用;`Packages/manifest.json` 只记录请求)。
- 若缺失,调用 `UnityEditor.PackageManager.Client.Add('com.unity.localization')` 安装。
- `Client.Add` 与 `Client.List` 是异步的;调用返回时请求仍是 `InProgress`,不要在同一条语句读取结果,也不要忙等 `IsCompleted` 阻塞主线程;发射安装请求后先返回,之后再轮询 `packages-lock.json` 直到 id 出现。安装会触发 domain reload,前几次轮询可能失败;新安装通常几秒完成。
- 使用类型前确认程序集已加载:`System.Type.GetType('UnityEngine.Localization.Settings.LocalizationSettings, Unity.Localization')`,返回非 null 才继续(可返回 ready)。
- 若 `LocalizationEditorSettings.ActiveLocalizationSettings` 为 null,先查找或创建。
- 查找:`AssetDatabase.FindAssets('t:LocalizationSettings', new[] { 'Assets' })`;找到后加载第一个并赋给 `LocalizationEditorSettings.ActiveLocalizationSettings`。
- 所有 `FindAssets` 都要传搜索目录;不限定范围会搜索整个项目包括只读包,可能返回不可编辑的包内资源。
- 创建:用 `ScriptableObject.CreateInstance<LocalizationSettings>()` 创建,再用 `AssetDatabase.CreateAsset()` 保存到 `Assets/Localization/LocalizationSettings.asset`。
- 激活:设置 `LocalizationEditorSettings.ActiveLocalizationSettings = settings`。
- 确保 locale(en、fr、de 等)存在;缺失则创建,并用 `LocalizationEditorSettings.AddLocale(locale)` 加入 settings。
- 对 String/Asset 表做程序化修改后必须通知 Editor。
- 除非项目已有,否则总是创建所需的 asset tables。
- 安全填充模式:按 `Locale.Identifier.Code` 显式匹配;`GetLocales()` 顺序不保证与输入数据数组一致,假定一致会导致静默数据错配且难调试。
- Asset Tables 用资源 GUID:`table.GetEntry(sharedId) ?? table.AddEntry(sharedId, guid)`。
- 刷新与通知:任何修改(添加 key、更新值)后通知 Editor,否则 Editor 显示陈旧数据到下个 reimport。
- 对每个被修改的 `Table` 调用 `EditorUtility.SetDirty(collection)`、`EditorUtility.SetDirty(collection.SharedData)`。
- Unity 6+ 通知:`LocalizationEditorSettings.EditorEvents.RaiseCollectionModified(sender, collection)`。
- 最后总是调用 `AssetDatabase.SaveAssets()`。
- UI 本地化与布局:始终限定命名空间,如 `UnityEngine.UI.Image`、`UnityEngine.UI.VerticalLayoutGroup`、`UnityEngine.UI.ScrollRect`、`UnityEngine.UI.Mask`、`UnityEngine.UI.CanvasScaler`、`UnityEngine.UI.GraphicRaycaster`、`UnityEngine.UI.ContentSizeFitter`、`UnityEngine.UI.LayoutRebuilder` 等。
- `UnityEngine.UI` 既是命名空间又是类容器,未限定名称会产生 `CS0118`(把命名空间当类型用);全限定可避免。
- 单实例:创建新 Canvas 前总是 `GameObject.Find('YourCanvasName')` 并销毁旧对象。
- 切换 locale 要用包,并区分预览与运行时。
- 编辑时预览 locale:用 `Window > Asset Management > Localization Scene Controls`;这是 Editor-only,不是运行时功能。
- 运行时切换 locale:赋值 `LocalizationSettings.SelectedLocale`;这是受支持入口,所有通过 `LocalizeStringEvent` 绑定的内容会随之更新。
- 固定游戏启动 locale:在 Localization Settings 资源上配置 startup locale selector;`SpecificLocaleSelector` 强制指定 locale,默认链会读取系统语言。
- 绝不手写 locale 状态;真实游戏内语言菜单可以,但必须设置 `SelectedLocale` 并让包传播变化。禁止调试下拉或菜单自己维护 current language 变量、自己替换字符串或绕过包,因为项目其他部分不会跟随。
- Localized String Events 稳健绑定:先判断目标是 `TextMeshPro` 还是旧版 `UnityEngine.UI.Text`。
- 正确绑定:添加公开的 `UnityEngine.Localization.Components.LocalizeStringEvent` 组件并自行接线;设置 `StringReference` 到表条目,再添加 `OnUpdateString` 监听器,把值赋给文本组件(TextMeshPro 用 `TMP_Text.text`,旧版 Text 用 `UnityEngine.UI.Text.text`)。
- 不要反射 `UnityEditor.Localization.Plugins.TMPro.LocalizeComponent_TMPro` 或其 UGUI 对应物;它们是 internal(Localization 1.5.12 实测),绕过访问控制且无稳定性承诺;用公开的 `LocalizeStringEvent`。
- 布局重建:设置本地化文本或填充列表后,调用 `UnityEngine.UI.LayoutRebuilder.ForceRebuildLayoutImmediate(parentTransform)` 确保尺寸更新。
- CJK 亚洲语言字体支持:避免为 CJK locale 使用 TMP Fallback Fonts;改为每个 locale 用 Asset Table Font Swapping,fallback 不可靠且缺字形时难调试。
- 前置条件:必须导入 TMP Essential Resources;在触碰任何 TMP API 前检查。
- 从未导入过的项目中 `TMP_Settings.instance` 为 null,TMP 调用会抛出无用的裸 `NullReferenceException`;`TMP_FontAsset.CreateFontAsset` 就是其中之一,字体会在第一行失败且看起来像代码 bug。
- 检查:`var ready = TMPro.TMP_Settings.instance != null;`。
- 若未就绪,非交互式导入 TMP Essential Resources。
- 不要用 `EditorApplication.ExecuteMenuItem('Window/TextMeshPro/Import TMP Essential Resources')`;它返回 true 后打开等待人工的对话框,资源不会导入且运行像挂起。
- 直接导入包:从 `Application.dataPath/../Library/PackageCache` 的目录中查找 `Package Resources/TMP Essential Resources.unitypackage`;Unity 6 中 TMP 随 `com.unity.ugui` 发布,目录名带版本 hash,所以搜索文件而非硬编码路径;调用 `UnityEditor.AssetDatabase.ImportPackage(package, false)`(false 为非交互)。
- 之后再在后续调用中轮询 `TMP_Settings.instance != null`,非 null 才继续;Unity 6000.5.8f1 实测非交互导入几秒完成,资源落在 `Assets/TextMesh Pro`。
- 使用 locale 专用字体:Arial/Liberation Sans 等西文字体不含 CJK 字形,会出现 tofu 方块;始终使用目标语言字体:zh-Hans 用 `msyh.ttc`(Microsoft YaHei)或等价字体;ja 用 `msgothic.ttc`(MS Gothic)或等价;ko 用 `malgun.ttf`(Malgun Gothic)或等价;若复制系统字体失败,停止并报告,不要用西文字体替代。
- 稳健创建字体:从导入字体创建动态 `TMP_FontAsset`。
- 多 Atlas 与动态模式:CJK 字符集太大,静态 atlas 会立刻耗尽空间;设置 `fontAsset.atlasPopulationMode = AtlasPopulationMode.Dynamic;` 和 `fontAsset.isMultiAtlasTexturesEnabled = true;`。
- 子资源:把 atlas 纹理和材质都加入子资源;只加纹理是常见错误,材质不会写入文件。Unity 6000.5.8f1 实测:不调用第二行保存的 font asset 磁盘上 `Material` 对象数为零;仅在内存中的材质不属于资源,重新加载该资源只会得到 TMP 重建的材质,配置会静默丢失。
- 子资源代码:`foreach (var atlas in fontAsset.atlasTextures) { UnityEditor.AssetDatabase.AddObjectToAsset(atlas, fontAsset); }`,再 `UnityEditor.AssetDatabase.AddObjectToAsset(fontAsset.material, fontAsset);`。
- 显式链接材质纹理:`fontAsset.material.mainTexture = fontAsset.atlasTexture;`,保存前把 font asset、其 material、其 textures 标脏;`atlasTexture` 是 `atlasTextures` 第一个条目,与上面添加所有纹理一致。
- 验证要针对文件而非手里的对象:`AssetDatabase.SaveAssets()` 后调用 `AssetDatabase.LoadAllAssetsAtPath(path)`,确认返回对象里有 `Material`;不要满足于 `fontAsset.material != null`,它无论材质是否保存都为 true,因为 TMP 会返回内存材质,无法区分已保存与未保存。
- Addressables:Asset Table 中引用的每个资源都必须标记为 Addressable。
- 不要在 Asset Table 中引用 `Resources/` 文件夹内的资源;会导致 `OperationException: Failed to load sub-asset`;若资源在 `Resources/`,先复制到 `Assets/Fonts/` 之类位置再设为 Addressable。
- 若 font asset 被删除并重建,新 GUID 必须手动更新到 Asset Table,并重新加入 Addressables。
- 专用类型:TextMesh Pro 字体切换优先用 `LocalizedTmpFont` 而非 `LocalizedAsset<TMP_FontAsset>`,避免隐式转换错误。
- 构建要求:更新 Asset Tables 或 Addressable groups 后触发 `AddressableAssetSettings.BuildPlayerContent();`。
- CJK 验证步骤:Tofu Check:把 Editor locale 切到 `zh-Hans`、`ja`、`ko`,检查 UI;任何字符显示为方块即字体设置失败。
- Asset Table Check:确认 CJK locale 的 `AssetTable` 指向正确的 CJK `TMP_FontAsset`,而不是默认西文字体。
- Multi-Atlas Check:确认 CJK font assets 的 `isMultiAtlasTexturesEnabled` 为 `true`。
- 自动布局(UGUI):父物体用 `VerticalLayoutGroup`,`Child Control Height: True`,`Child Force Expand Height: False`。
- 每个标签必须有 `ContentSizeFitter`,`Vertical Fit: Preferred Size`。
- TMP:设置 `Enable Word Wrapping: True` 和 `Overflow: Overflow`。
- 翻译已有项目注意事项:最小代码改动,绝不修改与本地化无关的代码;用静态 helper 类(如 `L10n`)包装 `LocalizationSettings.StringDatabase.GetLocalizedString`,方便注入现有脚本。
- 稳健映射策略:把 key 按字符串长度降序排序,最长字符串优先匹配,防止短字符串(如 `NO`)匹配到长句的一部分;适当使用不区分大小写匹配。
- 组件事件监听器:用 `UnityEventTools.AddPersistentListener` 接线 `LocalizeStringEvent.OnUpdateString`,传入基于文本组件公开 `text` setter 构建的委托;setter 没有 C# method-group 名称,所以按名称构建委托:`(UnityAction<string>)Delegate.CreateDelegate(typeof(UnityAction<string>), text, "set_text")`;这是对 public 成员的反射,没问题;工作版本见 `resources/L10nBatchProcessor.cs`,其中会先清除已有 persistent listeners,避免重复运行堆叠重复项。
- 不要通过 `SerializedObject` 直接写 persistent-call 字段(`m_MethodName`、`m_Mode`、`m_PersistentCalls`);这些是私有序列化名,无兼容性保证,而且没必要:`AddPersistentListener` 加上述委托会产生相同序列化调用(target=文本组件,method=`set_text`,mode=`EventDefined`)。
- 必须设置 call state,否则标签在 Editor 中不更新;`AddPersistentListener` 会把调用留在 `RuntimeOnly`,绑定正确但在 Play 模式外休眠;用公开的 `UnityEventBase.SetPersistentListenerState` 修复:`UnityEventTools.AddPersistentListener(lse.OnUpdateString, setText); var index = lse.OnUpdateString.GetPersistentEventCount() - 1; lse.OnUpdateString.SetPersistentListenerState(index, UnityEngine.Events.UnityEventCallState.EditorAndRuntime);`;Unity 6000.5.8f1 实测:没有第二步时,即使 prefab 保存并重载,listener 在 Edit mode 也不触发;有则 call state 变为 `EditorAndRuntime`,文本立即更新。
- Persistent listeners 必须指向 `UnityEngine.Object` 上的方法;lambda 会失败。
- 然后确认绑定是活的,不要假设;Inspector 看起来正确但不工作是此步骤的典型失败。所有需要的读回都是事件上的公开 API:`GetPersistentEventCount() > 0` 表示有接线;`GetPersistentTarget(i)` 与 `GetPersistentMethodName(i)` 应为组件和 `set_text`;`GetPersistentListenerState(i)` 应为 `EditorAndRuntime`;`lEvent.RefreshString()` 后文本值应变化。四项都要做;count 大于零只证明接了线,`RuntimeOnly` 对构建正确但过不了最后检查;读 state 才能区分休眠绑定和坏绑定。添加并配置但从不触发的组件比未本地化标签更糟,因为看起来已完成。
- 初始化与刷新:`LocalizationEditorSettings.CreateStringTableCollection` 需要目录路径(如 `Assets/Localization`),不是完整 asset 路径。
- 程序化赋值 `LocalizedString` 引用后,总是调用 `lEvent.RefreshString()` 立即更新 UI。
- Key 必须在 collection 的每张表(en、de、ja 等)中存在且值为非空;存在但空值是最常见缺口,且不是静默的:包会把 `No translation found for …` 文本打印到游戏 UI,导致发布画面显示开发者信息;不要靠肉眼检查,运行完整性检查。
- 命名空间与 Linq:搜索集合时始终 `using System.Linq;`;处理 locale 或 tables 时 `using UnityEngine.Localization;`。
- 验证:修改 tables 或 addressables 后运行 `AddressableAssetSettings.BuildPlayerContent()`,切换 Editor locale 验证变化,并在激活后检查 `LocalizationSettings.Instance` 状态。
- 表完整性检查(宣布完成前运行):用代码枚举所有 `LocalizationEditorSettings.GetStringTableCollections()`,对每个 collection 的 `SharedData.Entries` 和每张 `StringTables`,用 `table.GetEntry(key.Id)` 检查;entry 为 null 或 `string.IsNullOrWhiteSpace(entry.Value)` 都算缺口,加入 gaps 列表;统计 checkedCount。
- 完整性检查输出:若 `checkedCount == 0` 返回 `INCONCLUSIVE: no table entries found...`,不要当成功;若有缺口返回 `GAPS (n of checkedCount)` 并列出 `collection / locale / key`;若完整返回 `COMPLETE: checkedCount entries checked, no gaps`。Unity 6000.5.8f1 实测一个故意清空 `ja` 值的表会报告 `GAPS 1 of 4` 并精确命名该条目,填好后 `COMPLETE (4 checked)`,无表项目返回 `INCONCLUSIVE`。
- 报告缺口列表而不是静默修复;有些缺口是决策而非错误(未要求翻译的 locale、有意跨语言相同的 key);用英文填充会隐藏决策;列出并让用户说明哪些是有意的。
- Smart Strings:按需设置 smart strings;结合整个 UI 及其影响它的脚本检查每个字符串上下文;在 string table 上设置 context,确保翻译合理。
- 推荐翻译策略工作流:提取与组件设置。
- 查找所有出现位置有两个隐藏处,只扫一个会漏另一个。
- Author 文本位于 scenes 和 prefabs 的组件上:旧版 `UnityEngine.UI.Text` 与 TextMeshPro(`TMP_Text`,是 `TextMeshProUGUI` 和 `TextMeshPro` 的基类);两族都要遍历。真实项目实测:`FindObjectsByType<Text>` 找到 1 个组件,而 `FindObjectsByType<TMP_Text>` 找到 13 个,只扫旧版会报告成功但几乎没做事。
- 代码中拼的文本在编辑时从不出现在组件上,场景遍历看不到;例如 `scoreLabel.text = $"EXP {value}"` 对任何基于组件的扫描不可见,是“完成”本地化后仍会残留的字符串;在 C# 中查找:`grep -rnE '\.text\s*(=|\+=)\s*\$?"|SetText\(\s*\$?"' --include='*.cs' Assets/`;该模式捕获纯文本、插值、拼接、`+=` 和 `SetText`,并有意不匹配 `label.text = someVariable` 或 `label.text = Localize('KEY')`(前者无可提取,后者已路由);盲区是字面量存在别处变量或 const;若项目计数偏低,再 grep 该文件的字符串字面量。
- 报告未转换的内容;拼合字符串通常需要 smart string 或 format 参数,是判断问题,有些确实不值得本地化;无论选择什么,列出扫描到的每个位置以及是否转换、未转换原因;报告“已本地化 24 条”而有 9 个发现位置未处理,正是此清单要防止的失败模式:工作看起来完成,缺口只会在其他 locale 截图里暴露。
- 共享表:创建中心 String Table(如 `UIStrings`),包含基础语言和每 key 的 Context 列,指导译者。
- 附加组件:对每个找到的 UI 元素附加 `LocalizeStringEvent`(文本)和 `LocalizedFont` helper(字体切换)。
- 验证:确保这些组件配置 persistent listeners(`EditorAndRuntime`),使 locale 变化时在 Editor 立即更新。
- 上下文感知翻译:表填充后,为每个 locale 提供翻译。
- 上下文为王:始终参考 Context 列或检查 UI 布局,确保翻译符合意图和空间。
- 语法与语气:确保语气匹配游戏风格;例如按钮用祈使动词(德语用 `Lauf!` 而不是 `Laufen`),标签用正确复数(用 `Punkte` 而不是 `Punkt`)。
- QA:Scene Controls 用 `Window > Asset Management > Localization Scene Controls`,或脚本 `LocalizationSettings.SelectedLocale = LocalizationSettings.AvailableLocales.GetLocale('de');`。
- 视觉检查:系统检查基础语言和所有目标语言下的每个 prefab 和 scene。
- 布局适配:检查文本溢出或 tofu(缺字形);字符串太长时调整字号或用 `ContentSizeFitter`。
- API Reference:详细 API 用法、常见命名空间冲突、Addressables 模式和字体修复步骤见 `references/api-notes.md`。
- 加速本地化工作流:用批处理脚本一次处理所有 scenes,高效本地化整个项目。
- 行动前询问:运行任何批量操作前向用户确认:`This will open every scene in the project, attach LocalizeStringEvent components, and save all modified scenes. This cannot be undone automatically. Shall I proceed?`;用户确认后才继续;批处理器模板在 `resources/L10nBatchProcessor.cs`。
- 批处理器遍历 `Text` 和 `TMP_Text` 两者,`LocalizeAll` 返回无法匹配的标签(格式 `scene :: object :: "text"`);打印该列表,这是返回值的全部意义:一次接线 20 个标签却静默留下 9 个,看起来和完成没区别;该列表只覆盖 authored text,所以要和 Section 6 的代码扫描及 Section 5 的表完整性检查配合。
- 速度技术提示:Table References 用 `TableReference` 名称(字符串)而不是 GUID,便于阅读维护。
- 批量刷新:修改后用 `LocalizationSettings.Instance.ForceRefresh()` 强制 Editor UI 更新。
- 字体切换自动化:创建一次 `GameAssets` 表,用脚本一趟把 `LocalizeFontEvent` 重新赋给所有标签。
- `LocalizedFontAsset` 组件模板在 `resources/LocalizedFontAsset.cs`。
适用边界 · 注意事项
- 非 Unity 项目,或未安装/无法安装 `com.unity.localization` 包时。
- 无法导入 TMP Essential Resources、无法使用 Addressables,或项目禁止修改场景/资源时。
- 未获得用户确认就执行批量打开并保存所有场景的本地化操作时。