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.json
的 scripts
中添加:
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: 翻译键提取
- 在
babel.config.js
中,只启用babel-plugin-i18n-key-extractor.js
插件,注释掉其他两个。 - 运行命令:
npm run extract-keys
- 检查项目根目录下是否生成了
extracted_keys/i18n_keys.json
文件,其中应该包含提取到的翻译键(包括greeting
,welcome_message
,messages.new_mail
,user.welcome.John
)。
测试插件2: 静态翻译 / 编译时替换
- 在
babel.config.js
中,只启用babel-plugin-i18n-static-translator.js
插件,注释掉其他两个。 - 确保
translations/en.json
存在。 - 运行命令:
npm run build
- 检查
dist/app.js
文件。你会发现t('greeting')
等调用已经被替换为"Hello!"
等字符串字面量。t('non_existent_key')
和模板字符串t(
user.welcome.${user})
会保持原样,因为插件无法处理它们。
测试插件3: 翻译键校验
- 在
babel.config.js
中,只启用babel-plugin-i18n-key-validator.js
插件,注释掉其他两个。 - 确保
translations/en.json
存在。 - 运行命令:
npm run build
- 由于
src/app.js
中包含t('non_existent_key')
,你应该会看到 Babel 编译报错,提示该键未在参考文件中找到。如果将t('non_existent_key')
从src/app.js
中删除,或者在translations/en.json
中添加non_existent_key
,编译将成功。