vue-i18n-auto-plugin 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -0
- package/dist/index.cjs.js +555 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +532 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/scriptTransform.d.ts +2 -0
- package/dist/scriptTransform.d.ts.map +1 -0
- package/dist/templateTransform.d.ts +3 -0
- package/dist/templateTransform.d.ts.map +1 -0
- package/dist/utils.d.ts +15 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# vite-plugin-i18n-auto
|
|
2
|
+
|
|
3
|
+
Vite 插件,用于自动将 Vue/TypeScript 项目中的中文硬编码转换为 i18n 函数调用。
|
|
4
|
+
|
|
5
|
+
## 功能特点
|
|
6
|
+
|
|
7
|
+
- ✅ **基于 AST 解析**:使用 Babel 和 Vue Compiler SFC 进行代码解析
|
|
8
|
+
- ✅ **智能过滤**:自动跳过 import/export、对象键、函数参数等不应国际化的上下文
|
|
9
|
+
- ✅ **模板字符串支持**:自动提取 `${variable}` 变量,生成 `t('key', { name1: variable })` 格式
|
|
10
|
+
- ✅ **Vue 文件完整支持**:同时处理 `<template>` 和 `<script>` 部分
|
|
11
|
+
- ✅ **自动注入 i18n**:检测到中文时自动注入 `useI18n` 导入
|
|
12
|
+
- ✅ **自动生成 Key**:基于 HMAC-SHA256 生成唯一 key
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install vite-plugin-i18n-auto -D
|
|
18
|
+
# 或
|
|
19
|
+
yarn add vite-plugin-i18n-auto -D
|
|
20
|
+
# 或
|
|
21
|
+
pnpm add vite-plugin-i18n-auto -D
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 使用方法
|
|
25
|
+
|
|
26
|
+
在 `vite.config.ts` 中配置:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { defineConfig } from 'vite';
|
|
30
|
+
import vue from '@vitejs/plugin-vue';
|
|
31
|
+
import I18nAutoPlugin from 'vite-plugin-i18n-auto';
|
|
32
|
+
|
|
33
|
+
export default defineConfig({
|
|
34
|
+
plugins: [
|
|
35
|
+
vue(),
|
|
36
|
+
I18nAutoPlugin({
|
|
37
|
+
i18nPath: 'src/locales/zh-CN.ts', // 主语言文件路径
|
|
38
|
+
langPath: ['src/locales/en.ts'], // 其他语言文件路径数组
|
|
39
|
+
regi18n: 'useI18n', // 判断是否已引入i18n的标识
|
|
40
|
+
excludes: ['locale', 'useI18n'], // 排除的文件名
|
|
41
|
+
tempText: 't', // 模板中使用的函数名
|
|
42
|
+
jsText: 't', // JS中使用的函数名
|
|
43
|
+
injectToJS: `\nimport { useI18n } from 'vue-i18n'\nconst { t } = useI18n()\n`,
|
|
44
|
+
delay: 1000, // 防抖延迟时间(毫秒)
|
|
45
|
+
reserveKeys: [], // 生产环境需要保留的key
|
|
46
|
+
runBuild: true, // 是否在打包时执行
|
|
47
|
+
keyLength: 16, // 生成的key长度
|
|
48
|
+
cryptoKey: 'i18n', // 加密密钥
|
|
49
|
+
preText: 'common.', // key生成的前缀
|
|
50
|
+
}),
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 配置选项
|
|
56
|
+
|
|
57
|
+
| 选项 | 类型 | 默认值 | 说明 |
|
|
58
|
+
|------|------|--------|------|
|
|
59
|
+
| `i18nPath` | `string` | `'src/locales/zh-CN.ts'` | 主语言文件路径(通常是中文) |
|
|
60
|
+
| `langPath` | `string[]` | `['src/locales/en.ts']` | 其他语言文件路径数组 |
|
|
61
|
+
| `regi18n` | `string` | `'useI18n'` | 判断是否已引入i18n的标识 |
|
|
62
|
+
| `excludes` | `string[]` | `['locale', 'useI18n']` | 排除的文件名或路径 |
|
|
63
|
+
| `tempText` | `string` | `'t'` | 模板中使用的函数名 |
|
|
64
|
+
| `jsText` | `string` | `'t'` | JS中使用的函数名 |
|
|
65
|
+
| `injectToJS` | `string` | `'\nimport { useI18n } from \'vue-i18n\'\nconst { t } = useI18n()\n'` | 自动注入的代码 |
|
|
66
|
+
| `delay` | `number` | `1000` | 防抖延迟时间(毫秒) |
|
|
67
|
+
| `reserveKeys` | `string[]` | `[]` | 生产环境需要保留的key |
|
|
68
|
+
| `runBuild` | `boolean` | `false` | 是否在打包时执行 |
|
|
69
|
+
| `keyLength` | `number` | `16` | 生成的key长度 |
|
|
70
|
+
| `cryptoKey` | `string` | `'i18n'` | 加密密钥 |
|
|
71
|
+
| `preText` | `string` | `''` | key生成的前缀 |
|
|
72
|
+
|
|
73
|
+
## 转换示例
|
|
74
|
+
|
|
75
|
+
### 转换前
|
|
76
|
+
|
|
77
|
+
```vue
|
|
78
|
+
<template>
|
|
79
|
+
<div>
|
|
80
|
+
<h1>欢迎使用</h1>
|
|
81
|
+
<p :title="'点击查看详情'">这是一个测试</p>
|
|
82
|
+
</div>
|
|
83
|
+
</template>
|
|
84
|
+
|
|
85
|
+
<script setup>
|
|
86
|
+
const message = `你好,${userName}!`;
|
|
87
|
+
</script>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 转换后
|
|
91
|
+
|
|
92
|
+
```vue
|
|
93
|
+
<template>
|
|
94
|
+
<div>
|
|
95
|
+
<h1>{{ t('common.xxxxx') }}</h1>
|
|
96
|
+
<p :title="t('common.yyyyy')">{{ t('common.zzzzz') }}</p>
|
|
97
|
+
</div>
|
|
98
|
+
</template>
|
|
99
|
+
|
|
100
|
+
<script setup>
|
|
101
|
+
import { useI18n } from 'vue-i18n'
|
|
102
|
+
const { t } = useI18n()
|
|
103
|
+
const message = t('common.aaaaa', { name1: userName });
|
|
104
|
+
</script>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## 语言文件格式
|
|
108
|
+
|
|
109
|
+
### zh-CN.ts
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
export default {
|
|
113
|
+
"common.xxxxx": "欢迎使用",
|
|
114
|
+
"common.yyyyy": "点击查看详情",
|
|
115
|
+
"common.zzzzz": "这是一个测试",
|
|
116
|
+
"common.aaaaa": "你好,{name1}!"
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### en.ts
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
export default {
|
|
124
|
+
"common.xxxxx": "Welcome",
|
|
125
|
+
"common.yyyyy": "Click to view details",
|
|
126
|
+
"common.zzzzz": "This is a test",
|
|
127
|
+
"common.aaaaa": "Hello, {name1}!"
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## 注意事项
|
|
132
|
+
|
|
133
|
+
1. **开发环境性能**:在开发环境启用可能会影响构建速度,建议只在生产环境启用
|
|
134
|
+
2. **自动注入**:插件会自动检测并注入 `useI18n`,但如果文件结构复杂可能无法正确注入
|
|
135
|
+
3. **Key 生成**:相同的中文文本总是生成相同的 key
|
|
136
|
+
4. **文件格式**:语言文件必须使用 `export default` 格式
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT
|
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var compilerSfc = require('@vue/compiler-sfc');
|
|
4
|
+
var fs = require('fs');
|
|
5
|
+
var path = require('path');
|
|
6
|
+
var babelParser = require('@babel/parser');
|
|
7
|
+
var _generate = require('@babel/generator');
|
|
8
|
+
var _traverse = require('@babel/traverse');
|
|
9
|
+
var crypto = require('crypto');
|
|
10
|
+
var JSON5 = require('json5');
|
|
11
|
+
|
|
12
|
+
function _interopNamespaceDefault(e) {
|
|
13
|
+
var n = Object.create(null);
|
|
14
|
+
if (e) {
|
|
15
|
+
Object.keys(e).forEach(function (k) {
|
|
16
|
+
if (k !== 'default') {
|
|
17
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
18
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () { return e[k]; }
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
n.default = e;
|
|
26
|
+
return Object.freeze(n);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var babelParser__namespace = /*#__PURE__*/_interopNamespaceDefault(babelParser);
|
|
30
|
+
var _generate__namespace = /*#__PURE__*/_interopNamespaceDefault(_generate);
|
|
31
|
+
var _traverse__namespace = /*#__PURE__*/_interopNamespaceDefault(_traverse);
|
|
32
|
+
|
|
33
|
+
// 中文字符匹配函数(判断字符串是否包含中文字符)
|
|
34
|
+
function containsChinese(str) {
|
|
35
|
+
return /[\u4e00-\u9fa5]/.test(str);
|
|
36
|
+
}
|
|
37
|
+
// 收集字符串中的字符, '我的测试' + 'abc' + '测试呀', 针对这种字符串的拼接处理
|
|
38
|
+
function extractQuotedStrings(str) {
|
|
39
|
+
// 如果是 `这种拼接的`
|
|
40
|
+
const regex = /(["'])(.*?)\1/g;
|
|
41
|
+
let match;
|
|
42
|
+
const matches = [];
|
|
43
|
+
while ((match = regex.exec(str)) !== null) {
|
|
44
|
+
matches.push(match[0]);
|
|
45
|
+
}
|
|
46
|
+
return matches;
|
|
47
|
+
}
|
|
48
|
+
// 对js 字符串的模板进行处理 类似 `我的测试${variable}`
|
|
49
|
+
function extractTransformString(str) {
|
|
50
|
+
// 正则表达式匹配 ${variable} 中的内容
|
|
51
|
+
const regex = /\$\{([^}]+)\}/g;
|
|
52
|
+
if (!regex.test(str)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const placeholders = [];
|
|
56
|
+
let index = 1;
|
|
57
|
+
let transformedStr = str.replace(regex, (match, p1) => {
|
|
58
|
+
placeholders.push(`name${index}: ${p1}`);
|
|
59
|
+
const placeholder = `{name${index}}`;
|
|
60
|
+
index++;
|
|
61
|
+
return placeholder;
|
|
62
|
+
}).replace(/`/g, '');
|
|
63
|
+
return {
|
|
64
|
+
key: transformedStr,
|
|
65
|
+
data: placeholders.join(', ')
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// 生成唯一key
|
|
69
|
+
function generateKey(chineseStr) {
|
|
70
|
+
const text = (globalThis.preText || '') + chineseStr;
|
|
71
|
+
const hash = crypto.createHmac('sha256', globalThis.cryptoKey || 'i18n').update(text).digest('hex');
|
|
72
|
+
// 保留加密结果的前N位,N由配置中的keyLength决定
|
|
73
|
+
const len = globalThis.keyLength || 16;
|
|
74
|
+
return hash.slice(0, len);
|
|
75
|
+
}
|
|
76
|
+
// 获取和收集key
|
|
77
|
+
function getChineseKey(text) {
|
|
78
|
+
let key = '';
|
|
79
|
+
if (containsChinese(text)) {
|
|
80
|
+
const chineseText = text.trim().replace(/^&%&/, '');
|
|
81
|
+
key = generateKey(chineseText);
|
|
82
|
+
if (!globalThis.translationsMap[key]) {
|
|
83
|
+
globalThis.addTranslations.push({
|
|
84
|
+
key,
|
|
85
|
+
value: chineseText
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// 这里一定是 use key ,使用的key值,修改中文和书写中文的时候会一个 标注
|
|
89
|
+
globalThis.translationsMap[key] = chineseText;
|
|
90
|
+
}
|
|
91
|
+
let isKey = false;
|
|
92
|
+
if (text) {
|
|
93
|
+
// 使用正则的方法进行判断
|
|
94
|
+
isKey = /^&%\&/.test(text);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
key,
|
|
98
|
+
isKey
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// 读取文件映射相关的内容
|
|
102
|
+
function getFileJson(filePath) {
|
|
103
|
+
if (!fs.existsSync(filePath)) {
|
|
104
|
+
return {};
|
|
105
|
+
}
|
|
106
|
+
// 读取文件内容
|
|
107
|
+
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
108
|
+
// 使用贪婪模式匹配到最后一个 }
|
|
109
|
+
const objectStr = fileContent.replace(/export\s+default\s+/, '').trim();
|
|
110
|
+
try {
|
|
111
|
+
// 解析对象
|
|
112
|
+
return JSON5.parse(objectStr);
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
console.log('解析语言映射文件报错:', filePath);
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// 更新文件中的json
|
|
120
|
+
function updateJSONInFile(filePath, obj) {
|
|
121
|
+
// 确保目录存在(兼容 Windows 和 Unix 路径)
|
|
122
|
+
const dir = path.dirname(filePath);
|
|
123
|
+
if (!fs.existsSync(dir)) {
|
|
124
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
// 生成新的对象字符串
|
|
127
|
+
const newObjectStr = JSON.stringify(obj, null, 2);
|
|
128
|
+
// 替换回文件内容
|
|
129
|
+
const newFileContent = `export default ${newObjectStr}`;
|
|
130
|
+
// 保存文件
|
|
131
|
+
fs.writeFileSync(filePath, newFileContent, 'utf8');
|
|
132
|
+
}
|
|
133
|
+
function debounce(func, wait, immediate = false) {
|
|
134
|
+
let timeout;
|
|
135
|
+
return function (...args) {
|
|
136
|
+
//@ts-ignore
|
|
137
|
+
const context = this;
|
|
138
|
+
const later = () => {
|
|
139
|
+
timeout = null;
|
|
140
|
+
if (!immediate) {
|
|
141
|
+
func.apply(context, args);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const callNow = immediate && !timeout;
|
|
145
|
+
if (timeout) {
|
|
146
|
+
clearTimeout(timeout);
|
|
147
|
+
}
|
|
148
|
+
timeout = setTimeout(later, wait);
|
|
149
|
+
if (callNow) {
|
|
150
|
+
func.apply(context, args);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
//@ts-ignore
|
|
156
|
+
const traverse = _traverse__namespace.default || _traverse__namespace;
|
|
157
|
+
//@ts-ignore
|
|
158
|
+
const generate = _generate__namespace.default || _generate__namespace;
|
|
159
|
+
// 提取 script 中的中文
|
|
160
|
+
function extractChineseFromScript(content, jsText) {
|
|
161
|
+
if (!content)
|
|
162
|
+
return undefined;
|
|
163
|
+
let flag = false; // 是否有更新
|
|
164
|
+
try {
|
|
165
|
+
const ast = babelParser__namespace.parse(content, {
|
|
166
|
+
sourceType: 'module',
|
|
167
|
+
plugins: ['jsx', 'typescript', 'decorators-legacy'],
|
|
168
|
+
});
|
|
169
|
+
traverse(ast, {
|
|
170
|
+
StringLiteral(path) {
|
|
171
|
+
// 第一层:AST 上下文过滤
|
|
172
|
+
const parent = path.parent;
|
|
173
|
+
// 跳过 import/export 语句
|
|
174
|
+
if (parent.type === 'ImportDeclaration' || parent.type === 'ImportSpecifier' ||
|
|
175
|
+
parent.type === 'ExportDeclaration' || parent.type === 'ExportSpecifier') {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// 跳过 require() 调用
|
|
179
|
+
if (parent.type === 'CallExpression' &&
|
|
180
|
+
parent.callee.type === 'Identifier' &&
|
|
181
|
+
parent.callee.name === 'require') {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// 跳过对象属性键
|
|
185
|
+
if (parent.type === 'ObjectProperty' && parent.key === path.node) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// 跳过函数参数名
|
|
189
|
+
if ((parent.type === 'FunctionDeclaration' || parent.type === 'FunctionExpression' ||
|
|
190
|
+
parent.type === 'ArrowFunctionExpression') &&
|
|
191
|
+
parent.params && parent.params.includes(path.node)) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
// 跳过变量声明标识符
|
|
195
|
+
if (parent.type === 'VariableDeclarator' && parent.id === path.node) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// 排除打印等代码的中文文案(console.log, alert)
|
|
199
|
+
if (parent.type === 'CallExpression') {
|
|
200
|
+
const callee = parent.callee;
|
|
201
|
+
if ((callee.type === 'MemberExpression' &&
|
|
202
|
+
callee.object.name === 'console') ||
|
|
203
|
+
(callee.type === 'Identifier' && callee.name === 'alert')) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// 第二层:内容正则过滤(在 getChineseKey 中已包含)
|
|
208
|
+
const { key, isKey } = getChineseKey(path.node.value);
|
|
209
|
+
if (key) {
|
|
210
|
+
if (parent.type === 'JSXAttribute') {
|
|
211
|
+
if (isKey) {
|
|
212
|
+
path.node.extra.raw = `'${key}'`;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
path.node.extra.raw = `{${jsText}('${key}')}`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
// 其他的jsx 基本就是直接替换
|
|
220
|
+
if (isKey) {
|
|
221
|
+
path.node.extra.raw = `'${key}'`;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
path.node.extra.raw = `${jsText}('${key}')`;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
flag = true;
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
// 处理js 字符串模板的代码,我的测试${test}你在哪里啊?
|
|
231
|
+
TemplateLiteral(path) {
|
|
232
|
+
// 存储转换后的模板字符串和占位符对象
|
|
233
|
+
let transformedTemplate = '';
|
|
234
|
+
const placeholders = {};
|
|
235
|
+
let placeholderCounter = 0;
|
|
236
|
+
const rawTemplate = path.node.quasis.map((q) => q.value.raw).join('.{.*?}');
|
|
237
|
+
if (containsChinese(rawTemplate)) {
|
|
238
|
+
// 遍历模板字符串的静态部分和插值表达式
|
|
239
|
+
path.node.quasis.forEach((quasi, index) => {
|
|
240
|
+
// 添加静态部分到转换后的模板字符串
|
|
241
|
+
transformedTemplate += quasi.value.raw;
|
|
242
|
+
// 如果当前不是最后一个元素,则添加插值表达式的占位符
|
|
243
|
+
if (index < path.node.expressions.length) {
|
|
244
|
+
// 生成唯一的占位符名称
|
|
245
|
+
const placeholderName = `name${++placeholderCounter}`;
|
|
246
|
+
// 添加占位符到转换后的模板字符串
|
|
247
|
+
transformedTemplate += `{${placeholderName}}`;
|
|
248
|
+
// 添加占位符对象,其键为占位符名称,值为插值表达式的源代码
|
|
249
|
+
placeholders[placeholderName] = generate(path.node.expressions[index]).code;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
// 中文模板的不进行处理
|
|
253
|
+
const { key, isKey } = getChineseKey(transformedTemplate);
|
|
254
|
+
const regex = new RegExp('`' + rawTemplate + '`');
|
|
255
|
+
const keyData = JSON.stringify(placeholders).replace(/"/g, '');
|
|
256
|
+
if (isKey) {
|
|
257
|
+
path.replaceWithSourceString(`'${key}&%&${keyData}'`);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
path.replaceWithSourceString(`${jsText}('${key}',${keyData})`);
|
|
261
|
+
}
|
|
262
|
+
flag = true;
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
JSXElement(path) {
|
|
266
|
+
path.traverse({
|
|
267
|
+
// 处理jsx中标签包含的文本
|
|
268
|
+
JSXText(node) {
|
|
269
|
+
const { key } = getChineseKey(node.node.value);
|
|
270
|
+
if (key) {
|
|
271
|
+
node.node.value = `{${jsText}('${key}')}`;
|
|
272
|
+
flag = true;
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
// 是否有更新
|
|
279
|
+
if (flag) {
|
|
280
|
+
return generate(ast).code;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
console.warn('解析脚本文件出错:', error);
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// 对拼接的字符串进行处理整理
|
|
290
|
+
function concatenatedString(str, tempText) {
|
|
291
|
+
const strList = extractQuotedStrings(str);
|
|
292
|
+
if (!strList.length)
|
|
293
|
+
return;
|
|
294
|
+
if (strList.length) {
|
|
295
|
+
let strSource = str;
|
|
296
|
+
strList.forEach((item) => {
|
|
297
|
+
const { key } = getChineseKey(item.replace(/'|"/g, ''));
|
|
298
|
+
if (key) {
|
|
299
|
+
strSource = strSource.replace(item, `${tempText}('${key}')`);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
return strSource;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// 提取 template 中的中文
|
|
306
|
+
function extractChineseFromTemplate(content, tempText) {
|
|
307
|
+
if (!content) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
let templateContent = content;
|
|
311
|
+
// 使用@vue/compiler-sfc来解析模板
|
|
312
|
+
const descriptor = compilerSfc.parse(`<template>${content}</template>`).descriptor;
|
|
313
|
+
// 获取模板的AST
|
|
314
|
+
const ast = descriptor.template?.ast;
|
|
315
|
+
if (!ast)
|
|
316
|
+
return content;
|
|
317
|
+
// 定义一个函数来递归遍历AST并收集所有文本节点和插值节点
|
|
318
|
+
// AST 逆向 template 存在者问题这里使用替换的方式进行处理
|
|
319
|
+
function extractNodes(node, source) {
|
|
320
|
+
// 这是中的类型 {{ }}, 事件,也就是模板解析的都在这里
|
|
321
|
+
if (node.type === 5 && containsChinese(node.content?.content)) {
|
|
322
|
+
const tempStr = extractTransformString(node.content.content);
|
|
323
|
+
if (tempStr) {
|
|
324
|
+
const { key } = getChineseKey(tempStr.key);
|
|
325
|
+
if (key) {
|
|
326
|
+
const results = source.replace(node.content?.content.trim(), `${tempText}('${key}', { ${tempStr.data} })`);
|
|
327
|
+
templateContent = templateContent.replace(source, results);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
const strSource = concatenatedString(node.content.content, tempText);
|
|
332
|
+
if (strSource) {
|
|
333
|
+
const results = source.replace(node.content?.content.trim(), strSource);
|
|
334
|
+
templateContent = templateContent.replace(source, results);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// 这是 TEXT 类型
|
|
339
|
+
if (node.type === 2) {
|
|
340
|
+
const { key } = getChineseKey(node.content);
|
|
341
|
+
if (key) {
|
|
342
|
+
const results = source.replace(node.content.trim(), `{{${tempText}('${key}')}}`);
|
|
343
|
+
templateContent = templateContent.replace(source, results);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (node.children) {
|
|
347
|
+
let pstr = node.loc.source;
|
|
348
|
+
// 优先处理属性值
|
|
349
|
+
if (node?.props?.length) {
|
|
350
|
+
// 这里是处理属性值的地方
|
|
351
|
+
node.props.forEach((item) => {
|
|
352
|
+
if (item.type === 6) {
|
|
353
|
+
// 这个是纯的属性类型 title="我的测试"
|
|
354
|
+
const { key } = getChineseKey(item?.value?.content);
|
|
355
|
+
if (key) {
|
|
356
|
+
pstr = pstr.replace(item.loc.source, `:${item.name}="${tempText}('${key}')"`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
else if (item.type === 7 && item.exp?.content) {
|
|
360
|
+
// 这里是一个bind 这里统一对 等号后面的字符串提取出来处理
|
|
361
|
+
const strSource = concatenatedString(item.exp.content, tempText);
|
|
362
|
+
if (strSource) {
|
|
363
|
+
pstr = pstr.replace(item.exp.content, strSource);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
templateContent = templateContent.replace(node.loc.source, pstr);
|
|
368
|
+
}
|
|
369
|
+
// 同级的children 值
|
|
370
|
+
node.children.forEach((item) => {
|
|
371
|
+
// res 修改的值就是父级的值,父级的 source
|
|
372
|
+
extractNodes(item, pstr);
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// 检查 AST 的有效性
|
|
377
|
+
if (ast.children && ast.children.length > 0) {
|
|
378
|
+
ast.children.forEach((child) => {
|
|
379
|
+
extractNodes(child, ast.source);
|
|
380
|
+
});
|
|
381
|
+
return templateContent;
|
|
382
|
+
}
|
|
383
|
+
return content;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// 初始化全局变量
|
|
387
|
+
globalThis.translationsMap = {};
|
|
388
|
+
globalThis.addTranslations = [];
|
|
389
|
+
globalThis.useTranslations = [];
|
|
390
|
+
globalThis.keyLength = 16;
|
|
391
|
+
function I18nAutoPlugin(options) {
|
|
392
|
+
let root = '';
|
|
393
|
+
let isPro = false;
|
|
394
|
+
// 配置
|
|
395
|
+
const configOption = {
|
|
396
|
+
...options,
|
|
397
|
+
injectToJS: options.injectToJS ? `\n${options.injectToJS}\n` : `\nimport { useI18n } from 'vue-i18n'\nconst { t } = useI18n()\n`,
|
|
398
|
+
i18nPath: options.i18nPath || 'src/locales/zh-CN.ts',
|
|
399
|
+
langPath: options.langPath || ['src/locales/en.ts'],
|
|
400
|
+
regi18n: options.regi18n || 'useI18n',
|
|
401
|
+
excludes: options.excludes || ['locale', 'useI18n'],
|
|
402
|
+
tempText: options.tempText || 't',
|
|
403
|
+
jsText: options.jsText || 't',
|
|
404
|
+
delay: options.delay || 1000,
|
|
405
|
+
reserveKeys: options.reserveKeys || [],
|
|
406
|
+
runBuild: options.runBuild || false,
|
|
407
|
+
keyLength: options.keyLength || 16,
|
|
408
|
+
cryptoKey: options.cryptoKey || 'i18n',
|
|
409
|
+
preText: options.preText || '',
|
|
410
|
+
};
|
|
411
|
+
const dealWithLangFile = debounce((i18nPath) => {
|
|
412
|
+
updateJSONInFile(i18nPath, globalThis.translationsMap);
|
|
413
|
+
}, configOption.delay);
|
|
414
|
+
return {
|
|
415
|
+
name: 'i18n-auto-plugin', // 插件名称
|
|
416
|
+
enforce: 'pre', // 插件执行阶段(pre/normal/post)
|
|
417
|
+
configResolved(config) {
|
|
418
|
+
root = config.root;
|
|
419
|
+
isPro = config.isProduction;
|
|
420
|
+
globalThis.translationsMap = {};
|
|
421
|
+
globalThis.keyLength = configOption.keyLength;
|
|
422
|
+
globalThis.cryptoKey = configOption.cryptoKey;
|
|
423
|
+
globalThis.preText = configOption.preText;
|
|
424
|
+
if (!isPro) {
|
|
425
|
+
// 开发环境保留所有字段不进行任何的优化
|
|
426
|
+
const obj = getFileJson(path.resolve(root, configOption.i18nPath));
|
|
427
|
+
// 映射到全局之中去,反向映射出来
|
|
428
|
+
Object.keys(obj).forEach(key => {
|
|
429
|
+
globalThis.translationsMap[key] = obj[key];
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
else if (configOption?.reserveKeys?.length) {
|
|
433
|
+
// 生产环境下对代码, 对保留的key 不进行处理的key进行了处理
|
|
434
|
+
const obj = getFileJson(path.resolve(root, configOption.i18nPath));
|
|
435
|
+
Object.keys(obj).forEach(key => {
|
|
436
|
+
if (configOption.reserveKeys.includes(key)) {
|
|
437
|
+
globalThis.translationsMap[key] = obj[key];
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
transform(code, id) {
|
|
443
|
+
// 排除不需要处理的文件
|
|
444
|
+
if (configOption.excludes.some(i => id.includes(i)) ||
|
|
445
|
+
id.includes('node_modules') ||
|
|
446
|
+
id.includes('\x00') ||
|
|
447
|
+
id.includes('locales') || // 排除语言文件本身
|
|
448
|
+
!configOption.runBuild // 仅在 runBuild 为 true 时处理
|
|
449
|
+
) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
let rewrittenScript = code;
|
|
453
|
+
if (id.endsWith('.vue')) {
|
|
454
|
+
rewrittenScript = processVueFile(id, configOption) || code;
|
|
455
|
+
}
|
|
456
|
+
else if (['.ts', '.js', '.jsx', '.tsx'].some(i => id.split('?')[0].endsWith(i))) {
|
|
457
|
+
rewrittenScript = processJsFile(id, configOption);
|
|
458
|
+
}
|
|
459
|
+
const langFile = path.resolve(root, configOption.i18nPath);
|
|
460
|
+
if (!isPro && globalThis.addTranslations.length) {
|
|
461
|
+
dealWithLangFile(langFile);
|
|
462
|
+
globalThis.addTranslations = [];
|
|
463
|
+
}
|
|
464
|
+
// 始终返回转换后的代码(参考参考项目的实现)
|
|
465
|
+
return {
|
|
466
|
+
code: rewrittenScript,
|
|
467
|
+
map: null
|
|
468
|
+
};
|
|
469
|
+
},
|
|
470
|
+
buildEnd() {
|
|
471
|
+
// 打包构建的时候执行该代码, 这是打包阶段的了也是我们测试的时候使用
|
|
472
|
+
if (configOption.runBuild) {
|
|
473
|
+
const langFile = path.resolve(root, configOption.i18nPath);
|
|
474
|
+
// 这里整理所有的语言数据,所有的都是新的语言包,
|
|
475
|
+
updateJSONInFile(langFile, globalThis.translationsMap);
|
|
476
|
+
// 处理其他语言包的映射关系
|
|
477
|
+
if (configOption.langPath) {
|
|
478
|
+
configOption.langPath.forEach(item => {
|
|
479
|
+
const lf = path.resolve(root, item);
|
|
480
|
+
const lm = getFileJson(lf);
|
|
481
|
+
const obj = {};
|
|
482
|
+
const endList = [];
|
|
483
|
+
// 将未翻译的语言包也加入到最后
|
|
484
|
+
Object.keys(globalThis.translationsMap).forEach(key => {
|
|
485
|
+
if (lm[key]) {
|
|
486
|
+
obj[key] = lm[key];
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
// 新增的key 直接加入到末尾
|
|
490
|
+
endList.push({
|
|
491
|
+
key: key,
|
|
492
|
+
value: globalThis.translationsMap[key]
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
endList.forEach((item) => {
|
|
497
|
+
obj[item.key] = item.value;
|
|
498
|
+
});
|
|
499
|
+
updateJSONInFile(lf, obj);
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
function processJsFile(jsFilePath, options) {
|
|
507
|
+
const jsFileContent = fs.readFileSync(jsFilePath, 'utf-8');
|
|
508
|
+
let scriptTemp = extractChineseFromScript(jsFileContent, options.jsText);
|
|
509
|
+
if (scriptTemp) {
|
|
510
|
+
// 排除如果没有引入的值直接不处理
|
|
511
|
+
if (options.regi18n && !jsFileContent.includes(options.regi18n)) {
|
|
512
|
+
scriptTemp = `${options.injectToJS}${scriptTemp}`;
|
|
513
|
+
}
|
|
514
|
+
return scriptTemp;
|
|
515
|
+
}
|
|
516
|
+
return jsFileContent;
|
|
517
|
+
}
|
|
518
|
+
function processVueFile(vueFilePath, options) {
|
|
519
|
+
// 获取文件中的内容数据
|
|
520
|
+
let vueFileContent = fs.readFileSync(vueFilePath, 'utf-8');
|
|
521
|
+
// 使用 @vue/compiler-sfc 解析 Vue 文件
|
|
522
|
+
const { descriptor, errors } = compilerSfc.parse(vueFileContent);
|
|
523
|
+
// 如果解析时发生错误,打印错误信息
|
|
524
|
+
if (errors && errors.length) {
|
|
525
|
+
console.error('Errors occurred while parsing the Vue file:', vueFilePath);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const vueTemplate = extractChineseFromTemplate(descriptor.template?.content || '', options.tempText);
|
|
529
|
+
if (vueTemplate && descriptor.template?.content) {
|
|
530
|
+
vueFileContent = vueFileContent.replace(descriptor.template.content, vueTemplate);
|
|
531
|
+
}
|
|
532
|
+
const dsScript = descriptor.script || descriptor.scriptSetup;
|
|
533
|
+
if (dsScript?.content) {
|
|
534
|
+
let scriptTemp = extractChineseFromScript(dsScript.content, options.jsText);
|
|
535
|
+
if (scriptTemp) {
|
|
536
|
+
// 这里对字符串进行判断是否要注入js在里面, 如果文本没有修改
|
|
537
|
+
if (options.regi18n && !scriptTemp.includes(options.regi18n)) {
|
|
538
|
+
scriptTemp = `${options.injectToJS}${scriptTemp}`;
|
|
539
|
+
}
|
|
540
|
+
vueFileContent = vueFileContent.replace(dsScript.content, scriptTemp);
|
|
541
|
+
}
|
|
542
|
+
else if (vueTemplate !== descriptor.template?.content) {
|
|
543
|
+
// 这里对字符串进行判断是否要注入js在里面, 如果文本没有修改
|
|
544
|
+
let strcontent = dsScript.content;
|
|
545
|
+
if (options.regi18n && !strcontent.includes(options.regi18n)) {
|
|
546
|
+
strcontent = `${options.injectToJS}${strcontent}`;
|
|
547
|
+
}
|
|
548
|
+
vueFileContent = vueFileContent.replace(dsScript.content, strcontent);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return vueFileContent;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
module.exports = I18nAutoPlugin;
|
|
555
|
+
//# sourceMappingURL=index.cjs.js.map
|