ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

场景级一键翻译:Unity/Unreal本地化文本提取与写回实战

场景级一键翻译:Unity/Unreal本地化文本提取与写回实战 做游戏本地化的人基本都懂一句话把一款游戏翻译成多语言最耗时间的往往不是“翻译”本身而是“把翻译好的文本放回游戏里”。这句话听着有点反直觉但如果你在 Unity 或 Unreal 引擎里逐场景找过 UI 文字、对话、道具名再对照本地化表一条一条回填你一定会点头。传统流程下策划先整理文本外包翻译再导回引擎技术再挨个排查漏翻、错位、UI 溢出。一个几十万词的 RPG 项目本地化周期奔着几个月去。现在 AI 翻译服务的质量已经过了“能看”的线团队反而容易掉进另一个坑模型翻译很快可文本提取和写回仍然靠手工翻译完导回引擎后要么 Key 对不上要么长文本把按钮撑爆。于是“整个游戏场景也能一键翻译导回引擎直接用”就成了一个很值得认真讨论的问题。它要处理的不是“加一个翻译按钮”而是一条完整的本地化管线场景内文本提取、翻译服务接入、译文写回、引擎内即时预览。这篇文章不绑定任何现成工具而是把这条管线的原理、落地步骤和常见的坑都讲清楚让你在自己的 Unity / Unreal 项目里也能搭出一套而不是被“一键”两个字误导。1. 场景本地化的真正瓶颈不在“翻译”在“搬运”很多人第一次听到“场景一键翻译”第一反应是“用 AI 翻译不就行了吗”。如果只做到这一步那你十有八九会在导回引擎后崩溃。原因很简单游戏场景里的文本不是集中存放在某个文件里的而是散落在各种组件、资源、事件、数据表中的。以一个 Unity 场景为例。屏幕上显示的文本可能来自场景里的 UI Text、TextMeshPro 文本也可能来自动画事件、Timeline 字幕、挂载的 ScriptableObject 数据、Tile 名称甚至敌人的血条名字是运行时拼接的字符串。你只翻译其中一部分玩家打开游戏就会看到中英混杂的界面。Unreal 里的情况也类似。虽然 Unreal 有官方的 Localization Dashboard 和 FText 体系理论上所有 UI 文本都应该走 FText 并进入收集流程但实际项目里总有绕过这套体系写死的字符串。更麻烦的是FText 的收集、翻译、导入是基于编辑器工具的你要先把所有场景文本“收集”出来翻译完再“导回”中间任何一步出错都会造成漏翻或错位。所以我把本地化的核心矛盾总结成一句话翻译是“内容替换”而本地化是“信息搬运”。这两件事的复杂度完全不在一个量级。AI 可以把“你好”翻译成“Hello”但它不知道这行文本在哪个场景、哪个按钮上、原文里有没有占位符、译文会不会长到把 UI 顶破。要解决这些问题不能只靠模型要靠工程。这也是我在这篇文章里反复强调的判断游戏场景一键翻译真正的技术含量不在翻译引擎而在文本提取与写回管线的稳定性。2. 一句话理解“场景一键翻译”的原理不管底层是用 Unity、Unreal 还是自研引擎场景一键翻译的原理都可以拆成三层场景层所有可见文本所在的位置如 UI 组件、动画事件、数据资产、脚本字段。映射层一个稳定的 Key 对应一条原文。这个 Key 在场景、翻译文件、引擎资源之间保持唯一。翻译层把原文批量发送给翻译服务或人工译者得到译文后再按 Key 写回。如果你去看市面上各种本地化插件本质上都是在解决这三层之间的数据流转。区别只在于提取能力强不强能不能把所有可能藏文本的角落都扫到Key 稳定不稳定场景随便改个名字后译文还能不能对齐写回方式安不安全是直接改场景文件还是通过运行时本地化表替换。这里特别要展开的是 Key 的设计。很多初学本地化的人会直接用组件 InstanceID 当 Key这是最省事的做法但也是最不稳的。因为 InstanceID 是 Unity 运行时给对象的临时编号场景一旦重新打开就变了。翻译文件里存着旧 ID重新导回时全部错位。Key 生成方式稳定性对开发流程的要求适用场景GameObject 名称一般重名会冲突要求场景内命名规范小项目、原型验证对象路径 组件类型较高改名/移动会失效要求保持场景结构稳定中大型项目兜底人工维护语义 Key最高结构变化不受影响需要在开发阶段给文本配置 ID需要长期维护的正式项目InstanceID / GetInstanceID低编辑会话结束就变无自动生成只适合一次性工具脚本从工程角度看语义 Key 最可靠比如Level1_Town.Forgemaster_Intro.001。但语义 Key 需要开发时维护不能自动生成。比较实用的组合是优先使用项目里已有的本地化组件 Key没有 Key 时用“场景名 对象路径 组件类型”兜底。这样自动化程度和稳定性都能兼顾。3. 完整管线拆解从“场景里拿出所有文本”到“引擎内直接用”把一键翻译理解成八个步骤你就不会把它当成一个黑盒。收集遍历当前场景和引用的资源找到所有需要翻译的文本。去噪排除空字符串、纯数字、调试输出、技术标识符等不需要翻译的内容。生成 Key给每一条文本分配稳定 ID并记录它所在的场景路径和组件位置。导出输出为 JSON、CSV 等中间格式便于翻译服务或人工译者处理。翻译批量调用 AI 或翻译 API拿到译文。校验检查是否漏翻、占位符丢失、译文长度是否异常、编码是否损坏。写回把译文映射到引擎内对应的文本组件或本地化表。预览在编辑器里切换语言逐场景查看效果修复溢出和字体问题。这八个步骤里第 1 步和第 7 步最容易被低估。因为绝大多数文本困难和翻译事故都不是翻译模型造成的而是“该拿到的文本没拿到”“该写回的位置没写回”。我见过一个项目第一版翻译工具只处理了 UI Text没有处理 TextMeshPro。结果导回引擎后主界面 80% 的文本都切换过来了但所有伤害数字、弹幕文字、排行榜条目还是中文。玩家截图吐槽团队排查了半天才发现是组件类型漏了。所以你在设计提取器时第一原则是“宁可多抓不可漏抓”。多抓的文本可以在翻译校验时过滤掉漏抓的文本只能发版后由玩家告诉你。4. Unity 实战提取场景文本的编辑器脚本先讲 Unity 的落地方式。Unity 的 UI 系统有两种主流文本组件UnityEngine.UI.Text 和 TextMeshPro 的 TMP_Text。Text 比较老TMP 在新项目里更常用。下面这个编辑器脚本只提取 UGUI 的 Text核心逻辑是通用的你可以照着扩展 TMP。在 Editor 文件夹下新建文件SceneTextExtractor.csusing System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine; using UnityEngine.UI; public static class SceneTextExtractor { private const string ExportDir Localization/Exports; [MenuItem(Tools/Localization/Extract Current Scene)] public static void ExtractCurrentScene() { string scenePath UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().path; string sceneName Path.GetFileNameWithoutExtension(scenePath); var entries new ListTextEntry(); int index 0; // 注意Unity 2023 之后推荐使用 FindObjectsByTypeText(FindObjectsSortMode.None) // 老版本用 FindObjectsOfTypeText(true) 即可这里为了兼容给出最保守写法。 var texts Object.FindObjectsOfTypeText(true); foreach (var text in texts) { if (string.IsNullOrEmpty(text.text)) continue; string path GetTransformPath(text.transform); string key ${sceneName}|{path}|{text.GetType().FullName}; entries.Add(new TextEntry { key key, scenePath scenePath, source text.text }); index; } if (!Directory.Exists(ExportDir)) { Directory.CreateDirectory(ExportDir); } string outPath Path.Combine(ExportDir, ${sceneName}_texts.json); var wrapper new TextEntryWrapper { entries entries }; string json JsonUtility.ToJson(wrapper, true); File.WriteAllText(outPath, json, new System.Text.UTF8Encoding(true)); Debug.Log($[SceneTextExtractor] 提取 {entries.Count} 条文本 - {outPath}); } private static string GetTransformPath(Transform current) { if (current.parent null) { return current.name; } return GetTransformPath(current.parent) / current.name; } [System.Serializable] public class TextEntry { public string key; public string scenePath; public string source; } [System.Serializable] public class TextEntryWrapper { public ListTextEntry entries new ListTextEntry(); } }这段代码做的事情很直观打开场景点击菜单Tools/Localization/Extract Current Scene它会遍历当前场景里所有UnityEngine.UI.Text组件把非空文本收集起来生成一个 JSON 文件。这里真正容易踩坑的地方是 Key 的生成。我用了“场景名 对象路径 组件类型”的组合比 InstanceID 稳定很多。但它也有局限如果你在场景里移动了对象Key 会变化译文就会对不上。所以在正式项目里我建议引入一个LocalizationKey组件允许策划在界面上手动指定一个稳定 Key提取时优先读人工 Key读不到再用路径兜底。TextMeshPro 的提取逻辑几乎一样只要把遍历类型换成TMP_Text再把组件的 FullName 区分开即可。TMP 的文本在代码里是text属性和 UGUI 一致处理起来并不复杂。如果你项目里装了 TMP可以复制一套逻辑不要只处理 UI Text。5. 翻译服务接入批量调用与格式约定文本提取出来之后会得到一个 JSON 文件。接下来需要把其中的source字段批量翻译成目标语言。这里我以 Python 脚本为例因为它最适合做离线批处理。你可以把这一步接到任意翻译服务上无论是商业翻译 API 还是自部署的大模型接口只要支持 HTTP POST JSON 就行。代码里的 endpoint、鉴权字段都是占位符以你实际使用的服务文档为准。import json import os import requests import time EXPORT_FILE Localization/Exports/Level1_Town_texts.json OUTPUT_FILE Localization/Imports/Level1_Town_en.json TARGET_LANG en # 从环境变量读取密钥避免把敏感信息提交到 Git API_ENDPOINT os.environ.get(TRANSLATE_ENDPOINT, https://your-translation-service/translate) API_KEY os.environ.get(TRANSLATE_API_KEY, ) def load_entries(path): with open(path, r, encodingutf-8-sig) as f: data json.load(f) return data[entries] def call_translate(texts): headers { Content-Type: application/json, Authorization: fBearer {API_KEY} } payload { texts: texts, target_lang: TARGET_LANG } resp requests.post(API_ENDPOINT, jsonpayload, headersheaders, timeout60) resp.raise_for_status() return resp.json()[translations] def main(): entries load_entries(EXPORT_FILE) texts [e[source] for e in entries] print(f待翻译文本: {len(texts)} 条) # 分片调用避免单次请求体过大 batch_size 50 translations [] for i in range(0, len(texts), batch_size): batch texts[i:i batch_size] result call_translate(batch) translations.extend(result) time.sleep(0.2) # 温和限流避免触发服务方限制 if len(translations) ! len(entries): raise RuntimeError(f翻译结果数量 {len(translations)} 与输入数量 {len(entries)} 不一致) output_entries [] for entry, translated in zip(entries, translations): output_entries.append({ key: entry[key], scenePath: entry[scenePath], source: entry[source], translation: translated }) os.makedirs(os.path.dirname(OUTPUT_FILE), exist_okTrue) with open(OUTPUT_FILE, w, encodingutf-8) as f: json.dump({entries: output_entries}, f, ensure_asciiFalse, indent2) print(f翻译完成已输出: {OUTPUT_FILE}) if __name__ __main__: main()这个脚本之所以要和引擎流程分开是因为翻译服务调用往往涉及网络请求、重试、限流、频率控制放在 C# Editor 脚本里也能写但不如 Python 方便调试。尤其是团队里有策划参与本地化时一个独立的 Python 脚本比 “在 Unity 里配置 API Key” 更直觉。翻译结果的格式约定也很重要。我建议每个字段都要保留key和scenePath这是写回时定位组件的依据。如果你只导出“原文→译文”的映射忽略 Key 和场景路径那这个文件基本只能给人类看没法安全地写回引擎。另一个要强调的是编码问题。翻译文件必须统一使用 UTF-8。Windows 环境下Excel 打开 CSV 再保存很容易改成带 BOM 的 UTF-8 或 GBK如果你用 Python 读取 JSON最好在open时指定encodingutf-8-sig这样能兼容带 BOM 和不带 BOM 两种文件。6. 导回引擎译文写回与本地化资源组织翻译完成后最关键的是安全地写回。在 Unity 里写回有两种主流思路。第一种是编辑器直接改场景里每个 Text 组件。这种方式最直接但风险也最高尤其不适合频繁重新导出的开发流程。因为你每次改动都需要重新切换语言、重新导回会把本地化流程拖得很慢。第二种是把译文做成运行时字符串表通过一个本地化组件在运行时替换文本。这种方式不污染场景源文件回滚容易也符合 Unity Localization、Unreal Localization 这类系统的设计思路。为了让你看得更清楚这里给出第一种方式的编辑器写回脚本。它的逻辑很朴素读取翻译文件按照 Key 在当前场景里找到对应的 Text 组件替换文本。在 Editor 文件夹下新建SceneTextWriter.csusing System.Collections.Generic; using System.IO; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.UI; public static class SceneTextWriter { private const string ImportDir Localization/Imports; [MenuItem(Tools/Localization/Apply Translations To Current Scene)] public static void ApplyTranslations() { string scenePath EditorSceneManager.GetActiveScene().path; string sceneName Path.GetFileNameWithoutExtension(scenePath); string filePath Path.Combine(ImportDir, ${sceneName}_en.json); if (!File.Exists(filePath)) { Debug.LogError($[SceneTextWriter] 未找到译文文件: {filePath}); return; } string json File.ReadAllText(filePath, new System.Text.UTF8Encoding(true)); var wrapper JsonUtility.FromJsonTranslationWrapper(json); if (wrapper null || wrapper.entries null || wrapper.entries.Count 0) { Debug.LogError([SceneTextWriter] 译文文件格式不正确或内容为空); return; } // 构建 key - translation 映射 var map new Dictionarystring, string(); foreach (var entry in wrapper.entries) { map[entry.key] entry.translation; } var texts Object.FindObjectsOfTypeText(true); int appliedCount 0; foreach (var text in texts) { string path GetTransformPath(text.transform); string key ${sceneName}|{path}|{text.GetType().FullName}; if (map.TryGetValue(key, out string translated)) { text.text translated; appliedCount; } } EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene()); Debug.Log($[SceneTextWriter] 已应用 {appliedCount} 条译文请检查场景并保存); } private static string GetTransformPath(Transform current) { if (current.parent null) { return current.name; } return GetTransformPath(current.parent) / current.name; } [System.Serializable] public class TranslationEntry { public string key; public string scenePath; public string source; public string translation; } [System.Serializable] public class TranslationWrapper { public ListTranslationEntry entries new ListTranslationEntry(); } }如果你用的是 TextMeshPro需要把Object.FindObjectsOfTypeText(true)换成Object.FindObjectsOfTypeTMP_Text(true)。但注意如果项目没有引入 TMP 包这段代码会编译报错。所以我建议在实际项目中用#if预处理指令把两种组件的处理逻辑分开或者把 TMP 相关的代码单独放到带宏的脚本里。Unreal 的“导回”思路与 Unity 不同。Unreal 官方推荐的是 Localization Dashboard它的核心步骤是 Gather、Translate、Export/Import。Gather让编辑器扫描工程和场景中所有 FText生成翻译源文件。Translate在 Dashboard 里逐条翻译或导出 Portable Object.po文件给外部翻译。Import把翻译后的文件导回 Dashboard。Preview在编辑器里切换 Preview Language查看场景中的语言效果。因此在 Unreal 项目里做“场景一键翻译”更稳妥的路径不是自己写一个遍历场景、直接修改 FText 的插件而是利用 Localization Dashboard 的收集能力导出文件再用 AI 批量翻译后导入。理论上你也可以通过反射遍历场景里的 FText 属性来自动提取但官方管线已经解决了收集和写回的问题开发者把精力花在翻译质量和流程校验上更划算。Localization Dashboard 推荐工作流 1. 配置要收集的目标场景/关卡和插件目录 2. 点击 Gather Text 收集所有 FText 3. 导出 .po 文件或 JSON 4. 用翻译脚本批量翻译 5. 导入翻译结果 6. Preview Language 实时检查场景这不是偷懒而是“别重复造引擎已经造好的轮子”。Unreal 的 FText 和 Localization Dashboard 本来就是为这个问题设计的你只需要把翻译环节替换成 AI 批量翻译。7. 运行验证如何确认场景翻译没漏、没崩写完工具后最难的不是“跑通”而是“确认真的翻译完整了”。我推荐从四个维度做自动化校验。第一是数量校验。提取阶段如果有 1200 条文本翻译返回时也必须返回 1200 条。任何数量不一致都说明提取过程或翻译过程有遗漏。不要只看日志里“写回成功 1200 条”要对比提取时的原始数量。第二是空白文本校验。有些翻译服务会把包含占位符或特殊符号的文本当成“无需翻译”返回空字符串。如果你的脚本没有校验空字符串会直接覆盖原文。第三是长度校验。英文译成中文、中文译成英文长度比例经常超过 200%。如果一个按钮的原文是“确定”只有两个字译文变成“Are you sure you want to continue”UI 必然会溢出。这里可以用一个简单的 Python 脚本扫出可疑内容import json import sys def check_length(input_path, max_ratio1.5): with open(input_path, r, encodingutf-8-sig) as f: data json.load(f) for e in data[entries]: source e.get(source, ) translation e.get(translation, ) if len(source) 0: continue ratio len(translation) / len(source) if ratio max_ratio: print(f[LENGTH_WARN] {e[key]} 原文长度为 {len(source)}译文长度为 {len(translation)}比值 {ratio:.2f}) check_length(Localization/Imports/Level1_Town_en.json)第四是占位符校验。游戏文本里经常包含{0}、{playerName}、%s这类占位符。AI 翻译有时会改写成{零}或直接删掉运行时轻则显示混乱重则直接崩溃。校验脚本里可以加一段正则提取占位符对比原文和译文中包含的占位符集合是否一致。引擎内的预览验证同样重要。Unity 项目里如果使用运行时字符串表的方式可以加一个类似 “Language English” 的启动参数或配置项快速切语言。Unreal 打开 Localization Dashboard 的 Preview Language 后能直接在关卡视口中切换语言预览这是最直观的确认方式。真正发版前还应该做一次完整流程走查把每个主要场景打开快速扫一遍界面和对话重点看三样东西有没有漏翻、有没有 UI 溢出、有没有字体缺字。8. 常见问题与排查清单本地化管线的问题往往不是单个点引起的。下面这张表总结了实际项目里出现频率最高的几个状况。问题现象可能原因排查方式解决方案部分文本没有切换语言提取器只处理了 UGUI Text没有处理 TMP 或动画事件文本检查导出 JSON 中是否包含对应文本扩展提取器覆盖 TMP、Timeline、ScriptableObject 等文本来源翻译后 Key 对不上文本串位用了 InstanceID 或场景对象名作为 Key场景结构调整后 Key 变化导出两份 JSON对比 Key 差异改用对象路径 组件类型正式项目引入人工语义 Key中文字体正常英文或日文变成方块项目缺少目标语言字体或字体动态子集在编辑器切语言查看字体渲染接入动态字体或为多语言配置字体 fallback译文长度导致按钮溢出翻译没做长度校验UI 自适应关闭用长度校验脚本扫描异常比值开启 Overflow 处理、根据语言调整字号或对超长文本走多行副本CSV/JSON 导入后出现乱码文件编码不是 UTF-8或 Excel 二次保存后变成 GBK用编辑器打开文件看编码统一使用 UTF-8 with BOM避免 Excel 直接编辑工程文件写回后原文本被空字符串覆盖翻译服务对特殊文本返回空白做空白校验拒绝写入空译文在写回脚本里增加空字符串检查和日志告警Unreal 收集不到场景文本文本写在硬编码字符串中没有使用 FText检查本地化收集日志把硬编码字符串改为 FText并把相关模块加入收集范围脚本调用翻译接口超时一次性提交大量文本或网络代理问题查看翻译服务端日志和响应时长分片提交增加超时时间和重试机制这里特别强调一下“硬编码字符串”的问题。很多团队喜欢把临时文案直接写在代码里觉得省事。这些文本不进提取器不参与翻译也不会出现在本地化 Dashboard 中。对于小项目这可能是效率选择对于要做多语言发行的项目这是后期最大的技术债。你可以在工程里加几条规范所有给玩家看的字符串必须走本地化组件或资源代码里只保留日志和技术文案。9. 工程化建议让“一键翻译”在真实项目里可用把演示脚本跑通只是第一步。想要在正式项目里每天用、每周用还需要做工程化改造。我把最重要的几条建议列出来。9.1 文本尽量集中不要散落能放到配置表、DataTable、ScriptableObject 里的文本就不要散落在场景组件上。集中的文本天然好提取、好翻译、好写回。场景里只保留 UI 布局文本内容通过引用键获取这是长期一劳永逸的做法。9.2 Key 是本地化的生命线Key 的命名建议用“模块 关卡 简短语义”的方式比如Level1_BossIntro.001。Key 一旦生成尽量不要改动。如果必须改要通过工具批量重写翻译文件而不是手动在 CSV 里改。把 Key 的变动当成代码重构来对待每次变动都要 diff 和 review。9.3 翻译过程要支持“上下文注入”纯文本翻译很容易出现“同一个英文单词在不同的 UI 位置该翻成不同中文”的情况。AAI 和普通翻译 API 只给你一句话不会告诉你这句话是按钮、弹窗还是 NPC 台词。好的做法是在导出给翻译服务的 JSON 里增加context字段比如context: UI_Button或context: NPC_Dialog让翻译模型能区分语境。9.4 自动化要接入 CI而不是依赖人工点按钮每次场景改动后让 CI 自动跑一次文本提取对比上次导出结果把新增文本和变更文本单独生成翻译任务。这样团队不需要每周手动导一次漏翻概率也会明显下降。翻译结果合入后CI 再跑一轮校验阻止数量不一致或空译文进入主线。9.5 安全与合规要提前设计调用在线翻译服务等于会把游戏文案发到外部接口。所以大规模接入前要确认内容安全性避免把玩家输入的内容、隐私信息、未发布的剧情敏感内容直接发给第三方。API Key 不要硬编码进工程优先放环境变量或服务器端代理。如果游戏面向未成年人还要考虑文本输出和译文的合规审查。9.6 留好回滚路径写回前先备份无论是 Unity 编辑器直接改场景还是 Unreal 导入翻译文件都应该提前确认改动可回滚。Unity 的.unity文件本身是文本格式可以被版本控制但如果你在未提交的状态下直接大范围写回出问题时恢复会很痛苦。推荐做法是写回前先确认工作区干净或者让工具自动生成一个备份文件。10. 结尾别把“一键”当成魔法按钮回到标题的问题整个游戏场景也能一键翻译导回引擎直接用吗从能力上看能。只要把提取、翻译、写回这条管线打通再配合引擎的本地化预览一次处理几千条场景文本已经不是问题。但从工程上看真正决定这套流程好不好用的不是那一下点击而是你花了多少精力处理文本来源、Key 稳定性和校验。这套方案的性价比非常清晰。对于独立开发者和中小团队几天就能搭出最小可用版本一个提取脚本一个翻译脚本一个写回脚本再加一个长度校验工具就能把原本几周的本地化周期压缩到几小时。对于大型项目这套思路仍然适用只是需要更强的人工 LQA 流程和上下文管理。如果你正在准备做多语言版本我的建议是不要直接去找“哪个翻译工具最好”而是先盘点你的场景里到底有多少种文本来源再决定用引擎原生本地化方案还是自研管线。想清楚这几件事“一键翻译”才不会变成“一键翻车”。建议收藏这篇文章等真正搭管线的时候对照着踩坑清单来做。
返回列表