Vue2存量项目国际化改造踩坑
一、背景
在各类业务场景中,国际化作为非常重要的一部分已经有非常多成熟的方案,但对于一些存量项目则存在非常的改造成本,本文将分享一个的Vue2项目国际化改造方案,通过自定义Webpack插件自动提取中文文本,大大提升改造效率。
二、核心思路(提取阶段)
通过开放自定义Webpack插件,利用AST(抽象语法树)分析技术,自动扫描Vue组件中的中文文本
- 模板中的中文:插值表达式、文本节点、属性值
- 脚本中的中文:字符串字面量、模板字符串
- 自动生成语言包:输出标准的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分析的方案不仅准确率高,而且可以灵活扩展,是大型项目国际化改造的理想选择,后面会对自动化注入流程进行拆解~