i18ntk 3.3.0 → 4.1.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.
@@ -41,15 +41,17 @@ const { logger } = require('../utils/logger');
41
41
  const { getGlobalReadline, closeGlobalReadline } = require('../utils/cli');
42
42
  const SetupEnforcer = require('../utils/setup-enforcer');
43
43
 
44
- // Ensure setup is complete before running
45
- (async () => {
46
- try {
47
- await SetupEnforcer.checkSetupCompleteAsync();
48
- } catch (error) {
49
- console.error('Setup check failed:', error.message);
50
- process.exit(1);
51
- }
52
- })();
44
+ // Ensure setup is complete before running (only when executed directly)
45
+ if (require.main === module) {
46
+ (async () => {
47
+ try {
48
+ await SetupEnforcer.checkSetupCompleteAsync();
49
+ } catch (error) {
50
+ console.error('Setup check failed:', error.message);
51
+ process.exit(1);
52
+ }
53
+ })();
54
+ }
53
55
 
54
56
  loadTranslations();
55
57
 
@@ -82,9 +84,11 @@ class I18nSizingAnalyzer {
82
84
  this.format = options.format || 'table';
83
85
  this.outputReport = options.outputReport || false;
84
86
  this.sourceLanguage = options.sourceLanguage || config.sourceLanguage || 'en';
85
- this.detailed = options.detailed || false;
86
- this.detailedKeys = options.detailedKeys || false;
87
- this.rl = null;
87
+ this.detailed = options.detailed || false;
88
+ this.detailedKeys = options.detailedKeys || false;
89
+ this.predictExpansion = options.predictExpansion || false;
90
+ this.rl = null;
91
+ this.expansionPredictions = null;
88
92
 
89
93
  // Initialize i18n with UI language from config
90
94
  const uiLanguage = options.uiLanguage || config.uiLanguage || 'en';
@@ -490,8 +494,13 @@ class I18nSizingAnalyzer {
490
494
  });
491
495
  this.generateFileComparison();
492
496
 
493
- // Generate recommendations
494
- this.generateRecommendations();
497
+ // Generate recommendations
498
+ this.generateRecommendations();
499
+
500
+ // Generate expansion predictions if requested
501
+ if (this.predictExpansion) {
502
+ this.generateExpansionPredictions();
503
+ }
495
504
  }
496
505
 
