@chaoswise/intl 3.1.1 → 3.1.3
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 +17 -4
- package/bin/scripts/conf/default.js +16 -2
- package/bin/scripts/nozhcn.js +125 -17
- package/bin/scripts/nozhcnGuard.js +444 -0
- package/bin/scripts/util/findZhCnInFile.js +127 -6
- package/bin/scripts/util/fixI18nDefaultInFile.js +102 -4
- package/bin/scripts/util/fixZhCnInFile.js +290 -0
- package/bin/scripts/util/makeVisitorDowngrade.js +82 -0
- package/bin/scripts/util/transformAst.js +11 -2
- package/bin/scripts/verify.js +115 -34
- package/package.json +1 -1
|
@@ -35,6 +35,73 @@ const log = require('./log');
|
|
|
35
35
|
// Chinese character detection regex
|
|
36
36
|
const ZH_REGEX = /[\u4e00-\u9fa5]/;
|
|
37
37
|
|
|
38
|
+
function escapeRegExp(text) {
|
|
39
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildRawStringLiteral(value, quote) {
|
|
43
|
+
const q = quote === '"' ? '"' : '\'';
|
|
44
|
+
const escaped = String(value)
|
|
45
|
+
.replace(/\\/g, '\\\\')
|
|
46
|
+
.replace(/\r/g, '\\r')
|
|
47
|
+
.replace(/\n/g, '\\n')
|
|
48
|
+
.replace(/\u2028/g, '\\u2028')
|
|
49
|
+
.replace(/\u2029/g, '\\u2029')
|
|
50
|
+
.replace(new RegExp(escapeRegExp(q), 'g'), `\\${q}`);
|
|
51
|
+
|
|
52
|
+
return `${q}${escaped}${q}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function hasOwnTranslation(localeMap, key) {
|
|
56
|
+
return Object.prototype.hasOwnProperty.call(localeMap, key);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function replaceEmbeddedI18nDefaultInText(text, localeMap, opts, missing) {
|
|
60
|
+
const {
|
|
61
|
+
i18nObject = 'intl',
|
|
62
|
+
i18nMethod = 'get',
|
|
63
|
+
i18nDefaultFunctionKey = 'd',
|
|
64
|
+
} = opts || {};
|
|
65
|
+
|
|
66
|
+
const objectPrefix = i18nObject ? `${escapeRegExp(i18nObject)}\\s*\\.\\s*` : '';
|
|
67
|
+
const method = escapeRegExp(i18nMethod);
|
|
68
|
+
const dMethod = escapeRegExp(i18nDefaultFunctionKey);
|
|
69
|
+
|
|
70
|
+
const pattern = new RegExp(
|
|
71
|
+
`${objectPrefix}${method}\\s*\\(\\s*(["'])((?:\\\\.|(?!\\1).)*)\\1\\s*\\)\\s*\\.\\s*${dMethod}\\s*\\(\\s*(["'])((?:\\\\.|(?!\\3).)*)\\3\\s*\\)`,
|
|
72
|
+
'g'
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
let replaced = 0;
|
|
76
|
+
const next = text.replace(pattern, (full, keyQuote, rawKey, valueQuote, rawValue) => {
|
|
77
|
+
const key = rawKey.replace(/\\(["'])/g, '$1');
|
|
78
|
+
const chinese = rawValue.replace(/\\(["'])/g, '$1');
|
|
79
|
+
|
|
80
|
+
if (!ZH_REGEX.test(chinese)) {
|
|
81
|
+
return full;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!hasOwnTranslation(localeMap, key)) {
|
|
85
|
+
missing.push({ key, chinese });
|
|
86
|
+
return full;
|
|
87
|
+
}
|
|
88
|
+
const translation = localeMap[key];
|
|
89
|
+
|
|
90
|
+
const escapedTranslation = String(translation)
|
|
91
|
+
.replace(/\\/g, '\\\\')
|
|
92
|
+
.replace(new RegExp(valueQuote, 'g'), `\\${valueQuote}`);
|
|
93
|
+
|
|
94
|
+
replaced += 1;
|
|
95
|
+
|
|
96
|
+
return full.replace(
|
|
97
|
+
new RegExp(`${escapeRegExp(valueQuote)}((?:\\\\.|(?!${escapeRegExp(valueQuote)}).)*)${escapeRegExp(valueQuote)}\\s*\\)$`),
|
|
98
|
+
`${valueQuote}${escapedTranslation}${valueQuote})`
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return { text: next, replaced };
|
|
103
|
+
}
|
|
104
|
+
|
|
38
105
|
/**
|
|
39
106
|
* Extract the i18n key from the "inner" `.get('key')` call that wraps a `.d()`.
|
|
40
107
|
*
|
|
@@ -145,12 +212,11 @@ module.exports = function fixI18nDefaultInFile(filePath, localeMap, opts) {
|
|
|
145
212
|
const key = extractI18nKey(node, i18nMethod, i18nDefaultFunctionKey, i18nObject);
|
|
146
213
|
if (!key) return;
|
|
147
214
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (!translation) {
|
|
215
|
+
if (!hasOwnTranslation(localeMap, key)) {
|
|
151
216
|
missing.push({ key, chinese: arg.value });
|
|
152
217
|
return;
|
|
153
218
|
}
|
|
219
|
+
const translation = localeMap[key];
|
|
154
220
|
|
|
155
221
|
// Replace the argument node with a new StringLiteral containing
|
|
156
222
|
// the translation. We use Object.assign to preserve recast's
|
|
@@ -160,6 +226,38 @@ module.exports = function fixI18nDefaultInFile(filePath, localeMap, opts) {
|
|
|
160
226
|
Object.assign(arg, t.stringLiteral(translation), { extra: undefined });
|
|
161
227
|
fixed++;
|
|
162
228
|
},
|
|
229
|
+
StringLiteral(nodePath) {
|
|
230
|
+
const node = nodePath.node;
|
|
231
|
+
if (!node || typeof node.value !== 'string') return;
|
|
232
|
+
if (!ZH_REGEX.test(node.value)) return;
|
|
233
|
+
|
|
234
|
+
const result = replaceEmbeddedI18nDefaultInText(node.value, localeMap, opts, missing);
|
|
235
|
+
if (result.replaced > 0 && result.text !== node.value) {
|
|
236
|
+
const raw = node.extra && typeof node.extra.raw === 'string' ? node.extra.raw : '';
|
|
237
|
+
const originalQuote = raw.startsWith('"') ? '"' : '\'';
|
|
238
|
+
node.value = result.text;
|
|
239
|
+
node.extra = {
|
|
240
|
+
...(node.extra || {}),
|
|
241
|
+
rawValue: result.text,
|
|
242
|
+
raw: buildRawStringLiteral(result.text, originalQuote),
|
|
243
|
+
};
|
|
244
|
+
fixed += result.replaced;
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
TemplateElement(nodePath) {
|
|
248
|
+
const node = nodePath.node;
|
|
249
|
+
if (!node || !node.value || typeof node.value.cooked !== 'string') return;
|
|
250
|
+
if (!ZH_REGEX.test(node.value.cooked)) return;
|
|
251
|
+
|
|
252
|
+
const result = replaceEmbeddedI18nDefaultInText(node.value.cooked, localeMap, opts, missing);
|
|
253
|
+
if (result.replaced > 0 && result.text !== node.value.cooked) {
|
|
254
|
+
Object.assign(node, t.templateElement({
|
|
255
|
+
raw: result.text,
|
|
256
|
+
cooked: result.text,
|
|
257
|
+
}, node.tail), { extra: undefined });
|
|
258
|
+
fixed += result.replaced;
|
|
259
|
+
}
|
|
260
|
+
},
|
|
163
261
|
});
|
|
164
262
|
|
|
165
263
|
if (fixed === 0) {
|
|
@@ -167,7 +265,7 @@ module.exports = function fixI18nDefaultInFile(filePath, localeMap, opts) {
|
|
|
167
265
|
}
|
|
168
266
|
|
|
169
267
|
// ── Write back using recast (minimal diff) ───────────────────────────
|
|
170
|
-
const newCode = recast.print(recastAst).code;
|
|
268
|
+
const newCode = recast.print(recastAst, { quote: 'auto' }).code;
|
|
171
269
|
try {
|
|
172
270
|
fs.writeFileSync(filePath, newCode, { encoding: 'utf-8' });
|
|
173
271
|
} catch (err) {
|
|
@@ -215,3 +215,293 @@ module.exports = function fixZhCnInFile(filePath, findings, strategy = 'clean')
|
|
|
215
215
|
|
|
216
216
|
return patchedCount;
|
|
217
217
|
};
|
|
218
|
+
|
|
219
|
+
// ─── JSX SVG attribute fix ───────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
// Strip Chinese + CJK punctuation / fullwidth forms (design-tool residue)
|
|
222
|
+
const ZH_CLEAN_REGEX = /[\u4e00-\u9fa5\u3000-\u303f\uff00-\uffef]/g;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Fix JSX SVG attribute findings (type 'jsx-svg-attr') by stripping Chinese
|
|
226
|
+
* characters from the string literal values.
|
|
227
|
+
*
|
|
228
|
+
* For each finding:
|
|
229
|
+
* - strategy 'clean' : strip Chinese (+ CJK punctuation) from the value.
|
|
230
|
+
* If the cleaned value is empty, remove the entire
|
|
231
|
+
* JSX attribute (name + '=' + value).
|
|
232
|
+
* - strategy 'remove': remove the entire JSX attribute unconditionally.
|
|
233
|
+
*
|
|
234
|
+
* Byte offsets on the finding (start / end) point to the StringLiteral node
|
|
235
|
+
* including its quote characters, as produced by findZhCnInFile.js.
|
|
236
|
+
*
|
|
237
|
+
* @param {string} filePath - absolute file path
|
|
238
|
+
* @param {Finding[]} findings - array of { type:'jsx-svg-attr', start, end, ... }
|
|
239
|
+
* @param {'clean'|'remove'} strategy
|
|
240
|
+
* @returns {number} count of attributes fixed
|
|
241
|
+
*/
|
|
242
|
+
function fixJsxSvgAttrInFile(filePath, findings, strategy = 'clean') {
|
|
243
|
+
const attrFindings = findings.filter(
|
|
244
|
+
(f) => f.type === 'jsx-svg-attr' && typeof f.start === 'number' && typeof f.end === 'number'
|
|
245
|
+
);
|
|
246
|
+
if (!attrFindings.length) return 0;
|
|
247
|
+
|
|
248
|
+
// Defensive dedupe: duplicated ranges can cause second-pass patches to use
|
|
249
|
+
// stale offsets and accidentally trim following attributes.
|
|
250
|
+
const dedupMap = new Map();
|
|
251
|
+
attrFindings.forEach((f) => {
|
|
252
|
+
const key = `${f.start}:${f.end}`;
|
|
253
|
+
if (!dedupMap.has(key)) dedupMap.set(key, f);
|
|
254
|
+
});
|
|
255
|
+
const uniqueFindings = Array.from(dedupMap.values());
|
|
256
|
+
|
|
257
|
+
let source;
|
|
258
|
+
try {
|
|
259
|
+
source = fs.readFileSync(filePath, 'utf8');
|
|
260
|
+
} catch (err) {
|
|
261
|
+
log.error(`[nozhcn] Cannot read file for fixing: ${filePath} — ${err.message}`);
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Sort descending by start so later patches don't invalidate earlier offsets
|
|
266
|
+
const sorted = [...uniqueFindings].sort((a, b) => b.start - a.start);
|
|
267
|
+
|
|
268
|
+
let patchedCount = 0;
|
|
269
|
+
|
|
270
|
+
for (const finding of sorted) {
|
|
271
|
+
const { start, end } = finding;
|
|
272
|
+
const original = source.slice(start, end); // e.g. "编组1" (with quotes)
|
|
273
|
+
if (!original || original.length < 2) continue;
|
|
274
|
+
|
|
275
|
+
const quote = original[0]; // '"' or "'"
|
|
276
|
+
// If boundaries are no longer a string literal, skip safely.
|
|
277
|
+
if ((quote !== '"' && quote !== "'") || original[original.length - 1] !== quote) {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const rawValue = original.slice(1, -1);
|
|
281
|
+
const cleaned = strategy === 'remove' ? '' : rawValue.replace(ZH_CLEAN_REGEX, '').trim();
|
|
282
|
+
|
|
283
|
+
if (cleaned) {
|
|
284
|
+
// Just replace the string value in-place
|
|
285
|
+
source = source.slice(0, start) + quote + cleaned + quote + source.slice(end);
|
|
286
|
+
} else {
|
|
287
|
+
// Cleaned value is empty — remove the whole JSX attr: `attrName="value"`
|
|
288
|
+
// Walk backwards from start to find the beginning of the attribute name.
|
|
289
|
+
// Source just before start looks like: `... attrName="`
|
|
290
|
+
let attrEnd = end;
|
|
291
|
+
let pos = start - 1; // skip the opening quote's predecessor: should be '='
|
|
292
|
+
if (pos >= 0 && source[pos] === '=') pos--;
|
|
293
|
+
// Walk backwards past the attribute name characters (word chars, '-', ':')
|
|
294
|
+
while (pos >= 0 && /[\w:_-]/.test(source[pos])) pos--;
|
|
295
|
+
// pos now points to the char before the attr name (usually a space/newline)
|
|
296
|
+
const attrStart = pos + 1;
|
|
297
|
+
|
|
298
|
+
// If the attribute is on its own line (only whitespace before it on that
|
|
299
|
+
// line), remove the entire line to avoid leaving a blank line.
|
|
300
|
+
let lineStart = pos;
|
|
301
|
+
while (lineStart > 0 && source[lineStart - 1] !== '\n') lineStart--;
|
|
302
|
+
const beforeOnLine = source.slice(lineStart, attrStart);
|
|
303
|
+
const isAloneOnLine = /^\s*$/.test(beforeOnLine);
|
|
304
|
+
// Also check nothing meaningful follows on the same line
|
|
305
|
+
let afterEnd = attrEnd;
|
|
306
|
+
while (afterEnd < source.length && source[afterEnd] !== '\n' && source[afterEnd] !== '\r') afterEnd++;
|
|
307
|
+
const afterOnLine = source.slice(attrEnd, afterEnd);
|
|
308
|
+
const nothingAfter = /^\s*$/.test(afterOnLine);
|
|
309
|
+
|
|
310
|
+
if (isAloneOnLine && nothingAfter) {
|
|
311
|
+
// Remove from the start of this line (after previous \n) through the trailing \n
|
|
312
|
+
const lineEnd = afterEnd < source.length ? afterEnd + 1 : afterEnd;
|
|
313
|
+
source = source.slice(0, lineStart) + source.slice(lineEnd);
|
|
314
|
+
} else {
|
|
315
|
+
// Attribute is inline — just remove attr + one leading whitespace
|
|
316
|
+
const removeFrom = pos >= 0 && /[\s]/.test(source[pos]) ? pos : attrStart;
|
|
317
|
+
source = source.slice(0, removeFrom) + source.slice(attrEnd);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
patchedCount++;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (patchedCount === 0) return 0;
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
fs.writeFileSync(filePath, source, { encoding: 'utf-8' });
|
|
328
|
+
} catch (err) {
|
|
329
|
+
log.error(`[nozhcn] Cannot write fixed file: ${filePath} — ${err.message}`);
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return patchedCount;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Fix JSX SVG metadata text findings (type 'jsx-svg-metadata').
|
|
338
|
+
*
|
|
339
|
+
* Strategy:
|
|
340
|
+
* - 'empty' : clear only metadata text content (<title>中文</title> -> <title></title>)
|
|
341
|
+
* - 'remove' : remove the whole metadata element (<title>中文</title> -> '')
|
|
342
|
+
*
|
|
343
|
+
* @param {string} filePath
|
|
344
|
+
* @param {Finding[]} findings
|
|
345
|
+
* @param {'remove'|'empty'} strategy
|
|
346
|
+
* @returns {number}
|
|
347
|
+
*/
|
|
348
|
+
function fixJsxSvgMetadataInFile(filePath, findings, strategy = 'remove') {
|
|
349
|
+
if (!strategy) return 0;
|
|
350
|
+
if (!['remove', 'empty'].includes(strategy)) {
|
|
351
|
+
log.warnToLog(`[nozhcn] Invalid JSX SVG metadata strategy: ${strategy}, skip auto-fix.`);
|
|
352
|
+
return 0;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const metadataFindings = findings.filter(
|
|
356
|
+
(f) => f.type === 'jsx-svg-metadata' && typeof f.start === 'number' && typeof f.end === 'number'
|
|
357
|
+
);
|
|
358
|
+
if (!metadataFindings.length) return 0;
|
|
359
|
+
|
|
360
|
+
let source;
|
|
361
|
+
try {
|
|
362
|
+
source = fs.readFileSync(filePath, 'utf8');
|
|
363
|
+
} catch (err) {
|
|
364
|
+
log.error(`[nozhcn] Cannot read file for fixing: ${filePath} — ${err.message}`);
|
|
365
|
+
return 0;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
let patchedCount = 0;
|
|
369
|
+
|
|
370
|
+
if (strategy === 'empty') {
|
|
371
|
+
// Clear text content only, keep metadata tags.
|
|
372
|
+
const sorted = [...metadataFindings].sort((a, b) => b.start - a.start);
|
|
373
|
+
sorted.forEach((f) => {
|
|
374
|
+
source = source.slice(0, f.start) + source.slice(f.end);
|
|
375
|
+
patchedCount++;
|
|
376
|
+
});
|
|
377
|
+
} else {
|
|
378
|
+
// 'remove': remove whole metadata elements; dedupe by element range.
|
|
379
|
+
const ranges = [];
|
|
380
|
+
const seen = new Set();
|
|
381
|
+
metadataFindings.forEach((f) => {
|
|
382
|
+
if (typeof f.metaElementStart !== 'number' || typeof f.metaElementEnd !== 'number') return;
|
|
383
|
+
const key = `${f.metaElementStart}:${f.metaElementEnd}`;
|
|
384
|
+
if (seen.has(key)) return;
|
|
385
|
+
seen.add(key);
|
|
386
|
+
ranges.push({ start: f.metaElementStart, end: f.metaElementEnd });
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
if (!ranges.length) return 0;
|
|
390
|
+
|
|
391
|
+
ranges.sort((a, b) => b.start - a.start);
|
|
392
|
+
ranges.forEach(({ start, end }) => {
|
|
393
|
+
// If the metadata element occupies a whole line, remove that full line.
|
|
394
|
+
let lineStart = start;
|
|
395
|
+
while (lineStart > 0 && source[lineStart - 1] !== '\n') lineStart--;
|
|
396
|
+
const before = source.slice(lineStart, start);
|
|
397
|
+
const onlyWhitespaceBefore = /^\s*$/.test(before);
|
|
398
|
+
|
|
399
|
+
let lineEnd = end;
|
|
400
|
+
while (lineEnd < source.length && source[lineEnd] !== '\n') lineEnd++;
|
|
401
|
+
const after = source.slice(end, lineEnd);
|
|
402
|
+
const onlyWhitespaceAfter = /^\s*$/.test(after);
|
|
403
|
+
|
|
404
|
+
if (onlyWhitespaceBefore && onlyWhitespaceAfter) {
|
|
405
|
+
const removeEnd = lineEnd < source.length ? lineEnd + 1 : lineEnd;
|
|
406
|
+
source = source.slice(0, lineStart) + source.slice(removeEnd);
|
|
407
|
+
} else {
|
|
408
|
+
source = source.slice(0, start) + source.slice(end);
|
|
409
|
+
}
|
|
410
|
+
patchedCount++;
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (patchedCount === 0) return 0;
|
|
415
|
+
|
|
416
|
+
try {
|
|
417
|
+
fs.writeFileSync(filePath, source, { encoding: 'utf-8' });
|
|
418
|
+
} catch (err) {
|
|
419
|
+
log.error(`[nozhcn] Cannot write fixed file: ${filePath} — ${err.message}`);
|
|
420
|
+
return 0;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return patchedCount;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Remove console statement findings (type 'console') from a source file.
|
|
428
|
+
*
|
|
429
|
+
* We only auto-fix findings that carry ExpressionStatement ranges
|
|
430
|
+
* (`stmtStart` / `stmtEnd`). This keeps removal safe and predictable.
|
|
431
|
+
*
|
|
432
|
+
* @param {string} filePath
|
|
433
|
+
* @param {Finding[]} findings
|
|
434
|
+
* @param {'remove'|false} strategy
|
|
435
|
+
* @returns {number}
|
|
436
|
+
*/
|
|
437
|
+
function fixConsoleZhCnInFile(filePath, findings, strategy = 'remove') {
|
|
438
|
+
if (!strategy) return 0;
|
|
439
|
+
if (strategy !== 'remove') {
|
|
440
|
+
log.warnToLog(`[nozhcn] Invalid console strategy: ${strategy}, skip auto-fix.`);
|
|
441
|
+
return 0;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const consoleFindings = findings.filter(
|
|
445
|
+
(f) =>
|
|
446
|
+
f.type === 'console' &&
|
|
447
|
+
typeof f.stmtStart === 'number' &&
|
|
448
|
+
typeof f.stmtEnd === 'number'
|
|
449
|
+
);
|
|
450
|
+
if (!consoleFindings.length) return 0;
|
|
451
|
+
|
|
452
|
+
let source;
|
|
453
|
+
try {
|
|
454
|
+
source = fs.readFileSync(filePath, 'utf8');
|
|
455
|
+
} catch (err) {
|
|
456
|
+
log.error(`[nozhcn] Cannot read file for fixing: ${filePath} — ${err.message}`);
|
|
457
|
+
return 0;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Deduplicate statement ranges first (a single console stmt can produce multiple findings).
|
|
461
|
+
const rangeMap = new Map();
|
|
462
|
+
consoleFindings.forEach((f) => {
|
|
463
|
+
const key = `${f.stmtStart}:${f.stmtEnd}`;
|
|
464
|
+
if (!rangeMap.has(key)) {
|
|
465
|
+
rangeMap.set(key, { start: f.stmtStart, end: f.stmtEnd });
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const ranges = Array.from(rangeMap.values()).sort((a, b) => b.start - a.start);
|
|
470
|
+
let patchedCount = 0;
|
|
471
|
+
|
|
472
|
+
ranges.forEach(({ start, end }) => {
|
|
473
|
+
let lineStart = start;
|
|
474
|
+
while (lineStart > 0 && source[lineStart - 1] !== '\n') lineStart--;
|
|
475
|
+
const before = source.slice(lineStart, start);
|
|
476
|
+
const onlyWhitespaceBefore = /^\s*$/.test(before);
|
|
477
|
+
|
|
478
|
+
let lineEnd = end;
|
|
479
|
+
while (lineEnd < source.length && source[lineEnd] !== '\n') lineEnd++;
|
|
480
|
+
const after = source.slice(end, lineEnd);
|
|
481
|
+
const onlyWhitespaceAfter = /^\s*$/.test(after);
|
|
482
|
+
|
|
483
|
+
if (onlyWhitespaceBefore && onlyWhitespaceAfter) {
|
|
484
|
+
const removeEnd = lineEnd < source.length ? lineEnd + 1 : lineEnd;
|
|
485
|
+
source = source.slice(0, lineStart) + source.slice(removeEnd);
|
|
486
|
+
} else {
|
|
487
|
+
source = source.slice(0, start) + source.slice(end);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
patchedCount++;
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
if (patchedCount === 0) return 0;
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
fs.writeFileSync(filePath, source, { encoding: 'utf-8' });
|
|
497
|
+
} catch (err) {
|
|
498
|
+
log.error(`[nozhcn] Cannot write fixed file: ${filePath} — ${err.message}`);
|
|
499
|
+
return 0;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return patchedCount;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
module.exports.fixJsxSvgAttrInFile = fixJsxSvgAttrInFile;
|
|
506
|
+
module.exports.fixJsxSvgMetadataInFile = fixJsxSvgMetadataInFile;
|
|
507
|
+
module.exports.fixConsoleZhCnInFile = fixConsoleZhCnInFile;
|
|
@@ -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 &&
|