i18ntk 4.1.0 → 4.2.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +51 -5
  2. package/README.md +79 -17
  3. package/SECURITY.md +10 -4
  4. package/main/i18ntk-analyze.js +10 -20
  5. package/main/i18ntk-backup.js +106 -44
  6. package/main/i18ntk-init.js +153 -157
  7. package/main/i18ntk-setup.js +36 -13
  8. package/main/i18ntk-translate.js +169 -21
  9. package/main/i18ntk-usage.js +272 -103
  10. package/main/i18ntk-validate.js +38 -31
  11. package/main/manage/commands/AnalyzeCommand.js +7 -17
  12. package/main/manage/commands/CommandRouter.js +6 -6
  13. package/main/manage/commands/TranslateCommand.js +65 -56
  14. package/main/manage/commands/ValidateCommand.js +34 -26
  15. package/main/manage/index.js +11 -42
  16. package/main/manage/managers/InteractiveMenu.js +11 -40
  17. package/main/manage/services/InitService.js +114 -118
  18. package/main/manage/services/UsageService.js +244 -85
  19. package/package.json +21 -14
  20. package/runtime/enhanced.d.ts +5 -5
  21. package/runtime/enhanced.js +49 -25
  22. package/runtime/i18ntk.d.ts +30 -7
  23. package/runtime/index.d.ts +48 -19
  24. package/runtime/index.js +175 -90
  25. package/settings/settings-cli.js +115 -38
  26. package/settings/settings-manager.js +24 -6
  27. package/ui-locales/de.json +192 -11
  28. package/ui-locales/en.json +182 -8
  29. package/ui-locales/es.json +193 -12
  30. package/ui-locales/fr.json +189 -8
  31. package/ui-locales/ja.json +190 -8
  32. package/ui-locales/ru.json +191 -9
  33. package/ui-locales/zh.json +194 -9
  34. package/utils/cli-helper.js +8 -12
  35. package/utils/config-helper.js +1 -1
  36. package/utils/config-manager.js +8 -6
  37. package/utils/localized-confirm.js +55 -0
  38. package/utils/menu-layout.js +41 -0
  39. package/utils/report-writer.js +110 -0
  40. package/utils/security.js +15 -22
  41. package/utils/translate/api.js +31 -3
  42. package/utils/translate/placeholder.js +42 -1
  43. package/utils/translate/report.js +3 -2
  44. package/utils/translate/safe-network.js +24 -4
  45. package/utils/usage-insights.js +435 -0
  46. package/utils/usage-source.js +50 -0
  47. package/utils/watch-locales.js +1 -8