497
506
  generateFileComparison() {
@@ -557,6 +566,145 @@ class I18nSizingAnalyzer {
557
566
  this.stats.summary.recommendations = recommendations;
558
567
  }
559
568
 
569
+ // Language pair expansion reference table (average % expansion from English)
570
+ // Values represent typical character count ratio (target/source) minus 1, as percentage
571
+ getExpansionReference() {
572
+ return {
573
+ de: 35, es: 25, fr: 20, it: 15, pt: 20, nl: 30, sv: 15,
574
+ ru: 50, uk: 45, pl: 30, cs: 25, sk: 25, bg: 40, sr: 40,
575
+ ja: -40, zh: -45, ko: -35, th: -30, vi: -20, km: -5,
576
+ ar: 15, he: 10, fa: 20, tr: 10, fi: 25, hu: 20, el: 15,
577
+ da: 10, nb: 10, ro: 15, id: 5, ms: 5, hi: 15, bn: 20,
578
+ ta: 10, te: 15, mr: 20, gu: 20, ml: 25, kn: 15
579
+ };
580
+ }
581
+
582
+ classifyExpansionRisk(ratio) {
583
+ const absRatio = Math.abs(ratio);
584
+ if (absRatio < 30) return { tier: 'safe', label: 'Safe', color: 'green' };
585
+ if (absRatio < 50) return { tier: 'warning', label: 'Warning', color: 'yellow' };
586
+ return { tier: 'critical', label: 'Critical', color: 'red' };
587
+ }
588
+
589
+ generateExpansionPredictions() {
590
+ const languages = Object.keys(this.stats.languages);
591
+ if (languages.length < 2) return;
592
+
593
+ const baseLanguage = this.stats.summary.baseLanguage || this.config.sourceLanguage || 'en';
594
+ const expansionRef = this.getExpansionReference();
595
+ const predictions = {
596
+ baseLanguage,
597
+ perLanguage: {},
598
+ perKey: {},
599
+ topExpandedKeys: [],
600
+ summary: { safe: 0, warning: 0, critical: 0, total: 0 }
601
+ };
602
+
603
+ for (const lang of languages) {
604
+ if (lang === baseLanguage) continue;
605
+
606
+ const baseStats = this.stats.languages[baseLanguage];
607
+ const langStats = this.stats.languages[lang];
608
+ if (!baseStats || !langStats) continue;
609
+
610
+ const actualRatio = baseStats.totalCharacters > 0
611
+ ? ((langStats.totalCharacters - baseStats.totalCharacters) / baseStats.totalCharacters) * 100
612
+ : 0;
613
+
614
+ const referenceRatio = expansionRef[lang] || 10;
615
+ const risk = this.classifyExpansionRisk(actualRatio);
616
+ predictions.perLanguage[lang] = {
617
+ actualExpansionPercent: Math.round(actualRatio * 100) / 100,
618
+ referenceExpansionPercent: referenceRatio,
619
+ riskTier: risk.tier,
620
+ riskLabel: risk.label,
621
+ totalKeys: langStats.totalKeys,
622
+ totalChars: langStats.totalCharacters
623
+ };
624
+ }
625
+
626
+ const keyEntries = [];
627
+ for (const [key, langData] of Object.entries(this.stats.keys)) {
628
+ const baseData = langData[baseLanguage];
629
+ if (!baseData || baseData.length === 0) continue;
630
+
631
+ const keyPredictions = {};
632
+ for (const [lang, data] of Object.entries(langData)) {
633
+ if (lang === baseLanguage) continue;
634
+ const ratio = ((data.length - baseData.length) / baseData.length) * 100;
635
+ const risk = this.classifyExpansionRisk(ratio);
636
+ keyPredictions[lang] = {
637
+ sourceLength: baseData.length,
638
+ targetLength: data.length,
639
+ expansionPercent: Math.round(ratio * 100) / 100,
640
+ riskTier: risk.tier
641
+ };
642
+ predictions.summary[risk.tier]++;
643
+ predictions.summary.total++;
644
+ }
645
+
646
+ const maxExpansion = Math.max(...Object.values(keyPredictions).map(p => Math.abs(p.expansionPercent)));
647
+ keyEntries.push({ key, maxExpansion, predictions: keyPredictions });
648
+ }
649
+
650
+ keyEntries.sort((a, b) => b.maxExpansion - a.maxExpansion);
651
+ predictions.topExpandedKeys = keyEntries.slice(0, 30);
652
+ predictions.perKey = Object.fromEntries(keyEntries.slice(0, 200).map(e => [e.key, e.predictions]));
653
+
654
+ this.expansionPredictions = predictions;
655
+ this.stats.expansionPredictions = predictions;
656
+ }
657
+
658
+ displayExpansionPredictions() {
659
+ if (!this.expansionPredictions) return;
660
+
661
+ const p = this.expansionPredictions;
662
+ console.log('\n' + '='.repeat(60));
663
+ console.log(' EXPANSION PREDICTION ANALYSIS');
664
+ console.log('='.repeat(60));
665
+ console.log(` Base Language: ${p.baseLanguage}`);
666
+ console.log(` Total Key-Language Pairs Analyzed: ${p.summary.total}`);
667
+ console.log(` Safe (<30%): ${p.summary.safe} | Warning (30-50%): ${p.summary.warning} | Critical (>50%): ${p.summary.critical}`);
668
+
669
+ console.log('\n PER-LANGUAGE EXPANSION RATIOS:');
670
+ const langRows = Object.entries(p.perLanguage).map(([lang, data]) => ({
671
+ language: lang,
672
+ actual: `${data.actualExpansionPercent > 0 ? '+' : ''}${data.actualExpansionPercent}%`,
673
+ reference: `${data.referenceExpansionPercent > 0 ? '+' : ''}${data.referenceExpansionPercent}%`,
674
+ risk: data.riskLabel
675
+ }));
676
+ console.log(this.createTable([
677
+ { key: 'language', label: 'Language' },
678
+ { key: 'actual', label: 'Actual', align: 'right' },
679
+ { key: 'reference', label: 'Reference', align: 'right' },
680
+ { key: 'risk', label: 'Risk Tier' }
681
+ ], langRows));
682
+
683
+ if (p.topExpandedKeys.length > 0) {
684
+ console.log('\n TOP EXPANDED KEYS (highest risk of UI overflow):');
685
+ const keyRows = p.topExpandedKeys.slice(0, 15).map(entry => {
686
+ const langs = Object.entries(entry.predictions).map(([l, d]) =>
687
+ `${l}:${d.expansionPercent > 0 ? '+' : ''}${d.expansionPercent}%`
688
+ ).join(' ');
689
+ return { key: entry.key, maxExp: `${entry.maxExpansion > 0 ? '+' : ''}${Math.round(entry.maxExpansion)}%`, languages: langs };
690
+ });
691
+ console.log(this.createTable([
692
+ { key: 'key', label: 'Key' },
693
+ { key: 'maxExp', label: 'Max Exp', align: 'right' },
694
+ { key: 'languages', label: 'Per-Language' }
695
+ ], keyRows));
696
+ }
697
+
698
+ console.log('\n RECOMMENDATIONS:');
699
+ if (p.summary.critical > 0) {
700
+ console.log(` - ${p.summary.critical} key-language pairs have >50% expansion — review UI layouts for truncation risk`);
701
+ }
702
+ if (p.summary.warning > 0) {
703
+ console.log(` - ${p.summary.warning} key-language pairs have 30-50% expansion — test on target languages`);
704
+ }
705
+ console.log(' - Use the reference expansion ratios to plan UI element sizing for unsupported languages');
706
+ }
707
+
560
708
  // Display concise folder-level results
561
709
  displayFolderResults() {
562
710
  console.log("\n" + t("sizing.sizing_analysis_results"));
@@ -616,9 +764,13 @@ class I18nSizingAnalyzer {
616
764
  reportPath: this.outputDir
617
765
  }));
