其实本来我的标题是 “Quick-cocos2d-x 中的国际化与本地化”, 多语言虽然是其中的主要内容, 但还有很多额外的工作.

比如: 在韩国上线的游戏必须在游戏第一次启动时弹出一个内容十分长的用户协议, 用户同意后方可继续游戏; 比如很多赌博性质的活动(抽奖, 拉霸, 转盘)都需要修改为其他表现形式. 还有一些技术方面的要求比如游戏用户的数据不能存在 cache 目录下等等.

每个国家和地区的要求都不尽相同, 有的是硬性的法律法规要求, 有的则是照顾到当地风俗习惯以提高用户体验. 当然, 这些并不在我们这次讨论范畴之内, 等我们的经历足够丰富之后可以再次和大家分享一下.

今天, 主要和大家说说多语言.

一. 策略

1. 语言代码

就是不同语言我们需要一个 id 与之对应, 这个有很多种选择, 我们选择了微软翻译的代码:

Language Code English Name
zh-CHS Chinese Simplified
zh-CHT Chinese Traditional
en English

2. 多语言文本 id

我们的策略很简单, 每一个多语言文本都有一个唯一 id, 每一个语言都是由多个 id: text 组成的 json 文件, 如下所示:

1
2
3
4
5
6
7
{
"1493084502": "积分兑换超级大奖",
"1493084508": "[day]天[hour]小时后结束",
"1493172258": "活动积分",
"1493723018": "7日活动积分",
"1493731515": "该功能暂未开启, 请耐心等待."
}

在游戏初始化的时候选择不同语言的 json 加载, 然后有一个函数 tr 接受 id 返回 text, 这就是我们全部的策略. 就是这么简单的策略, 我们也踩了不少坑.

最开始, 我们是打算弄一个自增的 id, 我们规定了一个从 100000 开始, 每次自增 10 这样的一个 id 生成策略, 之所以自增 10 是考虑到插入 id 的需求. 我们自以为这个策略很鲁棒, 却还是栽了跟头:

合并时会冲突

我们不同的功能是在不同的分支上做的, 完成之后会合并到主分支上. 大家在不同的分支上开发不同的功能时, 没有考虑到多语言后期合并冲突的问题, 而且这个冲突解决起来很麻烦, 得为冲突中的一方分配新的 id, 还得将代码中的 id 都替换掉, 这个过程是十分容易出错的. 怎么办 ?

我们也想过规定不同的 id 区间, 不同的模块有着不同的 id 起始值, 这样虽然一定程度上解决了模块间冲突的问题, 就算忽略规定这个 id 起始值所带来的额外工作, 多人协作的同一模块怎么办 ? 小伙伴们是不是还得提心吊胆, 小心翼翼的工作 ? 这可不是我们的风格.

就在我一筹莫展的时候, 我偶然间发现了一个东西: 时间戳, 我们可以用这个做 id 呀 ! 虽然理论上还是有可能冲突, 但是两个多语言 id 在同一秒内生成的概率又能有多高呢 ?

这里还要说说为什么我们没用使用 英文意义 作为 id 呢 ? 诚然英文 id 有更高的可读性, 有两个原因导致我没有选择它:

  1. 小伙伴们的英文水平参差不齐, 如果使用英文 id 的话, 很有可能会出现词不达意的情况, 反而降低可读性.
  2. 一个 id 所代表意思可能会发生变化, 如果变了, 是否要修改所有的引用呢 ?

3. 占位符与格式化

我们一开始也是使用 %d %s 之类的东西做占位符, 但是这些东西是严格以来占位符及参数的顺序来替换的, 而同一个占位符在不同语言中的的位置可能是不同的. 比如:

中文: “军官统御等级每增加 %d(1),增加带兵量 %d(10)”
英文: “The size of the troop increase %d(10) by officer’s Command Level increase %d(1)”

大家可以看到, 这两种语言下两个占位符的顺序是完全相反的. 如果我们使用这种方式来的话, 就会对玩家造成误解. 那么我们应该怎么做呢?

中文: “军官统御等级每增加 [lv](1),增加带兵量 [amount](10)“
英文: “The size of the troop increase [amount](10) by officer’s Command Level increase [lv](1)“

