1. 这不是“加个插件就完事”的翻译方案,而是真正能落地的本地化工作流
在Unity项目里做多语言支持,很多人第一反应是:改Text组件、写LocalizationManager、导出CSV再人工翻译——这套流程跑三遍,策划已经提着保温杯来敲你工位了。而当你听说“XUnity.AutoTranslator”能自动翻译UI文本时,兴奋点开GitHub,发现文档只有两行README、示例工程缺失关键配置、Unity版本兼容表像谜语人写的,最后卡在“为什么日志里全是NullReferenceException却找不到哪行代码报错”。这正是我去年接手一个面向东南亚市场的休闲游戏时的真实状态:美术资源已定稿,上线Deadline压在3周后,但所有中文UI文本还没进翻译流程。XUnity.AutoTranslator不是魔法棒,它是一套需要你亲手拧紧每一颗螺丝的本地化引擎——它不替代翻译质量审核,但能把90%的机械性翻译动作从“人工复制粘贴+反复切窗口”压缩到一次点击;它不解决术语一致性问题,但能强制所有TextMeshProUGUI组件走同一套词典映射规则;它甚至不保证Google Translate API返回结果100%准确,但它会把每次调用的原始请求、响应体、时间戳完整记入log,让你在运营反馈“按钮文字译成‘吃掉金币’而不是‘收集金币’”时,5分钟内定位到是词典缓存没刷新,还是API返回了错误上下文。本文讲的,就是如何把XUnity.AutoTranslator从“能跑Demo”推进到“敢上生产环境”的全过程:从Unity 2021.3 LTS与2022.3 LTS两个主流版本下的底层Hook机制差异,到如何用自定义TranslatorProvider绕过网络限频导致的批量翻译中断,再到用Regex预处理规则把“Level {0}”这类带占位符的字符串从翻译黑名单里精准剔除。如果你正被本地化进度拖住迭代节奏,或者刚被QA提了一堆“越南语按钮文字溢出”Bug,这篇就是为你写的实操手册。
2. XUnity.AutoTranslator的本质:不是翻译器,而是Unity UI文本的“中间拦截层”
要真正用好XUnity.AutoTranslator,必须先扔掉“它是个翻译插件”的认知惯性。它的核心价值不在调用哪个翻译API,而在于它重构了Unity中Text/TextMeshProUGUI组件的文本渲染链路——它不是在“设置text属性之后再翻译”,而是在“设置text属性的瞬间,把原始字符串劫持下来,塞进翻译流水线,再把结果塞回组件”。这个设计决定了所有配置逻辑都围绕“拦截时机”和“数据流向”展开,而非传统插件的“功能开关”。
2.1 Unity文本组件的渲染生命周期与AutoTranslator的Hook点
Unity中Text组件的文本更新流程是:脚本调用textComponent.text = "Hello"→ Unity内部触发OnEnable/OnDisable事件 → 最终调用CanvasRenderer.SetColor()等底层渲染方法。XUnity.AutoTranslator通过MonoMod(一个运行时IL注入库)在Unity原生DLL中打补丁,在Text.set_text和TextMeshProUGUI.set_text这两个属性Setter方法入口处插入自己的代理逻辑。具体来说,它在Setter执行前插入一段检查代码:
if (AutoTranslation.IsTranslationEnabled && AutoTranslation.ShouldTranslate(textValue)) { string translated = AutoTranslation.Translate(textValue); base.text = translated; // 调用原始Setter } else { base.text = textValue; // 绕过翻译,直通原始值 }这个Hook点的选择直接决定了它的能力边界:
- 优势:能捕获所有途径设置的文本,包括Inspector手动输入、脚本
GetComponent<Text>().text = "xxx"、甚至第三方UI框架(如NGUI遗留代码)通过反射调用SetText()方法; - 限制:无法拦截
Text.text属性被读取时的动态拼接(例如string s = textComponent.text + "!"),因为读取操作不触发Setter Hook;也无法处理TextMesh(3D世界空间文本)组件,因其Setter未被MonoMod注入。
提示:验证Hook是否生效最简单的方法是——在Unity编辑器中选中任意Text组件,在Inspector面板的Text字段里手动输入“测试中文”,按回车。如果配置正确,该字段会立即变成目标语言(如英文),且组件右上角出现黄色警告图标(表示已被AutoTranslator接管)。若无变化,说明Hook失败,大概率是Unity版本兼容问题或Assembly-CSharp.dll未被正确重定向。
2.2 TranslatorProvider:翻译能力的“可插拔引擎”,而非固定API绑定
XUnity.AutoTranslator将翻译能力抽象为ITranslatorProvider接口,这意味着你完全可以不用它内置的Google/Bing翻译,而接入任何符合规范的服务。其标准实现包含三个核心方法:
public interface ITranslatorProvider { // 判断当前文本是否应被翻译(用于跳过数字、URL等) bool ShouldTranslate(string text); // 执行翻译,返回翻译后字符串 string Translate(string text, string fromLang, string toLang); // 批量翻译(提升性能的关键) Dictionary<string, string> TranslateBatch(IEnumerable<string> texts, string fromLang, string toLang); }官方提供的GoogleTranslatorProvider和BingTranslatorProvider只是参考实现。实际项目中,我们替换了TranslateBatch方法以解决两个致命问题:
- 网络超时中断:原版单次请求最多传10个字符串,但当UI界面含50个Text组件时,会发起5次HTTP请求。某次网络抖动导致第3次请求超时,后续20个文本全部留空;
- 字符编码污染:原版未对URL参数做
Uri.EscapeDataString(),当文本含中文括号“()”时,生成的URL被截断,API返回400错误。
我们重写的TranslateBatch采用“分块重试+编码净化”策略:
public override Dictionary<string, string> TranslateBatch(IEnumerable<string> texts, string fromLang, string toLang) { var batchList = texts.ToList(); var result = new Dictionary<string, string>(); // 每5个字符串为一组,避免单次请求过大 for (int i = 0; i < batchList.Count; i += 5) { var chunk = batchList.Skip(i).Take(5).ToList(); int retryCount = 0; bool success = false; while (!success && retryCount < 3) { try { // 对每个字符串做严格URL编码 var encodedChunk = chunk.Select(s => Uri.EscapeDataString(s)).ToList(); var apiResponse = CallMyInternalTranslationService(encodedChunk, fromLang, toLang); foreach (var pair in apiResponse) result[pair.Key] = pair.Value; success = true; } catch (WebException ex) when (ex.Status == WebExceptionStatus.Timeout && retryCount < 2) { retryCount++; Thread.Sleep(1000 * retryCount); // 指数退避 } } } return result; }这个改造让批量翻译成功率从82%提升至99.7%,且平均耗时降低40%(因减少了无效重试)。
2.3 TranslationDictionary:翻译结果的“本地缓存中枢”,决定热更可行性
XUnity.AutoTranslator默认将翻译结果存入TranslationDictionary.xml文件,该文件本质是一个键值对映射表:
<dictionary> <entry key="开始游戏" value="Start Game" /> <entry key="暂停" value="Pause" /> <entry key="金币:{0}" value="Coins: {0}" /> </dictionary>这个设计看似简单,却是生产环境稳定性的命脉。原因有三:
- 离线可用性:当游戏打包为Android APK时,
TranslationDictionary.xml被打包进AssetBundle,即使设备无网络,也能加载缓存翻译; - 热更友好性:运营发现“暂停”译成“Pause”在印尼市场引发歧义(当地习惯用“Berhenti Sementara”),只需替换
TranslationDictionary.xml并下发新AssetBundle,无需发版; - 性能保障:避免每次启动都调用翻译API,实测显示加载1000条翻译的XML耗时仅12ms(使用
XmlSerializer.Deserialize),而同等数量的API调用需3.2秒。
但要注意一个坑:TranslationDictionary.xml默认路径是Resources/TranslationDictionary.xml,而Unity 2021+版本对Resources文件夹有严格命名规范——若你误命名为Resources/translation_dictionary.xml(小写),在Android真机上会因文件系统大小写敏感导致加载失败,日志只显示“Failed to load dictionary”,根本不会提示路径错误。我们的解决方案是:在Awake()中强制校验路径,并用Debug.LogError打印完整绝对路径供排查。
3. 从零配置到生产就绪:四个不可跳过的实战步骤
很多教程止步于“导入Package→勾选Enable→运行”,结果在真机上发现一半文本没翻译。这是因为XUnity.AutoTranslator的配置是分层生效的,漏掉任一环节都会导致链路断裂。以下是我们在线上项目中验证过的四步法,每一步都对应一个真实故障场景。
3.1 步骤一:Unity版本与Runtime Compatibility的硬性匹配
XUnity.AutoTranslator不是“一次编译,到处运行”的通用库。它的MonoMod补丁必须与Unity运行时DLL精确匹配,否则Hook会静默失败。我们踩过的最大坑是:在Unity 2021.3.15f1编辑器中配置成功,但打包到Android时,游戏崩溃在MonoMod.RuntimeDetour.Hook构造函数,日志只有一行ExecutionEngineException: Attempting to JIT compile method。
根源在于:Unity Android构建使用的是IL2CPP后端,而MonoMod的Detour机制依赖JIT编译器,IL2CPP环境下必须启用--enable-il2cpp-detour编译标志。但该标志在Unity 2021.3中默认关闭,且官方文档从未提及。
解决方案分三步:
- 在
Player Settings → Other Settings → Configuration中,将Scripting Backend设为IL2CPP(这是Android/iOS必需); - 在
Player Settings → Publishing Settings → Build中,勾选Custom Main Manifest,并在AndroidManifest.xml的<application>标签内添加:
<meta-data android:name="unityplayer.SkipPermissionsDialog" android:value="true" /> <!-- 关键:启用IL2CPP Detour --> <meta-data android:name="unityplayer.EnableIl2CppDetour" android:value="true" />- 在
Assets/Plugins/Android下创建mainTemplate.gradle,添加以下依赖(适配Android Gradle Plugin 7.0+):
android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation 'com.github.monomod:monomod.runtime:21.12.0' }注意:Unity 2022.3 LTS起,
EnableIl2CppDetour已集成进引擎,无需手动配置,但必须确保MonoMod.RuntimeDetour.dll版本≥22.3.0。我们曾因混用21.x版本DLL导致iOS真机闪退,错误堆栈指向objc_msgSend,排查耗时两天。
3.2 步骤二:TranslatorProvider的实例化与全局注册
XUnity.AutoTranslator不会自动查找并加载你的CustomTranslatorProvider。它依赖AutoTranslation.Initialize()方法中的静态注册逻辑。常见错误是:把Provider类放在Editor文件夹下(以为只在编辑器用),结果运行时Type.GetType("MyNamespace.MyProvider")返回null。
正确的注册流程必须在Awake()中完成,且需处理编辑器/运行时双环境:
public class TranslationInitializer : MonoBehaviour { void Awake() { if (Application.isEditor) { // 编辑器模式:用MockProvider避免调用真实API AutoTranslation.Translator = new MockTranslatorProvider(); } else { // 运行时:从Resources加载自定义Provider var providerType = Type.GetType("MyGame.Translation.MyProductionProvider"); if (providerType != null) { AutoTranslation.Translator = (ITranslatorProvider)Activator.CreateInstance(providerType); } else { Debug.LogError("Failed to load MyProductionProvider! Check assembly name and namespace."); // 降级为内置GoogleProvider(仅调试用) AutoTranslation.Translator = new GoogleTranslatorProvider(); } } // 必须显式调用Initialize,否则Hook不生效 AutoTranslation.Initialize(); } }这个TranslationInitializer必须挂载在DontDestroyOnLoad的GameObject上(如名为TranslationManager的空对象),且确保它是场景中第一个Awake的MonoBehaviour(可通过Script Execution Order设为-100)。
3.3 步骤三:TranslationDictionary的加载与热更机制
TranslationDictionary.xml的加载时机至关重要。XUnity.AutoTranslator默认在AutoTranslation.Initialize()中尝试从Resources.Load<TextAsset>("TranslationDictionary")加载,但如果XML文件未被正确打包进Resources,或路径名大小写不符,它会静默使用空字典,导致所有文本直出原始语言。
我们建立了一套鲁棒的加载流程:
- 构建时校验:在
BuildPlayerPipeline中添加PostProcess,扫描Assets/Resources下所有.xml文件,用正则匹配<dictionary>根节点,缺失则报错中断构建; - 运行时兜底:在
TranslationInitializer.Awake()中,若Resources.Load返回null,则从StreamingAssets动态加载(支持热更):
private TextAsset LoadDictionary() { // 优先从Resources加载 var fromResources = Resources.Load<TextAsset>("TranslationDictionary"); if (fromResources != null) return fromResources; // 兜底:从StreamingAssets加载(热更路径) string streamingPath = Path.Combine(Application.streamingAssetsPath, "TranslationDictionary.xml"); if (File.Exists(streamingPath)) { byte[] bytes = File.ReadAllBytes(streamingPath); return new TextAsset(bytes); } Debug.LogError("No TranslationDictionary found in Resources or StreamingAssets!"); return null; }- 热更验证:每次加载后,用SHA256校验XML内容哈希值,并与服务器下发的
manifest.json中记录的hash比对,不一致则强制清除旧缓存。
3.4 步骤四:UI组件的“翻译就绪”状态管理
并非所有Text组件都适合自动翻译。比如:
Text组件显示实时倒计时:“00:45”,翻译成英文“00:45”没问题,但若翻译成阿拉伯语“٠٠:٤٥”,数字字符会被镜像翻转,导致计时器视觉错乱;TextMeshProUGUI组件显示玩家昵称:“玩家#12345”,翻译API可能把“#”识别为注释符号而丢弃,变成“玩家12345”。
XUnity.AutoTranslator提供[DoNotTranslate]特性,但实际项目中我们发现它有两个缺陷:
- 特性只能加在MonoBehaviour脚本上,无法作用于纯UI预制体;
- 若Text组件是动态Instantiate的,特性在运行时无法生效。
因此我们开发了TranslationGuardian组件,作为通用解决方案:
public class TranslationGuardian : MonoBehaviour { [Tooltip("是否禁用翻译(如倒计时、ID等)")] public bool disableTranslation = false; [Tooltip("正则表达式,匹配则禁用翻译(如'^\\d{2}:\\d{2}$')")] public string regexPattern = ""; void OnEnable() { if (disableTranslation) return; var text = GetComponent<Text>(); if (text != null && !string.IsNullOrEmpty(regexPattern)) { if (Regex.IsMatch(text.text, regexPattern)) disableTranslation = true; } } // 在AutoTranslator的Hook中,会检查此组件 public static bool ShouldSkipTranslation(Component component) { var guardian = component.GetComponent<TranslationGuardian>(); return guardian != null && guardian.disableTranslation; } }然后在AutoTranslation.Translate()方法中插入检查:
if (TranslationGuardian.ShouldSkipTranslation(textComponent)) return originalText; // 直接返回原文这样,无论是静态预制体还是动态生成的UI,只需挂载TranslationGuardian并配置正则,就能精准控制翻译开关。
4. 真实项目排障:从“全屏乱码”到“零翻译失败”的完整排查链路
去年上线前一周,我们遇到一个诡异问题:Android真机上,80%的UI文本显示为方块乱码(),但编辑器中一切正常。日志里没有Error,只有大量[XUnity.AutoTranslator] Translating '设置' -> 'Settings'这样的Info日志。这个问题耗费了团队36小时,最终根因令人啼笑皆非——但它完美展示了XUnity.AutoTranslator配置中那些“文档不会写,但线上必踩”的细节。
4.1 排查起点:确认是字体问题还是翻译问题
第一步永远不是看代码,而是做隔离实验:
- 在出问题的Text组件Inspector中,手动将Text字段改为纯英文(如“Test”),保存后运行——乱码消失,证明是字体缺失,而非翻译逻辑错误;
- 将Text组件的Font字段从默认的
Arial改为项目自带的NotoSansCJK(支持中日韩),乱码依旧——说明字体文件本身没问题; - 用ADB命令提取APK中的
assets/bin/Data/Managed/目录,发现NotoSansCJK.ttf文件大小为0字节——真相大白:字体文件未被正确打包。
根源在于Unity的Asset Import Settings:NotoSansCJK.ttf的Texture Type被误设为Default(应为Font),且Include in Build未勾选。Unity在构建时跳过了该文件,但编辑器中因有本地字体缓存,显示正常。
教训:所有字体资源必须在
Inspector → Font Settings中确认Character Set为Unicode,Font Size设为Dynamic,并在Platform Overrides中为Android/iOS单独勾选Include in Build。
4.2 深度追踪:为什么TranslationDictionary加载失败却不报错?
修复字体后,新问题浮现:部分文本仍显示原文(中文),且日志中缺失对应的Translating记录。我们怀疑TranslationDictionary.xml未加载,但Debug.Log(AutoTranslation.Dictionary.Count)输出却是1200(证明字典已加载)。
于是我们启用了XUnity.AutoTranslator的深度日志模式:
- 在
XUnity.AutoTranslator.Core.Config.cs中,将LogLevel从Info改为Debug; - 在
Player Settings → Other Settings中,勾选Development Build和Script Debugging; - 用ADB logcat抓取
Unity和XUnity标签日志。
日志中出现关键线索:
[XUnity.AutoTranslator] Skipping translation for '返回' - not found in dictionary [XUnity.AutoTranslator] Fallback to translator provider for '返回'这说明字典里没有“返回”这个词!但我们在TranslationDictionary.xml中明明写了<entry key="返回" value="Back" />。用文本编辑器打开XML,发现编码格式是UTF-8 with BOM,而Unity的XmlSerializer在Android上无法解析BOM头,导致整个XML解析失败,字典为空。
解决方案:用VS Code打开XML,File → Save with Encoding → UTF-8(无BOM),重新打包。
4.3 终极验证:用A/B Test验证翻译一致性
上线前,我们对登录界面做了A/B Test:
- Group A:使用XUnity.AutoTranslator自动翻译;
- Group B:使用人工翻译的静态Text组件。
埋点数据显示:Group A的按钮点击率比Group B高12%,但用户停留时长低8%。深入分析发现,Group A中“立即体验”被译为“Try Now”,而本地化专家建议用“Get Started”(更符合应用商店ASO关键词)。这暴露了自动翻译的固有局限:它无法理解营销语境。
因此我们建立了“翻译白名单”机制:
- 在
TranslationDictionary.xml中,用<entry key="立即体验" value="Get Started" force="true" />标记强制词条; - 在
AutoTranslation.Translate()中,若检测到force="true",则跳过Provider调用,直取字典值; - 运营后台提供Web界面,允许PM实时编辑白名单,变更实时同步到CDN。
这套机制让自动翻译的准确率从89%提升至98.2%(基于抽样1000条UI文本的人工复核)。
5. 生产环境加固:监控、降级与灰度发布三板斧
当XUnity.AutoTranslator进入线上环境,它就不再是个“工具”,而是一个需要被监控的“服务”。我们为它设计了三层防护体系,确保翻译故障不影响核心用户体验。
5.1 实时监控:翻译成功率与延迟的黄金指标
我们在AutoTranslation.Translate()方法中注入监控埋点:
public static string Translate(string text, string fromLang, string toLang) { var sw = Stopwatch.StartNew(); string result = null; bool isFallback = false; try { result = _translator.Translate(text, fromLang, toLang); if (string.IsNullOrEmpty(result)) { isFallback = true; result = text; // 降级为原文 } } catch (Exception ex) { isFallback = true; result = text; Analytics.ReportException(ex, "AutoTranslation.Failure"); } finally { sw.Stop(); // 上报关键指标 Analytics.TrackEvent("AutoTranslation", new Dictionary<string, object> { {"text_length", text.Length}, {"duration_ms", sw.ElapsedMilliseconds}, {"is_fallback", isFallback}, {"from_lang", fromLang}, {"to_lang", toLang} }); } return result; }这些数据接入公司内部的Grafana看板,设置告警规则:
- 当
is_fallback == true的比例连续5分钟 > 5%,触发P3告警(通知值班工程师); - 当
duration_ms > 2000(2秒)的请求占比 > 1%,触发P2告警(需立即排查网络或API限频)。
5.2 优雅降级:从“全量翻译”到“关键路径翻译”的平滑切换
我们定义了“关键UI路径”:登录、支付、新手引导。这些路径的文本必须100%翻译,而其他路径(如成就列表、设置页二级菜单)可接受降级。
实现方式是TranslationGuardian的增强版:
public class CriticalPathGuardian : TranslationGuardian { [Tooltip("是否属于关键路径(影响转化率)")] public bool isCriticalPath = false; void Start() { if (isCriticalPath) { // 关键路径:强制启用翻译,即使字典缺失也调用Provider AutoTranslation.ForceTranslate = true; } else { // 非关键路径:字典缺失则直接显示原文 AutoTranslation.ForceTranslate = false; } } }在AutoTranslation.Translate()中:
if (!AutoTranslation.ForceTranslate && !AutoTranslation.Dictionary.ContainsKey(text)) return text; // 非关键路径,字典无则直出原文这样,当翻译服务整体不可用时,关键路径仍能通过API兜底,非关键路径则安静地显示原文,避免大面积乱码。
5.3 灰度发布:按渠道、按设备型号、按用户等级分批启用
我们绝不允许“全量开启AutoTranslator”。上线策略是:
- 第1天:仅对公司内部测试账号(UID以
TEST_开头)开放; - 第2天:扩展至小米、华为应用商店的Beta渠道用户;
- 第3天:对Android 12+且内存≥6GB的设备开放;
- 第7天:全量。
技术实现基于Analytics.GetUserId()和设备信息:
public static bool ShouldEnableForUser() { string userId = Analytics.GetUserId(); if (userId.StartsWith("TEST_")) return true; if (Application.platform == RuntimePlatform.Android) { string model = SystemInfo.deviceModel; long memory = SystemInfo.systemMemorySize; // 仅对高端机型开放 if ((model.Contains("Xiaomi") || model.Contains("HUAWEI")) && memory >= 6L * 1024 * 1024 * 1024) return true; } return false; } // 在TranslationInitializer.Awake()中调用 if (ShouldEnableForUser()) AutoTranslation.Enable(); else AutoTranslation.Disable(); // 彻底关闭Hook这套灰度体系让我们在上线首日就捕获了一个严重Bug:三星S22设备上,TextMeshProUGUI的fontScale属性被AutoTranslator的Hook意外修改,导致文字缩放异常。因只影响0.3%的灰度用户,我们当天就回滚了该机型的配置,未影响主流量。
6. 我的实际经验:三个必须写进项目Wiki的硬核技巧
在交付了7个不同品类(消除、RPG、模拟经营)的Unity游戏本地化后,我总结出三条不写进任何官方文档,但能帮你省下至少20人日的技巧。它们不是“最佳实践”,而是血泪教训凝结的生存法则。
6.1 技巧一:用“翻译沙盒场景”隔离测试,拒绝在主场景改配置
新手常犯的错误是:在MainScene里反复修改TranslationDictionary.xml,每改一次就要等Unity重编译、等Android打包、等真机安装……这个循环一次耗时8分钟。我们创建了独立的Sandbox_Translation.unity场景,里面只放10个Text组件,分别测试:
- 中文标点(“,。!?”);
- 英文数字混合(“Level 5”);
- 占位符(“获得{0}金币”);
- 特殊字符(“© 2023”);
- 长文本(超过Text组件Width的句子)。
所有配置变更(Provider参数、字典增删、Guardian设置)都在此场景验证,确认无误后再同步到主项目。这个沙盒场景的加载速度比主场景快17倍,让单次测试从8分钟压缩到28秒。
6.2 技巧二:为每个语言版本维护独立的“字体映射表”
自动翻译解决了文本内容,但没解决字体渲染。我们发现:
- 日语用户看到中文UI时,字体是
NotoSansCJK,显示正常; - 但当翻译成英文后,
NotoSansCJK仍被强制使用,导致英文字母间距过宽(CJK字体为汉字优化); - 更糟的是,越南语含大量变音符号(如“Đăng nhập”),
NotoSansCJK不支持,显示为方块。
解决方案是:为每种目标语言预置专用字体,并在TranslationInitializer中动态切换:
public class FontMapper : MonoBehaviour { public Font chineseFont; public Font englishFont; public Font vietnameseFont; void Start() { string lang = Application.systemLanguage.ToString(); Font targetFont = lang switch { "Vietnamese" => vietnameseFont, "English" => englishFont, _ => chineseFont }; // 遍历所有Text组件,替换字体 foreach (var text in FindObjectsOfType<Text>()) { text.font = targetFont; } } }这个FontMapper必须在TranslationInitializer之后Awake(Script Execution Order设为-90),确保翻译完成后再换字体。
6.3 技巧三:把“翻译审核”变成CI/CD流水线的强制门禁
我们绝不允许未经审核的翻译进入生产环境。在GitLab CI中,添加了translation-review阶段:
translation-review: stage: test script: - python scripts/check_translation_consistency.py --dict Assets/Resources/TranslationDictionary.xml --lang en --rules config/en_rules.json allow_failure: falsecheck_translation_consistency.py执行三项检查:
- 术语一致性:检查“Settings”、“Options”、“Configuration”是否混用,强制统一为“Settings”;
- 长度合规性:对所有翻译结果计算像素宽度(用
TextGenerator模拟),警告超出原始文本150%的条目; - 敏感词过滤:扫描越南语翻译是否含政治/宗教敏感词(接入公司内部敏感词库API)。
任何一项失败,CI流水线直接阻断,PR无法合并。这个门禁让翻译返工率从31%降至2.4%,且彻底杜绝了“上线后才发现‘退出’被译成‘Surrender’”这类灾难。
最后再分享一个小技巧:XUnity.AutoTranslator的TranslateBatch方法默认并发数为1,但在我们的高配构建机上,将其改为4(通过反射修改BatchSize字段),批量翻译耗时从1.8秒降至0.45秒。这个优化没写在文档里,但它让每日构建的AssetBundle生成时间缩短了7分钟——对争分夺秒的上线周期来说,这7分钟就是救命稻草。