Vue2存量项目国际化改造踩坑

Vue2存量项目国际化改造踩坑

一、背景

在各类业务场景中,国际化作为非常重要的一部分已经有非常多成熟的方案,但对于一些存量项目则存在非常的改造成本,本文将分享一个的Vue2项目国际化改造方案,通过自定义Webpack插件自动提取中文文本,大大提升改造效率。

二、核心思路(提取阶段)

通过开放自定义Webpack插件,利用AST(抽象语法树)分析技术,自动扫描Vue组件中的中文文本

  1. 模板中的中文:插值表达式、文本节点、属性值
  2. 脚本中的中文:字符串字面量、模板字符串
  3. 自动生成语言包:输出标准的i18n语言文件

技术栈

  • vue-template-compiler:解析Vue单文件组件
  • @babel/parser:解析JavaScript代码为AST
  • @babel/traverse:遍历AST节点
  • Webpack Plugin API:集成到构建流程

三、插件实现

1.插件基础结构

javascript 复制代码
class MatureChineseExtractorPlugin {
  constructor(options = {}) {
    this.options = {
      outputPath: './i18n',
      includePatterns: [/src\/.*\.(vue|js|jsx|ts|tsx)$/, '/src/App.vue'],
      excludePatterns: [
        /node_modules/,
        /dist/,
        /i18n/,
        /plugins/,
        /public/,
        /webpack\.config\.js/,
        /package\.json/,
      ],
      keyPrefix: '',
      ...options,
    };
	// 解析结构
    this.extractedTexts = new Map();
    // 文件统计
    this.fileStats = {
      totalFiles: 0,
      processedFiles: 0,
      extractedCount: 0,
      injectedFiles: 0,
      skippedFiles: 0,
    };
  }

  /**
   * 插件入口文件
   * @param {*} compiler
   */
  apply(compiler) {
    // 插件主流程
  }
  
  // 插件核心方法
  // ......
}

2. AST语法树抽取

javascript 复制代码
  apply(compiler) {
    compiler.hooks.done.tap('MatureChineseExtractorPlugin', (stats) => {
      const projectRoot = compiler.context;
      const filePath = path.resolve(projectRoot, './src/App.vue');
      // 解析Vue组件
      const content = fs.readFileSync(filePath, 'utf-8');
      const component = parseComponent(content);

      // 解析模版AST语法树
      const templateAst = compile(component.template.content, {
        preserveWhitespace: false,
        whitespace: 'condense',
      });
      this.traverseTemplateAST(templateAst.ast, filePath);

      // 解析Script语法树
      const scriptAst = parse(component.script.content, {
        sourceType: 'module',
        plugins: ['jsx', 'typescript', 'decorators-legacy', 'classProperties'],
      });
      this.traverseScriptAst(scriptAst, filePath);

      // 输出结果
      this.outputResults();
    });
  }

3.Vue Template AST解析