我们使用明确意义的占位符来占位.

4. 图片的多语言

我在一篇文章中看到说要尽可能的避免使用带有文字的图片, 但是这种需求是无法避免的. 如果实现不同语言下用不同的图片呢 ? 我们的做法是给这个图片的命名中加入标记, 标记这是一个多语言图片, 在通过一个函数来获得真正的图片路径.

例如: 有一个图片的路径是 images/logo.png, 我们需要修改为 images/logo[zh-CHS].png, 标记这是一个中文下的图片, 同理会有一个 images/logo[en].png. 真正加载图片的时候, 会通过函数替换使用当前语言替换掉里面的占位符.

二. 代码支持

1. 当前语言的确定

一开始我们是直接使用的 device.language 来确定当前语言的, 但实际情况要复杂的多.

1). 语言残缺

cocos 默认只支持cn:中文, fr:法语, it:意大利语, gr:德语, sp:西班牙语, ru:俄语, jp:日语, en:英语 这几种语言, 如果我们要支持一个这里面没有的语言怎么办 ?

我们需要分别从 Android 和 iOS 哪里获取到设备的当前语言 language code, 然后在 lua 中进行判断.

Android:

1
public static String getLanguageCode() { return Locale.getDefault().toString();}

iOS:

1
NSString* language_code = [[NSLocale preferredLanguages] objectAtIndex:0];

2). 简体/繁体的确定

这个确实值得拿出来一说, 这个 language_code 其实是有一套标准的, 但这个标准有好几个版本, Android 和 iOS 返回的可能不是一个标准, iOS 不同版本可能返回的不是同一个标准, cocos 原生的那个写法实际上是有漏洞的, 就中文来说会有这么几个写法:

1
zh-CHS, zh-Hans, zh-CHT, zh-Hant, zh-cn, zh-tw, , zh-mo, zh-sg, zh-hk

最终我们需要把这些可能转化为两种 简体/繁体, 我们是这么做的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
local language = language_code
if string.startswith(language, "zh") then
if string.find(language,"Hans") then
language = "zh-CHS"
elseif string.find(language,"Hant") then
language = "zh-CHT"
elseif string.find(language,"TW") then
language = "zh-CHT"
elseif language == "zh-cn" or language == "zh-mo" or language == "zh-sg" then
language = "zh-CHS"
elseif language == "zh-hk" or language == "zh-tw" then
language = "zh-CHT"
else
language = "zh-CHS"
end
end

同理, 不光中文是这样的, 英文也同样有很多方言. 所以我们不能用 == 来判断某个语言, 要用 startswith .

3). 考虑支持的语言列表和玩家存档

要考虑这么两个问题, 通过上一步获取到一个你不支持的语言怎么办? 我们的做法是声明一个默认语言(英语), 某个语言不支持就用这个语言.

同时如果游戏内有选择语言功能的话, 我们要优先使用玩家选择的语言.

声明支持的语言:

1
2
3
4
5
6
7
8
9
10
11
12
platform.language_support = 
{
default = flavor.language.en,
support_list = {
flavor.language.en,
flavor.language.zh_chs,
flavor.language.zh_cht,
flavor.language.ar,
flavor.language.ko,
flavor.language.th,
}
}

判断语言:

1
2
3
4
5
6
7
8
if #(platform.language_support.support_list) >= 2 then
platform.language = Record.getLanguage()
if not table.contain(platform.language_support.support_list, platform.language) then
platform.language = platform.language_support.default
end
else
platform.language = platform.language_support.default
end

2. tr

tr 就是根据 id 返回真正文本的函数:

1
2
3
4
5
6
7
8
local text = json.decode(io.readfile(cc.FileUtils:getInstance():fullPathForFilename(("native/"..platform.language..".json"))))

function tr(_key)
if not _key then
return "???"
end
return text[tostring(_key)] or "404:"..tostring(_key)
end

