三个国际化实用的babel插件

1. 翻译键提取 (Key Extraction) 插件

这个插件会遍历你的源代码,查找所有形如 t('key')this.$t('key') 的函数调用,并提取出其中的翻译键(字符串字面量),最终将所有提取到的唯一键写入一个 JSON 文件。

文件:babel-plugin-i18n-key-extractor.js

js 复制代码
// 导入 Node.js 的文件系统模块和路径模块
const fs = require('fs');
const path = require('path');

/**
 * Babel 插件:i18n 翻译键提取器
 * 遍历 AST,查找 t() 或 this.$t() 调用,提取翻译键并保存到文件。
 *
 * @param {object} babel - Babel 核心对象,包含 types 等工具
 * @returns {object} Babel 插件对象
 */
module.exports = function({ types: t }) {
  // 用于存储提取到的所有唯一翻译键的 Set 集合
  // Set 自动处理去重
  const extractedKeys = new Set();

  return {
    // 插件名称,方便调试和日志输出
    name: "i18n-key-extractor",

    // visitor 对象定义了在 AST 遍历过程中要访问的节点类型
    visitor: {
      /**
       * 访问 CallExpression (函数调用) 节点
       * 例如:t('hello'), this.$t('world')
       * @param {object} path - AST 节点的路径对象,包含节点信息和操作方法
       */
      CallExpression(path) {
        // 获取函数调用的表达式 (例如 't' 或 'this.$t')
        const callee = path.node.callee;

        let isI18nCall = false; // 标记当前调用是否是 i18n 翻译函数

        // 检查是否是 't(...)' 形式的调用
        if (t.isIdentifier(callee) && callee.name === 't') {
          isI18nCall = true;
        }
        // 检查是否是 'this.$t(...)' 形式的调用
        else if (
          t.isMemberExpression(callee) && // 成员表达式,如 obj.property
          t.isThisExpression(callee.object) && // 对象是 'this'
          t.isIdentifier(callee.property) && // 属性是标识符
          callee.property.name === '$t' // 属性名是 '$t'
        ) {
          isI18nCall = true;
        }

        if (isI18nCall) {
          // 获取函数调用的第一个参数 (通常是翻译键)
          const keyArg = path.node.arguments[0];

          // 检查第一个参数是否是字符串字面量 (例如 'hello')
          if (t.isStringLiteral(keyArg)) {
            extractedKeys.add(keyArg.value); // 将键值添加到 Set 中
          }
          // 检查第一个参数是否是模板字符串字面量 (例如 `hello ${name}`)
          else if (t.isTemplateLiteral(keyArg)) {
            // 对于模板字符串,我们通常只提取其静态部分 (quasis)
            // 动态部分 (expressions) 无法作为静态翻译键
            keyArg.quasis.forEach(quasi => {
              if (quasi.value.raw) {
                extractedKeys.add(quasi.value.raw);
              }
            });
            // 打印警告,提示开发者翻译键应为静态字符串
            console.warn(
              `[i18n-key-extractor] 警告: 发现模板字符串作为翻译键 '${path.getSource()}'。` +
              `请确保翻译键是纯字符串字面量,以便正确提取和管理。`
            );
          }
          // 如果是其他类型的参数(如变量),则无法提取为静态键
          else {
            console.warn(
              `[i18n-key-extractor] 警告: 发现非字符串字面量或模板字符串作为翻译键 '${path.getSource()}'。` +
              `此插件无法提取动态翻译键。`
            );
          }
        }
      },

      /**
       * Program.exit 方法在整个 AST 遍历完成后执行
       * 适合在此处进行最终的数据处理和文件写入
       * @param {object} path - AST 节点的路径对象
       * @param {object} state - 插件的状态对象,包含 opts (插件选项)
       */
      Program: {
        exit(path, state) {
          // 从插件选项中获取输出文件的路径,默认为 'i18n_keys.json'
          const outputPath = state.opts.output || 'i18n_keys.json';
          // 获取输出文件所在的目录
          const outputDir = path.dirname(outputPath);

          // 确保输出目录存在,如果不存在则递归创建
          if (!fs.existsSync(outputDir)) {
            fs.mkdirSync(outputDir, { recursive: true });
          }

          // 将提取到的键从 Set 转换为数组,并进行排序,以便输出的文件内容稳定
          const sortedKeys = Array.from(extractedKeys).sort();
          // 将键数组写入 JSON 文件,格式化为两空格缩进
          fs.writeFileSync(outputPath, JSON.stringify(sortedKeys, null, 2), 'utf-8');

          console.log(
            `[i18n-key-extractor] 成功提取 ${sortedKeys.length} 个翻译键到 ${outputPath}`
          );
        }
      }
    }
  };
};

