@chaoswise/intl 3.1.1 → 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.
@@ -27,11 +27,17 @@ if (script === 'addLocale') {
27
27
  }
28
28
  // verify: 检测代码中所有 intl.get('id') 是否都在 relationKey.json 或本地 locale 文件中
29
29
  // Usage:
30
- // chaoswise-intl verify → 发现孤立 id 时 exit(1)(CI 门禁)
31
- // chaoswise-intl verify --warn → 仅警告,不 exit(1)
30
+ // chaoswise-intl verify → 发现孤立 id 时 exit(1)(CI 门禁)
31
+ // chaoswise-intl verify --warn → 仅警告,不 exit(1)
32
+ // chaoswise-intl verify fix → 将孤立 id 的 intl 调用降级回字符串字面量(downgrade 策略)
32
33
  if (script === 'verify') {
33
- const exitOnMissing = !args.includes('--warn');
34
- runVerify({ exitOnMissing });
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
+ }
35
41
  }
36
42
  // nozhcn: detect (and optionally fix) Chinese characters in source files
37
43
  // Usage:
@@ -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
+ };
@@ -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 &&
@@ -8,41 +8,14 @@ const transformAst = require('./util/transformAst');
8
8
  const log = require('./util/log');
9
9
  const file = require('./util/file');
10
10
 
11
- /**
12
- * verify: 检测当前工程输出文件中所有 intl.get('id') 的 id
13
- * - pending : 临时 key,存在于 relationKey.json(尚未执行 update 替换)
14
- * - ok : 已在本地 locale 文件中找到
15
- * - missing : 两处均找不到,属于"孤立 id",运行时将无法解析
16
- *
17
- * 用法:
18
- * chaoswise-intl verify → 发现 missing 时 exit(1)(CI 门禁)
19
- * chaoswise-intl verify --warn → 仅警告,不 exit(1)
20
- */
21
- async function verify({ exitOnMissing = true } = {}) {
22
- log.info('词条 id 校验中...');
23
-
24
- const conf = getConf();
25
- const files = targetOutputFiles(conf);
26
-
27
- if (!files.length) {
28
- log.warn('未找到需要扫描的文件,请检查 output 配置');
29
- return;
30
- }
31
-
32
- // ── Step 1: 收集代码中所有原始 id(传入空对象,不触发替换也不写文件)──────
11
+ // ── 公共:扫描并分类所有 id ────────────────────────────────────────────────────
12
+ function scanIds(conf, files) {
33
13
  const info = transformAst('update', files, conf, {});
34
14
  const allIds = [...new Set(info.downloadIds.filter(Boolean))];
35
15
 
36
- if (!allIds.length) {
37
- log.success('未发现任何国际化 id,无需校验');
38
- return;
39
- }
40
-
41
- // ── Step 2: 从 relationKey.json 获取待替换的临时 key 集合 ───────────────
42
16
  const relationKey = file.readJson('relationKey.json') || {};
43
17
  const pendingKeySet = new Set(Object.keys(relationKey));
44
18
 
45
- // ── Step 3: 从本地 locale 文件获取已下载的合法 id 集合 ────────────────────
46
19
  const rootPath = process.cwd();
47
20
  const localeDir = path.resolve(rootPath, conf.localeOutput, 'locales');
48
21
  const localeFiles = glob.sync(`${localeDir}/*.json`);
@@ -56,7 +29,6 @@ async function verify({ exitOnMissing = true } = {}) {
56
29
  }
57
30
  });
58
31
 
59
- // ── Step 4: 分类统计 ──────────────────────────────────────────────────────
60
32
  const pending = [];
61
33
  const missing = [];
62
34
  const ok = [];
@@ -71,27 +43,62 @@ async function verify({ exitOnMissing = true } = {}) {
71
43
  }
72
44
  }
73
45
 
74
- // ── Step 5: 输出结果 ──────────────────────────────────────────────────────
46
+ return { allIds, pending, missing, ok };
47
+ }
48
+
49
+ // ── 公共:打印分类结果 ─────────────────────────────────────────────────────────
50
+ function printResult({ allIds, pending, missing, ok }, files) {
75
51
  console.log('');
76
52
  log.info(`扫描到 ${allIds.length} 个唯一 id(文件数:${files.length})`);
77
53
 
78
54
  if (ok.length) {
79
55
  log.success(`✓ ${ok.length} 个 id 已在本地 locale 文件中覆盖`);
80
56
  }
81
-
82
57
  if (pending.length) {
83
58
  log.warnToLog(`⚠ ${pending.length} 个临时 key 尚未执行 update 替换:`);
84
59
  pending.forEach(({ tempKey, realId }) => {
85
60
  log.warnToLog(` ${tempKey} → ${realId}`);
86
61
  });
87
62
  }
88
-
89
63
  if (missing.length) {
90
- log.error(`✗ ${missing.length} idrelationKey.json 和本地 locale 文件中均不存在(孤立 id):`);
64
+ log.error(`✗ ${missing.length} 个孤立 idrelationKey.json 和本地 locale 文件中均不存在):`);
91
65
  missing.forEach((id) => log.error(` ${id}`));
92
66
  console.log('');
93
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);
94
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) {
95
102
  if (exitOnMissing) {
96
103
  process.exit(1);
97
104
  }
@@ -101,4 +108,78 @@ async function verify({ exitOnMissing = true } = {}) {
101
108
  }
102
109
  }
103
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
+
104
184
  module.exports = verify;
185
+ module.exports.fix = verifyFix;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chaoswise/intl",
3
- "version": "3.1.1",
3
+ "version": "3.1.2",
4
4
  "author": "cloudwiser",
5
5
  "description": "intl",
6
6
  "main": "lib/index.js",