下载 App
10/161 浏览案例分享
【案例分享】星火 i18n 本地化实录:漏翻会把 t_1710742800_a3f2b1c9 印在屏幕上,切语言要重启
投稿方向:星火编辑器论坛「案例分享」专区 / 生态贡献度
类型:图文案例
素材来源:engine-docs/recipes/i18n-translation.md、recipes/i18n-resource.md,对照 urhox-libs/Engine/i18n.lua(1724 行)与 .project/i18n.json 逐条核对
一句话结论
这套 i18n 的提取、原文替换、兜底策略全在构建期完成,运行时只剩「查表」和「重启」两件事——漏翻的条目不会显示成空白,而是把 t_1710742800_a3f2b1c9 这样的 hash key 直接画在屏幕上;切语言也不是刷新界面,是弹框确认后重启进程。
文档把「怎么开」写得很全,没写「开坏了长什么样」。我把 i18n.lua 读完,把几个静默失败点记下来。
一、.project/i18n.json:十来个字段,几个能定生死
字段:我填的值 作用
enabled:true 关着就跳过整个 i18n 构建步骤
source_lang:"zh_CN" 源语言,也是兜底语言
target_langs:["en"] 目标语言数组,至少 1 项
file_exts:[".lua", ".json", ".xml"] 扫描范围
extract_rules:4 条正则 逐行匹配提取,默认只抓含汉字的字符串
function_excludes:26 条 按函数名排除
content_excludes:8 条 按内容排除:URL、路径、带扩展名的串、日志 tag
code.deferred_tr:不写(默认 false) 见坑 5
untranslated.fallback:"source" 默认是 "key",见坑 4
lang_to_display:{"zh_CN": "简体中文", "en": "English"} 语言选择 UI 的显示名
extract_rules 我只留了 4 条:双引号中文串、单引号中文串、XML 文本节点、XML 属性值。抓多了全是误伤。
二、i18n_extract 怎么用,以及它管不着什么
它是 MCP 工具,不是 Lua 函数。流程五步:
1. 确认 .project/i18n.json 里 enabled: true,target_langs 有目标语言
2. 调 i18n_extract,扫项目产出 i18n/{lang}.pending.json
3. 编辑 pending.json——原文当 key,译文填 value,跳过的留 ""
4. 再调一次 i18n_extract(或者直接 build),已填的合并进 i18n/{lang}.json
5. build 时源码里的原文被换成 t_xxx,同时生成运行时翻译表
翻译进度怎么判断?打开 {lang}.pending.json,有内容就是还有没翻的,空 {} 就是翻完了。
限制一:i18n 这张表上根本没有提取入口。i18n.lua 暴露的是 AddTranslations / GetLanguage / SetLanguage / RequestChangeLanguage / GetSupportLanguages / GetLanguageDisplay / IsActive / GetSourceTextForDisplay。想在游戏脚本里「跑一下重新提取」做不到。
限制二:enabled: false 时整条流水线跳过,调了也没产出。
限制三:合并是单向的。pending.json 填完并进去,改已有译文得直接编辑 i18n/{lang}.json,grep 原文定位。文件太大就分块翻再合并。
限制四:registry.json 和 .cache.json 别手改。前者记的是「原文 → hash key → 来源位置」,产物里看到 t_xxx 靠它反查;后者是提取缓存。
三、可复制的 Lua:标记、加载、切换
源码里标要翻的串。构建会自动抓,显式标记更稳:
-- 会被构建换成 _tr("t_xxx", level, total),源语言下直接可用
local text = _tr("第 {0} 关 / 共 {1} 关", level, total)
-- 节点名、资源路径、事件名这类程序标识符,钉死不要翻
node:SetName(_raw("PlayerSpawn"))
_tr 的第一参数在源语言表里查不到时会原样返回,所以手写原文加 {0} 占位符,中文版立刻就能跑,不用等翻译。
追加一份翻译表(热更文本、活动文案):
local ok = i18n.AddTranslations("i18n/patch_2026Q4/en.json")
if not ok then
log:Write(LOG_WARNING, "[lang] patch table missing or malformed")
end
语言选择菜单:
local function buildLanguageList()
local current = i18n.GetLanguage()
local items = {}
for _, code in ipairs(i18n.GetSupportLanguages()) do
items[#items + 1] = {
code = code,
label = i18n.GetLanguageDisplay(code), -- "简体中文" / "English"
active = (code == current),
}
end
return items
end
local function onPickLanguage(code)
if code == i18n.GetLanguage() then return end
i18n.RequestChangeLanguage(code) -- 弹确认框 → SetLanguage → 重启
end
开发期抓漏翻。挂一层壳,把「返回值和 key 一模一样」的记下来:
-- 只在 debug 构建里挂,正式包删掉
local rawTr = _tr
local missing = {}
_tr = function(key, ...)
local text = rawTr(key, ...)
if type(key) == "string" and key:match("^t_[%w_]+$") and text == key then
missing[key] = true
end
return text
end
这个判据站得住:翻译表里没有这个 key 时,translateOnly 返回的就是传进去的 key 本身。
切换前探一下能不能切:
local canSwitch = cache.SetVariantTag ~= nil
老引擎没有 SetVariantTag,RequestChangeLanguage 会改成弹「当前引擎版本不支持语言切换」。
四、踩过的坑
坑 1:翻译文件存成了带 BOM 的 UTF-8
-- ❌ 记事本 / 某些编辑器默认给你加 BOM
-- ✅ 存成 UTF-8 无 BOM
loadTranslationsInto 里是 pcall(cjson.decode, content)。BOM 让 cjson 直接失败,函数 return false,日志只留一行 [i18n] AddTranslations: '...' parse failed。整张表是空的,然后 _tr 把 key 原样吐出来——两处静默叠在一起,屏幕上满屏 t_ 开头的串。
坑 2:占位符写成了 %d
-- ❌ "Level %d" → 屏幕上原样显示 "Level %d"
-- ❌ "Level" → {0} 丢了,参数静默消失
-- ✅ "Level {0}"
_tr 的替换是 text:gsub("{(%d+)}", ...),只认花括号包数字。%s、%d 是 string.format 那条路的(它有自己的 hook),两套别串。占位符位置可以调,语序不同本来就该调,但一个都不能少。译文里多写一个调用侧没传的 {2},那个位置会被替换成空字符串,也不报错。
坑 3:以为 SetLanguage 切完界面就变了
-- ❌ i18n.SetLanguage("en")
-- ✅ i18n.RequestChangeLanguage("en")
目标语言的 {lang}.json 在重启前的预加载里才进缓存。重启前调 cache:Exists(uri) 是 false,日志写 not loaded yet, will take effect after restart,界面纹丝不动。已经建好的 UI 控件的文本测量缓存也不会重算。
坑 4:untranslated.fallback 用了默认值
// ❌ 默认
"untranslated": { "fallback": "key" }
// ✅
"untranslated": { "fallback": "source" }
key 的意思是「产物翻译表里省掉这条」,于是 _tr 返回 hash key,玩家看到 t_1710742800_a3f2b1c9。source 会把原文填进去,至少是中文。
坑 5:想让切语言不重启
// ❌ 设了 deferred_tr 就以为所有文本都能热切
"code": { "deferred_tr": true }
deferred_tr: true 只对独立字符串生效——它们被替换成纯 hash key,由渲染 hook 在呈现时翻译。带拼接的串强制即时求值,concat_to_format 会把 "第 " .. level .. " 关" 转成 _tr("t_xxx", level),那个值在代码执行到那一行时就定死了。
坑 6:语言码写宽了或写窄了
// ❌ "zh" / "zh_Hans" / "en_US"
// ✅ "zh_CN" / "zh_TW" / "en" / "ja" / "ko"
系统语言匹配靠 MatchLanguage(系统语言, support_langs)。中文必须带区域码,否则分不出简繁;英语反过来,en 能覆盖所有变体,写 en_US 反而漏掉 en_GB 的玩家。匹配不上就落回 source_lang。
坑 7:让提取器扫到了程序标识符
-- ❌ 节点名被翻译了,运行时按名字找不到节点
node:SetName("PlayerSpawn")
event:SetVar("Damage", 10)
-- ✅ function_excludes 里加 SetName / SetVar / GetResource / AddTag / SendEvent
节点名、资源路径、事件名、Tag、云变量 key 都是标识符。function_excludes 我配了 26 条,content_excludes 8 条专门拦 URL、相对路径、绝对路径、带扩展名的字符串、[TAG] 形式的日志前缀。单条豁免用 _raw(),整行豁免在 .lua 里写 -- i18n-ignore。
坑 8:改原文等于换 key
翻译键长这样:t_1710742800_a3f2b1c9。前一段是 10 位 Unix 秒,后一段是 8 位十六进制,跟原文内容绑定。
i18n/{lang}.json 是以原文为 key 的(所以文档说 grep 原文就能定位),registry.json 才负责把原文映射到 t_xxx。原文改一个字,新串进 pending.json 变成待翻,旧译文没人引用,躺在 {lang}.json 里当孤儿。文案定稿之后再提取,比边写边提取省事得多。
五、图片和音频跟着语言走
文本之外的走资源变体:同一个资源路径,引擎按当前语言自动路由,代码不用改。
assets/UI/
├── title.png ← 默认(源语言)
├── title@en.png ← 英文变体
├── title@ja.png ← 日文变体
└── background.png ← 无变体,所有语言共用
-- 代码永远引用默认路径
local tex = cache:GetResource("Texture2D", "UI/title.png")
-- 英文环境拿到 title@en.png,法文没变体就降级到 title.png,不报错
规则四条:默认资源必须存在;语言码跟 i18n.json 一致;只给有差异的资源建变体;切回源语言时变体标签被清空(SetVariantTag(2000, "")),所以中文版看到的一定是默认图。
多标签可以叠,顺序无关:title@en@hd.png。优先级是 review = 2100,i18n 语言标签 = 2000,画质之类的自定义标签小于 2000。同时生效时引擎生成候选键序列,取第一个存在的。
资源变体别用来做文本翻译。脚本和配置里的字走 _tr()。
六、适用边界
情况:做不做
只有中文一版,也不打算出海:不做,enabled 保持 false
要上 intl 服,或者 TapTap 海外区:做,而且越早越好
文本还在天天改:先别提取,等文案定稿
只有几十条 UI 文案:手动维护两张表更快,不值得上流水线
大量程序标识符混在字符串里:先把 function_excludes 配全,不然提取出来全是噪音
翻译本身有成本:英文比中文长约 1.3 倍,UI 控件会被撑爆。引擎给了自动缩字号的兜底——译文比原文宽时按 8 档比例缩,只缩不放。两条路各有各的下限:bare-nvg 那边是 7px 的 CSS 逻辑字号,UI 控件那边是 DEFAULT_AUTOFIT_MIN_FONT_SIZE = 8,两个数不是一回事,控件作者在 Button.lua 里留了 TODO 说要统一。
UI.defaultAutoFitText 只在 i18n.IsActive() 为真时才开,也就是当前语言不等于源语言时才开。中文下测着正好的按钮,切到英文就溢出,本地测不出来。
七、可迁移的判断方法
碰到「构建期替换 + 运行期查表」这类设计,我会先问一句:这个流程失败的时候,玩家屏幕上会多出什么?
i18n 的答案是 hash key。它不是报错,是一串长得像变量名的字符串混在正常 UI 里,看起来像美术资源出错。知道这个形状,排查方向就定了——不是渲染问题,是翻译表没进去。
(完)
星火编辑器-新手教程-关于多游戏模式的配置和触发的获取截图
星火编辑器-新手教程-关于多游戏模式的配置和触发的获取
HELLO 大家好,我是阿木木牧。最近第一版的内部小范围(几人)第一个图的测试已经结束了。 整体游戏以装备数值的崩坏而告终~ (虽然大家都很欢快。。。) 现在准备开始开发下个阶段的内容啦,就是将游戏模式拆分出来。分成单人模式,多人组队模式(合作PVE),多人匹配模式(对抗模式)。那么,怎么配置呢?说来也很简单哈~ 1,在游戏编辑器中,数据编辑器,找到地图设置。 2,编辑地图设置。比如我希望最大数量
14 赞
3 回复
不支持当前设备截图
不支持当前设备
荣耀400pro
2 赞
3 回复
00:18
修复一下这个挂起过。有概率用不了的bug吧截图
修复一下这个挂起过。有概率用不了的bug吧
太绝望了[表情_豹哭][表情_豹哭][表情_豹哭][表情_豹哭]
1 赞
1 回复
05:19
1截图
1
1.完成后,获得信用点的增益依旧有效,可以卡无限信用点。理论上 2.部分天赋和在一起可以实现永续,那些天赋,我下一个发,因为只能传一个视频。 3.卡,人多点就卡,知道卡死,但是手机不会卡就游戏卡
2 赞
无须申请,星火编辑器现已公测开放下载截图
无须申请,星火编辑器现已公测开放下载
—————————————————— 各位关注星火编辑器的小伙伴: 感谢大家一直以来对编辑器支持! 破晓已去,曙光降临! 星火编辑器现已开启公测! · 关于下载和资格申请 现已无需申请,请去下面的官方首页直接下载! 缘起测试是星火编辑器自诞生以来第一次对外测试! 从2022年八月开始到十一月,历时三个月。 缘起之后缘聚测试,历时又七个月! 缘聚之后,破晓降临,历时又十二个月! 曾经微弱的星火现已渐
官方
331 赞
375 回复
00:11
人物有小概率会卡着不往前走截图
人物有小概率会卡着不往前走
3 赞
1 回复