i18ntk 4.2.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 CHANGED
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [4.2.1] - 2026-05-31
9
+
10
+ ### Changed
11
+ - Auto Translate now treats uppercase target-language placeholders such as `[AR] What We Offer` as untranslated target values when the bracketed code matches the target language, so target-aware mode sends the source text for translation instead of keeping the placeholder copy.
12
+ - Auto Translate now performs a final pre-write leftover check and retries values that still look like placeholder-prefixed untranslated text, untranslated markers, source-language copies, or broken output.
13
+ - Auto Translate reports leftover values in the post-translation report and exits with validation failure when leftovers remain after the final retry, instead of reporting a clean completion.
14
+
15
+ ### Fixed
16
+ - Usage analysis no longer writes its inferred app source fallback, such as `src`, back into the shared locale configuration when `sourceDir` and `i18nDir` are both the locale directory.
17
+ - Manager sizing now reads the configured i18n directory unless `--source-dir` is explicitly provided, so running sizing after usage no longer silently analyzes the wrong directory.
18
+ - Manager sizing now treats a failed sizing analysis as a command failure instead of printing a generic operation success.
19
+ - Validation summary reports now include warning and error details, including content-risk warning payloads, instead of only totals.
20
+
8
21
  ## [4.2.0] - 2026-05-30
9
22
 
10
23
  ### Security
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # i18ntk v4.2.0
1
+ # i18ntk v4.2.1
2
2
 
3
3
  A i18n toolkit - A zero-dependency internationalization toolkit for setup, scanning, analysis, validation, usage tracking, translation completion, automatic JSON locale translation, reporting, and runtime translation loading.
4
4
 
