前一阵子在做新项目的时候想了一下目前的项目在本地化文本管理方面遇到的诸多问题,正好又碰到了一个新朋友来问这个问题,翻出了7年前我写的一篇博客,现在回头看的话感觉当时写的太简陋了,因此决定重新分享我的个人经验,在多语言文本管理中遇到的问题,以及我建议的解决方法。
中心化管理:看起来很美
市面上最常见的多语言文本管理的手段是中心化的管理手段,即多语言文本完全脱离其所被使用的场合,作为一个单独的表单或文件存在,任何需要显示文本的地方,都以key为唯一的索引从这个文本中来获取对应的本地化语言的文本。
在举一个具体的例子之前,我们要明确一个观点:一个项目中的文本主要分两种:UI文本和数据文本。UI文本指的是界面上各种控件显示的文本,数据文本则是需要策划管理的游戏数据的文本。除此之外还有第三种文本:服务器文本,这个属于比较特殊的情况,不在下面的例子中讨论,文章结尾的地方会专门说明。
假如我们在做一款RPG游戏,有英雄和技能两个表,最终导出了三个文件,那么一个典型的中心化管理的多语言文本可能是这样设计的文件结构:
hero.json
skill.json
text.json
我们只看hero.json和text.json来说明问题。
假设一个英雄非常简单,只有两个属性:名字、技能,那么英雄的数据可能长这样:
hero_id = 1
hero_name = "text_hero_name_001"
hero_skill = [1,2,3]
本文重点关注的即是hero_name的部分,这是需要多语言管理的部分。显然"text_hero_name_001"是一个key,其指向的真实文本储存于text.json中。
那么text.json长什么样?如果我们用同一个text.json来存储全部多语言的文本,那么可能长得类似这样(Dictionary/Object风格):
text_hero_name_001 =
{
"zh_CN" : "王老王",
"en_US" : "King Old King",
}
也可能是这样(CSV风格):
key,"zh_CN","en_US",
text_hero_name_001,"王老王","King Old King",
也可能是每个语言对应一个text.json文件,这种就不再举例赘述,但不管哪种,本质都是一样的:文本脱离于其所被使用的环境,进行中心化管理,并且在被使用的地方通过Key来索引。
这看起来没有任何问题,也是业界的主流做法,但在我这些年游戏开发的经验中却觉得这样做的问题非常大,甚至可以说这都是程序思维的产物,能实现需求但却基本没考虑策划和UI的维护成本,下面就开始详细的阐述这种方式的弊端。
问题一:脱离使用环境
假设你是一个UI同学,现在做了一个简单的弹出层,有标题、正文和确定按钮,你需要三个文本来描述这个界面:
标题:确认充值?
正文:做游戏不赚钱,就是交个朋友。确认充值648钻?
按钮:确认
那么按照上述的管理方式,界面上可能是这样的:
标题:text_dialogue_title_iap_001
正文:text_dialogue_content_iap_001
按钮:text_dialogue_button_iap_001
一个UI同学在UI编辑器或游戏引擎中长久的面对这种不可读、不可排版的文本,长此以往对心灵会产生什么样的打击不言而喻——当然,这跟程序没关系,程序也不在乎。
假如UI抱怨的非常凶,要求程序在编辑环境中也要默认把这堆看不懂的key给显示成简体中文,而程序勉为其难的也给做了,那么至少解决了UI同学的问题,但前文提过,UI文本只是一个项目中的一部分,还有另外一个大头:数据文本。
还是刚才的hero.json的例子,假如我们这个RPG项目有100个英雄,那么我们在hero.json中能看到的是什么呢?是任何一个英雄你都不知道他的名字是什么,只能看到从text_hero_name_001到text_hero_name_100这种意义不明的东西。
作为一个策划,你不太可能记得住一个英雄的ID,反而记住他的名字要容易得多。那么现在你要去找一个英雄的数据,你要怎么办?你可能要去text.json中找到这个英雄的名字,再看对应的key来识别他的ID,再去hero.json中找到这个英雄的数据。
这时候假如有个策划不那么讲究,没按照规范去好好的给hero_name对应的文本建key,并且当时他偷摸就提交了,谁也不知道,本来应该叫做"text_hero_name_100",但这个策划给起成了"text_hero_name_wanglaowang",那你找起来就会想杀人了。
这时候还有更大的问题,如果一个策划想要知道一个英雄有什么技能,那怎么办?因为技能表里也是不带名字的,他就只能先想办法找到英雄的ID,然后再找到技能的ID,然后再去看技能的名字。
这种套娃操作每多一层,策划的心理都会多一层崩溃。
除此之外,脱离使用环境还使得你的项目需要翻译外包的时候看似容易,只要把text.json丢过去就行了,但因为翻译的人员不知道这个文本是用在哪里的,因此很容易出现翻译完了结合上下文意义错误或UI的显示出现异常的情况。这部分的沟通成本也是相当巨大的。
问题二:单一职能原则
假设我们还是上面那个做充值对话框的UI同学。这个项目肯定不止这样一个对话框,所以我们还会做很多很多其他的对话框,比如购买各种东西的确认对话框、各种危险操作的确认对话框、需要选择个数或者填写内容再提交的对话框等等。
做多了之后我们就会发现,这些对话框都有一个确定按钮,而且按钮的文本内容可能都一样,都是“确定”,但却每次都要起一个新的text的key。当这个UI同学打开了text.json后,发现有几十上百个不同key的文本都叫“确定”的时候,他一定会怀疑这种做法是不是有问题。
最后UI们商量了一下,决定所有的确定按钮都用一个key,比如这个key叫做“text_dialogue_button_confirm”。大家觉得工作简单多了,所有的确定按钮都用这一个文本,不用再弄大量臃肿的文本了。
直到再过了一段时间,策划提了一个需求,充值界面的确认按钮不能只写“确认”,要写“确认,我家有矿”。策划认为简单的改文本就行,于是就把“text_dialogue_button_confirm”的内容给直接改了。
改完了之后发现项目出了大问题,所有的对话框的确认按钮的文本都变成了“确认,我家有矿”。然后QA爸爸就提着刀过来了。
QA爸爸把这个问题捅到了项目主管那里,等到项目主管发现这个问题之后,决定检查一下所有的text.json里面的文本,要确认下到底有哪条key是不止被用了一次的,但却发现很难做到这样,因为项目进展到这个阶段,text.json里面可能已经有上万条数据了,你既没法简单的知道里面有没有完全没被引用实际上已经冗余了的条目(比如删了一个技能,但技能的名字没删),也没法确认里面的某一个条目有没有被多次引用(类似上面的确认的问题,策划和美术都会有这个情况),从而导致改动的时候引发意料之外的问题。
当然,你可以硬性的规定,凡是要复用的text都放到同一个文件里(比如它叫general_text.json),凡是应该被用且应该只被引用一次的text都放在text.json里,但毕竟只要key是人为的手写的,就无法完全避免这个问题,很快两个文件里面可能又会出现混乱——text.json里面出现了被引用多次的key,而general_text.json里面出现了冗余的key。
假设这时候有一个程序同学觉得可以从工具上来入手解决这个问题,于是他写了一个工具,从此text.json的key都是自动管理的了,策划同学可以方便的在各个数据表中添加、修改、删除文本,key的关联都是自动完成的,或者说text.json的key完全都由这个工具自动管理了,key甚至对策划来说是不可见的,从而从根本上避免了策划手误的问题。
这听起来很美好是吧?但其实并不是的,因为这个方案依然有问题。
从原则上来说我个人是非常不建议项目在常规的开发工作流中加入一些自己开发的工具的,除非是在这方面有相当丰富经验的开发团队,或者这个工具本身足够简单、不进入工作流(比如用了一次就不用了的“日抛型”工具)。原因有三点。
首先是这个工具的可维护性问题。这些工具在开发之时只是为了尽快的解决某些小问题,并没有严格的当做一款可以交付使用的产品来对待,因此缺乏文档,并且开发的也相对随意(用户是内部开发人员而不是游戏玩家)。这个工具本身需要测试但缺乏测试,它的完整性和可靠性需要支付相当大的成本,而这部分成本本来是可以去开发玩家可以体验到的功能的。这还只是短期的问题,如果我们从长计议的话,短期少做点功能,把工具做好,也没什么问题,但长期问题实际上更大。当这个工具被用了几个月甚至几年之后,不出bug的概率几乎等于0,而当初写这个工具的人可能早已不在这个项目组甚至离职了,而由于缺乏文档且开发随意,接手维护这个工具的人可能完全无法上手,这种情况下项目就陷入了一个两难的境地。还有很多情况下,随着工作流的调整或人员的变化,慢慢的这个工具也会被不断的迭代,但其迭代的速度往往是滞后于团队的变化的,甚至最终成为拖后腿的卡点——你不得不用它,而你又明知道它已经不好用了。
其次是这个工具的可靠性问题。这些工具所依赖的开发环境过于复杂,公司的一次停电、游戏引擎的一次升级、一个脏数据的写入、一个策划不小心的误操作(比如把ID填重复了),都有可能产生大量的问题,这些问题是最早做工具的同学所意料不到的(大家都知道,程序员是乐观的),而为了解决这些问题所有的策划都不得不停工,并且问题解决的代价和效果也不得而知(比如可能导致json中的所有排序都变了,虽然实际上没变化因为json里面排序不重要,但QA爸爸能看到的就是一个上万行的json文件的每一行都变了,而导致QA爸爸提着刀过来)。
最后一旦你开始依赖这种小工具后,往往会接二连三的做一大堆工具,前面的两个问题很快会变成多个问题,很快就会陷入按下葫芦起了瓢的状态,甚至当出了问题之后你都不知道究竟是哪个小工具的锅,开发工具的人抱怨使用工具的人提的问题模糊而不确定(甚至怀疑是使用者自己的问题),使用工具的人抱怨开发工具的人做的破玩意不靠谱,最终所有人都难受。
因此,如果你必须要把某个工具加入工作流中,那么如果有外部的、成熟的、有长期维护的解决方案,尽量不要自己造轮子,宁可花点钱买解决方案,也比自己花精力去做这些事情要强,否则当你过了几个月甚至几年后,一定会为当初自己的决定后悔——假如你还在这个项目组的话。
要相信你的问题别人都早就遇到过了,用别人造好的轮子总是比自己造轮子强,人类社会就是这么进步的。
问题三:依赖性问题
假如你是一个策划,已经设计好了一个新的英雄,以及他的技能,现在要开始配表了。
既然要加英雄,那么理所当然的你打开了hero.json,但是当你加到一半的时候发现加不下去了,因为hero_name需要一个key,你必须先去text.json里面配置好这个文本,再填回到hero.json中来才能完成配置。提交的时候你必须同时提交这两个文件,否则英雄的名字就会显示错误,显然这是会被QA爸爸暴揍的。
等你搞完了之后又发现这个英雄要加技能,那必须又先去配技能表。而配技能表的时候又发现了必须要去先去text.json里面写好技能的名字。
而现实情况中这个套娃的情况往往更严重,以我目前的项目为例,我需要新加一个宝箱,这个宝箱有【名字】有【描述】,这个宝箱有一个对应的道具ID,道具有【名字】有【描述】,这个宝箱里面是一件新时装,这个时装有【名字】有【描述】,这个新时装有对应的道具ID, 道具有【名字】有【描述】,这个新时装有对应的时装碎片,这个碎片有【名字】有【描述】,这个碎片有对应的道具 ID , 道具有【名字】有【描述】。等这一套折腾完,你会发现最耗费精力的不是宝箱->时装->碎片的套娃,而是不管你干啥都需要去text.json里面加名字,而这对于策划来说也是非常痛苦的。
当然,在删除配置的时候策划也会面对同样的噩梦,要删一个东西就要删大量对应的text,而这种删除操作的危险性在上面的一个问题中已经提到过了,因此最终往往会演变成“冗余就冗余,只加不减就好了, 这样至少不会出错”的情况。这会导致外包成本急剧增加,因为你也不知道哪些文本有用,哪些文本没用的。
更严重的是这会让你的项目很屎,而这屎不是喂给玩家的,是喂给策划的。大家都知道这里面充满了屎,但谁也没法认出来究竟哪些是屎。一个敢喂自己屎的策划,对玩家能做出多么丧心病狂的决策都是有可能的。
当一个东西变得反直觉的时候,大概率有更好的方案可以去替代它。
问题四:冲突问题
极端的情况下,不管你改客户端的什么表,都需要同时的去改text.json。而text.json只有一个,所有策划共用,因此冲突的几率极高,同时类似SVN的版本管理工具对json文件的比对支持的也不是特别好(你可能需要专门的json语意比对工具),于是策划的作业变成了一场噩梦,你只是想把自己改的东西提交上去而已,但却发现这竟然如此困难。
而更大的问题是,你不只是提交就完事了,你还需要合并呢,合并的时候更是一场噩梦。
虽然靠培训每个人都要学会提交、合并、解决冲突可以来克服这个困难,但毕竟这个困难实际上……有可能压根就不存在,而只是因为一开始设计的偷懒导致的没必要的困难。你让程序把所有代码都写在一个文件里他们自然不干,那为什么策划和UI的所有文本都在一个文件里他们就干了呢?——因为不用他们维护,这是个屁股决定脑袋的问题。
问题五:唯一性问题
由于大量的文本都堆在一个文件里,为了保持key的可读性和唯一性,命名就成了一大难题,其困难程度甚至比美术同学考虑美术资源的命名还困难,因为美术资源可以分文件夹,但key都堆在一起。
想找到一套完美的可以描述所有东西的key的方法不是不行,但这种结果大概率会让key变得非常冗长。比如美术可能考虑把界面的名字或者prefab的名字加进去,策划则要把是哪个表的哪个id的哪个字段用到的加进去。很快你的项目就会出现一个神奇的现象:大部分的字符串的key甚至比其本身的内容还要长的多的多,整个text.json文件奇大无比,但“三斤鸭子两斤嘴”,肉没多少。这种情况的体验就像是大家上班的时候互相不叫昵称也不叫姓名,而是互相喊身份证号的完整号码来互相沟通一样,显得非常蠢。
那怎么办?
解决方法我觉得很简单,去中心化,直接对症下药,需要完成以下几个需求:
文本不再中心化全放在一起,而是分散开来,根据各个模块分散到各个文件中,并且把程序设计成只能访问自己相关模块的文本。这样即能解决一个大文件策划互相冲突的问题,也能解决滥用key的引用导致无法追踪每个key都被哪里用到了问题。一言以蔽之,把text从全局的改成本地的。
文本尽量贴近其所被使用的场合,甚至可以完全免掉key是最好的。这样既不用费劲去给key取名字,也不用去琢磨key的冗余或被多次引用的问题了,也不用去开发劳什子自动关联key的工具了。
于是hero.json会变成什么样子?大概会变成这样:
hero_id = 1
hero_name =
{
"zh_CN" : "王老王"
"en_US" : "King Old King"
}
hero_skill = [1,2,3]
回头一看,这其实就是我七年前贴的文章中《炉石传说》的做法。暴雪在《星际争霸2》中采用的还是中心化的管理办法,而《炉石传说》他们选择了另外的做法,我猜是他们吃屎吃够了。
如果策划用Excel来管理多语言的话,需要写插件,否则一个格子里面写Dictonary/Object风格的东西会很蛋疼;或者策划可以在多列中配置语言,但最终把多列导出成Dictionary/Object风格的内容。但是毕竟长痛不如短痛,总比搞好几个表来回贴key要舒服得多。
有人可能说了,这样策划要每次都去写"zh_CN"和"en_US"这种文本,不很容易错吗?但你想想,是写这样固定的文本容易错,还是每次都要依照一个规则去想一个新的key更容易错呢?
还有人可能会说,这样客户端不管当前语言是什么,都会加载全部的语言文本,有点浪费资源。但根据我个人的理解,占内存大头的永远不会是文本文本,而是二进制文件,因此这方面的性能问题也可以忽略不计。反而这可能成为一个好处,即切换语言不需要重启客户端更容易实现了,但毕竟我不是专业的程序,这里可能还涉及到加载字体等问题,这里要视项目最初的需求是否需要不重启客户端就能切换语言了。
好了,策划的同学解决完了,那么UI同学怎么办?
比较简单的做法是扩展一个支持多语言的控件,比如原本系统的文本控件只能输入一套文本,让程序扩展一下可以写多套文本即可,同时默认显示中文。比如一个确定按钮,本来UI拉上来一个button后直接在text里面写“确定”就行了,现在的话会有多行的text,其中第一行是"zh_CN"的,UI同学要在这里写"确定",然后在第二行"en_US"里面写"Confirm"。同时还可以支持切换预览多语言效果。实际上很多多语言插件都是用类似的思路去做的,比如Unity的I2 Language(这里不是打广告,我没用过,但看demo感觉挺好用的,还支持图片和音频的多语言,而且这也是Unity Asset Store中最受欢迎的本地化插件)。
UI文本本地化之后的一个问题就是外包的时候我们还是需要导出文本,因此在一开始设计结构的时候需要尽量设计一个不依赖于Key同时还可以方便导出导入的结构,单独把每个界面prefab的文本导出,翻译完成后再导回来即可。上面提过的I2 Language貌似还支持谷歌翻译以及导入导出功能(这插件本质上还是中心化管理文本的,只是界面上隐藏了),感兴趣的同学可以自己试试。
但是以上毕竟只是脑洞,我决定在新项目中试一下用这种方式来管理文本的实际效果如何。因为都是我一个人做的,所以UI的多语言文本我决定不写在控件上,而写在代码里,一个prefab的代码把需要用到的文本统一在一个地方以Dictonary/Object的风格来声明好,方便后面调用。而策划配表的部分,我决定在Excel中每个文本一列,再导出的时候把同一个key的文本合并成Dictonary/Object风格的变量。
这里实际也引申出了另外两个关于UI实现问题。
第一个问题是Prefab这个东西往往是客户端程序和UI同学都要打交道的,这里非常容易出现问题,比如UI同学一不小心把某个节点隐藏了,QA同学提了BUG,程序同学绞尽脑汁的去调查问题最终发现居然不是自己的锅。解决方案业内也有一些,有的比较笨的办法是由程序同学来拼界面,这样避免UI同学染指Prefab,但拼完了难免坐标不对,UI同学要再调整一下;有的方法则是参考策划的工作流,把UI的编辑过程独立于游戏引擎之外,把界面当做是资源文件导入到项目中,从而实现了UI同学和程序同学作业对象的分离。也有其他的做法,比如对UI设计师和UI程序员的要求更高,双方必须紧密的结对作业,而不是各自分属不同的部门。还有的做法是UI在PhotoShop里面做完了工作后策划来拼界面,把和程序对接的工作交给了策划,从而UI不用考虑上传SVN的问题。还有的做法是逼着UI同学去学写代码,比如至少会用个蓝图啥的,从而避免程序同学染指Prefab。反正各种奇奇怪怪的做法都有,哪种是最好的不好说,要视团队和项目的具体情况而定。以上的种种方法中我个人最倾向的是哪种?如果是UI表现十分重要的游戏,那么提高对UI和程序人才素质的要求,强制其结对作业共同对UI的结果负责,是比较好的解决手段,能够实现质量和性能都比较出色的UI界面,但这样对人才的要求很高(相当于半个TA),大部分的团队不一定能做到,这种情况下另外一个选择是退而求其次,将UI的设计工作交给策划,只保证功能性但不保证美观度,在人力捉襟见肘的小公司/独立团队或功能无限庞杂的大项目中这也是个不错的方法。不管哪种,其本质都是要适应UI工作的特殊性,这不是一个可以简单的“美术vs程序”二元割裂的工作,而是必须有机的结合在一起才能完成的综合性工作,一切想着切割UI和程序的工作流的方案都注定不是最优的。
第二个问题是界面的很多状态和内容应该是以资源驱动为主导还是以代码驱动为主导?比如一个界面默认状态是应该隐藏的,那么是UI同学把隐藏给勾上比较好,还是程序同学在代码里初始化的部分写上强制隐藏好?按照我的倾向性的话,如果不影响性能的前提下,我倾向于用代码来控制。UI资源本身毕竟是静态的,因此凡是动态的东西都应该由代码来控制,这也是为什么多语言文本我觉得写在代码中比挂在界面上要好的原因,因为涉及到多语言,本身就已经不是静态的文本了。
祝我好运吧emmmm,前途未卜,不知道还会踩到多少坑,但总比现在的情况舒服。
(另外别问我为什么我不用I2 Language,因为Godot天下第一)
写在最后:关于服务器文本
所谓的服务器文本,即并列于UI文本和数据文本之外的第三类文本,这种文本常见的是停服公告、发给玩家的邮件、紧急维护通知跑马灯等种种由于具体写什么文本完全无法预估而只能在服务器端设置文本,客户端收到什么就显示什么的文本。这类文本大部分属于运营工具的范畴。
那么这类文本如何实现本地化?
这首先要看游戏服务器的结构,是各个语言的用户都混在一个服务器里面玩?还是每个语言的用户自己有自己的服务器?如果是后者的话那么很简单,每个语言的服务器发对应语言的公告就好了(甚至可能是完全不同的运营商),如果是前者那么这涉及到另外一个问题:客户端能否切换多语言?如果能的话,比如我用英文客户端登录之后,系统发给我的邮件可能是英语写的,这时候我切换成中文客户端重登后,看到的这封邮件是英文的还是中文的呢?确定了这个需求之后,才能设计服务器端文本的多语言是应该如何处理的。
只要提前考虑好,程序做起来都是很容易的。难的都是已经在线上跑了一阵子了又不得不改,这才是最难受的。
网友评论