在国际化开发过程的流程一般为:前端开发工程师在碰到中文时,需要先设计一个编码,通常为了避免编码重复,还需要符合一定规则且随着业务迭代越来越冗长的编码;然后导入国际化多语言工具函数,调用国际化多语言函数;然后翻译维护国际化配置数据;如果国际化数据是放在数据库中,支持线上动态配置,还需要数据给后端,统一维护在系统。整个过程冗长且需要不同人员协同,极易出现问题。
如使用react-intl-universal支持国际化:
import intl from 'react-intl-universal';
// 初始化代码在整个系统的入口文件时。
intl.get('SIMPLE').d('简单');
假设开发一个前端转译工具,在碰到代码中的中文时,自动导入国际化的工具函数,自动按照一定的规则生成编码,将原来的中文代码替换为国际化函数的调用,然后在整个项目编译后,收集所有的国际化语言数据,可以直接生成国际化语言的配置文件也好,或者生成一定的结构化数据用于插入数据库。
按照这个思路,就可以实现一个为项目自动装配国际化的方案。在该方案中,前端开发工程师开发时无需关注国际化,获取跟不需要国际化支持的项目一样的开发体验,可以将精力更多的放在业务开发上。同样该方案为一个基础支撑,挂载式的形式,能够快速支持一个开始不支持国际化,后来因为发展,需要面向国际的项目。
同样,这个方案着重点是如何自动生成国际化多语言函数的调用代码,对使用某个国际化框架是没有限制的。可以根据实际需求,选择任何国际化框架,然后对它的使用进行代码转换。
该方案只针对简单的国际化需求,对于一些复杂的需求,如金额,日期等,还是需要手动使用一些国际化框架的api。但一个项目中,最多的应该还是对于一些简单的展示文本进行国际化支持。
从方案的设计来看,主要是分为两部分:
- 分析代码:当碰到中文时,转译为国际化函数调用语句。
- 收集信息:将分析代码过程中的转换语句的信息收集起来,用于生成配置数据。
两个部分分别用两个工具去处理。
代码分析工具
分析代码可以实现一个babel插件,在转译js代码时进行中文国际化处理。
中文文本,主要是字符串或在模版字符串中,所以只需要对这两种语句进行解析转化即可,也就在babel插件需要处理StringLiteral和TemplateLiteral语句即可。
那么插件的主要结构为:
module.exports = (babel) => {
visitor: {
StringLiteral(path, state) {
},
TemplateLiteral: {
enter(_path, state) {
},
},
},
};
TemplateLiteral
处理起来比较复杂,所以以StringLiteral
为例说明关键逻辑。在StringLiteral
语句中分析字符串是否包含中文,用正则判断:
StringLiteral(path, state) {
const { node } = path;
const text = node.value;
if (str.search(/[^\x00-\xff]/) === -1) {
return;
}
},
如果不包含中文,则直接返回不处理。如果包含中文,则转换为国际化导入函数(以react-intl-universal
库的使用方式):
const intlMember = t.memberExpression(
t.identifier('intl'),
t.identifier('get'),
false, false,
);
// 编码生成,这里直接用中文作为编码。如果怕乱码等问题,
// 可以采用md5码或者或者根据实际规则和文件路径生成编码
const codeText = text;
const codeTextNode = t.stringLiteral(codeText);
// 解决
codeTextNode.extra = {
rawValue: codeText,
raw: `'${codeText.split("'").join('\\\'').split('\n').join('\\\n')}'`,
};
const intlCall = t.callExpression(intlMember, [codeTextNode]);
const memberExpression = t.memberExpression(intlCall, t.identifier('d'), false, false);
let fnNode = t.callExpression(memberExpression, [node]);
const parentNode = _.get(path, 'parentPath.node');
if (t.isJSXAttribute(parentNode)) {
fnNode = t.jsxExpressionContainer(fnNode);
}
path.replaceWith(fnNode);
这样对于文本
const text = '中文中文';
会转化为:
const test = intl.get('中文').d('中文');
上文中,对于intl是硬编码,且是直接使用,需要依赖入口文件将intl
函数放入全局对象中:
import intl from 'react-intl-universal';
window.intl = intl;
但为了更高的扩展性,可以用代码自动导入,在转换代码前,先进行国际化多语言函数的导入:
const node = addDefault(path, 'react-intl-universal', { nameHint: 'intl' });
const intlLibName = node.name;
const intlMember = t.memberExpression(
intlLibName,
t.identifier('get'),
false, false,
);
babel工具库@babel/helper-module-imports
中的addDefault
函数,可以生成一个默认导入组件库的语句,并且不会跟其他的变量产生命名冲突。
如果用的是其他的库,可以修改对应的生成导入语句的方式。
这样对于上面的文本会转为:
import intl from 'react-intl-universal';
const test = intl.get('中文').d('中文');
如果当前文件已经有手动导入了,如:
import intl from 'react-intl-universal';
const test = intl.get('code').d('已有文本');
const text = '中文';
将会转换为:
import intl from 'react-intl-universal';
const test = intl.get('code').d('已有文本');
const text = intl.get('中文').d('中文');;
这样已经实现核心的代码,将处理的信息保存下来,方便后续收集:
module.exports = (babel) => {
const records = new Map();
visitor: {
Program: {
enter(_1, state) {
records.clear();
},
exit(_1, state) {
const { filename: filePath } = state;
const _records = Array.from(records);
// 保存数据
records.clear();
},
},
StringLiteral(path, state) {
// 转换代码
records.set(codeText, text);
},
},
};
保存数据需要特殊处理,因为webpack4或者5中,一般都会使用多进程的方式构建,所以不能简单的放在内存中,可以放在文件系统中。且还需要解决多进程进行操作文件的锁问题,可以解析的每一个js文件的信息都放在一个单独的文件,或者同一个进程的信息放在一个文件中,避免锁竞争。
对于TemplateLiteral
的处理,由于模版中可能极其复杂,多种文本变量间隔,并且可能还内嵌了其他字符串或者模版语言,都需要特殊处理,如对于有模版语句:
const hasChineseTemplate = `中文${someVars}中文中文${1 + 1 + '你好'}哈哈哈`;
推荐两种处理方式:
全模版替换为国际化函数为:
const hasChineseTemplate = intl.get('code').d(`中文${someVars}中文中文${1+1 + '你好'}哈哈哈`);
模版中单独的项处理为国际化函数:
const hasChineseTemplate = `${intl.get('code1').d('中文')${someVars}}${intl.get('code2').d('中文中文')${1+1 + intl.get('code3').d('你好')}${intl.get('code4').d('哈哈哈')}`
在实际实现过程中,还需要处理重复解析的问题。由于babel的架构和该插件基本会在最先执行,当后续的插件进行转换后,可能还会触发该插件重新启用,就会对实际意义上是同一个语句进行重复解析,需要一个机制进行处理后的标记。
我已经实现了一个工具库 babel-plugin-i18n-chinese。在这个工具已经在线上运行了一年,解决了一些常见问题和尽可能的提供更多的扩展性。
信息收集工具
收集代码的工具,可以用一个生效于编译后的webpack插件。
信息收集工具需要处理的功能比较简单,根据代码分析工具存储国际化数据的方式,获取数据,然后生成根据实际需求数据文件。
唯一需要注意的是,该webpack插件需要在编译后生效,也就是需要这样注册插件:
module.exports = class AutoI18NWebpackPlugin {
apply(_compiler) {
const compiler = _compiler;
compiler.hooks.done.tapPromise(this.constructor.name, async () => {
// 获取分析工具生成的数据
// 输出数据文件
});
},
}
团队已经实现了对应的插件webpack-plugin-i18n-chinese。