2. 静态翻译 / 编译时替换 (Static Translation / Compile-time Replacement) 插件

这个插件会在编译时将 t('key')this.$t('key') 调用替换为实际的翻译文本。它需要一个翻译文件作为输入。注意:此插件只处理纯字符串键和简单的字符串替换,不处理插值(如 {name})或复数形式。 对于需要插值或复数的场景,通常仍需运行时 i18n 库。

文件:babel-plugin-i18n-static-translator.js

js 复制代码
// 导入 Node.js 的文件系统模块和路径模块
const fs = require('fs');
const path = require('path');

/**
 * Babel 插件:i18n 静态翻译器
 * 在编译时将 t() 或 this.$t() 调用替换为实际的翻译文本。
 *
 * @param {object} babel - Babel 核心对象,包含 types 等工具
 * @returns {object} Babel 插件对象
 */
module.exports = function({ types: t }) {
  let translations = {}; // 用于存储加载的翻译资源

  return {
    name: "i18n-static-translator",

    /**
     * pre 方法在 AST 遍历开始前执行
     * 适合在此处进行插件的初始化工作,例如加载翻译文件
     * @param {object} state - 插件的状态对象,包含 opts (插件选项)
     */
    pre(state) {
      // 从插件选项中获取翻译文件的路径
      const translationFilePath = state.opts.translationFile;
      if (!translationFilePath) {
        throw new Error(
          "[i18n-static-translator] 错误: 请在插件选项中提供 'translationFile' 路径,例如 { translationFile: 'path/to/en.json' }。"
        );
      }

      try {
        // 解析翻译文件的绝对路径
        const absolutePath = path.resolve(process.cwd(), translationFilePath);
        // 同步读取文件内容
        const fileContent = fs.readFileSync(absolutePath, 'utf-8');
        // 解析 JSON 内容到 translations 对象
        translations = JSON.parse(fileContent);
        console.log(`[i18n-static-translator] 成功加载翻译文件: ${absolutePath}`);
      } catch (e) {
        console.error(
          `[i18n-static-translator] 错误: 无法加载或解析翻译文件 ${translationFilePath}:`,
          e.message
        );
        translations = {}; // 如果加载失败,清空翻译数据,避免后续翻译查找出错
      }
    },

    visitor: {
      /**
       * 访问 CallExpression (函数调用) 节点
       * @param {object} path - AST 节点的路径对象
       */
      CallExpression(path) {
        const callee = path.node.callee;

        let isI18nCall = false; // 标记当前调用是否是 i18n 翻译函数
        if (t.isIdentifier(callee) && callee.name === 't') {
          isI18nCall = true;
        } else if (
          t.isMemberExpression(callee) &&
          t.isThisExpression(callee.object) &&
          t.isIdentifier(callee.property) &&
          callee.property.name === '$t'
        ) {
          isI18nCall = true;
        }

        if (isI18nCall) {
          // 获取函数调用的第一个参数 (翻译键)
          const keyArg = path.node.arguments[0];

          // 仅处理字符串字面量作为翻译键的情况
          if (t.isStringLiteral(keyArg)) {
            const key = keyArg.value;
            // 从加载的翻译资源中查找对应的翻译文本
            let translatedText = translations[key];

            if (translatedText !== undefined) {
              // 如果找到翻译,则将整个函数调用节点替换为新的字符串字面量节点
              // 例如:t('greeting') 替换为 "Hello!"
              path.replaceWith(t.stringLiteral(translatedText));
              // 标记为跳过,避免再次访问这个已替换的节点,提高性能
              path.skip();
            } else {
              // 如果未找到翻译,可以根据需求选择:
              // 1. 保持原样 (默认行为,不进行替换)
              // 2. 替换为原始键 (方便调试)
              // 3. 替换为带提示的字符串,例如 `[MISSING_TRANSLATION:key]`
              // 4. 抛出错误,阻止编译
              console.warn(
                `[i18n-static-translator] 警告: 未在翻译文件中找到键 '${key}'。` +
                `将保持原样或根据配置处理。`
              );
              // 示例:替换为提示信息
              // path.replaceWith(t.stringLiteral(`[MISSING_TRANSLATION:${key}]`));
            }
          } else {
            console.warn(
              `[i18n-static-translator] 警告: 发现非字符串字面量作为翻译键 '${path.getSource()}'。` +
              `静态翻译插件仅支持字符串字面量。`
            );
          }
        }
      }
    }
  };
};

