由于历史原因,很多老项目都是中文硬编码的,所以改造的大部分工作就是需要将这些 中文文案 替换成 i18n(key)
。在此之前,都是人力完成这些重复的工作,不仅低效,开发人员的积极性也不高,因此自动化方案成为后续项目国际化改造的不二之选。
auto-translate-converter 是这次自动化方案实践的产物,主要用来批量替换代码中的中文为 i18n(key)
。不过还有很多待优化完善的地方,仅供参考。
目前国际化改造的自动方案分为三个步骤:
- 提取出项目代码中所有的中文,生成对应的美杜莎key,最终生成xlsx(自动)
- 将xlsx交给业务方完成翻译文案(手动)
- 根据最后修改的xlsx,把代码中的中文替换成我们想要的i18n(key)(自动)
其中做的好处有:
- 将文案的翻译自由交给业务方,使翻译内容更贴合业务场景
- 将文案的key的修改自由交给前端同学,更方便管理语义化的变量名
- 把有迹可循的重复工作交给机器
因此我们的工具 auto-translate-converter
只需要两条命令
atc build
对应第一步骤atc replace
对应第三步骤
子曰:工欲善其事,必先利其器。 因此合理的工具选型也是必要的。 此次方案使用的主要类库如下:
- node-glob ———— 遍历项目文件
- babylon ———— 将代码解析(parse)成AST,作为babel背后的解析器,各种新语法都可以支持。
- recast ———— 遍历和修改AST,基于ast-types,也是ast-types作者的项目
- node-xlsx ———— 解析和生成xlsx
- pinyin ———— 将中文转成拼音,生成唯一key
工具都准备好了,就看如何来实现了
1.使用 glob
来遍历项目文件,fs.readFileSync
来读取文件代码
PS:
glob
只支持单一文件后缀,因此需要根据自己想获取的文件类型,来遍历出不同类型的文件
2.使用 recast
和 babylon
将读取的代码解析成AST,查询所有的字符串节点
- 在这里使用的是
recast
的parse
方法来解析,虽然recast默认使用esprima
来解析,不过它也支持自定义解析器,这样我们还是使用的babylon
作为解析器
const babylon = require('babylon');
const recast = require('recast');
const parseOptions = {
parser: {
parse: (source) => {
return babylon.parse(source, config.parseOpts)
}
}
};
let ast;
try {
ast = recast.parse(code, parseOptions);
}catch(e) {
throw new Error('file parse ast error');
}
recast
基于ast-types
,支持访问各种类型节点,此次方案主要查找了Literal
(字符串字面量),JSXText
(JSX文本),TemplateElement
(es6模版字符串)
visitAST(ast, cb) {
recast.visit(ast, {
visitLiteral: (path) => {
const v = path.node.value;
cb(path, v);
this.traverse(path);
},
visitJSXText: function(path){
const v = path.node.value;
cb(path, v);
this.traverse(path);
},
visitTemplateElement: function(path) {
const v = path.node.value.raw;
cb(path, v);
this.traverse(path);
}
})
}
3.根据正则表达式匹配出中文的节点,并生成该文案的唯一key。
- 根据这些字符串节点返回的值,来正则匹配是否为中文,匹配中文的正则表达式已根据unicode block list整理出来了
/[\u4E00-\u9FCC\u3400-\u4DB5\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\ud840-\ud868][\udc00-\udfff]|\ud869[\udc00-\uded6\udf00-\udfff]|[\ud86a-\ud86c][\udc00-\udfff]|\ud86d[\udc00-\udf34\udf40-\udfff]|\ud86e[\udc00-\udc1d]/
不过在使用正则时要慎用全局模式,是由于全局模式的正则表达式有个属性
lastIndex
, 用来表示上一次匹配文本之后的第一个字符的位置,若上次匹配的结果是由test()
或exec()
找到的,它们都以 lastIndex 属性所指的位置作为下次检索的起始点。而我们在每次匹配时,只需要lastIndex
都从0开始即可,所以可以不用全局模式,详情可见理解正则表达式的全局匹配
-
在遍历获取中文字符串时,同时也可以获取到该字符串所有文件路径,将该路径用
.
连接,即为key的前半部分,后半部分为该文案的拼音,但是有的文案属于一句话,全部提取会导致key太长。因此为了让后半部分的key唯一,采用如下策略:- 默认取字符串前两个字的拼音用
_
连接 - 将一个文件中所有的字符串按照字符数升序排列
- 若发现该文件中某个词的前两个字在已转的拼音中有重复的
- 若该词为谐音词,长度跟之前的相同,则在已转拼音后添加递增数字
- 若该词长度足够,则继续往后取两个字的拼音
效果如下
// 项目名称 autoTranslate-test // 文件路径 autoTranslate-test/src/pages/demo/Demo.jsx autoTranslate-test.pages.demo.ce_shi 测试 autoTranslate-test.pages.demo.ce_shi1 侧视 autoTranslate-test.pages.demo.ce_shi_shu_ju 测试数据
当然,如果不怕麻烦的话,也可以交给前端同学自定义key的后半部分,后续在auto-translate-conveter的配置文件中会讲到
- 默认取字符串前两个字的拼音用
4.将上面获取的文案和key拼成一个二维数组,生成xlsx 解析美杜莎的xlsx模版,将内容替换成我们从项目中生成的二维数组,再生成我们想要的xlsx,就像这样:
我们的国际化方案是使用i18n-helper,因此我们在项目使用自定义方法 i18n(key)
来获取美杜莎的文案。
1.首先在每个目录下,我们需要判断页面中有没有引入或者定义了i18n,没有引入的文件需要引入
let i18mImport = [];
recast.visit(ast, {
visitVariableDeclarator: function(path) {
if (path.node.id.name === 'i18n') {
i18nImport.push(path);
}
this.traverse(path);
},
visitImportDeclaration: function(path) {
if (path.node.source.value === 'i18n') {
i18nImport.push(path);
}
this.traverse(path);
}
})
if (i18mImport.length === 0) {
在头部引入i18n
}
2.根据最后更新的xlsx生成Map,然后在文件遍历时,若字符串在Map中存在,则把字符串替换成对应的i18n(key)
const recast = require('recast');
const n = recast.types.namedTypes;
const b = recast.types.builders;
const chnMap = {};
/**
* 将xlsx的数据转成chnMap
* chnMap
* {
* "测试": "autoTranslate-test.pages.demo.ce_shi"
* }
*/
visitAST(ast, (path, value) => {
if (value 在 chnMap 中存在) {
const i18nCall = b.callExpression(
b.identifier('i18n'),
[b.literal(key)]
);
const parentType = path.parentPath.node.type;
if (若父节点类型为SwitchCase || (父节点类型为ObjectProperty,且当前节点类型是key) {
//提醒用户,会语法出错,请手动修改实现方式
} else if (若该节点在JSX中或者JSX组件的属性中) {
// 则需要在i18nCall外面再包一层{}
path.replace(b.jsxExpressionContainer(i18nCall));
}else {
// 则直接替换
path.replace(i18nCall);
}
}
})
这里需要注意的一点是:在之前的项目中有很多在
Switch case
或者在Object
的key
用中文硬编码,若是替换成我们定义的i18n(key)
就会造成语法错误,在这里只能将错误信息提示给前端同学,手动修改逻辑代码的实现方式。
这样我们就完成了针对中文字符串的精准替换。
由于本次方案的工具 auto-translate-converter
是面向前端同学使用,那自然需要具备易上手,灵活配置的特性。
- 安装
npm install autotranslate -g
- 命令行的使用
// 以atc build为例,replace同理
atc build // 默认在根目录执行
atc build src/page/demo // 支持指定执行目录
- 配置文件
开发者可以在项目根目录下添加一个文件atc.config.js
进行配置, 输出内容如下:
{
root: './src', // 项目遍历的根目录
ignore: ['app', 'i18n', 'images', 'lib', 'util'], // 想忽略的文件目录
basename: ['js', 'jsx'], // 想遍历的文件类型
parseOpts: {}, // 自定义 babylon.parse(code, [options]),详情见https://github.com/benjamn/recast/blob/master/lib/options.js
printOpts: {}, // 自定义recast.print(code, [options], 详情见 https://github.com/benjamn/recast/blob/master/lib/options.js)
prefix: process.cwd().split('/').pop(), // key的前半部分的前缀,默认使用项目目录名
autoKey: true, // 是否自动生成完整key,false的话只会生成前半部分的路径key,后面可以自己在xlsx添加
}
用户自定义的配置项将会直接覆盖初始默认项,使用的Object.assign
这样使用上手很方便,也拥有一定灵活的配置。
这次写工具的经历,不仅仅是在技术方面的广度有所提升,还让我从产品的视角去分析考虑,不光只埋头于代码实现,也要关注流程优化,用户体验。
目前 auto-translate-converter
的默认配置只针对基于 nowa
的项目使用,当然还有一些配置也有待更新,比如自定义i18n方法。
如果您有比较好的建议和想法,欢迎提issue或者PR。