☰
ET框架多语言方案:从配置表到运行时完整实现
2026/10/20 8:59:50 网站建设 项目流程

1. 为什么游戏项目到了中期才想起做多语言,往往已经晚了

做Unity项目的人大多有过这种经历:游戏在国内跑通了,发行方突然说想上几个海外地区,于是团队火急火燎地开始做多语言。结果一打开工程,发现UI预制体里全是硬编码的中文字符串,策划配表里文本和逻辑混在一起,连个统一的文本入口都没有。这时候再回头做国际化,工作量几乎等于把整个项目重写一遍。

我自己参与过一个中型项目,早期没人考虑多语言,等到要出海的时候,光是梳理散落在各个Prefab、ScriptableObject和代码里的文本就花了两周。更麻烦的是,有些文本是运行时拼接出来的,比如"获得{0}个{1}"这种,翻译的时候语序完全对不上。所以多语言这件事,越早规划成本越低,而Unity生态里能把这套东西做扎实的框架并不多,ET框架算是其中一个比较有代表性的选择。

这篇内容主要聊的是在ET框架下怎么把多语言这套体系搭起来。ET本身是一套基于C#的双端框架,客户端和服务端共用一套逻辑代码,它的资源管理和配置体系有自己的设计哲学。多语言在ET里不是一个孤立的模块,它跟配置表、资源加载、UI组件都有耦合关系。我会从文本的存储结构、运行时切换机制、配置表集成、资源本地化这几个维度展开,把踩过的坑和验证过的方案都摊开讲。不管你是刚接触ET,还是已经在用ET做项目但还没碰多语言,这篇内容应该都能给你一些可以直接抄的参考。

2. 多语言在ET框架里的落点:文本该存在哪,谁来管

2.1 硬编码文本的三种常见藏身之处

在动手做多语言之前,得先搞清楚项目里的文本到底藏在哪些地方。我梳理过自己经手的几个项目,文本来源基本逃不出这三类:

第一类是UI预制体上的静态文本。Unity的Text或TMP_Text组件上直接填的中文,这类文本最容易被发现,但也最容易被遗漏,因为预制体多了之后没人记得哪个改过哪个没改。

第二类是配置表里的文本字段。ET框架有一套自己的配置表体系,通常用Excel或类似工具导出成二进制或JSON,策划在里面填的各种名称、描述、对话内容都属于这一类。这类文本量最大,也最需要统一管理。

第三类是代码里动态拼接的文本。比如提示信息、格式化字符串、错误码对应的描述等。这类文本最隐蔽,因为它们在代码里以字符串字面量的形式存在,不做全局搜索根本找不到。

我的经验是,在项目初期就约定一条规则:任何面向玩家的文本,都不允许直接写在Prefab或代码里,必须走统一的文本获取接口。这条规则执行得越早,后期做多语言的成本就越低。

2.2 ET框架下文本管理的核心思路

ET框架本身并没有内置一套完整的多语言方案,但它提供了足够的基础设施让你去搭。核心思路可以概括为:文本与逻辑分离,语言与资源解耦。

具体来说,文本不应该以字面量的形式出现在任何逻辑代码或UI组件里,而是通过一个唯一的Key去获取。这个Key对应的文本内容,根据当前语言环境从不同的数据源加载。ET的配置表体系天然适合做这件事,你可以建一张多语言配置表,结构大概是这样的:

字段名类型说明
Idint文本唯一标识
Keystring文本键名,如"login_title"
Chinesestring中文文本
Englishstring英文文本
Japanesestring日文文本
...string其他语言

这张表在ET的配置体系里就是一个普通的配置类,导出后可以通过框架的配置管理模块加载。运行时根据当前语言设置,从对应的字段取值。

但这里有个问题:如果语言种类很多,这张表会变得非常宽,每加一种语言就要改表结构、重新导出。更灵活的做法是把语言维度拆出来,做成两张表:一张是文本Key表,一张是翻译内容表,通过Key和Language两个维度来索引。这样加语言只需要加数据,不需要改结构。

2.3 为什么我不建议用Unity自带的Localization包

Unity官方有一个Localization包,功能挺全的,支持文本、资源、字体等多维度的本地化。但在ET框架的项目里,我不太推荐直接用它,原因有几个:

