看到一些老的系统通过手动配置语言的json文件,正好学习babel,所以通过babel去生成语言文件。
整体思路
通过babel配置使用的语言,然后从代码中晒出字符串并生成文件,接第三方或者手动配置语言文件(项目如果手动需要执行俩次打包,一次生成文件,第二次去应用)
通过babel.config 去配置使用的语言文件 配置了zh-CN
module.exports = {
"plugins": [
["./src/i18n/i18n.plugin.js", {locale: 'zh-CN'}],
"@babel/syntax-jsx" //配置jsx语法支持
]
}
接下来在遍历ast之前生成转换函数文件
转换函数文件在这块我是写了固定的路径,在项目中可以通过webpack配置别名如 :@:'src',引入可以改成import xx from '@/util/xxx.js',也可以通过externals配置去像node_modules一样去引用,这样就可以在项目中每个js文件都通用这个转换函数文件了,下面只是demo 所以转换函数文件放在了code同级目录,通过from './xxx'引用
pre(file) {
// 语言包的文件路径 此处locale是options传进来的
let localePath = `./${locale}.json`
// 判断转换函数的文件是否存在
if(!fs.pathExistsSync(process.cwd()+'/src/i18n/i18n-util.js')) {
let transformCode = `
import locale from '${localePath}'
export default function transform(key,...args) {
return locale[key].replace(/\{placeholder\}/, () => args[index++])
}
`
fs.outputFileSync(process.cwd()+'/src/i18n/i18n-util.js', transformCode)
}
file.localeId = 0 // 唯一标识的key
file.outputFile = {} // 最终输出的locale
}
接下来是操作ast 首先要把转换函数文件引入,如果import了就不需要再引入,否则要引入
Program: {
enter(path, state) {
path.traverse({
ImportDeclaration(curpath) {
let requirePath = curpath.get('source')
let requireValue = requirePath.node.value
if(requireValue === './i18n-util.js') {
let importPath = curpath.get('specifiers')[0].get('local')
let importIdentifier = importPath.node.name
state.importIdentifier = importIdentifier
curpath.stop()
}
}
})
if(!state.importIdentifier) {
state.importIdentifier = importModule.addDefault(path, './i18n-util.js',{
nameHint: path.scope.generateUid('./i18n-util.js')
}).name
}
}
}
string 与模板字符串处理,要注意模板字符串的插值与jsx属性 jsx属性做了属性名称+标识的key
StringLiteral(path, state) {
if(isStep(path)) return
// 能找到说明是jsx 的 attr
let parent = path.findParent(p => p.isJSXAttribute())
let localeId = ++state.file.localeId
// 统计到输出json的对象中
state.file.outputFile[`${parent?parent.node.name.name:''}${localeId}`] = path.node.value
let replaceExpression = api.template.ast(`${state.importIdentifier}('${parent?parent.node.name.name:''}${localeId}')`).expression
// jsx attr 的值要加{}
if (path.findParent(p => p.isJSXAttribute()) && !path.findParent(p=> p.isJSXExpressionContainer())) {
replaceExpression = api.types.JSXExpressionContainer(replaceExpression);
}
path.replaceWith(replaceExpression)
path.skip()
},
TemplateLiteral(path, state) {
if(isStep(path)) return
// 能找到说明是jsx 的 attr
let parent = path.findParent(p => p.isJSXAttribute())
// ${xxx + xxx} 转为code
let localeId = ++state.file.localeId
state.file.outputFile[`${parent?parent.node.name.name:''}${localeId}`] = path.get('quasis').map(item => item.node.value.raw).join('{placeholder}')
const value = path.node.expressions.map(item => generate(item).code)
let replaceExpression = api.template.ast(`${state.importIdentifier}('${parent?parent.node.name.name:''}${localeId}',${value.join(',')})`).expression
if (path.findParent(p => p.isJSXAttribute()) && !path.findParent(p=> p.isJSXExpressionContainer())) {
replaceExpression = api.types.JSXExpressionContainer(replaceExpression);
}
path.replaceWith(replaceExpression)
path.skip()
}
语言文件的输出
post(file) {
let {
outputFile,
} = file
let localeZhPath = `./src/i18n/zh-CN.json`
let localeEnPath = `./src/i18n/en-US.json`
fs.outputFileSync(path.join(process.cwd(), localeZhPath), JSON.stringify(outputFile, null, 2))
fs.outputFileSync(path.join(process.cwd(), localeEnPath), JSON.stringify(outputFile, null, 2))
}
最终输出
code
import _i18nUtilJs2 from "./i18n-util.js";
function App() {
const title = _i18nUtilJs2('1');
const desc = _i18nUtilJs2('2');
const desc2 =
/*i18n-disable*/
`desc`;
const desc3 = _i18nUtilJs2('3', title + desc, desc2);
return <div className={_i18nUtilJs2('className4')} title={_i18nUtilJs2('title5')}>
<img src={Logo} />
<h1>{title}</h1>
<p>{desc}</p>
<div>
{
/*i18n-disable*/
'中文'}
</div>
</div>;
}
util
import locale from './i18n/zh-CN.json'
export default function transform(key,...args) {
return locale[key].replace(/{placeholder}/, () => args[index++])
}
locale
{
"1": "title",
"2": "desc",
"3": "aaa {placeholder} bbb {placeholder} ccc",
"className4": "app",
"title5": "测试"
}
(代码仓库):[gitee.com/summarize/b…]