618
766
 
619
- if (this.detailedKeys) {
620
- this.displayDetailedKeys();
621
- }
767
+ if (this.detailedKeys) {
768
+ this.displayDetailedKeys();
769
+ }
770
+
771
+ if (this.predictExpansion && this.expansionPredictions) {
772
+ this.displayExpansionPredictions();
773
+ }
622
774
  }
623
775
 
624
776
  displayFileComparison() {
@@ -874,17 +1026,50 @@ Generated: ${new Date().toISOString()}
874
1026
  report += `
875
1027
  ### ${key}
876
1028
  `;
877
- Object.entries(data).forEach(([lang, keyData]) => {
878
- const length = keyData.length;
879
- const isEmpty = length === 0;
880
- const isLong = length > this.threshold;
881
- const status = isEmpty ? 'EMPTY' : isLong ? 'LONG' : 'OK';
882
- report += `- ${lang}: ${length} chars [${status}]\n`;
883
- });
1029
+ Object.entries(data).forEach(([lang, keyData]) => {
1030
+ const length = keyData.length;
1031
+ const isEmpty = length === 0;
1032
+ const isLong = length > this.threshold;
1033
+ const status = isEmpty ? 'EMPTY' : isLong ? 'LONG' : 'OK';
1034
+ report += `- ${lang}: ${length} chars [${status}]\n`;
1035
+ });
1036
+ });
1037
+ }
1038
+
1039
+ // Expansion predictions
1040
+ if (this.expansionPredictions) {
1041
+ const ep = this.expansionPredictions;
1042
+ report += `
1043
+ ## Expansion Prediction Analysis
1044
+
1045
+ ### Summary
1046
+ - Base Language: ${ep.baseLanguage}
1047
+ - Safe (<30%): ${ep.summary.safe}
1048
+ - Warning (30-50%): ${ep.summary.warning}
1049
+ - Critical (>50%): ${ep.summary.critical}
1050
+ - Total Pairs: ${ep.summary.total}
1051
+
1052
+ ### Per-Language Ratios
1053
+ `;
1054
+ Object.entries(ep.perLanguage).forEach(([lang, data]) => {
1055
+ report += `- ${lang}: ${data.actualExpansionPercent > 0 ? '+' : ''}${data.actualExpansionPercent}% (ref: ${data.referenceExpansionPercent > 0 ? '+' : ''}${data.referenceExpansionPercent}%) [${data.riskLabel}]\n`;
884
1056
  });
1057
+
1058
+ if (ep.topExpandedKeys.length > 0) {
1059
+ report += `
1060
+ ### Top Expanded Keys
1061
+ `;
1062
+ ep.topExpandedKeys.slice(0, 30).forEach((entry, idx) => {
1063
+ report += `${idx + 1}. ${entry.key} (max: ${entry.maxExpansion > 0 ? '+' : ''}${Math.round(entry.maxExpansion)}%)\n`;
1064
+ Object.entries(entry.predictions).forEach(([l, d]) => {
1065
+ report += ` ${l}: ${d.sourceLength} → ${d.targetLength} chars (${d.expansionPercent > 0 ? '+' : ''}${d.expansionPercent}%) [${d.riskTier}]\n`;
1066
+ });
1067
+ report += '\n';
1068
+ });
1069
+ }
885
1070
  }
