普通视图

发现新文章,点击刷新页面。
昨天以前首页

🚀 手动改 500 个文件?不存在的!我用 AST 撸了个 Vue 国际化神器

作者 baozj
2025年11月9日 11:13

🚀 手动改 500 个文件?不存在的!我用 AST 撸了个 Vue 国际化神器

😱 起因:一个让人头皮发麻的需求

周一早上,产品经理笑眯眯地走过来:"小王啊,咱们这个项目要支持多语言了,你看看什么时候能搞定?"

我打开项目一看,好家伙,500+ 个 Vue 文件,里面到处都是硬编码的中文:

<h1>欢迎使用我们的系统</h1>
<button>点击提交</button>
const message = "操作成功"

按照传统做法,我需要:

  1. 打开每个文件
  2. 找到所有中文字符串
  3. 手动加上 $t()t() 包裹
  4. 把中文提取到配置文件里

粗略估算了一下,这 TM 得改到猴年马月!😭

作为一个合格的懒人程序员,我的第一反应是:

"这种重复劳动,能不能让机器来干?"

于是,我花了 n 天时间撸了个自动化工具:VueI18nify

剧透一下结果:原本预计 5 天的工作量,工具跑了 10 秒就搞定了 ✨

不过过程中也踩了不少坑,这篇文章就来聊聊我是怎么用 AST 解决这个问题的,以及那些让我抓狂的技术细节。

🎯 这玩意儿到底能干啥?

先上效果,一图胜千言!

这个工具能自动帮你:

  1. 🔍 批量扫描文件 - 递归遍历整个项目,.vue.js.ts 一个都不放过
  2. 🎨 智能包裹中文 - 自动给所有中文字符串套上 i18n 函数,该用 $t()$t(),该用 t()t()
  3. 📦 生成配置文件 - 把所有中文提取出来,整整齐齐地放进 JSON 文件

举个栗子 🌰,它会把这样的"原始代码":

<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(),script 里用的是 $t(),这可不是随便写的,该用啥用啥。

🤔 技术选型:正则?不,我选择 AST!

第一个想法:用正则表达式?

刚开始我也想过偷懒,直接用正则表达式匹配中文字符串,然后替换成 $t('xxx') 不就完事了?

写了两行代码后,我就放弃了...

为啥? 因为正则表达式在这个场景下就是个定时炸弹 💣:

// 这些情况正则表达式根本搞不定:

// 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 这么香?

  1. 精准打击 - 只处理字符串字面量节点,注释、变量名啥的完全不会误伤
  2. 上下文感知 - 知道这个字符串是在模板里还是 script 里,该用 t() 还是 $t() 一清二楚
  3. 安全可靠 - 修改 AST 后重新生成代码,语法 100% 正确,不会出现括号不匹配之类的低级错误

🛠️ 核心实现:编译器三板斧

整个工具的架构其实很简单,就是经典的编译器三阶段:

Parser (解析) → Transformer (转换) → Generator (生成)

听起来很高大上?其实就是:读代码 → 改代码 → 写代码,就这么简单!

1️⃣ JavaScript/TypeScript 处理

对于 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('!欢迎使用')}`

💡 踩坑记录:一开始我直接把整个模板字符串替换成 $t(\你好,${name}`)`,结果发现 i18n 不支持这种写法...后来才知道要把固定的文本部分提取出来单独包裹。

2️⃣ Vue 模板处理

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 和编译原理有了更深的理解:

  1. AST 不是玄学 - 其实就是把代码变成树形结构,然后遍历修改,最后再变回代码
  2. 工具链很重要 - Babel 和 Vue Compiler 这些成熟的工具能省很多事
  3. 边界情况很多 - 看似简单的需求,实际实现起来要考虑各种边界情况
  4. 完成比完美重要 - 先做出能用的版本,再慢慢优化

项目地址: github.com/baozjj/VueI…

技术栈: TypeScript | Babel | AST | Vue Compiler

如果觉得有用,欢迎 Star ⭐️

❌
❌