feima-shortcuts 0.3.0-beta.11 → 0.3.0-beta.13
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/package.json +1 -1
- package/src/scripts/check-i18n-views/index.js +418 -204
package/package.json
CHANGED
|
@@ -1,271 +1,485 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
const fs = require('fs')
|
|
2
3
|
const path = require('path')
|
|
3
4
|
const glob = require('glob')
|
|
5
|
+
const ts = require('typescript')
|
|
4
6
|
const { parse } = require('@vue/compiler-sfc')
|
|
5
|
-
const babelParser = require('@babel/parser')
|
|
6
|
-
const traverse = require('@babel/traverse').default
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
const ROOT = process.cwd()
|
|
9
|
+
const SCAN_DIRS = ['src/views'] // views 目录
|
|
10
|
+
const COMPONENTS_DIR = 'src/components' // components 目录
|
|
11
|
+
const FILE_EXT = ['vue', 'ts', 'js', 'tsx', 'jsx']
|
|
12
|
+
|
|
13
|
+
// 匹配 t() 调用:t('key') 或 t('key', 'default')
|
|
14
|
+
const T_FUNCTION_REG = /\bt\s*\(\s*['"`]([^'"`]+)['"`]\s*(?:,\s*['"`][^'"`]*['"`])?\s*\)/g
|
|
13
15
|
|
|
16
|
+
// 匹配 $t() 调用:$t('key') 或 $t('key', 'default')
|
|
17
|
+
const DOLLAR_T_FUNCTION_REG = /\$t\s*\(\s*['"`]([^'"`]+)['"`]\s*(?:,\s*['"`][^'"`]*['"`])?\s*\)/g
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 扁平化 locale 对象,返回所有 key 的集合
|
|
21
|
+
*/
|
|
14
22
|
function flattenLocale(obj, prefix = '') {
|
|
15
|
-
|
|
23
|
+
const keys = new Set()
|
|
16
24
|
for (const k in obj) {
|
|
17
25
|
const key = prefix ? `${prefix}.${k}` : k
|
|
18
|
-
if (typeof obj[k] === 'object' && obj[k] !== null) {
|
|
19
|
-
|
|
26
|
+
if (typeof obj[k] === 'object' && obj[k] !== null && !Array.isArray(obj[k])) {
|
|
27
|
+
const nestedKeys = flattenLocale(obj[k], key)
|
|
28
|
+
nestedKeys.forEach(k => keys.add(k))
|
|
20
29
|
} else {
|
|
21
|
-
|
|
30
|
+
keys.add(key)
|
|
22
31
|
}
|
|
23
32
|
}
|
|
24
|
-
return
|
|
33
|
+
return keys
|
|
25
34
|
}
|
|
26
35
|
|
|
36
|
+
/**
|
|
37
|
+
* 加载 locale.ts 文件并提取所有定义的 key
|
|
38
|
+
*/
|
|
27
39
|
function loadLocaleKeys(localePath) {
|
|
28
|
-
if (!fs.existsSync(localePath))
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return flattenLocale(localeObj)
|
|
32
|
-
}
|
|
40
|
+
if (!fs.existsSync(localePath)) {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
33
43
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
try {
|
|
45
|
+
const code = fs.readFileSync(localePath, 'utf-8')
|
|
46
|
+
const sf = ts.createSourceFile(localePath, code, ts.ScriptTarget.Latest, true)
|
|
47
|
+
|
|
48
|
+
let obj = null
|
|
49
|
+
|
|
50
|
+
function visit(node) {
|
|
51
|
+
if (
|
|
52
|
+
ts.isExportAssignment(node) &&
|
|
53
|
+
ts.isObjectLiteralExpression(node.expression)
|
|
54
|
+
) {
|
|
55
|
+
try {
|
|
56
|
+
// 使用 eval 来解析对象字面量
|
|
57
|
+
obj = eval(`(${node.expression.getText()})`)
|
|
58
|
+
} catch (e) {
|
|
59
|
+
// 如果解析失败,尝试其他方式
|
|
60
|
+
console.warn(`⚠️ 无法解析 locale 文件: ${localePath}`)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
ts.forEachChild(node, visit)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
visit(sf)
|
|
67
|
+
return obj ? flattenLocale(obj) : new Set()
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.warn(`⚠️ 读取 locale 文件失败: ${localePath}`, error.message)
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
39
72
|
}
|
|
40
73
|
|
|
41
74
|
/**
|
|
42
|
-
*
|
|
43
|
-
* 向上查找最近含 locale.ts 的目录(包含自身目录)
|
|
44
|
-
* 到达 componentsRoot 停止
|
|
75
|
+
* 从代码中提取所有 t() 调用的 key
|
|
45
76
|
*/
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return localePath
|
|
52
|
-
}
|
|
53
|
-
const parent = path.dirname(current)
|
|
54
|
-
if (parent === current) break
|
|
55
|
-
current = parent
|
|
77
|
+
function extractTKeys(code) {
|
|
78
|
+
const keys = new Set()
|
|
79
|
+
let match
|
|
80
|
+
while ((match = T_FUNCTION_REG.exec(code)) !== null) {
|
|
81
|
+
keys.add(match[1])
|
|
56
82
|
}
|
|
57
|
-
return
|
|
83
|
+
return keys
|
|
58
84
|
}
|
|
59
85
|
|
|
60
86
|
/**
|
|
61
|
-
*
|
|
62
|
-
* 只查找当前文件同目录的 locale.ts
|
|
87
|
+
* 从代码中提取所有 $t() 调用的 key
|
|
63
88
|
*/
|
|
64
|
-
function
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
89
|
+
function extractDollarTKeys(code) {
|
|
90
|
+
const keys = new Set()
|
|
91
|
+
let match
|
|
92
|
+
while ((match = DOLLAR_T_FUNCTION_REG.exec(code)) !== null) {
|
|
93
|
+
keys.add(match[1])
|
|
94
|
+
}
|
|
95
|
+
return keys
|
|
68
96
|
}
|
|
69
97
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
98
|
+
/**
|
|
99
|
+
* 检查 locale 导入方式
|
|
100
|
+
* @returns {Object} { hasCorrectImport, hasOtherImport, importPaths }
|
|
101
|
+
*/
|
|
102
|
+
function checkLocaleImport(code) {
|
|
103
|
+
const result = {
|
|
104
|
+
hasCorrectImport: false,
|
|
105
|
+
hasOtherImport: false,
|
|
106
|
+
importPaths: []
|
|
107
|
+
}
|
|
73
108
|
|
|
74
|
-
|
|
109
|
+
// 匹配 import locale from 'xxx'
|
|
110
|
+
const importReg = /import\s+locale\s+from\s+['"]([^'"]+)['"]/g
|
|
111
|
+
let match
|
|
75
112
|
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
113
|
+
while ((match = importReg.exec(code)) !== null) {
|
|
114
|
+
const importPath = match[1]
|
|
115
|
+
result.importPaths.push(importPath)
|
|
116
|
+
if (importPath === './locale') {
|
|
117
|
+
result.hasCorrectImport = true
|
|
118
|
+
} else {
|
|
119
|
+
result.hasOtherImport = true
|
|
81
120
|
}
|
|
82
|
-
files.push(
|
|
83
|
-
...glob.sync('**/*.{vue,ts,tsx}', {
|
|
84
|
-
cwd: absDir,
|
|
85
|
-
absolute: true,
|
|
86
|
-
})
|
|
87
|
-
)
|
|
88
121
|
}
|
|
89
122
|
|
|
90
|
-
|
|
91
|
-
|
|
123
|
+
return result
|
|
124
|
+
}
|
|
92
125
|
|
|
93
|
-
|
|
94
|
-
|
|
126
|
+
/**
|
|
127
|
+
* 解析 Vue 文件,提取 script 部分的代码
|
|
128
|
+
*/
|
|
129
|
+
function parseVueFile(filePath) {
|
|
130
|
+
try {
|
|
131
|
+
const content = fs.readFileSync(filePath, 'utf-8')
|
|
132
|
+
const { descriptor } = parse(content)
|
|
133
|
+
|
|
134
|
+
// 合并所有 script 块的内容
|
|
135
|
+
let scriptContent = ''
|
|
136
|
+
if (descriptor.script) {
|
|
137
|
+
scriptContent += descriptor.script.content
|
|
138
|
+
}
|
|
139
|
+
if (descriptor.scriptSetup) {
|
|
140
|
+
scriptContent += '\n' + descriptor.scriptSetup.content
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 也检查 template 中的 t() 调用
|
|
144
|
+
if (descriptor.template) {
|
|
145
|
+
scriptContent += '\n' + descriptor.template.content
|
|
146
|
+
}
|
|
95
147
|
|
|
96
|
-
|
|
97
|
-
|
|
148
|
+
return scriptContent
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.warn(`⚠️ 解析 Vue 文件失败: ${filePath}`, error.message)
|
|
151
|
+
return ''
|
|
152
|
+
}
|
|
153
|
+
}
|
|
98
154
|
|
|
99
|
-
|
|
155
|
+
/**
|
|
156
|
+
* 查找文件对应的 locale.ts 路径
|
|
157
|
+
*/
|
|
158
|
+
function findLocalePath(filePath) {
|
|
159
|
+
const dir = path.dirname(filePath)
|
|
160
|
+
const localePath = path.join(dir, 'locale.ts')
|
|
161
|
+
return localePath
|
|
162
|
+
}
|
|
100
163
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
164
|
+
/**
|
|
165
|
+
* 扫描单个文件
|
|
166
|
+
*/
|
|
167
|
+
function scanFile(filePath, results) {
|
|
168
|
+
let code = ''
|
|
169
|
+
|
|
170
|
+
if (filePath.endsWith('.vue')) {
|
|
171
|
+
code = parseVueFile(filePath)
|
|
172
|
+
} else {
|
|
173
|
+
try {
|
|
174
|
+
code = fs.readFileSync(filePath, 'utf-8')
|
|
175
|
+
} catch (error) {
|
|
176
|
+
console.warn(`⚠️ 读取文件失败: ${filePath}`, error.message)
|
|
177
|
+
return
|
|
107
178
|
}
|
|
179
|
+
}
|
|
108
180
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
181
|
+
if (!code) return
|
|
182
|
+
|
|
183
|
+
// 提取 t() 调用的 key
|
|
184
|
+
const usedKeys = extractTKeys(code)
|
|
185
|
+
if (usedKeys.size === 0) return
|
|
186
|
+
|
|
187
|
+
// 检查 locale 导入方式
|
|
188
|
+
const importInfo = checkLocaleImport(code)
|
|
189
|
+
|
|
190
|
+
// 查找对应的 locale.ts
|
|
191
|
+
const localePath = findLocalePath(filePath)
|
|
192
|
+
const localeKeys = loadLocaleKeys(localePath)
|
|
193
|
+
|
|
194
|
+
const relativePath = path.relative(ROOT, filePath)
|
|
195
|
+
const relativeLocalePath = path.relative(ROOT, localePath)
|
|
196
|
+
|
|
197
|
+
// 如果使用了非标准的导入方式,记录(但不检查 key)
|
|
198
|
+
if (!importInfo.hasCorrectImport && importInfo.hasOtherImport) {
|
|
199
|
+
if (!results.invalidImports.has(relativePath)) {
|
|
200
|
+
results.invalidImports.set(relativePath, {
|
|
201
|
+
file: relativePath,
|
|
202
|
+
importPaths: importInfo.importPaths,
|
|
203
|
+
usedKeys: Array.from(usedKeys)
|
|
204
|
+
})
|
|
116
205
|
}
|
|
206
|
+
return // 非标准导入,不检查 key 是否缺失
|
|
207
|
+
}
|
|
117
208
|
|
|
118
|
-
|
|
119
|
-
|
|
209
|
+
// 如果没有导入 locale,记录(但不检查 key,可能使用全局 locale)
|
|
210
|
+
if (!importInfo.hasCorrectImport && !importInfo.hasOtherImport) {
|
|
211
|
+
if (!results.noLocaleImport.has(relativePath)) {
|
|
212
|
+
results.noLocaleImport.set(relativePath, {
|
|
213
|
+
file: relativePath,
|
|
214
|
+
usedKeys: Array.from(usedKeys)
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
return // 没有导入本地 locale,不检查 key 是否缺失
|
|
218
|
+
}
|
|
120
219
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
callee.type === 'Identifier' &&
|
|
134
|
-
callee.name === 't'
|
|
135
|
-
) {
|
|
136
|
-
const keyArg = p.node.arguments[0]
|
|
137
|
-
const defaultArg = p.node.arguments[1]
|
|
138
|
-
if (keyArg && keyArg.type === 'StringLiteral') {
|
|
139
|
-
usedKeys.push({
|
|
140
|
-
key: keyArg.value,
|
|
141
|
-
defaultValue:
|
|
142
|
-
defaultArg && defaultArg.type === 'StringLiteral'
|
|
143
|
-
? defaultArg.value
|
|
144
|
-
: null,
|
|
145
|
-
})
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
} else if (file.startsWith(componentsRoot)) {
|
|
149
|
-
// src/components 目录:只检查 $t()
|
|
150
|
-
if (
|
|
151
|
-
callee.type === 'Identifier' &&
|
|
152
|
-
callee.name === '$t'
|
|
153
|
-
) {
|
|
154
|
-
const keyArg = p.node.arguments[0]
|
|
155
|
-
const defaultArg = p.node.arguments[1]
|
|
156
|
-
if (keyArg && keyArg.type === 'StringLiteral') {
|
|
157
|
-
usedKeys.push({
|
|
158
|
-
key: keyArg.value,
|
|
159
|
-
defaultValue:
|
|
160
|
-
defaultArg && defaultArg.type === 'StringLiteral'
|
|
161
|
-
? defaultArg.value
|
|
162
|
-
: null,
|
|
163
|
-
})
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
},
|
|
220
|
+
// 只有使用标准导入方式(import locale from './locale')时,才检查 key
|
|
221
|
+
// 如果 locale.ts 不存在,记录缺失的 key
|
|
222
|
+
if (!localeKeys) {
|
|
223
|
+
usedKeys.forEach(key => {
|
|
224
|
+
if (!results.missingLocale.has(relativePath)) {
|
|
225
|
+
results.missingLocale.set(relativePath, {
|
|
226
|
+
file: relativePath,
|
|
227
|
+
localePath: relativeLocalePath,
|
|
228
|
+
missingKeys: new Set()
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
results.missingLocale.get(relativePath).missingKeys.add(key)
|
|
168
232
|
})
|
|
233
|
+
return
|
|
234
|
+
}
|
|
169
235
|
|
|
170
|
-
|
|
236
|
+
// 检查哪些 key 在 locale 中不存在
|
|
237
|
+
const missingKeys = Array.from(usedKeys).filter(key => !localeKeys.has(key))
|
|
238
|
+
|
|
239
|
+
if (missingKeys.length > 0) {
|
|
240
|
+
if (!results.missingKeys.has(relativePath)) {
|
|
241
|
+
results.missingKeys.set(relativePath, {
|
|
242
|
+
file: relativePath,
|
|
243
|
+
localePath: relativeLocalePath,
|
|
244
|
+
missingKeys: new Set()
|
|
245
|
+
})
|
|
246
|
+
}
|
|
247
|
+
missingKeys.forEach(key => {
|
|
248
|
+
results.missingKeys.get(relativePath).missingKeys.add(key)
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
}
|
|
171
252
|
|
|
172
|
-
|
|
253
|
+
/**
|
|
254
|
+
* 检查组件目录(使用 $t 注入方式)
|
|
255
|
+
*/
|
|
256
|
+
function checkComponent(componentDir, results) {
|
|
257
|
+
const localePath = path.join(componentDir, 'locale.ts')
|
|
258
|
+
if (!fs.existsSync(localePath)) {
|
|
259
|
+
return // 组件没有 locale.ts,跳过
|
|
260
|
+
}
|
|
173
261
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
262
|
+
const localeKeys = loadLocaleKeys(localePath)
|
|
263
|
+
if (!localeKeys) {
|
|
264
|
+
console.warn(`⚠️ 无法加载组件 locale: ${path.relative(ROOT, localePath)}`)
|
|
265
|
+
return
|
|
266
|
+
}
|
|
178
267
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
268
|
+
// 递归扫描组件目录下的所有文件
|
|
269
|
+
const allUsedKeys = new Set()
|
|
270
|
+
const componentFiles = []
|
|
271
|
+
|
|
272
|
+
FILE_EXT.forEach(ext => {
|
|
273
|
+
const pattern = `${componentDir}/**/*.${ext}`
|
|
274
|
+
const files = glob.sync(pattern, { nodir: true })
|
|
275
|
+
files.forEach(file => {
|
|
276
|
+
componentFiles.push(file)
|
|
277
|
+
let code = ''
|
|
278
|
+
|
|
279
|
+
if (file.endsWith('.vue')) {
|
|
280
|
+
code = parseVueFile(file)
|
|
281
|
+
} else {
|
|
282
|
+
try {
|
|
283
|
+
code = fs.readFileSync(file, 'utf-8')
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.warn(`⚠️ 读取文件失败: ${file}`, error.message)
|
|
286
|
+
return
|
|
287
|
+
}
|
|
186
288
|
}
|
|
187
289
|
|
|
188
|
-
if (
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
reason: '同目录无 locale.ts',
|
|
193
|
-
})
|
|
194
|
-
continue
|
|
290
|
+
if (code) {
|
|
291
|
+
// 提取 $t() 调用的 key
|
|
292
|
+
const dollarTKeys = extractDollarTKeys(code)
|
|
293
|
+
dollarTKeys.forEach(key => allUsedKeys.add(key))
|
|
195
294
|
}
|
|
295
|
+
})
|
|
296
|
+
})
|
|
196
297
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
298
|
+
if (allUsedKeys.size === 0) {
|
|
299
|
+
return // 组件中没有使用 $t()
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 检查哪些 key 在 locale 中不存在
|
|
303
|
+
const missingKeys = Array.from(allUsedKeys).filter(key => !localeKeys.has(key))
|
|
304
|
+
|
|
305
|
+
if (missingKeys.length > 0) {
|
|
306
|
+
const relativeComponentDir = path.relative(ROOT, componentDir)
|
|
307
|
+
const relativeLocalePath = path.relative(ROOT, localePath)
|
|
308
|
+
|
|
309
|
+
if (!results.componentMissingKeys.has(relativeComponentDir)) {
|
|
310
|
+
results.componentMissingKeys.set(relativeComponentDir, {
|
|
311
|
+
componentDir: relativeComponentDir,
|
|
312
|
+
localePath: relativeLocalePath,
|
|
313
|
+
missingKeys: new Set(),
|
|
314
|
+
files: []
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
missingKeys.forEach(key => {
|
|
319
|
+
results.componentMissingKeys.get(relativeComponentDir).missingKeys.add(key)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
// 记录使用了这些 key 的文件
|
|
323
|
+
componentFiles.forEach(file => {
|
|
324
|
+
let code = ''
|
|
325
|
+
if (file.endsWith('.vue')) {
|
|
326
|
+
code = parseVueFile(file)
|
|
327
|
+
} else {
|
|
328
|
+
try {
|
|
329
|
+
code = fs.readFileSync(file, 'utf-8')
|
|
330
|
+
} catch (error) {
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (code) {
|
|
336
|
+
const dollarTKeys = extractDollarTKeys(code)
|
|
337
|
+
const fileMissingKeys = Array.from(dollarTKeys).filter(key => missingKeys.includes(key))
|
|
338
|
+
if (fileMissingKeys.length > 0) {
|
|
339
|
+
const relativeFile = path.relative(ROOT, file)
|
|
340
|
+
const info = results.componentMissingKeys.get(relativeComponentDir)
|
|
341
|
+
if (!info.files.find(f => f.file === relativeFile)) {
|
|
342
|
+
info.files.push({
|
|
343
|
+
file: relativeFile,
|
|
344
|
+
missingKeys: fileMissingKeys
|
|
345
|
+
})
|
|
346
|
+
}
|
|
204
347
|
}
|
|
205
348
|
}
|
|
206
|
-
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* 主函数
|
|
355
|
+
*/
|
|
356
|
+
function run() {
|
|
357
|
+
const results = {
|
|
358
|
+
invalidImports: new Map(), // 使用了非 './locale' 的导入
|
|
359
|
+
noLocaleImport: new Map(), // 没有导入 locale 但使用了 t()
|
|
360
|
+
missingLocale: new Map(), // locale.ts 文件不存在
|
|
361
|
+
missingKeys: new Map(), // locale.ts 存在但缺少某些 key
|
|
362
|
+
componentMissingKeys: new Map() // 组件中 $t() 的 key 在 locale.ts 中缺失
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// 扫描 views 目录
|
|
366
|
+
SCAN_DIRS.forEach(dir => {
|
|
367
|
+
const absDir = path.join(ROOT, dir)
|
|
368
|
+
if (!fs.existsSync(absDir)) {
|
|
369
|
+
console.log(`⚠️ 目录不存在: ${dir}`)
|
|
370
|
+
return
|
|
207
371
|
}
|
|
208
372
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
373
|
+
FILE_EXT.forEach(ext => {
|
|
374
|
+
const pattern = `${absDir}/**/*.${ext}`
|
|
375
|
+
const files = glob.sync(pattern, { nodir: true })
|
|
376
|
+
files.forEach(file => scanFile(file, results))
|
|
377
|
+
})
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
// 扫描 components 目录
|
|
381
|
+
const componentsDir = path.join(ROOT, COMPONENTS_DIR)
|
|
382
|
+
if (fs.existsSync(componentsDir)) {
|
|
383
|
+
// 找到所有有 locale.ts 的组件目录
|
|
384
|
+
const localeFiles = glob.sync(`${componentsDir}/**/locale.ts`, { nodir: true })
|
|
385
|
+
localeFiles.forEach(localeFile => {
|
|
386
|
+
const componentDir = path.dirname(localeFile)
|
|
387
|
+
checkComponent(componentDir, results)
|
|
388
|
+
})
|
|
389
|
+
} else {
|
|
390
|
+
console.log(`⚠️ 目录不存在: ${COMPONENTS_DIR}`)
|
|
391
|
+
}
|
|
213
392
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
file: path.relative(cwd, file),
|
|
217
|
-
keys: usedKeys.map(i => i.key),
|
|
218
|
-
reason: '未找到可用 locale.ts',
|
|
219
|
-
})
|
|
220
|
-
continue
|
|
221
|
-
}
|
|
393
|
+
// 输出结果
|
|
394
|
+
console.log('\n📋 i18n 使用检查结果\n')
|
|
222
395
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
396
|
+
let hasError = false
|
|
397
|
+
|
|
398
|
+
// 1. 输出非标准导入方式
|
|
399
|
+
if (results.invalidImports.size > 0) {
|
|
400
|
+
hasError = true
|
|
401
|
+
console.log('❌ 使用了非标准的 locale 导入方式(应使用 import locale from \'./locale\'):\n')
|
|
402
|
+
results.invalidImports.forEach((info, file) => {
|
|
403
|
+
console.log(` ${file}`)
|
|
404
|
+
info.importPaths.forEach(importPath => {
|
|
405
|
+
console.log(` ↳ import locale from '${importPath}'`)
|
|
406
|
+
})
|
|
407
|
+
if (info.usedKeys && info.usedKeys.length > 0) {
|
|
408
|
+
console.log(` 使用的 key: ${info.usedKeys.join(', ')}`)
|
|
231
409
|
}
|
|
232
|
-
|
|
233
|
-
|
|
410
|
+
})
|
|
411
|
+
console.log()
|
|
234
412
|
}
|
|
235
413
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
)
|
|
246
|
-
}
|
|
414
|
+
// 2. 输出没有导入 locale 的文件
|
|
415
|
+
if (results.noLocaleImport.size > 0) {
|
|
416
|
+
console.log('⚠️ 以下文件使用了 t() 但未导入本地 locale(可能使用全局 locale):\n')
|
|
417
|
+
results.noLocaleImport.forEach((info, file) => {
|
|
418
|
+
console.log(` ${file}`)
|
|
419
|
+
console.log(` 使用的 key: ${info.usedKeys.join(', ')}`)
|
|
420
|
+
})
|
|
421
|
+
console.log()
|
|
422
|
+
}
|
|
247
423
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
)
|
|
255
|
-
|
|
424
|
+
// 3. 输出缺少 locale.ts 文件的情况
|
|
425
|
+
if (results.missingLocale.size > 0) {
|
|
426
|
+
hasError = true
|
|
427
|
+
console.log('❌ 以下文件使用了 t() 但缺少对应的 locale.ts 文件:\n')
|
|
428
|
+
results.missingLocale.forEach((info, file) => {
|
|
429
|
+
console.log(` ${file}`)
|
|
430
|
+
console.log(` 期望的 locale 文件: ${info.localePath}`)
|
|
431
|
+
console.log(` 缺失的 key: ${Array.from(info.missingKeys).join(', ')}`)
|
|
432
|
+
})
|
|
433
|
+
console.log()
|
|
434
|
+
}
|
|
256
435
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
436
|
+
// 4. 输出缺少的 key
|
|
437
|
+
if (results.missingKeys.size > 0) {
|
|
438
|
+
hasError = true
|
|
439
|
+
console.log('❌ 以下文件使用的 key 在 locale.ts 中未定义:\n')
|
|
440
|
+
results.missingKeys.forEach((info, file) => {
|
|
441
|
+
console.log(` ${file}`)
|
|
442
|
+
console.log(` locale 文件: ${info.localePath}`)
|
|
443
|
+
console.log(` 缺失的 key:`)
|
|
444
|
+
Array.from(info.missingKeys).forEach(key => {
|
|
445
|
+
console.log(` - ${key}`)
|
|
446
|
+
})
|
|
447
|
+
})
|
|
448
|
+
console.log()
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// 5. 输出组件中缺失的 key
|
|
452
|
+
if (results.componentMissingKeys.size > 0) {
|
|
453
|
+
hasError = true
|
|
454
|
+
console.log('❌ 以下组件使用的 $t() key 在 locale.ts 中未定义:\n')
|
|
455
|
+
results.componentMissingKeys.forEach((info, componentDir) => {
|
|
456
|
+
console.log(` 组件: ${componentDir}`)
|
|
457
|
+
console.log(` locale 文件: ${info.localePath}`)
|
|
458
|
+
console.log(` 缺失的 key:`)
|
|
459
|
+
Array.from(info.missingKeys).forEach(key => {
|
|
460
|
+
console.log(` - ${key}`)
|
|
461
|
+
})
|
|
462
|
+
if (info.files && info.files.length > 0) {
|
|
463
|
+
console.log(` 使用位置:`)
|
|
464
|
+
info.files.forEach(fileInfo => {
|
|
465
|
+
console.log(` ${fileInfo.file}`)
|
|
466
|
+
if (fileInfo.missingKeys && fileInfo.missingKeys.length > 0) {
|
|
467
|
+
console.log(` 缺失的 key: ${fileInfo.missingKeys.join(', ')}`)
|
|
468
|
+
}
|
|
469
|
+
})
|
|
470
|
+
}
|
|
471
|
+
console.log()
|
|
472
|
+
})
|
|
260
473
|
}
|
|
261
474
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
475
|
+
// 总结
|
|
476
|
+
if (hasError) {
|
|
477
|
+
console.log('❗ 检查未通过,请修复上述问题')
|
|
478
|
+
process.exitCode = 1
|
|
479
|
+
} else {
|
|
480
|
+
console.log('✅ 所有检查通过')
|
|
268
481
|
}
|
|
269
482
|
}
|
|
270
483
|
|
|
271
484
|
exports.run = run
|
|
485
|
+
|