@chaoswise/intl 3.1.0 → 3.1.2
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/chaoswise-intl.js
CHANGED
|
@@ -4,8 +4,9 @@ const runUpdate = require('./scripts/update');
|
|
|
4
4
|
const runInitConfig = require('./scripts/initConfig');
|
|
5
5
|
const runAddLocale = require('./scripts/addLocale');
|
|
6
6
|
const runNoZhCn = require('./scripts/nozhcn');
|
|
7
|
+
const runVerify = require('./scripts/verify');
|
|
7
8
|
|
|
8
|
-
const SCRIPTS = ['intl', 'collect', 'update', 'nozhcn', 'addLocale', 'init'];
|
|
9
|
+
const SCRIPTS = ['intl', 'collect', 'update', 'nozhcn', 'addLocale', 'init', 'verify'];
|
|
9
10
|
|
|
10
11
|
const args = process.argv.slice(2);
|
|
11
12
|
|
|
@@ -24,6 +25,20 @@ if (script === 'update') {
|
|
|
24
25
|
if (script === 'addLocale') {
|
|
25
26
|
runAddLocale();
|
|
26
27
|
}
|
|
28
|
+
// verify: 检测代码中所有 intl.get('id') 是否都在 relationKey.json 或本地 locale 文件中
|
|
29
|
+
// Usage:
|
|
30
|
+
// chaoswise-intl verify → 发现孤立 id 时 exit(1)(CI 门禁)
|
|
31
|
+
// chaoswise-intl verify --warn → 仅警告,不 exit(1)
|
|
32
|
+
// chaoswise-intl verify fix → 将孤立 id 的 intl 调用降级回字符串字面量(downgrade 策略)
|
|
33
|
+
if (script === 'verify') {
|
|
34
|
+
const subCommand = args[scriptIndex + 1];
|
|
35
|
+
if (subCommand === 'fix') {
|
|
36
|
+
runVerify.fix();
|
|
37
|
+
} else {
|
|
38
|
+
const exitOnMissing = !args.includes('--warn');
|
|
39
|
+
runVerify({ exitOnMissing });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
27
42
|
// nozhcn: detect (and optionally fix) Chinese characters in source files
|
|
28
43
|
// Usage:
|
|
29
44
|
// chaoswise-intl nozhcn → check mode (CI gate, exit 1 on findings)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const t = require('@babel/types');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* makeVisitorDowngrade
|
|
5
|
+
*
|
|
6
|
+
* 将代码中所有孤立 id(missingIdSet 中的 id)的 intl 调用降级回原始字符串字面量,
|
|
7
|
+
* 便于后续重新执行 collect → update 完成国际化。
|
|
8
|
+
*
|
|
9
|
+
* 支持两种形态:
|
|
10
|
+
*
|
|
11
|
+
* 形态 A(有 .d() 兜底):
|
|
12
|
+
* intl.get('orphan-id').d('确认提交')
|
|
13
|
+
* → '确认提交'
|
|
14
|
+
*
|
|
15
|
+
* 形态 B(裸 id,无默认文案):
|
|
16
|
+
* intl.get('orphan-id')
|
|
17
|
+
* → 无法恢复文案,记录到 noDefaultIds,由调用方输出警告,代码不修改
|
|
18
|
+
*
|
|
19
|
+
* @param {{ i18nObject, i18nMethod, i18nDefaultFunctionKey }} opts 来自 conf
|
|
20
|
+
* @param {{ missingIdSet: Set<string>, noDefaultIds: string[] }} returns
|
|
21
|
+
*/
|
|
22
|
+
module.exports = function makeVisitorDowngrade(
|
|
23
|
+
{ i18nObject, i18nMethod, i18nDefaultFunctionKey },
|
|
24
|
+
returns
|
|
25
|
+
) {
|
|
26
|
+
const { missingIdSet, noDefaultIds } = returns;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 判断一个节点是否是 intl.get('xxx') 调用,
|
|
30
|
+
* 返回第一个参数的字符串值,否则返回 null。
|
|
31
|
+
*/
|
|
32
|
+
function matchIntlCall(node) {
|
|
33
|
+
if (
|
|
34
|
+
node.type === 'CallExpression' &&
|
|
35
|
+
((node.callee.type === 'MemberExpression' &&
|
|
36
|
+
node.callee.object.name === i18nObject &&
|
|
37
|
+
node.callee.property.name === i18nMethod) ||
|
|
38
|
+
(!i18nObject &&
|
|
39
|
+
node.callee.type === 'Identifier' &&
|
|
40
|
+
node.callee.name === i18nMethod))
|
|
41
|
+
) {
|
|
42
|
+
const firstArg = node.arguments[0];
|
|
43
|
+
if (firstArg && firstArg.type === 'StringLiteral') {
|
|
44
|
+
return firstArg.value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
// 形态 A:intl.get('orphan-id').d('默认文案')
|
|
52
|
+
// 对应 AST:CallExpression { callee: MemberExpression('.d'), object: intl.get(...) }
|
|
53
|
+
CallExpression(path) {
|
|
54
|
+
const { node } = path;
|
|
55
|
+
|
|
56
|
+
if (
|
|
57
|
+
node.callee.type === 'MemberExpression' &&
|
|
58
|
+
node.callee.property.name === i18nDefaultFunctionKey &&
|
|
59
|
+
node.callee.object.type === 'CallExpression'
|
|
60
|
+
) {
|
|
61
|
+
const id = matchIntlCall(node.callee.object);
|
|
62
|
+
if (id && missingIdSet.has(id)) {
|
|
63
|
+
// 取 .d() 的第一个参数作为恢复后的字符串
|
|
64
|
+
const defaultArg = node.arguments[0];
|
|
65
|
+
if (defaultArg && defaultArg.type === 'StringLiteral') {
|
|
66
|
+
returns.hasTouch = true;
|
|
67
|
+
path.replaceWith(t.StringLiteral(defaultArg.value));
|
|
68
|
+
path.skip();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 形态 B:裸 intl.get('orphan-id'),无 .d()
|
|
75
|
+
const id = matchIntlCall(node);
|
|
76
|
+
if (id && missingIdSet.has(id)) {
|
|
77
|
+
// 无法恢复文案,记录警告,不修改代码
|
|
78
|
+
noDefaultIds.push({ id, loc: node.loc });
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
};
|
|
@@ -9,11 +9,14 @@ module.exports = function f(
|
|
|
9
9
|
const { replaceWords, downloadIds, defaultKeyWordMap } = returns;
|
|
10
10
|
|
|
11
11
|
// XXX: [TRICKY] 防止中文转码为 unicode
|
|
12
|
+
// NOTE: recast 的 StringLiteral 打印分支直接使用 node.value(nodeStr),
|
|
13
|
+
// 不像 @babel/generator 那样读取 extra.raw,所以必须把真实 id 写入 node.value。
|
|
12
14
|
function hackValue(value, id) {
|
|
13
|
-
|
|
15
|
+
const finalId = id || value;
|
|
16
|
+
return Object.assign(t.StringLiteral(finalId), {
|
|
14
17
|
extra: {
|
|
15
|
-
raw: `'${
|
|
16
|
-
rawValue:
|
|
18
|
+
raw: `'${finalId}'`,
|
|
19
|
+
rawValue: finalId,
|
|
17
20
|
},
|
|
18
21
|
});
|
|
19
22
|
}
|
|
@@ -17,6 +17,7 @@ const presetTypescript = require("@babel/preset-typescript").default;
|
|
|
17
17
|
|
|
18
18
|
const makeVisitorCollect = require("./makeVisitorCollect");
|
|
19
19
|
const makeVisitorUpdate = require("./makeVisitorUpdate");
|
|
20
|
+
const makeVisitorDowngrade = require("./makeVisitorDowngrade");
|
|
20
21
|
const log = require("./log");
|
|
21
22
|
|
|
22
23
|
// 获取文件中需要忽略转化通用国际化API规范的所有行号
|
|
@@ -84,7 +85,7 @@ function getIgnoreLines(ast) {
|
|
|
84
85
|
return ignoreLines;
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
module.exports = function (type, files = [], conf = {}, replaceWords) {
|
|
88
|
+
module.exports = function (type, files = [], conf = {}, replaceWords, extraData) {
|
|
88
89
|
const {
|
|
89
90
|
entry,
|
|
90
91
|
output,
|
|
@@ -152,6 +153,11 @@ module.exports = function (type, files = [], conf = {}, replaceWords) {
|
|
|
152
153
|
hasImport: false, // 是否已经引入通用国际化API,import {init} from ...;
|
|
153
154
|
hasTouch: false, // 是否存在需要替换的词条
|
|
154
155
|
};
|
|
156
|
+
// downgrade 模式:注入孤立 id 集合和无默认文案收集数组
|
|
157
|
+
if (type === "downgrade") {
|
|
158
|
+
r.missingIdSet = replaceWords; // 复用 replaceWords 参数位传入 Set<string>
|
|
159
|
+
r.noDefaultIds = extraData; // 无法恢复的裸 id 收集数组
|
|
160
|
+
}
|
|
155
161
|
const sourceCode = fs.readFileSync(filePath, "utf8");
|
|
156
162
|
|
|
157
163
|
let ast;
|
|
@@ -178,6 +184,8 @@ module.exports = function (type, files = [], conf = {}, replaceWords) {
|
|
|
178
184
|
let makeVisitor = makeVisitorCollect;
|
|
179
185
|
if (type === "update") {
|
|
180
186
|
makeVisitor = makeVisitorUpdate;
|
|
187
|
+
} else if (type === "downgrade") {
|
|
188
|
+
makeVisitor = makeVisitorDowngrade;
|
|
181
189
|
}
|
|
182
190
|
|
|
183
191
|
try {
|
|
@@ -192,7 +200,7 @@ module.exports = function (type, files = [], conf = {}, replaceWords) {
|
|
|
192
200
|
|
|
193
201
|
// 不传入需要替换的文词条则不进行文件修改
|
|
194
202
|
// 在只需要提取中文词条的情况下不传入replaceWords
|
|
195
|
-
if (!replaceWords) return;
|
|
203
|
+
if (type !== "downgrade" && !replaceWords) return;
|
|
196
204
|
|
|
197
205
|
// 使用 recast 输出代码,仅重新打印被修改的 AST 节点,保留原始格式
|
|
198
206
|
let code = recast.print(recastAst).code;
|
|
@@ -200,6 +208,7 @@ module.exports = function (type, files = [], conf = {}, replaceWords) {
|
|
|
200
208
|
if (!r.hasTouch) {
|
|
201
209
|
code = sourceCode;
|
|
202
210
|
} else if (
|
|
211
|
+
type !== "downgrade" &&
|
|
203
212
|
!r.hasImport &&
|
|
204
213
|
!file.special &&
|
|
205
214
|
importCode &&
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const glob = require('glob');
|
|
4
|
+
|
|
5
|
+
const getConf = require('./conf');
|
|
6
|
+
const { targetOutputFiles } = require('./util/getTargetFiles');
|
|
7
|
+
const transformAst = require('./util/transformAst');
|
|
8
|
+
const log = require('./util/log');
|
|
9
|
+
const file = require('./util/file');
|
|
10
|
+
|
|
11
|
+
// ── 公共:扫描并分类所有 id ────────────────────────────────────────────────────
|
|
12
|
+
function scanIds(conf, files) {
|
|
13
|
+
const info = transformAst('update', files, conf, {});
|
|
14
|
+
const allIds = [...new Set(info.downloadIds.filter(Boolean))];
|
|
15
|
+
|
|
16
|
+
const relationKey = file.readJson('relationKey.json') || {};
|
|
17
|
+
const pendingKeySet = new Set(Object.keys(relationKey));
|
|
18
|
+
|
|
19
|
+
const rootPath = process.cwd();
|
|
20
|
+
const localeDir = path.resolve(rootPath, conf.localeOutput, 'locales');
|
|
21
|
+
const localeFiles = glob.sync(`${localeDir}/*.json`);
|
|
22
|
+
const localIdSet = new Set();
|
|
23
|
+
localeFiles.forEach((f) => {
|
|
24
|
+
try {
|
|
25
|
+
const json = JSON.parse(fs.readFileSync(f, 'utf-8'));
|
|
26
|
+
Object.keys(json).forEach((k) => localIdSet.add(k));
|
|
27
|
+
} catch (e) {
|
|
28
|
+
log.error(`读取 locale 文件失败:${f}`);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const pending = [];
|
|
33
|
+
const missing = [];
|
|
34
|
+
const ok = [];
|
|
35
|
+
|
|
36
|
+
for (const id of allIds) {
|
|
37
|
+
if (pendingKeySet.has(id)) {
|
|
38
|
+
pending.push({ tempKey: id, realId: relationKey[id] });
|
|
39
|
+
} else if (localIdSet.has(id)) {
|
|
40
|
+
ok.push(id);
|
|
41
|
+
} else {
|
|
42
|
+
missing.push(id);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { allIds, pending, missing, ok };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── 公共:打印分类结果 ─────────────────────────────────────────────────────────
|
|
50
|
+
function printResult({ allIds, pending, missing, ok }, files) {
|
|
51
|
+
console.log('');
|
|
52
|
+
log.info(`扫描到 ${allIds.length} 个唯一 id(文件数:${files.length})`);
|
|
53
|
+
|
|
54
|
+
if (ok.length) {
|
|
55
|
+
log.success(`✓ ${ok.length} 个 id 已在本地 locale 文件中覆盖`);
|
|
56
|
+
}
|
|
57
|
+
if (pending.length) {
|
|
58
|
+
log.warnToLog(`⚠ ${pending.length} 个临时 key 尚未执行 update 替换:`);
|
|
59
|
+
pending.forEach(({ tempKey, realId }) => {
|
|
60
|
+
log.warnToLog(` ${tempKey} → ${realId}`);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (missing.length) {
|
|
64
|
+
log.error(`✗ ${missing.length} 个孤立 id(relationKey.json 和本地 locale 文件中均不存在):`);
|
|
65
|
+
missing.forEach((id) => log.error(` ${id}`));
|
|
66
|
+
console.log('');
|
|
67
|
+
log.error('提示:孤立 id 可能是未上传到国际化平台、未执行 update 拉取,或已在平台删除的词条');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* verify: 检测当前工程输出文件中所有 intl.get('id') 的 id
|
|
73
|
+
* - pending : 临时 key,存在于 relationKey.json(尚未执行 update 替换)
|
|
74
|
+
* - ok : 已在本地 locale 文件中找到
|
|
75
|
+
* - missing : 两处均找不到,属于"孤立 id",运行时将无法解析
|
|
76
|
+
*
|
|
77
|
+
* 用法:
|
|
78
|
+
* chaoswise-intl verify → 发现 missing 时 exit(1)(CI 门禁)
|
|
79
|
+
* chaoswise-intl verify --warn → 仅警告,不 exit(1)
|
|
80
|
+
*/
|
|
81
|
+
async function verify({ exitOnMissing = true } = {}) {
|
|
82
|
+
log.info('词条 id 校验中...');
|
|
83
|
+
|
|
84
|
+
const conf = getConf();
|
|
85
|
+
const files = targetOutputFiles(conf);
|
|
86
|
+
|
|
87
|
+
if (!files.length) {
|
|
88
|
+
log.warnToLog('未找到需要扫描的文件,请检查 output 配置');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const result = scanIds(conf, files);
|
|
93
|
+
|
|
94
|
+
if (!result.allIds.length) {
|
|
95
|
+
log.success('未发现任何国际化 id,无需校验');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
printResult(result, files);
|
|
100
|
+
|
|
101
|
+
if (result.missing.length) {
|
|
102
|
+
if (exitOnMissing) {
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
console.log('');
|
|
107
|
+
log.success('★★★ 所有词条 id 均已覆盖,校验通过 ★★★');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* verifyFix: 将孤立 id 对应的 intl 调用降级回字符串字面量(downgrade 策略)
|
|
113
|
+
*
|
|
114
|
+
* 形态 A intl.get('orphan-id').d('默认文案') → '默认文案'
|
|
115
|
+
* 形态 B intl.get('orphan-id') → 无法自动恢复,仅打印警告 + 行号
|
|
116
|
+
*
|
|
117
|
+
* 修复完成后重新扫描,确认无残留孤立 id。
|
|
118
|
+
*
|
|
119
|
+
* 用法:
|
|
120
|
+
* chaoswise-intl verify fix
|
|
121
|
+
*/
|
|
122
|
+
async function verifyFix() {
|
|
123
|
+
log.info('孤立词条降级修复中...');
|
|
124
|
+
|
|
125
|
+
const conf = getConf();
|
|
126
|
+
const files = targetOutputFiles(conf);
|
|
127
|
+
|
|
128
|
+
if (!files.length) {
|
|
129
|
+
log.warnToLog('未找到需要扫描的文件,请检查 output 配置');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Step 1: 扫描,找出孤立 id ────────────────────────────────────────────
|
|
134
|
+
const { missing, allIds } = scanIds(conf, files);
|
|
135
|
+
|
|
136
|
+
if (!allIds.length) {
|
|
137
|
+
log.success('未发现任何国际化 id,无需处理');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!missing.length) {
|
|
142
|
+
log.success('没有孤立 id,无需修复');
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
log.info(`共 ${missing.length} 个孤立 id,开始降级处理...`);
|
|
147
|
+
missing.forEach((id) => log.warnToLog(` - ${id}`));
|
|
148
|
+
console.log('');
|
|
149
|
+
|
|
150
|
+
// ── Step 2: 执行 downgrade AST 变换(写回文件)────────────────────────────
|
|
151
|
+
const missingIdSet = new Set(missing);
|
|
152
|
+
const noDefaultIds = []; // 形态B:无法自动恢复的裸 id
|
|
153
|
+
|
|
154
|
+
// transformAst 的 replaceWords 参数传入非空对象才触发写文件,
|
|
155
|
+
// downgrade visitor 通过 returns.missingIdSet / noDefaultIds 通信
|
|
156
|
+
transformAst('downgrade', files, conf, missingIdSet, noDefaultIds);
|
|
157
|
+
|
|
158
|
+
// ── Step 3: 打印形态 B 警告 ──────────────────────────────────────────────
|
|
159
|
+
if (noDefaultIds.length) {
|
|
160
|
+
console.log('');
|
|
161
|
+
log.warnToLog(`⚠ ${noDefaultIds.length} 处孤立 id 没有 .d() 默认文案,无法自动恢复,需手动处理:`);
|
|
162
|
+
noDefaultIds.forEach(({ id, loc }) => {
|
|
163
|
+
const locStr = loc ? ` (行 ${loc.start.line},列 ${loc.start.column + 1})` : '';
|
|
164
|
+
log.warnToLog(` ${id}${locStr}`);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Step 4: 重新扫描,确认修复结果 ──────────────────────────────────────
|
|
169
|
+
console.log('');
|
|
170
|
+
log.info('修复完成,重新扫描验证...');
|
|
171
|
+
const after = scanIds(conf, files);
|
|
172
|
+
printResult(after, files);
|
|
173
|
+
|
|
174
|
+
if (!after.missing.length) {
|
|
175
|
+
console.log('');
|
|
176
|
+
log.success('★★★ 孤立 id 已全部降级,请执行 chaoswise-intl collect 重新国际化 ★★★');
|
|
177
|
+
} else {
|
|
178
|
+
console.log('');
|
|
179
|
+
log.error(`仍有 ${after.missing.length} 个孤立 id 未能自动修复(均为形态 B),请手动处理后重试`);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = verify;
|
|
185
|
+
module.exports.fix = verifyFix;
|