feima-shortcuts 0.3.0-beta.5 → 0.3.0-beta.7
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/bin/feima.js
CHANGED
|
@@ -3,7 +3,9 @@ const inquirer = require("inquirer");
|
|
|
3
3
|
const { Command } = require('commander')
|
|
4
4
|
const feima = require("../src/generate");
|
|
5
5
|
|
|
6
|
-
const checkLocale
|
|
6
|
+
const checkLocale = require('../src/scripts/check-locale')
|
|
7
|
+
const checkI18nDefault = require('../src/scripts/check-i18n-default')
|
|
8
|
+
const checkI18nViews = require('../src/scripts/check-i18n-views')
|
|
7
9
|
const { version } = require("../package.json"); // 读取 package.json 中的版本号
|
|
8
10
|
|
|
9
11
|
const program = new Command()
|
|
@@ -19,6 +21,20 @@ program
|
|
|
19
21
|
checkLocale.run();
|
|
20
22
|
})
|
|
21
23
|
|
|
24
|
+
program
|
|
25
|
+
.command('check-i18n-default')
|
|
26
|
+
.description('检查未设置默认值的 t')
|
|
27
|
+
.action(() => {
|
|
28
|
+
checkI18nDefault.run();
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
program
|
|
32
|
+
.command('check-i18n-views')
|
|
33
|
+
.description('检查已使用未创建的 key')
|
|
34
|
+
.action(() => {
|
|
35
|
+
checkI18nViews.run();
|
|
36
|
+
})
|
|
37
|
+
|
|
22
38
|
|
|
23
39
|
const run = () => {
|
|
24
40
|
console.log(`🚀 当前版本:v${version}`);
|
package/package.json
CHANGED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* i18n 默认值检查脚本
|
|
5
|
+
*
|
|
6
|
+
* 规则:
|
|
7
|
+
* - 扫描 src/components、src/views
|
|
8
|
+
* - 支持 t / $t
|
|
9
|
+
* - 识别是否存在默认值(第二个参数)
|
|
10
|
+
* - 汇总缺失默认值的 key
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs')
|
|
14
|
+
const path = require('path')
|
|
15
|
+
const glob = require('glob')
|
|
16
|
+
|
|
17
|
+
const ROOT = process.cwd()
|
|
18
|
+
const SCAN_DIRS = ['src/components', 'src/views']
|
|
19
|
+
const FILE_EXT = '{ts,js,tsx,jsx,vue}'
|
|
20
|
+
|
|
21
|
+
// 匹配:t('xxx') / t("xxx", "默认值")
|
|
22
|
+
const I18N_REG = /\b(\$?t)\s*\(\s*(['"])([^'"]+)\2\s*(?:,\s*(['"])([^'"]*)\4)?\s*\)/g
|
|
23
|
+
|
|
24
|
+
/** 结果结构 */
|
|
25
|
+
const resultMap = new Map()
|
|
26
|
+
const missingList = []
|
|
27
|
+
|
|
28
|
+
function scanFile(filePath) {
|
|
29
|
+
const code = fs.readFileSync(filePath, 'utf-8')
|
|
30
|
+
let match
|
|
31
|
+
|
|
32
|
+
while ((match = I18N_REG.exec(code))) {
|
|
33
|
+
const [, fn, , key, , defaultValue] = match
|
|
34
|
+
|
|
35
|
+
if (!resultMap.has(key)) {
|
|
36
|
+
resultMap.set(key, {
|
|
37
|
+
key,
|
|
38
|
+
defaultValue: defaultValue || '',
|
|
39
|
+
locations: [],
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const record = resultMap.get(key)
|
|
44
|
+
record.locations.push({
|
|
45
|
+
file: path.relative(ROOT, filePath),
|
|
46
|
+
fn,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// 有一次缺失默认值就标记
|
|
50
|
+
if (!defaultValue) {
|
|
51
|
+
missingList.push({
|
|
52
|
+
key,
|
|
53
|
+
file: path.relative(ROOT, filePath),
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function run() {
|
|
60
|
+
SCAN_DIRS.forEach((dir) => {
|
|
61
|
+
const absDir = path.join(ROOT, dir)
|
|
62
|
+
if (!fs.existsSync(absDir)) return
|
|
63
|
+
|
|
64
|
+
const files = glob.sync(`${absDir}/**/*.${FILE_EXT}`, {
|
|
65
|
+
nodir: true,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
files.forEach(scanFile)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
console.log('\n📦 i18n 使用统计\n')
|
|
72
|
+
|
|
73
|
+
console.log(`共发现 key 数量:${resultMap.size}`)
|
|
74
|
+
console.log(`缺失默认值使用次数:${missingList.length}\n`)
|
|
75
|
+
|
|
76
|
+
if (missingList.length) {
|
|
77
|
+
console.log('❌ 以下 key 缺失默认值:\n')
|
|
78
|
+
|
|
79
|
+
const uniq = new Map()
|
|
80
|
+
missingList.forEach((i) => {
|
|
81
|
+
if (!uniq.has(i.key)) uniq.set(i.key, [])
|
|
82
|
+
uniq.get(i.key).push(i.file)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
uniq.forEach((files, key) => {
|
|
86
|
+
console.log(`- ${key}`)
|
|
87
|
+
files.forEach((f) => {
|
|
88
|
+
console.log(` ↳ ${f}`)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
console.log('\n⚠️ 请为以上 key 补充默认值')
|
|
93
|
+
process.exitCode = 1
|
|
94
|
+
} else {
|
|
95
|
+
console.log('✅ 所有 i18n key 均设置了默认值')
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
exports.run = run
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const glob = require('glob')
|
|
4
|
+
const { parse } = require('@vue/compiler-sfc')
|
|
5
|
+
const babelParser = require('@babel/parser')
|
|
6
|
+
const traverse = require('@babel/traverse').default
|
|
7
|
+
|
|
8
|
+
/* ---------------- defaults ---------------- */
|
|
9
|
+
|
|
10
|
+
const DEFAULT_OPTIONS = {
|
|
11
|
+
cwd: process.cwd(),
|
|
12
|
+
viewsDir: 'src/views',
|
|
13
|
+
silent: false,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* ---------------- utils ---------------- */
|
|
17
|
+
|
|
18
|
+
function flattenLocale(obj, prefix = '') {
|
|
19
|
+
let res = []
|
|
20
|
+
for (const k in obj) {
|
|
21
|
+
const key = prefix ? `${prefix}.${k}` : k
|
|
22
|
+
if (typeof obj[k] === 'object') {
|
|
23
|
+
res = res.concat(flattenLocale(obj[k], key))
|
|
24
|
+
} else {
|
|
25
|
+
res.push(key)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return res
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function loadLocaleKeys(localePath) {
|
|
32
|
+
if (!fs.existsSync(localePath)) return null
|
|
33
|
+
const mod = require(localePath)
|
|
34
|
+
const localeObj = mod.default || mod
|
|
35
|
+
return flattenLocale(localeObj)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseCode(code) {
|
|
39
|
+
return babelParser.parse(code, {
|
|
40
|
+
sourceType: 'module',
|
|
41
|
+
plugins: ['typescript', 'jsx'],
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* ---------------- core ---------------- */
|
|
46
|
+
|
|
47
|
+
function run(userOptions = {}) {
|
|
48
|
+
// ✅ 保证 options 永远有值
|
|
49
|
+
const options = { ...DEFAULT_OPTIONS, ...userOptions }
|
|
50
|
+
const { cwd, viewsDir, silent } = options
|
|
51
|
+
|
|
52
|
+
const absViewsDir = path.join(cwd, viewsDir)
|
|
53
|
+
|
|
54
|
+
if (!fs.existsSync(absViewsDir)) {
|
|
55
|
+
if (!silent) {
|
|
56
|
+
console.warn(`⚠️ views 目录不存在:${viewsDir}`)
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
success: true,
|
|
60
|
+
missingKeys: [],
|
|
61
|
+
noLocaleUsage: [],
|
|
62
|
+
scannedFiles: 0,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const files = glob.sync('**/*.{vue,ts,tsx}', {
|
|
67
|
+
cwd: absViewsDir,
|
|
68
|
+
absolute: true,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const missingKeys = []
|
|
72
|
+
const noLocaleUsage = []
|
|
73
|
+
|
|
74
|
+
for (const file of files) {
|
|
75
|
+
const content = fs.readFileSync(file, 'utf-8')
|
|
76
|
+
|
|
77
|
+
let scriptCode = content
|
|
78
|
+
|
|
79
|
+
if (file.endsWith('.vue')) {
|
|
80
|
+
const { descriptor } = parse(content)
|
|
81
|
+
scriptCode =
|
|
82
|
+
descriptor.scriptSetup?.content ||
|
|
83
|
+
descriptor.script?.content ||
|
|
84
|
+
''
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!scriptCode) continue
|
|
88
|
+
|
|
89
|
+
const ast = parseCode(scriptCode)
|
|
90
|
+
|
|
91
|
+
let usedLocale = false
|
|
92
|
+
const usedKeys = []
|
|
93
|
+
|
|
94
|
+
traverse(ast, {
|
|
95
|
+
ImportDeclaration(p) {
|
|
96
|
+
if (p.node.source.value === './locale') {
|
|
97
|
+
usedLocale = true
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
CallExpression(p) {
|
|
102
|
+
const callee = p.node.callee
|
|
103
|
+
if (
|
|
104
|
+
(callee.type === 'Identifier' && callee.name === 't') ||
|
|
105
|
+
(callee.type === 'Identifier' && callee.name === '$t')
|
|
106
|
+
) {
|
|
107
|
+
const arg = p.node.arguments[0]
|
|
108
|
+
if (arg && arg.type === 'StringLiteral') {
|
|
109
|
+
usedKeys.push(arg.value)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
if (!usedKeys.length) continue
|
|
116
|
+
|
|
117
|
+
const localePath = path.join(path.dirname(file), 'locale.ts')
|
|
118
|
+
const localeKeys = loadLocaleKeys(localePath)
|
|
119
|
+
|
|
120
|
+
if (!usedLocale || !localeKeys) {
|
|
121
|
+
noLocaleUsage.push({
|
|
122
|
+
file: path.relative(cwd, file),
|
|
123
|
+
keys: usedKeys,
|
|
124
|
+
})
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const key of usedKeys) {
|
|
129
|
+
if (!localeKeys.includes(key)) {
|
|
130
|
+
missingKeys.push({
|
|
131
|
+
file: path.relative(cwd, file),
|
|
132
|
+
key,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* ---------------- output ---------------- */
|
|
139
|
+
|
|
140
|
+
if (!silent) {
|
|
141
|
+
if (missingKeys.length) {
|
|
142
|
+
console.error('\n❌ 缺失的国际化 key:')
|
|
143
|
+
missingKeys.forEach(i =>
|
|
144
|
+
console.error(` ${i.file} → ${i.key}`)
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (noLocaleUsage.length) {
|
|
149
|
+
console.warn('\n⚠️ 使用了 t/$t 但未使用 ./locale:')
|
|
150
|
+
noLocaleUsage.forEach(i =>
|
|
151
|
+
console.warn(` ${i.file}`)
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!missingKeys.length && !noLocaleUsage.length) {
|
|
156
|
+
console.log('✅ views 国际化检查通过')
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
success: missingKeys.length === 0,
|
|
162
|
+
missingKeys,
|
|
163
|
+
noLocaleUsage,
|
|
164
|
+
scannedFiles: files.length,
|
|
165
|
+
options, // 🔍 调试/日志时很有用
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* ---------------- exports ---------------- */
|
|
170
|
+
|
|
171
|
+
exports.run = run
|