3. 翻译键校验 (Key Validation) 插件

这个插件会在编译时检查代码中使用的 t('key')this.$t('key') 调用中的键是否存在于一个给定的参考翻译文件中。如果发现不存在的键,它会抛出编译错误。

文件:babel-plugin-i18n-key-validator.js

js 复制代码
// 导入 Node.js 的文件系统模块和路径模块
const fs = require('fs');
const path = require('path');

/**
 * Babel 插件:i18n 翻译键校验器
 * 检查代码中使用的翻译键是否在提供的参考翻译文件中存在。
 *
 * @param {object} babel - Babel 核心对象,包含 types 等工具
 * @returns {object} Babel 插件对象
 */
module.exports = function({ types: t }) {
  // 用于存储所有有效翻译键的 Set 集合
  let validKeys = new Set();

  return {
    name: "i18n-key-validator",

    /**
     * pre 方法在 AST 遍历开始前执行
     * 适合在此处加载参考翻译文件并提取所有有效键
     * @param {object} state - 插件的状态对象,包含 opts (插件选项)
     */
    pre(state) {
      // 从插件选项中获取包含所有有效键的参考翻译文件路径
      const referenceTranslationFile = state.opts.referenceTranslationFile;
      if (!referenceTranslationFile) {
        throw new Error(
          "[i18n-key-validator] 错误: 请在插件选项中提供 'referenceTranslationFile' 路径,例如 { referenceTranslationFile: 'path/to/en.json' }。"
        );
      }

      try {
        // 解析参考翻译文件的绝对路径
        const absolutePath = path.resolve(process.cwd(), referenceTranslationFile);
        // 同步读取文件内容
        const fileContent = fs.readFileSync(absolutePath, 'utf-8');
        // 解析 JSON 内容
        const allTranslations = JSON.parse(fileContent);

        /**
         * 辅助函数:递归地从 JSON 对象中提取所有嵌套的键
         * 例如:{ "messages": { "new_mail": "..." } } 会提取出 "messages.new_mail"
         * @param {object} obj - 当前要提取键的对象
         * @param {string} prefix - 当前键的前缀,用于构建完整路径
         */
        function extractKeys(obj, prefix = '') {
          for (const key in obj) {
            // 确保是对象自身的属性,而不是原型链上的
            if (Object.prototype.hasOwnProperty.call(obj, key)) {
              // 构建当前键的完整路径
              const currentKey = prefix ? `${prefix}.${key}` : key;
              // 如果当前值是对象且不为 null,则递归调用
              if (typeof obj[key] === 'object' && obj[key] !== null) {
                extractKeys(obj[key], currentKey);
              } else {
                // 否则,将完整键路径添加到 validKeys Set 中
                validKeys.add(currentKey);
              }
            }
          }
        }
        extractKeys(allTranslations); // 开始提取键
        console.log(
          `[i18n-key-validator] 成功加载 ${validKeys.size} 个有效翻译键用于校验。`
        );
      } catch (e) {
        console.error(
          `[i18n-key-validator] 错误: 无法加载或解析参考翻译文件 ${referenceTranslationFile}:`,
          e.message
        );
        validKeys = new Set(); // 如果加载失败,清空有效键,避免后续校验出错
      }
    },

    visitor: {
      /**
       * 访问 CallExpression (函数调用) 节点
       * @param {object} path - AST 节点的路径对象
       */
      CallExpression(path) {
        const callee = path.node.callee;

        let isI18nCall = false; // 标记当前调用是否是 i18n 翻译函数
        if (t.isIdentifier(callee) && callee.name === 't') {
          isI18nCall = true;
        } else if (
          t.isMemberExpression(callee) &&
          t.isThisExpression(callee.object) &&
          t.isIdentifier(callee.property) &&
          callee.property.name === '$t'
        ) {
          isI18nCall = true;
        }

        if (isI18nCall) {
          // 获取函数调用的第一个参数 (翻译键)
          const keyArg = path.node.arguments[0];

          // 仅处理字符串字面量作为翻译键的情况
          if (t.isStringLiteral(keyArg)) {
            const key = keyArg.value;
            // 检查当前使用的键是否在有效键集合中
            if (!validKeys.has(key)) {
              // 如果键不存在,则抛出编译错误
              // path.buildCodeFrameError 会生成一个包含代码位置信息的错误
              throw path.buildCodeFrameError(
                `翻译键 '${key}' 未在参考翻译文件中找到。请检查键名或添加相应的翻译。`
              );
            }
          } else {
            console.warn(
              `[i18n-key-validator] 警告: 发现非字符串字面量作为翻译键 '${path.getSource()}'。` +
              `校验插件仅支持字符串字面量。`
            );
          }
        }
      }
    }
  };
};

