爱影视大全
44.7MB · 2025-11-11
周一早上,产品经理笑眯眯地走过来:"小王啊,咱们这个项目要支持多语言了,你看看什么时候能搞定?"
我打开项目一看,好家伙,500+ 个 Vue 文件,里面到处都是硬编码的中文:
<h1>欢迎使用我们的系统</h1>
<button>点击提交</button>
const message = "操作成功"
按照传统做法,我需要:
$t() 或 t() 包裹粗略估算了一下,这 TM 得改到猴年马月!
作为一个合格的懒人程序员,我的第一反应是:
于是,我花了 n 天时间撸了个自动化工具:VueI18nify
剧透一下结果:原本预计 5 天的工作量,工具跑了 10 秒就搞定了
不过过程中也踩了不少坑,这篇文章就来聊聊我是怎么用 AST 解决这个问题的,以及那些让我抓狂的技术细节。
先上效果,一图胜千言!
这个工具能自动帮你:
.vue、.js、.ts 一个都不放过$t() 用 $t(),该用 t() 用 t()举个栗子 ,它会把这样的"原始代码":
<template>
<div>
<h1>欢迎使用</h1>
<button @click="handleClick('点击了按钮')">点击我</button>
</div>
</template>
<script>
export default {
data() {
return {
message: '消息内容'
}
}
}
</script>
一键变身成这样的"国际化代码":
<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 配置文件:
{
"欢迎使用": "欢迎使用",
"点击了按钮": "点击了按钮",
"点击我": "点击我",
"消息内容": "消息内容"
}
刚开始我也想过偷懒,直接用正则表达式匹配中文字符串,然后替换成 $t('xxx') 不就完事了?
写了两行代码后,我就放弃了...
为啥? 因为正则表达式在这个场景下就是个定时炸弹 :
// 这些情况正则表达式根本搞不定:
// 1. 注释里的中文不应该被替换
// 这是一个中文注释
// 2. 已经包裹过的不应该重复包裹
const msg = $t('已经包裹过了')
// 3. 字符串里的引号怎么处理?
const text = "他说:'你好'"
// 4. 模板字符串里的变量怎么办?
const greeting = `你好,${name}`
// 5. 嵌套的对象属性呢?
const obj = { title: '标题', desc: '描述' }
试了几次后,我发现用正则表达式就像拿菜刀做手术,根本不靠谱!
既然正则不行,那就上AST(抽象语法树)!
技术栈:
@babel/parser - 把代码变成 AST@babel/traverse - 遍历和修改 AST@babel/generator - 把 AST 变回代码@vue/compiler-dom - 解析 Vue 模板为什么 AST 这么香?
t() 还是 $t() 一清二楚整个工具的架构其实很简单,就是经典的编译器三阶段:
Parser (解析) → Transformer (转换) → Generator (生成)
听起来很高大上?其实就是:读代码 → 改代码 → 写代码,就这么简单!
对于 JS/TS 代码,主要搞定两种情况:
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)
}
}
})
效果:
// 转换前
const message = '操作成功'
// 转换后
const message = $t('操作成功')
模板字符串是个大坑,因为里面可能有变量插值:
TemplateLiteral(path) {
path.node.quasis.forEach((quasi) => {
const text = quasi.value.raw
if (containsChinese(text)) {
// 将 `你好,${name}` 转换为 `${$t('你好,')}${name}`
quasi.value.raw = `${$t('${text.trim()}')}`
}
})
}
效果:
// 转换前
const greeting = `你好,${name}!欢迎使用`
// 转换后
const greeting = `${$t('你好,')}${name}${$t('!欢迎使用')}`
Vue 模板比 JS 代码复杂多了,因为要处理各种节点类型。
这个最简单,直接包裹就行:
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
}
效果:
<!-- 转换前 -->
<h1>欢迎使用</h1>
<!-- 转换后 -->
<h1>{{ t('欢迎使用') }}</h1>
属性里的中文也要处理,而且要把普通属性改成动态绑定:
// 普通属性:placeholder="请输入"
// 转换为::placeholder="t('请输入')"
if (containsChinese(value)) {
i18nCollector.add(value)
return `:${attrName}="t('${value}')"` // 注意前面加了冒号!
}
效果:
<!-- 转换前 -->
<input placeholder="请输入用户名" />
<!-- 转换后 -->
<input :placeholder="t('请输入用户名')" />
<!-- 转换前 -->
<button @click="handleClick('点击了按钮')">按钮</button>
<!-- 转换后 -->
<button @click="handleClick(t('点击了按钮'))">{{ t('按钮') }}</button>
这里需要解析事件处理器中的 JavaScript 表达式,找到字符串参数并替换。
实现方式:把事件处理器的表达式当成 JS 代码,用 Babel 处理一遍!
这个项目虽然小,但让我对 AST 和编译原理有了更深的理解:
项目地址: github.com/baozjj/VueI…
技术栈: TypeScript | Babel | AST | Vue Compiler
如果觉得有用,欢迎 Star ⭐️