@eevee-ta/cli 1.0.1 → 1.3.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/README.md +175 -9
- package/assets/skills/eevee-integration/SKILL.md +46 -0
- package/assets/skills/eevee-integration/agents/openai.yaml +3 -0
- package/assets/skills/eevee-integration/references/intake.md +87 -0
- package/assets/skills/eevee-integration/references/java-springboot.md +182 -0
- package/assets/skills/eevee-integration/references/umijs.md +210 -0
- package/assets/skills/eevee-mcp/SKILL.md +74 -5
- package/assets/skills/eevee-mcp/reference.md +48 -3
- package/dist/src/bin/cli.js +23 -7
- package/dist/src/bin/cli.js.map +1 -1
- package/dist/src/commands/business-command.js +174 -2
- package/dist/src/commands/business-command.js.map +1 -1
- package/dist/src/commands/i18n-command.js +194 -0
- package/dist/src/commands/i18n-command.js.map +1 -0
- package/dist/src/commands/skill-command.js +42 -26
- package/dist/src/commands/skill-command.js.map +1 -1
- package/dist/src/lib/business-catalog.js +50 -20
- package/dist/src/lib/business-catalog.js.map +1 -1
- package/dist/src/lib/i18n-exporter.js +402 -0
- package/dist/src/lib/i18n-exporter.js.map +1 -0
- package/dist/src/lib/i18n-scanner.js +1172 -0
- package/dist/src/lib/i18n-scanner.js.map +1 -0
- package/dist/src/lib/mcp-client.js +1 -1
- package/dist/src/lib/mcp-client.js.map +1 -1
- package/dist/src/lib/skill-installer.js +19 -10
- package/dist/src/lib/skill-installer.js.map +1 -1
- package/dist/src/lib/updater.js +4 -4
- package/dist/src/lib/updater.js.map +1 -1
- package/dist/src/lib/version.js +1 -1
- package/dist/src/lib/version.js.map +1 -1
- package/install.sh +3 -3
- package/package.json +2 -2
|
@@ -0,0 +1,1172 @@
|
|
|
1
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const DEFAULT_INCLUDES = [
|
|
4
|
+
'**/src/**/*.{js,jsx,mjs,cjs,ts,tsx,vue}',
|
|
5
|
+
'**/src/main/java/**/*.java',
|
|
6
|
+
'**/src/main/resources/templates/**/*.{html,htm,jsp,jspf,tag,ftl,vm}',
|
|
7
|
+
];
|
|
8
|
+
const DEFAULT_EXCLUDES = [
|
|
9
|
+
'**/node_modules/**',
|
|
10
|
+
'**/.umi/**',
|
|
11
|
+
'**/.umi-production/**',
|
|
12
|
+
'**/.umi-test/**',
|
|
13
|
+
'**/dist/**',
|
|
14
|
+
'**/build/**',
|
|
15
|
+
'**/target/**',
|
|
16
|
+
'**/generated/**',
|
|
17
|
+
'**/*.min.*',
|
|
18
|
+
'**/*.d.ts',
|
|
19
|
+
'**/*.{test,spec}.{js,jsx,mjs,cjs,ts,tsx,vue}',
|
|
20
|
+
'**/{mock,mocks,__mocks__,fixture,fixtures,__fixtures__}/**',
|
|
21
|
+
'**/*.{mock,fixture}.{js,jsx,mjs,cjs,ts,tsx,vue}',
|
|
22
|
+
'**/{mock,fixture}.{js,jsx,mjs,cjs,ts,tsx,vue}',
|
|
23
|
+
'**/{mock,fixture}*.{js,jsx,mjs,cjs,ts,tsx,vue}',
|
|
24
|
+
];
|
|
25
|
+
const JS_EXTENSIONS = new Set(['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.vue']);
|
|
26
|
+
const MARKUP_EXTENSIONS = new Set(['.html', '.htm', '.jsp', '.jspf', '.tag', '.ftl', '.vm']);
|
|
27
|
+
const HAS_CJK = /[\u3400-\u9fff\uf900-\ufaff]/;
|
|
28
|
+
const CJK_TEXT = /[\u3400-\u9fff\uf900-\ufaff][\u3400-\u9fff\uf900-\ufaff\u3000-\u303f\uff01-\uff5e\s]*/g;
|
|
29
|
+
export async function scanProjectI18n(projectRootInput, options = {}) {
|
|
30
|
+
const projectRoot = path.resolve(projectRootInput);
|
|
31
|
+
const rootStat = await stat(projectRoot);
|
|
32
|
+
if (!rootStat.isDirectory())
|
|
33
|
+
throw new Error(`项目目录不存在或不是目录: ${projectRoot}`);
|
|
34
|
+
const config = await loadConfig(projectRoot, options.configPath);
|
|
35
|
+
const includes = options.includes?.length ? options.includes : config.includes;
|
|
36
|
+
const excludes = [
|
|
37
|
+
...(options.excludes?.length ? options.excludes : config.excludes),
|
|
38
|
+
...config.outPaths.map((directory) => `${normalizeGlob(directory)}**`),
|
|
39
|
+
];
|
|
40
|
+
const normalizedOptions = {
|
|
41
|
+
...config,
|
|
42
|
+
includes: includes?.length ? includes : DEFAULT_INCLUDES,
|
|
43
|
+
excludes: [...new Set([...excludes, ...DEFAULT_EXCLUDES])],
|
|
44
|
+
excludeAscii: config.excludeAscii ?? true,
|
|
45
|
+
excludeTrack: config.excludeTrack ?? config.execludeTrack ?? true,
|
|
46
|
+
checkCallArguments: options.checkCallArguments ?? config.checkCallArguments ?? false,
|
|
47
|
+
checkAnnotations: options.checkAnnotations ?? config.checkAnnotations ?? false,
|
|
48
|
+
};
|
|
49
|
+
const dictionary = await loadDictionary(projectRoot, config.outPaths);
|
|
50
|
+
const files = await collectFiles(projectRoot, normalizedOptions.includes, normalizedOptions.excludes);
|
|
51
|
+
const issues = [];
|
|
52
|
+
for (const file of files) {
|
|
53
|
+
const content = await readFile(file, 'utf8');
|
|
54
|
+
const directives = scanJavaScriptDirectives(content);
|
|
55
|
+
if (directives.fileDisabled)
|
|
56
|
+
continue;
|
|
57
|
+
const fileIssues = scanFile({
|
|
58
|
+
projectRoot,
|
|
59
|
+
file,
|
|
60
|
+
content,
|
|
61
|
+
config: normalizedOptions,
|
|
62
|
+
dictionary,
|
|
63
|
+
}).filter((issue) => !directives.disabledLines.has(issue.line));
|
|
64
|
+
issues.push(...fileIssues);
|
|
65
|
+
}
|
|
66
|
+
issues.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column || a.term.localeCompare(b.term));
|
|
67
|
+
return {
|
|
68
|
+
projectRoot,
|
|
69
|
+
configPath: config.configPath,
|
|
70
|
+
scannedFiles: files.length,
|
|
71
|
+
dictionaryTerms: dictionary.terms.size,
|
|
72
|
+
dictionaryTags: [...dictionary.tags.keys()].sort(),
|
|
73
|
+
issues,
|
|
74
|
+
ok: issues.length === 0,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async function loadConfig(projectRoot, overridePath) {
|
|
78
|
+
const candidates = overridePath
|
|
79
|
+
? [overridePath]
|
|
80
|
+
: ['intl.json', '.eevee/i18n.json', 'eevee.i18n.json'].map((item) => path.join(projectRoot, item));
|
|
81
|
+
let selected = null;
|
|
82
|
+
for (const candidate of candidates) {
|
|
83
|
+
const resolved = path.resolve(candidate);
|
|
84
|
+
if ((await stat(resolved).catch(() => undefined))?.isFile()) {
|
|
85
|
+
selected = resolved;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (!selected && overridePath)
|
|
90
|
+
throw new Error(`国际化检查配置不存在: ${overridePath}`);
|
|
91
|
+
let raw = {};
|
|
92
|
+
if (selected) {
|
|
93
|
+
try {
|
|
94
|
+
raw = JSON.parse(await readFile(selected, 'utf8'));
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
throw new Error(`无法解析国际化配置 ${selected}: ${error.message}`);
|
|
98
|
+
}
|
|
99
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
100
|
+
throw new Error(`国际化配置必须是 JSON 对象: ${selected}`);
|
|
101
|
+
}
|
|
102
|
+
const outPaths = (raw.outPaths ?? [])
|
|
103
|
+
.map((item) => typeof item === 'string' ? item : item?.path ?? '')
|
|
104
|
+
.filter(Boolean)
|
|
105
|
+
.map((item) => normalizeGlob(item));
|
|
106
|
+
const legacyProperties = raw.excludePropertys ?? [];
|
|
107
|
+
const properties = raw.excludeProperties ?? legacyProperties;
|
|
108
|
+
return {
|
|
109
|
+
outPaths,
|
|
110
|
+
includes: raw.includes ?? [],
|
|
111
|
+
excludes: raw.excludes ?? [],
|
|
112
|
+
excludeAscii: raw.excludeAscii ?? true,
|
|
113
|
+
tagMatchMap: raw.tagMatchMap ?? {},
|
|
114
|
+
excludeProperties: properties,
|
|
115
|
+
excludeTrack: raw.excludeTrack ?? raw.execludeTrack ?? true,
|
|
116
|
+
checkCallArguments: raw.checkCallArguments ?? false,
|
|
117
|
+
checkAnnotations: raw.checkAnnotations ?? false,
|
|
118
|
+
configPath: selected,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async function loadDictionary(projectRoot, outPaths) {
|
|
122
|
+
const dictionary = { terms: new Map(), tags: new Map() };
|
|
123
|
+
for (const outputPath of outPaths) {
|
|
124
|
+
const directory = resolveLegacyProjectPath(projectRoot, outputPath);
|
|
125
|
+
const entries = await readdir(directory, { withFileTypes: true }).catch((error) => {
|
|
126
|
+
if (error.code === 'ENOENT')
|
|
127
|
+
return [];
|
|
128
|
+
throw new Error(`无法读取词条输出目录 ${directory}: ${error.message}`);
|
|
129
|
+
});
|
|
130
|
+
for (const entry of entries) {
|
|
131
|
+
if (!entry.isFile())
|
|
132
|
+
continue;
|
|
133
|
+
if (!isDictionaryFile(entry.name))
|
|
134
|
+
continue;
|
|
135
|
+
const tag = dictionaryTag(entry.name);
|
|
136
|
+
const values = await loadDictionaryValues(path.join(directory, entry.name), entry.name);
|
|
137
|
+
for (const term of values) {
|
|
138
|
+
if (!term)
|
|
139
|
+
continue;
|
|
140
|
+
const tags = dictionary.terms.get(term) ?? new Set();
|
|
141
|
+
tags.add(tag);
|
|
142
|
+
dictionary.terms.set(term, tags);
|
|
143
|
+
const terms = dictionary.tags.get(tag) ?? new Set();
|
|
144
|
+
terms.add(term);
|
|
145
|
+
dictionary.tags.set(tag, terms);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return dictionary;
|
|
150
|
+
}
|
|
151
|
+
function resolveLegacyProjectPath(projectRoot, value) {
|
|
152
|
+
const normalized = toPosix(value);
|
|
153
|
+
// 旧 intl.json 的 path 虽以 / 开头,但语义是项目根内的路径,而不是文件系统绝对路径。
|
|
154
|
+
return path.resolve(projectRoot, normalized.startsWith('/') ? normalized.slice(1) : normalized);
|
|
155
|
+
}
|
|
156
|
+
function isDictionaryFile(name) {
|
|
157
|
+
const extension = path.extname(name);
|
|
158
|
+
if (!['.json', '.js', '.cjs', '.mjs', '.properties'].includes(extension))
|
|
159
|
+
return false;
|
|
160
|
+
const lower = name.toLowerCase();
|
|
161
|
+
return lower.startsWith('zh-cn') || (/^messages/.test(lower) && /(?:zh|cn)/.test(lower));
|
|
162
|
+
}
|
|
163
|
+
function dictionaryTag(name) {
|
|
164
|
+
if (name.startsWith('zh-CN-')) {
|
|
165
|
+
const suffix = name.slice('zh-CN-'.length);
|
|
166
|
+
return suffix.slice(0, suffix.indexOf('.') >= 0 ? suffix.indexOf('.') : undefined) || 'default';
|
|
167
|
+
}
|
|
168
|
+
return 'spring';
|
|
169
|
+
}
|
|
170
|
+
async function loadDictionaryValues(file, name) {
|
|
171
|
+
if (name.endsWith('.properties'))
|
|
172
|
+
return parseProperties(await readFile(file, 'utf8'));
|
|
173
|
+
if (name.endsWith('.js') || name.endsWith('.cjs') || name.endsWith('.mjs')) {
|
|
174
|
+
return parseJavaScriptDictionaryKeys(await readFile(file, 'utf8'));
|
|
175
|
+
}
|
|
176
|
+
return flattenKeys(JSON.parse(await readFile(file, 'utf8')));
|
|
177
|
+
}
|
|
178
|
+
function flattenKeys(value, prefix = '') {
|
|
179
|
+
const keys = [];
|
|
180
|
+
for (const [key, item] of Object.entries(value)) {
|
|
181
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
182
|
+
keys.push(fullKey);
|
|
183
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
184
|
+
keys.push(...flattenKeys(item, fullKey));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return keys;
|
|
188
|
+
}
|
|
189
|
+
function parseProperties(content) {
|
|
190
|
+
return content.split(/\r?\n/)
|
|
191
|
+
.map((line) => line.trim())
|
|
192
|
+
.filter((line) => line && !line.startsWith('#') && !line.startsWith('!'))
|
|
193
|
+
.map((line) => {
|
|
194
|
+
const separator = line.search(/[:=]/);
|
|
195
|
+
return separator >= 0 ? line.slice(0, separator).trim() : '';
|
|
196
|
+
})
|
|
197
|
+
.filter(Boolean);
|
|
198
|
+
}
|
|
199
|
+
function parseJavaScriptDictionaryKeys(content) {
|
|
200
|
+
const assignment = /(?:export\s+default|module\.exports(?:\.default)?)(?:\s*=\s*)?/.exec(content);
|
|
201
|
+
const start = content.indexOf('{', assignment ? (assignment.index ?? 0) + assignment[0].length : 0);
|
|
202
|
+
if (start < 0)
|
|
203
|
+
return [];
|
|
204
|
+
const end = matchingBrace(content, start);
|
|
205
|
+
if (end < 0)
|
|
206
|
+
return [];
|
|
207
|
+
const keys = [];
|
|
208
|
+
let depth = 1;
|
|
209
|
+
let expectKey = true;
|
|
210
|
+
let index = start + 1;
|
|
211
|
+
while (index < end) {
|
|
212
|
+
index = skipJavaScriptNoise(content, index, !expectKey);
|
|
213
|
+
if (content[index] === '}')
|
|
214
|
+
break;
|
|
215
|
+
if (expectKey) {
|
|
216
|
+
const key = readJavaScriptKey(content, index);
|
|
217
|
+
if (!key)
|
|
218
|
+
return keys;
|
|
219
|
+
keys.push(key.value);
|
|
220
|
+
index = skipJavaScriptNoise(content, key.end, false);
|
|
221
|
+
if (content[index] !== ':')
|
|
222
|
+
return keys;
|
|
223
|
+
expectKey = false;
|
|
224
|
+
index++;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (content[index] === '{' || content[index] === '[')
|
|
228
|
+
depth++;
|
|
229
|
+
else if (content[index] === '}' || content[index] === ']')
|
|
230
|
+
depth--;
|
|
231
|
+
else if (content[index] === ',' && depth === 1)
|
|
232
|
+
expectKey = true;
|
|
233
|
+
index++;
|
|
234
|
+
}
|
|
235
|
+
return keys;
|
|
236
|
+
}
|
|
237
|
+
function matchingBrace(content, start) {
|
|
238
|
+
let depth = 0;
|
|
239
|
+
for (let index = start; index < content.length; index++) {
|
|
240
|
+
index = skipJavaScriptNoise(content, index);
|
|
241
|
+
if (content[index] === '{')
|
|
242
|
+
depth++;
|
|
243
|
+
else if (content[index] === '}') {
|
|
244
|
+
depth--;
|
|
245
|
+
if (depth === 0)
|
|
246
|
+
return index;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return -1;
|
|
250
|
+
}
|
|
251
|
+
function skipJavaScriptNoise(content, start, skipStrings = true) {
|
|
252
|
+
let index = start;
|
|
253
|
+
while (index < content.length) {
|
|
254
|
+
const char = content[index];
|
|
255
|
+
if (/\s/.test(char)) {
|
|
256
|
+
index++;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (char === '/' && content[index + 1] === '/') {
|
|
260
|
+
while (index < content.length && content[index] !== '\n')
|
|
261
|
+
index++;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (char === '/' && content[index + 1] === '*') {
|
|
265
|
+
const end = content.indexOf('*/', index + 2);
|
|
266
|
+
index = end >= 0 ? end + 2 : content.length;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (skipStrings && (char === '"' || char === "'" || char === '`')) {
|
|
270
|
+
index++;
|
|
271
|
+
while (index < content.length && content[index] !== char) {
|
|
272
|
+
if (content[index] === '\\')
|
|
273
|
+
index++;
|
|
274
|
+
index++;
|
|
275
|
+
}
|
|
276
|
+
index++;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
return index;
|
|
282
|
+
}
|
|
283
|
+
function readJavaScriptKey(content, start) {
|
|
284
|
+
if (content[start] === '"' || content[start] === "'") {
|
|
285
|
+
const quote = content[start];
|
|
286
|
+
let index = start + 1;
|
|
287
|
+
let value = '';
|
|
288
|
+
while (index < content.length && content[index] !== quote) {
|
|
289
|
+
if (content[index] === '\\') {
|
|
290
|
+
value += content[index + 1] ?? '';
|
|
291
|
+
index += 2;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
value += content[index++];
|
|
295
|
+
}
|
|
296
|
+
return { value, end: index + 1 };
|
|
297
|
+
}
|
|
298
|
+
const match = /^[\w$]+/.exec(content.slice(start));
|
|
299
|
+
return match ? { value: match[0], end: start + match[0].length } : null;
|
|
300
|
+
}
|
|
301
|
+
async function collectFiles(root, includes, excludes) {
|
|
302
|
+
const files = [];
|
|
303
|
+
const ignoredDirectories = new Set(['.git', 'node_modules', 'dist', 'build', 'target', 'generated']);
|
|
304
|
+
const walk = async (directory) => {
|
|
305
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
306
|
+
for (const entry of entries) {
|
|
307
|
+
const full = path.join(directory, entry.name);
|
|
308
|
+
if (entry.isDirectory()) {
|
|
309
|
+
if (ignoredDirectories.has(entry.name))
|
|
310
|
+
continue;
|
|
311
|
+
await walk(full);
|
|
312
|
+
}
|
|
313
|
+
else if (entry.isFile() && isSourceFile(entry.name)) {
|
|
314
|
+
const relative = toPosix(path.relative(root, full));
|
|
315
|
+
if (matchesAny(relative, includes, root) && !matchesAny(relative, excludes, root))
|
|
316
|
+
files.push(full);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
await walk(root);
|
|
321
|
+
return files.sort((a, b) => a.localeCompare(b));
|
|
322
|
+
}
|
|
323
|
+
function isSourceFile(name) {
|
|
324
|
+
const extension = path.extname(name).toLowerCase();
|
|
325
|
+
return JS_EXTENSIONS.has(extension) || extension === '.java' || MARKUP_EXTENSIONS.has(extension);
|
|
326
|
+
}
|
|
327
|
+
function scanFile(context) {
|
|
328
|
+
const extension = path.extname(context.file).toLowerCase();
|
|
329
|
+
if (extension === '.java')
|
|
330
|
+
return scanJava(context);
|
|
331
|
+
if (MARKUP_EXTENSIONS.has(extension))
|
|
332
|
+
return scanMarkup(context);
|
|
333
|
+
return scanJavaScript(context);
|
|
334
|
+
}
|
|
335
|
+
function scanJavaScript(context) {
|
|
336
|
+
const issues = [];
|
|
337
|
+
const { tokens, masked } = scanJavaScriptStrings(context.content);
|
|
338
|
+
for (const token of tokens) {
|
|
339
|
+
if (!shouldCheckTerm(token.value, context.config))
|
|
340
|
+
continue;
|
|
341
|
+
if (isIgnoredJavaScriptToken(token, masked, context.config))
|
|
342
|
+
continue;
|
|
343
|
+
const callName = enclosingCallName(masked, token.start);
|
|
344
|
+
if (isMessageCall(callName)) {
|
|
345
|
+
issues.push(...checkRegisteredTerm(token, context));
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (context.config.excludeTrack && isTrackCall(callName))
|
|
349
|
+
continue;
|
|
350
|
+
if (callName &&
|
|
351
|
+
!token.template &&
|
|
352
|
+
!context.config.checkCallArguments &&
|
|
353
|
+
isDirectCallArgument(masked, token.start, token.end))
|
|
354
|
+
continue;
|
|
355
|
+
issues.push(createIssue(context, token, 'javascript', 'no-i18n', `词条【${token.value}】没有国际化`));
|
|
356
|
+
}
|
|
357
|
+
if (extensionAllowsMarkup(extensionOf(context.file))) {
|
|
358
|
+
issues.push(...scanMarkupText(masked, context, true));
|
|
359
|
+
}
|
|
360
|
+
return deduplicate(issues);
|
|
361
|
+
}
|
|
362
|
+
function scanMarkup(context) {
|
|
363
|
+
const issues = [];
|
|
364
|
+
issues.push(...scanMarkupAttributes(context));
|
|
365
|
+
issues.push(...scanMarkupText(maskMarkupComments(context.content), context));
|
|
366
|
+
return deduplicate(issues);
|
|
367
|
+
}
|
|
368
|
+
function scanMarkupText(masked, context, jsxMode = false) {
|
|
369
|
+
const issues = [];
|
|
370
|
+
const content = jsxMode
|
|
371
|
+
? extractJsxTextSegments(masked)
|
|
372
|
+
: [
|
|
373
|
+
[
|
|
374
|
+
masked
|
|
375
|
+
.replace(/<[^>]*>/g, (item) => '\0'.repeat(item.length))
|
|
376
|
+
.replace(/\{\{[\s\S]*?\}\}/g, (item) => '\0'.repeat(item.length))
|
|
377
|
+
.replace(/(?:\$\{|#\{)[^}]*\}/g, (item) => '\0'.repeat(item.length)),
|
|
378
|
+
0,
|
|
379
|
+
],
|
|
380
|
+
];
|
|
381
|
+
for (const [text, offset] of content) {
|
|
382
|
+
// TSX 里形如 INTL.操作 / ALERT_INTL.空间 的是成员表达式,不是 JSX 文本。
|
|
383
|
+
const code = text.replace(/[A-Za-z_$][\w$]*(?:\s*\.\s*[A-Za-z_$\p{ID_Continue}]+)+/gu, (item) => ' '.repeat(item.length));
|
|
384
|
+
for (const match of code.matchAll(CJK_TEXT)) {
|
|
385
|
+
const term = normalizeSpace(decodeHtmlEntities(match[0]));
|
|
386
|
+
if (!term || !HAS_CJK.test(term))
|
|
387
|
+
continue;
|
|
388
|
+
const position = positionAt(context.content, offset + (match.index ?? 0));
|
|
389
|
+
issues.push(createRawIssue(context, position, term, 'markup', 'no-i18n', `词条【${term}】没有国际化`));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return issues;
|
|
393
|
+
}
|
|
394
|
+
function extractJsxTextSegments(content, baseOffset = 0) {
|
|
395
|
+
const segments = [];
|
|
396
|
+
let segmentStart = -1;
|
|
397
|
+
let index = 0;
|
|
398
|
+
const flush = (end) => {
|
|
399
|
+
if (segmentStart >= 0 && end > segmentStart) {
|
|
400
|
+
segments.push([content.slice(segmentStart, end), baseOffset + segmentStart]);
|
|
401
|
+
}
|
|
402
|
+
segmentStart = -1;
|
|
403
|
+
};
|
|
404
|
+
while (index < content.length) {
|
|
405
|
+
const char = content[index];
|
|
406
|
+
if (char === '<' && isTagStart(content, index)) {
|
|
407
|
+
flush(index);
|
|
408
|
+
const closing = content[index + 1] === '/';
|
|
409
|
+
const tagEnd = findTagEnd(content, index);
|
|
410
|
+
if (tagEnd < 0)
|
|
411
|
+
break;
|
|
412
|
+
// 标签属性中的 {表达式} 也要递归提取:Tabs/Menu 等常用
|
|
413
|
+
// `items={[{ children: (<div>中文</div>) }]}` 把 JSX 放进属性,
|
|
414
|
+
// 若只做括号平衡跳过,其中的裸中文文案会整体漏检。
|
|
415
|
+
if (!closing) {
|
|
416
|
+
let cursor = index + 1;
|
|
417
|
+
while (cursor < tagEnd) {
|
|
418
|
+
if (content[cursor] === '{') {
|
|
419
|
+
const attrEnd = findBalancedEnd(content, cursor, '{', '}');
|
|
420
|
+
if (attrEnd < 0 || attrEnd > tagEnd)
|
|
421
|
+
break;
|
|
422
|
+
const inner = content.slice(cursor + 1, attrEnd);
|
|
423
|
+
if (/<[A-Za-z>]/.test(inner)) {
|
|
424
|
+
segments.push(...extractJsxTextSegments(inner, baseOffset + cursor + 1));
|
|
425
|
+
}
|
|
426
|
+
cursor = attrEnd + 1;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
cursor++;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
const selfClosing = content.slice(index + 1, tagEnd).trimEnd().endsWith('/');
|
|
433
|
+
// 只有 JSX 开始标签后的内容是文本节点;结束标签和自闭合标签后回到 JS 代码。
|
|
434
|
+
segmentStart = !closing && !selfClosing ? tagEnd + 1 : -1;
|
|
435
|
+
index = tagEnd + 1;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (segmentStart >= 0 && char === '{') {
|
|
439
|
+
flush(index);
|
|
440
|
+
const expressionEnd = findBalancedEnd(content, index, '{', '}');
|
|
441
|
+
if (expressionEnd < 0)
|
|
442
|
+
break;
|
|
443
|
+
// 表达式内嵌套的 JSX 文本也要提取:`{cond && <div>中文</div>}`、
|
|
444
|
+
// `{cond ? <A>甲</A> : <B>乙</B>}` 是最常见条件渲染写法,
|
|
445
|
+
// 若整段跳过花括号会导致其中的裸中文文案全部漏检。
|
|
446
|
+
const inner = content.slice(index + 1, expressionEnd);
|
|
447
|
+
if (/<[A-Za-z>]/.test(inner)) {
|
|
448
|
+
segments.push(...extractJsxTextSegments(inner, baseOffset + index + 1));
|
|
449
|
+
}
|
|
450
|
+
index = expressionEnd + 1;
|
|
451
|
+
segmentStart = index;
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
index++;
|
|
455
|
+
}
|
|
456
|
+
flush(content.length);
|
|
457
|
+
return segments;
|
|
458
|
+
}
|
|
459
|
+
function isTagStart(content, offset) {
|
|
460
|
+
const next = content[offset + 1];
|
|
461
|
+
if (!next || !(/[A-Za-z]/.test(next) || next === '>' || next === '/'))
|
|
462
|
+
return false;
|
|
463
|
+
// TSX 中 `FC<Props>` / `Array<string>` / `useRef<HTMLInputElement>(...)` 的 `<` 紧跟在
|
|
464
|
+
// 普通标识符之后;而 JSX 开始标签前的上一个单词只会是关键字(return 等)或运算符。
|
|
465
|
+
// 若不区分泛型,`FC<Props>` 的 `Props>` 会被当作未闭合标签吞掉其后内容,
|
|
466
|
+
// 导致该行之后的所有 JSX 文本(裸中文文案)全部漏检。
|
|
467
|
+
let previous = offset - 1;
|
|
468
|
+
while (previous >= 0 && /\s/.test(content[previous]))
|
|
469
|
+
previous--;
|
|
470
|
+
if (previous < 0)
|
|
471
|
+
return true;
|
|
472
|
+
const before = content[previous];
|
|
473
|
+
// `=> <div>` / `</div><span>`:`<` 紧跟 `>`/`=`/`(` 等,仍视为 JSX 标签
|
|
474
|
+
if (before === '>' || before === '=' || before === '(' || before === ',' || before === '&')
|
|
475
|
+
return true;
|
|
476
|
+
if (/[A-Za-z0-9_$]/.test(before)) {
|
|
477
|
+
// 回溯完整单词:是关键字(return/typeof…)则视为 JSX;否则按 TS 泛型跳过
|
|
478
|
+
let start = previous;
|
|
479
|
+
while (start >= 0 && /[A-Za-z0-9_$]/.test(content[start]))
|
|
480
|
+
start--;
|
|
481
|
+
const word = content.slice(start + 1, previous + 1);
|
|
482
|
+
return /^(return|typeof|instanceof|in|of|as|satisfies|case|do|else|yield|await|new|delete|void|throw)$/.test(word);
|
|
483
|
+
}
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
function findTagEnd(content, start) {
|
|
487
|
+
for (let index = start + 1; index < content.length; index++) {
|
|
488
|
+
if (content[index] === '{') {
|
|
489
|
+
const end = findBalancedEnd(content, index, '{', '}');
|
|
490
|
+
if (end < 0)
|
|
491
|
+
return -1;
|
|
492
|
+
index = end;
|
|
493
|
+
}
|
|
494
|
+
else if (content[index] === '>')
|
|
495
|
+
return index;
|
|
496
|
+
}
|
|
497
|
+
return -1;
|
|
498
|
+
}
|
|
499
|
+
function findBalancedEnd(content, start, open, close) {
|
|
500
|
+
let depth = 0;
|
|
501
|
+
for (let index = start; index < content.length; index++) {
|
|
502
|
+
if (content[index] === open)
|
|
503
|
+
depth++;
|
|
504
|
+
else if (content[index] === close) {
|
|
505
|
+
depth--;
|
|
506
|
+
if (depth === 0)
|
|
507
|
+
return index;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return -1;
|
|
511
|
+
}
|
|
512
|
+
function maskMarkupComments(content) {
|
|
513
|
+
return content
|
|
514
|
+
.replace(/<!--[\s\S]*?-->/g, (item) => '\0'.repeat(item.length))
|
|
515
|
+
.replace(/<%--[\s\S]*?--%>/g, (item) => '\0'.repeat(item.length))
|
|
516
|
+
.replace(/<%#[\s\S]*?%>/g, (item) => '\0'.repeat(item.length));
|
|
517
|
+
}
|
|
518
|
+
function scanMarkupAttributes(context) {
|
|
519
|
+
const issues = [];
|
|
520
|
+
for (const tag of context.content.matchAll(/<[^>]+>/g)) {
|
|
521
|
+
const tagText = tag[0];
|
|
522
|
+
const tagOffset = tag.index ?? 0;
|
|
523
|
+
for (const attribute of tagText.matchAll(/([:@\w.-]+)\s*=\s*(["'])([\s\S]*?)\2/g)) {
|
|
524
|
+
const name = attribute[1].toLowerCase();
|
|
525
|
+
const value = attribute[3];
|
|
526
|
+
if (!HAS_CJK.test(value))
|
|
527
|
+
continue;
|
|
528
|
+
const offset = tagOffset + (attribute.index ?? 0);
|
|
529
|
+
const position = positionAt(context.content, offset);
|
|
530
|
+
const term = normalizeSpace(decodeHtmlEntities(value));
|
|
531
|
+
issues.push(createRawIssue(context, position, term, 'markup', 'no-i18n', `词条【${term}】没有国际化`));
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return issues;
|
|
535
|
+
}
|
|
536
|
+
function scanJavaScriptStrings(content) {
|
|
537
|
+
const output = content.split('');
|
|
538
|
+
const tokens = scanJavaScriptRange(content, 0, content.length, output);
|
|
539
|
+
return { tokens, masked: output.join('') };
|
|
540
|
+
}
|
|
541
|
+
function scanJavaScriptDirectives(content) {
|
|
542
|
+
const directives = {
|
|
543
|
+
fileDisabled: false,
|
|
544
|
+
disabledLines: new Set(),
|
|
545
|
+
};
|
|
546
|
+
let index = 0;
|
|
547
|
+
while (index < content.length) {
|
|
548
|
+
const char = content[index];
|
|
549
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
550
|
+
index = skipJavaScriptLiteral(content, index);
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (char === '/' && content[index + 1] === '/') {
|
|
554
|
+
const end = content.indexOf('\n', index);
|
|
555
|
+
const stop = end >= 0 ? end : content.length;
|
|
556
|
+
applyCommentDirective(content, content.slice(index, stop), index, directives);
|
|
557
|
+
index = stop;
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
if (char === '/' && content[index + 1] === '*') {
|
|
561
|
+
const end = content.indexOf('*/', index + 2);
|
|
562
|
+
const stop = end >= 0 ? end + 2 : content.length;
|
|
563
|
+
applyCommentDirective(content, content.slice(index, stop), index, directives);
|
|
564
|
+
index = stop;
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
index++;
|
|
568
|
+
}
|
|
569
|
+
return directives;
|
|
570
|
+
}
|
|
571
|
+
function applyCommentDirective(content, comment, start, directives) {
|
|
572
|
+
const normalized = comment
|
|
573
|
+
.replace(/^\/[/*]/, '')
|
|
574
|
+
.replace(/\*\//g, '')
|
|
575
|
+
.replace(/\*/g, ' ')
|
|
576
|
+
.trim();
|
|
577
|
+
const match = /(?:^|\s)eevee-i18n-disable(-next-line|-file)?(?![\w-])/.exec(normalized);
|
|
578
|
+
if (!match)
|
|
579
|
+
return;
|
|
580
|
+
const line = positionAt(content, start).line;
|
|
581
|
+
if (match[1] === '-file')
|
|
582
|
+
directives.fileDisabled = true;
|
|
583
|
+
else if (match[1] === '-next-line')
|
|
584
|
+
directives.disabledLines.add(line + 1);
|
|
585
|
+
else
|
|
586
|
+
directives.disabledLines.add(line);
|
|
587
|
+
}
|
|
588
|
+
function skipJavaScriptLiteral(content, start) {
|
|
589
|
+
const quote = content[start];
|
|
590
|
+
let index = start + 1;
|
|
591
|
+
while (index < content.length) {
|
|
592
|
+
const char = content[index];
|
|
593
|
+
if (char === '\\') {
|
|
594
|
+
index += 2;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (quote !== '`' && char === '\n')
|
|
598
|
+
return index;
|
|
599
|
+
if (char === quote)
|
|
600
|
+
return index + 1;
|
|
601
|
+
if (quote === '`' && char === '$' && content[index + 1] === '{') {
|
|
602
|
+
const expressionEnd = findTemplateExpressionEnd(content, index + 2, content.length);
|
|
603
|
+
if (expressionEnd < 0)
|
|
604
|
+
return content.length;
|
|
605
|
+
index = expressionEnd + 1;
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
index++;
|
|
609
|
+
}
|
|
610
|
+
return index;
|
|
611
|
+
}
|
|
612
|
+
function scanJavaScriptRange(content, rangeStart, rangeEnd, output) {
|
|
613
|
+
const tokens = [];
|
|
614
|
+
let index = rangeStart;
|
|
615
|
+
while (index < rangeEnd) {
|
|
616
|
+
const char = content[index];
|
|
617
|
+
if (char === '/' && content[index + 1] === '/') {
|
|
618
|
+
while (index < rangeEnd && content[index] !== '\n')
|
|
619
|
+
output[index++] = ' ';
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
if (char === '/' && content[index + 1] === '*') {
|
|
623
|
+
const end = content.indexOf('*/', index + 2);
|
|
624
|
+
const stop = Math.min(end >= 0 ? end + 2 : content.length, rangeEnd);
|
|
625
|
+
while (index < stop)
|
|
626
|
+
output[index++] = ' ';
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
if (char === '"' || char === "'") {
|
|
630
|
+
const start = index++;
|
|
631
|
+
let value = '';
|
|
632
|
+
while (index < rangeEnd) {
|
|
633
|
+
const current = content[index];
|
|
634
|
+
if (current === '\\') {
|
|
635
|
+
value += decodeStringEscape(content, index);
|
|
636
|
+
output[index++] = ' ';
|
|
637
|
+
if (index < rangeEnd)
|
|
638
|
+
output[index++] = ' ';
|
|
639
|
+
continue;
|
|
640
|
+
}
|
|
641
|
+
if (current === char || current === '\n')
|
|
642
|
+
break;
|
|
643
|
+
value += current;
|
|
644
|
+
output[index++] = ' ';
|
|
645
|
+
}
|
|
646
|
+
if (index < rangeEnd && content[index] === char)
|
|
647
|
+
output[index++] = ' ';
|
|
648
|
+
const position = positionAt(content, start);
|
|
649
|
+
tokens.push({ value, start, end: index, line: position.line, column: position.column, template: false });
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
if (char === '`') {
|
|
653
|
+
index++;
|
|
654
|
+
let quasiStart = index;
|
|
655
|
+
let value = '';
|
|
656
|
+
const flushQuasi = (end) => {
|
|
657
|
+
if (!value)
|
|
658
|
+
return;
|
|
659
|
+
const position = positionAt(content, quasiStart);
|
|
660
|
+
tokens.push({ value, start: quasiStart, end, line: position.line, column: position.column, template: true });
|
|
661
|
+
value = '';
|
|
662
|
+
};
|
|
663
|
+
while (index < rangeEnd) {
|
|
664
|
+
const current = content[index];
|
|
665
|
+
if (current === '\\') {
|
|
666
|
+
value += decodeStringEscape(content, index);
|
|
667
|
+
output[index++] = ' ';
|
|
668
|
+
if (index < rangeEnd)
|
|
669
|
+
output[index++] = ' ';
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
if (current === '`') {
|
|
673
|
+
flushQuasi(index);
|
|
674
|
+
output[index++] = ' ';
|
|
675
|
+
quasiStart = index;
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
if (current === '$' && content[index + 1] === '{') {
|
|
679
|
+
flushQuasi(index);
|
|
680
|
+
// 保留 ${ 与 },让 JSX 扫描仍能识别完整表达式;只清空模板文本。
|
|
681
|
+
output[index++] = '$';
|
|
682
|
+
output[index++] = '{';
|
|
683
|
+
const expressionEnd = findTemplateExpressionEnd(content, index, rangeEnd);
|
|
684
|
+
if (expressionEnd < 0)
|
|
685
|
+
break;
|
|
686
|
+
tokens.push(...scanJavaScriptRange(content, index, expressionEnd, output));
|
|
687
|
+
index = expressionEnd;
|
|
688
|
+
output[index++] = '}';
|
|
689
|
+
quasiStart = index;
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
value += current;
|
|
693
|
+
output[index++] = ' ';
|
|
694
|
+
}
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
index++;
|
|
698
|
+
}
|
|
699
|
+
return tokens;
|
|
700
|
+
}
|
|
701
|
+
function decodeStringEscape(content, offset) {
|
|
702
|
+
const escaped = content[offset + 1];
|
|
703
|
+
if (escaped === 'n')
|
|
704
|
+
return '\n';
|
|
705
|
+
if (escaped === 't')
|
|
706
|
+
return '\t';
|
|
707
|
+
if (escaped === '\n')
|
|
708
|
+
return '';
|
|
709
|
+
return escaped ?? '';
|
|
710
|
+
}
|
|
711
|
+
function findTemplateExpressionEnd(content, start, end) {
|
|
712
|
+
let depth = 0;
|
|
713
|
+
let quote;
|
|
714
|
+
for (let index = start; index < end; index++) {
|
|
715
|
+
const char = content[index];
|
|
716
|
+
if (quote) {
|
|
717
|
+
if (char === '\\')
|
|
718
|
+
index++;
|
|
719
|
+
else if (char === quote)
|
|
720
|
+
quote = undefined;
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
724
|
+
quote = char;
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
if (char === '{')
|
|
728
|
+
depth++;
|
|
729
|
+
else if (char === '}') {
|
|
730
|
+
if (depth === 0)
|
|
731
|
+
return index;
|
|
732
|
+
depth--;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return -1;
|
|
736
|
+
}
|
|
737
|
+
function scanJava(context) {
|
|
738
|
+
const issues = [];
|
|
739
|
+
const annotationRanges = findJavaAnnotationRanges(context.content);
|
|
740
|
+
const directives = scanJavaDirectives(context.content);
|
|
741
|
+
if (directives.fileDisabled)
|
|
742
|
+
return [];
|
|
743
|
+
const length = context.content.length;
|
|
744
|
+
let index = 0;
|
|
745
|
+
while (index < length) {
|
|
746
|
+
const char = context.content[index];
|
|
747
|
+
if (char === '/' && context.content[index + 1] === '/') {
|
|
748
|
+
while (index < length && context.content[index] !== '\n')
|
|
749
|
+
index++;
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (char === '/' && context.content[index + 1] === '*') {
|
|
753
|
+
const end = context.content.indexOf('*/', index + 2);
|
|
754
|
+
index = end >= 0 ? end + 2 : length;
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
if (char === '"') {
|
|
758
|
+
const isTextBlock = context.content.slice(index, index + 3) === '"""';
|
|
759
|
+
const start = index;
|
|
760
|
+
index += isTextBlock ? 3 : 1;
|
|
761
|
+
let value = '';
|
|
762
|
+
while (index < length) {
|
|
763
|
+
if (isTextBlock && context.content.slice(index, index + 3) === '"""') {
|
|
764
|
+
index += 3;
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
if (!isTextBlock && context.content[index] === '"') {
|
|
768
|
+
index++;
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
if (context.content[index] === '\\') {
|
|
772
|
+
const escaped = context.content[index + 1];
|
|
773
|
+
value += escaped === 'n' ? '\n' : escaped === 't' ? '\t' : escaped;
|
|
774
|
+
index += 2;
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
if (!isTextBlock && context.content[index] === '\n')
|
|
778
|
+
break;
|
|
779
|
+
value += context.content[index++];
|
|
780
|
+
}
|
|
781
|
+
const term = normalizeSpace(value);
|
|
782
|
+
if (shouldCheckTerm(term, context.config) &&
|
|
783
|
+
!directives.disabledLines.has(positionAt(context.content, start).line) &&
|
|
784
|
+
(context.config.checkAnnotations || !isInRanges(start, annotationRanges))) {
|
|
785
|
+
const position = positionAt(context.content, start);
|
|
786
|
+
issues.push(createRawIssue(context, position, term, 'java', 'no-i18n', `词条【${term}】没有国际化`));
|
|
787
|
+
}
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
index++;
|
|
791
|
+
}
|
|
792
|
+
return deduplicate(issues);
|
|
793
|
+
}
|
|
794
|
+
function scanJavaDirectives(content) {
|
|
795
|
+
const directives = {
|
|
796
|
+
fileDisabled: false,
|
|
797
|
+
disabledLines: new Set(),
|
|
798
|
+
};
|
|
799
|
+
let index = 0;
|
|
800
|
+
while (index < content.length) {
|
|
801
|
+
const char = content[index];
|
|
802
|
+
if (char === '/' && content[index + 1] === '/') {
|
|
803
|
+
const end = content.indexOf('\n', index);
|
|
804
|
+
const stop = end >= 0 ? end : content.length;
|
|
805
|
+
applyCommentDirective(content, content.slice(index, stop), index, directives);
|
|
806
|
+
index = stop;
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
if (char === '/' && content[index + 1] === '*') {
|
|
810
|
+
const end = content.indexOf('*/', index + 2);
|
|
811
|
+
const stop = end >= 0 ? end + 2 : content.length;
|
|
812
|
+
applyCommentDirective(content, content.slice(index, stop), index, directives);
|
|
813
|
+
index = stop;
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
if (char === '"') {
|
|
817
|
+
const isTextBlock = content.slice(index, index + 3) === '"""';
|
|
818
|
+
const terminator = isTextBlock ? '"""' : '"';
|
|
819
|
+
index += isTextBlock ? 3 : 1;
|
|
820
|
+
while (index < content.length) {
|
|
821
|
+
if (content[index] === '\\') {
|
|
822
|
+
index += 2;
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
if (content.slice(index, index + terminator.length) === terminator) {
|
|
826
|
+
index += terminator.length;
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
if (!isTextBlock && content[index] === '\n')
|
|
830
|
+
break;
|
|
831
|
+
index++;
|
|
832
|
+
}
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
if (char === '\'') {
|
|
836
|
+
index++;
|
|
837
|
+
if (content[index] === '\\')
|
|
838
|
+
index++;
|
|
839
|
+
if (index < content.length)
|
|
840
|
+
index++;
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
index++;
|
|
844
|
+
}
|
|
845
|
+
return directives;
|
|
846
|
+
}
|
|
847
|
+
function findJavaAnnotationRanges(content) {
|
|
848
|
+
const ranges = [];
|
|
849
|
+
for (const annotation of content.matchAll(/@[A-Za-z_$][\w$.]*/g)) {
|
|
850
|
+
let index = (annotation.index ?? 0) + annotation[0].length;
|
|
851
|
+
while (index < content.length && /\s/.test(content[index]))
|
|
852
|
+
index++;
|
|
853
|
+
if (content[index] !== '(')
|
|
854
|
+
continue;
|
|
855
|
+
const start = annotation.index ?? 0;
|
|
856
|
+
let depth = 0;
|
|
857
|
+
for (; index < content.length; index++) {
|
|
858
|
+
const char = content[index];
|
|
859
|
+
if (char === '"') {
|
|
860
|
+
index++;
|
|
861
|
+
while (index < content.length && content[index] !== '"') {
|
|
862
|
+
if (content[index] === '\\')
|
|
863
|
+
index++;
|
|
864
|
+
index++;
|
|
865
|
+
}
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
if (char === '(')
|
|
869
|
+
depth++;
|
|
870
|
+
else if (char === ')') {
|
|
871
|
+
depth--;
|
|
872
|
+
if (depth === 0) {
|
|
873
|
+
ranges.push([start, index + 1]);
|
|
874
|
+
break;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return ranges;
|
|
880
|
+
}
|
|
881
|
+
function isInRanges(offset, ranges) {
|
|
882
|
+
return ranges.some(([start, end]) => offset >= start && offset < end);
|
|
883
|
+
}
|
|
884
|
+
function shouldCheckTerm(value, config) {
|
|
885
|
+
const term = value.trim();
|
|
886
|
+
if (!term)
|
|
887
|
+
return false;
|
|
888
|
+
return !config.excludeAscii || HAS_CJK.test(term);
|
|
889
|
+
}
|
|
890
|
+
function isIgnoredJavaScriptToken(token, content, config) {
|
|
891
|
+
// 普通字符串 token 的 start/end 包含引号;模板 quasi token 不包含反引号。
|
|
892
|
+
const contentStart = token.start;
|
|
893
|
+
const before = previousNonWhitespace(content, contentStart);
|
|
894
|
+
const after = nextNonWhitespace(content, token.end);
|
|
895
|
+
if (after === ':' && (before === '{' || before === ','))
|
|
896
|
+
return true;
|
|
897
|
+
if (before === '.' || after === '.' || (before === '[' && after === ']'))
|
|
898
|
+
return true;
|
|
899
|
+
if (isImportOrExportSource(content, token.start))
|
|
900
|
+
return true;
|
|
901
|
+
const propertyName = propertyNameForValue(content, contentStart);
|
|
902
|
+
if (propertyName && config.excludeProperties.includes(propertyName))
|
|
903
|
+
return true;
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
function isImportOrExportSource(content, tokenStart) {
|
|
907
|
+
const statementStart = Math.max(0, ...[';', '\n', '{', '}'].map((item) => content.lastIndexOf(item, Math.max(0, tokenStart - 1))));
|
|
908
|
+
const prefix = content.slice(statementStart, tokenStart);
|
|
909
|
+
return /(?:^|\n)\s*(?:import|export)\b[^;\n]*\bfrom\s*$/.test(prefix);
|
|
910
|
+
}
|
|
911
|
+
function propertyNameForValue(content, tokenStart) {
|
|
912
|
+
let index = tokenStart - 1;
|
|
913
|
+
while (index >= 0 && /\s/.test(content[index]))
|
|
914
|
+
index--;
|
|
915
|
+
if (index < 0 || content[index] !== ':')
|
|
916
|
+
return null;
|
|
917
|
+
index--;
|
|
918
|
+
while (index >= 0 && /\s/.test(content[index]))
|
|
919
|
+
index--;
|
|
920
|
+
const end = index;
|
|
921
|
+
while (index >= 0 && /[\w$]/.test(content[index]))
|
|
922
|
+
index--;
|
|
923
|
+
const name = content.slice(index + 1, end + 1);
|
|
924
|
+
return /^[\w$]+$/.test(name) ? name : null;
|
|
925
|
+
}
|
|
926
|
+
function previousNonWhitespace(content, index) {
|
|
927
|
+
for (let i = index - 1; i >= 0; i--)
|
|
928
|
+
if (!/\s/.test(content[i]))
|
|
929
|
+
return content[i];
|
|
930
|
+
return '';
|
|
931
|
+
}
|
|
932
|
+
function nextNonWhitespace(content, index) {
|
|
933
|
+
for (let i = index; i < content.length; i++)
|
|
934
|
+
if (!/\s/.test(content[i]))
|
|
935
|
+
return content[i];
|
|
936
|
+
return '';
|
|
937
|
+
}
|
|
938
|
+
function enclosingCallName(content, tokenStart) {
|
|
939
|
+
let depth = 0;
|
|
940
|
+
for (let index = tokenStart - 1; index >= 0; index--) {
|
|
941
|
+
const char = content[index];
|
|
942
|
+
if (char === ')')
|
|
943
|
+
depth++;
|
|
944
|
+
else if (char === '(') {
|
|
945
|
+
if (depth > 0) {
|
|
946
|
+
depth--;
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
return identifierBefore(content, index);
|
|
950
|
+
}
|
|
951
|
+
else if (char === ';' && depth === 0) {
|
|
952
|
+
return null;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return null;
|
|
956
|
+
}
|
|
957
|
+
function identifierBefore(content, parenthesisIndex) {
|
|
958
|
+
let index = parenthesisIndex - 1;
|
|
959
|
+
while (index >= 0 && /\s/.test(content[index]))
|
|
960
|
+
index--;
|
|
961
|
+
const end = index;
|
|
962
|
+
while (index >= 0 && /[\w$.]/.test(content[index]))
|
|
963
|
+
index--;
|
|
964
|
+
const identifier = content.slice(index + 1, end + 1);
|
|
965
|
+
return identifier || null;
|
|
966
|
+
}
|
|
967
|
+
function isMessageCall(value) {
|
|
968
|
+
if (!value)
|
|
969
|
+
return false;
|
|
970
|
+
return /(?:^|\.)(?:formatMessage|getMessage)$/.test(value);
|
|
971
|
+
}
|
|
972
|
+
function isTrackCall(value) {
|
|
973
|
+
if (!value)
|
|
974
|
+
return false;
|
|
975
|
+
return /(?:^|\.)(?:trackEvent|track)$/.test(value);
|
|
976
|
+
}
|
|
977
|
+
function isDirectCallArgument(content, tokenStart, tokenEnd) {
|
|
978
|
+
const open = nearestEnclosingCallOpen(content, tokenStart);
|
|
979
|
+
if (open < 0)
|
|
980
|
+
return false;
|
|
981
|
+
const prefix = content.slice(open + 1, tokenStart);
|
|
982
|
+
if (prefix.trim() && !/^[\s,]*\.\.\.$/.test(prefix))
|
|
983
|
+
return false;
|
|
984
|
+
for (let index = tokenEnd; index < content.length; index++) {
|
|
985
|
+
const char = content[index];
|
|
986
|
+
if (char === ',')
|
|
987
|
+
return true;
|
|
988
|
+
if (char === ')')
|
|
989
|
+
return true;
|
|
990
|
+
if (/\S/.test(char))
|
|
991
|
+
return false;
|
|
992
|
+
}
|
|
993
|
+
return false;
|
|
994
|
+
}
|
|
995
|
+
function nearestEnclosingCallOpen(content, tokenStart) {
|
|
996
|
+
let depth = 0;
|
|
997
|
+
for (let index = tokenStart - 1; index >= 0; index--) {
|
|
998
|
+
const char = content[index];
|
|
999
|
+
if (char === ')')
|
|
1000
|
+
depth++;
|
|
1001
|
+
else if (char === '(') {
|
|
1002
|
+
if (depth > 0) {
|
|
1003
|
+
depth--;
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
return index;
|
|
1007
|
+
}
|
|
1008
|
+
else if (char === ';' && depth === 0)
|
|
1009
|
+
return -1;
|
|
1010
|
+
}
|
|
1011
|
+
return -1;
|
|
1012
|
+
}
|
|
1013
|
+
function checkRegisteredTerm(token, context) {
|
|
1014
|
+
const relative = toPosix(path.relative(context.projectRoot, context.file));
|
|
1015
|
+
const tags = tagsForFile(relative, context.config.tagMatchMap, context.projectRoot);
|
|
1016
|
+
if (!tags.length)
|
|
1017
|
+
return [];
|
|
1018
|
+
const registeredTags = context.dictionary.terms.get(token.value);
|
|
1019
|
+
if (!registeredTags) {
|
|
1020
|
+
return [createIssue(context, token, 'javascript', 'not-registered', `词条【${token.value}】未在平台登记`)];
|
|
1021
|
+
}
|
|
1022
|
+
if (!tags.some((tag) => registeredTags.has(tag))) {
|
|
1023
|
+
return [createIssue(context, token, 'javascript', 'tag-mismatch', `词条【${token.value}】标签不匹配`)];
|
|
1024
|
+
}
|
|
1025
|
+
return [];
|
|
1026
|
+
}
|
|
1027
|
+
function tagsForFile(relativeFile, tagMatchMap, projectRoot) {
|
|
1028
|
+
for (const [pattern, tags] of Object.entries(tagMatchMap)) {
|
|
1029
|
+
if (matchesGlob(relativeFile, pattern, projectRoot))
|
|
1030
|
+
return tags;
|
|
1031
|
+
}
|
|
1032
|
+
return [];
|
|
1033
|
+
}
|
|
1034
|
+
function createIssue(context, token, language, type, message) {
|
|
1035
|
+
return {
|
|
1036
|
+
type,
|
|
1037
|
+
file: toPosix(path.relative(context.projectRoot, context.file)),
|
|
1038
|
+
line: token.line,
|
|
1039
|
+
column: token.column,
|
|
1040
|
+
term: token.value,
|
|
1041
|
+
message,
|
|
1042
|
+
language,
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
function createRawIssue(context, position, term, language, type, message) {
|
|
1046
|
+
return {
|
|
1047
|
+
type,
|
|
1048
|
+
file: toPosix(path.relative(context.projectRoot, context.file)),
|
|
1049
|
+
line: position.line,
|
|
1050
|
+
column: position.column,
|
|
1051
|
+
term,
|
|
1052
|
+
message,
|
|
1053
|
+
language,
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
function positionAt(content, offset) {
|
|
1057
|
+
let line = 1;
|
|
1058
|
+
let lineStart = 0;
|
|
1059
|
+
for (let index = 0; index < offset; index++) {
|
|
1060
|
+
if (content[index] === '\n') {
|
|
1061
|
+
line++;
|
|
1062
|
+
lineStart = index + 1;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
return { line, column: offset - lineStart + 1 };
|
|
1066
|
+
}
|
|
1067
|
+
function extensionOf(file) {
|
|
1068
|
+
return path.extname(file).toLowerCase();
|
|
1069
|
+
}
|
|
1070
|
+
function extensionAllowsMarkup(extension) {
|
|
1071
|
+
return extension === '.jsx' || extension === '.tsx' || extension === '.vue';
|
|
1072
|
+
}
|
|
1073
|
+
function decodeHtmlEntities(value) {
|
|
1074
|
+
return value
|
|
1075
|
+
.replace(/ /g, ' ')
|
|
1076
|
+
.replace(/</g, '<')
|
|
1077
|
+
.replace(/>/g, '>')
|
|
1078
|
+
.replace(/"/g, '"')
|
|
1079
|
+
.replace(/'/g, "'")
|
|
1080
|
+
.replace(/&/g, '&');
|
|
1081
|
+
}
|
|
1082
|
+
function normalizeSpace(value) {
|
|
1083
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
1084
|
+
}
|
|
1085
|
+
function deduplicate(issues) {
|
|
1086
|
+
const seen = new Set();
|
|
1087
|
+
return issues.filter((issue) => {
|
|
1088
|
+
const key = `${issue.file}:${issue.line}:${issue.column}:${issue.type}:${issue.term}`;
|
|
1089
|
+
if (seen.has(key))
|
|
1090
|
+
return false;
|
|
1091
|
+
seen.add(key);
|
|
1092
|
+
return true;
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
function toPosix(value) {
|
|
1096
|
+
return value.replace(/\\/g, '/');
|
|
1097
|
+
}
|
|
1098
|
+
function normalizeGlob(value) {
|
|
1099
|
+
const normalized = toPosix(value).replace(/^\.\//, '');
|
|
1100
|
+
return normalized.endsWith('/') ? normalized : `${normalized}/`;
|
|
1101
|
+
}
|
|
1102
|
+
export function matchesAny(file, patterns, root) {
|
|
1103
|
+
return patterns.some((pattern) => matchesGlob(file, pattern, root));
|
|
1104
|
+
}
|
|
1105
|
+
export function matchesGlob(file, patternInput, root) {
|
|
1106
|
+
let pattern = normalizeGlobPattern(patternInput, root);
|
|
1107
|
+
if (!pattern)
|
|
1108
|
+
return false;
|
|
1109
|
+
if (pattern.endsWith('/'))
|
|
1110
|
+
pattern += '**';
|
|
1111
|
+
return expandBraces(pattern).some((candidate) => {
|
|
1112
|
+
const regex = new RegExp(`^${globToRegex(candidate)}$`, 'i');
|
|
1113
|
+
return regex.test(file);
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
function normalizeGlobPattern(pattern, root) {
|
|
1117
|
+
let value = toPosix(pattern).replace(/^\.\//, '');
|
|
1118
|
+
if (path.isAbsolute(value) && root)
|
|
1119
|
+
value = toPosix(path.relative(root, value));
|
|
1120
|
+
if (value.startsWith('./'))
|
|
1121
|
+
value = value.slice(2);
|
|
1122
|
+
return value;
|
|
1123
|
+
}
|
|
1124
|
+
function expandBraces(pattern) {
|
|
1125
|
+
const open = pattern.indexOf('{');
|
|
1126
|
+
if (open < 0)
|
|
1127
|
+
return [pattern];
|
|
1128
|
+
let depth = 0;
|
|
1129
|
+
let close = -1;
|
|
1130
|
+
for (let index = open; index < pattern.length; index++) {
|
|
1131
|
+
if (pattern[index] === '{')
|
|
1132
|
+
depth++;
|
|
1133
|
+
else if (pattern[index] === '}') {
|
|
1134
|
+
depth--;
|
|
1135
|
+
if (depth === 0) {
|
|
1136
|
+
close = index;
|
|
1137
|
+
break;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
if (close < 0)
|
|
1142
|
+
return [pattern];
|
|
1143
|
+
const before = pattern.slice(0, open);
|
|
1144
|
+
const after = pattern.slice(close + 1);
|
|
1145
|
+
return pattern.slice(open + 1, close)
|
|
1146
|
+
.split(',')
|
|
1147
|
+
.flatMap((part) => expandBraces(`${before}${part}${after}`));
|
|
1148
|
+
}
|
|
1149
|
+
function globToRegex(pattern) {
|
|
1150
|
+
let output = '';
|
|
1151
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
1152
|
+
const char = pattern[index];
|
|
1153
|
+
if (char === '*' && pattern[index + 1] === '*') {
|
|
1154
|
+
if (pattern[index + 2] === '/') {
|
|
1155
|
+
output += '(?:.*/)?';
|
|
1156
|
+
index += 2;
|
|
1157
|
+
}
|
|
1158
|
+
else {
|
|
1159
|
+
output += '.*';
|
|
1160
|
+
index++;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
else if (char === '*')
|
|
1164
|
+
output += '[^/]*';
|
|
1165
|
+
else if (char === '?')
|
|
1166
|
+
output += '[^/]';
|
|
1167
|
+
else
|
|
1168
|
+
output += char.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
|
|
1169
|
+
}
|
|
1170
|
+
return output;
|
|
1171
|
+
}
|
|
1172
|
+
//# sourceMappingURL=i18n-scanner.js.map
|