分类
Javascript

批量提取多语言词条

Vue I18n解决方案肯定是可行的,但快速实施还是有难度。历史原因,可能我们的前端项目一开始根本没有考虑要国际化的问题。项目后期再搞国际化。页面很多,逐个文件去提取词条再逐个替换,体力活呀,一天做100个应该所快的了。而且机械操作难免出错。能做个批量处理来解放大家的双手吗?具有强大工具能力的NodeJs登场了。

我们的目标是将工程中所有的JS和Vue文件中的中文词条提取出来。思路是这样的。使用Nodejs遍历读取每个JS和Vue文件,然后逐行读取每个文件,用正则表达式匹配中文词组,再将匹配的词组逐行写入一个文本文件。词组的前面按规则要加词条键值。为了方便,我们以KEY1、KEY2…KEYN来固定位置,后期可以手动改为有意义的键值。熟悉Node的fs模块API的话,脚本不难写。有几个要注意的地方:

一、文件过滤

需要过滤掉JS和Vue之外的文件。一种方法是读取分析文件后缀名来过滤。另外也可以根据目录名来判断。前提是项目的目录命名很规范。比如员代码都放src目录下,测试代码放test目录下,资源都放在assets目录下,样式都放styles目录下。有了这个约束代码也就好写了,而且可以看出目录名过滤运行效率更高。

// 排除目录
const excluded = ['assets', 'styles', 'css', 'i18n']
if (excluded.includes(path.basename(filePath))) return;

二、去除stylel内容

Vue文件style标签中的文本直接干掉。放在第一个处理出于性能考虑。

匹配style
/[\s\S]*?<\/style>/g

三、去除代码中的注释

也就是不能提取注释中的中文词组。逐行读取前要去掉注释行,这样后续处理性能也更高效。这一步很关键,要用到比较复杂的正则表达式。

1) 匹配htm注释

/<!--[\s\S]*?-->/g

2) 匹配/**/和//注释

/\/\/.*|\s*\/\*[\s\S]*?\*\/\s*/g

难理解的话,可以分解成两个正则表达式。相当于

匹配//注释

/\/\/[\s\S].*?/g

匹配/**/注释

/\*\*([\s\S]*?)\*\/g

四、去除console输出

console输出的中文词组也要干掉,这个正则表达式不难:

/\*\*([\s\S]*?)\*\/g

五、词条不重复

es6可以使用使用Set来去重。

const content = 'export default {\n\t' +
  [...set].map((item, index) => `KEY${index + 1}: '${item}'`).join(',\n\t') +
  '\n}'

六、排除已有的词条

如果要多次提取,还要考虑筛选已有的词条。完整i18n-generator.js脚本如下:

const fs = require('fs');
const path = require('path');
// 输出文件名
const outPutFile = 'i18n.js';
// 需要遍历的文件目录
const filePath = path.resolve('src');
// 多语言文件目录
const ZHPath = path.resolve(filePath, 'locales/zh-CN.js');
let values = [];
// 去重
let set = new Set();
// 同步读取已有的词条
try {
  const data = fs.readFileSync(path.resolve(ZHPath));
  data.toString().split(/\r|\n/g).forEach(line => {
    if (/.+:\s'[\u4e00-\u9fa5]+/g.test(line)) {
      const result = line.replace(/[',]/g, '')
        .replace(/{\d}/g, '').split(':')
      if (result)
        values.push(result[1].trim());
    }
  })
} catch (e) {
  console.log(e);
}
console.info('开始生成词条')
seek(filePath);
console.info('生成词条完毕')
// 调用文件遍历方法
function seek(filePath) {
  // 排除目录
  const excluded = ['assets', 'styles', 'css', 'i18n']
  if (excluded.includes(path.basename(filePath))) return;
  //根据文件路径读取文件,返回文件列表
  const files = fs.readdirSync(filePath)
  // 遍历读取到的文件列表
  files.forEach(filename => {
    // 获取当前文件的绝对路径
    const filedir = path.join(filePath, filename);
    // 根据文件路径获取文件信息
    const stats = fs.statSync(filedir)
    // 是文件
    const isFile = stats.isFile();
    // 是文件夹
    const isDir = stats.isDirectory();
    if (isFile) {
      try {
        const data = fs.readFileSync(filedir, 'utf8');
        const lines = data.toString()
          .replace(/<style.*?>[\s\S]*?<\/style>/g, '') // 匹配style
          .replace(/<!--[\s\S]*?-->/g, '') // 匹配html注释
          .replace(/\/\/.*|\s*\/\*[\s\S]*?\*\/\s*/g, '') // 匹配/**/的注释
          .replace(/console[\s\S]+?\);?/g, '') // 匹配console注释
          .split(/\r|\n/);  // 按回车分割成字符串数组
        lines.forEach(line => {
          const str = line.trim();
          if (!str) return;
          // 保留中文、常用标点符号
          const result = str.match(/[\u4e00-\u9fa5?!、,。“”]+/g);
          if (Array.isArray(result)) {
            result.forEach(value => {
              // 同步筛选已有的词条及标点词条
              if (!values.includes(value) && /[\u4e00-\u9fa5]+/.test(value)) {
                set.add(value);
              }
            })
          }
        })
        const content = 'export default {\n\t' +
          [...set].map((item, index) => `KEY${index + 1}: '${item}'`).join(',\n\t') +
          '\n}'
        try {
          fs.writeFileSync(outPutFile, content, { flags: 'w+' });
        } catch (e) {
          console.log(e);
        }
      } catch (e) {
        console.log(e);
      }
    }
    if (isDir) {
      // 递归:如果是文件夹,就遍历该文件夹下面的文件
      seek(filedir);
    }
  })
}

最终导出的词条如下:

export default {
  KEY1: '主页',
  KEY2: '产品',
  KEY3: '意见反馈',
  KEY4: '联系我们',
  KEY5: '菜单',
  KEY6: '面包屑'
  .....
}

注意:我们使用了同步的方式操作文件,牺牲了部分性能。但使用异步方式的话,不能保证每次返回的结果是一样的,也就无法做后续的持续提取替换工作。另外,KEYN作为键值还是可行的。有事,我们为了追求完美,但耗去了更多时间

将i18n-generator.js放在项目根目录下,执行node i18n-generator就生成src下所有的多语言词条。当然,我们也可以修改filePath变量值来生成特定目录下所有词条。如果更灵活,可以使用Interface类来编写一个cli工具。

最后就是替换的事了,写脚本不好实现。因为我们不知道该中文词组是template中的还是scriot中的,而且,使用了占位或参数的词条是很难用脚本处理的。所以建议用IDE或编辑器的正则表达式查找替换功能就行了。

小结:所以说工程化思想在前端开发架构中越来越重要,用好了可以大大提高我们开发人员的生产率。