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
package/main/i18ntk-translate.js
CHANGED
|
@@ -24,9 +24,11 @@
|
|
|
24
24
|
* --protection-file <path> JSON file with protected terms, keys, values, and patterns
|
|
25
25
|
* --create-protection-file Create the protection JSON file if it does not exist
|
|
26
26
|
* --no-protection Disable protected term/key/value handling for this run
|
|
27
|
-
* --concurrency <n> Max concurrent API requests (default:
|
|
27
|
+
* --concurrency <n> Max concurrent API requests (default: 12, Google max: 100)
|
|
28
28
|
* --batch-size <n> Number of text segments per batch (default: 50)
|
|
29
29
|
* --dry-run Preview mode without API calls
|
|
30
|
+
* --only-missing Translate only missing, marker, source-copy, or likely English target values
|
|
31
|
+
* --translate-all Re-translate every source string even when a target value already exists
|
|
30
32
|
* --report-file <path> Write report to file
|
|
31
33
|
* --report-stdout Print report to stdout
|
|
32
34
|
* --bom Output UTF-8 with BOM
|
|
@@ -61,9 +63,10 @@ const {
|
|
|
61
63
|
restoreText,
|
|
62
64
|
shouldPreserveWholeValue,
|
|
63
65
|
} = require('../utils/translate/protection');
|
|
64
|
-
const { translateBatch } = require('../utils/translate/api');
|
|
65
|
-
const { collectLeaves, setLeaf, deepClone } = require('../utils/translate/traverse');
|
|
66
|
+
const { translateBatch, DEFAULT_CONCURRENCY, clampProviderConcurrency } = require('../utils/translate/api');
|
|
67
|
+
const { collectLeaves, getLeaf, setLeaf, deepClone } = require('../utils/translate/traverse');
|
|
66
68
|
const { generateReport, writeReport, formatSummaryLine } = require('../utils/translate/report');
|
|
69
|
+
const { analyzeEnglishContent } = require('../utils/validation-risk');
|
|
67
70
|
const {
|
|
68
71
|
confirmGlobalChoice,
|
|
69
72
|
confirmPerKey,
|
|
@@ -101,10 +104,12 @@ function printHelp() {
|
|
|
101
104
|
' --protection-file <path> Protected terms/keys JSON file (default: i18ntk-auto-translate.json)',
|
|
102
105
|
' --create-protection-file Create the protection JSON file if missing',
|
|
103
106
|
' --no-protection Disable protected term/key/value handling',
|
|
104
|
-
' --concurrency <n> Max concurrent API requests (default:
|
|
107
|
+
' --concurrency <n> Max concurrent API requests (default: 12, Google max: 100)',
|
|
105
108
|
' --batch-size <n> Number of text segments per batch (default: 50)',
|
|
106
109
|
' --progress-interval <n> Progress update interval (default: 10)',
|
|
107
110
|
' --dry-run Preview: show what would be skipped',
|
|
111
|
+
' --only-missing Translate only missing, marker, source-copy, or likely English target values (default)',
|
|
112
|
+
' --translate-all Re-translate every source string even when target values already exist',
|
|
108
113
|
' --report-file <path> Write post-translation report to file',
|
|
109
114
|
' --report-stdout Print post-translation report to stdout',
|
|
110
115
|
' --bom Write output files with UTF-8 BOM',
|
|
@@ -141,10 +146,11 @@ function parseArgs(argv) {
|
|
|
141
146
|
protectionFile: DEFAULT_PROTECTION_FILE,
|
|
142
147
|
protectionEnabled: true,
|
|
143
148
|
createProtectionFile: false,
|
|
144
|
-
concurrency:
|
|
149
|
+
concurrency: DEFAULT_CONCURRENCY,
|
|
145
150
|
batchSize: 50,
|
|
146
151
|
progressInterval: 10,
|
|
147
152
|
dryRun: false,
|
|
153
|
+
onlyMissingOrEnglish: true,
|
|
148
154
|
reportFile: null,
|
|
149
155
|
reportStdout: false,
|
|
150
156
|
bom: false,
|
|
@@ -168,6 +174,8 @@ function parseArgs(argv) {
|
|
|
168
174
|
else if (arg === '--no-protection') { args.protectionEnabled = false; }
|
|
169
175
|
else if (arg === '--create-protection-file') { args.createProtectionFile = true; }
|
|
170
176
|
else if (arg === '--dry-run') { args.dryRun = true; }
|
|
177
|
+
else if (arg === '--translate-all' || arg === '--force-translate') { args.onlyMissingOrEnglish = false; }
|
|
178
|
+
else if (arg === '--only-missing' || arg === '--only-missing-or-english') { args.onlyMissingOrEnglish = true; }
|
|
171
179
|
else if (arg === '--report-stdout') { args.reportStdout = true; }
|
|
172
180
|
else if (arg === '--bom') { args.bom = true; }
|
|
173
181
|
else if (arg === '--source-dir' && i + 1 < argv.length) { args.sourceDir = argv[++i]; }
|
|
@@ -176,7 +184,7 @@ function parseArgs(argv) {
|
|
|
176
184
|
else if (arg === '--provider' && i + 1 < argv.length) { args.provider = argv[++i]; }
|
|
177
185
|
else if (arg === '--custom-regex' && i + 1 < argv.length) { args.customRegex.push(argv[++i]); }
|
|
178
186
|
else if (arg === '--protection-file' && i + 1 < argv.length) { args.protectionFile = argv[++i]; }
|
|
179
|
-
else if (arg === '--concurrency' && i + 1 < argv.length) { args.concurrency = parseInt(argv[++i], 10) ||
|
|
187
|
+
else if (arg === '--concurrency' && i + 1 < argv.length) { args.concurrency = parseInt(argv[++i], 10) || DEFAULT_CONCURRENCY; }
|
|
180
188
|
else if (arg === '--batch-size' && i + 1 < argv.length) { args.batchSize = parseInt(argv[++i], 10) || 50; }
|
|
181
189
|
else if (arg === '--progress-interval' && i + 1 < argv.length) { args.progressInterval = parseInt(argv[++i], 10) || 10; }
|
|
182
190
|
else if (arg === '--report-file' && i + 1 < argv.length) { args.reportFile = argv[++i]; }
|
|
@@ -202,13 +210,23 @@ function parseArgs(argv) {
|
|
|
202
210
|
process.exit(1);
|
|
203
211
|
}
|
|
204
212
|
|
|
205
|
-
args.concurrency =
|
|
213
|
+
args.concurrency = clampProviderConcurrency(args.concurrency, args.provider, DEFAULT_CONCURRENCY);
|
|
206
214
|
args.batchSize = clampInt(args.batchSize, 1, 10000, 50);
|
|
207
215
|
args.progressInterval = clampInt(args.progressInterval, 1, 10000, 10);
|
|
208
216
|
|
|
209
217
|
return args;
|
|
210
218
|
}
|
|
211
219
|
|
|
220
|
+
const UNTRANSLATED_MARKERS = new Set([
|
|
221
|
+
'',
|
|
222
|
+
'__not_translated__',
|
|
223
|
+
'not_translated',
|
|
224
|
+
'[translate]',
|
|
225
|
+
'[not translated]',
|
|
226
|
+
'todo',
|
|
227
|
+
'tbd',
|
|
228
|
+
]);
|
|
229
|
+
|
|
212
230
|
function clampInt(value, min, max, fallback) {
|
|
213
231
|
const num = parseInt(value, 10);
|
|
214
232
|
if (!Number.isInteger(num)) return fallback;
|
|
@@ -289,6 +307,136 @@ function classifyLeaves(leaves, customRegex) {
|
|
|
289
307
|
return { withPlaceholders, withoutPlaceholders };
|
|
290
308
|
}
|
|
291
309
|
|
|
310
|
+
function readExistingTargetData(targetPath) {
|
|
311
|
+
if (!SecurityUtils.safeExistsSync(targetPath, path.dirname(targetPath))) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
const raw = SecurityUtils.safeReadFileSync(targetPath, path.dirname(targetPath), 'utf-8').replace(/^\uFEFF/, '');
|
|
317
|
+
return JSON.parse(raw);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.warn(`Warning: Could not read existing target file "${targetPath}": ${error.message}`);
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function isUntranslatedMarker(value) {
|
|
325
|
+
const normalized = String(value ?? '').trim().toLowerCase();
|
|
326
|
+
return UNTRANSLATED_MARKERS.has(normalized);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function isLikelyEnglish(value, args) {
|
|
330
|
+
const text = String(value || '').trim();
|
|
331
|
+
if (!text) return false;
|
|
332
|
+
const analysis = analyzeEnglishContent(text, {
|
|
333
|
+
allowedEnglishTerms: args.allowedEnglishTerms,
|
|
334
|
+
});
|
|
335
|
+
const threshold = Number.isFinite(Number(args.englishThresholdPercent))
|
|
336
|
+
? Number(args.englishThresholdPercent)
|
|
337
|
+
: 10;
|
|
338
|
+
return analysis.englishPercentage > threshold && analysis.englishWordCount >= 2;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function normalizeLanguageCode(language) {
|
|
342
|
+
return String(language || '').trim().toLowerCase().replace(/_/g, '-').split('-')[0];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function parseLanguagePrefix(value) {
|
|
346
|
+
if (typeof value !== 'string') return null;
|
|
347
|
+
const match = value.match(/^\s*\[([A-Z]{2,3}(?:[-_][A-Z0-9]{2,4})?)\]\s*(.+)$/);
|
|
348
|
+
if (!match) return null;
|
|
349
|
+
return {
|
|
350
|
+
raw: match[1],
|
|
351
|
+
language: normalizeLanguageCode(match[1]),
|
|
352
|
+
text: match[2].trim(),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function hasLatinWordContent(value, minWords = 2) {
|
|
357
|
+
const words = String(value || '').match(/[A-Za-z][A-Za-z'-]*/g) || [];
|
|
358
|
+
return words.filter(word => word.length > 1).length >= minWords;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function isLanguagePrefixedEnglish(value, args = {}) {
|
|
362
|
+
const prefix = parseLanguagePrefix(value);
|
|
363
|
+
if (!prefix || !prefix.text) return false;
|
|
364
|
+
|
|
365
|
+
const targetLanguage = normalizeLanguageCode(args.targetLang);
|
|
366
|
+
if (targetLanguage && prefix.language !== targetLanguage) return false;
|
|
367
|
+
|
|
368
|
+
return hasLatinWordContent(prefix.text, 2) || isLikelyEnglish(prefix.text, args);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function isBrokenTranslationValue(value) {
|
|
372
|
+
if (typeof value !== 'string') return false;
|
|
373
|
+
const text = value.trim();
|
|
374
|
+
if (!text) return false;
|
|
375
|
+
if (text.includes('\uFFFD')) return true;
|
|
376
|
+
|
|
377
|
+
const compact = text.replace(/[\s.,!;:()[\]{}'"`~_\-]/g, '');
|
|
378
|
+
if (compact.length >= 1 && /^\?+$/.test(compact)) return true;
|
|
379
|
+
if (/\?{3,}/.test(text)) return true;
|
|
380
|
+
|
|
381
|
+
const questionCount = (text.match(/\?/g) || []).length;
|
|
382
|
+
const visibleLength = Math.max(text.replace(/\s/g, '').length, 1);
|
|
383
|
+
if (questionCount >= 3 && questionCount / visibleLength >= 0.5) return true;
|
|
384
|
+
|
|
385
|
+
if (/[\u0080-\u009F]/.test(text) && /[ÃÂÐÑ]/.test(text)) return true;
|
|
386
|
+
|
|
387
|
+
const mojibakePatterns = [
|
|
388
|
+
/[ÃÂ][\u0080-\u00BF]/,
|
|
389
|
+
/Ð[\u0080-\u00BF]/,
|
|
390
|
+
/Ñ[\u0080-\u00BF]/,
|
|
391
|
+
/ã[‚ƒ€]/,
|
|
392
|
+
];
|
|
393
|
+
return mojibakePatterns.some((pattern) => pattern.test(text));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function shouldTranslateTargetValue(sourceValue, targetValue, args) {
|
|
397
|
+
if (targetValue === undefined || targetValue === null) return true;
|
|
398
|
+
if (typeof targetValue !== 'string') return true;
|
|
399
|
+
if (isUntranslatedMarker(targetValue)) return true;
|
|
400
|
+
if (isBrokenTranslationValue(targetValue)) return true;
|
|
401
|
+
if (isLanguagePrefixedEnglish(targetValue, args)) return true;
|
|
402
|
+
if (targetValue.trim() === String(sourceValue ?? '').trim()) return true;
|
|
403
|
+
if (args.onlyMissingOrEnglish !== false && isLikelyEnglish(targetValue, args)) return true;
|
|
404
|
+
return args.onlyMissingOrEnglish === false;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function getResidualUntranslatedReason(sourceValue, targetValue, args) {
|
|
408
|
+
if (targetValue === undefined || targetValue === null) return 'missing';
|
|
409
|
+
if (typeof targetValue !== 'string') return 'non_string';
|
|
410
|
+
if (isUntranslatedMarker(targetValue)) return 'marker';
|
|
411
|
+
if (isBrokenTranslationValue(targetValue)) return 'broken';
|
|
412
|
+
if (isLanguagePrefixedEnglish(targetValue, args)) return 'language_prefix';
|
|
413
|
+
if (targetValue.trim() === String(sourceValue ?? '').trim()) return 'source_copy';
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function planTargetAwareLeaves(sourceLeaves, targetData, args) {
|
|
418
|
+
if (!targetData || args.onlyMissingOrEnglish === false) {
|
|
419
|
+
return {
|
|
420
|
+
translatableLeaves: sourceLeaves,
|
|
421
|
+
existingLeaves: [],
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const translatableLeaves = [];
|
|
426
|
+
const existingLeaves = [];
|
|
427
|
+
|
|
428
|
+
for (const leaf of sourceLeaves) {
|
|
429
|
+
const targetValue = getLeaf(targetData, leaf.keyPath);
|
|
430
|
+
if (shouldTranslateTargetValue(leaf.value, targetValue, args)) {
|
|
431
|
+
translatableLeaves.push(leaf);
|
|
432
|
+
} else {
|
|
433
|
+
existingLeaves.push({ ...leaf, value: targetValue, skipReason: 'existing' });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return { translatableLeaves, existingLeaves };
|
|
438
|
+
}
|
|
439
|
+
|
|
292
440
|
async function resolvePlaceholderStrategy(args) {
|
|
293
441
|
const interactive = isInteractive({ noPrompt: args.noConfirm });
|
|
294
442
|
|
|
@@ -403,7 +551,11 @@ function prepareDirectBatch(toTranslate, customRegex, protection) {
|
|
|
403
551
|
|
|
404
552
|
async function runTranslation(maskedBatch, targetLang, options) {
|
|
405
553
|
const batchItems = maskedBatch.map((item) => ({ value: item.masked, keyPath: item.keyPath }));
|
|
406
|
-
const results = await translateBatchInChunks(batchItems, targetLang,
|
|
554
|
+
const results = await translateBatchInChunks(batchItems, targetLang, {
|
|
555
|
+
...options,
|
|
556
|
+
stageLabel: 'Translating strings',
|
|
557
|
+
progressUnit: 'strings',
|
|
558
|
+
});
|
|
407
559
|
return results;
|
|
408
560
|
}
|
|
409
561
|
|
|
@@ -428,6 +580,9 @@ async function translateBatchInChunks(batch, targetLang, options) {
|
|
|
428
580
|
total: batch.length,
|
|
429
581
|
chunkCompleted: info.completed,
|
|
430
582
|
chunkTotal: info.total,
|
|
583
|
+
keyPath: info.keyPath,
|
|
584
|
+
stage: options.stageLabel || 'Translating',
|
|
585
|
+
unit: options.progressUnit || 'items',
|
|
431
586
|
});
|
|
432
587
|
}
|
|
433
588
|
},
|
|
@@ -522,7 +677,11 @@ async function translatePreservedItems(items, targetLang, options, customRegex,
|
|
|
522
677
|
return plan;
|
|
523
678
|
});
|
|
524
679
|
|
|
525
|
-
const translatedSegments = await translateBatchInChunks(segmentJobs, targetLang,
|
|
680
|
+
const translatedSegments = await translateBatchInChunks(segmentJobs, targetLang, {
|
|
681
|
+
...options,
|
|
682
|
+
stageLabel: 'Translating placeholder-safe text segments',
|
|
683
|
+
progressUnit: 'segments',
|
|
684
|
+
});
|
|
526
685
|
|
|
527
686
|
return plans.map((plan) => {
|
|
528
687
|
const value = plan.segments.map((segment) => {
|
|
@@ -580,8 +739,8 @@ async function translateItems(toTranslate, targetLang, options, customRegex, pro
|
|
|
580
739
|
return finalResults;
|
|
581
740
|
}
|
|
582
741
|
|
|
583
|
-
function applyResults(sourceData, translatedResults, toTranslate, toSkip) {
|
|
584
|
-
const output = deepClone(sourceData);
|
|
742
|
+
function applyResults(sourceData, translatedResults, toTranslate, toSkip, targetData = null) {
|
|
743
|
+
const output = deepClone(targetData || sourceData);
|
|
585
744
|
|
|
586
745
|
for (let i = 0; i < toTranslate.length; i++) {
|
|
587
746
|
setLeaf(output, toTranslate[i].keyPath, translatedResults[i]);
|
|
@@ -594,6 +753,47 @@ function applyResults(sourceData, translatedResults, toTranslate, toSkip) {
|
|
|
594
753
|
return output;
|
|
595
754
|
}
|
|
596
755
|
|
|
756
|
+
function findResidualUntranslatedLeaves(sourceLeaves, outputData, args, options = {}) {
|
|
757
|
+
const ignoredKeys = options.ignoredKeys || new Set();
|
|
758
|
+
const residual = [];
|
|
759
|
+
|
|
760
|
+
for (const leaf of sourceLeaves) {
|
|
761
|
+
if (ignoredKeys.has(leaf.keyPath)) continue;
|
|
762
|
+
|
|
763
|
+
const targetValue = getLeaf(outputData, leaf.keyPath);
|
|
764
|
+
const reason = getResidualUntranslatedReason(leaf.value, targetValue, args);
|
|
765
|
+
if (reason) {
|
|
766
|
+
residual.push({
|
|
767
|
+
keyPath: leaf.keyPath,
|
|
768
|
+
value: String(targetValue ?? ''),
|
|
769
|
+
sourceValue: leaf.value,
|
|
770
|
+
reason,
|
|
771
|
+
fileName: options.fileName,
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
return residual;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function buildResidualRetryItems(residualLeaves, customRegex) {
|
|
780
|
+
return residualLeaves.map((leaf) => {
|
|
781
|
+
const placeholders = detectPlaceholders(leaf.sourceValue, customRegex);
|
|
782
|
+
return {
|
|
783
|
+
keyPath: leaf.keyPath,
|
|
784
|
+
value: leaf.sourceValue,
|
|
785
|
+
placeholders,
|
|
786
|
+
placeholderMode: placeholders.length > 0 ? 'preserve' : 'none',
|
|
787
|
+
};
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function formatProgressKey(keyPath) {
|
|
792
|
+
if (!keyPath) return '';
|
|
793
|
+
const value = String(keyPath);
|
|
794
|
+
return value.length > 72 ? `${value.slice(0, 69)}...` : value;
|
|
795
|
+
}
|
|
796
|
+
|
|
597
797
|
function writeOutput(outputData, outputPath, bom) {
|
|
598
798
|
const resolvedOutputPath = path.resolve(process.cwd(), outputPath);
|
|
599
799
|
const dir = path.dirname(resolvedOutputPath);
|
|
@@ -611,6 +811,7 @@ async function processFile(sourcePath, targetLang, args) {
|
|
|
611
811
|
const fileName = path.basename(sourcePath);
|
|
612
812
|
const targetDir = args.outputDir || path.join(path.dirname(path.dirname(sourcePath)), targetLang);
|
|
613
813
|
const targetPath = path.join(targetDir, fileName);
|
|
814
|
+
const runArgs = { ...args, targetLang };
|
|
614
815
|
|
|
615
816
|
let sourceData;
|
|
616
817
|
try {
|
|
@@ -628,16 +829,19 @@ async function processFile(sourcePath, targetLang, args) {
|
|
|
628
829
|
return { total: 0, translated: 0, skipped: 0, skippedKeys: [] };
|
|
629
830
|
}
|
|
630
831
|
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
|
|
832
|
+
const targetData = readExistingTargetData(targetPath);
|
|
833
|
+
const { translatableLeaves: candidateLeaves, existingLeaves } = planTargetAwareLeaves(leaves, targetData, runArgs);
|
|
834
|
+
|
|
835
|
+
const protection = runArgs.protection || loadProtectionConfig(runArgs.protectionFile, {
|
|
836
|
+
enabled: runArgs.protectionEnabled,
|
|
837
|
+
create: runArgs.createProtectionFile,
|
|
634
838
|
});
|
|
635
|
-
const protectedLeaves =
|
|
839
|
+
const protectedLeaves = candidateLeaves
|
|
636
840
|
.filter((leaf) => shouldPreserveWholeValue(leaf.keyPath, leaf.value, protection))
|
|
637
841
|
.map((leaf) => ({ ...leaf, skipReason: 'protected' }));
|
|
638
|
-
const translatableLeaves =
|
|
639
|
-
const { withPlaceholders, withoutPlaceholders } = classifyLeaves(translatableLeaves,
|
|
640
|
-
const { strategy, interactiveMode } = await resolvePlaceholderStrategy(
|
|
842
|
+
const translatableLeaves = candidateLeaves.filter((leaf) => !shouldPreserveWholeValue(leaf.keyPath, leaf.value, protection));
|
|
843
|
+
const { withPlaceholders, withoutPlaceholders } = classifyLeaves(translatableLeaves, runArgs.customRegex);
|
|
844
|
+
const { strategy, interactiveMode } = await resolvePlaceholderStrategy(runArgs);
|
|
641
845
|
|
|
642
846
|
if (args.dryRun && strategy === 'skip' && withPlaceholders.length > 0) {
|
|
643
847
|
await previewSkipped(withPlaceholders);
|
|
@@ -649,13 +853,17 @@ async function processFile(sourcePath, targetLang, args) {
|
|
|
649
853
|
skippedKeys,
|
|
650
854
|
placeholderProtected: 0,
|
|
651
855
|
protectedSkipped: protectedLeaves.length,
|
|
856
|
+
skippedExisting: existingLeaves.length,
|
|
652
857
|
dryRun: true,
|
|
653
858
|
};
|
|
654
859
|
}
|
|
655
860
|
|
|
656
861
|
if (args.dryRun) {
|
|
657
862
|
const protectedCount = strategy === 'send' ? 0 : withPlaceholders.length;
|
|
658
|
-
console.log(`[${fileName}] Dry-run: ${leaves.length} strings would be translated.`);
|
|
863
|
+
console.log(`[${fileName}] Dry-run: ${candidateLeaves.length} of ${leaves.length} strings would be translated.`);
|
|
864
|
+
if (existingLeaves.length > 0) {
|
|
865
|
+
console.log(`[${fileName}] Dry-run: ${existingLeaves.length} existing translated strings would be kept.`);
|
|
866
|
+
}
|
|
659
867
|
if (protectedLeaves.length > 0) {
|
|
660
868
|
console.log(`[${fileName}] Dry-run: ${protectedLeaves.length} protected keys/values would be copied unchanged.`);
|
|
661
869
|
}
|
|
@@ -667,11 +875,12 @@ async function processFile(sourcePath, targetLang, args) {
|
|
|
667
875
|
}
|
|
668
876
|
return {
|
|
669
877
|
total: leaves.length,
|
|
670
|
-
translated:
|
|
878
|
+
translated: candidateLeaves.length - protectedLeaves.length,
|
|
671
879
|
skipped: protectedLeaves.length,
|
|
672
880
|
skippedKeys: protectedLeaves,
|
|
673
881
|
placeholderProtected: protectedCount,
|
|
674
882
|
termProtected: hasProtectionRules(protection),
|
|
883
|
+
skippedExisting: existingLeaves.length,
|
|
675
884
|
dryRun: true,
|
|
676
885
|
};
|
|
677
886
|
}
|
|
@@ -679,8 +888,9 @@ async function processFile(sourcePath, targetLang, args) {
|
|
|
679
888
|
const decisions = await resolvePerKeyDecisions(withPlaceholders, interactiveMode);
|
|
680
889
|
const { toTranslate, toSkip } = buildTranslateList(withPlaceholders, withoutPlaceholders, strategy, decisions);
|
|
681
890
|
toSkip.push(...protectedLeaves);
|
|
891
|
+
toSkip.push(...existingLeaves);
|
|
682
892
|
const placeholderProtected = toTranslate.filter((leaf) => leaf.placeholderMode === 'preserve').length;
|
|
683
|
-
const placeholderSkipped = toSkip.filter((leaf) => leaf.skipReason !== 'protected').length;
|
|
893
|
+
const placeholderSkipped = toSkip.filter((leaf) => leaf.skipReason !== 'protected' && leaf.skipReason !== 'existing').length;
|
|
684
894
|
|
|
685
895
|
if (placeholderSkipped > 0) {
|
|
686
896
|
console.log(`[${fileName}] Skipping ${placeholderSkipped} keys with placeholders.`);
|
|
@@ -691,6 +901,9 @@ async function processFile(sourcePath, targetLang, args) {
|
|
|
691
901
|
if (protectedLeaves.length > 0) {
|
|
692
902
|
console.log(`[${fileName}] Copying ${protectedLeaves.length} protected keys/values unchanged.`);
|
|
693
903
|
}
|
|
904
|
+
if (existingLeaves.length > 0) {
|
|
905
|
+
console.log(`[${fileName}] Keeping ${existingLeaves.length} existing translated keys.`);
|
|
906
|
+
}
|
|
694
907
|
if (hasProtectionRules(protection)) {
|
|
695
908
|
console.log(`[${fileName}] Protecting terms from: ${protection.filePath}`);
|
|
696
909
|
}
|
|
@@ -698,17 +911,21 @@ async function processFile(sourcePath, targetLang, args) {
|
|
|
698
911
|
const manifestPath = createPlaceholderManifest(sourcePath, targetLang, toTranslate);
|
|
699
912
|
|
|
700
913
|
const translateOptions = {
|
|
701
|
-
sourceLang:
|
|
702
|
-
provider:
|
|
703
|
-
concurrency:
|
|
704
|
-
batchSize:
|
|
705
|
-
retryCount:
|
|
706
|
-
retryDelay:
|
|
707
|
-
timeout:
|
|
708
|
-
customFn:
|
|
914
|
+
sourceLang: runArgs.sourceLang,
|
|
915
|
+
provider: runArgs.provider,
|
|
916
|
+
concurrency: runArgs.concurrency,
|
|
917
|
+
batchSize: runArgs.batchSize,
|
|
918
|
+
retryCount: runArgs.retryCount,
|
|
919
|
+
retryDelay: runArgs.retryDelay,
|
|
920
|
+
timeout: runArgs.timeout,
|
|
921
|
+
customFn: runArgs.translateFn,
|
|
709
922
|
onProgress: (info) => {
|
|
710
|
-
if (info.completed %
|
|
711
|
-
|
|
923
|
+
if (info.completed % runArgs.progressInterval === 0 || info.completed === info.total) {
|
|
924
|
+
const stage = info.stage || 'Translating';
|
|
925
|
+
const unit = info.unit || 'items';
|
|
926
|
+
const keyPath = formatProgressKey(info.keyPath);
|
|
927
|
+
const keySuffix = keyPath ? ` | ${keyPath}` : '';
|
|
928
|
+
process.stdout.write(`\r[${fileName}] ${stage}: ${info.completed}/${info.total} ${unit}${keySuffix}`);
|
|
712
929
|
}
|
|
713
930
|
},
|
|
714
931
|
onError: (err) => {
|
|
@@ -719,8 +936,10 @@ async function processFile(sourcePath, targetLang, args) {
|
|
|
719
936
|
let translatedResults;
|
|
720
937
|
try {
|
|
721
938
|
if (toTranslate.length > 0) {
|
|
722
|
-
|
|
939
|
+
console.log(`[${fileName}] Preparing translation plan for ${toTranslate.length} keys.`);
|
|
940
|
+
translatedResults = await translateItems(toTranslate, targetLang, translateOptions, runArgs.customRegex, protection);
|
|
723
941
|
process.stdout.write('\n');
|
|
942
|
+
console.log(`[${fileName}] Applying translated values.`);
|
|
724
943
|
} else {
|
|
725
944
|
translatedResults = [];
|
|
726
945
|
}
|
|
@@ -728,18 +947,47 @@ async function processFile(sourcePath, targetLang, args) {
|
|
|
728
947
|
cleanupPlaceholderManifest(manifestPath);
|
|
729
948
|
}
|
|
730
949
|
|
|
731
|
-
const output = applyResults(sourceData, translatedResults, toTranslate, toSkip);
|
|
732
|
-
|
|
950
|
+
const output = applyResults(sourceData, translatedResults, toTranslate, toSkip, targetData);
|
|
951
|
+
const ignoredResidualKeys = new Set(protectedLeaves.map((leaf) => leaf.keyPath));
|
|
952
|
+
let residualUntranslated = findResidualUntranslatedLeaves(leaves, output, runArgs, {
|
|
953
|
+
ignoredKeys: ignoredResidualKeys,
|
|
954
|
+
fileName,
|
|
955
|
+
});
|
|
956
|
+
let finalCheckRetried = 0;
|
|
957
|
+
|
|
958
|
+
if (residualUntranslated.length > 0) {
|
|
959
|
+
console.warn(`[${fileName}] Warning: Final check found ${residualUntranslated.length} values that still look untranslated. Retrying once before writing.`);
|
|
960
|
+
const retryItems = buildResidualRetryItems(residualUntranslated, runArgs.customRegex);
|
|
961
|
+
const retryResults = await translateItems(retryItems, targetLang, translateOptions, runArgs.customRegex, protection);
|
|
962
|
+
retryItems.forEach((item, index) => {
|
|
963
|
+
setLeaf(output, item.keyPath, retryResults[index]);
|
|
964
|
+
});
|
|
965
|
+
finalCheckRetried = retryItems.length;
|
|
966
|
+
residualUntranslated = findResidualUntranslatedLeaves(leaves, output, runArgs, {
|
|
967
|
+
ignoredKeys: ignoredResidualKeys,
|
|
968
|
+
fileName,
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
if (residualUntranslated.length > 0) {
|
|
973
|
+
console.warn(`[${fileName}] Warning: ${residualUntranslated.length} values still look untranslated after retry. Rerun Auto Translate to capture leftovers.`);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
console.log(`[${fileName}] Writing output.`);
|
|
977
|
+
writeOutput(output, targetPath, runArgs.bom);
|
|
733
978
|
|
|
734
979
|
console.log(`[${fileName}] Written: ${targetPath}`);
|
|
735
980
|
|
|
736
981
|
return {
|
|
737
982
|
total: leaves.length,
|
|
738
983
|
translated: translatedResults.length,
|
|
739
|
-
skipped: toSkip.length,
|
|
740
|
-
skippedKeys: toSkip,
|
|
984
|
+
skipped: toSkip.filter((leaf) => leaf.skipReason !== 'existing').length,
|
|
985
|
+
skippedKeys: toSkip.filter((leaf) => leaf.skipReason !== 'existing'),
|
|
741
986
|
placeholderProtected,
|
|
742
987
|
protectedSkipped: protectedLeaves.length,
|
|
988
|
+
skippedExisting: existingLeaves.length,
|
|
989
|
+
finalCheckRetried,
|
|
990
|
+
residualUntranslated,
|
|
743
991
|
};
|
|
744
992
|
}
|
|
745
993
|
|
|
@@ -787,6 +1035,9 @@ async function run(args) {
|
|
|
787
1035
|
let grandSkipped = 0;
|
|
788
1036
|
let grandPlaceholderProtected = 0;
|
|
789
1037
|
let grandProtectedSkipped = 0;
|
|
1038
|
+
let grandSkippedExisting = 0;
|
|
1039
|
+
let grandFinalCheckRetried = 0;
|
|
1040
|
+
const allResidualUntranslated = [];
|
|
790
1041
|
|
|
791
1042
|
for (const srcPath of sourceFiles) {
|
|
792
1043
|
const result = await processFile(srcPath, args.targetLang, args);
|
|
@@ -796,25 +1047,39 @@ async function run(args) {
|
|
|
796
1047
|
grandSkipped += result.skipped;
|
|
797
1048
|
grandPlaceholderProtected += result.placeholderProtected || 0;
|
|
798
1049
|
grandProtectedSkipped += result.protectedSkipped || 0;
|
|
1050
|
+
grandSkippedExisting += result.skippedExisting || 0;
|
|
1051
|
+
grandFinalCheckRetried += result.finalCheckRetried || 0;
|
|
799
1052
|
if (result.skippedKeys && result.skippedKeys.length > 0) {
|
|
800
1053
|
allSkippedKeys.push(...result.skippedKeys);
|
|
801
1054
|
}
|
|
1055
|
+
if (result.residualUntranslated && result.residualUntranslated.length > 0) {
|
|
1056
|
+
allResidualUntranslated.push(...result.residualUntranslated);
|
|
1057
|
+
}
|
|
802
1058
|
}
|
|
803
1059
|
}
|
|
804
1060
|
|
|
805
1061
|
console.log('');
|
|
806
|
-
console.log(formatSummaryLine(grandSkipped, grandTranslated, grandTotal, grandPlaceholderProtected, grandProtectedSkipped));
|
|
1062
|
+
console.log(formatSummaryLine(grandSkipped, grandTranslated, grandTotal, grandPlaceholderProtected, grandProtectedSkipped, grandSkippedExisting));
|
|
1063
|
+
if (grandFinalCheckRetried > 0) {
|
|
1064
|
+
console.log(`[translate] final check retried ${grandFinalCheckRetried} leftover values`);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
if (allResidualUntranslated.length > 0) {
|
|
1068
|
+
console.warn(`WARNING: ${allResidualUntranslated.length} values still look untranslated after Auto Translate.`);
|
|
1069
|
+
console.warn('Rerun Auto Translate to capture leftovers, or review the values listed in the report.');
|
|
1070
|
+
}
|
|
807
1071
|
|
|
808
|
-
if (allSkippedKeys.length > 0 || args.reportFile || args.reportStdout) {
|
|
1072
|
+
if (allSkippedKeys.length > 0 || allResidualUntranslated.length > 0 || args.reportFile || args.reportStdout) {
|
|
809
1073
|
const report = generateReport(allSkippedKeys, grandTranslated, grandTotal, {
|
|
810
1074
|
sourceFile: sourceFiles.length === 1 ? sourceFiles[0] : `${sourceFiles.length} files`,
|
|
811
1075
|
targetLang: args.targetLang,
|
|
812
1076
|
dryRun: args.dryRun,
|
|
813
1077
|
placeholderProtected: grandPlaceholderProtected,
|
|
814
1078
|
protectedSkipped: grandProtectedSkipped,
|
|
1079
|
+
residualUntranslated: allResidualUntranslated,
|
|
815
1080
|
});
|
|
816
1081
|
|
|
817
|
-
if (args.reportStdout || (!args.reportFile && allSkippedKeys.length > 0)) {
|
|
1082
|
+
if (args.reportStdout || (!args.reportFile && (allSkippedKeys.length > 0 || allResidualUntranslated.length > 0))) {
|
|
818
1083
|
console.log('');
|
|
819
1084
|
console.log(report);
|
|
820
1085
|
}
|
|
@@ -827,13 +1092,17 @@ async function run(args) {
|
|
|
827
1092
|
}
|
|
828
1093
|
|
|
829
1094
|
return {
|
|
830
|
-
success:
|
|
831
|
-
exitCode: ExitCodes.SUCCESS,
|
|
1095
|
+
success: allResidualUntranslated.length === 0,
|
|
1096
|
+
exitCode: allResidualUntranslated.length === 0 ? ExitCodes.SUCCESS : ExitCodes.VALIDATION_FAILED,
|
|
1097
|
+
error: allResidualUntranslated.length === 0 ? undefined : 'Auto Translate left untranslated placeholder values',
|
|
832
1098
|
total: grandTotal,
|
|
833
1099
|
translated: grandTranslated,
|
|
834
1100
|
skipped: grandSkipped,
|
|
835
1101
|
placeholderProtected: grandPlaceholderProtected,
|
|
836
1102
|
protectedSkipped: grandProtectedSkipped,
|
|
1103
|
+
skippedExisting: grandSkippedExisting,
|
|
1104
|
+
finalCheckRetried: grandFinalCheckRetried,
|
|
1105
|
+
residualUntranslated: allResidualUntranslated.length,
|
|
837
1106
|
};
|
|
838
1107
|
}
|
|
839
1108
|
|
|
@@ -852,6 +1121,7 @@ if (require.main === module) {
|
|
|
852
1121
|
module.exports = {
|
|
853
1122
|
parseArgs,
|
|
854
1123
|
resolveSourceFiles,
|
|
1124
|
+
isBrokenTranslationValue,
|
|
855
1125
|
processFile,
|
|
856
1126
|
run,
|
|
857
1127
|
};
|