系列
项目实战 - 国际化之基于Power Tools开发vscode代码提示插件
为什么要用AST提取中文?
项目中大概有2万处需要被替换,人工耗时耗力,采用AST自动处理(库jscodeshift),然后英文纠错快速处理,高效快捷又可重复,过程如下:项目源码
1、AST 匹配处中文,输出到txt文件中,并记录对应文件,行,列等信息
2、复制txt到百度或谷歌excel中,自动翻译出英文,于是就有了中英对照文档
3、用英文翻译,根据上述UKey规则,生成中文的UKey
4、根据中文和UKey的对照文档,替换源代码文件中响应的中文 如
5、剩余组织人力,纠错即可
recast 库介绍
recast 库是主角,拆-换-合都是这个库的方法,也有在线网站 astexplorer 方便查看,类型选recast
recast.parse和recast.print成对
recast.parse 是将代码拆分称 AST 语法书结构,recast.print是将 AST 语法树转换称代码
展开 AST 解析
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 代码找到文件指定的代码片段去修改。
参考
如何使用jscodeshift+Commander来开发一个简易的重构脚手架
实操第一步,提取中文信息
采用脚手架方式,使用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
拷贝i18nMapChinese.txt到谷歌excel可以自动批量翻译
用谷歌excel批量翻译
将翻译后英文放入i18nMapEnglish.txt
实操第三步,替换源码文件
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" });
}