项目实战 - 国际化之jscodeshift用AST提取中文

1,474 阅读3分钟

系列

项目实战 - 国际化之规范

项目实战 - 国际化之AST提取中文

项目实战 - 国际化之基于Power Tools开发vscode代码提示插件

为什么要用AST提取中文?

项目中大概有2万处需要被替换,人工耗时耗力,采用AST自动处理(库jscodeshift),然后英文纠错快速处理,高效快捷又可重复,过程如下:项目源码

1、AST 匹配处中文,输出到txt文件中,并记录对应文件,行,列等信息

2、复制txt到百度或谷歌excel中,自动翻译出英文,于是就有了中英对照文档

3、用英文翻译,根据上述UKey规则,生成中文的UKey

4、根据中文和UKey的对照文档,替换源代码文件中响应的中文 如

5、剩余组织人力,纠错即可

recast 库介绍

recast 库是主角,拆-换-合都是这个库的方法,也有在线网站 astexplorer 方便查看,类型选recast

image.png

recast.parserecast.print成对

recast.parse 是将代码拆分称 AST 语法书结构,recast.print是将 AST 语法树转换称代码

展开 AST 解析

image.png

1、左上角是原始函数 HelloWorld,做了 a + b 运算

2、右上角是经过 recast.parse 后得到的 AST 语法树,注意Identifier是函数名,对应visit的是visitIdentifier

3、左下角在 visit 中修改函数名,使得函数名倒叙,必须是 return false 或者 return 替换的字符串 或者选择以下写法 this.traverse(path)

调试时,如果你想输出AST对象,可以console.log(node);如果你想输出AST对象对应的源码,可以printSource(node)

recast.visit(ast, {
      visitExpressionStatement: function(path) {
        const node = path.node
        printSource(node)
        this.traverse(path)
      }
    })

4、右下角是 recast.print 生成的代码,可以看到输入的函数名HelloWorld被修改为倒叙dlroWolleH

recast.visit 修改源代码

上述的提到 recast.visit 使用 visitIdentifier 修改源代码,下面列常用的几个 visit 类型,更多查看 JavaScript 常见 AST 梳理

visitIdentifier 标识符

visitStringLiteral 字符串

visitTemplateElement 模板字符串

visitJSXText JSX文本 <h1>你好</h1>

visitObjectProperty 对象属性

visitCallExpression 函数调用 if(obj.func('中文'))....

visitLogicalExpression 复杂逻辑表达式

visitBinaryExpression 处理运算符

visitTSEnumMember 处理枚举enum {success = '成功'}

visitThrowStatement 处理异常throw new Error('错误')

visitTSTypeAnnotation

jscodeshift库介绍

jscodeshift是一个可以重构js和ts文件的工具集。 jscodeshift的api基于recast封装,语法十分接近jquery。

jscodeshift的优点:

  • 同时支持 js , jsx , ts , tsx
  • 封装的recast,语法解决jquery
  • 提供可视化的 astexplorer。利用这个语法树再加上 API,就能通过 js 代码找到文件指定的代码片段去修改。

参考

像玩jQuery一样玩AST

如何使用jscodeshift+Commander来开发一个简易的重构脚手架

AST介绍

重构利器 jscodeshift

实操第一步,提取中文信息

采用脚手架方式,使用commander或者yargs

编写transform命令提取中文,指定要执行的目录

获取目录下及子目录所有文件

所有文件遍历用jscodeshift查询StringLiteral

提取获取到的中文,写入i18nMapChinese.txt

将i18nMapChinese.txt翻译后文件放入i18nMapEnglish.txt

  • 引入相关库
#!/usr/bin/env node
const lodash = require("loadsh");
const commander = require("commander");
const globby = require("globby");
const jscodeshift = require("jscodeshift");
const path = require("path");
const fs = require("fs");
  • 编写transform命令提取中文,指定要执行的目录
commander.command("transform <fileOrfolder>").action(async (fileOrfolder) => {
  Promise.resolve()
    .then(() => {
      return {
        allFiles: [],
        i18nMap: [],
      };
    })
    .then((res) => {
      res.allFiles = getAllFiles(fileOrfolder);
      return res;
    })
    .then((res) => {
      res.allFiles.forEach((fullpath) => transform(fullpath, res.i18nMap));
      return res;
    })
    .then((res) => {
      writeToI18nMapJson(res.i18nMap);
      return res;
    })
    .then(() => {
      fs.writeFileSync(
        path.resolve(process.cwd(), "./i18nMapEnglish.txt"),
        "将翻译内容替换到这里,只要英文,这一行中文也不要了,英文要和i18nMapChinese每行对应,如: \nHello\nMoney\nYellow",
        "utf-8"
      );
    });
});
  • 获取目录下及子目录所有文件
