该计划提供一个外挂式的前端我的项目国际化实现计划,能够反对因为某些起因在一开始没有反对国际化,后续在简直不须要革新原有业务代码的状况下反对国际化。利用构建工具,做到业务开发无感的国际化计划。

在国际化开发过程的流程个别为:前端开发工程师在碰到中文时,须要先设计一个编码,通常为了防止编码反复,还须要合乎肯定规定且随着业务迭代越来越简短的编码;而后导入国际化多语言工具函数,调用国际化多语言函数;而后翻译保护国际化配置数据;如果国际化数据是放在数据库中,反对线上动静配置,还须要数据给后端,对立保护在零碎。整个过程简短且须要不同人员协同,极易呈现问题。

如应用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。