@@ -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: 3)
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: 3)',
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: 3,
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) || 3; }
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 = clampInt(args.concurrency, 1, 25, 3);
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,95 @@ 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 isBrokenTranslationValue(value) {
342
+ if (typeof value !== 'string') return false;
343
+ const text = value.trim();
344
+ if (!text) return false;
345
+ if (text.includes('\uFFFD')) return true;
346
+
347
+ const compact = text.replace(/[\s.,!;:()[\]{}'"`~_\-]/g, '');
348
+ if (compact.length >= 1 && /^\?+$/.test(compact)) return true;
349
+ if (/\?{3,}/.test(text)) return true;
350
+
351
+ const questionCount = (text.match(/\?/g) || []).length;
352
+ const visibleLength = Math.max(text.replace(/\s/g, '').length, 1);
353
+ if (questionCount >= 3 && questionCount / visibleLength >= 0.5) return true;
354
+
355
+ if (/[\u0080-\u009F]/.test(text) && /[ÃÂÐÑ]/.test(text)) return true;
356
+
357
+ const mojibakePatterns = [
358
+ /[ÃÂ][\u0080-\u00BF]/,
359
+ /Ð[\u0080-\u00BF]/,
360
+ /Ñ[\u0080-\u00BF]/,
361
+ /ã[‚ƒ€]/,
362
+ ];
363
+ return mojibakePatterns.some((pattern) => pattern.test(text));
364
+ }
365
+
366
+ function shouldTranslateTargetValue(sourceValue, targetValue, args) {
367
+ if (targetValue === undefined || targetValue === null) return true;
368
+ if (typeof targetValue !== 'string') return true;
369
+ if (isUntranslatedMarker(targetValue)) return true;
370
+ if (isBrokenTranslationValue(targetValue)) return true;
371
+ if (targetValue.trim() === String(sourceValue ?? '').trim()) return true;
372
+ if (args.onlyMissingOrEnglish !== false && isLikelyEnglish(targetValue, args)) return true;
373
+ return args.onlyMissingOrEnglish === false;
374
+ }
375
+
376
+ function planTargetAwareLeaves(sourceLeaves, targetData, args) {
377
+ if (!targetData || args.onlyMissingOrEnglish === false) {
378
+ return {
379
+ translatableLeaves: sourceLeaves,
380
+ existingLeaves: [],
381
+ };
382
+ }
383
+
384
+ const translatableLeaves = [];
385
+ const existingLeaves = [];
386
+
387
+ for (const leaf of sourceLeaves) {
388
+ const targetValue = getLeaf(targetData, leaf.keyPath);
389
+ if (shouldTranslateTargetValue(leaf.value, targetValue, args)) {
390
+ translatableLeaves.push(leaf);
391
+ } else {
392
+ existingLeaves.push({ ...leaf, value: targetValue, skipReason: 'existing' });
393
+ }
394
+ }
395
+
396
+ return { translatableLeaves, existingLeaves };
397
+ }
398
+
292
399
  async function resolvePlaceholderStrategy(args) {
293
400
  const interactive = isInteractive({ noPrompt: args.noConfirm });
294
401
 
@@ -403,7 +510,11 @@ function prepareDirectBatch(toTranslate, customRegex, protection) {
403
510
 
404
511
  async function runTranslation(maskedBatch, targetLang, options) {
405
512
  const batchItems = maskedBatch.map((item) => ({ value: item.masked, keyPath: item.keyPath }));
406
- const results = await translateBatchInChunks(batchItems, targetLang, options);
513
+ const results = await translateBatchInChunks(batchItems, targetLang, {
514
+ ...options,
515
+ stageLabel: 'Translating strings',
516
+ progressUnit: 'strings',
517
+ });
407
518
  return results;
408
519
  }
409
520
 
@@ -428,6 +539,9 @@ async function translateBatchInChunks(batch, targetLang, options) {
428
539
  total: batch.length,
429
540
  chunkCompleted: info.completed,
430
541
  chunkTotal: info.total,
542
+ keyPath: info.keyPath,
543
+ stage: options.stageLabel || 'Translating',
544
+ unit: options.progressUnit || 'items',
431
545
  });
432
546
  }
433
547
  },
@@ -522,7 +636,11 @@ async function translatePreservedItems(items, targetLang, options, customRegex,
522
636
  return plan;
523
637
  });
524
638
 
525
- const translatedSegments = await translateBatchInChunks(segmentJobs, targetLang, options);
639
+ const translatedSegments = await translateBatchInChunks(segmentJobs, targetLang, {
640
+ ...options,
641
+ stageLabel: 'Translating placeholder-safe text segments',
642
+ progressUnit: 'segments',
643
+ });
526
644
 
