多语言问题不只是在表里多几列
Godot 项目做到出海或多地区发布时,很多团队会先把翻译表接起来,然后才发现字体才是更棘手的部分。中文、日文、韩文、泰文、阿拉伯文、俄文的字形覆盖、行高、断行、组合字符和阅读方向都不同。即使暂时只做简体中文和英文,也会遇到第一打开公告时卡一下、某些符号变方块、切语言后按钮高度跳动、动态生成的玩家名没有合适字体等问题。
这篇文章关注客户端侧的工程处理:字体回退链怎么配置,什么时候预热字形,如何在不牺牲启动速度的前提下降低首次渲染卡顿,怎么把缺字问题变成可追踪报告。它不是翻译流程文章,但会和翻译表、UI 适配、QA 工具紧密相关。
字体回退链要按语言和用途配置
不要把一个巨大的全字符字体塞给所有 Label。这样虽然缺字少,但内存和纹理图集压力会很高,拉丁文本也可能失去美术想要的风格。更好的方案是按字体用途配置回退链:标题字体、正文字体、数字字体、图标字体分别管理。每个 locale 选择主字体,再追加通用符号和 emoji 或控制图标字体。
Godot 的 Theme 可以统一管理字体资源,但动态语言切换时要注意 Control 树刷新。建议有一个 FontProfile Resource,字段包括 locale_pattern、primary_font、fallback_fonts、base_size_offset、line_spacing_adjust。例如日文可能需要不同的行高,泰文需要更保守的上边距,中文正文可以使用 CJK 字体而数字仍用等宽数字字体。把这些写成数据,UI 团队才能调整。
缺字要报告,不要只在屏幕上变方块
玩家看到方块字通常已经太晚了。开发包里应该在文本渲染前做一次轻量检查:给定字符串和字体回退链,找出无法覆盖的 codepoint,记录文本 key、locale、缺失字符、当前界面路径。Godot 底层能处理字体 fallback,但项目仍需要自己的报告,因为缺字往往来自运营文本、玩家昵称、平台名称和特殊符号。
报告不要每帧刷屏。可以按 key 去重,写到一个 missing_glyph_report.json,或者在调试面板显示最近 N 条。对线上包,也可以采样上报缺字统计,但要注意玩家输入隐私,玩家名或聊天内容不应原文上传,只上传字符类别和 codepoint 范围。这样既能定位字体覆盖缺口,又不会引入隐私风险。
字形预热流程
首次显示一大段新语言文本时,字体可能需要栅格化字形并扩展图集,低端移动设备会出现短暂停顿。下面这个流程把语言切换和字形预热放到预算队列里:
flowchart TD
A["Locale Changed"] --> B["Collect Visible Text Keys"]
B --> C["FontFallback Resolver"]
C --> D["Glyph Warmup Queue"]
D --> E["Budgeted Pre-rasterization"]
E --> F["UI Refresh"]
C --> G["Missing Glyph Report"]
G --> H["Localization QA"]
语言切换时,先收集即将显示的关键文本:主菜单、设置页、常驻 HUD、当前任务、弹窗按钮。把这些字符串交给 FontFallback Resolver,得到需要预热的字体和字符集合。预热队列按每帧预算处理,完成后刷新 UI。对于不在首屏的长文本,比如公告详情,可以在打开弹窗前预热,或者显示轻量加载态。
预热不是把整套 CJK 都塞进图集
CJK 字库动辄几千上万字,完整预热会浪费大量时间和显存。客户端应预热“高概率马上出现”的字符,而不是整个语言。主界面文本、常用数字、标点、货币符号、按钮文案值得预热;长公告、剧情文本可以分段处理。Godot 项目里可以通过翻译 key 分组,例如 boot_critical、hud_common、shop_common,启动时只加载关键组。
还要注意字体图集的生命周期。切到中文后生成了一批字形,再切回英文,如果马上释放,玩家切回中文又会卡;如果永远保留,内存会上升。可以按 locale 保留最近使用的图集,或在低内存事件后清理。移动端尤其需要观察字体纹理内存,不要只看场景资源。
运行时语言切换的细节
很多项目在设置里允许即时切语言。切换时不能只调用 TranslationServer.set_locale(),还要处理字体 profile、布局刷新、文本方向、数字格式、缓存文本、RichTextLabel 的 bbcode 内容。某些 Label 的文本可能是代码拼接的,例如“还剩 3 天”,不同语言的词序不同,应该改成带占位符的翻译 key,而不是中文模板硬拼。
Godot 的 Control 布局会在文本变化后重新计算尺寸,但如果容器里写死了最小宽度,长文本仍会溢出。切语言后建议在开发包执行一次 UI overflow 扫描:遍历可见 Control,检查文本内容的实际尺寸是否超过容器,并输出路径。之前做过伪本地化的项目可以复用这套扫描,但这里重点是字体和字形覆盖。
和运营文本、玩家输入的边界
运营公告和邮件常常包含特殊符号、活动道具图标、外部复制来的空格和不可见字符。RichTextLabel 渲染前应该做规范化:替换不支持的空白、过滤危险标签、限制字号和图片标签。字体系统也要能处理这些文本,至少在缺字时给出降级字体或替代符号。
玩家输入更复杂。昵称可能包含 emoji、罕见汉字、组合字符。是否允许显示,取决于产品规则;客户端不要擅自截断 UTF-8 字节。若服务端允许某些字符但客户端字体不支持,UI 应显示可读占位,而不是空白。聊天、排行榜、好友列表要共用同一套文本清洗和字体回退逻辑,否则一个界面正常,另一个界面方块满屏。
QA 怎么测
字体 QA 不应该靠人工随便点几页。准备一组文本样本:长英文、长中文、日文假名混汉字、韩文、俄文、带重音拉丁、数字货币、emoji、玩家名边界、RTL 样例。即使当前版本不支持某语言,也可以用样本检查缺字报告是否正常。每次换字体或改主题后,跑一遍可见页面截图和缺字扫描。
性能测试要记录语言切换耗时、首次打开公告耗时、字体纹理内存、图集扩展次数。低端机上尤其关注“打开页面第一帧”。如果预热策略有效,第一次打开商店和任务页不应该出现肉眼可见卡顿。不要等翻译全部回来才测,越早用伪数据和样本测,越容易修布局和字体链。
落地建议
先建立 FontProfile 和缺字报告,再做预热。没有报告时,团队会把所有问题归咎于“字体不全”;有了报告,才能知道是某个运营符号、某个 fallback 没挂上,还是 UI 拼接产生了错误字符。Godot 的多语言能力足够支撑复杂项目,但它不会替你决定哪些字符需要提前准备、哪些文本需要降级。把字体当成运行时系统管理,翻译上线那天才不会让第一行文字卡住玩家。
文本 key 分组的实际做法
为了预热字形,需要知道哪些文本马上会出现。可以在翻译表里给 key 加前缀或分组,例如 menu.start、hud.hp、shop.buy_confirm。启动阶段只收集 menu 和通用 common,进入战斗前预热 hud 和技能名,打开商店前预热 shop。这比扫描全表高效,也更符合玩家路径。
如果翻译系统不支持分组,可以维护一个 TextWarmupProfile Resource,列出关键 key。每个场景或 UI 面板声明自己的 profile,打开前交给 FontWarmupQueue。这样内容团队新增文本时也能跟随面板配置,而不是程序硬编码 key 列表。
动态文本的预热
静态翻译 key 之外,还有大量动态文本:数字、玩家名、道具随机词条、服务器活动名。数字和常用单位可以统一预热;玩家名不适合提前预热全量,只能在列表打开前对当前可见项做小批量预热。排行榜一页 50 个名字,直接一次性栅格化可能卡顿,可以先显示骨架或默认字体,分帧预热后再刷新。
道具词条可以按词库预热。装备系统通常有固定属性名和数值格式,把属性名、加号、百分号、数字预热好,随机组合就不会太卡。活动名来自服务器,客户端无法预知,至少要保证缺字报告和 fallback 足够可靠。
字体内存的观测
很多团队只看贴图和模型内存,忽略字体图集。多语言切换几次后,字体纹理可能悄悄增长。开发包可以在字体管理器里记录每个 FontFile 或 FontVariation 的图集数量、尺寸、最近使用时间。Godot 暴露的底层数据有限时,也可以用项目级统计:预热字符数、缺字数、当前 locale、缓存 profile 数量。
内存紧张时,优先清理不在当前 locale 的预热缓存,而不是当前屏幕正在用的字体。清理后要能再生成,不能让 Label 持有已经失效的资源引用。移动端切后台再回来也要测,有些平台会回收图形资源,字体图集需要重新建立。
排版不是字体系统的附属品
字体换了,行高、基线、标点宽度都会变。中文标题在某个按钮里刚好居中,换成日文可能略偏上,换成英文可能太窄。FontProfile 里的 size offset 和 line spacing 只能解决一部分,UI 容器还要允许弹性。不要为了中文截图好看把按钮高度写死到极限。
RichTextLabel 还要注意 bbcode 标签嵌套和字体切换。图标字体、彩色品质名、玩家名混排时,某一段缺字可能让整行高度异常。建议运营富文本只允许有限标签,并在预览工具里按所有支持 locale 渲染截图。Godot 运行时能显示出来,不代表版式是可接受的。
字形缺失的降级策略
缺字时最差是空白,其次是方块。更好的降级是按字符类别处理:常见符号缺失时用近似符号,emoji 缺失时用文本占位或移除,罕见 CJK 字缺失时使用更大的通用 fallback。对玩家名,宁可显示可识别的方块加 codepoint,也不要让名字变成空字符串。
降级也要可观测。每次降级记录 key 和字符类别,避免团队以为 fallback 完全覆盖。正式环境可以只统计数量,不上传原文。运营文本频繁出现某个缺字时,说明字体或文本规范需要调整。
RTL 和复杂排版的准备
即使项目当前不支持阿拉伯语或希伯来语,也要避免把文本系统写死成从左到右。数字、图标、冒号、进度格式都可能受方向影响。Godot 对复杂文本有基础支持,但 UI 组合仍要测试。比如“等级: 10”在 RTL 语言里顺序和标点位置可能需要翻译模板控制。
如果未来支持泰文、阿拉伯文等复杂脚本,字体选择和断行会更重要。不要在代码里用简单字符数截断文本,这会破坏组合字符。使用文本渲染系统提供的测量和截断能力,或者按 grapheme cluster 处理。早期保持这个习惯,后期扩语言不会重写大量 UI。
富文本图片和字体的边界
很多游戏用 RichTextLabel 显示道具图标、货币图标和品质颜色。图标不是字体字形,但会和文字共同行高。切语言后,如果文字行高变化,内嵌图片可能偏上或偏下。可以统一定义 inline icon 的基线偏移和尺寸,随 FontProfile 调整。
运营富文本预览工具要加载真实字体 profile,而不是浏览器默认字体。否则后台预览正常,Godot 客户端里溢出。对经常发公告的项目,给运营一个本地或内部预览页面很有价值,至少能看到缺字、行高和标签错误。
首屏策略
启动阶段不要为了预热所有字体拖慢进主菜单。可以先加载主语言的关键字符,让玩家尽快看到首屏;同时后台预热设置页、账号页、公告入口。玩家首次打开语言选择界面时,再预热候选语言名称和必要按钮。这样既降低卡顿,又不把启动时间浪费在玩家可能不用的语言上。
如果项目有启动 logo 和合规公告,公告文本也要进入 boot critical。很多地区的合规文本包含特殊标点或版权符号,缺字会显得很不专业。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。