javascript 复制代码
 /**
   * 模版AST语法树处理
   * @param {AST节点} node
   */
  traverseTemplateAST(node, filePath) {
    if (!node) return;

    // 处理元素节点的属性
    if (node.type === 1) {
      // 处理静态属性
      if (node.attrsList) {
        node.attrsList.forEach((attr) => {
          if (attr.value && this.containsChinese(attr.value)) {
            this.addExtractedText(attr.value, filePath, `template-attr-${attr.name}`, {
              line: node.start || 0,
              column: 0,
            });
          }
        });
      }

      // 处理动态属性
      if (node.attrs) {
        node.attrs.forEach((attr) => {
          if (attr.value && this.containsChinese(attr.value)) {
            this.addExtractedText(attr.value, filePath, `template-dynamic-attr-${attr.name}`, {
              line: 0,
              column: 0,
            });
          }
        });
      }
    }

    // 处理{{}}表达式节点
    if (node.type === 2 && node.expression) {
      // 检查表达式中是否包含中文字符串
      const chineseMatches =
        node.expression.match(/'([^']*[\u4e00-\u9fa5][^']*)'/g) ||
        node.expression.match(/"([^"]*[\u4e00-\u9fa5][^"]*)"/g) ||
        node.expression.match(/`([^`]*[\u4e00-\u9fa5][^`]*)`/g);

      if (chineseMatches) {
        chineseMatches.forEach((match) => {
          // 去掉引号
          const text = match.slice(1, -1);
          if (this.containsChinese(text)) {
            this.addExtractedText(text, filePath, 'template-expression', {
              line: node.start || 0,
              column: 0,
            });
          }
        });
      }

      // 处理模板字符串中的中文
      if (node.expression.includes('`') && this.containsChinese(node.expression)) {
        // 简单提取模板字符串中的中文部分
        const templateStringMatch = node.expression.match(/`([^`]*)`/);
        if (templateStringMatch) {
          const templateContent = templateStringMatch[1];
          // 提取非变量部分的中文
          const chineseParts = templateContent
            .split('${')
            .map((part) => {
              return part.split('}')[part.includes('}') ? 1 : 0];
            })
            .filter((part) => part && this.containsChinese(part));
          chineseParts.forEach((part) => {
            this.addExtractedText(part, filePath, 'template-string', {
              line: node.start || 0,
              column: 0,
            });
          });
        }
      }
    }

    // 处理文本节点
    if (node.type === 3 && node.text) {
      const text = node.text.trim();
      if (this.containsChinese(text)) {
      }
    }

    // 递归处理子节点
    if (node.children) {
      node.children.forEach((child) => {
        this.traverseTemplateAST(child, filePath);
      });
    }
  }

4. Vue Script AST解析

javascript 复制代码
/**
   * 脚本AST语法树处理
   * @param {*} astTree
   * @param {*} filePath
   */
  traverseScriptAst(astTree, filePath) {
    traverse(astTree, {
      // 捕获所有字符串字面量中的中文
      StringLiteral: (path) => {
        const value = path.node.value;
        if (this.containsChinese(value)) {
          this.addExtractedText(value, filePath, 'script-string', {
            line: path.node.loc ? path.node.loc.start.line : 0,
            column: path.node.loc ? path.node.loc.start.column : 0,
          });
        }
      },
      // 捕获模板字符串中的中文
      TemplateLiteral: (path) => {
        path.node.quasis.forEach((quasi) => {
          if (quasi.value.raw && this.containsChinese(quasi.value.raw)) {
            this.addExtractedText(quasi.value.raw, filePath, 'script-template', {
              line: quasi.loc ? quasi.loc.start.line : 0,
              column: quasi.loc ? quasi.loc.start.column : 0,
            });
          }
        });
      },
    });
  }

5. 语言包生成

javascript 复制代码
 /**
   * 输出结果
   */
  outputResults() {
    const results = Array.from(this.extractedTexts.values());
    console.log(results);
    // 生成中文映射文件
    const chineseMap = {};
    results.forEach((item) => {
      chineseMap[item.text] = item.text;
    });
    const entries = Object.entries(chineseMap);

    // 写入JSON文件
    const outputFile = path.join(this.options.outputPath, 'extracted-chinese.json');
    fs.writeFileSync(outputFile, JSON.stringify(chineseMap, null, 2), 'utf-8');

    // 生成对象属性字符串
    const properties = entries
      .map(([key, value]) => {
        // 转义特殊字符
        const escapedValue = value
          .replace(/\\/g, '\\\\')
          .replace(/'/g, "\\'")
          .replace(/\n/g, '\\n')
          .replace(/\r/g, '\\r');
        return `  '${key}': '${escapedValue}'`;
      })
      .join(',\n');

    // 生成zh.js文件
    const zhJsPath = path.join(this.options.outputPath, 'zh.js');
    fs.writeFileSync(
      zhJsPath,
      `// 自动生成的中文语言包
// 生成时间: ${new Date().toLocaleString()}
// 共提取 ${entries.length} 个中文片段

export default {
${properties}
};`,
      'utf-8'
    );

    console.log(`提取完成,共提取 ${results.length} 个中文片段`);
    console.log(`结果已保存到: ${outputFile}`);
  }

6. 其它辅助方法

typescript 复制代码
 /**
   * 添加提取的文本
   */
  addExtractedText(text, filePath, type, location) {
    this.extractedTexts.set(text, {
      text,
      filePath,
      type,
      location,
    });
    this.fileStats.extractedCount++;
  }

  /**
   * 检查是否包含中文
   */
  containsChinese(text) {
    return /[\u4e00-\u9fa5]/.test(text);
  }

四、使用方式

1. Webpack配置

java 复制代码
// webpack.config.extract.js
const MatureChineseExtractorPlugin = require('./MatureChineseExtractorPlugin');

module.exports = {
  // ... 其他配置
  plugins: [
    new MatureChineseExtractorPlugin({
      outputPath: './i18n',
      verbose: true
    })
  ]
};

2. 运行配置

package.json(script)

json 复制代码
"extract": "webpack --config webpack.config.extract.js  --mode development "

运行

arduino 复制代码
npm run extract

3. 项目案例

xml 复制代码
<template>
  <div id="app">
    <header class="header">
      <h1>{{ '欢迎使用Vue2国际化演示' }}</h1>
      <p>{{ '这是一个完整的国际化解决方案演示项目' }}</p>
    </header>

    <nav class="nav">
      <button @click="currentView = 'home'" :class="{ active: currentView === 'home' }">
        {{ '首页' }}
      </button>
      <button @click="currentView = 'user'" :class="{ active: currentView === 'user' }">
        {{ '用户管理' }}
      </button>
      <button @click="currentView = 'product'" :class="{ active: currentView === 'product' }">
        {{ '商品管理' }}
      </button>
    </nav>

    <main class="main">
      <div class="status-bar">
        <span>当前页面:{{ currentComponent }}</span>
        <span>{{ userInfo.name }},欢迎使用系统</span>
        <span>今天是{{ currentDate }},祝您工作愉快</span>
      </div>
      <component :is="currentComponent"></component>
    </main>

    <footer class="footer">
      <p>{{ '版权所有 © 2024 Vue2国际化演示项目' }}</p>
    </footer>
  </div>
</template>

<script>
import HomePage from './components/HomePage.vue';
import UserManagement from './components/UserManagement.vue';
import ProductManagement from './components/ProductManagement.vue';

export default {
  name: 'App',
  components: {
    HomePage,
    UserManagement,
    ProductManagement,
  },
  data() {
    return {
      currentView: 'home',
      userInfo: {
        name: '张三',
      },
      currentDate: new Date().toLocaleDateString(),
    };
  },
  methods: {
    sayHello() {
      console.log('你好,这是一个Vue2国际化改造案例~');
    },
  },
  computed: {
    currentComponent() {
      const components = {
        home: '首页',
        user: '用户管理',
        product: '商品管理',
      };
      return components[this.currentView];
    },
  },
};
</script>

4. 提取结果

arduino 复制代码
i18n/
└──zh.js                    # 中文语言包

// 自动生成的中文语言包
// 生成时间: 2025/9/1 11:38:04
// 共提取 12 个中文片段

export default {
  '欢迎使用Vue2国际化演示': '欢迎使用Vue2国际化演示',
  '这是一个完整的国际化解决方案演示项目': '这是一个完整的国际化解决方案演示项目',
  '首页': '首页',
  '用户管理': '用户管理',
  '商品管理': '商品管理',
  '当前页面:': '当前页面:',
  ',欢迎使用系统': ',欢迎使用系统',
  '今天是': '今天是',
  ',祝您工作愉快': ',祝您工作愉快',
  '版权所有 © 2024 Vue2国际化演示项目': '版权所有 © 2024 Vue2国际化演示项目',
  '张三': '张三',
  '你好,这是一个Vue2国际化改造案例~': '你好,这是一个Vue2国际化改造案例~'
};

五、总结

通过自定义Webpack插件的方式,我们成功实现了Vue2项目中文文本的自动提取,大大提升了国际化改造的效率。这种基于AST分析的方案不仅准确率高,而且可以灵活扩展,是大型项目国际化改造的理想选择,后面会对自动化注入流程进行拆解~

相关推荐
Juchecar6 小时前
解决Windows下根目录运行 pnpm dev “无法启动 Vite 前端,只能启动 Express 后端”
前端·后端·node.js
薛定谔的算法7 小时前
面试官问你知道哪些es6新特性?赶紧收好,猜这里一定有你不知道的?
前端·javascript·面试
BUG收容所所长7 小时前
为什么浏览器要有同源策略?跨域问题怎么优雅解决?——一份面向初学者的全流程解读
前端·面试·浏览器
用户47949283569157 小时前
🚀 打包工具文件名哈希深度解析:为什么bundle.js变成了bundle.abc123.js
前端·javascript·面试
晴空雨7 小时前
遇到第三方库 bug 怎么办?5 种修改外部依赖的方法帮你搞定
前端·javascript·架构
Danny_FD7 小时前
前端开发提效神器:`concurrently` 实战指南
前端
早起的年轻人7 小时前
Flutter WebAssembly (Wasm) 支持 - 实用指南
前端·flutter
木西7 小时前
React Native DApp 开发全栈实战·从 0 到 1 系列(铸造NFT-前端部分)
前端·react native·web3
yzzzzzzzzzzzzzzzzz7 小时前
ES6/ES2015 - ES16/ES2025
前端·ecmascript·es6