@chaoswise/intl 2.1.10 → 3.1.0
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 +20 -12
- package/bin/scripts/conf/default.js +49 -1
- package/bin/scripts/nozhcn.js +440 -0
- package/bin/scripts/util/babelOptions.js +49 -0
- package/bin/scripts/util/findZhCnInFile.js +377 -0
- package/bin/scripts/util/findZhCnInSvgFile.js +139 -0
- package/bin/scripts/util/fixI18nDefaultInFile.js +179 -0
- package/bin/scripts/util/fixZhCnInFile.js +217 -0
- package/bin/scripts/util/fixZhCnInSvgFile.js +206 -0
- package/bin/scripts/util/makeVisitorCollect.js +14 -6
- package/bin/scripts/util/transformAst.js +38 -20
- package/package.json +4 -2
package/bin/chaoswise-intl.js
CHANGED
|
@@ -3,29 +3,37 @@ const runCollect = require('./scripts/collect');
|
|
|
3
3
|
const runUpdate = require('./scripts/update');
|
|
4
4
|
const runInitConfig = require('./scripts/initConfig');
|
|
5
5
|
const runAddLocale = require('./scripts/addLocale');
|
|
6
|
+
const runNoZhCn = require('./scripts/nozhcn');
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
const SCRIPTS = ['intl', 'collect', 'update'];
|
|
8
|
+
const SCRIPTS = ['intl', 'collect', 'update', 'nozhcn', 'addLocale', 'init'];
|
|
9
9
|
|
|
10
10
|
const args = process.argv.slice(2);
|
|
11
11
|
|
|
12
|
-
const scriptIndex = args.findIndex(arg =>
|
|
13
|
-
const script = SCRIPTS.filter(script => arg === script)[0];
|
|
14
|
-
return arg === script;
|
|
15
|
-
});
|
|
16
|
-
|
|
12
|
+
const scriptIndex = args.findIndex(arg => SCRIPTS.includes(arg));
|
|
17
13
|
const script = scriptIndex === -1 ? args[0] : args[scriptIndex];
|
|
18
14
|
|
|
19
|
-
if(script === 'init') {
|
|
15
|
+
if (script === 'init') {
|
|
20
16
|
runInitConfig();
|
|
21
17
|
}
|
|
22
|
-
if(script === 'collect') {
|
|
18
|
+
if (script === 'collect') {
|
|
23
19
|
runCollect();
|
|
24
20
|
}
|
|
25
|
-
if(script === 'update') {
|
|
21
|
+
if (script === 'update') {
|
|
26
22
|
runUpdate();
|
|
27
23
|
}
|
|
28
|
-
|
|
29
|
-
if(script === 'addLocale') {
|
|
24
|
+
if (script === 'addLocale') {
|
|
30
25
|
runAddLocale();
|
|
31
26
|
}
|
|
27
|
+
// nozhcn: detect (and optionally fix) Chinese characters in source files
|
|
28
|
+
// Usage:
|
|
29
|
+
// chaoswise-intl nozhcn → check mode (CI gate, exit 1 on findings)
|
|
30
|
+
// chaoswise-intl nozhcn check → same as above
|
|
31
|
+
// chaoswise-intl nozhcn fix → auto-fix comments, report remaining issues
|
|
32
|
+
// chaoswise-intl nozhcn report → non-blocking scan, output JSON report
|
|
33
|
+
if (script === 'nozhcn') {
|
|
34
|
+
// The sub-mode is the argument after 'nozhcn', default to 'check'
|
|
35
|
+
const modeArg = args[scriptIndex + 1];
|
|
36
|
+
const validModes = ['check', 'fix', 'report'];
|
|
37
|
+
const mode = validModes.includes(modeArg) ? modeArg : 'check';
|
|
38
|
+
runNoZhCn(mode);
|
|
39
|
+
}
|
|
@@ -94,7 +94,55 @@ module.exports = function (excludes = []) {
|
|
|
94
94
|
|
|
95
95
|
// 国际化平台地址
|
|
96
96
|
baseURL: 'http://10.0.1.133:18000',
|
|
97
|
-
|
|
97
|
+
|
|
98
|
+
// ─────────────────────────────────────────────────────────────────
|
|
99
|
+
// nozhcn (No Chinese) 专属配置
|
|
100
|
+
// ─────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
// 需要检测的中文类型,可按需删减:
|
|
103
|
+
// 'comment' - 注释(可自动修复)
|
|
104
|
+
// 'string' - 字符串字面量
|
|
105
|
+
// 'template' - 模板字符串
|
|
106
|
+
// 'jsx' - JSX 文本内容
|
|
107
|
+
// 'identifier' - 标识符名称(极少出现,需手动修复)
|
|
108
|
+
noZhCnCheckTypes: ['comment', 'string', 'template', 'jsx'],
|
|
109
|
+
|
|
110
|
+
// 注释中文修复策略(fix 模式生效):
|
|
111
|
+
// 'clean' - 仅删除中文字符,保留非中文内容;若注释全为中文则整条删除
|
|
112
|
+
// 'remove' - 含中文的注释整条全部删除
|
|
113
|
+
noZhCnCommentStrategy: 'clean',
|
|
114
|
+
|
|
115
|
+
// 报告文件输出路径(null 表示不输出报告文件,report 模式默认使用此路径)
|
|
116
|
+
noZhCnReportFile: null,
|
|
117
|
+
|
|
118
|
+
// nozhcn 专属文件排除规则(叠加到主 exclude 之上)
|
|
119
|
+
noZhCnExclude: [],
|
|
120
|
+
|
|
121
|
+
// 是否忽略 intl.get('key').d('中文') 中 .d() 内的默认中文值
|
|
122
|
+
// 说明:collect 脚本会将中文字符串替换为 intl.get('key').d('原始中文')
|
|
123
|
+
// .d() 中的中文是有意保留的回退值,开启此选项可跳过这类误报
|
|
124
|
+
noZhCnIgnoreI18nDefault: true,
|
|
125
|
+
|
|
126
|
+
// fix 模式下,将 .d('中文') 替换为指定语言的翻译文本。
|
|
127
|
+
// 值为语言包文件名(不含 .json),对应 localeOutput/locales/{lang}.json。
|
|
128
|
+
// 例如:'en_US' → 读取 src/public/locales/en_US.json 中对应 key 的翻译
|
|
129
|
+
// null 表示不执行此替换(保持现有 .d() 内容不变)
|
|
130
|
+
noZhCnDefaultLang: null,
|
|
131
|
+
// 是否扫描项目中的独立 .svg 文件(默认关闭,按需开启)
|
|
132
|
+
// SVG 文件无法用 Babel AST 解析,采用正则扫描
|
|
133
|
+
noZhCnIncludeSvg: false,
|
|
134
|
+
|
|
135
|
+
// SVG 元数据元素(<title>、<desc>、<metadata>)的修复策略(fix 模式生效):
|
|
136
|
+
// 'remove' - 删除整个元素
|
|
137
|
+
// 'empty' - 保留元素但清空内容:<title>搜索</title> → <title></title>
|
|
138
|
+
noZhCnSvgMetadataStrategy: 'remove',
|
|
139
|
+
|
|
140
|
+
// SVG 属性中文修复策略(fix 模式生效):
|
|
141
|
+
// 'clean' - 删除属性值中的中文字符(及 CJK 标点),清理后为空则移除整个属性;
|
|
142
|
+
// 若修改了 id 属性,同步更新 SVG 内部引用(url(#…), href)
|
|
143
|
+
// 'remove' - 移除包含中文的属性
|
|
144
|
+
// false - 不自动修复属性中的中文
|
|
145
|
+
noZhCnSvgAttrStrategy: 'clean', };
|
|
98
146
|
|
|
99
147
|
excludes.forEach((key) => delete config[key]);
|
|
100
148
|
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nozhcn.js ─ No Chinese Characters
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates scanning and (optionally) fixing Chinese characters in
|
|
5
|
+
* all source files covered by the intl configuration.
|
|
6
|
+
*
|
|
7
|
+
* Modes
|
|
8
|
+
* ─────
|
|
9
|
+
* check (default) Scan every file. Print findings. Exit with code 1 if
|
|
10
|
+
* any Chinese is found — suitable for CI/CD gate use.
|
|
11
|
+
*
|
|
12
|
+
* fix Scan → auto-fix comments + .d() defaults + SVG →
|
|
13
|
+
* single re-scan → print remaining findings.
|
|
14
|
+
* Exit with code 1 if unfixable findings remain.
|
|
15
|
+
*
|
|
16
|
+
* report Like check but writes a structured JSON report file
|
|
17
|
+
* even when findings exist. Does NOT exit with code 1
|
|
18
|
+
* (non-blocking, good for dashboards / tracking).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const glob = require('glob');
|
|
24
|
+
const chalk = require('chalk');
|
|
25
|
+
const getConf = require('./conf');
|
|
26
|
+
const { targetEntryFiles } = require('./util/getTargetFiles');
|
|
27
|
+
const findZhCnInFile = require('./util/findZhCnInFile');
|
|
28
|
+
const fixZhCnInFile = require('./util/fixZhCnInFile');
|
|
29
|
+
const fixI18nDefaultInFile = require('./util/fixI18nDefaultInFile');
|
|
30
|
+
const findZhCnInSvgFile = require('./util/findZhCnInSvgFile');
|
|
31
|
+
const fixZhCnInSvgFile = require('./util/fixZhCnInSvgFile');
|
|
32
|
+
const log = require('./util/log');
|
|
33
|
+
const file = require('./util/file');
|
|
34
|
+
|
|
35
|
+
// ─── Display labels ──────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
const TYPE_LABEL = {
|
|
38
|
+
comment: 'Comment ',
|
|
39
|
+
string: 'String ',
|
|
40
|
+
template: 'Template ',
|
|
41
|
+
jsx: 'JSX Text ',
|
|
42
|
+
'jsx-svg': 'JSX SVG ',
|
|
43
|
+
identifier: 'Identifier',
|
|
44
|
+
'svg-comment': 'SVG Comment ',
|
|
45
|
+
'svg-metadata': 'SVG Metadata',
|
|
46
|
+
'svg-text': 'SVG Text ',
|
|
47
|
+
'svg-attr': 'SVG Attr ',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const TYPE_COLOR = {
|
|
51
|
+
comment: chalk.gray,
|
|
52
|
+
string: chalk.yellow,
|
|
53
|
+
template: chalk.yellow,
|
|
54
|
+
jsx: chalk.cyan,
|
|
55
|
+
'jsx-svg': chalk.magenta,
|
|
56
|
+
identifier: chalk.red,
|
|
57
|
+
'svg-comment': chalk.gray,
|
|
58
|
+
'svg-metadata': chalk.gray,
|
|
59
|
+
'svg-text': chalk.magenta,
|
|
60
|
+
'svg-attr': chalk.magenta,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Types that can be auto-fixed by nozhcn fix
|
|
64
|
+
// Note: 'svg-attr' is conditionally fixable when noZhCnSvgAttrStrategy is set
|
|
65
|
+
const AUTO_FIXABLE_TYPES = new Set(['comment', 'svg-comment', 'svg-metadata', 'svg-attr']);
|
|
66
|
+
|
|
67
|
+
// Types that `collect` script can handle
|
|
68
|
+
const COLLECT_TYPES = new Set(['string', 'template', 'jsx']);
|
|
69
|
+
|
|
70
|
+
// Types that need SVG-specific manual handling
|
|
71
|
+
const SVG_MANUAL_TYPES = new Set(['jsx-svg', 'svg-text']);
|
|
72
|
+
|
|
73
|
+
// ─── Output helpers ──────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
function printHeader(msg) {
|
|
76
|
+
console.log(chalk.bold.blue(`\n[nozhcn] ${msg}`));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function printDivider() {
|
|
80
|
+
console.log(chalk.dim('─'.repeat(72)));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Print all findings grouped by file path.
|
|
85
|
+
* @param {Finding[]} findings
|
|
86
|
+
*/
|
|
87
|
+
function printFindings(findings) {
|
|
88
|
+
if (!findings.length) {
|
|
89
|
+
log.success('[nozhcn] No Chinese characters found.');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Group by filePath
|
|
94
|
+
const byFile = new Map();
|
|
95
|
+
findings.forEach((f) => {
|
|
96
|
+
if (!byFile.has(f.filePath)) byFile.set(f.filePath, []);
|
|
97
|
+
byFile.get(f.filePath).push(f);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
printDivider();
|
|
101
|
+
byFile.forEach((items, fp) => {
|
|
102
|
+
// Show relative path from cwd for readability
|
|
103
|
+
const rel = path.relative(process.cwd(), fp);
|
|
104
|
+
console.log(chalk.underline(`\n ${rel}`));
|
|
105
|
+
|
|
106
|
+
items.forEach((item) => {
|
|
107
|
+
const lineStr = String(item.line || '?').padStart(6);
|
|
108
|
+
const typeLabel = (TYPE_COLOR[item.type] || chalk.white)(
|
|
109
|
+
`[${TYPE_LABEL[item.type] || item.type}]`
|
|
110
|
+
);
|
|
111
|
+
// Truncate long content to 80 chars for terminal readability
|
|
112
|
+
const content = (item.content || '').replace(/\n/g, '\\n').slice(0, 80);
|
|
113
|
+
console.log(` Line ${lineStr} ${typeLabel} ${content}`);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
printDivider();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Print a summary of findings by type.
|
|
121
|
+
* @param {Finding[]} findings
|
|
122
|
+
* @param {'check'|'fix'|'report'} mode
|
|
123
|
+
* @param {number} autoFixedCount - number of auto-fixed comment findings
|
|
124
|
+
* @param {number} i18nDefaultFixed - number of auto-fixed .d() default values
|
|
125
|
+
*/
|
|
126
|
+
function printSummary(findings, mode, autoFixedCount, i18nDefaultFixed) {
|
|
127
|
+
if (!findings.length && autoFixedCount === 0 && i18nDefaultFixed === 0) {
|
|
128
|
+
log.success('[nozhcn] ✓ All clean — no Chinese characters detected.');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Count by type
|
|
133
|
+
const counts = {};
|
|
134
|
+
findings.forEach((f) => {
|
|
135
|
+
counts[f.type] = (counts[f.type] || 0) + 1;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const total = findings.length;
|
|
139
|
+
|
|
140
|
+
if (mode === 'fix' && autoFixedCount > 0) {
|
|
141
|
+
log.success(`[nozhcn] Auto-fixed ${autoFixedCount} comment(s).`);
|
|
142
|
+
}
|
|
143
|
+
if (mode === 'fix' && i18nDefaultFixed > 0) {
|
|
144
|
+
log.success(`[nozhcn] Replaced ${i18nDefaultFixed} .d() default value(s) with translations.`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (total > 0) {
|
|
148
|
+
const countStr = Object.entries(counts)
|
|
149
|
+
.map(([t, n]) => `${n} ${t}`)
|
|
150
|
+
.join(', ');
|
|
151
|
+
log.warnToLog(`[nozhcn] ${total} Chinese occurrence(s) remaining: ${countStr}`);
|
|
152
|
+
|
|
153
|
+
const collectNeeded = findings.some((f) => COLLECT_TYPES.has(f.type));
|
|
154
|
+
if (collectNeeded) {
|
|
155
|
+
console.log(
|
|
156
|
+
chalk.dim(
|
|
157
|
+
" Tip: Run 'npm run intl-collect' (or: chaoswise-intl collect) to internationalize string/template/jsx occurrences."
|
|
158
|
+
)
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const svgManualNeeded = findings.some((f) => SVG_MANUAL_TYPES.has(f.type));
|
|
163
|
+
if (svgManualNeeded) {
|
|
164
|
+
console.log(
|
|
165
|
+
chalk.dim(
|
|
166
|
+
' Tip: SVG text/attribute occurrences require manual handling.\n' +
|
|
167
|
+
' For standalone .svg files, run fix to clean comments/metadata;\n' +
|
|
168
|
+
' for <text> content, consider replacing with a JS-driven label.'
|
|
169
|
+
)
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const identifierFound = findings.some((f) => f.type === 'identifier');
|
|
174
|
+
if (identifierFound) {
|
|
175
|
+
console.log(
|
|
176
|
+
chalk.dim(' Tip: Identifier occurrences require manual renaming.')
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
} else if (mode === 'fix') {
|
|
180
|
+
log.success('[nozhcn] ✓ All auto-fixable Chinese occurrences have been fixed.');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── Main entry ──────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* @param {'check'|'fix'|'report'} mode
|
|
188
|
+
*/
|
|
189
|
+
module.exports = function noZhCn(mode) {
|
|
190
|
+
mode = mode || 'check';
|
|
191
|
+
|
|
192
|
+
printHeader(`Scanning for Chinese characters... (mode: ${mode})`);
|
|
193
|
+
|
|
194
|
+
const conf = getConf();
|
|
195
|
+
const {
|
|
196
|
+
noZhCnCheckTypes = ['comment', 'string', 'template', 'jsx'],
|
|
197
|
+
noZhCnCommentStrategy = 'clean',
|
|
198
|
+
noZhCnReportFile = null,
|
|
199
|
+
noZhCnExclude = [],
|
|
200
|
+
noZhCnIgnoreI18nDefault = true,
|
|
201
|
+
noZhCnDefaultLang = null,
|
|
202
|
+
noZhCnIncludeSvg = false,
|
|
203
|
+
noZhCnSvgMetadataStrategy = 'remove',
|
|
204
|
+
noZhCnSvgAttrStrategy = 'clean',
|
|
205
|
+
localeOutput = 'src/public',
|
|
206
|
+
primaryRegx,
|
|
207
|
+
babelPresets,
|
|
208
|
+
babelPlugins,
|
|
209
|
+
i18nObject,
|
|
210
|
+
i18nMethod,
|
|
211
|
+
i18nDefaultFunctionKey,
|
|
212
|
+
ignoreComponents = ['svg', 'style'],
|
|
213
|
+
} = conf;
|
|
214
|
+
|
|
215
|
+
// Merge nozhcn-specific excludes into the main exclude list
|
|
216
|
+
const mergedConf = {
|
|
217
|
+
...conf,
|
|
218
|
+
exclude: [...(conf.exclude || []), ...(noZhCnExclude || [])],
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const files = targetEntryFiles(mergedConf);
|
|
222
|
+
|
|
223
|
+
if (!files.length) {
|
|
224
|
+
log.warnToLog('[nozhcn] No files found to scan. Please check your `entry` config.');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
log.info(`[nozhcn] ${files.length} file(s) to scan (types: ${noZhCnCheckTypes.join(', ')})`);
|
|
229
|
+
|
|
230
|
+
// Shared scan options
|
|
231
|
+
const scanOpts = {
|
|
232
|
+
checkTypes: noZhCnCheckTypes,
|
|
233
|
+
babelPresets,
|
|
234
|
+
babelPlugins,
|
|
235
|
+
primaryRegx,
|
|
236
|
+
i18nObject,
|
|
237
|
+
i18nMethod,
|
|
238
|
+
i18nDefaultFunctionKey,
|
|
239
|
+
ignoreI18nDefault: noZhCnIgnoreI18nDefault,
|
|
240
|
+
ignoreComponents,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// ── Phase 1: Initial scan ─────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
let allFindings = [];
|
|
246
|
+
const errorFiles = [];
|
|
247
|
+
|
|
248
|
+
files.forEach(({ filePath }) => {
|
|
249
|
+
const result = findZhCnInFile(filePath, scanOpts);
|
|
250
|
+
if (result.error) {
|
|
251
|
+
errorFiles.push({ filePath, error: result.error });
|
|
252
|
+
}
|
|
253
|
+
allFindings = allFindings.concat(result.findings);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (errorFiles.length) {
|
|
257
|
+
log.warnToLog(`[nozhcn] ${errorFiles.length} file(s) failed to parse:`);
|
|
258
|
+
errorFiles.forEach(({ filePath: fp, error }) => {
|
|
259
|
+
const rel = path.relative(process.cwd(), fp);
|
|
260
|
+
console.log(chalk.dim(` ${rel}: ${error}`));
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── Phase 1b: SVG file scan (opt-in) ────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
let svgFiles = [];
|
|
267
|
+
if (noZhCnIncludeSvg) {
|
|
268
|
+
const entryDirs = [].concat(mergedConf.entry || ['src']);
|
|
269
|
+
const excludePatterns = mergedConf.exclude || [];
|
|
270
|
+
entryDirs.forEach((dir) => {
|
|
271
|
+
const matched = glob.sync(`${dir}/**/*.svg`, { ignore: excludePatterns });
|
|
272
|
+
svgFiles = svgFiles.concat(matched);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
log.info(`[nozhcn] ${svgFiles.length} SVG file(s) to scan`);
|
|
276
|
+
|
|
277
|
+
svgFiles.forEach((fp) => {
|
|
278
|
+
const result = findZhCnInSvgFile(fp);
|
|
279
|
+
if (result.error) {
|
|
280
|
+
errorFiles.push({ filePath: fp, error: result.error });
|
|
281
|
+
}
|
|
282
|
+
allFindings = allFindings.concat(result.findings);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ── Phase 2: Auto-fix (fix mode only) ────────────────────────────────────
|
|
287
|
+
//
|
|
288
|
+
// All fix sub-phases (2 / 2b / 2c) run first WITHOUT intermediate re-scans.
|
|
289
|
+
// A single final re-scan at the end produces accurate remaining findings.
|
|
290
|
+
// This avoids up to 3 redundant full-project AST parses on large codebases.
|
|
291
|
+
|
|
292
|
+
let autoFixedCount = 0;
|
|
293
|
+
let i18nDefaultFixed = 0;
|
|
294
|
+
const i18nDefaultMissing = []; // { key, chinese } entries with no translation found
|
|
295
|
+
let svgFixedCount = 0;
|
|
296
|
+
|
|
297
|
+
if (mode === 'fix') {
|
|
298
|
+
let anyFixApplied = false;
|
|
299
|
+
// ── Phase 2a: Fix comments in JS/TS files ─────────────────────────────
|
|
300
|
+
const commentByFile = new Map();
|
|
301
|
+
allFindings.forEach((f) => {
|
|
302
|
+
if (f.type !== 'comment') return;
|
|
303
|
+
if (!commentByFile.has(f.filePath)) commentByFile.set(f.filePath, []);
|
|
304
|
+
commentByFile.get(f.filePath).push(f);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
commentByFile.forEach((findings, fp) => {
|
|
308
|
+
autoFixedCount += fixZhCnInFile(fp, findings, noZhCnCommentStrategy);
|
|
309
|
+
});
|
|
310
|
+
if (autoFixedCount > 0) anyFixApplied = true;
|
|
311
|
+
|
|
312
|
+
// ── Phase 2b: Replace .d('Chinese') with translations ─────────────────
|
|
313
|
+
if (noZhCnDefaultLang) {
|
|
314
|
+
const localePath = path.resolve(
|
|
315
|
+
process.cwd(),
|
|
316
|
+
localeOutput,
|
|
317
|
+
'locales',
|
|
318
|
+
`${noZhCnDefaultLang}.json`
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
if (!fs.existsSync(localePath)) {
|
|
322
|
+
log.warnToLog(
|
|
323
|
+
`[nozhcn] noZhCnDefaultLang='${noZhCnDefaultLang}' is set but locale file does not exist: ${localePath}\n` +
|
|
324
|
+
` Run 'npm run intl-update' (or: chaoswise-intl update) first to generate locale files.`
|
|
325
|
+
);
|
|
326
|
+
} else {
|
|
327
|
+
let localeMap;
|
|
328
|
+
try {
|
|
329
|
+
localeMap = JSON.parse(fs.readFileSync(localePath, 'utf8'));
|
|
330
|
+
} catch (err) {
|
|
331
|
+
log.error(`[nozhcn] Failed to parse locale file: ${localePath} — ${err.message}`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (localeMap) {
|
|
335
|
+
const fixOpts = { i18nObject, i18nMethod, i18nDefaultFunctionKey, babelPresets, babelPlugins };
|
|
336
|
+
// Collect JS/TS file paths from initial findings (exclude svg-* types)
|
|
337
|
+
const filesToFix = new Set(
|
|
338
|
+
allFindings.filter((f) => !f.type.startsWith('svg-')).map((f) => f.filePath)
|
|
339
|
+
);
|
|
340
|
+
filesToFix.forEach((fp) => {
|
|
341
|
+
const result = fixI18nDefaultInFile(fp, localeMap, fixOpts);
|
|
342
|
+
i18nDefaultFixed += result.fixed;
|
|
343
|
+
i18nDefaultMissing.push(...result.missing.map((m) => ({ ...m, filePath: fp })));
|
|
344
|
+
});
|
|
345
|
+
if (i18nDefaultFixed > 0) anyFixApplied = true;
|
|
346
|
+
|
|
347
|
+
if (i18nDefaultMissing.length) {
|
|
348
|
+
log.warnToLog(
|
|
349
|
+
`[nozhcn] ${i18nDefaultMissing.length} .d() default value(s) have no '${noZhCnDefaultLang}' translation yet:`
|
|
350
|
+
);
|
|
351
|
+
i18nDefaultMissing.slice(0, 10).forEach(({ filePath: fp, key, chinese }) => {
|
|
352
|
+
const rel = path.relative(process.cwd(), fp);
|
|
353
|
+
console.log(chalk.dim(` ${rel} key=${key} zh='${chinese}'`));
|
|
354
|
+
});
|
|
355
|
+
if (i18nDefaultMissing.length > 10) {
|
|
356
|
+
console.log(chalk.dim(` ... and ${i18nDefaultMissing.length - 10} more`));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ── Phase 2c: Fix SVG files ───────────────────────────────────────────
|
|
364
|
+
if (noZhCnIncludeSvg && svgFiles.length) {
|
|
365
|
+
const svgByFile = new Map();
|
|
366
|
+
allFindings.forEach((f) => {
|
|
367
|
+
if (!f.type.startsWith('svg-')) return;
|
|
368
|
+
if (!svgByFile.has(f.filePath)) svgByFile.set(f.filePath, []);
|
|
369
|
+
svgByFile.get(f.filePath).push(f);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
svgByFile.forEach((findings, fp) => {
|
|
373
|
+
const result = fixZhCnInSvgFile(fp, findings, {
|
|
374
|
+
commentStrategy: noZhCnCommentStrategy,
|
|
375
|
+
metadataStrategy: noZhCnSvgMetadataStrategy,
|
|
376
|
+
attrStrategy: noZhCnSvgAttrStrategy,
|
|
377
|
+
});
|
|
378
|
+
svgFixedCount += result.fixed;
|
|
379
|
+
});
|
|
380
|
+
if (svgFixedCount > 0) anyFixApplied = true;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ── Single final re-scan after all fixes ──────────────────────────────
|
|
384
|
+
if (anyFixApplied) {
|
|
385
|
+
allFindings = [];
|
|
386
|
+
files.forEach(({ filePath }) => {
|
|
387
|
+
const result = findZhCnInFile(filePath, scanOpts);
|
|
388
|
+
allFindings = allFindings.concat(result.findings);
|
|
389
|
+
});
|
|
390
|
+
if (noZhCnIncludeSvg && svgFiles.length) {
|
|
391
|
+
svgFiles.forEach((fp) => {
|
|
392
|
+
const result = findZhCnInSvgFile(fp);
|
|
393
|
+
allFindings = allFindings.concat(result.findings);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ── Phase 3: Output ───────────────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
printFindings(allFindings);
|
|
402
|
+
printSummary(allFindings, mode, autoFixedCount + svgFixedCount, i18nDefaultFixed);
|
|
403
|
+
|
|
404
|
+
// Write JSON report if configured
|
|
405
|
+
const reportPath = noZhCnReportFile || (mode === 'report' ? 'nozhcn-report.json' : null);
|
|
406
|
+
if (reportPath) {
|
|
407
|
+
// Build summary counts
|
|
408
|
+
const summary = {};
|
|
409
|
+
allFindings.forEach((f) => {
|
|
410
|
+
summary[f.type] = (summary[f.type] || 0) + 1;
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const report = {
|
|
414
|
+
timestamp: new Date().toISOString(),
|
|
415
|
+
mode,
|
|
416
|
+
total: allFindings.length,
|
|
417
|
+
autoFixed: autoFixedCount,
|
|
418
|
+
i18nDefaultFixed,
|
|
419
|
+
i18nDefaultMissingCount: i18nDefaultMissing.length,
|
|
420
|
+
summary,
|
|
421
|
+
findings: allFindings.map((f) => ({
|
|
422
|
+
filePath: path.relative(process.cwd(), f.filePath),
|
|
423
|
+
type: f.type,
|
|
424
|
+
line: f.line,
|
|
425
|
+
col: f.col,
|
|
426
|
+
content: f.content,
|
|
427
|
+
})),
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
file.write(reportPath, JSON.stringify(report, null, 2));
|
|
431
|
+
log.info(`[nozhcn] Report written to: ${reportPath}`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ── Phase 4: Exit code ────────────────────────────────────────────────────
|
|
435
|
+
|
|
436
|
+
// 'report' mode is non-blocking (used for dashboards / tracking)
|
|
437
|
+
if (mode !== 'report' && allFindings.length > 0) {
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* babelOptions.js
|
|
3
|
+
*
|
|
4
|
+
* Shared Babel transform options used by all AST-based scan/fix utilities.
|
|
5
|
+
* Matches the superset-of-JS/TS/JSX/TSX syntax configuration from transformAst.js.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const pluginSyntaxJSX = require('@babel/plugin-syntax-jsx');
|
|
9
|
+
const pluginSyntaxProposalOptionalChaining = require('@babel/plugin-proposal-optional-chaining');
|
|
10
|
+
const pluginSyntaxClassProperties = require('@babel/plugin-syntax-class-properties');
|
|
11
|
+
const pluginSyntaxDecorators = require('@babel/plugin-syntax-decorators');
|
|
12
|
+
const pluginSyntaxObjectRestSpread = require('@babel/plugin-syntax-object-rest-spread');
|
|
13
|
+
const pluginSyntaxAsyncGenerators = require('@babel/plugin-syntax-async-generators');
|
|
14
|
+
const pluginSyntaxDoExpressions = require('@babel/plugin-syntax-do-expressions');
|
|
15
|
+
const pluginSyntaxDynamicImport = require('@babel/plugin-syntax-dynamic-import');
|
|
16
|
+
const pluginSyntaxFunctionBind = require('@babel/plugin-syntax-function-bind');
|
|
17
|
+
const pluginExportDefaultFrom = require('@babel/plugin-proposal-export-default-from');
|
|
18
|
+
const presetTypescript = require('@babel/preset-typescript').default;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {Array} [babelPresets]
|
|
22
|
+
* @param {Array} [babelPlugins]
|
|
23
|
+
* @returns {import('@babel/core').TransformOptions}
|
|
24
|
+
*/
|
|
25
|
+
module.exports = function buildTransformOptions(babelPresets, babelPlugins) {
|
|
26
|
+
return {
|
|
27
|
+
sourceType: 'module',
|
|
28
|
+
ast: true,
|
|
29
|
+
configFile: false,
|
|
30
|
+
parserOpts: { tokens: true },
|
|
31
|
+
presets: [
|
|
32
|
+
...(babelPresets || []),
|
|
33
|
+
[presetTypescript, { isTSX: true, allExtensions: true }],
|
|
34
|
+
],
|
|
35
|
+
plugins: [
|
|
36
|
+
pluginSyntaxJSX,
|
|
37
|
+
pluginSyntaxProposalOptionalChaining,
|
|
38
|
+
pluginSyntaxClassProperties,
|
|
39
|
+
[pluginSyntaxDecorators, { decoratorsBeforeExport: true }],
|
|
40
|
+
pluginSyntaxObjectRestSpread,
|
|
41
|
+
pluginSyntaxAsyncGenerators,
|
|
42
|
+
pluginSyntaxDoExpressions,
|
|
43
|
+
pluginSyntaxDynamicImport,
|
|
44
|
+
pluginSyntaxFunctionBind,
|
|
45
|
+
pluginExportDefaultFrom,
|
|
46
|
+
...(babelPlugins || []),
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
};
|