美文网首页
react项目国际化:实现自动装配方案

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

作者: joyer_li | 来源:发表于2022-05-25 10:17 被阅读0次

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

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

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

    相关文章

      网友评论

          本文标题:react项目国际化:实现自动装配方案

          本文链接:https://www.haomeiwen.com/subject/smvcprtx.html