批量提取多语言词条

批量提取多语言词条

Flying
2022-02-25 / 0 评论 / 187 阅读 / 正在检测是否收录...

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

regex.svg

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

文件过滤

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

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

去除 stylel 内容

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

匹配 style

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

去除代码中的注释

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

  1. 匹配 htm 注释
/<!--[\s\S]*?-->/g
  1. 匹配 /**/ 和 // 注释
/\/\/.*|\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) &amp;&amp; /[\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 中的还是 script 中的,而且,使用了占位或参数的词条是很难用脚本处理的。所以建议用 IDE 或编辑器的正则表达式查找替换功能就行了。

注意:JavaScript 和 Vue 文件之间,Vue 文件内部模板和脚本之间对应的 i18n 语法是不一样的

总结

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

2

评论 (0)

取消