国际化学到的东西

2019/10/6 i18n,javascript

记录一下最近一段时间做的前端国际化遇到的问题。

# 挑选一款好用的国际化库

国际化必须的几个过程是:构造字典、翻译、字符串替换,任何翻译库都需要解决这几个核心问题。所以可以对比这几个过程的写法来提供一些参考,API越简单越好,自动化程度越高越好,代码侵入性越低越好。

首先说说API越简单越好,这个很容易理解,API越多,用法越复杂,开发者越懵逼,越容易产生不当使用。

自动化程度越高越好,典型的场景是字典的构造,如果是手动构造字典,那这个成本将会是巨大的,所以正常做法应该是有脚本自动分析代码extract出待翻译的字典,以及处理好后续翻译内容的update。如果国际化库不带这个功能,建议你换一个,或者自己撸一套cli工具。

代码侵入性越小越好,这主要指的是像linguijs这个框架会引入自己的babel-plugin,从语法层面魔改了js,虽然极大简化了手写代码的工作量,但是让库的切换,或者升级babel版本变得更加困难。

# 国际化与禁止用中文做状态码

代码规范:禁止用中文做状态码

现在考考你,上面这条规范的意义是什么?

很早以前我并不理解为什么要有这样一个规定。因为从编译器或runtime的角度来说,中文和英文只不过是不同unicode的编码罢了。既然都是string,凭什么英文可以做状态码,中文不可以呢?而且中文的可读性更好,表达能力更强,用中文做状态码连注释都省了。

后来接触javascript多了,明白javascript对非BMP字符的处理能力有缺陷,这似乎可以作为一个理由,但那只是javascript引擎的问题,可这个编程规范似乎对各种编程语言都适用,所以一定还有什么别的原因。

一直到最近做国际化的东西,我才突然明白这个规则的意义。这个规则其实有很多场景下的变种,他们本质上都是想表达“逻辑与展示应当分离”这个意思。到底什么意思呢?举个例子:

if (status === '成功') { // 这里是逻辑
  console.log(status); // 这里是展示
}

这是一个展示与逻辑混在一起的例子,代码中status既参与了逻辑(if条件)又参与了展示(conosle.log)。这种做法就是属于不好的例子。假如我们想修改一下文案,把status的值从成功换成通过,为了保证逻辑正确,就同时也要修改逻辑部分,把if条件也改了。

正确的做法是“展示与逻辑分离”,比如:

if (status === STATUS_SUCCESS) { // 这里是逻辑
  console.log('成功'); // 这里是展示
}

这样,如果要改文案,只需要将console.log处的文本变一下就行了。

当然这只是一个非常简单的例子,实际代码可能有非常多的变种,但核心思想都是一样的的,即:“展示与逻辑分离”。那这与不能用中文做状态码有什么关系呢?这是因为如果中文做了状态码,那么这种代码会“诱惑”开发者写出展示与逻辑混合在一起的代码,但如果状态码是英文或数字,那么开发者就没有选择把展示和逻辑分开写。比如上面的例子,如果status的取值只可能是0,1之类的数字,那肯定不会不会出现混合的代码了。

现在说回国际化,国际化需要对所有展示的部分做修改,如果代码是展示与逻辑分离的,那么我们只需要改动展示的部分即可,但如果代码是展示与逻辑混在一起的,那很不幸,要该的地方就非常不可控了。

# 好的eslint规则能使国际化效率提升非常非常多

对大型项目做国际化最头疼的是找出那些“未国际化”的地方,比如:

function doSomething(thing) {
  ...
  console.log('成功!'); // <-- 这里是需要国际化的地方
}

一个一个文件找效率太低了,而且极易出现遗漏,所以你需要一个工具能帮你快速找出哪些地方没有国际化。eslint规则恰好可以用来做这个事情。

那什么样的代码算是需要国际化的呢?一个简单的规则是:带有中文的地方。所以你的eslint规则甚至可以单纯用正则匹配中文即可。

