i18ntk 3.2.0 → 4.0.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/CHANGELOG.md +62 -2
- package/README.md +177 -22
- package/SECURITY.md +28 -8
- package/main/i18ntk-backup.js +305 -62
- package/main/i18ntk-complete.js +120 -49
- package/main/i18ntk-scanner.js +188 -49
- package/main/i18ntk-sizing.js +223 -29
- package/main/i18ntk-translate.js +25 -1
- package/main/i18ntk-usage.js +203 -3
- package/main/i18ntk-validate.js +107 -3
- package/main/manage/commands/FixerCommand.js +23 -21
- package/main/manage/index.js +13 -7
- package/main/manage/services/FileManagementService.js +12 -6
- package/package.json +3 -3
- package/runtime/i18ntk.d.ts +22 -16
- package/runtime/index.d.ts +9 -7
- package/runtime/index.js +240 -50
- package/ui-locales/de.json +1389 -1359
- package/ui-locales/en.json +1 -1
- package/ui-locales/es.json +1503 -1473
- package/ui-locales/fr.json +1626 -1596
- package/ui-locales/ja.json +1595 -1565
- package/ui-locales/ru.json +1638 -1608
- package/ui-locales/zh.json +1613 -1583
- package/utils/translate/api.js +164 -41
- package/utils/translate/protection.js +147 -6
- package/utils/translate/safe-network.js +280 -0
- package/utils/watch-locales.js +183 -36
package/main/i18ntk-complete.js
CHANGED
|
@@ -161,9 +161,14 @@ class I18nCompletionTool {
|
|
|
161
161
|
return lowered.startsWith('backup-') || lowered === 'backup' || lowered === 'reports' || lowered === 'i18ntk-reports';
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
// Get all JSON files from a language directory
|
|
165
|
-
getLanguageFiles(language) {
|
|
166
|
-
const
|
|
164
|
+
// Get all JSON files from a language directory
|
|
165
|
+
getLanguageFiles(language) {
|
|
166
|
+
const monolithFile = path.join(this.sourceDir, `${language}.json`);
|
|
167
|
+
if (SecurityUtils.safeExistsSync(monolithFile, this.config.projectRoot)) {
|
|
168
|
+
return [`${language}.json`];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const languageDir = path.join(this.sourceDir, language);
|
|
167
172
|
|
|
168
173
|
if (!SecurityUtils.safeExistsSync(languageDir, this.config.projectRoot)) {
|
|
169
174
|
return [];
|
|
@@ -173,8 +178,20 @@ class I18nCompletionTool {
|
|
|
173
178
|
.filter(file => {
|
|
174
179
|
return file.endsWith('.json') &&
|
|
175
180
|
!this.config.excludeFiles.includes(file);
|
|
176
|
-
});
|
|
177
|
-
}
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
getLanguageFilePath(language, fileName) {
|
|
185
|
+
if (fileName === `${language}.json`) {
|
|
186
|
+
return path.join(this.sourceDir, fileName);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return path.join(this.sourceDir, language, fileName);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
usesMonolithFile(language) {
|
|
193
|
+
return SecurityUtils.safeExistsSync(path.join(this.sourceDir, `${language}.json`), this.config.projectRoot);
|
|
194
|
+
}
|
|
178
195
|
|
|
179
196
|
// Parse key path and determine which file it belongs to
|
|
180
197
|
parseKeyPath(keyPath) {
|
|
@@ -238,25 +255,30 @@ class I18nCompletionTool {
|
|
|
238
255
|
}
|
|
239
256
|
|
|
240
257
|
// Add missing keys to a language
|
|
241
|
-
addMissingKeysToLanguage(language, missingKeys, dryRun = false) {
|
|
242
|
-
const languageDir = path.join(this.sourceDir, language);
|
|
243
|
-
const changes = [];
|
|
258
|
+
addMissingKeysToLanguage(language, missingKeys, dryRun = false) {
|
|
259
|
+
const languageDir = path.join(this.sourceDir, language);
|
|
260
|
+
const changes = [];
|
|
261
|
+
const usesMonolith = this.usesMonolithFile(language);
|
|
244
262
|
|
|
245
263
|
// Group keys by file
|
|
246
264
|
const keysByFile = {};
|
|
247
265
|
|
|
248
|
-
missingKeys.forEach(keyPath => {
|
|
249
|
-
const { file, key } =
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
266
|
+
missingKeys.forEach(keyPath => {
|
|
267
|
+
const { file, key } = usesMonolith
|
|
268
|
+
? { file: `${language}.json`, key: keyPath }
|
|
269
|
+
: this.parseKeyPath(keyPath);
|
|
270
|
+
if (!keysByFile[file]) {
|
|
271
|
+
keysByFile[file] = [];
|
|
272
|
+
}
|
|
253
273
|
keysByFile[file].push({ keyPath, key });
|
|
254
274
|
});
|
|
255
275
|
|
|
256
|
-
// Process each file
|
|
257
|
-
for (const [fileName, keys] of Object.entries(keysByFile)) {
|
|
258
|
-
const filePath =
|
|
259
|
-
|
|
276
|
+
// Process each file
|
|
277
|
+
for (const [fileName, keys] of Object.entries(keysByFile)) {
|
|
278
|
+
const filePath = usesMonolith
|
|
279
|
+
? path.join(this.sourceDir, fileName)
|
|
280
|
+
: path.join(languageDir, fileName);
|
|
281
|
+
let fileContent = {};
|
|
260
282
|
|
|
261
283
|
// Load existing file or create new
|
|
262
284
|
if (SecurityUtils.safeExistsSync(filePath, this.config.projectRoot)) {
|
|
@@ -266,12 +288,12 @@ class I18nCompletionTool {
|
|
|
266
288
|
console.warn(t("completeTranslations.warning_could_not_parse_filepa", { filePath })); ;
|
|
267
289
|
fileContent = {};
|
|
268
290
|
}
|
|
269
|
-
} else {
|
|
270
|
-
// Create directory if it doesn't exist
|
|
271
|
-
if (!SecurityUtils.safeExistsSync(languageDir, this.config.projectRoot)) {
|
|
272
|
-
if (!dryRun) {
|
|
273
|
-
SecurityUtils.safeMkdirSync(languageDir, this.config.projectRoot, { recursive: true });
|
|
274
|
-
}
|
|
291
|
+
} else {
|
|
292
|
+
// Create directory if it doesn't exist
|
|
293
|
+
if (!usesMonolith && !SecurityUtils.safeExistsSync(languageDir, this.config.projectRoot)) {
|
|
294
|
+
if (!dryRun) {
|
|
295
|
+
SecurityUtils.safeMkdirSync(languageDir, this.config.projectRoot, { recursive: true });
|
|
296
|
+
}
|
|
275
297
|
}
|
|
276
298
|
}
|
|
277
299
|
|
|
@@ -303,19 +325,66 @@ class I18nCompletionTool {
|
|
|
303
325
|
return changes;
|
|
304
326
|
}
|
|
305
327
|
|
|
306
|
-
// Generate appropriate translation value based on key and language
|
|
307
|
-
generateTranslationValue(keyPath, language) {
|
|
308
|
-
|
|
309
|
-
const baseValue =
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
328
|
+
// Generate appropriate translation value based on key and language
|
|
329
|
+
generateTranslationValue(keyPath, language) {
|
|
330
|
+
const sourceValue = this.getSourceValueForKeyPath(keyPath);
|
|
331
|
+
const baseValue = typeof sourceValue === 'string' && sourceValue.trim() !== ''
|
|
332
|
+
? sourceValue
|
|
333
|
+
: this.generateValueFromKey(keyPath);
|
|
334
|
+
|
|
335
|
+
// For source language, use the generated value
|
|
336
|
+
if (language === this.config.sourceLanguage) {
|
|
337
|
+
return baseValue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return `[${language.toUpperCase()}] ${baseValue}`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
getNestedValue(obj, keyPath) {
|
|
344
|
+
const keys = String(keyPath || '').split('.');
|
|
345
|
+
let current = obj;
|
|
346
|
+
|
|
347
|
+
for (const key of keys) {
|
|
348
|
+
if (!current || typeof current !== 'object' || !(key in current)) {
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
current = current[key];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return current;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
getSourceValueForKeyPath(keyPath) {
|
|
358
|
+
if (!this.sourceLanguageDir && !this.usesMonolithFile(this.config.sourceLanguage)) {
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const sourceFiles = this.getLanguageFiles(this.config.sourceLanguage);
|
|
363
|
+
const keyPathStr = String(keyPath || '');
|
|
364
|
+
const parsed = this.parseKeyPath(keyPathStr);
|
|
365
|
+
|
|
366
|
+
for (const fileName of sourceFiles) {
|
|
367
|
+
const sourceFilePath = this.getLanguageFilePath(this.config.sourceLanguage, fileName);
|
|
368
|
+
try {
|
|
369
|
+
const sourceContent = SecurityUtils.safeParseJSON(SecurityUtils.safeReadFileSync(sourceFilePath, this.config.projectRoot, 'utf8'));
|
|
370
|
+
if (!sourceContent || typeof sourceContent !== 'object') continue;
|
|
371
|
+
|
|
372
|
+
const candidates = [keyPathStr];
|
|
373
|
+
if (fileName === parsed.file) {
|
|
374
|
+
candidates.push(parsed.key);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
for (const candidate of candidates) {
|
|
378
|
+
const value = this.getNestedValue(sourceContent, candidate);
|
|
379
|
+
if (value !== undefined) return value;
|
|
380
|
+
}
|
|
381
|
+
} catch (error) {
|
|
382
|
+
console.warn(t("complete.couldNotParseSource", { file: sourceFilePath }));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
319
388
|
|
|
320
389
|
// Generate a readable value from a key path
|
|
321
390
|
generateValueFromKey(keyPath) {
|
|
@@ -351,18 +420,18 @@ class I18nCompletionTool {
|
|
|
351
420
|
}
|
|
352
421
|
|
|
353
422
|
// Get missing keys by comparing source language with target languages
|
|
354
|
-
getMissingKeysFromComparison() {
|
|
355
|
-
const sourceFiles = this.getLanguageFiles(this.config.sourceLanguage);
|
|
356
|
-
const missingKeys = [];
|
|
357
|
-
|
|
358
|
-
if (
|
|
359
|
-
console.log(t("complete.sourceLanguageNotFound", { sourceLanguage: this.config.sourceLanguage }));
|
|
360
|
-
return [];
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Process each file in source language
|
|
364
|
-
for (const fileName of sourceFiles) {
|
|
365
|
-
const sourceFilePath =
|
|
423
|
+
getMissingKeysFromComparison() {
|
|
424
|
+
const sourceFiles = this.getLanguageFiles(this.config.sourceLanguage);
|
|
425
|
+
const missingKeys = [];
|
|
426
|
+
|
|
427
|
+
if (sourceFiles.length === 0) {
|
|
428
|
+
console.log(t("complete.sourceLanguageNotFound", { sourceLanguage: this.config.sourceLanguage }));
|
|
429
|
+
return [];
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Process each file in source language
|
|
433
|
+
for (const fileName of sourceFiles) {
|
|
434
|
+
const sourceFilePath = this.getLanguageFilePath(this.config.sourceLanguage, fileName);
|
|
366
435
|
|
|
367
436
|
try {
|
|
368
437
|
const sourceContent = SecurityUtils.safeParseJSON(SecurityUtils.safeReadFileSync(sourceFilePath, this.config.projectRoot, 'utf8'));
|
|
@@ -373,7 +442,9 @@ class I18nCompletionTool {
|
|
|
373
442
|
for (const language of languages) {
|
|
374
443
|
if (language === this.config.sourceLanguage) continue;
|
|
375
444
|
|
|
376
|
-
const targetFilePath =
|
|
445
|
+
const targetFilePath = fileName === `${this.config.sourceLanguage}.json` || this.usesMonolithFile(language)
|
|
446
|
+
? path.join(this.sourceDir, `${language}.json`)
|
|
447
|
+
: path.join(this.sourceDir, language, fileName);
|
|
377
448
|
let targetKeys = [];
|
|
378
449
|
|
|
379
450
|
if (SecurityUtils.safeExistsSync(targetFilePath, this.config.projectRoot)) {
|
package/main/i18ntk-scanner.js
CHANGED
|
@@ -109,49 +109,64 @@ class I18nTextScanner {
|
|
|
109
109
|
const args = process.argv.slice(2);
|
|
110
110
|
const parsed = {};
|
|
111
111
|
|
|
112
|
-
args.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
break;
|
|
130
|
-
case '
|
|
131
|
-
parsed.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
112
|
+
for (let i = 0; i < args.length; i++) {
|
|
113
|
+
const arg = args[i];
|
|
114
|
+
if (arg.startsWith('--')) {
|
|
115
|
+
const [key, ...valueParts] = arg.substring(2).split('=');
|
|
116
|
+
let value = valueParts.join('=');
|
|
117
|
+
if (!value && args[i + 1] && !args[i + 1].startsWith('--')) {
|
|
118
|
+
value = args[i + 1];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
switch (key) {
|
|
122
|
+
case 'source-dir':
|
|
123
|
+
parsed.sourceDir = value || '';
|
|
124
|
+
if (value === args[i + 1]) i++;
|
|
125
|
+
break;
|
|
126
|
+
case 'framework':
|
|
127
|
+
parsed.framework = value || '';
|
|
128
|
+
if (value === args[i + 1]) i++;
|
|
129
|
+
break;
|
|
130
|
+
case 'patterns':
|
|
131
|
+
parsed.patterns = value ? value.split(',').map(p => p.trim()).filter(Boolean) : [];
|
|
132
|
+
if (value === args[i + 1]) i++;
|
|
133
|
+
break;
|
|
134
|
+
case 'exclude':
|
|
135
|
+
parsed.exclude = value ? value.split(',').map(e => e.trim()).filter(Boolean) : [];
|
|
136
|
+
if (value === args[i + 1]) i++;
|
|
137
|
+
break;
|
|
138
|
+
case 'output-dir':
|
|
139
|
+
parsed.outputDir = value || '';
|
|
140
|
+
if (value === args[i + 1]) i++;
|
|
141
|
+
break;
|
|
142
|
+
case 'min-length':
|
|
143
|
+
parsed.minLength = parseInt(value) || 3;
|
|
144
|
+
if (value === args[i + 1]) i++;
|
|
145
|
+
break;
|
|
146
|
+
case 'max-length':
|
|
147
|
+
parsed.maxLength = parseInt(value) || 100;
|
|
148
|
+
if (value === args[i + 1]) i++;
|
|
149
|
+
break;
|
|
150
|
+
case 'output-report':
|
|
151
|
+
parsed.outputReport = true;
|
|
141
152
|
break;
|
|
142
153
|
case 'include-tests':
|
|
143
|
-
parsed.includeTests = true;
|
|
144
|
-
break;
|
|
145
|
-
case '
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
break;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
154
|
+
parsed.includeTests = true;
|
|
155
|
+
break;
|
|
156
|
+
case 'source-language':
|
|
157
|
+
parsed.sourceLanguage = value || '';
|
|
158
|
+
if (value === args[i + 1]) i++;
|
|
159
|
+
break;
|
|
160
|
+
case 'help':
|
|
161
|
+
case 'h':
|
|
162
|
+
parsed.help = true;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return parsed;
|
|
169
|
+
}
|
|
155
170
|
|
|
156
171
|
detectFramework(projectRoot) {
|
|
157
172
|
const packagePath = path.join(projectRoot, 'package.json');
|
|
@@ -293,43 +308,148 @@ class I18nTextScanner {
|
|
|
293
308
|
}
|
|
294
309
|
|
|
295
310
|
isEnglishText(text) {
|
|
296
|
-
// Enhanced text detection for Unicode and multilingual support
|
|
297
311
|
const trimmed = text.trim();
|
|
298
312
|
if (trimmed.length < 3) return false;
|
|
299
313
|
|
|
300
|
-
// Skip if it's just numbers or special characters
|
|
301
314
|
if (/^\d+$/.test(trimmed)) return false;
|
|
302
315
|
if (/^[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>?]+$/.test(trimmed)) return false;
|
|
303
316
|
|
|
304
|
-
// Allow Unicode characters including CJK, Cyrillic, etc.
|
|
305
317
|
const validChars = trimmed.match(/[\p{L}\p{N}\s\-,.!?':"()\[\]{}]/gu) || [];
|
|
306
318
|
const validRatio = validChars.length / trimmed.length;
|
|
307
319
|
|
|
308
|
-
// Must have at least 50% valid characters and some alphabetic characters
|
|
309
320
|
const hasAlpha = /[a-zA-Z\u00C0-\u024F\u1E00-\u1EFF\u0400-\u04FF\u4E00-\u9FFF\uAC00-\uD7AF]/u.test(trimmed);
|
|
310
321
|
|
|
311
322
|
return validRatio >= 0.5 && hasAlpha;
|
|
312
323
|
}
|
|
313
324
|
|
|
325
|
+
getLanguageProfile(langCode) {
|
|
326
|
+
const profiles = {
|
|
327
|
+
en: {
|
|
328
|
+
name: 'English',
|
|
329
|
+
charRegex: /[a-zA-Z\u00C0-\u024F]/u,
|
|
330
|
+
stopwords: ['the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'had', 'her', 'was', 'one', 'our', 'out', 'has', 'have', 'from', 'they', 'that', 'with', 'this', 'will', 'your', 'which', 'their', 'them', 'than', 'then', 'been', 'being', 'would', 'should', 'could', 'about', 'after'],
|
|
331
|
+
minLength: 3,
|
|
332
|
+
maxLength: 150
|
|
333
|
+
},
|
|
334
|
+
de: {
|
|
335
|
+
name: 'German',
|
|
336
|
+
charRegex: /[a-zA-Z\u00C0-\u00FF\u0100-\u017F\u00DF\u1E00-\u1EFF]/u,
|
|
337
|
+
stopwords: ['der', 'die', 'das', 'und', 'ist', 'von', 'mit', 'sich', 'des', 'auf', 'dem', 'nicht', 'ein', 'eine', 'auch', 'als', 'aus', 'bei', 'nach', 'wie', 'oder', 'war', 'hat', 'ich', 'sie', 'einem', 'um', 'am', 'im', 'es'],
|
|
338
|
+
minLength: 3,
|
|
339
|
+
maxLength: 180
|
|
340
|
+
},
|
|
341
|
+
fr: {
|
|
342
|
+
name: 'French',
|
|
343
|
+
charRegex: /[a-zA-Z\u00C0-\u00FF\u0152\u0153]/u,
|
|
344
|
+
stopwords: ['le', 'la', 'les', 'des', 'est', 'pas', 'que', 'une', 'dans', 'sur', 'plus', 'par', 'pour', 'avec', 'aux', 'ces', 'ses', 'mes', 'tes', 'notre', 'votre', 'leur', 'dont', 'sont', 'comme', 'mais', 'alors', 'peut', 'tout', 'tous', 'fait'],
|
|
345
|
+
minLength: 3,
|
|
346
|
+
maxLength: 170
|
|
347
|
+
},
|
|
348
|
+
es: {
|
|
349
|
+
name: 'Spanish',
|
|
350
|
+
charRegex: /[a-zA-Z\u00C0-\u00FF\u00F1\u00D1]/u,
|
|
351
|
+
stopwords: ['que', 'los', 'las', 'del', 'como', 'por', 'para', 'con', 'una', 'sus', 'muy', 'más', 'pero', 'este', 'esta', 'hay', 'son', 'eran', 'fue', 'han', 'será', 'está', 'todo', 'otro', 'otra'],
|
|
352
|
+
minLength: 3,
|
|
353
|
+
maxLength: 150
|
|
354
|
+
},
|
|
355
|
+
ja: {
|
|
356
|
+
name: 'Japanese',
|
|
357
|
+
charRegex: /[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\uFF66-\uFF9F]/u,
|
|
358
|
+
stopwords: ['の', 'に', 'は', 'を', 'た', 'が', 'で', 'て', 'と', 'し', 'れ', 'さ', 'る', 'す', 'ん', 'な', 'い', 'か', 'ま', 'も', 'こ', 'り', 'ち', 'き', 'ょ', 'う'],
|
|
359
|
+
minLength: 2,
|
|
360
|
+
maxLength: 80
|
|
361
|
+
},
|
|
362
|
+
zh: {
|
|
363
|
+
name: 'Chinese',
|
|
364
|
+
charRegex: /[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF]/u,
|
|
365
|
+
stopwords: ['的', '是', '在', '不', '了', '有', '和', '人', '这', '中', '大', '为', '上', '个', '国', '我', '以', '要', '他', '时', '来', '用', '们', '生', '到', '作', '地'],
|
|
366
|
+
minLength: 1,
|
|
367
|
+
maxLength: 50
|
|
368
|
+
},
|
|
369
|
+
ru: {
|
|
370
|
+
name: 'Russian',
|
|
371
|
+
charRegex: /[\u0400-\u04FF\u0500-\u052F]/u,
|
|
372
|
+
stopwords: ['и', 'в', 'не', 'на', 'что', 'как', 'по', 'к', 'от', 'это', 'за', 'то', 'для', 'все', 'его', 'она', 'так', 'же', 'но', 'был', 'быть', 'еще', 'уже', 'кто', 'мой', 'ее', 'их', 'из'],
|
|
373
|
+
minLength: 2,
|
|
374
|
+
maxLength: 200
|
|
375
|
+
},
|
|
376
|
+
ko: {
|
|
377
|
+
name: 'Korean',
|
|
378
|
+
charRegex: /[\uAC00-\uD7AF\u1100-\u11FF\u3130-\u318F]/u,
|
|
379
|
+
stopwords: ['이', '그', '저', '것', '수', '등', '들', '및', '년', '월', '일', '에서', '에게', '으로', '보다', '에게서', '의', '에', '는', '은', '가', '를', '과', '와', '도', '만', '까지', '부터'],
|
|
380
|
+
minLength: 1,
|
|
381
|
+
maxLength: 70
|
|
382
|
+
},
|
|
383
|
+
ar: {
|
|
384
|
+
name: 'Arabic',
|
|
385
|
+
charRegex: /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]/u,
|
|
386
|
+
stopwords: ['في', 'من', 'على', 'عن', 'مع', 'هو', 'هي', 'كان', 'هذا', 'ذلك', 'بين', 'بعد', 'قبل', 'عند', 'حتى', 'الى', 'او', 'لا', 'ما', 'لم', 'لن', 'كل', 'بعض', 'أي'],
|
|
387
|
+
minLength: 2,
|
|
388
|
+
maxLength: 150
|
|
389
|
+
},
|
|
390
|
+
hi: {
|
|
391
|
+
name: 'Hindi',
|
|
392
|
+
charRegex: /[\u0900-\u097F]/u,
|
|
393
|
+
stopwords: ['का', 'की', 'के', 'है', 'हैं', 'था', 'थे', 'होगा', 'होगी', 'में', 'से', 'पर', 'को', 'तक', 'और', 'या', 'लेकिन', 'जब', 'तब', 'कि', 'यह', 'वह', 'एक', 'दो'],
|
|
394
|
+
minLength: 2,
|
|
395
|
+
maxLength: 160
|
|
396
|
+
},
|
|
397
|
+
vanilla: {
|
|
398
|
+
name: 'Generic Latin',
|
|
399
|
+
charRegex: /[a-zA-Z\u00C0-\u024F]/u,
|
|
400
|
+
stopwords: [],
|
|
401
|
+
minLength: 3,
|
|
402
|
+
maxLength: 150
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
return profiles[langCode] || profiles.en;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
isTextInLanguage(text, langCode) {
|
|
409
|
+
const profile = this.getLanguageProfile(langCode);
|
|
410
|
+
const trimmed = text.trim();
|
|
411
|
+
|
|
412
|
+
if (trimmed.length < profile.minLength) return false;
|
|
413
|
+
if (trimmed.length > profile.maxLength) return false;
|
|
414
|
+
|
|
415
|
+
if (/^\d+$/.test(trimmed)) return false;
|
|
416
|
+
if (/^[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>?]+$/.test(trimmed)) return false;
|
|
417
|
+
|
|
418
|
+
const hasScriptChar = profile.charRegex.test(trimmed);
|
|
419
|
+
if (!hasScriptChar) return false;
|
|
420
|
+
|
|
421
|
+
if (profile.stopwords.length > 0) {
|
|
422
|
+
const words = trimmed.toLowerCase().split(/\s+/);
|
|
423
|
+
for (const word of words) {
|
|
424
|
+
if (profile.stopwords.includes(word)) return true;
|
|
425
|
+
}
|
|
426
|
+
const validChars = trimmed.match(/[\p{L}\p{N}\s\-,.!?':"()\[\]{}]/gu) || [];
|
|
427
|
+
const validRatio = validChars.length / trimmed.length;
|
|
428
|
+
return validRatio >= 0.5;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
|
|
314
434
|
scanFile(filePath, patterns, minLength, maxLength) {
|
|
315
435
|
try {
|
|
316
436
|
const content = SecurityUtils.safeReadFileSync(filePath, path.dirname(filePath), 'utf8');
|
|
317
437
|
const lines = content.split('\n');
|
|
318
438
|
const results = [];
|
|
439
|
+
const sourceLang = this.config.sourceLanguage || 'en';
|
|
319
440
|
|
|
320
441
|
patterns.forEach(pattern => {
|
|
321
442
|
let match;
|
|
322
443
|
while ((match = pattern.exec(content)) !== null) {
|
|
323
444
|
const text = match[1] || match[0];
|
|
324
445
|
|
|
325
|
-
// Skip translation function calls
|
|
326
446
|
const beforeMatch = content.substring(Math.max(0, match.index - 20), match.index);
|
|
327
447
|
if (beforeMatch.includes('t(') || beforeMatch.includes('i18next.t(') ||
|
|
328
448
|
beforeMatch.includes('$t(') || beforeMatch.includes('translate(')) {
|
|
329
449
|
continue;
|
|
330
450
|
}
|
|
331
451
|
|
|
332
|
-
if (text && this.
|
|
452
|
+
if (text && this.isTextInLanguage(text, sourceLang) &&
|
|
333
453
|
text.length >= minLength && text.length <= maxLength) {
|
|
334
454
|
|
|
335
455
|
const lineNumber = content.substring(0, match.index).split('\n').length;
|
|
@@ -355,7 +475,23 @@ class I18nTextScanner {
|
|
|
355
475
|
}
|
|
356
476
|
|
|
357
477
|
generateSuggestion(text) {
|
|
358
|
-
const
|
|
478
|
+
const sourceLang = this.config.sourceLanguage || 'en';
|
|
479
|
+
const transliterations = {
|
|
480
|
+
ja: { 'あ': 'a', 'い': 'i', 'う': 'u', 'え': 'e', 'お': 'o', 'か': 'ka', 'き': 'ki', 'く': 'ku', 'け': 'ke', 'こ': 'ko', 'さ': 'sa', 'し': 'shi', 'す': 'su', 'せ': 'se', 'そ': 'so', 'た': 'ta', 'ち': 'chi', 'つ': 'tsu', 'て': 'te', 'と': 'to', 'な': 'na', 'に': 'ni', 'ぬ': 'nu', 'ね': 'ne', 'の': 'no', 'は': 'ha', 'ひ': 'hi', 'ふ': 'fu', 'へ': 'he', 'ほ': 'ho', 'ま': 'ma', 'み': 'mi', 'む': 'mu', 'め': 'me', 'も': 'mo', 'や': 'ya', 'ゆ': 'yu', 'よ': 'yo', 'ら': 'ra', 'り': 'ri', 'る': 'ru', 'れ': 're', 'ろ': 'ro', 'わ': 'wa', 'を': 'wo', 'ん': 'n' },
|
|
481
|
+
ru: { 'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo', 'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u', 'ф': 'f', 'х': 'kh', 'ц': 'ts', 'ч': 'ch', 'ш': 'sh', 'щ': 'sch', 'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya' },
|
|
482
|
+
zh: { '的': 'de', '一': 'yi', '是': 'shi', '在': 'zai', '不': 'bu', '了': 'le', '有': 'you', '和': 'he', '人': 'ren', '这': 'zhe', '中': 'zhong', '大': 'da', '为': 'wei', '上': 'shang', '个': 'ge', '国': 'guo', '我': 'wo', '以': 'yi_t', '要': 'yao', '他': 'ta', '时': 'shi_t', '来': 'lai', '用': 'yong', '们': 'men', '生': 'sheng', '到': 'dao', '作': 'zuo', '地': 'di' }
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
let transliterated = text;
|
|
486
|
+
const table = transliterations[sourceLang];
|
|
487
|
+
if (table) {
|
|
488
|
+
transliterated = '';
|
|
489
|
+
for (const ch of text) {
|
|
490
|
+
transliterated += table[ch] || ch;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const key = transliterated.toLowerCase()
|
|
359
495
|
.replace(/[^a-z0-9\s]/g, '')
|
|
360
496
|
.replace(/\s+/g, '_')
|
|
361
497
|
.substring(0, 50);
|
|
@@ -559,6 +695,9 @@ class I18nTextScanner {
|
|
|
559
695
|
|
|
560
696
|
this.sourceDir = this.config.sourceDir || './src';
|
|
561
697
|
|
|
698
|
+
// Source language for multi-language detection
|
|
699
|
+
this.sourceLanguage = args['source-language'] || this.config.sourceLanguage || 'en';
|
|
700
|
+
|
|
562
701
|
// Resolve framework with precedence: CLI arg > config.framework.preference|string > auto-detect > fallback
|
|
563
702
|
const cliFramework = args.framework;
|
|
564
703
|
const cfgFramework = this.config.framework;
|
|
@@ -637,4 +776,4 @@ if (require.main === module) {
|
|
|
637
776
|
})();
|
|
638
777
|
}
|
|
639
778
|
|
|
640
|
-
module.exports = I18nTextScanner;
|
|
779
|
+
module.exports = I18nTextScanner;
|