很多用Delphi做桌面应用的朋友,迟早会遇到一个绕不开的问题:界面菜单是英文的,想要翻译成中文。这个项目就是一次完整的菜单本地化过程,从最初的硬编码扫描,到语言包设计,再到动态控件翻译,前后踩了不少坑。这篇内容适合那些正在维护老Delphi项目、或者新项目一开始就想做多语言支持的开发人员,无论你用VCL还是FireMonkey,思路都通用。我会把实际步骤、关键代码和坑都摊开讲,尽量让你看完能直接在自己项目里用起来。
1. 项目背景:菜单中文化并没那么简单
1.1 这个项目的真实起点
我们手上有一套用Delphi写的行业软件,功能很完整,但界面菜单、右键菜单、按钮提示全是英文的。客户倒不是看不懂英文,但验收的时候提了一条:希望界面菜单全部显示为中文,减少培训成本。这个要求听上去轻飘飘的,真正做起来才发现牵一发动全身。
菜单中文化不只是把“File”改成“文件”那么简单。一个典型的主程序里,菜单可能分好几层:主菜单、子菜单、右键弹出菜单、工具栏上的按钮Hint、Action列表里的Caption,甚至还会有动态创建的菜单项。这些文本有的写在DFM文件里,有的写在代码里,有的藏在第三方控件的默认字符串里。如果只是打开DFM挨个改,第一次能应付,但后续更新程序时,英文原版一覆盖,中文就没了。
我们当时统计了一下,整个程序里的菜单项和按钮提示加起来大概有800多条。再加上消息框、对话框标题、状态栏等公共文本,总量接近1600条。这还是保守估计,因为有些文本是通过拼接生成的,比如“确定要删除订单 #10086 吗?”这种运行时拼出来的句子,翻译起来比静态文本麻烦得多。
1.2 Delphi界面本地化的三大痛点
第一个痛点是字符串散落。Delphi项目的界面文本分布非常分散:DFM资源文件里有,单元文件里硬编码字符串常量里有,ActonManager的Action列表里有,第三方皮肤/界面库(比如DevExpress、TMS)的组件属性里也有。想靠人工找,几乎不可能找全。
第二个痛点是编码与字体。Delphi 2009之前默认用AnsiString,中文在旧项目里经常出现乱码;2009之后整个RTL切换到Unicode,情况好了很多,但如果你用的是旧版本编译出的DLL、第三方库,还是会遇到字符编码转换问题。另外,菜单字体如果只指定了英文界面常用字体如Tahoma,翻译成中文后可能显示成方框或乱字形。
第三个痛点是动态性。软件运行时会根据用户权限、配置来动态生成菜单,比如最近打开的文件列表、自定义工具栏按钮、插件注册的菜单项。这些项目在编译期根本没有对应的DFM,翻译引擎必须能在运行时捕获并替换。
所以,我们最终没有选择“直接在DFM里改中文”这种一次性方案,而是做了一个小而稳的运行时翻译框架:扫描界面控件树,从语言包中读取中文映射,批量更新Caption、Hint和ShortCut。这套东西后来不仅用于菜单,也扩展到了整个界面的中文化,效果还不错。
2. 菜单翻译的方案选型:先想清楚再动手
2.1 基于DFM资源文件直译:最原始但最繁琐
最直接的做法是用Delphi自带的Translation Manager(集成翻译环境,ITE),从已编译的程序里提取出所有ResString资源,然后逐条翻译成不同语言。这个方案对窗体标题、静态控件的Caption很有效,因为它能直接修改DFM副本,甚至还能以增量方式同步英文版本更新。
但实际用起来有几个问题:第一,它生成的翻译资源是一种类似“外部翻译仓库”的结构,和代码工程绑定太紧,换人维护成本高。第二,它的扫描范围以窗体、数据模块为单元,对于代码里动态赋值的字符串无能为力,比如MenuItem := TMenuItem.Create(nil); MenuItem.Caption := 'Recent Files';,这种来源完全扫不到。第三,一旦涉及第三方控件内部封装的默认文本,比如一个按钮控件自带的下拉箭头Hint,ITE根本不会处理。
不是说ITE没用,它适合那种“界面文本极其统一、项目结构简单”的小工具。但对我们这种菜单频繁动态变化、还要支持远程更新的产品来说,它太笨重了。
2.2 基于运行时语言包:实际项目里的主流做法
后来我们换了思路:不追求修改Delphi资源文件,而是把翻译做成运行时替换。具体来说,程序启动时加载一个外部语言文件(比如JSON或INI),里面是一个字符串ID到中文文案的映射表。然后递归遍历当前窗体上的所有组件,遇到TMenuItem、TButton、TLabel、TAction等控件,就用映射表里的文案替换掉原文本。
这个方案的优点非常明显:
- 不污染原始DFM,英文原版仍然是开发基线,中文包独立维护。
- 支持热更新,改动语言包不需要重新编译exe。
- 能通过代码主动注册动态菜单项,让运行时创建的控件也有翻译机会。
- 可以同时支持多语言,只要加载不同语言包文件就能切换。
代价也不小:需要自己写遍历器、字符串提取器和映射表维护工具。不过对于大型项目来说,这笔投入是值得的。
2.3 开源库与商业组件的取舍
市面上有一些现成的多语言组件,比如Delphi GetText(dxgettext)、SimpleTranslation、i18n for Delphi,还有商业的TsiLang。它们各有侧重点。
dxgettext采用Linux生态常见的GNU GetText方案,用.po文件存翻译,还自带一个扫描工具可以提取源代码里的_('...')包裹的字符串。这个设计很优雅,但它要求开发者在写代码时就遵守“所有用户可见字符串都用_函数包裹”的规范。如果是老项目改造,代码里没有规范标记,扫描工具帮不了太多。
TsiLang这类商业组件功能很全,能做IDE集成、DFM批量转换、运行时切换,价格也不便宜。我们评估后认为,为了菜单翻译引入一个重量级商业组件,后期授权和升级成本都不小,而且它同样做不到对动态创建控件的完美翻译。
最终我们选择了自己写一个轻量框架,主要基于两点考虑:一是项目里已经有大量历史代码,不可能全部用_()重写;二是不想引入额外商业依赖。我们的做法是“双轨制”:DFM里已有的静态文本,通过遍历控件属性来翻译;代码里的硬编码字符串,尽量集中到一个常量注册表里,运行时统一查表。
3. 核心实操:从扫描文本到批量替换
3.1 扫清界面菜单上所有文本的藏身之处
动手之前,我们先把菜单相关的文本分成四类,避免漏网:
- 第一类:主菜单(TMainMenu)和弹出菜单(TPopupMenu)里的
TMenuItem.Caption和Hint。 - 第二类:工具栏按钮(TToolButton)、ActnBar等控件的
Caption、Hint,以及关联的TAction.Caption、TAction.Hint。 - 第三类:运行时动态创建的菜单,通常代码里有
CreateMenuItem之类的函数,里面直接写了Caption := 'xxx'。 - 第四类:状态栏、消息框、对话框标题等公共文本,虽然不算菜单,但经常和菜单联动。
你需要把项目里的DFM文件全部打开,用文本搜索搜Caption和Hint关键字,先建立一个静态文本清单。这一步很枯燥,但非常重要。我们用了一个小工具,把DFM里的字符串正则提取出来,去掉重复,导成一个初始语言包。语言包格式我们选的是JSON,因为Delphi自带的System.JSON可以直接解析,不需要额外引用第三方解析库。
3.2 写一个通用的控件递归遍历器
要翻译界面上的控件,核心是一个递归函数:给定一个组件(通常是窗体或Frame),遍历它下面的所有子组件,检查是否是需要翻译的类型。
Delphi所有可视控件都继承自TComponent,每个组件都有ComponentCount和Components属性,所以遍历本身很简单。关键是怎么把翻译逻辑做得通用。我们写的遍历器大致长这样:
procedure TranslateComponentStrings(AComponent: TComponent; const LangTable: TDictionary<string, string>); var i: Integer; Child: TComponent; begin if AComponent is TMenuItem then begin TMenuItem(AComponent).Caption := GetTranslatedText(TMenuItem(AComponent).Caption, LangTable); TMenuItem(AComponent).Hint := GetTranslatedText(TMenuItem(AComponent).Hint, LangTable); end else if AComponent is TAction then begin TAction(AComponent).Caption := GetTranslatedText(TAction(AComponent).Caption, LangTable); TAction(AComponent).Hint := GetTranslatedText(TAction(AComponent).Hint, LangTable); end else if AComponent is TLabel then begin TLabel(AComponent).Caption := GetTranslatedText(TLabel(AComponent).Caption, LangTable); end else if AComponent is TButton then begin TButton(AComponent).Caption := GetTranslatedText(TButton(AComponent).Caption, LangTable); end // 可以根据需要继续扩展 TCheckBox、TGroupBox、TSheet等 for i := 0 to AComponent.ComponentCount - 1 do begin Child := AComponent.Components[i]; TranslateComponentStrings(Child, LangTable); end; end;注意这里有个细节:很多开发者只遍历WinControl,漏掉了非可视组件。但菜单和Action恰恰是TComponent而非TWinControl,如果你只遍历窗体上的Control集合,永远扫描不到菜单项。所以一定要用ComponentCount体系,把所有子组件都过一遍。
3.3 语言包加载与翻译命中逻辑
语言包加载我们做了两层结构。第一层是原始的“英文字符串→中文字符串”映射,从JSON文件读入后放到TDictionary<string, string>里,Key是原始英文字符串,Value是中文。第二层是一个“已翻译控件注册表”,用TObjectList<TMenuItem>之类的东西记录哪些菜单项已经被替换过,防止重复翻译。
那为什么直接用原始英文做Key?因为菜单代码里没有稳定的字符串ID,用英文原文当Key成本最低,而且原文在运行时一定能拿到。缺点是有时候两条不同界面上的文本完全一样,但翻译语境不同。比如“Display”在菜单里可能是“显示”,在“Display Settings”里又得翻译成“显示设置”,如果只按单词翻译会出错。我们后来为这种情况单独加了Key属性,在动态创建菜单时手工指定翻译ID。
翻译命中逻辑也需要注意回退问题。如果语言包里没有当前文本,我们有三条策略:一是保持原文不动;二是尝试去掉快捷键前缀后匹配一次,比如“&File”匹配不到时用“File”再查;三是记录到未翻译日志里,方便后续补翻译。
3.4 中文显示与字体处理的几个关键点
菜单文本换成中文后,如果字体没处理好,界面看起来会非常糟糕。Windows上VCL默认的菜单字体是Tahoma或Segoe UI,中文渲染时通常由字体回退机制负责,但Delphi的老项目经常在Form.Font或者Screen.MenuFont里显式指定了英文字体,回退就可能失效,表现成方块或寥寥草草的笔画。
我们当时做了三件事:第一,在翻译中文化模式下,把Screen.MenuFont.Name设为'Microsoft YaHei'(微软雅黑),这样主菜单和弹出菜单统一用雅黑显示。第二,对每个窗体的Font.Name也做检查,如果发现是Tahoma或Arial,就替换为'Microsoft YaHei',字号保持原样或者略微调大到9pt。第三,如果目标系统是Windows 7之前的旧服务器,雅黑可能没装,可以回退到'SimSun'(宋体),丑一点但至少不会成乱码。
在FireMonkey框架下,情况类似但不完全相同。FMX的TMenuItem属于不同控件树,而且跨平台时对字体名称的解析更严格。如果目标是Linux或macOS,建议用TTextFont统一管理器设置Family,而不是硬编码字体名。热词里很多人搜“delphi 12 firemonkey源码”大概也是想解决这类问题,思路一致:菜单本地化不是改字符串,而是“字符串+字体+布局”三项一起改。
4. 动手实现:代码片段与完整流程演示
4.1 翻译引擎核心单元
我们把整个方案封装成一个叫MenuLocalizer.pas的单元,对外只暴露两个函数:LocalizeForm(AForm)和RegisterDynamicMenu(AMenuItem, AOriginalString)。这样业务代码不需要关心翻译细节。
伪代码的核心部分如下:
unit MenuLocalizer; interface uses System.SysUtils, System.Classes, System.JSON, System.Generics.Collections, VCL.Menus, VCL.ActnMan, VCL.StdCtrls; procedure LoadLanguagePack(const AFileName: string); procedure LocalizeForm(AForm: TForm); procedure RegisterDynamicMenu(AMenuItem: TMenuItem; const AOriginalString: string); function GetTranslatedText(const AEnglishText: string): string; implementation var GLangTable: TDictionary<string, string>; GLoaded: Boolean; procedure LoadLanguagePack(const AFileName: string); var LJson: TJSONObject; LPair: TJSONPair; LRaw: string; begin if not GLoaded then begin GLangTable := TDictionary<string, string>.Create; GLoaded := True; end; GLangTable.Clear; LRaw := TFile.ReadAllText(AFileName, TEncoding.UTF8); LJson := TJSONObject.ParseJSONValue(LRaw) as TJSONObject; try for LPair in LJson do GLangTable.AddOrSetValue(LPair.JsonString.Value, LPair.JsonValue.Value); finally LJson.Free; end; end; function GetTranslatedText(const AEnglishText: string): string; begin Result := AEnglishText; if AEnglishText = '' then Exit; if GLangTable.TryGetValue(AEnglishText, Result) then Exit; // 尝试去掉 & 前缀 if IsShortCutPrefixed(AEnglishText) then begin if GLangTable.TryGetValue(StringReplace(AEnglishText, '&', '', [rfReplaceAll]), Result) then Exit; end; // 记录未翻译字符串 LogUntranslated(AEnglishText); end;JSON文件的格式长这样:
{ "&File": "文件(&F)", "&Edit": "编辑(&E)", "&Open...": "打开...", "&Save As...": "另存为...", "Exit": "退出", "Delete Order": "删除订单", "Confirm": "确认" }LoadLanguagePack要在Application.Initialize之后、主窗体显示之前调用,保证所有窗体创建时就能拿到翻译表。
4.2 菜单与ActionManager的处理
纯手工创建的菜单用上面的遍历器就能搞定,但如果你用了TActionManager或者TActionToolBar,遍历逻辑要额外注意。因为TAction是独立于菜单控件的对象,通常挂在ActionManager的一个列表里,不在窗体组件的直接子级中。我们的遍历器已经对TAction做了处理,但这里有个顺序问题:先翻译Action,再翻译菜单项,因为菜单项的Caption会自动跟随Action的Caption。如果反过来先翻译菜单项,随后Action把英文又同步回菜单,你就白干了。
实际处理顺序是这样的:
- 先遍历窗体组件树,但只处理
TAction,更新所有Action的Caption和Hint。 - 再遍历窗体组件树,处理
TMenuItem、TToolButton等控件。 - 最后遍历
TStatusBar.Panels,因为它们不是组件子项,需要单独访问。
在FireMonkey里,TActionManager的使用比VCL少一些,但动态创建的TMenuItem同样需要手动注册。我们还写了一个辅助类TMenuTranslator,它维护一个TList<TMenuItem>,当用户通过代码新增菜单项时,调用RegisterDynamicMenu(NewItem, 'Recent Files'),下一次语言切换时可以自动更新。这个类把动态菜单的坑基本填平了。
4.3 动态菜单与运行期创建控件的翻译
动态菜单是很多项目都会忽略的地方。比如你的程序有“最近打开文件”列表,每次启动都会把历史文件动态添加到底部菜单里。这个列表本身就是变化的,语言包没法预先定义。但我们依然可以对列表项统一加前缀,例如&File菜单下的“Recent File 1 : C:\xxx”通常不需要翻译,但菜单组标题“Recent Files”需要。
更麻烦的是自定义插件系统。插件通过接口向主程序注册菜单项,菜单标题由插件内的字符串常量定义。我们做了个约定:插件注册菜单时,必须带上一个TranslationKey,主程序的翻译引擎会在语言包里搜索plugin.菜单标识这种Key。如果找不到,就显示插件自带的英文标题。这样插件菜单也能纳入统一翻译体系。
代码示例如下:
procedure RegisterDynamicMenu(AMenuItem: TMenuItem; const AOriginalString: string); var LTranslated: string; begin LTranslated := GetTranslatedText(AOriginalString); if LTranslated <> AOriginalString then AMenuItem.Caption := LTranslated; FDynamicMenus.Add(AMenuItem); end; procedure RefreshDynamicMenus; var LItem: TMenuItem; begin for LItem in FDynamicMenus do begin LItem.Caption := GetTranslatedText(LItem.Caption); end; end;注意刷新动态菜单时,不要把已经翻译成中文的Caption再次放到语言包里查。如果语言包里的某个中文文案碰巧也是另一个条目的Key,会出现二次翻译的鬼畜情况。我们在RefreshDynamicMenus里加了标记,翻译成功后把Tag置为1,后续跳过。
4.4 测试与验证结果
翻译完成后,测试不能只看主界面。我们把所有菜单截屏,和英文版并排对比,检查三件事:有没有漏译、有没有翻译错位、中文是否被截断。
漏译检查我们靠日志文件。LogUntranslated把所有未命中的字符串写到untranslated.txt里,测试时跑一遍完整操作流程,回看日志就知道还有多少漏网之鱼。翻译错位主要靠人工检查,尤其是涉及&前缀和快捷键的地方。中文文本通常比较紧凑,像“另存为...”这样的菜单项宽度足够,但有些自定义控件绘制的Toolbar按钮,中文可能被裁剪成省略号,这时需要调整Layout属性或增加MinimumWidth。
我们还做了多语言切换测试:在程序里用一个语言下拉框,切换中文和英文,菜单必须立即全部刷新。这里有个小技巧,切换语言后调用Form.ActiveControl := Form.ActiveControl强制重绘,或者遍历Screen.Forms重新执行LocalizeForm。测试结果最终是满意的,800多条菜单文本全部翻译完成,动态菜单也能正常切换。
5. 踩坑实录:菜单翻译中遇到的常见问题
5.1 中文乱码,到底是谁的锅
乱码问题是菜单翻译里最让人头疼的。有一次我们在一个Windows Server 2012的机器上跑翻译后的程序,主菜单全是问号。排查了半天,发现不是代码问题,而是系统的“非Unicode程序的语言”设置为英文,Delphi编译出来的程序如果使用了UTF-8编码的语言包,在特定环境下读取会异常。
解决办法是我们统一让语言包文件用UTF-8 with BOM保存,并且读取时显式指定TEncoding.UTF8。另外,如果使用了老旧的第三方DLL,它返回的字符串是Ansi字符串,你需要用TEncoding.GetEncoding(936)(GBK)手动转换。
我还遇到过一个特例:某个菜单项的Caption字符串本来是全角的“:”而不是半角“:”,翻译表中写了半角,匹配失败,整条菜单没翻译。这种事只能靠测试日志抓,没有更好的办法。
5.2 菜单翻译了一半就不动了
这不是程序死机,而是“只翻译了主窗体菜单,弹出菜单没反应”。原因通常是弹出菜单没有作为任何组件的子组件挂接,它们只是赋值给了某个控件的PopupMenu属性,并没有在窗体的Component对象树里。遍历ComponentCount根本碰不到它们。
解决办法:遍历时要额外扫描每个控件的PopupMenu属性。如果某控件存在PopupMenu,单独对它执行一次TranslateComponentStrings,并且把这个弹出菜单加入一个全局列表统一管理。另一个类似坑是TFrame,Frame里的组件树和宿主窗体不在同一个层级,需要单独调用TranslateComponentStrings(AFrame, LangTable)。
5.3 手工新建的菜单项不在资源里
很多人喜欢在代码里写mi := TMenuItem.Create(nil); mi.Caption := 'New Action'; MainMenu.Items.Add(mi);,这种写法有两个问题:一是TMenuItem.Create(nil)没有Owner,控件树里找不到它,遍历时直接漏掉;二是Caption是代码硬编码,语言包靠原始英文匹配也找不到。
我们对这种代码做改造时,强行定了一个规范:所有动态创建的菜单项,一律调用RegisterDynamicMenu注册;所有用户可见的硬编码字符串,尽量提取为常量数组,由语言包统一映射。虽然历史代码很多,但用正则一页一页过,一个星期也能改完。热词里“delphi listbox additem”也是类似场景,动态添加的TListBox.Item里的文本同样需要走注册机制,否则翻译不到。
5.4 快捷键和助记符冲突的处理
中文菜单经常出现快捷键冲突。比如“文件”和“编辑”如果都写(&F)和(&E)不会冲突,但“帮助”如果写(&H)就和“编辑”没关系了。真正麻烦的是同一个菜单里,两个子菜单想用同一个助记符,比如“另存为”和“定位”都想要(&L),这种情况就要手工调整。
我们制定了三条规则:第一,中文菜单的助记符优先使用中文对应的英文单词首字母,保持用户习惯;第二,如果冲突,按“文件F、编辑E、视图V、工具T、帮助H”的固定映射进行分配;第三,尽量通过ShortCut定义真正的快捷键,而不是依赖助记符。翻译表里我们直接存中文+助记符的完整字符串,比如Save As...翻译为“另存为(&A)”,这样显示和快捷键都正确。
5.5 第三方界面库自带的英文文本
项目里用了DevExpress、TMS之类的界面库后,它们的工具栏按钮、RibbonPage标题、内置对话框按钮都有默认英文。我们试过用遍历库自带控件来处理,工作量极大。后来发现不少组件库自带了官方语言资源(比如DevExpress的dxLocalization),直接引入它,再把语言包导入就好。
以DevExpress为例,在菜单翻译完成后,调用dxLocaleRegister('zh-CN', ...),然后设置cxLocalizer.Active := True,很多默认文本会自动变成中文。第三方库的本地化最好和你的菜单翻译方案分离,不要混在一个语言包里,因为它们更新频繁,单独维护反而省心。
6. 向外延伸:从菜单翻译到整个Delphi应用的本地化
6.1 不仅仅是菜单:与热词相关的其他本地化场景
菜单翻译只是界面本地化的第一层。真正做产品国际化时,你会发现需要翻译的地方还包括工具栏、右键菜单、对话框、报表导出和数据库字段提示。热词里“delphi rbuilder 导出成pdf”就是一个典型:如果你把报表里的中文数据导出成PDF,但是PDF没有嵌入中文字体,导出的文件就是乱码或空白。这个和菜单字体问题同源,都是“文本能显示,但渲染环境拿不到正确的字体资源”。
处理方案一般是在生成PDF时嵌入“微软雅黑”或“思源黑体”,或者使用支持Unicode映射的组件,例如TdxPDFViewer、iText for Delphi。iText是Java版移植过来的Delphi组件,导出PDF时需要额外设置BaseFont.CreateFont并指定中文字体路径。如果你只是在做程序本地化,别忽略这些导出模块,否则菜单是中文了,用户导出的报表还是满屏乱码,验收依然过不了。
6.2 借助翻译API与大模型批量处理语言包
手动维护1600条翻译太累了。我们当时把提取出来的英文字符串放到一个JSON文件里,写了个脚本调用通用翻译接口批量翻译。现在很多热词比如“腾讯翻译 python”“大模型翻译json字段”“沉浸式翻译”都表明大家已经不满足于一个个查字典,而是用大模型或在线翻译一次处理整个语言包。
实际操作中,我们先用本地工具把DFM和代码里的字符串提取成source_zh.json,然后用一个Python小脚本把JSON里未翻译的英文批量发送给翻译服务。注意两点:一是加&前缀的快捷键字符串要保留原始占位符,不能让翻译接口把&F吃掉;二是翻译回来的文案必须经过评审,因为在线翻译在处理“OK”“Cancel”这类词时可能给出“好的”“取消”这种不统一的术语,这时需要建立一个术语表,例如统一用“确定”而不是“好的”。
批处理完之后,再导入Delphi程序跑一遍未翻译日志,剩下的少量特殊句子手工修。这套流程从“扫描-翻译-导入-测试”走一轮只需要两天时间,比之前全靠手改快了一个量级。热词里“大模型翻译json字段”其实已经是很多Delphi开发者的常态操作。
6.3 把语言包管理纳入日常开发流程
如果你打算长期做多语言,语言包不应该只是发布前的一次性工作。我们后来把语言包文件放到了版本库里,和代码一起提交。CI脚本里加了一步校验,如果检测到新增了未翻译字符串,构建就报警。这样开发人员每次写新功能时,顺手把英文文案补充到en.json里,翻译人员只需要维护一份对比表即可。
再往后我们还做了一个小管理页面,直接浏览语言包JSON,标注翻译状态,这个对非技术人员特别友好。菜单翻译虽然看起来已经是完成态,但产品更新几次之后,新菜单、新按钮不断出现,如果没有这套流程,语言包很快就会过期。所以从开始就把它当产品的一部分来维护,而不是当成“一次性翻译任务”。
我在实际项目中体会到,Delphi的菜单翻译,技术难度不在“怎么改字符串”,而在“怎么把所有字符串都收拢到一起,并且让未来的每一次改动都不漏”。这套运行时遍历+语言包的方式,虽然初期要写代码、扫文本,但长期收益非常明显。最后再分享一个小技巧:在中文语言包里,一开始就把所有消息框文案定义好,不要等编码时临时写,因为翻译引擎统一收口比事后补漏要轻松得多。