886
1071
 
887
- return report;
1072
+ return report;
888
1073
  }
889
1074
 
890
1075
  // Generate CSV report
@@ -917,45 +1102,7 @@ Generated: ${new Date().toISOString()}
917
1102
  }
918
1103
  }
919
1104
 
920
- // Main analysis method
921
- async analyze() {
922
- const startTime = Date.now();
923
-
924
- try {
925
- logger.info(t("sizing.starting_i18n_sizing_analysis"));
926
- logger.info(t("sizing.source_directory", { sourceDir: this.sourceDir }));
927
-
928
- const files = this.getLanguageFiles();
929
-
930
- if (files.length === 0) {
931
- logger.warn(t("sizing.no_translation_files_found"));
932
- return;
933
- }
934
-
935
- logger.info(t("sizing.found_languages", { languages: files.map(f => f.language).join(', ') }));
936
-
937
- this.analyzeFileSizes(files);
938
- this.analyzeTranslationContent(files);
939
- this.generateSizeComparison();
940
-
941
- if (this.format === 'table') {
942
- this.displayFolderResults();
943
- } else if (this.format === 'json') {
944
- logger.info(t("sizing.analysisStats", { stats: JSON.stringify(this.stats, null, 2) }));
945
- }
946
-
947
- await this.generateHumanReadableReport();
948
-
949
- const endTime = Date.now();
950
- logger.info(t("sizing.analysis_completed", { duration: ((endTime - startTime) / 1000).toFixed(2) }));
951
-
952
- } catch (error) {
953
- logger.error(t("sizing.analysis_failed", { errorMessage: error.message }));
954
- process.exit(1);
955
- }
956
- }
957
-
958
- // Parse command line arguments without yargs
1105
+ // Parse command line arguments without yargs
959
1106
  parseArgs() {
960
1107
  const args = process.argv.slice(2);
961
1108
  const options = {
@@ -1018,6 +1165,8 @@ Generated: ${new Date().toISOString()}
1018
1165
  options.d = options.detailed;
1019
1166
  } else if (key === 'detailed-keys') {
1020
1167
  options['detailed-keys'] = value.toLowerCase() !== 'false';
1168
+ } else if (key === 'predict-expansion') {
1169
+ options['predict-expansion'] = value.toLowerCase() !== 'false';
1021
1170
  } else if (key === 'output-dir') {
1022
1171
  options['output-dir'] = value;
1023
1172
  }
@@ -1073,17 +1222,24 @@ Generated: ${new Date().toISOString()}
1073
1222
  options.detailed = true;
1074
1223
  options.d = true;
1075
1224
  }
