@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
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const ROOT = process.cwd();
|
|
5
|
+
const DEFAULT_CONFIG_FILE = '.nozhcn-guard.json';
|
|
6
|
+
const CJK_REGEXP = /[\u3400-\u9FFF]/g;
|
|
7
|
+
|
|
8
|
+
const DEFAULT_CONFIG = {
|
|
9
|
+
include: ['.'],
|
|
10
|
+
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.svg', '.html', '.yml', '.yaml'],
|
|
11
|
+
scanAllFiles: true,
|
|
12
|
+
skipBinaryFiles: true,
|
|
13
|
+
exclude: [
|
|
14
|
+
'**/.git/**',
|
|
15
|
+
'**/node_modules/**',
|
|
16
|
+
'**/portalWeb/**',
|
|
17
|
+
'**/publish/**',
|
|
18
|
+
'**/helpdesk-dashboard/**',
|
|
19
|
+
'**/.kiro/**',
|
|
20
|
+
'**/memories/**',
|
|
21
|
+
],
|
|
22
|
+
allowChinesePaths: [
|
|
23
|
+
'docs/**',
|
|
24
|
+
'locales/**',
|
|
25
|
+
'src/locales/**',
|
|
26
|
+
'portalWeb.json',
|
|
27
|
+
'NOZHCN.md',
|
|
28
|
+
],
|
|
29
|
+
ignoreI18nDefault: false,
|
|
30
|
+
maxPrintPerFile: 20,
|
|
31
|
+
reportFile: 'nozhcn-guard-report.json',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function normalizePath(filePath) {
|
|
35
|
+
return filePath.split(path.sep).join('/').replace(/^\.\//, '');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function hasGlob(pattern) {
|
|
39
|
+
return /[*?]/.test(pattern);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function escapeRegExp(text) {
|
|
43
|
+
return text.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function globToRegExp(glob) {
|
|
47
|
+
const normalized = normalizePath(glob);
|
|
48
|
+
if (normalized.startsWith('**/')) {
|
|
49
|
+
const rest = normalized.slice(3);
|
|
50
|
+
const restRegexp = globToRegExp(rest).source.replace(/^\^/, '').replace(/\$$/, '');
|
|
51
|
+
return new RegExp(`^(?:.*/)?${restRegexp}$`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let source = '';
|
|
55
|
+
for (let i = 0; i < normalized.length; i += 1) {
|
|
56
|
+
const ch = normalized[i];
|
|
57
|
+
const next = normalized[i + 1];
|
|
58
|
+
if (ch === '*') {
|
|
59
|
+
if (next === '*') {
|
|
60
|
+
source += '.*';
|
|
61
|
+
i += 1;
|
|
62
|
+
} else {
|
|
63
|
+
source += '[^/]*';
|
|
64
|
+
}
|
|
65
|
+
} else if (ch === '?') {
|
|
66
|
+
source += '[^/]';
|
|
67
|
+
} else {
|
|
68
|
+
source += escapeRegExp(ch);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return new RegExp(`^${source}$`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function loadConfig(configFile) {
|
|
76
|
+
const configPath = path.resolve(ROOT, configFile || DEFAULT_CONFIG_FILE);
|
|
77
|
+
if (!fs.existsSync(configPath)) {
|
|
78
|
+
return { configPath, config: { ...DEFAULT_CONFIG } };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
82
|
+
const userConfig = JSON.parse(raw);
|
|
83
|
+
const config = {
|
|
84
|
+
...DEFAULT_CONFIG,
|
|
85
|
+
...userConfig,
|
|
86
|
+
include: Array.isArray(userConfig.include) ? userConfig.include : DEFAULT_CONFIG.include,
|
|
87
|
+
extensions: Array.isArray(userConfig.extensions) ? userConfig.extensions : DEFAULT_CONFIG.extensions,
|
|
88
|
+
exclude: Array.isArray(userConfig.exclude) ? userConfig.exclude : DEFAULT_CONFIG.exclude,
|
|
89
|
+
allowChinesePaths: Array.isArray(userConfig.allowChinesePaths)
|
|
90
|
+
? userConfig.allowChinesePaths
|
|
91
|
+
: DEFAULT_CONFIG.allowChinesePaths,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return { configPath, config };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildMatchers(patterns) {
|
|
98
|
+
const regexps = [];
|
|
99
|
+
const literals = new Set();
|
|
100
|
+
|
|
101
|
+
for (const pattern of patterns) {
|
|
102
|
+
if (hasGlob(pattern)) {
|
|
103
|
+
regexps.push(globToRegExp(pattern));
|
|
104
|
+
} else {
|
|
105
|
+
literals.add(normalizePath(pattern));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { regexps, literals };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function matchPath(relativePath, matchers) {
|
|
113
|
+
const normalized = normalizePath(relativePath);
|
|
114
|
+
if (matchers.literals.has(normalized)) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const regexp of matchers.regexps) {
|
|
119
|
+
if (regexp.test(normalized)) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function shouldScanFile(relativePath, config, excludeMatchers, allowMatchers) {
|
|
128
|
+
if (matchPath(relativePath, excludeMatchers)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
if (matchPath(relativePath, allowMatchers)) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
if (config.scanAllFiles) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const ext = path.extname(relativePath).toLowerCase();
|
|
139
|
+
return config.extensions.includes(ext);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isLikelyBinaryBuffer(buffer) {
|
|
143
|
+
if (!buffer || buffer.length === 0) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
(buffer.length >= 3 && buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) ||
|
|
149
|
+
(buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) ||
|
|
150
|
+
(buffer.length >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff)
|
|
151
|
+
) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const sampleLength = Math.min(buffer.length, 8000);
|
|
156
|
+
let suspicious = 0;
|
|
157
|
+
|
|
158
|
+
for (let i = 0; i < sampleLength; i += 1) {
|
|
159
|
+
const byte = buffer[i];
|
|
160
|
+
if (byte === 0) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const isTextControl = byte === 9 || byte === 10 || byte === 13;
|
|
165
|
+
const isPrintableAscii = byte >= 32 && byte <= 126;
|
|
166
|
+
const isHighByte = byte >= 128;
|
|
167
|
+
if (!isTextControl && !isPrintableAscii && !isHighByte) {
|
|
168
|
+
suspicious += 1;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return suspicious / sampleLength > 0.3;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function decodeUtf16LE(buffer, offset) {
|
|
176
|
+
const chars = [];
|
|
177
|
+
for (let i = offset || 0; i + 1 < buffer.length; i += 2) {
|
|
178
|
+
chars.push(String.fromCharCode(buffer[i] | (buffer[i + 1] << 8)));
|
|
179
|
+
}
|
|
180
|
+
return chars.join('');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function decodeUtf16BE(buffer, offset) {
|
|
184
|
+
const chars = [];
|
|
185
|
+
for (let i = offset || 0; i + 1 < buffer.length; i += 2) {
|
|
186
|
+
chars.push(String.fromCharCode((buffer[i] << 8) | buffer[i + 1]));
|
|
187
|
+
}
|
|
188
|
+
return chars.join('');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function looksLikeUtf16WithoutBom(buffer) {
|
|
192
|
+
const sampleLength = Math.min(buffer.length, 8000);
|
|
193
|
+
if (sampleLength < 4) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let evenZero = 0;
|
|
198
|
+
let oddZero = 0;
|
|
199
|
+
let pairs = 0;
|
|
200
|
+
for (let i = 0; i + 1 < sampleLength; i += 2) {
|
|
201
|
+
if (buffer[i] === 0) {
|
|
202
|
+
evenZero += 1;
|
|
203
|
+
}
|
|
204
|
+
if (buffer[i + 1] === 0) {
|
|
205
|
+
oddZero += 1;
|
|
206
|
+
}
|
|
207
|
+
pairs += 1;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (pairs === 0) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const evenRatio = evenZero / pairs;
|
|
215
|
+
const oddRatio = oddZero / pairs;
|
|
216
|
+
|
|
217
|
+
if (oddRatio > 0.35 && oddRatio > evenRatio * 1.5) {
|
|
218
|
+
return 'utf16le';
|
|
219
|
+
}
|
|
220
|
+
if (evenRatio > 0.35 && evenRatio > oddRatio * 1.5) {
|
|
221
|
+
return 'utf16be';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function decodeTextBuffer(buffer) {
|
|
228
|
+
if (!buffer || buffer.length === 0) {
|
|
229
|
+
return { text: '', isBinary: false };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (buffer.length >= 3 && buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) {
|
|
233
|
+
return { text: buffer.toString('utf8', 3), isBinary: false };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) {
|
|
237
|
+
return { text: decodeUtf16LE(buffer, 2), isBinary: false };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (buffer.length >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff) {
|
|
241
|
+
return { text: decodeUtf16BE(buffer, 2), isBinary: false };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const inferredUtf16 = looksLikeUtf16WithoutBom(buffer);
|
|
245
|
+
if (inferredUtf16 === 'utf16le') {
|
|
246
|
+
return { text: decodeUtf16LE(buffer, 0), isBinary: false };
|
|
247
|
+
}
|
|
248
|
+
if (inferredUtf16 === 'utf16be') {
|
|
249
|
+
return { text: decodeUtf16BE(buffer, 0), isBinary: false };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (isLikelyBinaryBuffer(buffer)) {
|
|
253
|
+
return { text: '', isBinary: true };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { text: buffer.toString('utf8'), isBinary: false };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function eraseRangeWithSpaces(input, start, end) {
|
|
260
|
+
let output = '';
|
|
261
|
+
for (let i = start; i < end; i += 1) {
|
|
262
|
+
output += input[i] === '\n' ? '\n' : ' ';
|
|
263
|
+
}
|
|
264
|
+
return output;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function blankI18nDefaultText(content) {
|
|
268
|
+
const dCallRegexp = /\.d\(\s*(?:'[^'\\]*(?:\\.[^'\\]*)*'|"[^"\\]*(?:\\.[^"\\]*)*"|`[\s\S]*?`)\s*\)/g;
|
|
269
|
+
let transformed = '';
|
|
270
|
+
let cursor = 0;
|
|
271
|
+
let match = dCallRegexp.exec(content);
|
|
272
|
+
|
|
273
|
+
while (match) {
|
|
274
|
+
const start = match.index;
|
|
275
|
+
const end = start + match[0].length;
|
|
276
|
+
transformed += content.slice(cursor, start);
|
|
277
|
+
transformed += eraseRangeWithSpaces(content, start, end);
|
|
278
|
+
cursor = end;
|
|
279
|
+
match = dCallRegexp.exec(content);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
transformed += content.slice(cursor);
|
|
283
|
+
return transformed;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function collectChineseLines(content, maxPrintPerFile) {
|
|
287
|
+
const lines = content.split('\n');
|
|
288
|
+
const hits = [];
|
|
289
|
+
|
|
290
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
291
|
+
const line = lines[i];
|
|
292
|
+
const match = line.match(CJK_REGEXP);
|
|
293
|
+
if (!match) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
hits.push({
|
|
298
|
+
line: i + 1,
|
|
299
|
+
sample: line.trim().slice(0, 200),
|
|
300
|
+
hitCountInLine: match.length,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
if (hits.length >= maxPrintPerFile) {
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return hits;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function walkTarget(absTargetPath, files, excludeMatchers) {
|
|
312
|
+
if (!fs.existsSync(absTargetPath)) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const relativePath = normalizePath(path.relative(ROOT, absTargetPath));
|
|
317
|
+
if (relativePath && !relativePath.startsWith('..') && matchPath(relativePath, excludeMatchers)) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const stat = fs.statSync(absTargetPath);
|
|
322
|
+
if (stat.isFile()) {
|
|
323
|
+
files.push(absTargetPath);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const entries = fs.readdirSync(absTargetPath);
|
|
328
|
+
for (const entry of entries) {
|
|
329
|
+
walkTarget(path.join(absTargetPath, entry), files, excludeMatchers);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function parseArgs(cliArgs) {
|
|
334
|
+
const args = cliArgs || process.argv.slice(2);
|
|
335
|
+
const getArgValue = (name) => {
|
|
336
|
+
const index = args.indexOf(name);
|
|
337
|
+
if (index === -1 || index === args.length - 1) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
return args[index + 1];
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
reportOnly: args.includes('--report'),
|
|
345
|
+
silent: args.includes('--silent'),
|
|
346
|
+
configFile: getArgValue('--config') || DEFAULT_CONFIG_FILE,
|
|
347
|
+
reportFile: getArgValue('--report-file'),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function runNoZhCnGuard(cliArgs) {
|
|
352
|
+
const options = parseArgs(cliArgs);
|
|
353
|
+
const loaded = loadConfig(options.configFile);
|
|
354
|
+
const config = loaded.config;
|
|
355
|
+
const excludeMatchers = buildMatchers(config.exclude);
|
|
356
|
+
const allowMatchers = buildMatchers(config.allowChinesePaths);
|
|
357
|
+
|
|
358
|
+
const allFiles = [];
|
|
359
|
+
for (const target of config.include) {
|
|
360
|
+
walkTarget(path.resolve(ROOT, target), allFiles, excludeMatchers);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const reports = [];
|
|
364
|
+
let scannedFiles = 0;
|
|
365
|
+
let totalHitLines = 0;
|
|
366
|
+
let skippedBinaryFiles = 0;
|
|
367
|
+
|
|
368
|
+
for (const absFilePath of allFiles) {
|
|
369
|
+
const relativePath = normalizePath(path.relative(ROOT, absFilePath));
|
|
370
|
+
if (!relativePath || relativePath.startsWith('..')) {
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (!shouldScanFile(relativePath, config, excludeMatchers, allowMatchers)) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const rawBuffer = fs.readFileSync(absFilePath);
|
|
378
|
+
const decoded = decodeTextBuffer(rawBuffer);
|
|
379
|
+
if (config.skipBinaryFiles && decoded.isBinary) {
|
|
380
|
+
skippedBinaryFiles += 1;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const scanSource = config.ignoreI18nDefault
|
|
385
|
+
? blankI18nDefaultText(decoded.text)
|
|
386
|
+
: decoded.text;
|
|
387
|
+
const hitLines = collectChineseLines(scanSource, config.maxPrintPerFile);
|
|
388
|
+
|
|
389
|
+
scannedFiles += 1;
|
|
390
|
+
if (hitLines.length === 0) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const fullHitLineCount = (scanSource.match(/[^\n]*[\u3400-\u9FFF][^\n]*/g) || []).length;
|
|
395
|
+
totalHitLines += fullHitLineCount;
|
|
396
|
+
|
|
397
|
+
reports.push({
|
|
398
|
+
file: relativePath,
|
|
399
|
+
hitLineCount: fullHitLineCount,
|
|
400
|
+
examples: hitLines,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
reports.sort((a, b) => b.hitLineCount - a.hitLineCount);
|
|
405
|
+
|
|
406
|
+
const result = {
|
|
407
|
+
generatedAt: new Date().toISOString(),
|
|
408
|
+
scannedFiles,
|
|
409
|
+
problemFiles: reports.length,
|
|
410
|
+
totalHitLines,
|
|
411
|
+
skippedBinaryFiles,
|
|
412
|
+
ignoreI18nDefault: Boolean(config.ignoreI18nDefault),
|
|
413
|
+
reports,
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const reportPath = path.resolve(ROOT, options.reportFile || config.reportFile);
|
|
417
|
+
fs.writeFileSync(reportPath, `${JSON.stringify(result, null, 2)}\n`, 'utf8');
|
|
418
|
+
|
|
419
|
+
if (!options.silent) {
|
|
420
|
+
console.log(`[guard] config: ${normalizePath(path.relative(ROOT, loaded.configPath))}`);
|
|
421
|
+
console.log(`[guard] scanned files: ${scannedFiles}`);
|
|
422
|
+
console.log(`[guard] files with chinese: ${reports.length}`);
|
|
423
|
+
console.log(`[guard] chinese hit lines: ${totalHitLines}`);
|
|
424
|
+
if (config.skipBinaryFiles) {
|
|
425
|
+
console.log(`[guard] skipped binary files: ${skippedBinaryFiles}`);
|
|
426
|
+
}
|
|
427
|
+
console.log(`[guard] report: ${normalizePath(path.relative(ROOT, reportPath))}`);
|
|
428
|
+
|
|
429
|
+
if (reports.length > 0) {
|
|
430
|
+
console.log('\n[guard] top files:');
|
|
431
|
+
for (const item of reports.slice(0, 20)) {
|
|
432
|
+
console.log(` - ${item.file} (${item.hitLineCount} line hits)`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (options.reportOnly) {
|
|
438
|
+
process.exit(0);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
process.exit(reports.length > 0 ? 1 : 0);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
module.exports = runNoZhCnGuard;
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* - jsx : JSX text content (outside ignored components)
|
|
9
9
|
* - jsx-svg : JSX text content INSIDE an ignored component (e.g. <svg>)
|
|
10
10
|
* NOT auto-fixable via intl.get() — needs manual SVG handling
|
|
11
|
+
* - jsx-svg-metadata : JSX text content inside <svg><title|desc|metadata>
|
|
12
|
+
* auto-fixable by nozhcn fix metadata strategy
|
|
13
|
+
* - console : console.* call text containing Chinese (can be auto-removed)
|
|
11
14
|
* - identifier : identifier names (rare, manual fix needed)
|
|
12
15
|
*
|
|
13
16
|
* Returns an array of Finding objects:
|
|
@@ -152,6 +155,17 @@ function collectCommentTokens(ast) {
|
|
|
152
155
|
return comments;
|
|
153
156
|
}
|
|
154
157
|
|
|
158
|
+
function isConsoleCall(nodePath) {
|
|
159
|
+
const callee = nodePath.node && nodePath.node.callee;
|
|
160
|
+
if (!callee || callee.type !== 'MemberExpression') return false;
|
|
161
|
+
if (callee.computed) return false;
|
|
162
|
+
return callee.object && callee.object.type === 'Identifier' && callee.object.name === 'console';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isInsideConsoleCall(nodePath) {
|
|
166
|
+
return !!nodePath.findParent((p) => p.isCallExpression() && isConsoleCall(p));
|
|
167
|
+
}
|
|
168
|
+
|
|
155
169
|
/**
|
|
156
170
|
* Main export: scan a single file and return all Chinese findings.
|
|
157
171
|
*
|
|
@@ -265,16 +279,65 @@ module.exports = function findZhCnInFile(filePath, opts) {
|
|
|
265
279
|
checkTypes.includes('template') ||
|
|
266
280
|
checkTypes.includes('jsx') ||
|
|
267
281
|
checkTypes.includes('jsx-svg') ||
|
|
282
|
+
checkTypes.includes('jsx-svg-metadata') ||
|
|
283
|
+
checkTypes.includes('console') ||
|
|
268
284
|
checkTypes.includes('identifier');
|
|
269
285
|
|
|
270
286
|
if (needsAstScan) {
|
|
271
287
|
traverse(ast, {
|
|
272
288
|
// ── String literals ─────────────────────────────────────────────
|
|
273
289
|
StringLiteral(nodePath) {
|
|
274
|
-
if (!checkTypes.includes('string')) return;
|
|
275
290
|
const node = nodePath.node;
|
|
276
291
|
if (!primaryRegx.test(node.value)) return;
|
|
277
292
|
if (isIgnoredLine(node.loc && node.loc.start.line)) return;
|
|
293
|
+
if (checkTypes.includes('console') && isInsideConsoleCall(nodePath)) return;
|
|
294
|
+
|
|
295
|
+
// ── JSX SVG attribute detection ──────────────────────────────
|
|
296
|
+
// If this string is a JSX attribute value inside an SVG element
|
|
297
|
+
// (or other ignored component), tag it as 'jsx-svg-attr' so it
|
|
298
|
+
// can be auto-fixed via the svg attr strategy rather than being
|
|
299
|
+
// treated as a regular string literal requiring internationalisation.
|
|
300
|
+
if (nodePath.parent && nodePath.parent.type === 'JSXAttribute') {
|
|
301
|
+
let insideIgnored = false;
|
|
302
|
+
nodePath.findParent((p) => {
|
|
303
|
+
if (!p.isJSXElement()) return false;
|
|
304
|
+
const openingName = p.node.openingElement && p.node.openingElement.name;
|
|
305
|
+
if (!openingName) return false;
|
|
306
|
+
const componentName =
|
|
307
|
+
openingName.name ||
|
|
308
|
+
(openingName.namespace && openingName.namespace.name) ||
|
|
309
|
+
'';
|
|
310
|
+
if (ignoreComponents.includes(componentName)) {
|
|
311
|
+
insideIgnored = true;
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
if (insideIgnored) {
|
|
318
|
+
if (checkTypes.includes('string') || checkTypes.includes('jsx-svg-attr')) {
|
|
319
|
+
const attrName =
|
|
320
|
+
nodePath.parent.name && nodePath.parent.name.name
|
|
321
|
+
? nodePath.parent.name.name
|
|
322
|
+
: '';
|
|
323
|
+
findings.push({
|
|
324
|
+
filePath,
|
|
325
|
+
type: 'jsx-svg-attr',
|
|
326
|
+
line: node.loc && node.loc.start.line,
|
|
327
|
+
col: node.loc && node.loc.start.column,
|
|
328
|
+
content: node.value,
|
|
329
|
+
attrName,
|
|
330
|
+
// Byte offsets (includes quote chars) — used by fixJsxSvgAttrInFile
|
|
331
|
+
start: node.start,
|
|
332
|
+
end: node.end,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return; // do NOT also report as 'string'
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ── Normal string literal ─────────────────────────────────────
|
|
340
|
+
if (!checkTypes.includes('string')) return;
|
|
278
341
|
if (
|
|
279
342
|
ignoreI18nDefault &&
|
|
280
343
|
isI18nDefaultValue(nodePath, i18nMethod, i18nDefaultFunctionKey, i18nObject)
|
|
@@ -300,6 +363,7 @@ module.exports = function findZhCnInFile(filePath, opts) {
|
|
|
300
363
|
) {
|
|
301
364
|
return;
|
|
302
365
|
}
|
|
366
|
+
if (checkTypes.includes('console') && isInsideConsoleCall(nodePath)) return;
|
|
303
367
|
nodePath.node.quasis.forEach((quasi) => {
|
|
304
368
|
if (!primaryRegx.test(quasi.value.raw)) return;
|
|
305
369
|
if (isIgnoredLine(quasi.loc && quasi.loc.start.line)) return;
|
|
@@ -313,9 +377,37 @@ module.exports = function findZhCnInFile(filePath, opts) {
|
|
|
313
377
|
});
|
|
314
378
|
},
|
|
315
379
|
|
|
380
|
+
CallExpression(nodePath) {
|
|
381
|
+
if (!checkTypes.includes('console')) return;
|
|
382
|
+
if (!isConsoleCall(nodePath)) return;
|
|
383
|
+
const node = nodePath.node;
|
|
384
|
+
const line = node.loc && node.loc.start.line;
|
|
385
|
+
if (isIgnoredLine(line)) return;
|
|
386
|
+
const rawCall = source.slice(node.start, node.end);
|
|
387
|
+
if (!primaryRegx.test(rawCall)) return;
|
|
388
|
+
|
|
389
|
+
const stmt = nodePath.parentPath && nodePath.parentPath.isExpressionStatement()
|
|
390
|
+
? nodePath.parentPath.node
|
|
391
|
+
: null;
|
|
392
|
+
|
|
393
|
+
findings.push({
|
|
394
|
+
filePath,
|
|
395
|
+
type: 'console',
|
|
396
|
+
line: (stmt && stmt.loc && stmt.loc.start.line) || line,
|
|
397
|
+
col: (stmt && stmt.loc && stmt.loc.start.column) || (node.loc && node.loc.start.column),
|
|
398
|
+
content: rawCall,
|
|
399
|
+
stmtStart: stmt && typeof stmt.start === 'number' ? stmt.start : null,
|
|
400
|
+
stmtEnd: stmt && typeof stmt.end === 'number' ? stmt.end : null,
|
|
401
|
+
});
|
|
402
|
+
},
|
|
403
|
+
|
|
316
404
|
// ── JSX text ────────────────────────────────────────────────────
|
|
317
405
|
JSXText(nodePath) {
|
|
318
|
-
|
|
406
|
+
const wantsAnyJsx =
|
|
407
|
+
checkTypes.includes('jsx') ||
|
|
408
|
+
checkTypes.includes('jsx-svg') ||
|
|
409
|
+
checkTypes.includes('jsx-svg-metadata');
|
|
410
|
+
if (!wantsAnyJsx) return;
|
|
319
411
|
const node = nodePath.node;
|
|
320
412
|
const text = node.value.trim();
|
|
321
413
|
if (!text || !primaryRegx.test(text)) return;
|
|
@@ -325,6 +417,8 @@ module.exports = function findZhCnInFile(filePath, opts) {
|
|
|
325
417
|
// component (e.g. <svg>, <style>). If so, tag as 'jsx-svg' so
|
|
326
418
|
// the caller knows this cannot be fixed via intl.get().
|
|
327
419
|
let insideIgnored = false;
|
|
420
|
+
let insideSvgMetadata = false;
|
|
421
|
+
let metadataElement = null;
|
|
328
422
|
nodePath.findParent((p) => {
|
|
329
423
|
if (!p.isJSXElement()) return false;
|
|
330
424
|
const openingName = p.node.openingElement && p.node.openingElement.name;
|
|
@@ -334,6 +428,12 @@ module.exports = function findZhCnInFile(filePath, opts) {
|
|
|
334
428
|
openingName.name ||
|
|
335
429
|
(openingName.namespace && openingName.namespace.name) ||
|
|
336
430
|
'';
|
|
431
|
+
|
|
432
|
+
if (!insideSvgMetadata && ['title', 'desc', 'metadata'].includes(componentName)) {
|
|
433
|
+
insideSvgMetadata = true;
|
|
434
|
+
metadataElement = p.node;
|
|
435
|
+
}
|
|
436
|
+
|
|
337
437
|
if (ignoreComponents.includes(componentName)) {
|
|
338
438
|
insideIgnored = true;
|
|
339
439
|
return true; // stop traversal
|
|
@@ -341,16 +441,37 @@ module.exports = function findZhCnInFile(filePath, opts) {
|
|
|
341
441
|
return false;
|
|
342
442
|
});
|
|
343
443
|
|
|
344
|
-
|
|
345
|
-
if (
|
|
444
|
+
let type = 'jsx';
|
|
445
|
+
if (insideIgnored && insideSvgMetadata) {
|
|
446
|
+
type = 'jsx-svg-metadata';
|
|
447
|
+
if (!(checkTypes.includes('jsx') || checkTypes.includes('jsx-svg') || checkTypes.includes('jsx-svg-metadata'))) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
} else if (insideIgnored) {
|
|
451
|
+
type = 'jsx-svg';
|
|
452
|
+
if (!checkTypes.includes('jsx-svg')) return;
|
|
453
|
+
} else {
|
|
454
|
+
if (!checkTypes.includes('jsx')) return;
|
|
455
|
+
}
|
|
346
456
|
|
|
347
|
-
|
|
457
|
+
const finding = {
|
|
348
458
|
filePath,
|
|
349
459
|
type,
|
|
350
460
|
line: node.loc && node.loc.start.line,
|
|
351
461
|
col: node.loc && node.loc.start.column,
|
|
352
462
|
content: text,
|
|
353
|
-
}
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
if (type === 'jsx-svg-metadata') {
|
|
466
|
+
finding.start = node.start;
|
|
467
|
+
finding.end = node.end;
|
|
468
|
+
if (metadataElement) {
|
|
469
|
+
finding.metaElementStart = metadataElement.start;
|
|
470
|
+
finding.metaElementEnd = metadataElement.end;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
findings.push(finding);
|
|
354
475
|
},
|
|
355
476
|
|
|
356
477
|
// ── Identifiers (variable / function / class names) ─────────────
|