Defold 本地化与多语言

系统讲解 Defold 的多语言方案:文本表资源与 CSV 组织、语言配置读取与运行时切换、字体资源与字形覆盖(含中日韩字符集)、GUI 文本排版与溢出处理、图片音频资源变体,以及数字日期与复数规则的适配要点。

引言

多语言不是「把文案翻译一遍」那么简单:文本表怎么组织、字体能不能显示中文、切换语言要不要重启、换行会不会溢出、数字和日期怎么按区域格式化——每一环都可能在发版后炸掉。本文系统讲 Defold 的本地化方案:从文本表资源与语言配置讲起,覆盖运行时切换、字体与字形覆盖、GUI 排版适配、图片音频变体,以及数字日期复数规则。

前置阅读:GUI 节点与文本组件、资源导入与管理。


目录


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_stringkey, value写入运行时配置
sys.get_config_stringkey, 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 的写法
en1,234,567.89
de1.234.567,89
fr1 234 567,89
zh1,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 字体
文本显示为 keykey 拼写错误或文本表未加载打印 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

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「defold」更多文章

  1. Defold 测试与持续集成
  2. Defold 团队协作与版本控制
  3. Defold 分析与崩溃上报