关于 localization
这是一份 Unity Localization 工作流指南,涵盖语言环境、表格、UI 绑定和 CJK 字体资源配置,面向自动化处理 Unity 项目本地化设置与更新的开发者。
主要功能
- 01检查并安装 Localization 包
- 02配置设置与语言环境
- 03创建和更新字符串及资源表
- 04将本地化字符串绑定到 UI 组件
- 05设置特定语言的 CJK 字体
- 06刷新资源并构建 Addressables
适用场景
- 01在 Unity 项目中设置本地化
- 02根据本地化数据填充表格
- 03在预览或运行时切换语言环境
- 04为本地化内容添加 CJK 字体资源
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ unity-technologies/ skills --skill localization安装后在 Claude Code 中输入/localization确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/api-notes.mdresources/L10nBatchProcessor.csLocalizedFontAsset.cs详细介绍
本指南介绍如何设置和配置 Unity Localization,包括地区设置、字符串表和资产表、Addressables 集成,以及通过资产表支持 CJK 字体。
0. 检查软件包安装情况
开始之前,请先确认已安装 Localization 软件包。如果未安装,本文中的许多 API 会静默失败或抛出令人困惑的错误。
- 通过读取项目检查,而不是询问 Package Manager。 在
Packages/packages-lock.json中查找com.unity.localization。该文件记录了 Unity 实际解析的内容,是纯 JSON,无需 Editor,也无需异步调用即可读取。 (Packages/manifest.json只记录了 请求安装 的内容,因此请检查 lock 文件。) - 如果缺少,则安装:
UnityEditor.PackageManager.Client.Add("com.unity.localization")。 - 正确等待。
Client.Add和Client.List是异步的:调用时会返回一个仍处于InProgress状态的请求,因此在同一语句中读取结果毫无意义。也不要对IsCompleted进行忙等待,因为这会阻塞当前运行的主线程。应在触发安装后返回,然后在后续调用中轮询packages-lock.json,直到出现该 ID。安装也会触发域重载,因此前一两次轮询可能会失败;全新安装通常会在几秒内完成。 - 使用这些类型之前,先确认它们已实际加载,因为 lock 文件可能会在程序集就绪前写入:
只有在返回var t = System.Type.GetType( "UnityEngine.Localization.Settings.LocalizationSettings, Unity.Localization"); return t != null ? "ready" : "not loaded yet";ready后才能继续。
1. Localization 设置和地区
如果 LocalizationEditorSettings.ActiveLocalizationSettings 为 null,则必须找到或创建设置:
- 查找: 使用
AssetDatabase.FindAssets("t:LocalizationSettings", new[] { "Assets" })。如果找到,则加载第一个并将其赋值给LocalizationEditorSettings.ActiveLocalizationSettings。- 始终传入搜索文件夹。 不指定范围的
FindAssets会搜索整个项目,包括只读软件包,因此可能返回软件包中的资产,导致项目指向无法编辑的内容。本指南中的每次FindAssets调用都适用此规则。
- 始终传入搜索文件夹。 不指定范围的
- 创建: 如果未找到,则创建新实例并将其保存到
Assets/Localization/LocalizationSettings.asset。使用ScriptableObject.CreateInstance<LocalizationSettings>(),然后调用AssetDatabase.CreateAsset()。 - 启用: 设置
LocalizationEditorSettings.ActiveLocalizationSettings = settings。 - 地区: 确保所需地区(en、fr、de 等)均已存在。若缺少,则创建并使用
LocalizationEditorSettings.AddLocale(locale)将其添加到设置中。
2. 修改 Localization 表
以编程方式更改字符串表或资产表时,必须通知 Editor。 除非项目中已存在相应资产表,否则务必创建所需的资产表。
安全填充模式
使用数据集填充表时,请明确匹配 Locale.Identifier.Code。GetLocales() 的顺序不保证与输入数据数组一致,若作此假设,将导致难以排查的静默数据错配。
对于资产表,请使用资产的 GUID:table.GetEntry(sharedId) ?? table.AddEntry(sharedId, guid);。
刷新和通知
添加键或更新值等修改完成后,通知 Editor 以刷新其内部状态。若跳过此步骤,Editor 将继续显示过时数据,直到下次重新导入。
- 对每个已修改的
Table,调用EditorUtility.SetDirty(collection)和EditorUtility.SetDirty(collection.SharedData)。 - Unity 6+ 通知:
LocalizationEditorSettings.EditorEvents.RaiseCollectionModified(sender, collection); - 最后务必调用
AssetDatabase.SaveAssets()。
3. 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(将命名空间用作类型)。使用完整限定名即可彻底避免此问题。- **单实例:**创建新对象前,始终检查
GameObject.Find("YourCanvasName"),并销毁旧对象。 - **切换区域设置:使用软件包,并将预览与运行时分开。**这是两种不同的机制;混为一谈正是区域设置切换经常需要自行实现的原因。
- **创作时预览某个区域设置,**请使用 Localization Scene Controls 窗口(
Window > Asset Management > Localization Scene Controls)。此功能仅限 Editor 使用,不适用于运行时,因此无法满足游戏本身需要语言设置的场景。 - **在运行时切换区域设置,**请设置
LocalizationSettings.SelectedLocale。这是受支持的入口,通过LocalizeStringEvent绑定的所有内容都会据此更新。 - **指定游戏启动时使用的区域设置,**请在 Localization Settings 资源上配置启动区域设置选择器。
SpecificLocaleSelector用于强制选择指定区域设置;否则,默认链会采用系统语言。 - **绝不要自行实现区域设置状态。**真正的游戏内语言菜单完全可行且符合预期,只要它设置
SelectedLocale并让软件包传播更改即可。禁止的是使用调试下拉菜单或菜单跟踪自己的“当前语言”变量、自行替换字符串或绕过软件包,因为项目中的其他内容都不会随之更新。
- **创作时预览某个区域设置,**请使用 Localization Scene Controls 窗口(
本地化字符串事件(可靠绑定)
-
**检查组件类型:**确认目标是
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 中测得),访问它们就意味着绕过访问控制,使用 Unity 不承诺稳定性的 API——它可能在任何软件包版本中更改或消失。LocalizeStringEvent是公开的,可完成相同工作,并明确展示绑定方式。 -
**重建布局:**设置本地化文本或填充列表后,调用
UnityEngine.UI.LayoutRebuilder.ForceRebuildLayoutImmediate(parentTransform),确保尺寸得到更新。
4. 亚洲语言字体支持(CJK)
避免在 CJK 区域设置中使用 TMP 后备字体。应针对每个特定区域设置使用资源表字体替换;缺少字形时,后备字体不可靠且难以调试。
前提条件:必须导入 TMP Essential Resources
**使用任何 TMP API 前,请先检查这一点。**在从未导入这些资源的项目中,TMP_Settings.instance 为 null,TMP 调用会失败并抛出一个没有实用信息的裸 NullReferenceException。TMP_FontAsset.CreateFontAsset 也属于此类调用,因此字体创建会在第一行就失败,错误看起来像是代码本身有问题。
// The check.
var ready = TMPro.TMP_Settings.instance != null;
如果尚未就绪,请以非交互方式导入:
// Do NOT use EditorApplication.ExecuteMenuItem("Window/TextMeshPro/Import TMP Essential Resources").
// It returns true and then opens a dialog that waits for a human, so nothing gets imported and the
// run appears to hang. Import the package directly instead.
string package = null;
var cache = System.IO.Path.GetFullPath(System.IO.Path.Combine(
UnityEngine.Application.dataPath, "..", "Library", "PackageCache"));
foreach (var dir in System.IO.Directory.GetDirectories(cache))
{
// TMP ships inside com.unity.ugui in Unity 6, and the folder name carries a version hash,
// so search for the file rather than hardcoding a path.
var candidate = System.IO.Path.Combine(dir, "Package Resources", "TMP Essential Resources.unitypackage");
if (System.IO.File.Exists(candidate)) { package = candidate; break; }
}
UnityEditor.AssetDatabase.ImportPackage(package, false); // false = non-interactive
然后在后续调用中轮询 TMP_Settings.instance != null,方式与步骤 0 中的软件包检查相同;只有确认其不为 null 后才继续。已在 Unity 6000.5.8f1 上验证:非交互式导入会在几秒内完成,资源会放在 Assets/TextMesh Pro 中。
- 使用特定语言的字体: Arial 或 Liberation Sans 等西文字体不包含 CJK 字形,因此会显示“豆腐块”(方块)。务必使用为目标语言设计的字体:
- 对于简体中文(zh-Hans):使用
msyh.ttc(Microsoft YaHei)或同等字体。 - 对于日语(ja):使用
msgothic.ttc(MS Gothic)或同等字体。 - 对于韩语(ko):使用
malgun.ttf(Malgun Gothic)或同等字体。 - 如果复制系统字体失败,请停止并报告。不要用西文字体替代。
- 对于简体中文(zh-Hans):使用
- 可靠地创建字体: 从导入的字体动态创建
TMP_FontAsset。 - 多图集与动态模式: CJK 字符集太大,无法使用静态图集;单个图集很快就会耗尽空间。
fontAsset.atlasPopulationMode = AtlasPopulationMode.Dynamic;fontAsset.isMultiAtlasTexturesEnabled = true;
- 子资源: 添加图集纹理和材质。只添加纹理是常见错误,这样材质就永远不会写入文件:在 Unity 6000.5.8f1 上测得,保存时未调用第二次方法的字体资源在磁盘上包含零个
Material对象,而调用了该方法的则包含一个。仅存在于内存中的材质不属于该资源,因此任何重新加载该资源的对象都会获得 TMP 重建的材质,而不是你配置的材质,你对其应用的任何设置也会悄然丢失。// 每个图集纹理,而不只是第一个。步骤 3 启用了多图集,因此可能会有多个图集。 foreach (var atlas in fontAsset.atlasTextures) { UnityEditor.AssetDatabase.AddObjectToAsset(atlas, fontAsset); } // 还要添加材质。没有这一行,材质就不会写入资源。 UnityEditor.AssetDatabase.AddObjectToAsset(fontAsset.material, fontAsset);- 显式关联材质的纹理:
fontAsset.material.mainTexture = fontAsset.atlasTexture;并在保存前将字体资源、其材质和纹理标记为脏数据。(atlasTexture是atlasTextures中的第一个条目,也是主材质绘制所用的纹理,因此这与上面添加所有纹理的做法一致。) - 根据文件验证,而不是根据你手头的对象验证。 调用
AssetDatabase.SaveAssets()后,调用AssetDatabase.LoadAllAssetsAtPath(path),并确认返回的对象中包含一个Material。不要只满足于fontAsset.material != null:无论材质是否已保存,这个条件都会成立,因为 TMP 会返回一个内存中的材质,因此无法据此判断材质是否已保存。
- 显式关联材质的纹理:
- Addressables: Asset Table 中引用的每个资源都必须标记为 Addressable。
- 不要在 Asset Table 中引用
Resources/文件夹里的资源。这会导致OperationException: Failed to load sub-asset错误。如果资源位于Resources/中,请先将其复制到Assets/Fonts/或类似位置,再将其标记为 Addressable。 - 如果字体资源被删除并重新创建,必须手动更新 Asset Table 中的新 GUID,并将其重新添加到 Addressables。
- 不要在 Asset Table 中引用
- 专用类型: 对于 TextMesh Pro 字体切换,优先使用
LocalizedTmpFont,而不是LocalizedAsset<TMP_FontAsset>,以避免隐式转换错误。 - 构建要求: 更新 Asset Table 或 Addressable 组后,触发一次构建:
AddressableAssetSettings.BuildPlayerContent();。
验证步骤
在完成任何 CJK 本地化任务之前:
- 豆腐块检查: 将编辑器区域设置切换为
zh-Hans、ja和ko。检查 UI。如果有任何字符显示为方块(豆腐块),则字体配置失败。 - Asset Table 检查: 验证 CJK 区域设置对应的
AssetTable指向正确的 CJKTMP_FontAsset,而不是默认的西文字体。 - 多图集检查: 确认 CJK 字体资源上的
isMultiAtlasTexturesEnabled为true。
5. 自动布局(UGUI)
- 父对象:
VerticalLayoutGroup的Child Control Height: True、Child Force Expand Height: False。 - 标签: 每个标签都必须设置
ContentSizeFitter,并将Vertical Fit设为Preferred Size。 - TMP: 将
Enable Word Wrapping设为True,并将Overflow设为Overflow。
翻译现有项目时的注意事项
- 尽量减少代码改动:不要修改与本地化无关的代码。使用静态辅助类(例如
L10n)封装LocalizationSettings.StringDatabase.GetLocalizedString,以便轻松注入现有脚本。 - 稳健的映射策略:将现有 UI 文本映射到键时,按字符串长度降序排列键,并优先匹配最长的字符串。这样可以避免较短的字符串(如 "NO")匹配到较长句子的部分内容。适当时使用不区分大小写的匹配。
- 组件事件监听器:使用
UnityEventTools.AddPersistentListener为LocalizeStringEvent.OnUpdateString连接监听器,并传入基于文本组件公开textsetter 创建的委托。setter 没有 C# 方法组名称,因此要按名称创建委托:(UnityAction<string>)Delegate.CreateDelegate(typeof(UnityAction<string>), text, "set_text")。 这是对公开成员进行反射,没有问题。参见 resources/L10nBatchProcessor.cs,其中包含可正常工作的版本,并会先清除所有现有的持久监听器,以免重复运行时叠加重复项。-
不要通过
SerializedObject直接写入持久调用字段(m_MethodName、m_Mode、m_PersistentCalls)。这些是没有兼容性保证的私有序列化名称,而且没有必要这样做:使用上述委托调用AddPersistentListener会生成相同的序列化调用(目标 = 文本组件,方法 =set_text,模式 =EventDefined)。 -
接下来必须设置调用状态,否则标签不会在 Editor 中更新。
AddPersistentListener会将调用设为UnityEventCallState.RuntimeOnly,因此绑定虽然正确,但在 Play 模式之外处于休眠状态:在 Editor 中切换语言区域不会有任何变化,保存并重新加载后也仍然如此。使用公开 APIUnityEventBase.SetPersistentListenerState修复:UnityEventTools.AddPersistentListener(lse.OnUpdateString, setText); var index = lse.OnUpdateString.GetPersistentEventCount() - 1; lse.OnUpdateString.SetPersistentListenerState( index, UnityEngine.Events.UnityEventCallState.EditorAndRuntime);已在 Unity 6000.5.8f1 上验证:不进行第二次调用时,即使预制件保存并重新加载,监听器也不会在 Edit 模式下触发;进行第二次调用后,调用状态会变为
EditorAndRuntime,文本会立即更新。 -
持久监听器必须指向
UnityEngine.Object上的方法;lambda 会失败。 -
然后确认绑定确实生效,不要想当然。 在 Inspector 中看似正确的连接却不起作用,是此步骤的典型故障。所需的所有回读操作都是事件的公开 API,因此无需触碰任何序列化字段:
检查项 调用 预期结果 已连接监听器 GetPersistentEventCount()> 0目标是文本组件 GetPersistentTarget(i)、GetPersistentMethodName(i)组件、 set_text编辑时会触发 GetPersistentListenerState(i)EditorAndRuntime确实更新标签 lEvent.RefreshString()文本值发生变化 全部执行这四项。计数大于零只能证明某些内容已接线,而 RuntimeOnly调用会导致最后一项检查失败,尽管它对构建来说完全正确,因此读取状态才能区分休眠绑定和损坏的绑定。已添加并配置却从未触发的组件比未本地化的标签更糟,因为它看起来像是已经完成。
-
- 初始化与刷新:
LocalizationEditorSettings.CreateStringTableCollection需要的是目录路径(例如Assets/Localization),而不是完整的资源路径。- 以编程方式分配
LocalizedString引用后,务必调用lEvent.RefreshString(),以立即更新 UI。 - 每个集合中的所有表(en、de、ja、……)都必须存在对应键,且值不能为空。键存在但值为空是最常见的遗漏,而且这不会悄无声息:程序包会将自己的“找不到……的翻译”文本显示在游戏 UI 中,导致发布的界面显示开发者消息。不要凭目测判断。运行下面的完整性检查。
- 命名空间与 Linq:搜索集合时,务必包含
using System.Linq;;处理区域设置或表时,务必包含using UnityEngine.Localization;。 - 验证:修改表或 Addressables 后,运行
AddressableAssetSettings.BuildPlayerContent()并切换 Editor 区域设置以验证更改。激活后检查LocalizationSettings.Instance的状态。
表完整性检查(宣布工作完成前运行此检查)
枚举表可以机械地回答“每个区域设置中的每个键是否都有值”,从而在有人开始玩游戏前发现缺失项。运行检查并报告输出。
var gaps = new System.Collections.Generic.List<string>();
var checkedCount = 0;
foreach (var col in UnityEditor.Localization.LocalizationEditorSettings.GetStringTableCollections())
{
foreach (var key in col.SharedData.Entries)
{
foreach (var table in col.StringTables)
{
checkedCount++;
var entry = table.GetEntry(key.Id);
// A missing entry and a present-but-empty entry both show as untranslated in game.
if (entry == null || string.IsNullOrWhiteSpace(entry.Value))
{
gaps.Add($"{col.TableCollectionName} / {table.LocaleIdentifier.Code} / {key.Key}");
}
}
}
}
// Zero entries is not a pass. It means no collections or no locales were found, so the check
// examined nothing: report that distinctly instead of letting it read as success.
if (checkedCount == 0)
{
return "INCONCLUSIVE: no table entries found. Either no String Table Collection exists yet, "
+ "or the collection has no locale tables. Fix that before trusting this check.";
}
return gaps.Count == 0
? $"COMPLETE: {checkedCount} entries checked, no gaps"
: $"GAPS ({gaps.Count} of {checkedCount} checked):\n " + string.Join("\n ", gaps);
在 Unity 6000.5.8f1 上验证:对于一个故意将 ja 值清空的表,检查会报告 GAPS 1 of 4,并准确指出该条目;填入值后会报告 COMPLETE (4 checked);在没有表的项目中则会报告 INCONCLUSIVE。
请报告缺失项列表,不要悄悄解决。 有些缺失项是待定决策,而非错误:例如,用户未要求翻译的区域设置,或有意在不同语言中保持一致的键。用英文文本填补这些缺失项会掩盖相关决策。请列出这些项,让用户指出哪些是有意为之。
- 智能字符串:在需要时设置智能字符串。检查每个字符串的上下文时,应考虑其所在的整个 UI,以及所有会影响它的脚本。在字符串表中设置上下文,确保翻译符合语境。
6. 推荐的翻译策略
要高效地翻译现有项目,请遵循以下多步工作流:
-
提取与组件设置:
- 查找所有出现的位置。有两个独立的隐藏位置,只扫描其中一个会漏掉另一个。
- 编写的文本位于场景和预制件中的组件上:旧版
UnityEngine.UI.Text和 TextMeshPro(TMP_Text,是TextMeshProUGUI和TextMeshPro的基类)。请遍历这两个系列。在实际项目中测得:FindObjectsByType<Text>找到 1 个组件,而FindObjectsByType<TMP_Text>找到 13 个,因此只扫描旧版组件会在几乎什么都没做的情况下报告成功。 - 在代码中拼接的文本在编辑时不会出现在组件上,因此遍历场景无法发现它。
scoreLabel.text = $"EXP {value}"对所有基于组件的扫描都不可见,而这个字符串会在“已完成”的本地化流程结束后遗留下来。请直接在 C# 中查找:
该模式能捕获普通、插值、拼接和# 携带字符串字面量的赋值和 SetText 调用。 grep -rnE '\.text\s*(=|\+=)\s*\$?"|SetText\(\s*\$?"' --include='*.cs' Assets/+=形式,以及SetText,并有意不匹配label.text = someVariable或label.text = Localize("KEY")(前者没有可提取的内容,后者已完成路由)。它无法发现存放在其他位置声明的变量或常量中的字面量;如果项目的匹配数量看起来偏少,也请搜索该文件中的字符串字面量。
- 编写的文本位于场景和预制件中的组件上:旧版
- 报告未转换的内容。 拼接字符串通常需要智能字符串或格式参数,这需要进行判断;而有些字符串确实不值得本地化。无论选择哪种做法,都要列出扫描发现的每个位置,并说明是否已转换;如果没有转换,也要说明原因。如果扫描发现 9 处未处理,却只报告“已本地化 24 个字符串”,这正是此清单要避免的问题:工作看起来已经完成,漏洞却只会在其他语言环境的截图中暴露出来。
- 共享表格: 创建一个集中的 String Table(例如
UIStrings),其中包含基础语言,并为每个键添加“Context”列,为译者提供参考。 - 附加组件: 为找到的每个 UI 元素附加一个
LocalizeStringEvent(用于文本)和一个LocalizedFont辅助组件(用于替换字体)。 - 验证: 确保这些组件已设置持久化侦听器(
EditorAndRuntime),以便在区域设置变化时立即在编辑器中更新。
- 查找所有出现的位置。有两个独立的隐藏位置,只扫描其中一个会漏掉另一个。
-
结合上下文进行翻译:
- 翻译: 填充表格后,为每种语言提供译文。
- 上下文至关重要: 始终参考“Context”列或检查 UI 布局,确保译文符合预期含义并适合显示空间。
- 语法与语气: 确保语气符合游戏风格。例如,按钮应使用祈使动词(例如,德语使用“Lauf!”而不是“Laufen”),标签应使用正确的复数形式(例如,使用“Punkte”而不是“Punkt”)。
-
质量保证(QA):
- 场景控件: 使用
Window > Asset Management > Localization Scene Controls,或运行脚本:LocalizationSettings.SelectedLocale = LocalizationSettings.AvailableLocales.GetLocale("de");。 - 目视检查: 系统地检查基础语言和所有目标语言中的每个预制件和场景。
- 布局适配: 检查文本是否溢出,或是否出现“豆腐块”(缺失字形)。如果字符串太长,可调整字号或使用
ContentSizeFitter。
- 场景控件: 使用
API 参考
有关详细的 API 用法、常见命名空间冲突、Addressables 模式和字体修复步骤,请参阅 references/api-notes.md。
7. 加速本地化工作流
要高效地本地化整个项目,请使用批处理脚本一次性处理所有场景。
操作前先询问: 运行任何批处理操作前,请先向用户确认:
“这会打开项目中的每个场景,附加
LocalizeStringEvent组件,并保存所有已修改的场景。此操作无法自动撤销。要继续吗?”
只有在用户确认后才能继续。批处理器模板位于 resources/L10nBatchProcessor.cs。
LocalizeAll 本身也会强制执行这些检查,因此调整模板时不要移除或绕过这些检查:它会拒绝在批处理模式下运行,提示用户保存或放弃未保存的场景更改,并显示一个阻塞式 Editor 对话框,列出即将重写的场景。告知用户留意 Editor 并点击确认按钮;如果用户取消,它会抛出 OperationCanceledException,且不会写入任何内容。请将此情况报告为取消,而不是需要绕过的错误。
它会遍历 Text 和 TMP_Text,而且 LocalizeAll 会返回无法匹配的标签
(格式为 scene :: object :: "text")。请打印此列表。返回此列表正是其目的:一次运行连接了 20 个标签,却悄悄漏掉另外 9 个,除此之外看起来会和全部处理完成毫无区别。该列表仅涵盖编写在场景中的文本,因此要结合第 6 节的代码扫描和第 5 节的表格完整性检查。
提速技术建议
- 表格引用: 使用
TableReference名称(字符串),而不是 GUID;这样更易于阅读和维护。 - 批量刷新: 修改后使用
LocalizationSettings.Instance.ForceRefresh(),强制 Editor 中的 UI 更新。 - 字体替换自动化: 创建一次
GameAssets表,然后使用脚本一次性为所有标签重新分配LocalizeFontEvent。 - LocalizedFontAsset 组件: 模板位于 resources/LocalizedFontAsset.cs。