甚至可以再写一个vscode插件与elisnt规则一起配合做一些事情。

# pseudo-translation意义不大

做国际化之前看过一些文章,里面提到了pseudo-translation这种技术,大致原理是使用一些自动化的方法将代码中的文本替换为一些待翻译的语言字符,用来快速检测UI是否可能需要调整。

为什么会有这个过程呢?这主要是因为翻译文本需要专业的翻译团队,而高质量的翻译需要时间(包括翻译、专业人士审校等流程),比如5000句话可能要翻译3周时间。如果等翻译完成了再检查UI,周期太长,为了能并行翻译和UI调整两个动作,就需要一种快速“翻译”的方式,这种方式就是pseudo-translation。

一开始,我写了一些自动化脚本将中文替换成了对应的拼音(毕竟拼音也算是要英文字符),勉强实现pseudo-translation的效果。效果嘛自然差强人意,只能说聊胜于无。

后来,受钉钉自动翻译功能的启发,我把拼音改成了调用百度翻译API得到机器翻译的英文。这下像那么回事了,别说有些地方翻译的效果还不错,我也颇得意了一阵子。

再后来有一天,从同事那里偶然得知chrome浏览器的自动翻译功能,可以完全自动翻译页面,不仅是自动的,而且效果还更好,我晕,早知道直接这样翻译得了,要什么pseudo-translation,甚至国际化工作都不需要做了。

然而更大的打击还在后头,等我做完了pseudo-translation,准备让设计团队看看UI如何调整,没想到设计团队对这个行为表现出了强烈的抵触。因为在他们看来,这种pseudo-translation并不是最终的翻译文案,即使现在检查一遍UI并做一些调整,等真正的翻译给出来以后,还要再检查一遍,相当于要做两遍一样的的事情,所以他们不同意。于是pseudo-translation自始至终都没有真正推行过。

总之,pseudo-translation意义不大,如果真的需要,用chrome浏览器自动翻译就行了(捂脸)。

# 中文做源语言,有好有坏

假如国际化是从A语言翻译到B语言,那么我们称A语言为源语言,B语言为目标语言。

中文的语法非常简洁,没有大小写、时态、名词单复数、序数、语序这些乱七八糟的玩意儿,在做源语言的时候反倒有时候会带来一些麻烦。举个例子,“申请”有两种意思,一种是动词,一种是名词,而中文都是一个词,英文却是两个词(apply和application)。假设我们是从英文翻译到中文,那没问题,无论是哪种情况都能准确翻译,但是从中文翻译到英文就必须依靠上下文环境才能区分了,有时候遇到没有上下文环境的情况,确实比较麻烦。

所谓:由俭入奢易,由奢入俭难,讲的正是这个道理。

但是,中文做源语言也无形中带来了一个好处,什么呢?就是待翻译的地方极其明显。我们基本只要把代码中所有出现中文的地方过滤一遍就可以了。因为代码是基于英文字母的,中文无形中被突出了。假如我们是英文做源语言,如何区分哪些地方应该翻译将会是一件比较麻烦的事情,哈哈哈哈。

# 精简待翻译的文本

在国际化的过程中,我发现我们的代码有很多这样的待翻译的内容:

创建申请成功
添加部门成功
移动阶段成功
安排面试成功
...

这些文本都是用来描述操作状态的,其实他们想表达的意思都是“操作成功了”,但每个地方的文本都不一样,都需要独立翻译,且增大了翻译包的体积。在不影响用户体验的情况下,这些文本其实都可以简化成————操作成功。

# 国际化带来的UI调整非常多

我知道国际化可能会让某些UI的样式出现问题,但没想到会有这么多问题。我们翻译了5000处文本,UI上需要调整的问题大大小小报了超过200个,远超预期,所以后续还花了很多时间做UI的修复。