一是双端一致性问题。ET是双端框架,服务端也需要处理多语言文本,比如给玩家发邮件、返回错误提示等。Unity的Localization包是客户端专属的,服务端用不了。如果客户端和服务端各用一套多语言方案,维护成本会翻倍。

二是配置体系冲突。ET有自己的配置表加载和管理机制,如果引入Unity的Localization,相当于在项目里跑了两套配置体系,资源加载路径、生命周期管理都会变得混乱。

三是热更新友好度。ET框架对热更新有比较好的支持,配置表可以通过热更包更新。而Unity的Localization包在热更场景下的表现需要额外处理,不如直接用ET的配置体系来得直接。

所以我的建议是,在ET项目里做多语言,优先考虑基于ET配置表自建一套轻量方案,而不是引入外部包。这套方案的核心组件其实不多:一个文本管理类、一张配置表、一个语言切换入口,再加上UI组件的自动绑定机制,基本就能覆盖大部分场景。

3. 从配置表到运行时:多语言文本的完整加载链路

3.1 配置表的设计与导出

先看配置表怎么设计。前面提到了两张表的方案,这里展开说一下具体结构。

文本Key表(TextKeyConfig)负责定义所有文本的唯一标识和分组信息:

字段名类型说明
Idint自增主键
Keystring文本键名,全局唯一
Groupstring分组,如"UI"、"Item"、"Skill"
Commentstring备注,给策划看的

翻译内容表(TextTranslationConfig)负责存储每种语言的具体内容:

字段名类型说明
Idint自增主键
TextKeyIdint关联TextKey表的Id
Languagestring语言标识,如"zh"、"en"、"ja"
Contentstring翻译内容

这种设计的好处是,加语言只需要在翻译表里加数据,不需要动表结构。导出的时候,ET的配置导出工具会把这两张表分别导出成配置类,运行时通过框架的ConfigComponent加载。

这里有个细节要注意:翻译内容表的数据量可能会很大,如果每种语言都全量加载到内存,对内存的压力不小。我的做法是只加载当前语言的翻译数据,切换语言时重新加载。ET的配置加载支持按条件过滤,可以在加载时传入当前语言参数,只拉取对应语言的数据。

3.2 运行时文本管理类的实现

配置表准备好之后,需要一个运行时管理类来统一处理文本获取。这个类我一般叫它TextManager,核心职责有三个:加载配置、根据Key和当前语言返回文本、处理文本格式化。

public static class TextManager { private static Dictionary<string, string> _textDict = new Dictionary<string, string>(); private static string _currentLanguage = "zh"; public static void Init(string language) { _currentLanguage = language; LoadTextConfig(); } private static void LoadTextConfig() { _textDict.Clear(); var configs = ConfigComponent.Instance.GetAllTextTranslationConfig(); foreach (var config in configs) { if (config.Language == _currentLanguage) { var keyConfig = ConfigComponent.Instance.GetTextKeyConfig(config.TextKeyId); if (keyConfig != null) { _textDict[keyConfig.Key] = config.Content; } } } } public static string Get(string key, params object[] args) { if (!_textDict.TryGetValue(key, out var text)) { Log.Warning($"Text key not found: {key}"); return key; } if (args != null && args.Length > 0) { return string.Format(text, args); } return text; } public static void SwitchLanguage(string language) { _currentLanguage = language; LoadTextConfig(); EventSystem.Instance.Publish(new LanguageChangedEvent()); } }

这段代码有几个关键点值得说明。第一,文本字典用Key作为索引,查找是O(1)的,性能没问题。第二,找不到Key的时候返回Key本身而不是空字符串,这样在开发阶段能快速定位缺失的翻译。第三,切换语言后发了一个事件,UI层可以监听这个事件来刷新显示。

3.3 语言切换时的UI刷新机制

语言切换不是简单地把字典换掉就完事了,界面上已经显示出来的文本需要同步更新。ET框架有完善的事件系统,这里正好用得上。

我的做法是给所有需要响应语言切换的UI组件挂一个统一的脚本,这个脚本在OnEnable时注册语言切换事件,在OnDisable时反注册。事件触发时,重新调用TextManager.Get获取最新文本并赋值给TMP_Text组件。