1076
- } else if (key === 'detailed-keys') {
1077
- if (nextArg && !nextArg.startsWith('-') && ['true', 'false'].includes(nextArg.toLowerCase())) {
1078
- options['detailed-keys'] = nextArg.toLowerCase() !== 'false';
1079
- i++;
1080
- } else {
1081
- options['detailed-keys'] = true;
1082
- }
1083
- } else if (key === 'output-dir') {
1084
- options['output-dir'] = nextArg || options['output-dir'];
1085
- if (nextArg && !nextArg.startsWith('-')) i++;
1086
- }
1225
+ } else if (key === 'detailed-keys') {
1226
+ if (nextArg && !nextArg.startsWith('-') && ['true', 'false'].includes(nextArg.toLowerCase())) {
1227
+ options['detailed-keys'] = nextArg.toLowerCase() !== 'false';
1228
+ i++;
1229
+ } else {
1230
+ options['detailed-keys'] = true;
1231
+ }
1232
+ } else if (key === 'predict-expansion') {
1233
+ if (nextArg && !nextArg.startsWith('-') && ['true', 'false'].includes(nextArg.toLowerCase())) {
1234
+ options['predict-expansion'] = nextArg.toLowerCase() !== 'false';
1235
+ i++;
1236
+ } else {
1237
+ options['predict-expansion'] = true;
1238
+ }
1239
+ } else if (key === 'output-dir') {
1240
+ options['output-dir'] = nextArg || options['output-dir'];
1241
+ if (nextArg && !nextArg.startsWith('-')) i++;
1242
+ }
1087
1243
  }
1088
1244
  }
1089
1245
 
@@ -1102,6 +1258,7 @@ Options:
1102
1258
  --source-language <code> Source language baseline for comparisons (default: en)
1103
1259
  -d, --detailed Generate detailed report with more information
1104
1260
  --detailed-keys Show detailed key-level analysis
1261
+ --predict-expansion Predict UI layout expansion risk per language
1105
1262
  --output-dir <dir> Output directory for reports (default: ./i18ntk-reports)
1106
1263
  --help Show this help message
1107
1264
  `);
@@ -1124,8 +1281,9 @@ Options:
1124
1281
  this.languages = args.languages ? args.languages.split(',').map(l => l.trim()) : [];
1125
1282
  this.outputReport = args['output-report'] !== undefined ? args['output-report'] : false;
1126
1283
  this.format = args.format || 'table';
1127
- this.detailed = args.detailed;
1128
- this.detailedKeys = args['detailed-keys'];
1284
+ this.detailed = args.detailed;
1285
+ this.detailedKeys = args['detailed-keys'];
1286
+ this.predictExpansion = args['predict-expansion'] || false;
1129
1287
  this.sourceLanguage = args['source-language'] || config.sourceLanguage || this.sourceLanguage || 'en';
1130
1288
 
1131
1289
  if (!fromMenu) {
@@ -1141,7 +1299,7 @@ Options:
1141
1299
 
1142
1300
  const cliHelper = require('../utils/cli-helper');
1143
1301
  const pin = await cliHelper.promptPin(t('adminCli.enterPin'));
1144
- const isValid = await this.adminAuth.verifyPin(pin);
1302
+ const isValid = await adminAuth.verifyPin(pin);
1145
1303
 
1146
1304
  if (!isValid) {
1147
1305
  console.log(t('adminCli.invalidPin'));
@@ -1183,7 +1341,11 @@ Options:
1183
1341
  this.generateSizeComparison();
1184
1342
 
1185
1343
  // Display results
1186
- this.displayFolderResults();
1344
+ if (this.format === 'table') {
1345
+ this.displayFolderResults();
1346
+ } else if (this.format === 'json') {
1347
+ logger.info(t("sizing.analysisStats", { stats: JSON.stringify(this.stats, null, 2) }));
1348
+ }
1187
1349
 
1188
1350
  // Generate reports if requested
1189
1351
  await this.generateHumanReadableReport();