i18ntk 4.1.0 → 4.2.1
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/CHANGELOG.md +64 -5
- package/README.md +73 -17
- package/SECURITY.md +10 -4
- package/main/i18ntk-analyze.js +10 -20
- package/main/i18ntk-backup.js +106 -44
- package/main/i18ntk-init.js +153 -157
- package/main/i18ntk-setup.js +36 -13
- package/main/i18ntk-sizing.js +44 -27
- package/main/i18ntk-translate.js +311 -41
- package/main/i18ntk-usage.js +272 -103
- package/main/i18ntk-validate.js +38 -31
- package/main/manage/commands/AnalyzeCommand.js +7 -17
- package/main/manage/commands/CommandRouter.js +6 -6
- package/main/manage/commands/SizingCommand.js +5 -2
- package/main/manage/commands/TranslateCommand.js +73 -56
- package/main/manage/commands/ValidateCommand.js +58 -26
- package/main/manage/index.js +11 -42
- package/main/manage/managers/InteractiveMenu.js +11 -40
- package/main/manage/services/InitService.js +114 -118
- package/main/manage/services/UsageService.js +247 -96
- package/package.json +19 -14
- package/runtime/enhanced.d.ts +5 -5
- package/runtime/enhanced.js +49 -25
- package/runtime/i18ntk.d.ts +30 -7
- package/runtime/index.d.ts +48 -19
- package/runtime/index.js +175 -90
- package/settings/settings-cli.js +115 -38
- package/settings/settings-manager.js +24 -6
- package/ui-locales/de.json +192 -11
- package/ui-locales/en.json +182 -8
- package/ui-locales/es.json +193 -12
- package/ui-locales/fr.json +189 -8
- package/ui-locales/ja.json +190 -8
- package/ui-locales/ru.json +191 -9
- package/ui-locales/zh.json +194 -9
- package/utils/cli-helper.js +8 -12
- package/utils/config-helper.js +1 -1
- package/utils/config-manager.js +8 -6
- package/utils/localized-confirm.js +55 -0
- package/utils/menu-layout.js +41 -0
- package/utils/report-writer.js +110 -0
- package/utils/security.js +15 -22
- package/utils/translate/api.js +31 -3
- package/utils/translate/placeholder.js +42 -1
- package/utils/translate/report.js +32 -4
- package/utils/translate/safe-network.js +24 -4
- package/utils/usage-insights.js +435 -0
- package/utils/usage-source.js +50 -0
- package/utils/watch-locales.js +1 -8
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
const KEY_BOUNDARY = /[A-Za-z0-9_.:*-]/;
|
|
4
|
+
const HUMAN_TEXT_MIN = 3;
|
|
5
|
+
const HUMAN_TEXT_MAX = 120;
|
|
6
|
+
const MAX_DYNAMIC_EXPANSIONS = 25;
|
|
7
|
+
|
|
8
|
+
function stripComments(content) {
|
|
9
|
+
return String(content || '')
|
|
10
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
11
|
+
.replace(/(^|[^:])\/\/.*$/gm, '$1');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function findLineColumn(content, index) {
|
|
15
|
+
const before = content.slice(0, Math.max(0, index));
|
|
16
|
+
const lines = before.split(/\r?\n/);
|
|
17
|
+
return {
|
|
18
|
+
line: lines.length,
|
|
19
|
+
column: lines[lines.length - 1].length + 1,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isBoundaryAt(content, index) {
|
|
24
|
+
if (index < 0 || index >= content.length) return true;
|
|
25
|
+
return !KEY_BOUNDARY.test(content[index]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function findLiteralKeyReferences(content, availableKeys) {
|
|
29
|
+
const source = stripComments(content);
|
|
30
|
+
const references = [];
|
|
31
|
+
|
|
32
|
+
for (const key of Array.from(availableKeys || []).sort((a, b) => b.length - a.length)) {
|
|
33
|
+
if (!key || typeof key !== 'string') continue;
|
|
34
|
+
let fromIndex = 0;
|
|
35
|
+
|
|
36
|
+
while (fromIndex < source.length) {
|
|
37
|
+
const index = source.indexOf(key, fromIndex);
|
|
38
|
+
if (index === -1) break;
|
|
39
|
+
|
|
40
|
+
const beforeOk = isBoundaryAt(source, index - 1);
|
|
41
|
+
const afterOk = isBoundaryAt(source, index + key.length);
|
|
42
|
+
const lineStart = source.lastIndexOf('\n', index) + 1;
|
|
43
|
+
const beforeOnLine = source.slice(lineStart, index);
|
|
44
|
+
const looksLikeObjectValue = /:\s*['"`]?$/.test(beforeOnLine);
|
|
45
|
+
if (beforeOk && afterOk && !looksLikeObjectValue) {
|
|
46
|
+
references.push({
|
|
47
|
+
key,
|
|
48
|
+
matchType: 'literal',
|
|
49
|
+
...findLineColumn(source, index),
|
|
50
|
+
});
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fromIndex = index + key.length;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return references;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseStringList(raw) {
|
|
62
|
+
const values = [];
|
|
63
|
+
const source = String(raw || '');
|
|
64
|
+
const stringPattern = /['"`]([^'"`\r\n$]+)['"`]/g;
|
|
65
|
+
let match;
|
|
66
|
+
|
|
67
|
+
while ((match = stringPattern.exec(source)) !== null) {
|
|
68
|
+
const value = String(match[1] || '').trim();
|
|
69
|
+
if (/^[A-Za-z0-9_.:-]+$/.test(value)) values.push(value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return values;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseObjectKeyValues(raw) {
|
|
76
|
+
const entries = [];
|
|
77
|
+
const source = String(raw || '');
|
|
78
|
+
const propertyPattern = /(?:^|[,{])\s*['"]?([A-Za-z_$][\w$-]*)['"]?\s*:\s*['"`]([^'"`\r\n$]+)['"`]/g;
|
|
79
|
+
let match;
|
|
80
|
+
|
|
81
|
+
while ((match = propertyPattern.exec(source)) !== null) {
|
|
82
|
+
const value = String(match[2] || '').trim();
|
|
83
|
+
if (/^[A-Za-z0-9_.:-]+$/.test(value)) {
|
|
84
|
+
entries.push([match[1], value]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return entries;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function collectSimpleBindings(content) {
|
|
92
|
+
const source = stripComments(content);
|
|
93
|
+
const bindings = new Map();
|
|
94
|
+
const declarationPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*([^;\n]+)/g;
|
|
95
|
+
let match;
|
|
96
|
+
|
|
97
|
+
while ((match = declarationPattern.exec(source)) !== null) {
|
|
98
|
+
const name = match[1];
|
|
99
|
+
const rawValue = String(match[2] || '').trim();
|
|
100
|
+
|
|
101
|
+
if (rawValue.startsWith('[') || rawValue.startsWith('{')) {
|
|
102
|
+
const values = parseStringList(rawValue);
|
|
103
|
+
if (values.length > 0 && values.length <= MAX_DYNAMIC_EXPANSIONS) {
|
|
104
|
+
bindings.set(name, values);
|
|
105
|
+
}
|
|
106
|
+
if (rawValue.startsWith('{')) {
|
|
107
|
+
for (const [property, value] of parseObjectKeyValues(rawValue)) {
|
|
108
|
+
bindings.set(`${name}.${property}`, [value]);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const values = parseStringList(rawValue);
|
|
115
|
+
if (values.length === 1 && /^['"`]/.test(rawValue)) {
|
|
116
|
+
bindings.set(name, values);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const iteratorPattern = /\b([A-Za-z_$][\w$]*)\s*\.\s*(?:map|forEach|filter|some|every)\s*\(\s*([A-Za-z_$][\w$]*)\s*=>/g;
|
|
121
|
+
while ((match = iteratorPattern.exec(source)) !== null) {
|
|
122
|
+
const sourceValues = bindings.get(match[1]);
|
|
123
|
+
if (sourceValues && sourceValues.length <= MAX_DYNAMIC_EXPANSIONS) {
|
|
124
|
+
bindings.set(match[2], sourceValues);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return bindings;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function expandTemplateLiteral(template, bindings) {
|
|
132
|
+
const parts = [];
|
|
133
|
+
let index = 0;
|
|
134
|
+
const expressionPattern = /\$\{\s*([A-Za-z_$][\w$]*)\s*\}/g;
|
|
135
|
+
let match;
|
|
136
|
+
|
|
137
|
+
while ((match = expressionPattern.exec(template)) !== null) {
|
|
138
|
+
parts.push([template.slice(index, match.index)]);
|
|
139
|
+
const values = bindings.get(match[1]);
|
|
140
|
+
if (!values || values.length === 0) return [];
|
|
141
|
+
parts.push(values);
|
|
142
|
+
index = match.index + match[0].length;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
parts.push([template.slice(index)]);
|
|
146
|
+
|
|
147
|
+
let expanded = [''];
|
|
148
|
+
for (const values of parts) {
|
|
149
|
+
const next = [];
|
|
150
|
+
for (const prefix of expanded) {
|
|
151
|
+
for (const value of values) {
|
|
152
|
+
next.push(prefix + value);
|
|
153
|
+
if (next.length > MAX_DYNAMIC_EXPANSIONS) return [];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
expanded = next;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return expanded.filter(key => /^[A-Za-z0-9_.:*-]+$/.test(key));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function expressionToAvailableKeys(expression, bindings, availableKeys) {
|
|
163
|
+
const available = availableKeys || new Set();
|
|
164
|
+
const source = String(expression || '').trim();
|
|
165
|
+
const results = [];
|
|
166
|
+
|
|
167
|
+
const literal = source.match(/^['"`]([^'"`\r\n$]+)['"`]$/);
|
|
168
|
+
if (literal && available.has(literal[1])) return [literal[1]];
|
|
169
|
+
|
|
170
|
+
const identifier = source.match(/^([A-Za-z_$][\w$]*)$/);
|
|
171
|
+
if (identifier) {
|
|
172
|
+
const values = bindings.get(identifier[1]) || [];
|
|
173
|
+
return values.filter(value => available.has(value));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const memberLookup = source.match(/^([A-Za-z_$][\w$]*)\s*\[[^\]]+\]$/);
|
|
177
|
+
if (memberLookup) {
|
|
178
|
+
const values = bindings.get(memberLookup[1]) || [];
|
|
179
|
+
return values.filter(value => available.has(value));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const propertyLookup = source.match(/^([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$-]*)$/);
|
|
183
|
+
if (propertyLookup) {
|
|
184
|
+
const values = bindings.get(`${propertyLookup[1]}.${propertyLookup[2]}`) || [];
|
|
185
|
+
return values.filter(value => available.has(value));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const ternary = source.match(/^[^?]+?\?\s*([^:]+)\s*:\s*(.+)$/);
|
|
189
|
+
if (ternary) {
|
|
190
|
+
return [
|
|
191
|
+
...expressionToAvailableKeys(ternary[1], bindings, available),
|
|
192
|
+
...expressionToAvailableKeys(ternary[2], bindings, available),
|
|
193
|
+
].filter((key, index, keys) => keys.indexOf(key) === index);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const template = source.match(/^`([^`]*)`$/);
|
|
197
|
+
if (template) {
|
|
198
|
+
return expandTemplateLiteral(template[1], bindings).filter(key => available.has(key));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return results;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function inferDynamicKeyReferences(content, availableKeys) {
|
|
205
|
+
const source = stripComments(content);
|
|
206
|
+
const bindings = collectSimpleBindings(source);
|
|
207
|
+
const references = [];
|
|
208
|
+
const seen = new Set();
|
|
209
|
+
const callPattern = /(?:\bt|\bi18n\.t|\$t|\btranslate)\s*\(\s*(`[^`]*`|['"][^'"\r\n]+['"]|[^,\)\r\n]+)/g;
|
|
210
|
+
let match;
|
|
211
|
+
|
|
212
|
+
while ((match = callPattern.exec(source)) !== null) {
|
|
213
|
+
const keys = expressionToAvailableKeys(match[1], bindings, availableKeys);
|
|
214
|
+
if (keys.length === 0) continue;
|
|
215
|
+
|
|
216
|
+
const location = findLineColumn(source, match.index);
|
|
217
|
+
for (const key of keys) {
|
|
218
|
+
if (seen.has(key)) continue;
|
|
219
|
+
seen.add(key);
|
|
220
|
+
references.push({
|
|
221
|
+
key,
|
|
222
|
+
matchType: match[1].startsWith('`') ? 'dynamic-template' : 'dynamic-variable',
|
|
223
|
+
...location,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return references;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function inferDynamicPrefix(expression) {
|
|
232
|
+
const source = String(expression || '').trim();
|
|
233
|
+
const template = source.match(/^`([^`]*)\$\{[^}]+\}/);
|
|
234
|
+
if (template && /^[A-Za-z0-9_.:-]+$/.test(template[1])) return template[1];
|
|
235
|
+
|
|
236
|
+
const concat = source.match(/^['"]([A-Za-z0-9_.:-]+)['"]\s*\+/);
|
|
237
|
+
if (concat) return concat[1];
|
|
238
|
+
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function findUnresolvedDynamicReferences(content, availableKeys) {
|
|
243
|
+
const source = stripComments(content);
|
|
244
|
+
const bindings = collectSimpleBindings(source);
|
|
245
|
+
const unresolved = [];
|
|
246
|
+
const seen = new Set();
|
|
247
|
+
const callPattern = /(?:\bt|\bi18n\.t|\$t|\btranslate)\s*\(\s*(`[^`]*`|['"][^'"\r\n]+['"]|[^,\)\r\n]+)/g;
|
|
248
|
+
let match;
|
|
249
|
+
|
|
250
|
+
while ((match = callPattern.exec(source)) !== null) {
|
|
251
|
+
const expression = String(match[1] || '').trim();
|
|
252
|
+
if (!expression || expressionToAvailableKeys(expression, bindings, availableKeys).length > 0) continue;
|
|
253
|
+
if (!/[`+$[\]?]/.test(expression) && !/^[A-Za-z_$][\w$]*$/.test(expression)) continue;
|
|
254
|
+
|
|
255
|
+
const location = findLineColumn(source, match.index);
|
|
256
|
+
const key = `${expression}\0${location.line}\0${location.column}`;
|
|
257
|
+
if (seen.has(key)) continue;
|
|
258
|
+
seen.add(key);
|
|
259
|
+
|
|
260
|
+
unresolved.push({
|
|
261
|
+
expression,
|
|
262
|
+
prefix: inferDynamicPrefix(expression),
|
|
263
|
+
reason: 'Could not resolve expression to literal translation keys without executing code.',
|
|
264
|
+
line: location.line,
|
|
265
|
+
column: location.column,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return unresolved;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function normalizeSegment(segment) {
|
|
273
|
+
return String(segment || '')
|
|
274
|
+
.replace(/\.(jsx?|tsx?|vue|svelte|html|py)$/i, '')
|
|
275
|
+
.replace(/^\[+|\]+$/g, '')
|
|
276
|
+
.replace(/^\(+|\)+$/g, '')
|
|
277
|
+
.replace(/[^A-Za-z0-9_-]/g, '')
|
|
278
|
+
.toLowerCase();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function deriveNamespaceCandidates(relativePath) {
|
|
282
|
+
const rawParts = String(relativePath || '').split(/[\\/]+/).map(normalizeSegment).filter(Boolean);
|
|
283
|
+
const ignored = new Set([
|
|
284
|
+
'src', 'app', 'pages', 'page', 'route', 'routes', 'index', 'layout',
|
|
285
|
+
'components', 'component', 'ui', 'lib', 'utils', 'hooks', 'shared',
|
|
286
|
+
]);
|
|
287
|
+
const candidates = [];
|
|
288
|
+
|
|
289
|
+
for (const part of rawParts) {
|
|
290
|
+
if (ignored.has(part)) continue;
|
|
291
|
+
if (/^\d+$/.test(part)) continue;
|
|
292
|
+
if (!candidates.includes(part)) candidates.push(part);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return candidates.slice(0, 3);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function createTextKey(text, namespace = 'ui') {
|
|
299
|
+
const slug = String(text || '')
|
|
300
|
+
.trim()
|
|
301
|
+
.toLowerCase()
|
|
302
|
+
.replace(/['"]/g, '')
|
|
303
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
304
|
+
.replace(/^_+|_+$/g, '')
|
|
305
|
+
.slice(0, 60);
|
|
306
|
+
|
|
307
|
+
return `${namespace || 'ui'}.${slug || 'text'}`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function looksLikeHumanText(text) {
|
|
311
|
+
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
|
312
|
+
if (value.length < HUMAN_TEXT_MIN || value.length > HUMAN_TEXT_MAX) return false;
|
|
313
|
+
if (!/[A-Za-z]/.test(value)) return false;
|
|
314
|
+
if (/^[A-Z0-9_./:-]+$/.test(value)) return false;
|
|
315
|
+
if (/^[a-z0-9_.:-]+$/.test(value) && value.includes('.')) return false;
|
|
316
|
+
if (/^(true|false|null|undefined|className|href|src|type)$/i.test(value)) return false;
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function collectHardcodedText(content, relativePath, translationValueIndex) {
|
|
321
|
+
const source = stripComments(content);
|
|
322
|
+
const namespace = deriveNamespaceCandidates(relativePath)[0] || 'ui';
|
|
323
|
+
const results = [];
|
|
324
|
+
const seen = new Set();
|
|
325
|
+
|
|
326
|
+
const patterns = [
|
|
327
|
+
/>\s*([^<>{}\n][^<>{}\n]*?[A-Za-z][^<>{}\n]*?)\s*</g,
|
|
328
|
+
/\b(?:aria-label|title|alt|placeholder|label)=["']([^"']+)["']/g,
|
|
329
|
+
];
|
|
330
|
+
|
|
331
|
+
for (const pattern of patterns) {
|
|
332
|
+
let match;
|
|
333
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
334
|
+
const text = String(match[1] || '').replace(/\s+/g, ' ').trim();
|
|
335
|
+
if (!looksLikeHumanText(text)) continue;
|
|
336
|
+
|
|
337
|
+
const location = findLineColumn(source, match.index);
|
|
338
|
+
const key = `${text}\0${location.line}`;
|
|
339
|
+
if (seen.has(key)) continue;
|
|
340
|
+
seen.add(key);
|
|
341
|
+
|
|
342
|
+
results.push({
|
|
343
|
+
text,
|
|
344
|
+
suggestedKey: createTextKey(text, namespace),
|
|
345
|
+
existingKey: translationValueIndex && translationValueIndex.get(text),
|
|
346
|
+
filePath: relativePath,
|
|
347
|
+
line: location.line,
|
|
348
|
+
column: location.column,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return results;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function buildNamespaceRecommendation(relativePath, keyReferences, availableKeys) {
|
|
357
|
+
const namespace = deriveNamespaceCandidates(relativePath)[0];
|
|
358
|
+
if (!namespace) return null;
|
|
359
|
+
|
|
360
|
+
const keys = keyReferences.map(ref => ref.key).filter(Boolean);
|
|
361
|
+
const namespacePrefix = `${namespace}.`;
|
|
362
|
+
const hasNamespaceKey = keys.some(key => key.startsWith(namespacePrefix));
|
|
363
|
+
const hasAvailableNamespace = Array.from(availableKeys || []).some(key => key.startsWith(namespacePrefix));
|
|
364
|
+
|
|
365
|
+
if (hasNamespaceKey) return null;
|
|
366
|
+
if (keys.length === 0 && !hasAvailableNamespace) return null;
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
namespace,
|
|
370
|
+
expectedFile: `${namespace}.json`,
|
|
371
|
+
message: `Source path suggests namespace "${namespace}". Prefer ${namespace}.* keys and a ${namespace}.json locale file for this page/feature.`,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function analyzeSourceForUsageInsights({
|
|
376
|
+
content,
|
|
377
|
+
relativePath,
|
|
378
|
+
availableKeys,
|
|
379
|
+
directKeys = [],
|
|
380
|
+
translationValueIndex = new Map(),
|
|
381
|
+
}) {
|
|
382
|
+
const references = [];
|
|
383
|
+
const seen = new Set();
|
|
384
|
+
const dynamicReferences = inferDynamicKeyReferences(content, availableKeys);
|
|
385
|
+
const unresolvedDynamicReferences = findUnresolvedDynamicReferences(content, availableKeys);
|
|
386
|
+
const literalReferences = findLiteralKeyReferences(content, availableKeys);
|
|
387
|
+
const exactInferredKeys = new Set([
|
|
388
|
+
...dynamicReferences.map(ref => ref.key),
|
|
389
|
+
...literalReferences.map(ref => ref.key),
|
|
390
|
+
]);
|
|
391
|
+
|
|
392
|
+
for (const key of directKeys || []) {
|
|
393
|
+
if (!key || seen.has(key)) continue;
|
|
394
|
+
if (key.endsWith('*')) {
|
|
395
|
+
const prefix = key.slice(0, -1);
|
|
396
|
+
if (Array.from(exactInferredKeys).some(inferredKey => inferredKey.startsWith(prefix))) {
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (unresolvedDynamicReferences.some(ref => ref.prefix === prefix)) {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
seen.add(key);
|
|
404
|
+
references.push({ key, matchType: 'direct', line: null, column: null });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
for (const ref of dynamicReferences) {
|
|
408
|
+
if (seen.has(ref.key)) continue;
|
|
409
|
+
seen.add(ref.key);
|
|
410
|
+
references.push(ref);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
for (const ref of literalReferences) {
|
|
414
|
+
if (seen.has(ref.key)) continue;
|
|
415
|
+
seen.add(ref.key);
|
|
416
|
+
references.push(ref);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
keyReferences: references,
|
|
421
|
+
unresolvedDynamicReferences,
|
|
422
|
+
hardcodedTexts: collectHardcodedText(content, relativePath, translationValueIndex),
|
|
423
|
+
namespaceRecommendation: buildNamespaceRecommendation(relativePath, references, availableKeys),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
module.exports = {
|
|
428
|
+
analyzeSourceForUsageInsights,
|
|
429
|
+
createTextKey,
|
|
430
|
+
deriveNamespaceCandidates,
|
|
431
|
+
findLiteralKeyReferences,
|
|
432
|
+
inferDynamicKeyReferences,
|
|
433
|
+
findUnresolvedDynamicReferences,
|
|
434
|
+
looksLikeHumanText,
|
|
435
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const SecurityUtils = require('./security');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_SOURCE_DIRS = ['src', 'app', 'lib', 'source'];
|
|
7
|
+
|
|
8
|
+
function resolveUsageSourceDir(options = {}) {
|
|
9
|
+
const projectRoot = path.resolve(options.projectRoot || process.cwd());
|
|
10
|
+
const sourceDir = options.sourceDir ? path.resolve(projectRoot, options.sourceDir) : null;
|
|
11
|
+
const i18nDir = options.i18nDir ? path.resolve(projectRoot, options.i18nDir) : null;
|
|
12
|
+
|
|
13
|
+
if (options.explicitSourceDir) {
|
|
14
|
+
return {
|
|
15
|
+
sourceDir,
|
|
16
|
+
disabled: false,
|
|
17
|
+
reason: null,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (sourceDir && i18nDir && sourceDir === i18nDir) {
|
|
22
|
+
for (const candidate of DEFAULT_SOURCE_DIRS) {
|
|
23
|
+
const candidatePath = path.resolve(projectRoot, candidate);
|
|
24
|
+
if (SecurityUtils.safeExistsSync(candidatePath, projectRoot)) {
|
|
25
|
+
return {
|
|
26
|
+
sourceDir: candidatePath,
|
|
27
|
+
disabled: false,
|
|
28
|
+
reason: `sourceDir equals i18nDir; using ${candidate}`,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
sourceDir: null,
|
|
35
|
+
disabled: true,
|
|
36
|
+
reason: 'sourceDir equals i18nDir and no application source directory was found',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
sourceDir,
|
|
42
|
+
disabled: !sourceDir,
|
|
43
|
+
reason: sourceDir ? null : 'No source directory configured',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = {
|
|
48
|
+
DEFAULT_SOURCE_DIRS,
|
|
49
|
+
resolveUsageSourceDir,
|
|
50
|
+
};
|
package/utils/watch-locales.js
CHANGED
|
@@ -147,19 +147,12 @@ function watchLocales(dirs, onChange, options = {}) {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
const resolved = path.resolve(d);
|
|
150
|
-
const validated = SecurityUtils.validatePath(resolved,
|
|
150
|
+
const validated = SecurityUtils.validatePath(resolved, path.dirname(resolved));
|
|
151
151
|
if (!validated) {
|
|
152
152
|
emitter.emit('error', new Error(`Path validation failed for: ${d}`));
|
|
153
153
|
continue;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
const projectRoot = path.resolve(process.cwd());
|
|
157
|
-
const rel = path.relative(projectRoot, validated);
|
|
158
|
-
if (rel.startsWith('..')) {
|
|
159
|
-
emitter.emit('error', new Error(`Directory outside project root: ${d}`));
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
156
|
watchDirectory(validated, emitter, watchers, { debounceMs, hashTracking, watchState });
|
|
164
157
|
}
|
|
165
158
|
|