前言
最近在研究如何将一个英文的项目,自动做国际化,不需要手动做国际化,写了一个脚本,虽然并没有准确无误的完成国际化,但是实现了半自动化,记录一下方案。
开发cli插件步骤
- 初始化一个项目
npm init -y
- 创建项目目录
- 在package.json文件中加入bin
"bin": {
"translate-cli": "./bin/cli.js"
},
复制代码
- 执行
npm link
,translate-cli 会插入到环境变量中 translate-cli
脚本就可以执行啦。
lingui.js—react国际化库
这个库很好用,非常推荐这个库,他还支持javascript,typescript翻译,不需要手动写国际化的静态文件,他可以执行命令自动生成国际化的静态文件。而且官方文件特别详细。
官方文档:lingui.js.org/
jscodeshift—代码自动化重构利器
- 他可以解析javascript,typescript代码
- 将代码转换成AST语法树,然后根据节点批量查询,操作,输出
- code->AST可视化网址:astexplorer.net/
- 提供了api:find查找,replaceWith()替换,insertBefore()插入等。
国际化插件方案
- 批量读取文件
- 使用jscodeshift插件转化成AST语法树
- 根据AST语法树查看需要做国际化的节点规则
- 根据节点规则查找节点,判断是否做国际化
- 将需要做国际化的节点的值替换成国际化代码
国际化插件主要逻辑
配置文件translate-cli.js
module.exports = {
pathRoot: 'public',
regRules: [
{
reg: 'public/**/*[!.test].tsx',
exten: 'tsx',
rules: [
'ExpressionStatement:StringLiteral',
'JSXElement:StringLiteral',
'JSXText',
'ClassProperty:Literal',
'TemplateLiteral',
'ObjectExpression:Literal'
],
},
{
reg: 'public/**/*.ts',
exten: 'ts',
rules: [
'ExpressionStatement:StringLiteral',
'TemplateLiteral',
'ObjectExpression:Literal'
],
},
],
};
复制代码
1.根据配置文件translate-cli.js中的匹配文件路径匹配规则regRules,遍历regRules读取文件
// 遍历文件路径匹配规则
for (value of regRules) {
mapFiles(value);
}
复制代码
2.glob扫描需要翻译的文件。之后遍历文件,使用fs.readFileSync()读取文件内容,将内容使用jscodeshift插件转为AST语法树,根据配置的需要做国际化的节点(AST语法树中的节点)rules,遍历规则做国际化。
/**
* 遍历文件,读取文件,根据不同的规则,来替换国际化
* @param {reg} 文件路径匹配规则
* @param {exten} 文件后缀名
* @param {rules} 该文件路径规则中的需要做国际化的节点
*/
const mapFiles = ({ reg, exten, rules }) => {
const j = jscodeshift.withParser(exten);
const files = glob.sync(path.join(process.cwd(), reg));
for (value of files) {
console.log(`${value}处理中。。。`);
isFirstImport = false;
const filedata = fs.readFileSync(value, 'utf-8');
const ast = j(filedata);
// 遍历一个文件中的不同节点
for (rule of rules) {
replaceIntl(j, ast, rule);
}
fs.writeFileSync(value, ast.toSource());
console.log(`${value}国际化规则替换成功`);
}
};
复制代码
3.之后使用ast.find()在AST语法树中提取节点,根据查找节点的值,根据总结出的是否需要做国际化的规则(需要做的国际化规则,是从项目中总结出来的,与项目强关联,如当值为string类型,且首字母是大写的时候),返回true,false。
/**
* 国际化匹配规则
* @param {*} rule 根据节点名称
* @param {*} value 值
* @returns
*/
const replaceRule = (rule, item, j) => {
const { value } = item.value;
// 不需要翻译的信息直接返回
if (
noIntl.includes(value) ||
item.name === 'key' ||
item?.parentPath?.value.type === 'TaggedTemplateExpression' ||
item?.parentPath?.parentPath?.value?.callee?.name === 'navigationLogger'
) {
return false;
}
// 当首字母为大写,的string类型的,返回true
const reg =
typeof value === 'string'
? /[A-Z]/.test(
value.replace(/\n/g, '').replace(/\r/g, '').trim().slice(0, 1)
)
: false;
switch (rule) {
case 'JSXText':
case 'ExpressionStatement:StringLiteral':
case 'JSXElement:StringLiteral':
case 'ClassProperty:Literal':
case 'ObjectExpression:Literal':
return reg;
case 'TemplateLiteral':
const { raw } = j(item).find(j.TemplateElement).at(0).get().value.value;
console.log(rule, raw);
return raw
? /[A-Z]/.test(
raw.replace('\n', '').replace('\r', '').trim().slice(0, 1)
)
: false;
default:
return false;
}
};
复制代码
3.当返回true的时候,将该节点使用ast.replaceWith()函数将内容退换成做国际化的代码,国际化的代码也是规矩不同的规则,返回国际化的模板,(如:在标签中写需要使用{},使用字符串模板,动态传值时)。
/**
* 根据值中有单引号,或者双引号返回不同的模板
* @param {*} value
*/
const templateScheme = (value) => {
return `t\`${value
.replace(/`/g, '\\`')
.replace(/{{/g, "'{{")
.replace(/}}/g, "}}'")
.replace(/{}/g, "'{}'")}\``;
};
/**
* 国际化替换模板
* @param {*} rule 根据规则返回模板
* @param {*} value 值
* @returns
*/
const template = (rule, p, j) => {
const value = p?.value?.value?.replace(/\n/g, '').replace(/\r/g, '').trim();
const { type } = p.parentPath.value;
switch (type) {
case 'JSXAttribute':
return `{${templateScheme(value)}}`; // <div rule='Demo' ></div>
}
switch (rule) {
case 'JSXText': // <div>Dddddd</div>
return `{${templateScheme(value)}}`;
case 'TemplateLiteral': // 字符串模板时
return templateTemplateLiteral(p, j);
case 'ExpressionStatement:StringLiteral': // 函数中国际化
case 'JSXElement:StringLiteral': // <div>{Demo}</div>
case 'ClassProperty:Literal': // 函数中国际化
case 'ObjectExpression:Literal':
return `${templateScheme(value)}`;
}
};
/**
* 当模板字符串时,返回国际化翻译
* @param {*} p
*/
const templateTemplateLiteral = (p, j) => {
let tel = '';
const templateElements = j(p).find(j.TemplateElement);
const identifiers = j(p).find(j.Identifier);
templateElements.forEach((templateElement, index) => {
const value = identifiers.at(index).length
? identifiers.at(index).get().value.name
: '';
tel = `${tel}${templateElement.value.value.raw}${
value ? ` \${${value}} ` : ''
}`;
});
return templateScheme(tel);
};
复制代码
4.读取文件时如果该文件中存在需要做国际化的节点时,在该文件的头部引入国际化库即在头部插入import { t } from "@lingui/macro"
/**
* 在文件头部插入国际化
*/
const insertIntl = (j, ast) => {
// 判断是否含有需要做国际化的节点,该文件是否已经插入过
if (
!ast
.find(j.ImportDeclaration)
.find(j.Identifier)
.filter((item) => {
return item.value.name === 't';
}).length &&
!isFirstImport
) {
isFirstImport = true;
ast.get().node.program.body.unshift("import { t } from '@lingui/macro';");
console.log('import { t } from "@lingui/macro" 插入成功');
}
};
复制代码
参考
- 插件代码地址:github.com/lkxing/tran…
- 在做研究的时候参考一篇文章:字节前端如何基于 AST 做国际化重构,提供了主要思路。
总结
此文章中的国际化插件可能不适用其他项目,仅供参考。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END