public class LocalizedText : MonoBehaviour { public string TextKey; private TMP_Text _text; private void Awake() { _text = GetComponent<TMP_Text>(); } private void OnEnable() { Refresh(); EventSystem.Instance.Subscribe<LanguageChangedEvent>(OnLanguageChanged); } private void OnDisable() { EventSystem.Instance.Unsubscribe<LanguageChangedEvent>(OnLanguageChanged); } private void OnLanguageChanged(LanguageChangedEvent evt) { Refresh(); } private void Refresh() { if (_text != null && !string.IsNullOrEmpty(TextKey)) { _text.text = TextManager.Get(TextKey); } } }

这个组件替代了直接往TMP_Text上填文本的做法。策划或UI在预制体上只需要填一个TextKey,文本内容由运行时决定。这样做还有一个额外的好处:如果某个Key在配置表里改了内容,不需要重新打预制体,只需要更新配置表就行。

注意:这个组件在OnEnable时会调用一次Refresh,这意味着每次界面打开都会重新取一次文本。对于频繁打开关闭的界面,可以考虑加一个缓存机制,避免重复查找。不过实测下来,字典查找的开销非常小,除非是极端情况,否则不需要过早优化。

4. 资源本地化:图片、字体和音频怎么跟着语言走

4.1 图片资源的语言适配

文本搞定了,接下来是资源。游戏里有很多带文字的图片,比如Logo、按钮底图、活动横幅等。这些图片在不同语言下需要替换成对应版本。

ET框架的资源管理是基于地址寻址的,资源路径可以动态拼接。我的做法是在资源命名上约定一个规则:需要本地化的资源,在文件名后面加上语言标识。比如login_banner_zh.png、login_banner_en.png。加载的时候,根据当前语言拼接路径:

public static string GetLocalizedAssetPath(string basePath) { string language = TextManager.CurrentLanguage; string localizedPath = $"{basePath}_{language}"; if (ResourceComponent.Instance.Exists(localizedPath)) { return localizedPath; } return basePath; }

这段逻辑的意思是:先尝试加载带语言后缀的资源,如果不存在就回退到默认资源。这样对于没有做本地化版本的资源,不需要额外处理,直接用默认的就行。

4.2 字体资源的处理

字体是资源本地化里比较容易被忽略的一块。中文字体文件通常很大,一个完整的中文字体动辄十几MB,如果每种语言都打包一套完整字体,包体会膨胀得很厉害。

我的建议是按语言拆分字体资源。中文用一套字体,英文和欧洲语言用另一套(通常体积小很多),日文、韩文各自用对应的字体。ET框架支持资源的分组加载,可以把不同语言的字体放到不同的资源组里,根据当前语言动态加载对应的字体组。

具体实现上,可以在TMP的FontAsset上做文章。TMP支持FontAsset的Fallback机制,可以设置一个主字体和多个备选字体。当主字体里找不到某个字符时,会自动从备选字体里找。利用这个机制,可以把常用字符放在主字体里,生僻字符或特定语言的字符放在Fallback字体里。

实测经验:中文字体的优化空间很大。如果游戏里的中文文本量不是特别大,可以用字体子集化工具,只保留用到的字符,能把字体文件从十几MB压到一两MB。这个优化在出海项目里效果非常明显。

4.3 音频资源的语言版本管理

音频的本地化相对简单,主要是配音。如果游戏有角色配音,不同语言需要不同的语音包。处理思路和图片类似,按语言分目录存放,加载时根据当前语言选择对应的目录。

但音频有一个特殊之处:语音和文本的时长可能不一致。中文一句话可能两秒说完,英文可能要三秒,日文可能两秒半。如果UI上有语音播放的进度条或者字幕同步,需要考虑到这个差异。我的做法是在配置表里给每条语音标注时长,UI根据实际时长来调整显示节奏。

5. 踩坑记录:多语言方案落地时最容易出问题的几个环节

5.1 文本拼接的语序陷阱

这是多语言里最经典的坑。中文说"获得3个金币",英文是"Obtained 3 Gold",日文是"金貨を3個獲得"。如果代码里写成"获得" + count + "个" + itemName,翻译的时候根本没法处理。

正确的做法是用占位符。配置表里存的是"获得{0}个{1}",运行时用string.Format填充。这样翻译人员可以根据目标语言的语序自由调整占位符的位置。英文可以写成"Obtained {0} {1}",日文可以写成"{1}を{0}個獲得"。

但占位符也有坑。有些语言的复数形式很复杂,比如英文的"1 item"和"2 items",俄语更复杂,有单数、少数、多数好几种形式。如果项目要支持这些语言,简单的占位符就不够了,需要引入复数规则处理。不过对于大多数项目来说,如果目标语言不涉及复杂的复数变化,占位符方案已经够用了。

