ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Godot本地化全攻略:翻译、字体与多语言切换

Godot本地化全攻略:翻译、字体与多语言切换 做游戏这几年我最开始对本地化这件事完全不上心。说白了自己的Demo自己能看懂菜单按钮也就那么几个翻译不翻译似乎影响不大。直到有一次把原型发给海外玩家试玩对方用机器翻译读懂了我界面上的中文按钮却因为剧情文本乱码直接放弃了整段体验。那之后我才认真研究Godot引擎里到底怎么搞翻译才发现这套机制并不复杂甚至比很多商业引擎更顺手。如果你也想为自己的Godot项目加入多语言支持又不想靠硬编码糊弄过去这篇基于实际踩坑写出来的指引应该能帮你省不少时间。1. 定位与整体思路1.1 本地化不等于“把单词换掉”很多人觉得游戏翻译就是把按钮上的“开始”替换成“Start”这种想法只对了一半。真实项目里的文本大概分三类界面标签、剧情对话、动态拼接文案。界面标签最简单剧情对话要考虑换行和排版动态拼接文案则麻烦在语言习惯、复数规则、性别词形这些东西。Godot提供了一个比较完整的本地化管线核心思路是把“原文”和“译文”分离再用一个翻译服务在运行时根据当前语言取回对应文本。我见过不少小团队用最粗暴的字典表方案写一个全局数组然后到处if语言判断。前几十条字符串没问题几百条之后就乱了因为修改文案时要同时改代码和翻译表漏一处就出现半中半英的界面。Godot内置的本地化机制其实就是为了解决这种问题它允许你以“键”作为翻译入口代码里只写键名具体显示什么由语言资源池决定。这样代码和文案完全解耦美术改版、策划调整措辞都不用动引擎脚本。1.2 选型CSV还是Gettext格式Godot支持两种主要的翻译资源格式足以覆盖大多数场景CSV表格和Gettext风格的文件。CSV的优势是能用Excel表格管理适合不熟悉代码的策划和外包翻译Gettext的优势是可以利用现成的翻译记忆工具对版本控制友好适合长期维护。选用哪种格式主要看团队习惯其实运行时效果没有本质差别。我这里建议中小型项目优先使用CSV因为它的结构天然适合“原文、译文”对齐一个工人拿着Excel填写也能操作。如果项目要扩展到几十种语言或者有社区志愿者协作翻译那CSV的冲突会变得频繁建议迁移到Gettext的PO文件配合自动化工具做语言代码检测和复数处理这是后话了。CSV格式在Godot里的传统要求是第一列是翻译键之后的列分别对应不同语言代码语言代码用en、zh_CN、ja这种命名。导出时需要在Project Settings里注册翻译文件然后Godot会给每个字段生成翻译对象。1.3 基于“键值对”的工程习惯无论选哪种格式你都要养成一个习惯界面上的用户可见文本一律不直接写死原文而是写一个键名。比如一个主菜单按钮我不写“开始游戏”而是写MAIN_START手动控制权交给翻译表。这么做看起来很绕但能带来三个好处。第一个好处是修改文案不用碰代码。策划觉得“开始游戏”不够热切要改成“立即开玩”你只需改翻译表不需要去GDScript脚本里寻找字符串。第二个好处是翻译键可以清晰表达上下文比如MENU_START和DIALOGUE_CONFIRM_START翻译者看到键名更容易理解这个文本出现在哪个场景。第三个好处是方便做语言回退Godot在找不到目标语言时会回退到项目默认语言甚至原文键名至少界面不会空白。2. 翻译系统的运行原理2.1 Godot怎么匹配字符串在Godot 4.x版本里本地化核心涉及几个类TranslationServer、Translation、Locale。TranslationServer是全局单例负责管理所有已注册的翻译资源和当前语言Translation是翻译资源抽象CSV和PO文件加载后都会生成对应子类对象。当你调用tr(键名)时Godot做的实际上是这样一件事根据当前活动语言代码去所有已注册的Translation对象里查找键名对应的译文。如果找到就直接返回译文没找到就回到项目设置的locale/fallback语言再查一次再找不到就返回键名本身。这个匹配顺序很关键很多“为什么界面会出现英文键名”的问题其实就是翻译表没注册或者语言代码不匹配。在编辑器里你可以在Project Settings中看到internationalization相关设置其中locale/fallback默认跟随项目主语言。我建议一开始就把回退语言设为你的母语或开发主力语言这样漏翻译时至少玩家看到的是可读的开发语言而不是莫名其妙的键名。2.2 项目设置与本地化文件夹组织我的项目目录习惯是这样的在根目录建一个localization文件夹里面按照语言代码分子目录再放所有翻译文件。比如localization/en.po、localization/zh_CN.po、localization/ja.po。同时把CSV文件或编译后的PO文件统一放到localization/compiled下面避免源文件和引擎产物混在一起。实际配置步骤并不复杂。导出CSV时分类跑一遍工具然后在Project Settings的Localization - Translations中逐个添加.csv或.po文件也可以把整个localization目录拖动到翻译资源列表里。这里有个容易忽略的细节编辑器只识别手动注册过的翻译资源你仅仅把文件放在项目目录里不会自动生效。运行时加载的翻译文件如果不在导出包里也会导致所有语言切换失效。2.3 运行时切换语言的关键代码切换语言不需要重启游戏Godot提供了运行时API。你可以用TranslationServer.set_locale(ja)直接切换也可以用TranslationServer.get_locale()查看当前语言。但要注意set_locale只影响之后tr()的返回值并不会自动刷新界面上已经显示过的所有文本。这意味着你需要在切换完成后对所有可见节点做一次手动刷新。我的做法是做一个全局信号LanguageManager.language_changed所有需要跟随语言切换的UI节点监听这个信号在回调里重新读取键名并设置控件文本。如果是控制台基础节点我一般给Label编写类似refresh()的函数内部调用set_text(tr(MAIN_START))。老手可能觉得这个循环麻烦但一旦漏掉某个节点用户在切换语言后就会看到新旧混合的界面这是很掉价的事。3. 实操把一个Demo界面完整翻译3.1 搭一个最简界面为了演示流程我建了一个最简单的游戏暂停界面上面有标题、开始按钮、设置按钮和一段剧情文本。场景树大概是一个CanvasLayer下挂PanelPanel里放VBoxContainer再往里放三个Label和一个Button。所有控件的文本我都不直接填具体内容而是先填临时占位字符串比如TITLE_PAUSE、BUTTON_START、TEXT_OPENING。这样做的理由是在编辑器里你看到的只是键名但预览时可以依赖Godot的一个功能编辑器顶部选择模拟语言请求tr()时会按照当前模拟语言显示译文。你可以在编辑状态下打开“模拟翻译语言”功能实时预览多种语言下的布局效果。这个功能能帮你提前发现按钮宽度不够、句子过长溢出这类排版问题而不必每次都导出项目。3.2 写翻译文件并绑定这里我选择CSV来做演示因为表格形式最直观。打开Excel或任意文本编辑器建一个两列以上的内容第一列是键名第二列及以后分别是各语言翻译。比如第一行是key,en,zh_CN,ja第二行是TITLE_PAUSE,Pause,暂停,一時停止。其余键名依次摆放。注意CSV的编码问题这是新手踩坑重灾区。中文、日文都建议保存为UTF-8 with BOM否则Godot在加载时可能把中文当乱码尤其当你用Windows记事本编辑时更容易出问题。如果你用Excel可以另存为“CSV UTF-8”也可以手动用VS Code保存时选UTF-8 with BOM。文件保存好后在Project Settings的本地化列表中注册。选中.csv文件Godot会自动识别表头里的语言代码并生成翻译资源。之后你在脚本里调用tr()就能拿到译文。3.3 语言选择器实现接下来我要做一个简单的语言切换菜单让玩家能自由在中文、英文、日文之间切换。实现方式很多我用一个OptionButton下拉框在_ready()里手动填充三种语言名称并把语言代码存在每个item的元数据里。extends Control signal language_changed(locale_code) var locales : { 简体中文: zh_CN, English: en, 日本語: ja, } func _ready() - void: var option : $OptionButton option.clear() for display_name in locales.keys(): option.add_item(display_name) option.set_item_metadata(option.item_count - 1, locales[display_name]) option.item_selected.connect(_on_language_selected) func _on_language_selected(index: int) - void: var option : $OptionButton var code: String option.get_item_metadata(index) TranslationServer.set_locale(code) language_changed.emit(code)然后具体的界面节点监听这个language_changed信号在回调里刷新自己的文本。有些开发者会用自动遍历树的方法刷新所有带自动化翻译属性的节点但那样很耗性能也不太可控。我更推荐显式刷新在_refresh_ui()里写清楚每一个需要变更的控件。界面数量一旦超过二十个这个函数确实有点长但它的可读性和稳定性都远高于“全树扫描法”。3.4 验证导出与打包细节本地化最扎心的问题往往不在编辑器里而在打包之后。你编辑调试时翻译正常一导出到手机或桌面就全部变回键名十有八九是导出预设没包含翻译文件。在Godot的导出预设中资源筛选可以配置“导出所有非资源文件”或直接包含localization文件夹。确保translations相关的.csv、.po、.pot文件都被打进包里。另外要注意自动加载顺序。如果你的启动场景在_ready()里就需要显示本地化文本而翻译资源还没注册完成就会出现第一帧语言不对的情况。稳妥做法是让一个专门的管理节点先加载翻译文件再动态加载主场景。你甚至可以把翻译表格放到远程更新包里让游戏发布后能通过资源更新机制追加语言避免每个语种都重新发布一次。4. 进阶字体、复数、富文本与动态文本4.1 东亚与特殊字体的加载策略很多新手第一次给Godot加中文支持时发现所有中文都变成方框而不是乱码这是字体缺失的典型表现。Godot默认字体不含中文字形需要在项目里打包一个包含所需字形的字体文件然后为控件或主题设置字体。这里有一个容易忽略的点字体文件本身是二进制资源它不会因为你添加了翻译文件就自动出现所以无论如何都要把字体文件一并导出。一旦涉及多语言字体管理就有一点讲究。中文、日文、韩文共用的字形比例很高但完全共用一个字体文件也可能导致某些特殊字符缺字。更推荐的方法是做字体回退链比如主字体用基础的拉丁字符集回退字体中文字库再回退日文字库。Godot的字体配置里可以添加多个fallback字体让一个字找不到字形时自动去下一个字体里找。这个能力对东亚三语言的支持尤其友好否则你可能要为每种语言单独做一套字体方案。4.2 复数与Context用法英文语法里有单数和复数区分比如“1 item remaining”和“5 items remaining”但中文通常没有这种词形变化。如果你在代码里拼接str(count) item(s) remaining英文玩家看到的就是不专业的界面。Godot的Gettext插件支持复数形式定义翻译条目时可以根据数量选择不同译文。# 伪代码示意实际翻译工具里设置复数规则 var n items_remaining var text tr_n(ITEM_REMAINING, ITEMS_REMAINING, n, n)这里tr_n会根据数量n在单数和复数两条编译中选择合适的一条。但对CSV格式来说复数处理并不直接除非你在工具链里加入Gettext模式。所以我更建议从项目一开始就明确文本的格式规范所有需要复数的句子不走CSV而走PO格式这样英文复数规则可以被完整保留。Context的概念也值得了解。同一个英文词可能在不同界面里有不同译法比如“Start”在菜单里是“开始”在赛事场景里是“起跑”。Gettext可以用上下文区分Godot的tr()也支持带上下文的变体。如果你发现某条词在不同场景意思不同不要把它当成同一个翻译键来用而是给键名加上前缀这是最朴素但最有效的做法。4.3 RichTextLabel与动态字符串的处理富文本标签适合显示带格式的对话、任务提示和带有不同颜色的说明。Godot的RichTextLabel默认有自己的BBCode解析机制而tr()返回的字符串如果包含BBCode标签需要额外小心。最简单的办法是只翻译纯文本内容格式标签保留在代码侧例如p[colorred] tr(ALERT_TEXT) [/color]/p。这样翻译者不会误删标签渲染端也能稳定解析。动态字符串的坑出现在拼接顺序。中文、英文、日文的语序并不一致比如“你获得了3个金币”和“You obtained 3 gold coins”如果代码里硬拼接tr(GAINED) str(amount) tr(GOLD_COIN)翻译者根本没法调整语序只能固定成一种很僵硬的翻译。推荐用占位符语法例如tr(GAINED_ITEMS) % {amount: amount, item: tr(ITEM_GOLD)}翻译者可以把占位符放到句子的任何地方语序就自由了。4.4 RTL语言和换行问题如果你想支持阿拉伯语或希伯来语事情会复杂一点。这些是RTL从右到左书写的语言Godot对RTL的支持从4.x开始有所增强但依然需要你手动测试。你要为Label、RichTextLabel、LineEdit等控件考虑对齐属性覆盖默认的左对齐。字体也需要包含阿拉伯文所需的连字和变体字形否则文字显示会非常奇怪。换行是另一个容易忽略的点。英文一个单词很长中文一个汉字一个字符日本语的行尾规则又不同。如果你把界面按钮宽度固定为200像素几种语言可能会呈现完全不同的视觉效果。我习惯用一个“本地化走查表”在编辑器的模拟语言功能下逐一截图对比所有界面。发现文本截断就把控件宽度属性改大或者改成长文本可换行模式。语言切换后还可能出现控件高度不统一的问题使用容器并开启自动最小高度可以缓解。5. 常见问题与排查经验5.1 常见故障速查表从做本地化至今我累积了一些高频问题整理成一个速查表适合在遇到问题时先对照检查。现象可能原因解决办法界面显示键名而不是译文翻译文件未注册或未打包检查Project Settings中Translations列表检查导出预设翻译有部分生效部分不生效语言代码或键名不一致用TranslationServer.get_locale()确认当前语言对照CSV表头中文显示为方块字体缺少字形添加包含中文字形的字体并设置fallback切换语言后界面不更新节点文本没有重新刷新监听语言切换信号显式调用set_text(tr(...))导出后翻译丢失文件没被打进包在导出预设中勾选翻译文件夹或按文档配置Remote文本过长被截断控件尺寸不足使用autowrap_mode开启自动换行调整容器尺寸CSV中文乱码文件编码不对统一保存为UTF-8 with BOM5.2 我踩过的本地化坑最让我难忘的一次是项目里嵌入了大量武器名称我直接用原文做键名比如Sword、Bow。中文翻译表里把它们映射成“剑”“弓”一切正常。后来策划新增了道具英文原名叫Sword of Flame我在翻译表里补了一行但没注意英文列写错了字符结果游戏里所有拿到这个道具的玩家都看到裸键名。从那以后我强制要求所有键名使用大写加下划线格式类似ITEM_SWORD_OF_FLAME不跟原文内容挂钩出问题至少知道是配置问题还是翻译问题。第二个坑是中文和日文的字体体积很大。一次把所有语言字体都打进游戏包里安装包一下子多了几十兆对移动端玩家不友好。我现在会把字体拆分成基础拉丁字符和小型东亚子集必要时动态加载字体资源。一个干净的界面加载速度也能快不少。第三个坑是动态语言的文本长度影响了整体UI布局。我最初给对话文本框固定了高度中文版本里一句话只占两行英文版本却占了三行多出现滚动条之后观感很差。后来我给对话区域设置了最小高度并允许在长文本场景下自动扩展彻底解决了右下角文本溢出问题。5.3 怎么养成可持续的翻译工作流本地化不是发版前一个月的事而是项目全生命周期的事。我建议在仓库里维护一个翻译状态文档记录哪些语言已完成、剩余多少未翻译键、谁负责语言校验。每周抽半小时跑一遍本地化检查脚本把缺少译文的目标语言清单列出来比上线前临时翻几百条文本要轻松得多。我还会给游戏接一个外部翻译管理服务或者至少在CI流程里做自动化校验检查所有ID在两种语言下的键是否一一对应防止有人因为合并了旧文件导致键漂移。如果一个键在源代码里已经删除翻译表里还留着这类脏数据虽然不影响运行却会让翻译费用和文本维护成本上升。定期清理这些废弃条目长期看能省掉大量沟通成本。对于兼职翻译的外包我通常会给他们一份上下文说明逐条注释键名出现的场景。最好在项目的本地化预览工具里能看到截图这样翻译者不会把“plant”翻成植物而实际上那是工厂里的“设备”。流程上虽然多一步但翻译质量和沟通效率都会明显提升。最后再分享一个我自己的使用习惯每次提交翻译相关改动前我都会在编辑器里用模拟语言跑一遍核心流程把每个界面的文字布局截图存档。这样回看版本变化时能快速定位是哪次改动把某个按钮撑破的。无论你做的游戏是商业发行还是兴趣分享把本地化当成一等公民来对待出海时才会更从容。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进