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

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

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

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

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

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 文件

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

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

  1. 精准打击 - 只处理字符串字面量节点,注释、变量名啥的完全不会误伤
  2. 上下文感知 - 知道这个字符串是在模板里还是 script 里,该用 t() 还是 $t() 一清二楚
  3. 安全可靠 - 修改 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 和编译原理有了更深的理解:

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

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

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

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

相关推荐
用户4099322502122 小时前
为什么Vue 3的计算属性能解决模板臃肿、性能优化和双向同步三大痛点?
前端·ai编程·trae
海云前端12 小时前
Vue首屏加速秘籍 组件按需加载真能省一半时间
前端
蛋仔聊测试2 小时前
Playwright 中route 方法模拟测试数据(Mocking)详解
前端·python·测试
零号机2 小时前
使用TRAE 30分钟极速开发一款划词中英互译浏览器插件
前端·人工智能
molly cheung2 小时前
FetchAPI 请求流式数据 基本用法
javascript·fetch·请求取消·流式·流式数据·流式请求取消
疯狂踩坑人3 小时前
结合400行mini-react代码,图文解说React原理
前端·react.js·面试
Mintopia3 小时前
🚀 共绩算力:3分钟拥有自己的文生图AI服务-容器化部署 StableDiffusion1.5-WebUI 应用
前端·人工智能·aigc
街尾杂货店&3 小时前
CSS - transition 过渡属性及使用方法(示例代码)
前端·css
CH_X_M3 小时前
为什么在AI对话中选择用sse而不是web socket?
前端