# 国际化的基本原理
假设下面一段代码,要怎么国际化呢?
console.log('你好');
一种常见思路是使用一个translate方法进行翻译,比如:
// translate方法用来把中文翻译为英文
console.log(translate('你好'));
translate方法的实现逻辑也很简单,大致就是拿一个汉英大字典,将中文转换成英文,例如:
// dictionary.js
const dictionary = {
'你好': 'You ok',
};
// index.js
function translate(message, dictionary) {
return dictionary[message];
}
这就是国际化的基本原理.
# 国际化方案 1.0
通过上面的分析我们知道,国际化的重要内容是构造一个汉英大字典,那么如何构造呢?代码里各种字符串,我怎么知道哪些需要翻译呢? 常见的做法是使用特殊的写法将需要翻译的字符串标记出来,然后借助babel等工具对代码做一次预处理,将需要翻译的文本"提取"出来:
console.log(mark('你好')); // 借助babel plugin,我们先将mark对应的文本提取出来,然后将mark偷偷去掉,嘿嘿嘿
这个过程,就叫做extract
当我们提取了对应的文本后,就会得到一个空的汉英字典:
const dictionary = {
'你好': '',
'我好': '',
'大家好': '',
};
下一步就是找翻译人员将字典补充完整:
const dictionary = {
'你好': 'You ok',
'我好': 'I ok',
'大家好': 'We ok',
};
最终的代码大致是这样:
import dictionary from './dictionary.js';
import { mark, translate } from './i18n.js';
console.log(translate(mark('你好'), dictionary));
聪明如你肯定会想到,既然如此,不如把translate和mark合二为一吧,比如这样:
import dictionary from './dictionary.js';
import { t } from './i18n.js';
console.log(t('你好', dictionary)); // mark和translate合成了一个t
甚至dictionary也可以做成配置项,隐式调用,比如:
import dictionary from './dictionary.js';
import { setDictionary, t } from './i18n.js';
setDictionary(dictionary);
console.log(t('你好')); // dictionary隐式生效
看上去不错哦,极大提升了开发效率.不过需要注意的是,单独的mark和translate仍然是有意义的,因为有些翻译场景需要处理的文本是个变量,只能在运行时确定:
mark('男'); // 为了能正常提取待翻译文本,这里先将gender所有可能的值都标记下来
mark('女');
console.log(translate(gender)); // gender是变量,变量只有在runtime才能够确定
# 国际化方案 2.0
上面的方法有个问题,只支持对常量文本的翻译:
str = t('小明喜欢篮球吗?'); // 这能支持常量文本
str = t(`${name}喜欢${sport}吗?`); // 这种是不支持的,想想为什么
如果遇到带变量的文本,我们只能在变量处切割文本,然后对每一小片单独翻译
str = name + t('喜欢') + sport + t('吗?');
不过这样做的效果有可能很差,首先是构造的字典非常琐碎对翻译人员很不友好:
const dictionary = {
'喜欢': 'like',
'吗?': '?', // 翻译人员:臣妾也无能为力啊
}
其次是拼接出来的语序未必符合英文语法,比如上面的例子就会得到:
XiaoMing like basketball?
这显然不是一个好的结果,正常的翻译结果应该是:
Does XiaoMing like basketball?
要解决这个问题,我们的汉英字典就需要升级,要支持嵌套变量,比如:
const dictionary = {
'{name}喜欢{sport}吗?': ''
};
此外我们的字典要支持变量插入:
const dictionary = {
'{name}喜欢{sport}吗?': (values) => { // 字典的value已经不是一个单纯的文本,而是一个可以传入参数的callback方法.
return `Does ${values.name} like ${values.sport}?`;
}
};
// 此时的t还负责执行字典的方法
function t(message, values) {
return dictionary[message](values);
}
// t的用法
t('{name}喜欢{sport}吗?', { name: 'XiaoMing', sport: 'basketball' });
构造新的字典不光是翻译,还要生成对应的callback.这个过程叫compile.
实际情况中可不只是简单的替换变量就行了,有时候还涉及到时态,单复数,序数等变种.那如何统一规则呢?答案是使用统一的ICU format书写message. ICU format就是专门用来解决这个问题存在的,它是一个通用的格式,有一定的语法,就好比markdown一样.使用这种格式构造字典并实现翻译逻辑,就能完美解决我们的问题了!
当然,这个方案有个fallback的小缺陷.
t('你好') // 对中文的翻译,如果字典的翻译缺失,大不了把中文直接展示出来即可(fallback到message)
t('{name}喜欢{sport}吗?', { name: 'XiaoMing', sport: 'basketball' }); // 由于'{name}喜欢{sport}吗?'不是一个合法的string,所以如果字典缺失,fallback到message,只能显示一段'{name}喜欢{sport}吗?'类似的文本.
也就是说,只要有遇到带文本变量嵌套的翻译文本,就必须构造一次字典,以保证待翻译的文本在字典里能够找得到.
# 国际化方案 3.0
让我们在仔细看看我们目前的方案:
str = `${name}喜欢${sport}吗?`; // 国际化以前
str = t('{name}喜欢{sport}吗?', { name, sport }) // 国际化以后
上面的方案虽然解决了问题,但对开发人员很不友好啊,国际化之前和国际化之后的写法也差太多了,而且很不直观.怎么办呢?
答案是借助babel的力量,在编译阶段替我们做一些事情,比如:
str = t(`${name}喜欢${sport}吗?`) // 原始写法
str = t('{name}喜欢{sport}吗?', { name, sport }) // babel-plugin处理后的代码
太棒了,这下我们既能保证开发的友好程度,又能做到国际化了! 但仔细想想,这个bebel-plugin该怎么写呢?且听下回分解.
# 总结
- 国际化就是查字典
- mark收集(预处理阶段),translate翻译(运行时阶段)
- ICU format & 优化的babel-plugin写法