function getAllFiles(fileOrfolder) {
  const fullPath = path
    .resolve(process.cwd(), fileOrfolder)
    .replace(/\\/g, "/");
  const allFiles = globby
    .sync(`${fullPath}/**/!(*.d).{ts,tsx,js,jsx}`, {
      dot: true,
      ignore: [],
    })
    .map((x) => path.resolve(x));
  return allFiles;
}
  • 所有文件遍历用jscodeshift查询StringLiteral等等,可继续添加
function transform(fullpath, i18nMap, i18nUkeyMap) {
  const content = fs.readFileSync(fullpath, { encoding: "utf-8" });
  const parser = fullpath.substr(fullpath.lastIndexOf(".") + 1);
  const j = jscodeshift.withParser(parser);
  const root = j(content);
  root
    .find(j.StringLiteral, (p) => /[\u4e00-\u9fa5]/.test(p.value))
    .forEach((path) => {
      // 中文
      const value = path.node.value;
      if (!i18nMap.includes(value)) {
        i18nMap.push(value);
      }
    });
}
  • 提取获取到的中文,写入i18nMapChinese.txt
function writeToI18nMapJson(i18nMap) {
  fs.writeFileSync(
    path.resolve(process.cwd(), "./i18nMapChinese.txt"),
    i18nMap.join("\n"),
    "utf-8"
  );
}

实操第二步,批量翻译中文,转换UKey格式

node执行transform命令

node mycli.js transform projectdemo/pages/activity

image.png

拷贝i18nMapChinese.txt到谷歌excel可以自动批量翻译

image.png

用谷歌excel批量翻译

image.png

将翻译后英文放入i18nMapEnglish.txt

image.png

实操第三步,替换源码文件

node执行traverse命令

node mycli.js traverse projectdemo/pages/activity

编写traverse命令

commander.command("traverse <fileOrfolder>").action(async (fileOrfolder) => {
  Promise.resolve()
    .then(() => {
      const res = {
        i18nUkeyMap: reloadOldI18nMap(),
      };
      return res;
    })
    .then((res) => {
      res.allFiles = getAllFiles(fileOrfolder);
      return res;
    })
    .then((res) => {
      res.allFiles.forEach((fullpath) => traverse(fullpath, res.i18nUkeyMap));
    });
});

合并中英文配置文件,生成对应关系

function reloadOldI18nMap() {
  // 中文配置集合
  const i18nMapFile = globby.sync(
    path.resolve(process.cwd(), "./i18nMapChinese.txt").replace(/\\/g, "/")
  );
  const i18nMapEnglishFile = globby.sync(
    path.resolve(process.cwd(), "./i18nMapEnglish.txt").replace(/\\/g, "/")
  );
  if (i18nMapFile.length && i18nMapEnglishFile.length) {
    const oc = fs
      .readFileSync(i18nMapFile[0], {
        encoding: "utf-8",
      })
      .split("\n");
    const oceng = fs
      .readFileSync(i18nMapEnglishFile[0], {
        encoding: "utf-8",
      })
      .split("\n");
    if (oc.length != oceng.length) {
      throw "错误!!中英文配置文件行数不同";
    }
    const op = oc.reduce(
      (p, c, i) => {
        const k = buildUKey(oceng[i], p.ukeys);
        p.ukeys.push(k);
        p.object[c] = k;
        return p;
      },
      {
        object: {},
        ukeys: [],
      }
    );
    return op.object;
  } else {
    throw "错误!!中或者英文配置文件为空";
  }
}

继续取得所有文件getAllFiles,同transform中

编写traverse,改造transform支持更新

function traverse(fullpath, i18nUkeyMap) {
  transform(fullpath, Object.keys(i18nUkeyMap || {}), i18nUkeyMap);
}
  • 改transform
function transform(fullpath, i18nMap, i18nUkeyMap) {
  const content = fs.readFileSync(fullpath, { encoding: "utf-8" });
  const parser = fullpath.substr(fullpath.lastIndexOf(".") + 1);
  const j = jscodeshift.withParser(parser);
  const root = j(content);
  root
    .find(j.StringLiteral, (p) => /[\u4e00-\u9fa5]/.test(p.value))
    .forEach((path) => {
      // 中文
      const value = path.node.value;
      if (!i18nMap.includes(value)) {
        i18nMap.push(value);
      }
      // 替换
      if (i18nUkeyMap) {
        // Ukey
        const Ukey = i18nUkeyMap[value];
        const Intkey = Ukey;
        if (Ukey) {
          j(path).replaceWith((p) => {
            p.node.value = `intl.get('${Intkey}')`;
            return p.node.value;
          });
        }
      }
    });
  fs.writeFileSync(fullpath, root.toSource(), { encoding: "utf-8" });
}

重点在 transform 函数中