需求背景
1.进行国际化要转换的是字符串,主要是 StringLiteral 和 TemplateLiteral 节点,把它们替换成从资源包取值的形式
如const a = '中文'替换成const a = intl.t('intl1');
2.intl.t 是根据 key 从 bundle 中取值的,语言包 bundle 里存储了各种语言环境下 key 对应的文案:
// zh_CN.js
module.exports = {
intl1: '中文',
intl2: 'hello {placeholder}'
}
3.intl.t 是从资源 bundle 中取值,并且用传入的参数替换其中的占位符。
const locale = 'zh-CN';
intl.t = function(key, ...args) {
let index = 0;
return bundle[locale][key].replace(/\{placeholder\}/, () => args[index++]);
}
要实现这种转换,需要做三件事情:
- 如果没有引入 intl 模块,就自动引入,并且生成唯一的标识符,不和作用域的其他声明冲突
- 把字符串和模版字符串替换为 intl.t的函数调用的形式
- 把收集到的值收集起来,输出到一个资源文件中
- 带有
/*i18n-disable*/注释的字符串就忽略掉。 - 注意jsx中要替换为 {} 包裹的表达式
代码实现
sourceCode
import intl from 'intl2';
/**
* App
*/
function App() {
const title = 'title';
const desc = `desc`;
const desc2 = /*i18n-disable*/`desc`;
const desc3 = `aaa ${ title + desc} bbb ${ desc2 } ccc`;
return (
<div className="app" title={"测试"}>
<img src={Logo} />
<h1>${title}</h1>
<p>${desc}</p>
<div>
{
/*i18n-disable*/'中文'
}
</div>
</div>
);
}
插件调用
const { transformFromAstSync } = require('@babel/core');
const parser = require('@babel/parser');
const autoI18nPlugin = require('./plugin/auto-i18n-plugin');
const fs = require('fs');
const path = require('path');
const sourceCode = fs.readFileSync(path.join(__dirname, './sourceCode.js'), {
encoding: 'utf-8'
});
const ast = parser.parse(sourceCode, {
sourceType: 'unambiguous',
plugins: ['jsx']
});
const { code } = transformFromAstSync(ast, sourceCode, {
plugins: [[autoI18nPlugin, {
outputDir: path.resolve(__dirname, './output')
}]]
});
console.log(code);
插件代码
const { declare } = require('@babel/helper-plugin-utils');
const fse = require('fs-extra');
const path = require('path');
const generate = require('@babel/generator').default;
let intlIndex = 0;
function nextIntlKey() {
//唯一key
++intlIndex;
return `intl${intlIndex}`;
}
const autoTrackPlugin = declare((api, options, dirname) => {
api.assertVersion(7);
//判断有没有传输出路径
if (!options.outputDir) {
throw new Error('outputDir in empty');
}
function getReplaceExpression(path, value, intlUid) {
//expressionParams 就是 ${}里面的内容(数组形式)
const expressionParams = path.isTemplateLiteral() ? path.node.expressions.map(item => generate(item).code) : null
//如果是模版字符串字面量(TemplateLiteral),还要把 expressions 作为参数传入。
let replaceExpression = api.template.ast(`${intlUid}.t('${value}'${expressionParams ? ',' + expressionParams.join(',') : ''})`).expression;
//要判断是否在 JSXAttribute 下,如果是,则必须要包裹在 JSXExpressionContainer 节点中(也就是{})
if (path.findParent(p => p.isJSXAttribute()) && !path.findParent(p=> p.isJSXExpressionContainer())) {
replaceExpression = api.types.JSXExpressionContainer(replaceExpression);
}
return replaceExpression;
}
//收集替换的 key 和 value,保存到 file 中
function save(file, key, value) {
const allText = file.get('allText');
allText.push({
key, value
});
file.set('allText', allText);
}
return {
pre(file) {
//file表示文件的对象,可在插件里面通过 state.file 拿到,可以在file中放一些东西,遍历的时候取出来
file.set('allText', []);
},
visitor: {
Program: {
enter(path, state) {
let imported;
//判断是否引入intl 模块,如果没引入 intl 模块,则引入,并且生成唯一 id 记录到 state 中
path.traverse({
ImportDeclaration(p) {
const source = p.node.source.value;
if(source === 'intl') {
imported = true;
}
}
});
if (!imported) {
const uid = path.scope.generateUid('intl');
const importAst = api.template.ast(`import ${uid} from 'intl'`);
path.node.body.unshift(importAst);
state.intlUid = uid;
}
//对所有的有 /*i18n-disable*/ 注释的字符串和模版字符串节点打个标记,用于之后跳过处理。然后把这个注释节点从 ast 中去掉
path.traverse({
'StringLiteral|TemplateLiteral'(path) {
//leadingComments 表示前方注释
if(path.node.leadingComments) {
path.node.leadingComments = path.node.leadingComments.filter((comment, index) => {
if (comment.value.includes('i18n-disable')) {
//跳过处理
path.node.skipTransform = true;
return false;
}
return true;
})
}
//import 语句中的路径值也属于StringLiteral,而路径值不应国际化
//findParent: 向父节点搜寻节点
if(path.findParent(p => p.isImportDeclaration())) {
path.node.skipTransform = true;
}
}
});
}
},
StringLiteral(path, state) {
if (path.node.skipTransform) {
return;
}
let key = nextIntlKey(); //递增key值
save(state.file, key, path.node.value);
const replaceExpression = getReplaceExpression(path, key, state.intlUid);
path.replaceWith(replaceExpression);
path.skip();
},
TemplateLiteral(path, state) {
if (path.node.skipTransform) {
return;
}
//quasis: templateElement数组
//模版字符串需要吧 ${} 表达式的部分替换为 {placeholder} 的占位字符串
const value = path.get('quasis').map(item => item.node.value.raw).join('{placeholder}');
if(value) {
let key = nextIntlKey();
save(state.file, key, value);
const replaceExpression = getReplaceExpression(path, key, state.intlUid);
path.replaceWith(replaceExpression);
path.skip();
}
},
},
post(file) {
const allText = file.get('allText');
const intlData = allText.reduce((obj, item) => {
obj[item.key] = item.value;
return obj;
}, {});
const content = `const resource = ${JSON.stringify(intlData, null, 4)};\nexport default resource;`;
fse.ensureDirSync(options.outputDir);
fse.writeFileSync(path.join(options.outputDir, 'zh_CN.js'), content);
fse.writeFileSync(path.join(options.outputDir, 'en_US.js'), content);
}
}
});
module.exports = autoTrackPlugin;
输出结果
import _intl from 'intl';
import intl from 'intl2';
/**
* App
*/
function App() {
const title = _intl.t('intl1');
const desc = _intl.t('intl2');
const desc2 = `desc`;
const desc3 = _intl.t('intl3', title + desc, desc2);
return <div className={_intl.t('intl4')} title={_intl.t('intl5')}>
<img src={Logo} />
<h1>${title}</h1>
<p>${desc}</p>
<div>
{'中文'}
</div>
</div>;
}
并且生成了相应的资源文件:
const resource = {
"intl1": "title",
"intl2": "desc",
"intl3": "aaa {placeholder} bbb {placeholder} ccc",
"intl4": "app",
"intl5": "测试"
};
export default resource;
总结
要替换字符串和模版字符串为对应的函数调用语句,要做模块的自动引入。引入的 id 要生成全局唯一的,注意 jsx 中如果是属性的替换要用 {} 包裹。
自动国际化就是通过 AST 分析出要转换的代码,然后自动转换。
版权声明:本文为weixin_44800563原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。