前端自动国际化解决方案
作者在接手vue老项目时,有多次遇到需要把项目进行国际化的需求。而项目里通常有大量散落的中文硬编码,要一个个把中文找出来,提取到json语言包里,再用i18n国际化标记对源码里的中文进行替换,这个过程非常费时费力。我就考虑在github
上找一找有什么工具能够自动处理下,且能够满足以下几点要求:
- 自动提取项目里的中文到json语言包
- 自动将源码里的中文替换成i18n标记
- 自动翻译成多种语言
遇到的问题
提取和翻译二者难以两全,有的工具要么仅支持提取转换中文,要么仅支持翻译。如果要满足的我开发需求得安装两套工具
代码转换的准确率低。开源项目里大多数对中文进行提取转换的工具是基于正则表达式实现的,这种做法的最大缺点是缺少上下文语义的分析。比如对下面模板字符串的转换
`测试${a}`
使用正则替换的工具通常错误的转换成
`this.$t('测试')${a}`
而正确的应该是
`${this.$t('测试')}${a}`
导致这个错误的原因就在于工具没分析出这个中文出现在模板字符串里(虽然对于正则大佬还是能处理这种bug,但是要考虑的场景非常之多)
转化工具可定制程度较低。由于市面上像
vue-i18n
、react-intl
等i18n库特别多,它们的国际化标记往往是不统一的,有的是this.$t('xxx')
形式,有的是t('xx')
形式。如果转化工具不支持用户定制i18的函数名和调用对象,就很难适配不同的i18n库框架支持度单一。大部分提取工具仅支持vue或react
鉴于我找到的工具大部分都存在问题1和问题2,于是我决定自己动手实现了一个自动提取中文并翻译的命令行工具,让整个国际化流程可以实现自动化。
解决方案
整体流程
首先我们通过
glob
工具遍历项目目录下的文件,利用Nodejs读取文件源码,使用babel将其解析成抽象语法树。
解析react
babel插件包已提供了对jsx语法的解析,这也是为什么react的文件比vue文件容易解析。使用方式为
const babel = require('@babel/core')
const pluginSyntaxJSX = require('@babel/plugin-syntax-jsx')
babel.parseSync(code,
plugins: [
pluginSyntaxJSX
]
)
解析typescript
babel对ts语法也提供了良好的支持,使用方式如下
const babel = require('@babel/core')
const presetTypescript = require('@babel/preset-typescript')
babel.parseSync(code,
plugins: [
presetTypescript, { isTSX: true, allExtensions: true }
]
)
解析vue
对于vue文件处理有点特殊,直接使用babel是无法直接对其进行解析的。这时我们可以使用vue官方提供的@vue/compiler-sfc
工具。
import { parse } from '@vue/compiler-sfc'
const { descriptor, errors } = parse(code)
const { template, script, scriptSetup, styles } = descriptor
将文件拆成html,js,css三个部分,分别去解析。
其中css可以跳过直接用源码,js部分依然用babel处理,html部分我们可以使用htmlparser2
工具解析:
new htmlparser2.Parser({
onopentag(){
// 处理html属性里的中文
...
},
ontext(){
// 处理文本节点里的中文
...
},
oncomment() {
// 判断是否跳过转换
...
}
})
解析完后,再将html,js和css这三组代码重新拼接起来即可。
替换中文
这里借助babel
工具我们可以很方便的在遍历的过程中对每个节点进行处理,当节点中发现中文时,使用babel重新生成新的节点进行替换即可(节点生成比较复杂有一定的学习成本,下文直接略过,读者可以自行查阅掘金上的一些技术分享。有条件的同学,推荐看下光哥的babel掘金小册,内容写的很好):
traverse(ast, {
enter() {
// 判断是否跳过i18n转换
...
},
StringLiteral() {
// 处理字符串里的中文
...
},
TemplateLiteral() {
// 处理模板字符串里的中文
...
},
JSXText() {
// 处理jsx文本节点里的中文
...
},
JSXAttribute() {
// 处理jsx属性里的中文
...
},
CallExpression() {
// 处理表达式里的中文
...
},
ImportDeclaration() {
// 根据配置加入导入声明
...
}
})
在遍历的过程中,我们可以顺便将中文以key-value
的形式保存到json文件中:
// 目录结构:
src
├── locales
│ ├── en-US.json
│ └── zh-CN.json
└── index.js
// zh-CN.json
{
"我是中文": "我是中文",
"嗯": "嗯"
}
遍历完成后,最后以zh-CN.json
为蓝本,调用谷歌或有道翻译,翻译成其他语言包即可。
翻译规则
翻译时要考虑一种特殊情况,假如项目里之前已经存在翻译好的语言包。如果主语言和目标语言存在相同的key。那么目标语言包里key对应的value,不会被重新翻译,而是复用原来的值。这是考虑到英文翻译后,有时会遇到文字超出容器宽度,影响到布局样式,为了解决这个问题,国际化实践中,开发者往往会用更短的同义词替换长的单词,如果我们二次翻译时把用户替换好的单词又覆盖掉,用户就不得不自己重新替换一遍。
最终效果
- react示例
转换前
import { useState } from 'react'
/*i18n-ignore*/
const b = '被忽略提取的文案'
function Example() {
const [msg, setMsg] = useState('你好')
return (
<div>
<p title="标题">{msg + '呵呵'}</p>
<button onClick={() => setMsg(msg + '啊')}>点击</button>
</div>
)
}
export default Example
转换后
import { t } from 'i18n'
import { useState } from 'react'
/*i18n-ignore*/
const b = '被忽略提取的文案'
function Example() {
const [msg, setMsg] = useState(t('你好'))
return (
<div>
<p title={t('标题')}>{msg + t('呵呵')}</p>
<button onClick={() => setMsg(msg + t('啊'))}>{t('点击')}</button>
</div>
)
}
export default Example
- vue示例
转换前
<template>
<div :label="'标签'" :title="1 + '标题'">
<p title="测试注释">内容</p>
<button @click="handleClick('信息')">点击</button>
</div>
</template>
<script>
export default {
methods: {
handleClick() {
console.log('点了')
},
},
}
</script>
转换后
<template>
<div :label="$t('标签')" :title="1 + $t('标题')">
<p :title="$t('测试注释')">{{ $t('内容') }}</p>
<button @click="handleClick($t('信息'))">{{ $t('点击') }}</button>
</div>
</template>
<script>
export default {
methods: {
handleClick() {
console.log(this.$t('点了'))
},
},
}
</script>