527
645
  return plans.map((plan) => {
528
646
  const value = plan.segments.map((segment) => {
@@ -580,8 +698,8 @@ async function translateItems(toTranslate, targetLang, options, customRegex, pro
580
698
  return finalResults;
581
699
  }
582
700
 
583
- function applyResults(sourceData, translatedResults, toTranslate, toSkip) {
584
- const output = deepClone(sourceData);
701
+ function applyResults(sourceData, translatedResults, toTranslate, toSkip, targetData = null) {
702
+ const output = deepClone(targetData || sourceData);
585
703
 
586
704
  for (let i = 0; i < toTranslate.length; i++) {
587
705
  setLeaf(output, toTranslate[i].keyPath, translatedResults[i]);
@@ -594,6 +712,12 @@ function applyResults(sourceData, translatedResults, toTranslate, toSkip) {
594
712
  return output;
595
713
  }
596
714
 
715
+ function formatProgressKey(keyPath) {
716
+ if (!keyPath) return '';
717
+ const value = String(keyPath);
718
+ return value.length > 72 ? `${value.slice(0, 69)}...` : value;
719
+ }
720
+
597
721
  function writeOutput(outputData, outputPath, bom) {
598
722
  const resolvedOutputPath = path.resolve(process.cwd(), outputPath);
599
723
  const dir = path.dirname(resolvedOutputPath);
@@ -628,14 +752,17 @@ async function processFile(sourcePath, targetLang, args) {
628
752
  return { total: 0, translated: 0, skipped: 0, skippedKeys: [] };
629
753
  }
630
754
 
755
+ const targetData = readExistingTargetData(targetPath);
756
+ const { translatableLeaves: candidateLeaves, existingLeaves } = planTargetAwareLeaves(leaves, targetData, args);
757
+
631
758
  const protection = args.protection || loadProtectionConfig(args.protectionFile, {
632
759
  enabled: args.protectionEnabled,
633
760
  create: args.createProtectionFile,
634
761
  });
635
- const protectedLeaves = leaves
762
+ const protectedLeaves = candidateLeaves
636
763
  .filter((leaf) => shouldPreserveWholeValue(leaf.keyPath, leaf.value, protection))
637
764
  .map((leaf) => ({ ...leaf, skipReason: 'protected' }));
638
- const translatableLeaves = leaves.filter((leaf) => !shouldPreserveWholeValue(leaf.keyPath, leaf.value, protection));
765
+ const translatableLeaves = candidateLeaves.filter((leaf) => !shouldPreserveWholeValue(leaf.keyPath, leaf.value, protection));
639
766
  const { withPlaceholders, withoutPlaceholders } = classifyLeaves(translatableLeaves, args.customRegex);
640
767
  const { strategy, interactiveMode } = await resolvePlaceholderStrategy(args);
641
768
 
@@ -649,13 +776,17 @@ async function processFile(sourcePath, targetLang, args) {
649
776
  skippedKeys,
650
777
  placeholderProtected: 0,
651
778
  protectedSkipped: protectedLeaves.length,
779
+ skippedExisting: existingLeaves.length,
652
780
  dryRun: true,
653
781
  };
654
782
  }
655
783
 
656
784
  if (args.dryRun) {
657
785
  const protectedCount = strategy === 'send' ? 0 : withPlaceholders.length;
658
- console.log(`[${fileName}] Dry-run: ${leaves.length} strings would be translated.`);
786
+ console.log(`[${fileName}] Dry-run: ${candidateLeaves.length} of ${leaves.length} strings would be translated.`);
787
+ if (existingLeaves.length > 0) {
788
+ console.log(`[${fileName}] Dry-run: ${existingLeaves.length} existing translated strings would be kept.`);
789
+ }
659
790
  if (protectedLeaves.length > 0) {
660
791
  console.log(`[${fileName}] Dry-run: ${protectedLeaves.length} protected keys/values would be copied unchanged.`);
661
792
  }
@@ -667,11 +798,12 @@ async function processFile(sourcePath, targetLang, args) {
667
798
  }
668
799
  return {
669
800
  total: leaves.length,
670
- translated: leaves.length - protectedLeaves.length,
801
+ translated: candidateLeaves.length - protectedLeaves.length,
671
802
  skipped: protectedLeaves.length,
672
803
  skippedKeys: protectedLeaves,
673
804
  placeholderProtected: protectedCount,
674
805
  termProtected: hasProtectionRules(protection),
806
+ skippedExisting: existingLeaves.length,
675
807
  dryRun: true,
676
808
  };
677
809
  }
@@ -679,8 +811,9 @@ async function processFile(sourcePath, targetLang, args) {
679
811
  const decisions = await resolvePerKeyDecisions(withPlaceholders, interactiveMode);
680
812
  const { toTranslate, toSkip } = buildTranslateList(withPlaceholders, withoutPlaceholders, strategy, decisions);
681
813
  toSkip.push(...protectedLeaves);
814
+ toSkip.push(...existingLeaves);
682
815
  const placeholderProtected = toTranslate.filter((leaf) => leaf.placeholderMode === 'preserve').length;
683
- const placeholderSkipped = toSkip.filter((leaf) => leaf.skipReason !== 'protected').length;
816
+ const placeholderSkipped = toSkip.filter((leaf) => leaf.skipReason !== 'protected' && leaf.skipReason !== 'existing').length;
684
817
 
685
818
  if (placeholderSkipped > 0) {
686
819
  console.log(`[${fileName}] Skipping ${placeholderSkipped} keys with placeholders.`);
@@ -691,6 +824,9 @@ async function processFile(sourcePath, targetLang, args) {
691
824
  if (protectedLeaves.length > 0) {
692
825
  console.log(`[${fileName}] Copying ${protectedLeaves.length} protected keys/values unchanged.`);
693
826
  }
827
+ if (existingLeaves.length > 0) {
828
+ console.log(`[${fileName}] Keeping ${existingLeaves.length} existing translated keys.`);
829
+ }
694
830
  if (hasProtectionRules(protection)) {
695
831
  console.log(`[${fileName}] Protecting terms from: ${protection.filePath}`);
696
832
  }
@@ -708,7 +844,11 @@ async function processFile(sourcePath, targetLang, args) {
708
844
  customFn: args.translateFn,
709
845
  onProgress: (info) => {
710
846
  if (info.completed % args.progressInterval === 0 || info.completed === info.total) {
711
- process.stdout.write(`\r[${fileName}] Translating... ${info.completed}/${info.total}`);
847
+ const stage = info.stage || 'Translating';
848
+ const unit = info.unit || 'items';
849
+ const keyPath = formatProgressKey(info.keyPath);
850
+ const keySuffix = keyPath ? ` | ${keyPath}` : '';
851
+ process.stdout.write(`\r[${fileName}] ${stage}: ${info.completed}/${info.total} ${unit}${keySuffix}`);
712
852
  }
713
853
  },
714
854
  onError: (err) => {
@@ -719,8 +859,10 @@ async function processFile(sourcePath, targetLang, args) {
719
859
  let translatedResults;
720
860
  try {
721
861
  if (toTranslate.length > 0) {
862
+ console.log(`[${fileName}] Preparing translation plan for ${toTranslate.length} keys.`);
722
863
  translatedResults = await translateItems(toTranslate, targetLang, translateOptions, args.customRegex, protection);
723
864
  process.stdout.write('\n');
865
+ console.log(`[${fileName}] Applying translated values.`);
724
866
  } else {
725
867
  translatedResults = [];
726
868
  }
@@ -728,7 +870,8 @@ async function processFile(sourcePath, targetLang, args) {
728
870
  cleanupPlaceholderManifest(manifestPath);
729
871
  }
730
872
 
731
- const output = applyResults(sourceData, translatedResults, toTranslate, toSkip);
873
+ const output = applyResults(sourceData, translatedResults, toTranslate, toSkip, targetData);
874
+ console.log(`[${fileName}] Writing output.`);
732
875
  writeOutput(output, targetPath, args.bom);
733
876
 
734
877
  console.log(`[${fileName}] Written: ${targetPath}`);
@@ -736,10 +879,11 @@ async function processFile(sourcePath, targetLang, args) {
736
879
  return {
737
880
  total: leaves.length,
738
881
  translated: translatedResults.length,
739
- skipped: toSkip.length,
740
- skippedKeys: toSkip,
882
+ skipped: toSkip.filter((leaf) => leaf.skipReason !== 'existing').length,
883
+ skippedKeys: toSkip.filter((leaf) => leaf.skipReason !== 'existing'),
741
884
  placeholderProtected,
742
885
  protectedSkipped: protectedLeaves.length,
886
+ skippedExisting: existingLeaves.length,
743
887
  };
744
888
  }
745
889
 
@@ -787,6 +931,7 @@ async function run(args) {
787
931
  let grandSkipped = 0;
788
932
  let grandPlaceholderProtected = 0;
789
933
  let grandProtectedSkipped = 0;
934
+ let grandSkippedExisting = 0;
790
935
 
791
936
  for (const srcPath of sourceFiles) {
792
937
  const result = await processFile(srcPath, args.targetLang, args);
@@ -796,6 +941,7 @@ async function run(args) {
796
941
  grandSkipped += result.skipped;
797
942
  grandPlaceholderProtected += result.placeholderProtected || 0;
798
943
  grandProtectedSkipped += result.protectedSkipped || 0;
944
+ grandSkippedExisting += result.skippedExisting || 0;
799
945
  if (result.skippedKeys && result.skippedKeys.length > 0) {
800
946
  allSkippedKeys.push(...result.skippedKeys);
801
947
  }
@@ -803,7 +949,7 @@ async function run(args) {
803
949
  }
804
950
 
805
951
  console.log('');
806
- console.log(formatSummaryLine(grandSkipped, grandTranslated, grandTotal, grandPlaceholderProtected, grandProtectedSkipped));
952
+ console.log(formatSummaryLine(grandSkipped, grandTranslated, grandTotal, grandPlaceholderProtected, grandProtectedSkipped, grandSkippedExisting));
807
953
 
808
954
  if (allSkippedKeys.length > 0 || args.reportFile || args.reportStdout) {
809
955
  const report = generateReport(allSkippedKeys, grandTranslated, grandTotal, {
@@ -834,6 +980,7 @@ async function run(args) {
834
980
  skipped: grandSkipped,
835
981
  placeholderProtected: grandPlaceholderProtected,
836
982
  protectedSkipped: grandProtectedSkipped,
983
+ skippedExisting: grandSkippedExisting,
837
984
  };
838
985
  }
839
986
 
@@ -852,6 +999,7 @@ if (require.main === module) {
852
999
  module.exports = {
853
1000
  parseArgs,
854
1001
  resolveSourceFiles,
1002
+ isBrokenTranslationValue,
855
1003
  processFile,
856
1004
  run,
857
1005
  };