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

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

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351

推荐阅读更多精彩内容