5.2 配置表导出的编码问题

ET的配置表导出工具默认可能用的是某种编码格式,如果翻译内容里包含特殊字符(比如日文的假名、韩文的谚文、欧洲语言的重音符号),导出后可能会出现乱码。

我遇到过一次,翻译表里填了法文,导出后é变成了é。排查了半天,发现是导出工具在写文件时用了错误的编码。解决办法是在导出配置里显式指定UTF-8编码,并且在读取的时候也用UTF-8。这个问题在不同版本的ET框架里表现可能不一样,建议在项目初期就用多语言文本做一次导出测试,确认编码没问题。

5.3 语言切换后的界面残留

语言切换后,大部分文本会通过事件刷新,但有些地方容易遗漏:

  • 已经打开的弹窗:如果弹窗是在语言切换前打开的,且没有监听语言切换事件,里面的文本不会更新。
  • 滚动列表里的项:列表项通常是复用的,如果复用的时候没有重新取文本,会显示旧语言的文本。
  • 缓存的数据:有些界面会把文本缓存到本地变量里,语言切换后缓存没有失效。

我的处理方式是:语言切换事件触发后,除了刷新当前显示的文本,还会强制关闭所有非核心弹窗,让玩家重新打开。这样虽然体验上稍微有点突兀,但能确保不会出现中英混杂的情况。对于滚动列表,在Item的刷新逻辑里强制重新取文本,不依赖缓存。

5.4 服务端文本的同步问题

ET是双端框架,服务端也会产生面向玩家的文本,比如邮件内容、排行榜名称、错误提示等。这些文本的多语言处理需要特别注意。

我的做法是:服务端只返回文本Key和参数,不返回具体文本。客户端收到Key和参数后,用本地的TextManager获取对应语言的文本并格式化。这样做的好处是服务端不需要关心玩家用什么语言,也不需要存储多语言文本,逻辑更干净。

但有些场景下服务端必须返回具体文本,比如玩家昵称、公会名称这种用户生成内容。这类文本不做翻译,直接原样返回。需要在UI上做好区分,避免把用户生成内容也当成可翻译文本去处理。

6. 一套可复用的多语言接入流程

6.1 项目初期的接入清单

如果你正在启动一个新项目,打算用ET框架做多语言,可以按这个清单来:

  1. 建立文本规范:约定所有面向玩家的文本必须通过TextManager.Get获取,禁止硬编码。
  2. 创建配置表:建TextKey和TextTranslation两张表,定义好字段结构。
  3. 实现TextManager:包括配置加载、文本获取、语言切换、事件通知。
  4. 封装LocalizedText组件:替代直接往TMP_Text填文本的做法。
  5. 约定资源命名规则:需要本地化的资源加语言后缀,加载时动态拼接。
  6. 字体方案确定:确定主字体和Fallback字体的策略,做好子集化优化。
  7. 服务端文本规范:约定服务端返回Key+参数的模式,不返回具体文本。

这套流程走下来,多语言的基础设施就搭好了。后续加语言只需要在配置表里加数据,不需要改代码。

6.2 中期项目的补救措施

如果项目已经做了一半,之前没考虑多语言,现在要补,工作量会大很多,但也不是没法做。我的建议是分步走:

第一步,全局搜索硬编码文本。用IDE的搜索功能,把所有中文字符串找出来,逐个替换成TextManager.Get调用。这一步最耗时,但必须做。

第二步,梳理配置表。把配置表里所有面向玩家的文本字段找出来,迁移到多语言配置表里。可以写个脚本做批量迁移,减少手工操作。

第三步,处理动态拼接。把代码里所有字符串拼接的地方找出来,改成占位符格式。这一步需要仔细,因为拼接逻辑往往散落在各处。

第四步,UI组件替换。把预制体上的TMP_Text组件替换成LocalizedText组件,填上对应的TextKey。

这个过程大概会占到项目总工作量的10%到20%,取决于项目规模和文本量。所以还是那句话,多语言越早做越好。

6.3 翻译团队协作的注意事项

