共计 4263 个字符,预计需要花费 11 分钟才能阅读完成。
该计划提供一个外挂式的前端我的项目国际化实现计划,能够反对因为某些起因在一开始没有反对国际化,后续在简直不须要革新原有业务代码的状况下反对国际化。利用构建工具,做到业务开发无感的国际化计划。
在国际化开发过程的流程个别为:前端开发工程师在碰到中文时,须要先设计一个编码,通常为了防止编码反复,还须要合乎肯定规定且随着业务迭代越来越简短的编码;而后导入国际化多语言工具函数,调用国际化多语言函数;而后翻译保护国际化配置数据;如果国际化数据是放在数据库中,反对线上动静配置,还须要数据给后端,对立保护在零碎。整个过程简短且须要不同人员协同,极易呈现问题。
如应用 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。