这些问题也暴露出来了一些CSS样式的书写缺陷,比如有些地方写得太死,宽度高度固定,不能很好兼容到不同的文本长度。还有些地方使用了line-height做垂直居中,结果文字长度增加后变成两行,行间距变得极其诡异。

总之各种错位、溢出问题。

# 翻译文本也需要优化

国际化翻译的过程是这样的,提取出代码中需要翻译的地方,然后将这些待翻译的内容交给翻译团队做。这里有个问题,那就是翻译团队拿到的是纯文本,没有上下文和UI信息,所以即使是歪果仁翻译的可能出现不准确的情况。

举个例子,我们要翻译这么一段文本“推荐人于XXX时间操作”,这个推荐人是什么意思?结合业务看,是指内推人还是猎头?实际上两者都不是,它指的是“推荐到其他职位”的那个操作人,也就是HR。这种例子从实际情况上看,虽然出现的频率不高,但是林子大了什么鸟都有,基数大了出现的次数也不少。而且这种例子只能通过人工一个一个校正。

说到校正,这里还有一个问题,即翻译文本如何对应UI。我们在拿到翻译文本的时候可以一并拿到这个文本所在的代码文件,对于开发人员这来说,这个信息基本可以确定这个文本所属的业务逻辑以及UI,但是对于非开发人员这来说,这个信息没用,因为他们不理解代码。不幸的是,负责校正翻译文案的人往往不是开发人员,所以校正工作本身就好像蒙住了眼睛一样,无法高效且准确地完成。

至于这个问题如何克服,目前还没有找到合适的方法。

# babel-plugin的坑(linguijs)

这一点就与具体的国际化实现方式有关了,我们用的是lingui-js这个库做国际化的,这个库会先用babel-plugin对代码做一次预处理。将一些国际化的简写替换成完整写法,例如:

// 源代码
i18n.t`${name}喜欢${sport}`

// transform后的结果
i18n._(`{0}喜欢{1}`, { name, sport });

这本是一个方便开发者的特性,但某些场景下遇到了严重的问题。因为做这个transform的babel-plugin与es6-plugin产生了一些微妙的不好的化学反应。

这是因为babel在做transform的时候,只会遍历一次ATS,然后针对每个节点依次应用各种plugin。假如现在有两个plugin,一个处理专门处理A类型的节点,一个会把B类型的节点转换成A类型的节点。如果babel先遍历了B类型的节点,再遍历A类型的节点,那么就没有问题,但假如babel先遍历了A类型的节点(第一个plugin生效了),然后遍历B类型的节点(第二个plugin把B类型转换成了A类型),因为A类型的节点已经遍历过了,不会再生效了,所以这个A类型的节点将不会被第一个plugin处理。结果就得到了错误的转换结果。

针对这个问题,其实没有什么解决办法,除非修改babel的执行逻辑,但这基本是不可能的事情,而且这种问题通常只有在运行时才会暴露,非常难发现。

唯一的解决办法就是不要用babel-plugin,babel-plugin越少越好。

# 支持局部翻译,做好灰度测试

国际化不是一个小工程,它对整个系统带来的改变是非常巨大的。如果指望国际化全部翻译完成再上线,回归范围将变得无比庞大,风险是不可控的。

为了控制风险,我们必须一小部分一小部分地实现国际化,小步快跑。改一部分测一部分再上线一部分。为了做到这一点,必须要有灰度发布的能力,不完整的国际化UI仅对测试账号开放,方便测试。等所有国际化工作完成之后在正式开放给其他用户。

其实对于代码重构也是一样,不要想着翻天覆地的大重构,你要考虑测试同事们的感受。

# 国际化可以用来检测CSS水平

这算是国际化带来的一个启发吧,如果你想面试前端工程师,或者想考察前端工程师的CSS水平,那就让他写一个稍微复杂点的页面吧,然后用chrome的翻译功能转换成英文,如果页面样式乱得一塌糊涂,说明CSS写得不够灵活,反之说明CSS水平高,哈哈哈哈。

Designed by Lishunyang | All right reserved