@@ -9,7 +9,7 @@ A i18n toolkit - A zero-dependency internationalization toolkit for setup, scann
9
9
  [![node](https://img.shields.io/badge/node-%3E%3D16-339933)](https://nodejs.org)
10
10
  [![dependencies](https://img.shields.io/badge/dependencies-0-success)](https://www.npmjs.com/package/i18ntk)
11
11
  [![license](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE)
12
- [![socket](https://socket.dev/api/badge/npm/package/i18ntk/4.2.0)](https://socket.dev/npm/package/i18ntk/overview/4.2.0)
12
+ [![socket](https://socket.dev/api/badge/npm/package/i18ntk/4.2.1)](https://socket.dev/npm/package/i18ntk/overview/4.2.1)
13
13
 
14
14
  ## Install
15
15
 
@@ -30,20 +30,14 @@ Requirements:
30
30
  - npm `>=8.0.0`
31
31
  - No runtime dependencies
32
32
 
33
- ## What's New in 4.2.0
34
-
35
- - **SECURITY**: Hardened path containment for restore and shared filesystem helpers, including artifact-like filenames, environment-added internal prefixes, and Windows cross-drive paths.
36
- - **SECURITY**: Runtime locale loading now rejects unsafe language identifiers before resolving locale files, preventing `../` language names from reading JSON outside the configured locale base.
37
- - **SECURITY**: Auto Translate provider URL checks now block IPv4-mapped IPv6 private/loopback hosts.
38
- - **REPORTS**: Init and analysis reports now default to readable Markdown. Set `reports.format` to `markdown`, `json`, or `text` in Settings or `.i18ntk-config`.
39
- - **USAGE**: Usage analysis no longer scans the project root when locales are also configured as the source directory, avoiding inflated missing-key counts.
40
- - **I18N UX**: Init backup prompts, completion summaries, report prompts, default target languages, and native yes/no confirmations are now localized.
41
- - **AUTO TRANSLATE**: Auto Translate is out of beta, keeps existing translated target values by default, and only sends missing/source-copy/likely-English strings unless `--translate-all` is used.
42
- - **AUTO TRANSLATE**: Google Auto Translate concurrency now defaults to 12 and can be raised up to 100 for larger locale sets.
43
- - **AUTO TRANSLATE**: Corrupt target strings such as `?????`, replacement characters, and common mojibake are now repaired from the English source, and progress output distinguishes key translation from placeholder-safe text-segment translation.
44
- - **CLI UX**: Manager menu spacing is grouped and aligned, and validation no longer prints duplicate source/i18n/output directory blocks.
45
- - **DOCS**: Versioned docs and migration guidance now reflect the current 4.2.0 command surface.
46
- - **CLEANUP**: Removed stale duplicate fixed artifacts from the development tree to reduce audit and supply-chain drift.
33
+ ## What's New in 4.2.1
34
+
35
+ - **AUTO TRANSLATE**: Existing target values like `[AR] What We Offer` are now treated as untranslated placeholders for the matching target language and are translated from the source text.
36
+ - **AUTO TRANSLATE**: Before writing each output file, Auto Translate now performs a final leftover check and retries any placeholder-prefixed or source-copy values once.
37
+ - **AUTO TRANSLATE**: If leftovers remain after the final retry, the command warns, includes them in the report, recommends rerunning Auto Translate, and exits with validation failure instead of reporting a clean completion.
38
+ - **SIZING/USAGE**: Usage analysis no longer writes its inferred app source fallback back into the shared locale config, so running usage before sizing no longer makes sizing analyze the wrong directory.
39
+ - **VALIDATION REPORTS**: Validation summary files now include warning and error details, including English-content warning payloads, instead of only totals.
40
+ - **DOCS**: Versioned docs and migration guidance now reflect the current 4.2.1 command surface.
47
41
 
48
42
  ## What's New in 4.1.0
49
43
 
@@ -509,7 +503,7 @@ Example:
509
503
 
510
504
  ```json
511
505
  {
512
- "version": "4.2.0",
506
+ "version": "4.2.1",
513
507
  "sourceDir": "./locales",
514
508
  "i18nDir": "./locales",
515
509
  "outputDir": "./i18ntk-reports",
@@ -571,7 +565,7 @@ The public package manifest includes `readmeFilename: "README.md"`, and the rele
571
565
  - [Auto Translate Guide](./docs/auto-translate.md)
572
566
  - [Scanner Guide](./docs/scanner-guide.md)
573
567
  - [Environment Variables](./docs/environment-variables.md)
574
- - [Migration Guide v4.2.0](./docs/migration-guide-v4.2.0.md)
568
+ - [Migration Guide v4.2.1](./docs/migration-guide-v4.2.1.md)
575
569
 
576
570
  ## Security
577
571
 
@@ -31,12 +31,13 @@
31
31
  * i18ntk sizing --threshold=30 --output-report
32
32
  */
33
33
 
34
- const fs = require('fs');
35
- const path = require('path');
36
- const { loadTranslations, t } = require('../utils/i18n-helper');
37
- const configManager = require('../settings/settings-manager');
38
- const SecurityUtils = require('../utils/security');
39
- const { getUnifiedConfig } = require('../utils/config-helper');
34
+ const fs = require('fs');
35
+ const path = require('path');
36
+ const { loadTranslations, t } = require('../utils/i18n-helper');
37
+ const configManager = require('../settings/settings-manager');
38
+ const projectConfigManager = require('../utils/config-manager');
39
+ const SecurityUtils = require('../utils/security');
40
+ const { getUnifiedConfig } = require('../utils/config-helper');
40
41
  const { logger } = require('../utils/logger');
41
42
  const { getGlobalReadline, closeGlobalReadline } = require('../utils/cli');
42
43
  const SetupEnforcer = require('../utils/setup-enforcer');
@@ -1120,8 +1121,10 @@ Generated: ${new Date().toISOString()}
1120
1121
  'l': '',
1121
1122
  'o': true,
1122
1123
  'f': 'table',
1123
- 't': 50,
1124
- 'd': false
1124
+ 't': 50,
1125
+ 'd': false,
1126
+ sourceDirExplicit: false,
1127
+ outputDirExplicit: false
1125
1128
  };
1126
1129
 
1127
1130
  for (let i = 0; i < args.length; i++) {
@@ -1138,9 +1141,10 @@ Generated: ${new Date().toISOString()}
1138
1141
  const key = keyValueMatch[1];
1139
1142
  const value = keyValueMatch[2];
1140
1143
 
1141
- if (key === 'source-dir' || key === 's') {
1142
- options['source-dir'] = value;
1143
- options.s = value;
1144
+ if (key === 'source-dir' || key === 's') {
1145
+ options['source-dir'] = value;
1146
+ options.s = value;
1147
+ options.sourceDirExplicit = true;
1144
1148
  } else if (key === 'languages' || key === 'l') {
1145
1149
  options.languages = value;
1146
1150
  options.l = value;
@@ -1167,9 +1171,10 @@ Generated: ${new Date().toISOString()}
1167
1171
  options['detailed-keys'] = value.toLowerCase() !== 'false';
1168
1172
  } else if (key === 'predict-expansion') {
1169
1173
  options['predict-expansion'] = value.toLowerCase() !== 'false';
1170
- } else if (key === 'output-dir') {
1171
- options['output-dir'] = value;
1172
- }
1174
+ } else if (key === 'output-dir') {
1175
+ options['output-dir'] = value;
1176
+ options.outputDirExplicit = true;
1177
+ }
1173
1178
  continue;
1174
1179
  }
1175
1180
 
@@ -1179,10 +1184,11 @@ Generated: ${new Date().toISOString()}
1179
1184
  const key = match[1];
1180
1185
  const nextArg = args[i + 1];
1181
1186
 
1182
- if (key === 'source-dir' || key === 's') {
1183
- options['source-dir'] = nextArg || options['source-dir'];
1184
- options.s = options['source-dir'];
1185
- if (nextArg && !nextArg.startsWith('-')) i++;
1187
+ if (key === 'source-dir' || key === 's') {
1188
+ options['source-dir'] = nextArg || options['source-dir'];
1189
+ options.s = options['source-dir'];
1190
+ options.sourceDirExplicit = true;
1191
+ if (nextArg && !nextArg.startsWith('-')) i++;
1186
1192
  } else if (key === 'languages' || key === 'l') {
1187
1193
  options.languages = nextArg || options.languages;
1188
1194
  options.l = options.languages;
@@ -1238,6 +1244,7 @@ Generated: ${new Date().toISOString()}
1238
1244
  }
1239
1245
  } else if (key === 'output-dir') {
1240
1246
  options['output-dir'] = nextArg || options['output-dir'];
1247
+ options.outputDirExplicit = true;
1241
1248
  if (nextArg && !nextArg.startsWith('-')) i++;
1242
1249
  }
1243
1250
  }
@@ -1268,15 +1275,25 @@ Options:
1268
1275
  return options;
1269
1276
  }
1270
1277
 
1271
- // Add run method for compatibility with manager
1272
- async run(options = {}) {
1273
- const { fromMenu = false } = options;
1274
-
1275
- const args = this.parseArgs();
1276
- const config = await getUnifiedConfig('sizing', args);
1277
-
1278
- this.sourceDir = path.resolve(config.projectRoot || '.', config.sourceDir || './locales');
1279
- this.outputDir = path.resolve(config.projectRoot || '.', config.outputDir || './i18ntk-reports');
1278
+ // Add run method for compatibility with manager
1279
+ async run(options = {}) {
1280
+ const { fromMenu = false } = options;
1281
+
1282
+ const args = this.parseArgs();
1283
+ const commonArgs = {};
1284
+ if (args.sourceDirExplicit) commonArgs.sourceDir = args['source-dir'];
1285
+ if (args.outputDirExplicit) commonArgs.outputDir = args['output-dir'];
1286
+ if (args['source-language']) commonArgs.sourceLanguage = args['source-language'];
1287
+ const persistedConfig = projectConfigManager.getConfig();
1288
+ const config = await getUnifiedConfig('sizing', commonArgs);
1289
+ const projectRoot = config.projectRoot || persistedConfig.projectRoot || process.cwd();
1290
+ const configuredSizingDir = persistedConfig.scriptDirectories?.sizing;
1291
+ const translationDir = args.sourceDirExplicit
1292
+ ? args['source-dir']
1293
+ : (configuredSizingDir || persistedConfig.i18nDir || persistedConfig.sourceDir || config.i18nDir || config.sourceDir || './locales');
1294
+
1295
+ this.sourceDir = path.resolve(projectRoot, translationDir);
1296
+ this.outputDir = path.resolve(config.projectRoot || '.', config.outputDir || './i18ntk-reports');
1280
1297
  this.threshold = args.threshold ?? config.processing?.sizingThreshold ?? 50;
1281
1298
  this.languages = args.languages ? args.languages.split(',').map(l => l.trim()) : [];
1282
1299
  this.outputReport = args['output-report'] !== undefined ? args['output-report'] : false;
@@ -338,6 +338,36 @@ function isLikelyEnglish(value, args) {
338
338
  return analysis.englishPercentage > threshold && analysis.englishWordCount >= 2;
339
339
  }
340
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
+
341
371
  function isBrokenTranslationValue(value) {
342
372
  if (typeof value !== 'string') return false;
343
373
  const text = value.trim();
@@ -368,11 +398,22 @@ function shouldTranslateTargetValue(sourceValue, targetValue, args) {
368
398
  if (typeof targetValue !== 'string') return true;
369
399
  if (isUntranslatedMarker(targetValue)) return true;
370
400
  if (isBrokenTranslationValue(targetValue)) return true;
401
+ if (isLanguagePrefixedEnglish(targetValue, args)) return true;
371
402
  if (targetValue.trim() === String(sourceValue ?? '').trim()) return true;
372
403
  if (args.onlyMissingOrEnglish !== false && isLikelyEnglish(targetValue, args)) return true;
373
404
  return args.onlyMissingOrEnglish === false;
374
405
  }
375
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
+
376
417
  function planTargetAwareLeaves(sourceLeaves, targetData, args) {
377
418
  if (!targetData || args.onlyMissingOrEnglish === false) {
378
419
  return {
@@ -712,6 +753,41 @@ function applyResults(sourceData, translatedResults, toTranslate, toSkip, target
712
753
  return output;
713
754
  }
714
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
+
715
791
  function formatProgressKey(keyPath) {
716
792
  if (!keyPath) return '';
717
793
  const value = String(keyPath);
@@ -735,6 +811,7 @@ async function processFile(sourcePath, targetLang, args) {
735
811
  const fileName = path.basename(sourcePath);
736
812
  const targetDir = args.outputDir || path.join(path.dirname(path.dirname(sourcePath)), targetLang);
737
813
  const targetPath = path.join(targetDir, fileName);
814
+ const runArgs = { ...args, targetLang };
738
815
 
739
816
  let sourceData;
740
817
  try {
@@ -753,18 +830,18 @@ async function processFile(sourcePath, targetLang, args) {
753
830
  }
754
831
 
755
832
  const targetData = readExistingTargetData(targetPath);
756
- const { translatableLeaves: candidateLeaves, existingLeaves } = planTargetAwareLeaves(leaves, targetData, args);
833
+ const { translatableLeaves: candidateLeaves, existingLeaves } = planTargetAwareLeaves(leaves, targetData, runArgs);
757
834
 
758
- const protection = args.protection || loadProtectionConfig(args.protectionFile, {
759
- enabled: args.protectionEnabled,
760
- create: args.createProtectionFile,
835
+ const protection = runArgs.protection || loadProtectionConfig(runArgs.protectionFile, {
836
+ enabled: runArgs.protectionEnabled,
837
+ create: runArgs.createProtectionFile,
761
838
  });
762
839
  const protectedLeaves = candidateLeaves
763
840
  .filter((leaf) => shouldPreserveWholeValue(leaf.keyPath, leaf.value, protection))
764
841
  .map((leaf) => ({ ...leaf, skipReason: 'protected' }));
765
842
  const translatableLeaves = candidateLeaves.filter((leaf) => !shouldPreserveWholeValue(leaf.keyPath, leaf.value, protection));
766
- const { withPlaceholders, withoutPlaceholders } = classifyLeaves(translatableLeaves, args.customRegex);
767
- const { strategy, interactiveMode } = await resolvePlaceholderStrategy(args);
843
+ const { withPlaceholders, withoutPlaceholders } = classifyLeaves(translatableLeaves, runArgs.customRegex);
844
+ const { strategy, interactiveMode } = await resolvePlaceholderStrategy(runArgs);
768
845
 
769
846
  if (args.dryRun && strategy === 'skip' && withPlaceholders.length > 0) {
770
847
  await previewSkipped(withPlaceholders);
@@ -834,16 +911,16 @@ async function processFile(sourcePath, targetLang, args) {
834
911
  const manifestPath = createPlaceholderManifest(sourcePath, targetLang, toTranslate);
835
912
 
836
913
  const translateOptions = {
837
- sourceLang: args.sourceLang,
838
- provider: args.provider,
839
- concurrency: args.concurrency,
840
- batchSize: args.batchSize,
841
- retryCount: args.retryCount,
842
- retryDelay: args.retryDelay,
843
- timeout: args.timeout,
844
- customFn: args.translateFn,
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,
845
922
  onProgress: (info) => {
846
- if (info.completed % args.progressInterval === 0 || info.completed === info.total) {
923
+ if (info.completed % runArgs.progressInterval === 0 || info.completed === info.total) {
847
924
  const stage = info.stage || 'Translating';
848
925
  const unit = info.unit || 'items';
849
926
  const keyPath = formatProgressKey(info.keyPath);
@@ -860,7 +937,7 @@ async function processFile(sourcePath, targetLang, args) {
860
937
  try {
861
938
  if (toTranslate.length > 0) {
862
939
  console.log(`[${fileName}] Preparing translation plan for ${toTranslate.length} keys.`);
863
- translatedResults = await translateItems(toTranslate, targetLang, translateOptions, args.customRegex, protection);
940
+ translatedResults = await translateItems(toTranslate, targetLang, translateOptions, runArgs.customRegex, protection);
864
941
  process.stdout.write('\n');
865
942
  console.log(`[${fileName}] Applying translated values.`);
866
943
  } else {
@@ -871,8 +948,33 @@ async function processFile(sourcePath, targetLang, args) {
871
948
  }
872
949
 
873
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
+
874
976
  console.log(`[${fileName}] Writing output.`);
875
- writeOutput(output, targetPath, args.bom);
977
+ writeOutput(output, targetPath, runArgs.bom);
876
978
 
877
979
  console.log(`[${fileName}] Written: ${targetPath}`);
878
980
 
@@ -884,6 +986,8 @@ async function processFile(sourcePath, targetLang, args) {
884
986
  placeholderProtected,
885
987
  protectedSkipped: protectedLeaves.length,
886
988
  skippedExisting: existingLeaves.length,
989
+ finalCheckRetried,
990
+ residualUntranslated,
887
991
  };
888
992
  }
889
993
 
@@ -932,6 +1036,8 @@ async function run(args) {
932
1036
  let grandPlaceholderProtected = 0;
933
1037
  let grandProtectedSkipped = 0;
934
1038
  let grandSkippedExisting = 0;
1039
+ let grandFinalCheckRetried = 0;
1040
+ const allResidualUntranslated = [];
935
1041
 
936
1042
  for (const srcPath of sourceFiles) {
937
1043
  const result = await processFile(srcPath, args.targetLang, args);
@@ -942,25 +1048,38 @@ async function run(args) {
942
1048
  grandPlaceholderProtected += result.placeholderProtected || 0;
943
1049
  grandProtectedSkipped += result.protectedSkipped || 0;
944
1050
  grandSkippedExisting += result.skippedExisting || 0;
1051
+ grandFinalCheckRetried += result.finalCheckRetried || 0;
945
1052
  if (result.skippedKeys && result.skippedKeys.length > 0) {
946
1053
  allSkippedKeys.push(...result.skippedKeys);
947
1054
  }
1055
+ if (result.residualUntranslated && result.residualUntranslated.length > 0) {
1056
+ allResidualUntranslated.push(...result.residualUntranslated);
1057
+ }
948
1058
  }
949
1059
  }
950
1060
 
951
1061
  console.log('');
952
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
+ }
953
1071
 
954
- if (allSkippedKeys.length > 0 || args.reportFile || args.reportStdout) {
1072
+ if (allSkippedKeys.length > 0 || allResidualUntranslated.length > 0 || args.reportFile || args.reportStdout) {
955
1073
  const report = generateReport(allSkippedKeys, grandTranslated, grandTotal, {
956
1074
  sourceFile: sourceFiles.length === 1 ? sourceFiles[0] : `${sourceFiles.length} files`,
957
1075
  targetLang: args.targetLang,
958
1076
  dryRun: args.dryRun,
959
1077
  placeholderProtected: grandPlaceholderProtected,
960
1078
  protectedSkipped: grandProtectedSkipped,
1079
+ residualUntranslated: allResidualUntranslated,
961
1080
  });
962
1081
 
963
- if (args.reportStdout || (!args.reportFile && allSkippedKeys.length > 0)) {
1082
+ if (args.reportStdout || (!args.reportFile && (allSkippedKeys.length > 0 || allResidualUntranslated.length > 0))) {
964
1083
  console.log('');
965
1084
  console.log(report);
966
1085
  }
@@ -973,14 +1092,17 @@ async function run(args) {
973
1092
  }
974
1093
 
975
1094
  return {
976
- success: true,
977
- 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',
978
1098
  total: grandTotal,
979
1099
  translated: grandTranslated,
980
1100
  skipped: grandSkipped,
981
1101
  placeholderProtected: grandPlaceholderProtected,
982
1102
  protectedSkipped: grandProtectedSkipped,
983
1103
  skippedExisting: grandSkippedExisting,
1104
+ finalCheckRetried: grandFinalCheckRetried,
1105
+ residualUntranslated: allResidualUntranslated.length,
984
1106
  };
985
1107
  }
986
1108
 
@@ -30,8 +30,11 @@ class SizingCommand {
30
30
  try {
31
31
  const I18nSizingAnalyzer = require('../../i18ntk-sizing');
32
32
  const sizingAnalyzer = new I18nSizingAnalyzer();
33
- await sizingAnalyzer.run(options);
34
- return { success: true, command: 'sizing' };
33
+ const result = await sizingAnalyzer.run(options);
34
+ if (result && result.success === false) {
35
+ throw new Error(result.error || 'Sizing analysis failed');
36
+ }
37
+ return { success: true, command: 'sizing', result };
35
38
  } catch (error) {
36
39
  console.error(`Sizing command failed: ${error.message}`);
37
40
  throw error;
@@ -254,6 +254,14 @@ class TranslateCommand {
254
254
  for (const r of results) {
255
255
  console.log(` ${r.ok ? '\u{2705}' : '\u{274C}'} ${r.lang}${r.error ? ' (' + r.error + ')' : ''}`);
256
256
  }
257
+ const failed = results.filter(r => !r.ok);
258
+ if (failed.length > 0) {
259
+ const message = failed.length === 1
260
+ ? `Translation finished with warnings for ${failed[0].lang}. Rerun Auto Translate to capture leftovers.`
261
+ : `Translation finished with warnings for ${failed.length} languages. Rerun Auto Translate to capture leftovers.`;
262
+ console.log('\n ' + this.tr('translate.summary.incomplete', { count: failed.length }, message));
263
+ return { success: false, results, error: message };
264
+ }
257
265
  console.log('\n ' + this.tr('translate.summary.complete', {}, 'Translation complete!'));
258
266
  return { success: true, results };
259
267
  }
@@ -669,6 +669,30 @@ class ValidateCommand {
669
669
  );
670
670
  });
671
671
 
672
+ if (this.errors.length > 0) {
673
+ lines.push('');
674
+ lines.push('Errors');
675
+ lines.push('------');
676
+ this.errors.forEach((error, index) => {
677
+ lines.push(`${index + 1}. ${error.message}`);
678
+ if (error.details && Object.keys(error.details).length > 0) {
679
+ lines.push(` Details: ${JSON.stringify(error.details, null, 2).replace(/\n/g, '\n ')}`);
680
+ }
681
+ });
682
+ }
683
+
684
+ if (this.warnings.length > 0) {
685
+ lines.push('');
686
+ lines.push('Warnings');
687
+ lines.push('--------');
688
+ this.warnings.forEach((warning, index) => {
689
+ lines.push(`${index + 1}. ${warning.message}`);
690
+ if (warning.details && Object.keys(warning.details).length > 0) {
691
+ lines.push(` Details: ${JSON.stringify(warning.details, null, 2).replace(/\n/g, '\n ')}`);
692
+ }
693
+ });
694
+ }
695
+
672
696
  SecurityUtils.safeWriteFileSync(reportPath, lines.join('\n') + '\n', process.cwd(), 'utf8');
673
697
  return reportPath;
674
698
  } catch (error) {
@@ -1476,12 +1476,12 @@ class UsageService {
1476
1476
  this.sourceLanguageDir = path.join(this.i18nDir, this.config.sourceLanguage);
1477
1477
  }
1478
1478
 
1479
- if (this.sourceDir || this.i18nDir) {
1480
- await configManager.updateConfig({
1481
- sourceDir: configManager.toRelative(this.sourceDir || this.config.sourceDir),
1482
- i18nDir: configManager.toRelative(this.i18nDir || this.config.i18nDir)
1483
- });
1484
- }
1479
+ if (args.sourceDir || args.i18nDir) {
1480
+ await configManager.updateConfig({
1481
+ sourceDir: configManager.toRelative(this.sourceDir || this.config.sourceDir),
1482
+ i18nDir: configManager.toRelative(this.i18nDir || this.config.i18nDir)
1483
+ });
1484
+ }
1485
1485
 
1486
1486
  const usageSource = resolveUsageSourceDir({
1487
1487
  sourceDir: this.sourceDir || this.config.sourceDir,
@@ -1494,11 +1494,6 @@ class UsageService {
1494
1494
  }
1495
1495
  this.sourceDir = usageSource.sourceDir;
1496
1496
  this.config.sourceDir = usageSource.sourceDir;
1497
- if (this.sourceDir) {
1498
- await configManager.updateConfig({
1499
- sourceDir: configManager.toRelative(this.sourceDir)
1500
- });
1501
- }
1502
1497
 
1503
1498
  // 🚧 prevent scanning locales as source
1504
1499
  if (this.sourceDir && !args.sourceDir && path.resolve(this.sourceDir) === path.resolve(this.i18nDir)) {
@@ -1511,11 +1506,8 @@ class UsageService {
1511
1506
  console.warn(`⚠️ Fallback directory ${fallback} does not exist. Using project root for source scanning.`);
1512
1507
  this.sourceDir = path.resolve(this.config.projectRoot || '.');
1513
1508
  }
1514
- this.config.sourceDir = this.sourceDir;
1515
- await configManager.updateConfig({
1516
- sourceDir: configManager.toRelative(this.sourceDir)
1517
- });
1518
- }
1509
+ this.config.sourceDir = this.sourceDir;
1510
+ }
1519
1511
 
1520
1512
  console.log(t('usage.detectedSourceDirectory', { sourceDir: this.sourceDir || t('usage.noSourceDirectoryConfigured') || '(none)' }));
1521
1513
  console.log(t('usage.detectedI18nDirectory', { i18nDir: this.i18nDir }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18ntk",
3
- "version": "4.2.0",
3
+ "version": "4.2.1",
4
4
  "description": "i18n Tool Kit - Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, auto translation, fixing, reporting, and runtime translation loading.",
5
5
  "readmeFilename": "README.md",
6
6
  "keywords": [
@@ -168,27 +168,25 @@
168
168
  },
169
169
  "preferGlobal": true,
170
170
  "versionInfo": {
171
- "version": "4.2.0",
172
- "releaseDate": "30/05/2026",
173
- "lastUpdated": "30/05/2026",
171
+ "version": "4.2.1",
172
+ "releaseDate": "31/05/2026",
173
+ "lastUpdated": "31/05/2026",
174
174
  "maintainer": "Vlad Noskov",
175
175
  "changelog": "./CHANGELOG.md",
176
176
  "documentation": "./README.md",
177
177
  "apiReference": "./docs/api/API_REFERENCE.md",
178
178
  "majorChanges": [
179
- "SECURITY: Path containment, backup restore, runtime language loading, and Auto Translate provider URL validation are hardened.",
180
- "AUTO TRANSLATE: The manager and docs no longer mark Auto Translate beta; existing translated target values are kept by default, and Google concurrency can now be raised up to 100.",
181
- "RUNTIME: The lightweight runtime gains per-call language overrides, translateBatch(), cache helpers, and safer JSON parsing.",
182
- "REPORTS: Init and analysis reports default to readable Markdown, with pretty JSON and text available through reports.format.",
183
- "UX: Manager menu spacing is grouped and validation output no longer repeats source/i18n/output directory blocks.",
184
- "USAGE: Usage analysis avoids scanning the project root when locale and source directories are the same.",
185
- "USAGE: Usage reports now index known keys to source locations, resolve bounded dynamic key patterns, report unresolved dynamic expressions, recommend route-based namespaces, and flag hardcoded text candidates."
179
+ "AUTO TRANSLATE: Uppercase target-language placeholders such as [AR] What We Offer are detected, translated from source, and checked again before completion.",
180
+ "AUTO TRANSLATE: Runs now warn, report, and exit with validation failure if placeholder-prefixed or source-copy leftovers remain after the final retry.",
181
+ "SIZING: Manager sizing uses the configured i18n directory unless --source-dir is explicitly provided, so usage analysis no longer poisons the next sizing run.",
182
+ "VALIDATION: Validation summary reports now include warning and error details, not only totals.",
183
+ "USAGE: Usage fallback source detection no longer writes its inferred app source directory back into the shared locale configuration."
186
184
  ],
187
185
  "breakingChanges": [
188
186
  "i18ntk/runtime module-level helpers keep the first initialized runtime configuration for compatibility instead of being overwritten by later initRuntime() calls.",
189
187
  "utils/watch-locales.js returns a callable watcher object with EventEmitter methods and stop(); existing bare stop-function usage remains supported."
190
188
  ],
191
- "nextVersion": "4.2.1",
189
+ "nextVersion": "4.2.2",
192
190
  "supportedNodeVersions": ">=16.0.0",
193
191
  "supportedFrameworks": {
194
192
  "react-i18next": ">=11.0.0",
@@ -210,7 +208,7 @@
210
208
  "spring-boot": ">=2.5.0",
211
209
  "laravel": ">=8.0.0"
212
210
  },
213
- "supportPolicy": "Versions earlier than 4.2.0 may be unstable or insecure. Upgrade to 4.2.0 or newer."
211
+ "supportPolicy": "Versions earlier than 4.2.1 may be unstable or insecure. Upgrade to 4.2.1 or newer."
214
212
  },
215
- "readme": "# i18ntk v4.2.0\n\nA i18n toolkit - A zero-dependency internationalization toolkit for setup, scanning, analysis, validation, usage tracking, translation completion, automatic JSON locale translation, reporting, and runtime translation loading.\n\n![i18ntk Logo](https://raw.githubusercontent.com/vladnoskv/i18ntk/main/docs/screenshots/i18ntk-logo-public.PNG)\n\n[![npm version](https://img.shields.io/npm/v/i18ntk.svg?color=brightgreen)](https://www.npmjs.com/package/i18ntk)\n[![npm downloads](https://img.shields.io/npm/dt/i18ntk.svg)](https://www.npmjs.com/package/i18ntk)\n[![node](https://img.shields.io/badge/node-%3E%3D16-339933)](https://nodejs.org)\n[![dependencies](https://img.shields.io/badge/dependencies-0-success)](https://www.npmjs.com/package/i18ntk)\n[![license](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE)\n[![socket](https://socket.dev/api/badge/npm/package/i18ntk/4.2.0)](https://socket.dev/npm/package/i18ntk/overview/4.2.0)\n\n## Install\n\n```bash\n# global CLI use\nnpm install -g i18ntk\n\n# local project use\nnpm install --save-dev i18ntk\n\n# one-off execution\nnpx i18ntk --help\n```\n\nRequirements:\n\n- Node.js `>=16.0.0`\n- npm `>=8.0.0`\n- No runtime dependencies\n\n## What's New in 4.2.0\n\n- **SECURITY**: Hardened path containment for restore and shared filesystem helpers, including artifact-like filenames, environment-added internal prefixes, and Windows cross-drive paths.\n- **SECURITY**: Runtime locale loading now rejects unsafe language identifiers before resolving locale files, preventing `../` language names from reading JSON outside the configured locale base.\n- **SECURITY**: Auto Translate provider URL checks now block IPv4-mapped IPv6 private/loopback hosts.\n- **REPORTS**: Init and analysis reports now default to readable Markdown. Set `reports.format` to `markdown`, `json`, or `text` in Settings or `.i18ntk-config`.\n- **USAGE**: Usage analysis no longer scans the project root when locales are also configured as the source directory, avoiding inflated missing-key counts.\n- **I18N UX**: Init backup prompts, completion summaries, report prompts, default target languages, and native yes/no confirmations are now localized.\n- **AUTO TRANSLATE**: Auto Translate is out of beta, keeps existing translated target values by default, and only sends missing/source-copy/likely-English strings unless `--translate-all` is used.\n- **AUTO TRANSLATE**: Google Auto Translate concurrency now defaults to 12 and can be raised up to 100 for larger locale sets.\n- **AUTO TRANSLATE**: Corrupt target strings such as `?????`, replacement characters, and common mojibake are now repaired from the English source, and progress output distinguishes key translation from placeholder-safe text-segment translation.\n- **CLI UX**: Manager menu spacing is grouped and aligned, and validation no longer prints duplicate source/i18n/output directory blocks.\n- **DOCS**: Versioned docs and migration guidance now reflect the current 4.2.0 command surface.\n- **CLEANUP**: Removed stale duplicate fixed artifacts from the development tree to reduce audit and supply-chain drift.\n\n## What's New in 4.1.0\n\n- **FIX**: Critical and high-impact bugs resolved across the v4.0.0 feature set — runtime staleness crashes, backup hash-chain verification, sizing adminAuth crash, scanner `--source-language` propagation, watch callback subscriptions, dead key detection performance, validator key style enforcement, and protection Unicode boundary handling. See [CHANGELOG.md](./CHANGELOG.md) for complete details.\n\n## What's New in 4.0.0\n\n- **SIZING**: `--predict-expansion` flag computes per-key expansion ratios across languages with Safe/Warning/Critical risk tiers for UI layout planning.\n- **WATCH**: `watchLocales()` now returns an EventEmitter-compatible watcher with debounced `change`/`add`/`unlink`/`error` events and SHA-256 hash tracking.\n- **USAGE**: `--cleanup` and `--dry-run-delete` flags identify dead translation keys with confidence scores.\n- **VALIDATOR**: `--enforce-key-style` enforces dot.notation, snake_case, camelCase, kebab-case, or flat naming conventions.\n- **SCANNER**: `--source-language` supports multi-language hardcoded text detection with 12+ language profiles.\n- **BACKUP**: `--incremental` flag creates differential backups with SHA-256 hashing and chained restores.\n- **RUNTIME**: `lazy: true` option defers locale file loading until first key access for lower memory usage.\n- **PROTECTION**: Context-aware rules (`after:word`, `before:word`, `standalone`, `surrounded:left,right`) for precise term masking.\n\nSee [CHANGELOG.md](./CHANGELOG.md) for more release details.\n\n## Quick Start\n\nInitialize a project:\n\n```bash\ni18ntk\n# or with explicit command\ni18ntk --command=init\n```\n\nRun common checks:\n\n```bash\ni18ntk --command=analyze\ni18ntk --command=validate\ni18ntk --command=usage\ni18ntk --command=sizing\ni18ntk --command=summary\n```\n\nComplete or fix translation files:\n\n```bash\ni18ntk --command=complete\ni18ntk-fixer --help\n```\n\nAuto-translate locale JSON:\n\n```bash\ni18ntk --command=translate\n# or\ni18ntk-translate locales/en/common.json de --report-stdout\n```\n\nThe full onboarding guide is in [docs/getting-started.md](./docs/getting-started.md).\n\n## Main Commands\n\nPrimary CLI:\n\n```bash\ni18ntk\ni18ntk --help\ni18ntk --command=init\ni18ntk --command=analyze\ni18ntk --command=validate\ni18ntk --command=usage\ni18ntk --command=scanner\ni18ntk --command=sizing\ni18ntk --command=complete\ni18ntk --command=translate\ni18ntk --command=summary\n```\n\nStandalone executables:\n\n```bash\ni18ntk-init\ni18ntk-analyze\ni18ntk-validate\ni18ntk-usage\ni18ntk-scanner\ni18ntk-sizing\ni18ntk-complete\ni18ntk-summary\ni18ntk-doctor\ni18ntk-fixer\ni18ntk-backup\ni18ntk-translate\n```\n\nNote: manager route `i18ntk --command=backup` is disabled in current builds. Use `i18ntk-backup` directly for backup operations.\n\n## Command Reference\n\n| Command | What it does | Looks for | Writes or changes |\n| --- | --- | --- | --- |\n| `i18ntk` | Opens the interactive management menu. | Project config, setup state, available commands. | Only changes files after you choose a command that writes. |\n| `i18ntk --command=init` / `i18ntk-init` | Sets up locale folders and missing target-language files. | Source language files and selected target languages. | Locale JSON files, `.i18ntk-config`, optional reports/backups. |\n| `i18ntk --command=analyze` / `i18ntk-analyze` | Compares source and target translation coverage. | Missing keys, extra keys, untranslated markers, completion by language. | Markdown/JSON/text reports when report output is enabled. |\n| `i18ntk --command=validate` / `i18ntk-validate` | Validates structure and translation quality risks. | Placeholder mismatches, missing keys, risky URLs/emails/secrets, likely English target text. | Validation summary report. Does not edit locale files. |\n| `i18ntk --command=usage` / `i18ntk-usage` | Maps translation keys to source files and finds unused/missing keys. | Direct i18n calls, literal known-key references, bounded dynamic templates/object maps, unresolved dynamic expressions, hardcoded text candidates, namespace/file naming mismatches. | Usage report with key locations, namespace recommendations, unresolved dynamic expressions, hardcoded text suggestions, and optional dead-key report. Does not delete unless cleanup deletion is explicitly enabled. |\n| `i18ntk --command=scanner` / `i18ntk-scanner` | Scans source for i18n issues and hardcoded user-facing text. | JSX/template text, common text attributes, i18n usage patterns, source-language text profiles. | Scanner report. Does not edit files. |\n| `i18ntk --command=complete` / `i18ntk-complete` | Adds missing keys to target language files for 100% key coverage. | Source-language keys missing from targets. | Target locale JSON files, using missing translation markers/prefixes. |\n| `i18ntk --command=translate` / `i18ntk-translate` | Auto-translates locale JSON using configured provider behavior. | Missing, empty, untranslated-marker, source-copy, likely-English, or visibly corrupt target values by default. | Target locale JSON files and translation reports. Existing translated values are kept unless `--translate-all` is used. |\n| `i18ntk --command=sizing` / `i18ntk-sizing` | Estimates translated string length expansion and layout risk. | Text length, expansion ratios, placeholder-bearing strings. | Sizing report. Does not edit locale files. |\n| `i18ntk --command=summary` / `i18ntk-summary` | Shows project translation status. | Configured locales, reports, completeness status. | Console/report output only. |\n| `i18ntk-fixer` | Fixes placeholder and missing-marker issues. | Placeholder corruption, missing translation markers, configured language files. | Locale JSON files when fixes are applied. Use dry-run options where available before bulk edits. |\n| `i18ntk-backup` | Creates, verifies, restores, and cleans locale backups. | Locale JSON files and backup manifests. | Backup archives/manifests, or restored locale files when using restore. |\n\n## Common Options\n\nMany commands support:\n\n- `--source-dir <path>`\n- `--i18n-dir <path>`\n- `--output-dir <path>`\n- `--source-language <code>`\n- `--ui-language <code>`\n- `--no-prompt`\n- `--help`\n\nCommand-specific tools add their own flags such as `--dry-run`, `--output-report`, `--cleanup`, `--predict-expansion`, or Auto Translate provider options.\n\nExample:\n\n```bash\ni18ntk --command=analyze --source-dir=./src --i18n-dir=./locales --output-dir=./i18ntk-reports\n```\n\n## Auto Translate\n\nInteractive manager flow:\n\n```bash\ni18ntk\n# choose \"Auto Translate\"\n```\n\nDirect CLI examples:\n\n```bash\ni18ntk-translate locales/en/common.json de\ni18ntk-translate locales/en/common.json fr --dry-run --report-stdout\ni18ntk-translate locales/en es --source-dir locales/en --files \"*.json\" --no-confirm --preserve-placeholders\n```\n\nProvider examples:\n\n```bash\nexport DEEPL_API_KEY=\"your-deepl-api-key\"\ni18ntk-translate locales/en/common.json de --provider deepl --no-confirm --preserve-placeholders\n\nexport LIBRETRANSLATE_URL=\"https://libretranslate.com/translate\"\nexport LIBRETRANSLATE_API_KEY=\"optional-api-key\"\ni18ntk-translate locales/en/common.json es --provider libretranslate --no-confirm --preserve-placeholders\n```\n\n`google` remains the default provider. You can also set `I18NTK_TRANSLATE_PROVIDER=deepl` or `I18NTK_TRANSLATE_PROVIDER=libretranslate`.\n\nProvider requests are HTTPS-only and response-size limited, and security logs redact provider query strings and response bodies. DeepL is pinned to official DeepL hosts by default; set `I18NTK_ALLOW_CUSTOM_TRANSLATE_HOSTS=1` only for a trusted DeepL-compatible proxy. Custom LibreTranslate URLs are blocked for localhost/private IP ranges unless `I18NTK_ALLOW_PRIVATE_TRANSLATE_URLS=1` is set for trusted local testing. Keep provider API keys in environment variables or a secret manager.\n\nThe manager flow asks for:\n\n- source locale directory, either the folder with JSON files or a locale root such as `./locales`\n- source language code\n- one or more target languages, or `all`\n- one JSON file or all JSON files in the source directory\n\nIf you select a locale root such as `./locales` and choose source language `en`, the manager automatically uses `./locales/en` when that folder contains the source JSON files.\n\nBefore writing files, the manager can run a dry-run preview. After confirmation it writes translated files under sibling target-language folders, for example:\n\n```text\nlocales/en/common.json\nlocales/de/common.json\nlocales/fr/common.json\n```\n\nAuto Translate is target-aware by default. When a target file already exists, it keeps translated target values and only sends values that are missing, empty, marked as untranslated, still identical to the source, likely still English, or visibly corrupt from encoding damage such as `?????`, replacement characters, or common mojibake. Use `--translate-all` when you intentionally want to re-translate every source string.\n\n### Placeholder Handling\n\nAuto Translate detects common placeholders such as:\n\n- `{name}`\n- `{{count}}`\n- `%s`\n- `%d`\n- `:id`\n- `%{name}`\n- `${value}`\n- `{count, plural, one {# item} other {# items}}`\n- `$t(common.save)`\n- `%(total).2f`\n\nUseful flags:\n\n- `--preserve-placeholders`: translate text around placeholders and reinsert original tokens\n- `--skip-placeholders`: copy placeholder-bearing strings unchanged\n- `--send-placeholders`: send placeholder-bearing strings through translation after masking\n- `--custom-regex <regex>`: add project-specific placeholder detection\n- `--only-missing`: keep existing translated target values and translate only missing/source-copy/likely English values (default)\n- `--translate-all`: re-translate every source string\n\nProgress output is stage-aware for large files. Normal keys are reported as `Translating strings`, while preserve-mode placeholder work is reported as `Translating placeholder-safe text segments`; each progress update includes the current key path when available.\n\n### Protected Terms and Keys\n\nAuto Translate can create and use a project-local protection file:\n\n```bash\ni18ntk-translate locales/en/common.json de --create-protection-file --protection-file ./i18ntk-auto-translate.json\n```\n\nExample `i18ntk-auto-translate.json`:\n\n```json\n{\n \"version\": 1,\n \"terms\": [\n \"BrandName\",\n \"PRODUCT_CODE\",\n { \"value\": \"OK\", \"context\": \"after:Click|Press|Tap\" },\n { \"value\": \"API\", \"context\": \"standalone\" }\n ],\n \"keys\": [\"app.brandName\", \"legal.companyName\", \"product.*.symbol\"],\n \"values\": [\"BrandName Ltd\", \"support@example.com\"],\n \"patterns\": [\"[A-Z]{2,}-\\\\d+\"]\n}\n```\n\n- `terms` are masked before translation and restored exactly afterward.\n - **Plain strings**: masked everywhere (backward compatible).\n - **Context objects**: masked only in specific contexts (`after:word`, `before:word`, `standalone`, `surrounded:left,right`).\n- `keys` are exact key paths or `*` wildcard paths copied unchanged.\n- `values` are exact source values copied unchanged.\n- `patterns` are JavaScript regex strings for advanced protected substrings.\n\nUseful flags:\n\n- `--protection-file <path>`\n- `--create-protection-file`\n- `--no-protection`\n\nOpen Settings and choose `Auto Translate` to edit defaults for placeholder mode, translate-only-needed mode, concurrency, batch size, retry settings, report output, BOM output, protection file path, first-run setup prompt, and update prompt.\n\nSee [docs/auto-translate.md](./docs/auto-translate.md) for the full Auto Translate guide.\n\n## Validation\n\nValidation checks locale structure, completeness, placeholders, and content risks.\n\nValidation warning types are specific:\n\n- `Potential risky content`: URL, email address, or secret-like value\n- `Possible untranslated English content`: target-language value appears to contain too much English\n\nEnglish-content warnings include:\n\n- detected English percentage\n- configured threshold\n- matched word count\n- sample matched words\n\nTune warnings in `.i18ntk-config`:\n\n```json\n{\n \"englishContentThresholdPercent\": 10,\n \"allowedEnglishTerms\": [\"BrandName\", \"PRODUCT_CODE\"]\n}\n```\n\n## Sizing Analysis\n\n`i18ntk-sizing` reports translation file sizes, key counts, average value length, and file-set mismatches across language folders.\n\n```bash\ni18ntk-sizing --source-dir ./locales --format table\ni18ntk-sizing --source-dir ./locales --detailed --output-dir ./i18ntk-reports\n```\n\nUse `--detailed` to print per-file rows in the terminal.\n\n### Expansion Prediction (New in 4.0.0)\n\nPredict UI layout overflow risk by analyzing per-key character-count expansion across languages:\n\n```bash\ni18ntk-sizing --source-dir ./locales --predict-expansion --output-report\n```\n\nExpansion ratios are classified into risk tiers:\n\n- **Safe** (<30% expansion): no UI impact expected\n- **Warning** (30–50%): may overflow in tight layouts — test on target languages\n- **Critical** (>50%): high risk of truncation — review UI element sizing\n\nThe report includes a built-in language-pair expansion reference table (EN→DE +35%, EN→RU +50%, EN→JA −40%, etc.) and lists the top-30 most-expanded keys.\n\n## Scanner: Multi-Language Detection (New in 4.0.0)\n\n`i18ntk-scanner` now supports detecting hardcoded text in multiple source languages beyond English:\n\n```bash\ni18ntk-scanner --source-dir ./src --source-language de\ni18ntk-scanner --source-dir ./src --source-language ja --output-report\n```\n\nSupported language profiles (12+): English, German, French, Spanish, Japanese, Chinese, Russian, Korean, Arabic, Hindi, and more. Each profile includes language-specific character ranges, stopword lists for false-positive filtering, and transliteration rules for key generation.\n\n## Usage: Dead Key Detection (New in 4.0.0)\n\n`i18ntk-usage` can identify translation keys that are defined but never referenced in source code:\n\n```bash\ni18ntk-usage --source-dir ./src --i18n-dir ./locales --cleanup\ni18ntk-usage --source-dir ./src --i18n-dir ./locales --cleanup --dry-run-delete\n```\n\nEach dead key receives a confidence score (0.0–1.0) factoring:\n- Unresolved dynamic key patterns (e.g., `` t(`prefix.${dynamic}`) ``) — lower score and listed in the usage report; simple consts, bounded arrays, object maps, and ternaries are expanded to exact keys where possible\n- Key appears in source code comments or JSDoc — medium score\n- Parent file recently modified (<30 days) — medium score\n- No references found anywhere — high score (>0.8)\n\nThe `--dry-run-delete` flag writes a `.dead-keys.json` report for review before any destructive action.\n\n## Validator: Key Naming Conventions (New in 4.0.0)\n\nEnforce consistent translation key naming across your project:\n\n```bash\ni18ntk-validate --enforce-key-style\n```\n\nConfigure the expected style in `.i18ntk-config`:\n\n```json\n{\n \"keyStyle\": \"dot.notation\"\n}\n```\n\nSupported styles: `dot.notation`, `snake_case`, `camelCase`, `kebab-case`, `flat`. Violations are reported as warnings with suggested canonical forms.\n\n## Watch: Hot Reload (New in 4.0.0)\n\n`utils/watch-locales.js` now provides debounced file watching with EventEmitter support:\n\n```js\nconst watchLocales = require('i18ntk/utils/watch-locales');\nconst watcher = watchLocales('./locales');\n\nwatcher.on('change', (filePath) => {\n console.log('Locale changed:', filePath);\n});\n\nwatcher.on('add', (filePath) => {\n console.log('Locale added:', filePath);\n});\n\n// Later:\nwatcher.stop();\n```\n\nFeatures: 300ms debounce (configurable), SHA-256 hash tracking to skip no-change saves, and a maximum of 50 watched directories.\n\n### Migration\n\nThe `watchLocales` return value gained EventEmitter methods in v4.0.0. Existing stop-function usage still works:\n\n```js\nconst stop = watchLocales('./locales', onChange);\n```\n\nCan be updated to:\n\n```js\nconst watcher = watchLocales('./locales');\nwatcher.on('change', onChange);\nwatcher.stop();\n```\n\nPassing a callback as the second argument is still supported — it auto-subscribes to `change` and `add` events.\n\n## Backup: Incremental Mode (New in 4.0.0)\n\nCreate differential backups that only include changed files:\n\n```bash\ni18ntk-backup create ./locales --incremental\n```\n\nIncremental backups store SHA-256 hashes per file and a parent-chain reference. Restoring an incremental backup automatically chains from the oldest full backup through each incremental diff in order. Chain depth is capped at 10 increments. Use `verify` to validate the hash chain.\n\n## Runtime: Lazy Loading (New in 4.0.0)\n\nReduce memory usage by deferring locale file loads until first key access:\n\n```js\nconst runtime = require('i18ntk/runtime');\n\nconst i18n = runtime.initRuntime({\n baseDir: './locales',\n language: 'en',\n lazy: true\n});\n\nconsole.log(i18n.t('common.hello')); // loads common.json on first access\n```\n\nWhen `lazy: true`, the runtime builds a key-to-file manifest on first access and loads individual files on demand. Files are loaded once and cached. If the manifest is missing or incomplete, the runtime falls back to full eager loading for that language. Manifest size is capped at 100KB with path containment validation.\n\nProduction guidance:\n\n- Prefer the object returned from `initRuntime()` instead of module-level `runtime.t()` in apps with multiple tenants, projects, or locale roots.\n- Use `lazy: true` for large modular locale folders where lower steady-state memory matters more than a small first-key lookup cost.\n- Use `preload: true` without `lazy` for small locale sets or latency-sensitive startup paths.\n- Call `refresh(language)` after deploying or writing changed locale files so cached data and lazy manifests are rebuilt.\n- Use per-call language overrides when rendering one-off alternate-language strings: `i18n.t('common.hello', {}, { language: 'de' })`.\n- Use `translateBatch()` for small groups of labels and `clearCache()` / `getCacheInfo()` for cache maintenance and diagnostics.\n- `i18ntk/runtime/enhanced` remains available for compatibility with existing async/encryption users, but new production integrations should start with `i18ntk/runtime`.\n\n## Runtime API\n\nUse `i18ntk/runtime` when an application needs to read locale JSON files at runtime.\n\n```js\nconst runtime = require('i18ntk/runtime');\n\nconst i18n = runtime.initRuntime({\n baseDir: './locales',\n language: 'en',\n fallbackLanguage: 'en',\n keySeparator: '.',\n preload: true\n});\n\nconsole.log(i18n.t('common.hello'));\ni18n.setLanguage('fr');\nconsole.log(i18n.getLanguage());\nconsole.log(i18n.getAvailableLanguages());\ni18n.refresh('fr');\n```\n\nUseful production helpers:\n\n```js\ni18n.t('common.hello', {}, { language: 'de' }); // per-call language override\ni18n.translateBatch(['menu.home', 'menu.settings']);\ni18n.clearCache('fr');\nconsole.log(i18n.getCacheInfo());\n```\n\nSee [docs/runtime.md](./docs/runtime.md) for runtime details.\n\n## Configuration\n\ni18ntk uses a project-local `.i18ntk-config` file.\n\nExample:\n\n```json\n{\n \"version\": \"4.2.0\",\n \"sourceDir\": \"./locales\",\n \"i18nDir\": \"./locales\",\n \"outputDir\": \"./i18ntk-reports\",\n \"sourceLanguage\": \"en\",\n \"defaultLanguages\": [\"en\", \"de\", \"es\", \"fr\", \"ru\"],\n \"reports\": {\n \"format\": \"markdown\"\n },\n \"englishContentThresholdPercent\": 10,\n \"allowedEnglishTerms\": [\"BrandName\", \"PRODUCT_CODE\"],\n \"autoTranslate\": {\n \"placeholderMode\": \"preserve\",\n \"concurrency\": 12,\n \"batchSize\": 100,\n \"progressInterval\": 25,\n \"retryCount\": 3,\n \"retryDelay\": 1000,\n \"timeout\": 15000,\n \"dryRunFirst\": true,\n \"onlyMissingOrEnglish\": true,\n \"reportStdout\": true,\n \"bom\": false,\n \"protectionEnabled\": true,\n \"protectionFile\": \"./i18ntk-auto-translate.json\",\n \"promptProtectionSetup\": true,\n \"promptProtectionUpdate\": true\n },\n \"setup\": {\n \"completed\": true\n }\n}\n```\n\nSee [docs/api/CONFIGURATION.md](./docs/api/CONFIGURATION.md) for the full configuration model.\n\n## Public Package Contents\n\nThe public package intentionally ships runtime and CLI files only. The publish staging script excludes development-only content such as tests, scripts, docs, release staging folders, local config files, and generated protection files.\n\nThe package includes:\n\n- CLI entry points under `main/`\n- manager commands and services\n- runtime API files under `runtime/`\n- settings UI files required at runtime\n- bundled internal UI locales\n- shared utilities required by the shipped commands\n- `README.md`, `CHANGELOG.md`, `LICENSE`, and policy files\n\nThe public package manifest includes `readmeFilename: \"README.md\"`, and the release staging script fails if `README.md` is missing or empty.\n\n## Documentation\n\n- [Documentation Index](./docs/README.md)\n- [Getting Started](./docs/getting-started.md)\n- [API Reference](./docs/api/API_REFERENCE.md)\n- [Configuration Guide](./docs/api/CONFIGURATION.md)\n- [Runtime API Guide](./docs/runtime.md)\n- [Auto Translate Guide](./docs/auto-translate.md)\n- [Scanner Guide](./docs/scanner-guide.md)\n- [Environment Variables](./docs/environment-variables.md)\n- [Migration Guide v4.2.0](./docs/migration-guide-v4.2.0.md)\n\n## Security\n\n- No API key is required for the default Auto Translate flow.\n- Do not store secrets in locale files, `.i18ntk-config`, or protection files.\n- Project-specific brand/product terms should be configured by the user, not hardcoded into the package.\n- Report security issues using [SECURITY.md](./SECURITY.md).\n\n## Community\n\n- [Contributing](./CONTRIBUTING.md)\n- [Code of Conduct](./CODE_OF_CONDUCT.md)\n- [Funding](./FUNDING.md)\n\n## License\n\nMIT. See [LICENSE](./LICENSE).\n"
213
+ "readme": "# i18ntk v4.2.1\n\nA i18n toolkit - A zero-dependency internationalization toolkit for setup, scanning, analysis, validation, usage tracking, translation completion, automatic JSON locale translation, reporting, and runtime translation loading.\n\n![i18ntk Logo](https://raw.githubusercontent.com/vladnoskv/i18ntk/main/docs/screenshots/i18ntk-logo-public.PNG)\n\n[![npm version](https://img.shields.io/npm/v/i18ntk.svg?color=brightgreen)](https://www.npmjs.com/package/i18ntk)\n[![npm downloads](https://img.shields.io/npm/dt/i18ntk.svg)](https://www.npmjs.com/package/i18ntk)\n[![node](https://img.shields.io/badge/node-%3E%3D16-339933)](https://nodejs.org)\n[![dependencies](https://img.shields.io/badge/dependencies-0-success)](https://www.npmjs.com/package/i18ntk)\n[![license](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE)\n[![socket](https://socket.dev/api/badge/npm/package/i18ntk/4.2.1)](https://socket.dev/npm/package/i18ntk/overview/4.2.1)\n\n## Install\n\n```bash\n# global CLI use\nnpm install -g i18ntk\n\n# local project use\nnpm install --save-dev i18ntk\n\n# one-off execution\nnpx i18ntk --help\n```\n\nRequirements:\n\n- Node.js `>=16.0.0`\n- npm `>=8.0.0`\n- No runtime dependencies\n\n## What's New in 4.2.1\n\n- **AUTO TRANSLATE**: Existing target values like `[AR] What We Offer` are now treated as untranslated placeholders for the matching target language and are translated from the source text.\n- **AUTO TRANSLATE**: Before writing each output file, Auto Translate now performs a final leftover check and retries any placeholder-prefixed or source-copy values once.\n- **AUTO TRANSLATE**: If leftovers remain after the final retry, the command warns, includes them in the report, recommends rerunning Auto Translate, and exits with validation failure instead of reporting a clean completion.\n- **SIZING/USAGE**: Usage analysis no longer writes its inferred app source fallback back into the shared locale config, so running usage before sizing no longer makes sizing analyze the wrong directory.\n- **VALIDATION REPORTS**: Validation summary files now include warning and error details, including English-content warning payloads, instead of only totals.\n- **DOCS**: Versioned docs and migration guidance now reflect the current 4.2.1 command surface.\n\n## What's New in 4.1.0\n\n- **FIX**: Critical and high-impact bugs resolved across the v4.0.0 feature set — runtime staleness crashes, backup hash-chain verification, sizing adminAuth crash, scanner `--source-language` propagation, watch callback subscriptions, dead key detection performance, validator key style enforcement, and protection Unicode boundary handling. See [CHANGELOG.md](./CHANGELOG.md) for complete details.\n\n## What's New in 4.0.0\n\n- **SIZING**: `--predict-expansion` flag computes per-key expansion ratios across languages with Safe/Warning/Critical risk tiers for UI layout planning.\n- **WATCH**: `watchLocales()` now returns an EventEmitter-compatible watcher with debounced `change`/`add`/`unlink`/`error` events and SHA-256 hash tracking.\n- **USAGE**: `--cleanup` and `--dry-run-delete` flags identify dead translation keys with confidence scores.\n- **VALIDATOR**: `--enforce-key-style` enforces dot.notation, snake_case, camelCase, kebab-case, or flat naming conventions.\n- **SCANNER**: `--source-language` supports multi-language hardcoded text detection with 12+ language profiles.\n- **BACKUP**: `--incremental` flag creates differential backups with SHA-256 hashing and chained restores.\n- **RUNTIME**: `lazy: true` option defers locale file loading until first key access for lower memory usage.\n- **PROTECTION**: Context-aware rules (`after:word`, `before:word`, `standalone`, `surrounded:left,right`) for precise term masking.\n\nSee [CHANGELOG.md](./CHANGELOG.md) for more release details.\n\n## Quick Start\n\nInitialize a project:\n\n```bash\ni18ntk\n# or with explicit command\ni18ntk --command=init\n```\n\nRun common checks:\n\n```bash\ni18ntk --command=analyze\ni18ntk --command=validate\ni18ntk --command=usage\ni18ntk --command=sizing\ni18ntk --command=summary\n```\n\nComplete or fix translation files:\n\n```bash\ni18ntk --command=complete\ni18ntk-fixer --help\n```\n\nAuto-translate locale JSON:\n\n```bash\ni18ntk --command=translate\n# or\ni18ntk-translate locales/en/common.json de --report-stdout\n```\n\nThe full onboarding guide is in [docs/getting-started.md](./docs/getting-started.md).\n\n## Main Commands\n\nPrimary CLI:\n\n```bash\ni18ntk\ni18ntk --help\ni18ntk --command=init\ni18ntk --command=analyze\ni18ntk --command=validate\ni18ntk --command=usage\ni18ntk --command=scanner\ni18ntk --command=sizing\ni18ntk --command=complete\ni18ntk --command=translate\ni18ntk --command=summary\n```\n\nStandalone executables:\n\n```bash\ni18ntk-init\ni18ntk-analyze\ni18ntk-validate\ni18ntk-usage\ni18ntk-scanner\ni18ntk-sizing\ni18ntk-complete\ni18ntk-summary\ni18ntk-doctor\ni18ntk-fixer\ni18ntk-backup\ni18ntk-translate\n```\n\nNote: manager route `i18ntk --command=backup` is disabled in current builds. Use `i18ntk-backup` directly for backup operations.\n\n## Command Reference\n\n| Command | What it does | Looks for | Writes or changes |\n| --- | --- | --- | --- |\n| `i18ntk` | Opens the interactive management menu. | Project config, setup state, available commands. | Only changes files after you choose a command that writes. |\n| `i18ntk --command=init` / `i18ntk-init` | Sets up locale folders and missing target-language files. | Source language files and selected target languages. | Locale JSON files, `.i18ntk-config`, optional reports/backups. |\n| `i18ntk --command=analyze` / `i18ntk-analyze` | Compares source and target translation coverage. | Missing keys, extra keys, untranslated markers, completion by language. | Markdown/JSON/text reports when report output is enabled. |\n| `i18ntk --command=validate` / `i18ntk-validate` | Validates structure and translation quality risks. | Placeholder mismatches, missing keys, risky URLs/emails/secrets, likely English target text. | Validation summary report. Does not edit locale files. |\n| `i18ntk --command=usage` / `i18ntk-usage` | Maps translation keys to source files and finds unused/missing keys. | Direct i18n calls, literal known-key references, bounded dynamic templates/object maps, unresolved dynamic expressions, hardcoded text candidates, namespace/file naming mismatches. | Usage report with key locations, namespace recommendations, unresolved dynamic expressions, hardcoded text suggestions, and optional dead-key report. Does not delete unless cleanup deletion is explicitly enabled. |\n| `i18ntk --command=scanner` / `i18ntk-scanner` | Scans source for i18n issues and hardcoded user-facing text. | JSX/template text, common text attributes, i18n usage patterns, source-language text profiles. | Scanner report. Does not edit files. |\n| `i18ntk --command=complete` / `i18ntk-complete` | Adds missing keys to target language files for 100% key coverage. | Source-language keys missing from targets. | Target locale JSON files, using missing translation markers/prefixes. |\n| `i18ntk --command=translate` / `i18ntk-translate` | Auto-translates locale JSON using configured provider behavior. | Missing, empty, untranslated-marker, source-copy, likely-English, or visibly corrupt target values by default. | Target locale JSON files and translation reports. Existing translated values are kept unless `--translate-all` is used. |\n| `i18ntk --command=sizing` / `i18ntk-sizing` | Estimates translated string length expansion and layout risk. | Text length, expansion ratios, placeholder-bearing strings. | Sizing report. Does not edit locale files. |\n| `i18ntk --command=summary` / `i18ntk-summary` | Shows project translation status. | Configured locales, reports, completeness status. | Console/report output only. |\n| `i18ntk-fixer` | Fixes placeholder and missing-marker issues. | Placeholder corruption, missing translation markers, configured language files. | Locale JSON files when fixes are applied. Use dry-run options where available before bulk edits. |\n| `i18ntk-backup` | Creates, verifies, restores, and cleans locale backups. | Locale JSON files and backup manifests. | Backup archives/manifests, or restored locale files when using restore. |\n\n## Common Options\n\nMany commands support:\n\n- `--source-dir <path>`\n- `--i18n-dir <path>`\n- `--output-dir <path>`\n- `--source-language <code>`\n- `--ui-language <code>`\n- `--no-prompt`\n- `--help`\n\nCommand-specific tools add their own flags such as `--dry-run`, `--output-report`, `--cleanup`, `--predict-expansion`, or Auto Translate provider options.\n\nExample:\n\n```bash\ni18ntk --command=analyze --source-dir=./src --i18n-dir=./locales --output-dir=./i18ntk-reports\n```\n\n## Auto Translate\n\nInteractive manager flow:\n\n```bash\ni18ntk\n# choose \"Auto Translate\"\n```\n\nDirect CLI examples:\n\n```bash\ni18ntk-translate locales/en/common.json de\ni18ntk-translate locales/en/common.json fr --dry-run --report-stdout\ni18ntk-translate locales/en es --source-dir locales/en --files \"*.json\" --no-confirm --preserve-placeholders\n```\n\nProvider examples:\n\n```bash\nexport DEEPL_API_KEY=\"your-deepl-api-key\"\ni18ntk-translate locales/en/common.json de --provider deepl --no-confirm --preserve-placeholders\n\nexport LIBRETRANSLATE_URL=\"https://libretranslate.com/translate\"\nexport LIBRETRANSLATE_API_KEY=\"optional-api-key\"\ni18ntk-translate locales/en/common.json es --provider libretranslate --no-confirm --preserve-placeholders\n```\n\n`google` remains the default provider. You can also set `I18NTK_TRANSLATE_PROVIDER=deepl` or `I18NTK_TRANSLATE_PROVIDER=libretranslate`.\n\nProvider requests are HTTPS-only and response-size limited, and security logs redact provider query strings and response bodies. DeepL is pinned to official DeepL hosts by default; set `I18NTK_ALLOW_CUSTOM_TRANSLATE_HOSTS=1` only for a trusted DeepL-compatible proxy. Custom LibreTranslate URLs are blocked for localhost/private IP ranges unless `I18NTK_ALLOW_PRIVATE_TRANSLATE_URLS=1` is set for trusted local testing. Keep provider API keys in environment variables or a secret manager.\n\nThe manager flow asks for:\n\n- source locale directory, either the folder with JSON files or a locale root such as `./locales`\n- source language code\n- one or more target languages, or `all`\n- one JSON file or all JSON files in the source directory\n\nIf you select a locale root such as `./locales` and choose source language `en`, the manager automatically uses `./locales/en` when that folder contains the source JSON files.\n\nBefore writing files, the manager can run a dry-run preview. After confirmation it writes translated files under sibling target-language folders, for example:\n\n```text\nlocales/en/common.json\nlocales/de/common.json\nlocales/fr/common.json\n```\n\nAuto Translate is target-aware by default. When a target file already exists, it keeps translated target values and only sends values that are missing, empty, marked as untranslated, still identical to the source, likely still English, or visibly corrupt from encoding damage such as `?????`, replacement characters, or common mojibake. Use `--translate-all` when you intentionally want to re-translate every source string.\n\n### Placeholder Handling\n\nAuto Translate detects common placeholders such as:\n\n- `{name}`\n- `{{count}}`\n- `%s`\n- `%d`\n- `:id`\n- `%{name}`\n- `${value}`\n- `{count, plural, one {# item} other {# items}}`\n- `$t(common.save)`\n- `%(total).2f`\n\nUseful flags:\n\n- `--preserve-placeholders`: translate text around placeholders and reinsert original tokens\n- `--skip-placeholders`: copy placeholder-bearing strings unchanged\n- `--send-placeholders`: send placeholder-bearing strings through translation after masking\n- `--custom-regex <regex>`: add project-specific placeholder detection\n- `--only-missing`: keep existing translated target values and translate only missing/source-copy/likely English values (default)\n- `--translate-all`: re-translate every source string\n\nProgress output is stage-aware for large files. Normal keys are reported as `Translating strings`, while preserve-mode placeholder work is reported as `Translating placeholder-safe text segments`; each progress update includes the current key path when available.\n\n### Protected Terms and Keys\n\nAuto Translate can create and use a project-local protection file:\n\n```bash\ni18ntk-translate locales/en/common.json de --create-protection-file --protection-file ./i18ntk-auto-translate.json\n```\n\nExample `i18ntk-auto-translate.json`:\n\n```json\n{\n \"version\": 1,\n \"terms\": [\n \"BrandName\",\n \"PRODUCT_CODE\",\n { \"value\": \"OK\", \"context\": \"after:Click|Press|Tap\" },\n { \"value\": \"API\", \"context\": \"standalone\" }\n ],\n \"keys\": [\"app.brandName\", \"legal.companyName\", \"product.*.symbol\"],\n \"values\": [\"BrandName Ltd\", \"support@example.com\"],\n \"patterns\": [\"[A-Z]{2,}-\\\\d+\"]\n}\n```\n\n- `terms` are masked before translation and restored exactly afterward.\n - **Plain strings**: masked everywhere (backward compatible).\n - **Context objects**: masked only in specific contexts (`after:word`, `before:word`, `standalone`, `surrounded:left,right`).\n- `keys` are exact key paths or `*` wildcard paths copied unchanged.\n- `values` are exact source values copied unchanged.\n- `patterns` are JavaScript regex strings for advanced protected substrings.\n\nUseful flags:\n\n- `--protection-file <path>`\n- `--create-protection-file`\n- `--no-protection`\n\nOpen Settings and choose `Auto Translate` to edit defaults for placeholder mode, translate-only-needed mode, concurrency, batch size, retry settings, report output, BOM output, protection file path, first-run setup prompt, and update prompt.\n\nSee [docs/auto-translate.md](./docs/auto-translate.md) for the full Auto Translate guide.\n\n## Validation\n\nValidation checks locale structure, completeness, placeholders, and content risks.\n\nValidation warning types are specific:\n\n- `Potential risky content`: URL, email address, or secret-like value\n- `Possible untranslated English content`: target-language value appears to contain too much English\n\nEnglish-content warnings include:\n\n- detected English percentage\n- configured threshold\n- matched word count\n- sample matched words\n\nTune warnings in `.i18ntk-config`:\n\n```json\n{\n \"englishContentThresholdPercent\": 10,\n \"allowedEnglishTerms\": [\"BrandName\", \"PRODUCT_CODE\"]\n}\n```\n\n## Sizing Analysis\n\n`i18ntk-sizing` reports translation file sizes, key counts, average value length, and file-set mismatches across language folders.\n\n```bash\ni18ntk-sizing --source-dir ./locales --format table\ni18ntk-sizing --source-dir ./locales --detailed --output-dir ./i18ntk-reports\n```\n\nUse `--detailed` to print per-file rows in the terminal.\n\n### Expansion Prediction (New in 4.0.0)\n\nPredict UI layout overflow risk by analyzing per-key character-count expansion across languages:\n\n```bash\ni18ntk-sizing --source-dir ./locales --predict-expansion --output-report\n```\n\nExpansion ratios are classified into risk tiers:\n\n- **Safe** (<30% expansion): no UI impact expected\n- **Warning** (30–50%): may overflow in tight layouts — test on target languages\n- **Critical** (>50%): high risk of truncation — review UI element sizing\n\nThe report includes a built-in language-pair expansion reference table (EN→DE +35%, EN→RU +50%, EN→JA −40%, etc.) and lists the top-30 most-expanded keys.\n\n## Scanner: Multi-Language Detection (New in 4.0.0)\n\n`i18ntk-scanner` now supports detecting hardcoded text in multiple source languages beyond English:\n\n```bash\ni18ntk-scanner --source-dir ./src --source-language de\ni18ntk-scanner --source-dir ./src --source-language ja --output-report\n```\n\nSupported language profiles (12+): English, German, French, Spanish, Japanese, Chinese, Russian, Korean, Arabic, Hindi, and more. Each profile includes language-specific character ranges, stopword lists for false-positive filtering, and transliteration rules for key generation.\n\n## Usage: Dead Key Detection (New in 4.0.0)\n\n`i18ntk-usage` can identify translation keys that are defined but never referenced in source code:\n\n```bash\ni18ntk-usage --source-dir ./src --i18n-dir ./locales --cleanup\ni18ntk-usage --source-dir ./src --i18n-dir ./locales --cleanup --dry-run-delete\n```\n\nEach dead key receives a confidence score (0.0–1.0) factoring:\n- Unresolved dynamic key patterns (e.g., `` t(`prefix.${dynamic}`) ``) — lower score and listed in the usage report; simple consts, bounded arrays, object maps, and ternaries are expanded to exact keys where possible\n- Key appears in source code comments or JSDoc — medium score\n- Parent file recently modified (<30 days) — medium score\n- No references found anywhere — high score (>0.8)\n\nThe `--dry-run-delete` flag writes a `.dead-keys.json` report for review before any destructive action.\n\n## Validator: Key Naming Conventions (New in 4.0.0)\n\nEnforce consistent translation key naming across your project:\n\n```bash\ni18ntk-validate --enforce-key-style\n```\n\nConfigure the expected style in `.i18ntk-config`:\n\n```json\n{\n \"keyStyle\": \"dot.notation\"\n}\n```\n\nSupported styles: `dot.notation`, `snake_case`, `camelCase`, `kebab-case`, `flat`. Violations are reported as warnings with suggested canonical forms.\n\n## Watch: Hot Reload (New in 4.0.0)\n\n`utils/watch-locales.js` now provides debounced file watching with EventEmitter support:\n\n```js\nconst watchLocales = require('i18ntk/utils/watch-locales');\nconst watcher = watchLocales('./locales');\n\nwatcher.on('change', (filePath) => {\n console.log('Locale changed:', filePath);\n});\n\nwatcher.on('add', (filePath) => {\n console.log('Locale added:', filePath);\n});\n\n// Later:\nwatcher.stop();\n```\n\nFeatures: 300ms debounce (configurable), SHA-256 hash tracking to skip no-change saves, and a maximum of 50 watched directories.\n\n### Migration\n\nThe `watchLocales` return value gained EventEmitter methods in v4.0.0. Existing stop-function usage still works:\n\n```js\nconst stop = watchLocales('./locales', onChange);\n```\n\nCan be updated to:\n\n```js\nconst watcher = watchLocales('./locales');\nwatcher.on('change', onChange);\nwatcher.stop();\n```\n\nPassing a callback as the second argument is still supported — it auto-subscribes to `change` and `add` events.\n\n## Backup: Incremental Mode (New in 4.0.0)\n\nCreate differential backups that only include changed files:\n\n```bash\ni18ntk-backup create ./locales --incremental\n```\n\nIncremental backups store SHA-256 hashes per file and a parent-chain reference. Restoring an incremental backup automatically chains from the oldest full backup through each incremental diff in order. Chain depth is capped at 10 increments. Use `verify` to validate the hash chain.\n\n## Runtime: Lazy Loading (New in 4.0.0)\n\nReduce memory usage by deferring locale file loads until first key access:\n\n```js\nconst runtime = require('i18ntk/runtime');\n\nconst i18n = runtime.initRuntime({\n baseDir: './locales',\n language: 'en',\n lazy: true\n});\n\nconsole.log(i18n.t('common.hello')); // loads common.json on first access\n```\n\nWhen `lazy: true`, the runtime builds a key-to-file manifest on first access and loads individual files on demand. Files are loaded once and cached. If the manifest is missing or incomplete, the runtime falls back to full eager loading for that language. Manifest size is capped at 100KB with path containment validation.\n\nProduction guidance:\n\n- Prefer the object returned from `initRuntime()` instead of module-level `runtime.t()` in apps with multiple tenants, projects, or locale roots.\n- Use `lazy: true` for large modular locale folders where lower steady-state memory matters more than a small first-key lookup cost.\n- Use `preload: true` without `lazy` for small locale sets or latency-sensitive startup paths.\n- Call `refresh(language)` after deploying or writing changed locale files so cached data and lazy manifests are rebuilt.\n- Use per-call language overrides when rendering one-off alternate-language strings: `i18n.t('common.hello', {}, { language: 'de' })`.\n- Use `translateBatch()` for small groups of labels and `clearCache()` / `getCacheInfo()` for cache maintenance and diagnostics.\n- `i18ntk/runtime/enhanced` remains available for compatibility with existing async/encryption users, but new production integrations should start with `i18ntk/runtime`.\n\n## Runtime API\n\nUse `i18ntk/runtime` when an application needs to read locale JSON files at runtime.\n\n```js\nconst runtime = require('i18ntk/runtime');\n\nconst i18n = runtime.initRuntime({\n baseDir: './locales',\n language: 'en',\n fallbackLanguage: 'en',\n keySeparator: '.',\n preload: true\n});\n\nconsole.log(i18n.t('common.hello'));\ni18n.setLanguage('fr');\nconsole.log(i18n.getLanguage());\nconsole.log(i18n.getAvailableLanguages());\ni18n.refresh('fr');\n```\n\nUseful production helpers:\n\n```js\ni18n.t('common.hello', {}, { language: 'de' }); // per-call language override\ni18n.translateBatch(['menu.home', 'menu.settings']);\ni18n.clearCache('fr');\nconsole.log(i18n.getCacheInfo());\n```\n\nSee [docs/runtime.md](./docs/runtime.md) for runtime details.\n\n## Configuration\n\ni18ntk uses a project-local `.i18ntk-config` file.\n\nExample:\n\n```json\n{\n \"version\": \"4.2.1\",\n \"sourceDir\": \"./locales\",\n \"i18nDir\": \"./locales\",\n \"outputDir\": \"./i18ntk-reports\",\n \"sourceLanguage\": \"en\",\n \"defaultLanguages\": [\"en\", \"de\", \"es\", \"fr\", \"ru\"],\n \"reports\": {\n \"format\": \"markdown\"\n },\n \"englishContentThresholdPercent\": 10,\n \"allowedEnglishTerms\": [\"BrandName\", \"PRODUCT_CODE\"],\n \"autoTranslate\": {\n \"placeholderMode\": \"preserve\",\n \"concurrency\": 12,\n \"batchSize\": 100,\n \"progressInterval\": 25,\n \"retryCount\": 3,\n \"retryDelay\": 1000,\n \"timeout\": 15000,\n \"dryRunFirst\": true,\n \"onlyMissingOrEnglish\": true,\n \"reportStdout\": true,\n \"bom\": false,\n \"protectionEnabled\": true,\n \"protectionFile\": \"./i18ntk-auto-translate.json\",\n \"promptProtectionSetup\": true,\n \"promptProtectionUpdate\": true\n },\n \"setup\": {\n \"completed\": true\n }\n}\n```\n\nSee [docs/api/CONFIGURATION.md](./docs/api/CONFIGURATION.md) for the full configuration model.\n\n## Public Package Contents\n\nThe public package intentionally ships runtime and CLI files only. The publish staging script excludes development-only content such as tests, scripts, docs, release staging folders, local config files, and generated protection files.\n\nThe package includes:\n\n- CLI entry points under `main/`\n- manager commands and services\n- runtime API files under `runtime/`\n- settings UI files required at runtime\n- bundled internal UI locales\n- shared utilities required by the shipped commands\n- `README.md`, `CHANGELOG.md`, `LICENSE`, and policy files\n\nThe public package manifest includes `readmeFilename: \"README.md\"`, and the release staging script fails if `README.md` is missing or empty.\n\n## Documentation\n\n- [Documentation Index](./docs/README.md)\n- [Getting Started](./docs/getting-started.md)\n- [API Reference](./docs/api/API_REFERENCE.md)\n- [Configuration Guide](./docs/api/CONFIGURATION.md)\n- [Runtime API Guide](./docs/runtime.md)\n- [Auto Translate Guide](./docs/auto-translate.md)\n- [Scanner Guide](./docs/scanner-guide.md)\n- [Environment Variables](./docs/environment-variables.md)\n- [Migration Guide v4.2.1](./docs/migration-guide-v4.2.1.md)\n\n## Security\n\n- No API key is required for the default Auto Translate flow.\n- Do not store secrets in locale files, `.i18ntk-config`, or protection files.\n- Project-specific brand/product terms should be configured by the user, not hardcoded into the package.\n- Report security issues using [SECURITY.md](./SECURITY.md).\n\n## Community\n\n- [Contributing](./CONTRIBUTING.md)\n- [Code of Conduct](./CODE_OF_CONDUCT.md)\n- [Funding](./FUNDING.md)\n\n## License\n\nMIT. See [LICENSE](./LICENSE).\n"
216
214
  }
@@ -9,6 +9,7 @@ function generateReport(skippedKeys, translatedCount, totalCount, options = {})
9
9
  timestamp = new Date().toISOString(),
10
10
  placeholderProtected = 0,
11
11
  protectedSkipped = 0,
12
+ residualUntranslated = [],
12
13
  } = options;
13
14
  const placeholderSkipped = skippedKeys.filter(key => key.skipReason !== 'protected');
14
15
  const protectedKeys = skippedKeys.filter(key => key.skipReason === 'protected');
@@ -28,13 +29,36 @@ function generateReport(skippedKeys, translatedCount, totalCount, options = {})
28
29
  lines.push(` Placeholder-safe: ${String(placeholderProtected).padStart(6)}`);
29
30
  lines.push(` Protected: ${String(protectedSkipped).padStart(6)}`);
30
31
  lines.push(` Skipped: ${skippedKeys.length}`);
32
+ lines.push(` Leftover warnings: ${String(residualUntranslated.length).padStart(3)}`);
31
33
  lines.push('='.repeat(72));
32
34
 
33
- if (skippedKeys.length === 0) {
35
+ if (skippedKeys.length === 0 && residualUntranslated.length === 0) {
34
36
  lines.push('');
35
37
  lines.push(' All strings were processed. No keys were skipped.');
36
38
  lines.push('');
37
39
  } else {
40
+ if (residualUntranslated.length > 0) {
41
+ lines.push('');
42
+ lines.push(' WARNING: The following values still look untranslated after');
43
+ lines.push(' Auto Translate and one final retry.');
44
+ lines.push('');
45
+ lines.push(' Rerun Auto Translate to capture leftovers, then review this');
46
+ lines.push(' report if any warnings remain.');
47
+ lines.push('');
48
+ lines.push(` ${'-'.repeat(72)}`);
49
+ lines.push(' File Key Path Current Value');
50
+ lines.push(` ${'-'.repeat(72)}`);
51
+ for (const item of residualUntranslated) {
52
+ const fileDisplay = String(item.fileName || path.basename(sourceFile || '') || 'N/A').padEnd(20).slice(0, 20);
53
+ const keyDisplay = String(item.keyPath || '').padEnd(40).slice(0, 40);
54
+ const valDisplay = String(item.value || '').length > 90
55
+ ? String(item.value).slice(0, 87) + '...'
56
+ : String(item.value || '');
57
+ lines.push(` ${fileDisplay} ${keyDisplay} ${valDisplay}`);
58
+ }
59
+ lines.push(` ${'-'.repeat(72)}`);
60
+ }
61
+
38
62
  if (placeholderSkipped.length > 0) {
39
63
  lines.push('');
40
64
  lines.push(' WARNING: The following keys were SKIPPED because they contain');
@@ -78,7 +102,10 @@ function generateReport(skippedKeys, translatedCount, totalCount, options = {})
78
102
  }
79
103
 
80
104
  lines.push('');
81
- if (skippedKeys.length === 0) {
105
+ if (residualUntranslated.length > 0) {
106
+ lines.push(' Auto Translate did not fully complete because leftover');
107
+ lines.push(' placeholder-prefixed or English-looking values remain.');
108
+ } else if (skippedKeys.length === 0) {
82
109
  lines.push(' The generated file can be used immediately after review.');
83
110
  lines.push(' Placeholder tokens were preserved automatically where found.');
84
111
  } else if (placeholderSkipped.length > 0) {