引言
多语言不是「把文案翻译一遍」那么简单:文本表怎么组织、字体能不能显示中文、切换语言要不要重启、换行会不会溢出、数字和日期怎么按区域格式化——每一环都可能在发版后炸掉。本文系统讲 Defold 的本地化方案:从文本表资源与语言配置讲起,覆盖运行时切换、字体与字形覆盖、GUI 排版适配、图片音频变体,以及数字日期复数规则。
前置阅读:GUI 节点与文本组件、资源导入与管理。
目录
- 1. 本地化基础与整体方案
- 2. 文本表设计与资源组织
- 3. 运行时语言读取与切换
- 4. 字体资源与字形覆盖
- 5. GUI 文本排版与溢出处理
- 6. 图片与音频的本地化
- 7. 数字日期与复数规则
- 8. 常见问题与调试
- 9. 速查表
- 相关阅读
- 延伸阅读
1. 本地化基础与整体方案
1. 本地化的四个层次
| 层次 | 内容 | 工作量 |
|---|---|---|
| 文本 | 界面文案、剧情对白 | 最大 |
| 字体 | 字形覆盖、行高、字距 | 中 |
| 资源 | 图片、配音、图标 | 中 |
| 格式 | 数字、日期、货币、复数 | 小但易漏 |
很多团队只做了第一层就上线,结果中文界面里字体显示成方块,或者数字写成 1,234 而德语用户期望 1.234。
2. 整体方案
locales/
en.csv ← 英文文本表
zh.csv ← 简体中文
ja.csv ← 日文
fonts/
default.font ← 拉丁字体
cjk.font ← 中日韩字体(含大字符集)
scripts/
i18n.lua ← 语言管理模块
核心思路:所有界面文案通过 key 访问,绝不硬编码字符串;语言切换只改一个全局状态,然后广播事件让 UI 刷新。
3. key 命名规范
menu.start 界面.元素.用途
menu.settings
dialog.npc_01.greet 剧情.角色.行号
error.network_timeout
item.sword.name
item.sword.desc
踩坑:key 里不要用大写和空格。Defold 的文本表按行存储,key 是字符串精确匹配,写错大小写不会报错,只会显示成 key 本身。
2. 文本表设计与资源组织
1. CSV 文本表格式
Defold 使用第一列为 key、第一行为语言代码的 CSV:
keys,en,zh,ja
menu.start,Start,开始,スタート
menu.settings,Settings,设置,設定
menu.quit,Quit,退出,終了
dialog.npc_01.greet,Hello traveler!,你好,旅行者!,こんにちは、旅人よ!
item.sword.name,Iron Sword,铁剑,鉄の剣
2. 加载文本表
-- 在 main.script 中加载
function init(self)
self.atlas = resource.load("/locales/locales.atlas")
-- 或者直接指定 csv 文件
self.locales = resource.load("/locales/locales.csv", "text")
-- 设置当前语言
sys.set_config_string("locale", "zh")
end
说明:sys.set_config_string 会写入配置,sys.get_config_string 可读回。推荐做法是用 sys.get_sys_info() 拿到系统语言作为默认值:
function init(self)
local info = sys.get_sys_info()
local lang = info.language or "en" -- 例如 "zh"、"en"、"ja"
-- 只保留主语言标签
lang = string.match(lang, "^%a+") or "en"
self.lang = lang
end
3. 文本表资源清单
如果按语言拆成多个文件,需要在 game.project 中把它们都加入资源清单,或者用 resource.load 动态加载。文本表文件名必须与 CSV 的 keys 列一致。
| 属性 | 说明 |
|---|---|
| 文件名 | 任意,但建议与语言无关 |
| 第一列 | 必须是 keys |
| 语言列 | 使用 ISO 639-1 代码 |
| 编码 | 必须是 UTF-8 无 BOM |
踩坑:Excel 另存的 CSV 默认是 GBK 或带 BOM 的 UTF-8,Defold 读出来会乱码。务必用文本编辑器确认编码为「UTF-8 无 BOM」。
3. 运行时语言读取与切换
1. 翻译函数
-- scripts/i18n.lua
local M = {
current = "en",
table = {},
}
function M.init(default_lang)
M.current = default_lang or "en"
M.table = {}
-- 逐 key 读入当前语言的文本
end
function M.t(key, ...)
local text = M.table[key]
if not text then
-- 缺 key 时返回 key 本身,方便定位遗漏
return "[" .. key .. "]"
end
if select("#", ...) > 0 then
return string.format(text, ...)
end
return text
end
function M.set_language(lang)
if lang == M.current then return end
M.current = lang
msg.post("#", "language_changed", { lang = lang })
end
return M
2. 读取单个 key
Defold 提供 sys.get_config_string 读取文本表条目,格式为 语言.key:
-- 读取英文文本
local s = sys.get_config_string("en.menu.start", "Start")
-- 读取当前语言(把语言代码拼进去)
local lang = i18n.current
local s2 = sys.get_config_string(lang .. ".menu.start", "Start")
注意:这个接口每次调用都会走配置查询,不要在 update 里每帧调用。启动时批量读进 Lua table 缓存,之后只查缓存。
3. 切换语言的完整流程
-- settings.script
function on_message(self, message_id, message, sender)
if message_id == hash("select_language") then
-- 1. 记录新语言
sys.set_config_string("locale", message.lang)
i18n.set_language(message.lang)
-- 2. 广播给所有 UI 节点
msg.post("/gui#ui_root", "refresh_texts")
-- 3. 重新加载字体(如果需要切换字体集)
local font = resource.load("/fonts/" .. message.lang .. ".font")
msg.post("/gui#ui_root", "set_font", { font = font })
end
end
参数说明:
| API | 参数 | 说明 |
|---|---|---|
| sys.set_config_string | key, value | 写入运行时配置 |
| sys.get_config_string | key, default | 读取配置,缺失返回 default |
| sys.get_sys_info | 无 | 返回 language、device_model 等 |
4. 语言切换要不要重启
Defold 允许运行时切换文本,但字体切换需要重新加载 font 资源。稳妥做法是:
方案 A(推荐):文本即时切换,字体在启动时按语言预加载好
方案 B:切换语言后提示「重启生效」,重启时读配置
方案 C:同时加载两套字体,切换时替换 gui.set_font 的引用
4. 字体资源与字形覆盖
1. 字体资源的组成
Defold 的 .font 文件是一个「字体集合」,可以包含多个 .ttf 作为 fallback:
myfont.font
├── main.ttf ← 主字体(拉丁)
├── cjk.ttf ← fallback(中日韩)
└── emoji.ttf ← fallback(emoji)
Fallback 顺序很重要:字符在主字体找不到时,按列表顺序依次查找。
2. 字形缓存与字符集
Defold 使用运行时字形缓存(glyph cache),但需要在 .font 中声明预生成字符集,否则第一次显示某字符时会有卡顿:
-- 预加载常用字符,避免运行时抖动
local chars = "0123456789"
.. "abcdefghijklmnopqrstuvwxyz"
.. "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
.. ",。!?:;、()《》"
.. "的一是了我不人在他有这上们来到时大地为子中你说生国年着就那和要她出也得里后自以会家可下而过天去能对小多然于心学么之都好看起发当没成只如事把还用第样道想作种开美总从无情己面最女但现前些所同日手又行意动方期它头经长儿回位分爱老因很给名法间斯知世什两次使身者被高已亲其进此话常与活正感"
function init(self)
-- 通过 resource 预载字体,触发字形生成
resource.load("/fonts/cjk.font")
end
踩坑:中文字符集有 2 万多个常用字,全量预生成会让字体图集非常大(几十 MB)。正确做法是只预生成界面文案里实际出现的字符,或者干脆依赖运行时缓存 + 加载界面兜底。
3. 按语言切换字体
-- ui_root.gui_script
local FONT_LATIN = hash("/fonts/latin.font")
local FONT_CJK = hash("/fonts/cjk.font")
local function font_for_lang(lang)
if lang == "zh" or lang == "ja" or lang == "ko" then
return FONT_CJK
end
return FONT_LATIN
end
function on_message(self, message_id, message, sender)
if message_id == hash("set_font") then
local font = font_for_lang(i18n.current)
-- 遍历所有文本节点替换字体
for _, node in ipairs(gui.get_nodes_by_type("text")) do
gui.set_font(node, font)
end
end
end
5. GUI 文本排版与溢出处理
1. 自动换行与尺寸模式
| 模式 | 属性 | 适用场景 |
|---|---|---|
| 固定宽度 | adjust_mode = FIT | 按钮、固定区域 |
| 自适应高度 | adjust_mode = ZOOM | 对话气泡 |
| 自动换行 | line_break = true | 长段落 |
-- 设置文本节点自动换行并按宽度适配
gui.set(node, "line_break", true)
gui.set(node, "adjust_mode", gui.ADJUST_FIT)
2. 文本溢出检测
德语、俄语的译文通常比英语长 30%~50%,固定宽度按钮很容易溢出:
-- 检查文本实际尺寸,必要时缩小字号
local function fit_text(node, max_width)
local size = gui.get_text_size(node)
if size.x > max_width then
local scale = max_width / size.x
gui.set_scale(node, vmath.vector3(scale, scale, 1))
end
end
踩坑:
gui.get_text_size返回的是未缩放的文本尺寸。如果节点本身有缩放,需要把节点缩放乘进去再比较。
3. 不同语言的行数差异
对话系统要预留足够的行数。英文 2 行的内容,中文可能 1 行,日文可能 3 行:
English : "Hello traveler, welcome to the village!"
Chinese : "你好,旅行者,欢迎来到村庄!"
Japanese: "こんにちは、旅人よ、村へようこそ!"
工程做法:对话框按最长语言预留高度,或者启用 ADJUST_ZOOM 让节点自动撑高。
4. 数字占位与变量插值
keys,en,zh
quest.kill,Defeat %d enemies,击败 %d 个敌人
quest.reward,Reward: %s,奖励:%s
local text = i18n.t("quest.kill", 5)
-- 英文: Defeat 5 enemies
-- 中文: 击败 5 个敌人
注意:
string.format的%d位置在不同语言可能不同。如果译文需要调整语序,用%1$s、%2$s这种带序号的占位符。
6. 图片与音频的本地化
1. 图片资源变体
带文字的图片(标题图、教程图)需要按语言出多套:
images/
title_en.png
title_zh.png
title_ja.png
-- 按语言拼接资源路径
local function localized_texture(name)
return "/images/" .. name .. "_" .. i18n.current .. ".png"
end
local path = localized_texture("title")
gui.set_texture(node, hash(path))
2. 音频本地化
配音是最贵的本地化项。常见策略:
| 策略 | 说明 | 成本 |
|---|---|---|
| 全配音 | 每语言一套语音包 | 最高 |
| 仅字幕 | 只本地化文本 | 低 |
| 关键语音 | 只配剧情关键句 | 中 |
-- 按语言选择语音包
local voice_path = "/audio/voice_" .. i18n.current
if sys.exists(voice_path) then
-- 加载对应语言语音
else
-- 回退到默认语言
voice_path = "/audio/voice_en"
end
7. 数字日期与复数规则
1. 数字格式化
不同地区的千分位与小数点符号不同:
-- 简单实现:按语言切换分隔符
local function format_number(n, lang)
local s = tostring(n)
if lang == "de" or lang == "fr" then
-- 德语/法语:千分位用点,小数点用逗号
s = s:gsub(",", "#"):gsub("%.", ","):gsub("#", ".")
end
return s
end
| 语言 | 1234567.89 的写法 |
|---|---|
| en | 1,234,567.89 |
| de | 1.234.567,89 |
| fr | 1 234 567,89 |
| zh | 1,234,567.89 |
2. 日期格式
local os_date = os.date("*t")
local patterns = {
en = "%m/%d/%Y",
zh = "%Y年%m月%d日",
ja = "%Y年%m月%d日",
de = "%d.%m.%Y",
}
local fmt = patterns[i18n.current] or "%Y-%m-%d"
local text = os.date(fmt, os.time(os_date))
3. 复数规则
英语只有单复数两种形式,俄语有三种,阿拉伯语有六种。简化方案:
keys,en,zh,ru
item.count.one,%d item,%d 个物品,%d предмет
item.count.many,%d items,%d 个物品,%d предметов
local function item_count(n)
local key = "item.count.many"
if n == 1 then
key = "item.count.one"
end
-- 中文没有复数变化,两个 key 指向同一文案即可
return i18n.t(key, n)
end
记忆:中文不需要复数变形,把
one和many指向同一文案即可;俄语、波兰语等斯拉夫语系才需要真正的复数规则。
8. 常见问题与调试
| 问题 | 原因 | 解决 |
|---|---|---|
| 中文显示为方块 | 字体无中文字形 | 添加 CJK fallback 字体 |
| 文本显示为 key | key 拼写错误或文本表未加载 | 打印 i18n.t 的返回值排查 |
| 文本表乱码 | CSV 编码非 UTF-8 | 另存为 UTF-8 无 BOM |
| 切换语言后旧文案残留 | 未广播刷新事件 | 切换后 msg.post 通知所有 UI |
| 界面溢出 | 译文比原文长 | 启用自动换行 + 缩放适配 |
| 数字格式不对 | 硬编码了分隔符 | 按语言做格式化 |
| 首帧卡顿 | 字形未预生成 | 加载界面预载常用字符 |
1. 缺 key 检测脚本
上线前用脚本扫描所有 i18n.t("...") 调用,与 CSV 的 key 列比对:
-- 开发期把缺 key 记录到日志
local missing = {}
function i18n.t(key, ...)
local text = i18n.table[key]
if not text then
missing[key] = (missing[key] or 0) + 1
return "[" .. key .. "]"
end
return select("#", ...) > 0 and string.format(text, ...) or text
end
function i18n.dump_missing()
for k, count in pairs(missing) do
print(string.format("[i18n] missing key: %s (used %d times)", k, count))
end
end
9. 速查表
| 需求 | API 或做法 | 备注 |
|---|---|---|
| 读取文本 | sys.get_config_string(lang .. "." .. key, default) | 启动时缓存,勿每帧调用 |
| 系统语言 | sys.get_sys_info().language | 返回如 zh、en、ja |
| 设置语言 | sys.set_config_string("locale", lang) | 写入运行时配置 |
| 翻译函数 | i18n.t(key, ...) | 缺 key 返回 [key] 便于排查 |
| 字体 fallback | .font 中按顺序添加多个 ttf | 主字体缺字形时依次查找 |
| 文本换行 | gui.set(node, "line_break", true) | 配合 adjust_mode |
| 溢出缩放 | gui.get_text_size + gui.set_scale | 注意缩放系数 |
| 图片变体 | 路径拼接语言后缀 | 构建时按语言裁剪 |
| 数字格式 | 按语言替换分隔符 | 德语点号千分位 |
| 日期格式 | os.date(模式, os.time()) | 每语言一个模式 |
| 复数 | 按语言选 key | 中文无需变形 |
一句话记忆:文本走 CSV 文本表 + sys.get_config_string 按 语言.key 取值并缓存;字体必须准备 CJK fallback 并预生成常用字形;语言切换靠「改配置 + 广播事件 + 刷新 UI」三步;数字日期复数按语言规则格式化,别在代码里硬编码分隔符。
相关阅读
延伸阅读
- 图集与贴图切换机制
- 音频资源组织与语音包切换
- 多地区发布与包体控制
- 通过热更新下发语言包
- 游戏开发专题 — 游戏本地化工程实践
-- ======================================
-- 完整示例:i18n.lua 语言管理模块
-- 放在 /scripts/i18n.lua
-- ======================================
local M = {
current = "en",
cache = {},
missing = {},
}
local SUPPORTED = { en = true, zh = true, ja = true, ko = true, de = true, fr = true }
-- 从系统语言推断默认语言
function M.detect_default()
local info = sys.get_sys_info()
local lang = info and info.language or "en"
lang = string.match(lang, "^%a+") or "en"
if SUPPORTED[lang] then
return lang
end
return "en"
end
function M.init()
M.current = sys.get_config_string("locale", M.detect_default())
M.cache = {}
print("[i18n] 初始化完成,当前语言:", M.current)
end
function M.t(key, ...)
local text = M.cache[key]
if not text then
-- 首次访问时从配置读取并缓存
text = sys.get_config_string(M.current .. "." .. key, nil)
if not text then
M.missing[key] = (M.missing[key] or 0) + 1
return "[" .. key .. "]"
end
M.cache[key] = text
end
if select("#", ...) > 0 then
local ok, formatted = pcall(string.format, text, ...)
if ok then
return formatted
end
return text
end
return text
end
function M.set_language(lang)
if not SUPPORTED[lang] then
print("[i18n] 不支持的语言:", lang)
return false
end
M.current = lang
M.cache = {}
sys.set_config_string("locale", lang)
-- 广播给所有 UI 刷新文案
msg.post("/gui#ui_root", "refresh_texts", { lang = lang })
return true
end
function M.dump_missing()
local n = 0
for k, c in pairs(M.missing) do
print(string.format("[i18n] 缺失 key: %s (调用 %d 次)", k, c))
n = n + 1
end
print("[i18n] 缺失 key 总数:", n)
end
return M
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。