多语言离不开翻译。和翻译团队协作的时候,有几个点需要提前对齐:

  • 提供上下文:光给一个Key和原文,翻译人员可能不知道这个词用在什么场景。最好在配置表里加一个备注字段,说明这个词的用途、语气、字数限制等。
  • 约定占位符格式:告诉翻译人员占位符不能改、不能删、不能调换顺序(除非目标语言需要)。
  • 控制文本长度:不同语言的文本长度差异很大,中文四个字翻译成英文可能要十几个字符。UI上要预留足够的空间,或者在配置表里标注字数限制。
  • 版本管理:翻译内容会不断更新,需要有一套版本管理机制,确保翻译团队拿到的是最新的文本Key列表,翻译完成后能正确合并回配置表。

我自己的做法是,每次导出翻译任务时,附带一个变更说明,列出新增、修改、删除的Key。翻译团队只需要处理变更部分,不需要每次全量翻译。这样效率高很多,也不容易出错。

7. 一些让多语言更好维护的工程习惯

7.1 Key的命名规范

TextKey的命名看起来是小事,但Key多了之后,命名混乱会让人非常头疼。我一般遵循这样的规范:

  • 用英文小写字母加下划线,如login_title、item_desc_1001。
  • 按模块加前缀,如ui_、item_、skill_、mail_。
  • 动态内容的Key用Id做后缀,如item_name_1001、skill_desc_2003。
  • 避免用拼音,避免用缩写,保证Key本身有可读性。

这样命名之后,看到Key大概能猜到它的用途,查找和排查都方便很多。

7.2 缺失翻译的检测机制

项目大了之后,难免会有Key在配置表里漏了翻译。如果等到运行时才发现,可能已经上线了。我的做法是在打包前加一个检测步骤,遍历所有TextKey,检查每种目标语言是否都有对应的翻译内容。缺失的Key输出到日志里,让策划补全。

这个检测可以写成一个编辑器工具,在CI流程里自动执行。检测不通过就不允许打包,从流程上保证翻译的完整性。

7.3 语言包的按需加载

如果游戏支持的语言很多,把所有语言的翻译数据都加载到内存里是浪费。我的做法是只加载当前语言的翻译数据,切换语言时重新加载。ET的配置加载支持条件过滤,实现起来不难。

对于资源(图片、字体、音频),也是同样的思路。不同语言的资源放在不同的资源组里,根据当前语言动态加载对应的资源组。ET的资源管理支持资源组的引用计数和卸载,切换语言时把旧语言的资源组卸载掉,释放内存。

7.4 测试环节的语言覆盖

多语言功能的测试容易被忽略。我的建议是在测试计划里明确加入多语言相关的用例:

  • 切换每种支持的语言,检查所有界面的文本是否正确显示。
  • 检查是否有文本溢出、截断、重叠的情况。
  • 检查资源是否正确替换,有没有加载到错误语言的资源。
  • 检查服务端返回的文本是否正确显示。
  • 检查语言切换后,已打开的界面是否正确刷新。

这些用例看起来简单,但实际执行的时候往往能发现不少问题。尤其是文本溢出,英文和德文的文本通常比中文长很多,UI上很容易出问题。

8. 关于多语言方案选型的一点个人看法

做了几个出海项目之后,我对多语言方案的选择有一些自己的体会。ET框架下自建多语言方案,前期确实需要投入一些开发成本,但后期的可控性和维护性是最好的。你完全掌控文本的存储结构、加载时机、切换逻辑,遇到问题也能快速定位和修复。

如果项目规模很小,文本量不大,也可以考虑更轻量的方案,比如直接用JSON文件存翻译,运行时加载。但如果项目有一定规模,或者有双端需求,我还是建议走配置表这条路。ET的配置体系已经帮你处理了加载、缓存、热更这些底层问题,你只需要在上面搭一层文本管理逻辑就行。

另外,多语言不只是技术问题,更是流程问题。技术方案再好,如果策划不配合、翻译流程不顺畅、测试不覆盖,最后还是会出现各种问题。所以我的经验是,多语言这件事要从项目管理的层面去推动,技术方案只是其中一环。把规范定好、流程跑通、工具到位,剩下的就是执行了。

最后分享一个我一直在用的小技巧:在开发阶段,可以加一个快捷键,快速切换语言。这样在调试UI的时候,可以随时切换语言看效果,不用每次都去设置界面里改。这个快捷键只在编辑器下生效,打包后自动禁用。实现起来很简单,在Update里检测按键,调用TextManager.SwitchLanguage就行。别小看这个功能,它能帮你省下大量来回切换的时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询