很简单, 还有一些可以拓展的空间, 比如最近发现的同一个中文如何对应不同 case 英文的问题. 举个栗子: 道具的英文是 item, 我们可能很多地方都会用到这个单词, 但是不同的地方可能会有一些小的区别:

  1. 主界面入口上需要显示为全大写: ITEM
  2. 行首的拼接需要首字母大写: Item
  3. 行中的拼接需要全小写: item

这个难道要多用好几个 id 来实现吗 ? 我们可以通过 tr 的第二个参数来指定格式.

1
2
3
tr(10000, "upper") -- 大写
tr(10000, "title") -- 首字母大写
tr(10000) -- 小写

内部再处理下这个参数就可以实现同一个中文对应不同的英文了.

3. formatex

上面说过有一个函数替换占位符, 其实现如下:

1
2
3
4
5
6
7
8
9
10
11
function string.formatex(format, map)
format = string.gsub(format, "%[(.-)]", map)
return format
end

-- example
string.formatex("[attacker]砍了[defender]一刀, 造成了[damage]伤害", {
attacker = "张三",
defender = "李四",
damage = 10,
})

很方便的有木有 ?

三. UI 的适配

1. 多个横向排版 Label

我们先来看两张图:

上图中的左侧的 Label 在不同语言下的宽度是不一样的, 如果我们不想在代码中手动调整的话有这么几个办法:

  1. 使用一个容器存放多个 Label, 容器会自动排版多个元素的位置
  2. 在编辑 UI 时就预留好可一定的空间
  3. 如果只有两个 Lable 的话, 左侧的右对齐, 右侧的左对齐

2. Label Overflow

这个概念是从 Cocos Creater 哪里找到的, 如果一个 Label 的实际尺寸超出了其在 UI 编辑时设定的最大范围的话如何处理呢 ?

  1. 缩小 Lable 的字体尺寸
  2. 多余的子使用 … 代替
  3. 增大 Label 的高度

我在 Quick-x 搞了一个很简陋的实现, 原理就是弄一个死循环, 判断尺寸超了就缩小一个单位, 但是效率不高, 就不贴实现了.

3. 阿拉伯语的适配

大家都知道阿拉伯语的阅读顺序是从右往左的, 因此我们的 UI 最好也能是从右往左的. 但是除非从立项一开始就料想到了这一点, 否则更改全部 UI 是不现实的.

但是我们可以修改部分 UI, 如上面我们说的 横向排列的多个UI , 如果我们采用一个容器来实现的话, 那么很容易的实现容器内的元素顺序逆转.

我们按照这个思路实现了一个 BoxLayout , 在阿语下 layout 时会从元素的最后一个开始, 反向排版.

另外一个特殊处理就是 RichLabel, 我们自己实现了一个按字符遍历的富文本. 但是阿语下这个实现几乎变得不可用, 于是在阿语下我们使用普通文本来替换了富文本.

三. 外部工具

1. 多语言转换工具

json 作为程序读取的格式是没有什么问题的, 但是用来给翻译人员来翻译就很不方便了, excel 则是一个很不错的选择. 因此我们实现了一套 json > excelexcel > json 工具用来做这个转换.

同时, 为了能更高效的处理各种需求, 我们还有 diff, format, deduplicate 工具.

2. 多语言提取工具

我们的多语言有很多是配置在 excel 中的, 这些 excel 最终会转换为游戏的静态配置, 我们希望最终在游戏中读取的一个多语言的 id, 而不是一串文本. 这样做能够降低静态配置的文件大小, 因为我们的文本有很多是重复的.

因此我们实现了一个抽取工具, 能够为 excel 中的文本打上标记, 这样在转换的过程中就可以使用 id, 而不是文本了. 效果如下图, # 前是文本, 后面是 id:

我们用的编辑器 CocosBuilder , 很古老的一个 UI 编辑器, 因此也没有多语言的功能, 所以我们做了一个从 ccb 中提取多语言的工具. ccb 本身是一个 plist 的文件结构, 很多语言都有对应的解析库, 写起来很容易.


参考资料:

  1. https://zh.wikipedia.org/wiki/ISO_639
  2. https://www.zhihu.com/question/20797118
  3. http://www.gameres.com/thread_480715_1_1.html (前辈的踩坑指南, 推荐阅读)