如何配置和使用这些 Babel 插件

为了演示这些插件,我们将设置一个简单的项目结构。

1. 项目初始化

创建一个新文件夹,例如 my-i18n-babel-demo

进入该文件夹并初始化 npm 项目:

perl 复制代码
mkdir my-i18n-babel-demo
cd my-i18n-babel-demo
npm init -y

2. 安装 Babel 依赖

bash 复制代码
npm install -D @babel/core @babel/cli @babel/preset-env

3. 创建插件文件

将上面提供的三个插件代码分别保存为:

  • babel-plugin-i18n-key-extractor.js
  • babel-plugin-i18n-static-translator.js
  • babel-plugin-i18n-key-validator.js
    你可以将它们放在项目根目录。

4. 创建示例源代码

创建 src/app.js

js 复制代码
// src/app.js
function renderContent() {
  // 这是用于测试翻译键提取和静态翻译的键
  console.log(t('greeting'));
  console.log(this.$t('welcome_message'));
  console.log(t('messages.new_mail')); // 假设这是嵌套键

  // 这是一个故意错误的翻译键,用于测试校验插件
  console.log(t('non_existent_key'));

  // 模板字符串键 (提取器会警告并提取静态部分,校验器/静态翻译器会跳过)
  const user = 'John';
  console.log(t(`user.welcome.${user}`));
}

renderContent();

5. 创建翻译文件

创建 translations/en.json (用于静态翻译和校验):

js 复制代码
// translations/en.json
{
  "greeting": "Hello!",
  "welcome_message": "Welcome to our application!",
  "messages": {
    "new_mail": "You have new mail."
  },
  "user": {
    "welcome": {
      "John": "Welcome, John!"
    }
  }
}

6. 配置 Babel

创建 babel.config.js 文件在项目根目录。你可以根据需要启用或禁用不同的插件来测试它们。

js 复制代码
// babel.config.js
module.exports = {
  presets: [
    '@babel/preset-env' // 用于转换 ES6+ 语法到 ES5
  ],
  plugins: [
    // 插件1: 翻译键提取
    // 运行此插件时,建议暂时注释掉其他插件,特别是校验插件,因为它可能会因为缺失键而报错。
    // [
    //   './babel-plugin-i18n-key-extractor.js',
    //   {
    //     output: 'extracted_keys/i18n_keys.json' // 指定提取键的输出文件路径
    //   }
    // ],

    // 插件2: 静态翻译 / 编译时替换
    // 运行此插件时,建议暂时注释掉其他插件,特别是校验插件,因为它可能会因为缺失键而报错。
    // [
    //   './babel-plugin-i18n-static-translator.js',
    //   {
    //     translationFile: 'translations/en.json' // 指定要使用的翻译文件
    //   }
    // ],

    // 插件3: 翻译键校验
    // 运行此插件时,建议暂时注释掉其他插件,特别是静态翻译插件,因为它会改变 AST。
    // [
    //   './babel-plugin-i18n-key-validator.js',
    //   {
    //     referenceTranslationFile: 'translations/en.json' // 指定参考翻译文件,用于校验
    //   }
    // ]
  ]
};

