🚀 手动改 500 个文件?不存在的!我用 AST 撸了个 Vue 国际化神器
😱 起因:一个让人头皮发麻的需求
周一早上,产品经理笑眯眯地走过来:"小王啊,咱们这个项目要支持多语言了,你看看什么时候能搞定?"
我打开项目一看,好家伙,500+ 个 Vue 文件,里面到处都是硬编码的中文:
vue
<h1>欢迎使用我们的系统</h1>
<button>点击提交</button>
const message = "操作成功"
按照传统做法,我需要:
- 打开每个文件
- 找到所有中文字符串
- 手动加上
$t()或t()包裹 - 把中文提取到配置文件里
粗略估算了一下,这 TM 得改到猴年马月!😭
作为一个合格的懒人程序员,我的第一反应是:
"这种重复劳动,能不能让机器来干?"
于是,我花了 n 天时间撸了个自动化工具:VueI18nify
剧透一下结果:原本预计 5 天的工作量,工具跑了 10 秒就搞定了 ✨
不过过程中也踩了不少坑,这篇文章就来聊聊我是怎么用 AST 解决这个问题的,以及那些让我抓狂的技术细节。
🎯 这玩意儿到底能干啥?
先上效果,一图胜千言!
这个工具能自动帮你:
- 🔍 批量扫描文件 - 递归遍历整个项目,
.vue、.js、.ts一个都不放过 - 🎨 智能包裹中文 - 自动给所有中文字符串套上 i18n 函数,该用
$t()用$t(),该用t()用t() - 📦 生成配置文件 - 把所有中文提取出来,整整齐齐地放进 JSON 文件
举个栗子 🌰,它会把这样的"原始代码":
vue
<template>
<div>
<h1>欢迎使用</h1>
<button @click="handleClick('点击了按钮')">点击我</button>
</div>
</template>
<script>
export default {
data() {
return {
message: '消息内容'
}
}
}
</script>
一键变身成这样的"国际化代码":
vue
<template>
<div>
<h1>{{ t('欢迎使用') }}</h1>
<button @click="handleClick(t('点击了按钮'))">{{ t('点击我') }}</button>
</div>
</template>
<script>
export default {
data() {
return {
message: $t('消息内容') // 注意这里用的是 $t()
}
}
}
</script>
同时还会贴心地生成一个 i18n-messages.json 配置文件:
json
{
"欢迎使用": "欢迎使用",
"点击了按钮": "点击了按钮",
"点击我": "点击我",
"消息内容": "消息内容"
}
💡 小细节 :注意到了吗?模板里用的是
t(),script 里用的是$t(),这可不是随便写的,该用啥用啥。
🤔 技术选型:正则?不,我选择 AST!
第一个想法:用正则表达式?
刚开始我也想过偷懒,直接用正则表达式匹配中文字符串,然后替换成 $t('xxx') 不就完事了?
写了两行代码后,我就放弃了...
为啥? 因为正则表达式在这个场景下就是个定时炸弹 💣:
javascript
// 这些情况正则表达式根本搞不定:
// 1. 注释里的中文不应该被替换
// 这是一个中文注释
// 2. 已经包裹过的不应该重复包裹
const msg = $t('已经包裹过了')
// 3. 字符串里的引号怎么处理?
const text = "他说:'你好'"
// 4. 模板字符串里的变量怎么办?
const greeting = `你好,${name}`
// 5. 嵌套的对象属性呢?
const obj = { title: '标题', desc: '描述' }
试了几次后,我发现用正则表达式就像拿菜刀做手术,根本不靠谱!
最终方案:AST 大法好!
既然正则不行,那就上AST(抽象语法树)!
什么是 AST?简单来说,就是把代码解析成一棵树,每个节点都有明确的类型和位置信息。就像给代码做了个 CT 扫描,啥都能看得一清二楚。
技术栈:
- 🔧 Babel 全家桶 - 处理 JavaScript/TypeScript
@babel/parser- 把代码变成 AST@babel/traverse- 遍历和修改 AST@babel/generator- 把 AST 变回代码
- 🎨 Vue Compiler - 处理 Vue 模板
@vue/compiler-dom- 解析 Vue 模板
- 💪 TypeScript - 类型安全,写着放心
为什么 AST 这么香?
- ✅ 精准打击 - 只处理字符串字面量节点,注释、变量名啥的完全不会误伤
- ✅ 上下文感知 - 知道这个字符串是在模板里还是 script 里,该用
t()还是$t()一清二楚 - ✅ 安全可靠 - 修改 AST 后重新生成代码,语法 100% 正确,不会出现括号不匹配之类的低级错误
🛠️ 核心实现:编译器三板斧
整个工具的架构其实很简单,就是经典的编译器三阶段:
scss
Parser (解析) → Transformer (转换) → Generator (生成)
听起来很高大上?其实就是:读代码 → 改代码 → 写代码,就这么简单!
1️⃣ JavaScript/TypeScript 处理
对于 JS/TS 代码,主要搞定两种情况:
场景一:普通字符串
typescript
traverse(ast, {
StringLiteral(path) {
if (containsChinese(path.node.value)) {
// 收集中文文本,后面要生成配置文件
i18nCollector.add(path.node.value)
// 检查是否已经被包裹过了,避免重复包裹
if (isAlreadyWrappedInI18n(path)) {
return // 已经包裹过了,跳过!
}
// 创建 $t() 函数调用节点
const replaceNode = t.callExpression(t.identifier('$t'), [t.stringLiteral(path.node.value)])
// 替换原来的节点
path.replaceWith(replaceNode)
}
}
})
效果:
javascript
// 转换前
const message = '操作成功'
// 转换后
const message = $t('操作成功')
场景二:模板字符串 (这个有点坑!)
模板字符串是个大坑,因为里面可能有变量插值:
typescript
TemplateLiteral(path) {
path.node.quasis.forEach((quasi) => {
const text = quasi.value.raw
if (containsChinese(text)) {
// 将 `你好,${name}` 转换为 `${$t('你好,')}${name}`
quasi.value.raw = `\${$t('${text.trim()}')}`
}
})
}
效果:
javascript
// 转换前
const greeting = `你好,${name}!欢迎使用`
// 转换后
const greeting = `${$t('你好,')}${name}${$t('!欢迎使用')}`
💡 踩坑记录 :一开始我直接把整个模板字符串替换成
$t(\你好,${name}`)`,结果发现 i18n 不支持这种写法...后来才知道要把固定的文本部分提取出来单独包裹。
2️⃣ Vue 模板处理
Vue 模板比 JS 代码复杂多了,因为要处理各种节点类型。
场景一:文本节点
这个最简单,直接包裹就行:
typescript
const transformText = (node: TextNode): string => {
const content = node.content
if (containsChinese(content)) {
i18nCollector.add(content.trim())
// 检查是否已经包裹过了
if (content.includes('t(')) {
return content
}
// 包裹成 {{ t('xxx') }}
return `{{ t('${content.trim()}') }}`
}
return content
}
效果:
vue
<!-- 转换前 -->
<h1>欢迎使用</h1>
<!-- 转换后 -->
<h1>{{ t('欢迎使用') }}</h1>
场景二:属性节点
属性里的中文也要处理,而且要把普通属性改成动态绑定:
typescript
// 普通属性:placeholder="请输入"
// 转换为::placeholder="t('请输入')"
if (containsChinese(value)) {
i18nCollector.add(value)
return `:${attrName}="t('${value}')"` // 注意前面加了冒号!
}
效果:
vue
<!-- 转换前 -->
<input placeholder="请输入用户名" />
<!-- 转换后 -->
<input :placeholder="t('请输入用户名')" />
场景三:事件处理器中的字符串
vue
<!-- 转换前 -->
<button @click="handleClick('点击了按钮')">按钮</button>
<!-- 转换后 -->
<button @click="handleClick(t('点击了按钮'))">{{ t('按钮') }}</button>
这里需要解析事件处理器中的 JavaScript 表达式,找到字符串参数并替换。
实现方式:把事件处理器的表达式当成 JS 代码,用 Babel 处理一遍!
🎓 总结:
收获
这个项目虽然小,但让我对 AST 和编译原理有了更深的理解:
- AST 不是玄学 - 其实就是把代码变成树形结构,然后遍历修改,最后再变回代码
- 工具链很重要 - Babel 和 Vue Compiler 这些成熟的工具能省很多事
- 边界情况很多 - 看似简单的需求,实际实现起来要考虑各种边界情况
- 完成比完美重要 - 先做出能用的版本,再慢慢优化
项目地址 : github.com/baozjj/VueI...
技术栈: TypeScript | Babel | AST | Vue Compiler
如果觉得有用,欢迎 Star ⭐️