likes
comments
collection
share

react项目国际化:实现自动装配方案

作者站长头像
站长
· 阅读数 23
该方案提供一个外挂式的前端项目国际化实现方案,可以支持由于某些原因在一开始没有支持国际化,后续在几乎不需要改造原有业务代码的情况下支持国际化。利用构建工具,做到业务开发无感的国际化方案。

在国际化开发过程的流程一般为:前端开发工程师在碰到中文时,需要先设计一个编码,通常为了避免编码重复,还需要符合一定规则且随着业务迭代越来越冗长的编码;然后导入国际化多语言工具函数,调用国际化多语言函数;然后翻译维护国际化配置数据;如果国际化数据是放在数据库中,支持线上动态配置,还需要数据给后端,统一维护在系统。整个过程冗长且需要不同人员协同,极易出现问题。

如使用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