7. 添加 npm 脚本

package.jsonscripts 中添加:

json 复制代码
{
  "name": "my-i18n-babel-demo",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "build": "babel src/app.js --out-file dist/app.js",
    "extract-keys": "babel src/app.js --out-file /dev/null --config-file ./babel.config.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.24.8",
    "@babel/core": "^7.24.8",
    "@babel/preset-env": "^7.24.8"
  }
}
  • npm run build: 运行 Babel 编译 src/app.js 并输出到 dist/app.js。根据 babel.config.js 中启用的插件执行相应操作。
  • npm run extract-keys: 专门用于运行键提取插件。--out-file /dev/null (Windows 上为 NUL) 意味着不生成实际的输出文件,因为我们只关心插件的副作用(写入 i18n_keys.json)。

运行测试

测试插件1: 翻译键提取

  1. babel.config.js 中,只启用 babel-plugin-i18n-key-extractor.js 插件,注释掉其他两个。
  2. 运行命令:npm run extract-keys
  3. 检查项目根目录下是否生成了 extracted_keys/i18n_keys.json 文件,其中应该包含提取到的翻译键(包括 greeting, welcome_message, messages.new_mail, user.welcome.John)。

测试插件2: 静态翻译 / 编译时替换

  1. babel.config.js 中,只启用 babel-plugin-i18n-static-translator.js 插件,注释掉其他两个。
  2. 确保 translations/en.json 存在。
  3. 运行命令:npm run build
  4. 检查 dist/app.js 文件。你会发现 t('greeting') 等调用已经被替换为 "Hello!" 等字符串字面量。t('non_existent_key') 和模板字符串 t(user.welcome.${user}) 会保持原样,因为插件无法处理它们。

测试插件3: 翻译键校验

  1. babel.config.js 中,只启用 babel-plugin-i18n-key-validator.js 插件,注释掉其他两个。
  2. 确保 translations/en.json 存在。
  3. 运行命令:npm run build
  4. 由于 src/app.js 中包含 t('non_existent_key'),你应该会看到 Babel 编译报错,提示该键未在参考文件中找到。如果将 t('non_existent_key')src/app.js 中删除,或者在 translations/en.json 中添加 non_existent_key,编译将成功。
相关推荐
超人不会飛8 分钟前
就着HTTP聊聊SSE的前世今生
前端·javascript·http
蓝胖子的多啦A梦11 分钟前
Vue+element 日期时间组件选择器精确到分钟,禁止选秒的配置
前端·javascript·vue.js·elementui·时间选选择器·样式修改
夏天想14 分钟前
vue2+elementui使用compressorjs压缩上传的图片
前端·javascript·elementui
今晚打老虎z22 分钟前
dotnet-env: .NET 开发者的环境变量加载工具
前端·chrome·.net
用户38022585982428 分钟前
vue3源码解析:diff算法之patchChildren函数分析
前端·vue.js
烛阴33 分钟前
XPath 进阶:掌握高级选择器与路径表达式
前端·javascript
小鱼小鱼干37 分钟前
【JS/Vue3】关于Vue引用透传
前端
JavaDog程序狗39 分钟前
【前端】HTML+JS 实现超燃小球分裂全过程
前端
独立开阀者_FwtCoder43 分钟前
URL地址末尾加不加 "/" 有什么区别
前端·javascript·github
独立开阀者_FwtCoder1 小时前
Vue3 新特性:原来watch 也能“暂停”和“恢复”了!
前端·javascript·github