i18ntk 4.0.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +116 -29
  2. package/README.md +83 -18
  3. package/SECURITY.md +13 -5
  4. package/main/i18ntk-analyze.js +10 -20
  5. package/main/i18ntk-backup.js +227 -111
  6. package/main/i18ntk-init.js +153 -157
  7. package/main/i18ntk-scanner.js +9 -7
  8. package/main/i18ntk-setup.js +36 -13
  9. package/main/i18ntk-sizing.js +18 -50
  10. package/main/i18ntk-translate.js +169 -21
  11. package/main/i18ntk-usage.js +298 -154
  12. package/main/i18ntk-validate.js +49 -37
  13. package/main/manage/commands/AnalyzeCommand.js +7 -17
  14. package/main/manage/commands/CommandRouter.js +6 -6
  15. package/main/manage/commands/TranslateCommand.js +65 -56
  16. package/main/manage/commands/ValidateCommand.js +34 -26
  17. package/main/manage/index.js +11 -42
  18. package/main/manage/managers/InteractiveMenu.js +11 -40
  19. package/main/manage/services/InitService.js +114 -118
  20. package/main/manage/services/UsageService.js +244 -85
  21. package/package.json +55 -4
  22. package/runtime/enhanced.d.ts +5 -5
  23. package/runtime/enhanced.js +49 -25
  24. package/runtime/i18ntk.d.ts +30 -7
  25. package/runtime/index.d.ts +48 -19
  26. package/runtime/index.js +188 -97
  27. package/settings/settings-cli.js +115 -38
  28. package/settings/settings-manager.js +24 -6
  29. package/ui-locales/de.json +192 -11
  30. package/ui-locales/en.json +182 -8
  31. package/ui-locales/es.json +193 -12
  32. package/ui-locales/fr.json +189 -8
  33. package/ui-locales/ja.json +190 -8
  34. package/ui-locales/ru.json +191 -9
  35. package/ui-locales/zh.json +194 -9
  36. package/utils/cli-helper.js +8 -12
  37. package/utils/config-helper.js +1 -1
  38. package/utils/config-manager.js +8 -6
  39. package/utils/localized-confirm.js +55 -0
  40. package/utils/menu-layout.js +41 -0
  41. package/utils/report-writer.js +110 -0
  42. package/utils/security.js +15 -22
  43. package/utils/translate/api.js +31 -3
  44. package/utils/translate/placeholder.js +42 -1
  45. package/utils/translate/protection.js +17 -12
  46. package/utils/translate/report.js +3 -2
  47. package/utils/translate/safe-network.js +24 -4
  48. package/utils/usage-insights.js +435 -0
  49. package/utils/usage-source.js +50 -0
  50. package/utils/watch-locales.js +13 -9
@@ -64,16 +64,23 @@ const { detectTranslationContentRisks } = require('../utils/validation-risk');
64
64
  loadTranslations('en', path.resolve(__dirname, '..', 'ui-locales'));
65
65
 
66
66
  class I18nValidator {
67
- constructor(config = {}) {
68
- this.config = config;
69
- this.errors = [];
70
- this.warnings = [];
71
- this.keyNamingViolations = [];
72
- this.rl = null;
73
- }
67
+ constructor(config = {}) {
68
+ this.config = config;
69
+ this.errors = [];
70
+ this.warnings = [];
71
+ this.keyNamingViolations = [];
72
+ this.rl = null;
73
+ this.initialized = false;
74
+ this.pathsDisplayed = false;
75
+ this.validationBannerDisplayed = false;
76
+ }
74
77
 
75
- async initialize() {
76
- try {
78
+ async initialize() {
79
+ try {
80
+ if (this.initialized) {
81
+ return;
82
+ }
83
+
77
84
  // Initialize i18n with UI language first
78
85
  const args = this.parseArgs();
79
86
  if (args.help) {
@@ -120,7 +127,9 @@ class I18nValidator {
120
127
  }
121
128
  }
122
129
 
123
- displayPaths({ sourceDir: this.sourceDir, i18nDir: this.i18nDir, outputDir: this.config.outputDir });
130
+ displayPaths({ sourceDir: this.sourceDir, i18nDir: this.i18nDir, outputDir: this.config.outputDir });
131
+ this.pathsDisplayed = true;
132
+ this.initialized = true;
124
133
 
125
134
  SecurityUtils.logSecurityEvent(
126
135
  'I18n validator initialized successfully',
@@ -164,12 +173,13 @@ class I18nValidator {
164
173
  const args = process.argv.slice(2);
165
174
  args.forEach(arg => {
166
175
  const sanitizedArg = SecurityUtils.sanitizeInput(arg);
167
- if (sanitizedArg.startsWith('--') && !sanitizedArg.includes('=')) {
176
+ if (sanitizedArg.startsWith('--enforce-key-style')) {
177
+ const val = arg.split('=')[1];
178
+ baseArgs.enforceKeyStyle = val === undefined ? true : val !== 'false';
179
+ } else if (sanitizedArg.startsWith('--') && !sanitizedArg.includes('=')) {
168
180
  const key = sanitizedArg.substring(2);
169
181
  if (['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(key)) {
170
182
  baseArgs.uiLanguage = key;
171
- } else if (key === 'enforce-key-style') {
172
- baseArgs.enforceKeyStyle = true;
173
183
  }
174
184
  }
175
185
  });
@@ -242,7 +252,7 @@ class I18nValidator {
242
252
  const files = items
243
253
  .filter(item => {
244
254
  return item.isFile() && item.name.endsWith('.json') &&
245
- !this.config.excludeFiles.includes(item.name);
255
+ (!Array.isArray(this.config.excludeFiles) || !this.config.excludeFiles.includes(item.name));
246
256
  }).map(item => item.name);
247
257
 
248
258
  return files;
@@ -691,7 +701,8 @@ class I18nValidator {
691
701
  const violations = [];
692
702
  for (const key of allKeys) {
693
703
  const sanitizedKey = SecurityUtils.sanitizeInput(key);
694
- if (!regex.test(sanitizedKey)) {
704
+ const testKey = keyStyle === 'flat' ? sanitizedKey.split('.').pop() : sanitizedKey;
705
+ if (!regex.test(testKey)) {
695
706
  violations.push({
696
707
  key: sanitizedKey,
697
708
  suggestedFix: this.suggestKeyFix(sanitizedKey, keyStyle),
@@ -729,7 +740,7 @@ class I18nValidator {
729
740
  case 'kebab-case':
730
741
  return segments.map(s => s.toLowerCase()).join('-');
731
742
  case 'flat':
732
- return segments.map((s, i) => i === 0 ? s.toLowerCase() : s.charAt(0).toUpperCase() + s.slice(1).toLowerCase()).join('');
743
+ return segments.map(s => s.toLowerCase()).join('');
733
744
  default:
734
745
  return sanitizedKey;
735
746
  }
@@ -778,9 +789,12 @@ class I18nValidator {
778
789
  const args = this.parseArgs();
779
790
  const jsonOutput = new JsonOutput('validate');
780
791
 
781
- if (!args.json) {
782
- console.log(t('validate.title'));
783
- console.log(t('validate.message'));
792
+ if (!args.json) {
793
+ console.log('');
794
+ console.log(t('validate.title'));
795
+ if (!this.validationBannerDisplayed) {
796
+ console.log(t('validate.message'));
797
+ }
784
798
 
785
799
  // Delete old validation report if it exists
786
800
  const reportPath = path.join(process.cwd(), 'validation-report.txt');
@@ -810,11 +824,14 @@ class I18nValidator {
810
824
  this.config.strictMode = true;
811
825
  }
812
826
 
813
- if (!args.json) {
814
- console.log(t('validate.sourceDirectory', { dir: this.sourceDir }));
815
- console.log(t('validate.sourceLanguage', { sourceLanguage: this.config.sourceLanguage }));
816
- console.log(t('validate.strictMode', { mode: this.config.strictMode ? 'ON' : 'OFF' }));
817
- }
827
+ if (!args.json) {
828
+ if (!this.pathsDisplayed) {
829
+ console.log(t('validate.sourceDirectory', { dir: this.sourceDir }));
830
+ }
831
+ console.log(t('validate.sourceLanguage', { sourceLanguage: this.config.sourceLanguage }));
832
+ console.log(t('validate.strictMode', { mode: this.config.strictMode ? 'ON' : 'OFF' }));
833
+ console.log('');
834
+ }
818
835
 
819
836
  // Validate source language directory exists
820
837
  SecurityUtils.validatePath(this.sourceLanguageDir);
@@ -1054,23 +1071,16 @@ class I18nValidator {
1054
1071
 
1055
1072
  const args = this.parseArgs();
1056
1073
 
1074
+ try {
1057
1075
  // Ensure config is always initialized
1058
1076
  if (!this.config) {
1059
1077
  this.config = {};
1060
1078
  }
1061
1079
 
1062
- // Initialize configuration properly when called from menu
1063
- if (fromMenu && !this.sourceDir) {
1064
- const baseConfig = await getUnifiedConfig('validate', args);
1065
- this.config = { ...baseConfig, ...(this.config || {}) };
1066
-
1067
- const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
1068
- loadTranslations(uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));
1069
- this.sourceDir = this.config.sourceDir;
1070
- this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
1071
- } else {
1072
- await this.initialize();
1073
- }
1080
+ if (!this.initialized) {
1081
+ await this.initialize();
1082
+ }
1083
+ this.config.enforceKeyStyle = args.enforceKeyStyle !== undefined ? args.enforceKeyStyle : this.config.enforceKeyStyle;
1074
1084
 
1075
1085
  // Skip admin authentication when called from menu
1076
1086
  if (!fromMenu) {
@@ -1101,7 +1111,8 @@ class I18nValidator {
1101
1111
  }
1102
1112
  const execute = async () => {
1103
1113
 
1104
- console.log(t('validate.startingValidationProcess'));
1114
+ console.log('\n' + t('validate.startingValidationProcess'));
1115
+ this.validationBannerDisplayed = true;
1105
1116
  SecurityUtils.logSecurityEvent(
1106
1117
  t('validate.runStarted'),
1107
1118
  'info',
@@ -1146,6 +1157,7 @@ class I18nValidator {
1146
1157
  );
1147
1158
  throw error;
1148
1159
  }
1160
+ }
1149
1161
  }
1150
1162
 
1151
1163
 
@@ -17,6 +17,8 @@ const AdminCLI = require('../../../utils/admin-cli');
17
17
  const AdminAuth = require('../../../utils/admin-auth');
18
18
  const watchLocales = require('../../../utils/watch-locales');
19
19
  const JsonOutput = require('../../../utils/json-output');
20
+ const configManager = require('../../../utils/config-manager');
21
+ const { normalizeReportFormat, writeReportFile } = require('../../../utils/report-writer');
20
22
 
21
23
  loadTranslations('en', path.resolve(__dirname, '../../../ui-locales'));
22
24
 
@@ -752,23 +754,11 @@ class AnalyzeCommand {
752
754
  return null;
753
755
  }
754
756
 
755
- // Create a safe filename
756
- const safeLanguage = language.replace(/[^\w-]/g, '_');
757
- const reportPath = path.resolve(validatedOutputDir, `translation-report-${safeLanguage}.json`);
758
-
759
- // Ensure the final path is still within the output directory
760
- if (!reportPath.startsWith(validatedOutputDir)) {
761
- console.error('Invalid report path detected, potential directory traversal attack');
762
- return null;
763
- }
764
-
765
- // Use safeWriteFile for secure file writing
766
- const success = await SecurityUtils.safeWriteFile(reportPath, JSON.stringify(report, null, 2), process.cwd(), 'utf8');
767
- if (!success) {
768
- throw new Error(t('analyze.failedToWriteReportFile') || 'Failed to write report file securely');
769
- }
770
-
771
- return reportPath;
757
+ const safeLanguage = language.replace(/[^\w-]/g, '_');
758
+ const settings = configManager.getConfig ? configManager.getConfig() : {};
759
+ const format = normalizeReportFormat(this.config?.reports?.format || settings.reports?.format || this.config?.reportFormat || 'markdown');
760
+ const reportPath = await writeReportFile(validatedOutputDir, `translation-report-${safeLanguage}`, report, { format, title: `Translation Report ${safeLanguage.toUpperCase()}` });
761
+ return reportPath;
772
762
 
773
763
  } catch (error) {
774
764
  console.error(`Failed to save report for ${language}:`, error.message);
@@ -119,11 +119,11 @@ class CommandRouter {
119
119
  return true;
120
120
  }
121
121
 
122
- /**
123
- * Execute a command with proper routing and error handling
124
- */
125
- async executeCommand(command, options = {}) {
126
- console.log(t('menu.executingCommand', { command }));
122
+ /**
123
+ * Execute a command with proper routing and error handling
124
+ */
125
+ async executeCommand(command, options = {}) {
126
+ console.log('\n' + t('menu.executingCommand', { command }));
127
127
 
128
128
  // Enhanced context detection
129
129
  const executionContext = this.getExecutionContext(options);
@@ -161,7 +161,7 @@ class CommandRouter {
161
161
  const result = await this.routeCommand(command, options, executionContext);
162
162
 
163
163
  // Handle command completion based on execution context
164
- console.log(t('operations.completed'));
164
+ console.log('\n' + t('operations.completed'));
165
165
 
166
166
  if (isManagerExecution && !this.isNonInteractiveMode && this.prompt) {
167
167
  // Interactive menu execution - return to menu
@@ -11,7 +11,9 @@ const path = require('path');
11
11
  const SecurityUtils = require('../../../utils/security');
12
12
  const configManager = require('../../../utils/config-manager');
13
13
  const { getUnifiedConfig } = require('../../../utils/config-helper');
14
- const { loadTranslations } = require('../../../utils/i18n-helper');
14
+ const { loadTranslations, t } = require('../../../utils/i18n-helper');
15
+ const { parseConfirmation } = require('../../../utils/localized-confirm');
16
+ const { DEFAULT_CONCURRENCY, getProviderConcurrencyLimit } = require('../../../utils/translate/api');
15
17
  const SetupEnforcer = require('../../../utils/setup-enforcer');
16
18
  const {
17
19
  createProtectionFile,
@@ -38,6 +40,11 @@ class TranslateCommand {
38
40
  this.safeClose = safeClose;
39
41
  }
40
42
 
43
+ tr(key, replacements = {}, fallback = '') {
44
+ const value = t(key, replacements);
45
+ return value && value !== key ? value : fallback;
46
+ }
47
+
41
48
  async execute(options = {}) {
42
49
  try {
43
50
  await SetupEnforcer.checkSetupCompleteAsync();
@@ -46,15 +53,15 @@ class TranslateCommand {
46
53
  return { success: false, error: 'Setup required' };
47
54
  }
48
55
 
49
- loadTranslations('en', path.resolve(__dirname, '..', '..', '..', 'ui-locales'));
50
-
51
56
  const config = this.config || {};
52
57
  let unified;
53
58
  try {
54
- unified = await getUnifiedConfig('translate', options);
59
+ unified = { ...(await getUnifiedConfig('translate', options)), ...config };
55
60
  } catch (_) {
56
61
  unified = config;
57
62
  }
63
+ const uiLanguage = unified.uiLanguage || unified.language || 'en';
64
+ loadTranslations(uiLanguage, path.resolve(__dirname, '..', '..', '..', 'ui-locales'));
58
65
  this.autoTranslateSettings = this.getAutoTranslateSettings(unified);
59
66
 
60
67
  const defaultSourceDir = unified.sourceDir || unified.i18nDir || path.resolve(process.cwd(), 'locales', 'en');
@@ -62,13 +69,13 @@ class TranslateCommand {
62
69
  this.configuredTargetLangs = this.getConfiguredTargetLanguages(unified, defaultSourceDir);
63
70
 
64
71
  console.log('\n============================================================');
65
- console.log(' \u{1F310} AUTO TRANSLATE (BETA)');
72
+ console.log(` ${t('translate.title') || '\u{1F310} Auto Translate'}`);
66
73
  console.log('============================================================');
67
74
 
68
75
  if (this.isNonInteractiveMode) {
69
76
  this.sourceDir = defaultSourceDir;
70
77
  if (!SecurityUtils.safeExistsSync(this.sourceDir, path.dirname(this.sourceDir))) {
71
- console.error(`Source locale directory not found: ${this.sourceDir}`);
78
+ console.error(this.tr('translate.errors.sourceDirectoryNotFound', { dir: this.sourceDir }, `Source locale directory not found: ${this.sourceDir}`));
72
79
  return { success: false, error: 'Source directory not found' };
73
80
  }
74
81
  const resolvedSource = this.resolveSourceDirectoryForLanguage(this.sourceDir, this.sourceLang);
@@ -105,25 +112,25 @@ class TranslateCommand {
105
112
 
106
113
  async promptSourceDir(ask, defaultDir) {
107
114
  while (true) {
108
- console.log('\n Source locale directory');
109
- console.log(` Default: ${defaultDir}`);
110
- console.log(` Current project: ${process.cwd()}`);
111
- console.log(' Accepted: an absolute path, or a path relative to the current project.');
112
- console.log(' Examples:');
115
+ console.log('\n ' + this.tr('translate.sourceDirectory.title', {}, 'Source locale directory'));
116
+ console.log(' ' + this.tr('translate.common.default', { value: defaultDir }, `Default: ${defaultDir}`));
117
+ console.log(' ' + this.tr('translate.sourceDirectory.currentProject', { dir: process.cwd() }, `Current project: ${process.cwd()}`));
118
+ console.log(' ' + this.tr('translate.sourceDirectory.accepted', {}, 'Accepted: an absolute path, or a path relative to the current project.'));
119
+ console.log(' ' + this.tr('translate.common.examples', {}, 'Examples:'));
113
120
  console.log(' ./locales/en');
114
- console.log(' ./locales (then choose source language: en)');
121
+ console.log(' ' + this.tr('translate.sourceDirectory.localeRootExample', {}, './locales (then choose source language: en)'));
115
122
  console.log(` ${defaultDir}`);
116
- console.log(' The folder can contain JSON files directly, or language folders such as ./locales/en.');
117
- console.log(' Press Enter to use the default.');
123
+ console.log(' ' + this.tr('translate.sourceDirectory.folderHint', {}, 'The folder can contain JSON files directly, or language folders such as ./locales/en.'));
124
+ console.log(' ' + this.tr('translate.common.pressEnterDefault', {}, 'Press Enter to use the default.'));
118
125
  const input = await ask(' > ');
119
126
 
120
127
  if (!input.trim()) {
121
128
  if (!SecurityUtils.safeExistsSync(defaultDir, path.dirname(defaultDir))) {
122
- console.log(` Default directory not found: ${defaultDir}`);
123
- console.log(' Please enter an existing directory with JSON locale files.');
129
+ console.log(' ' + this.tr('translate.sourceDirectory.defaultNotFound', { dir: defaultDir }, `Default directory not found: ${defaultDir}`));
130
+ console.log(' ' + this.tr('translate.sourceDirectory.enterExisting', {}, 'Please enter an existing directory with JSON locale files.'));
124
131
  continue;
125
132
  }
126
- console.log(` Using default: ${defaultDir}`);
133
+ console.log(' ' + this.tr('translate.common.usingDefault', { value: defaultDir }, `Using default: ${defaultDir}`));
127
134
  return defaultDir;
128
135
  }
129
136
 
@@ -132,31 +139,31 @@ class TranslateCommand {
132
139
  ? path.resolve(cleanInput)
133
140
  : path.resolve(process.cwd(), cleanInput);
134
141
  if (!SecurityUtils.safeExistsSync(resolved, path.dirname(resolved))) {
135
- console.log(` Directory not found: ${resolved}`);
136
- console.log(' Enter an existing folder, for example ./locales/en.');
142
+ console.log(' ' + this.tr('translate.sourceDirectory.directoryNotFound', { dir: resolved }, `Directory not found: ${resolved}`));
143
+ console.log(' ' + this.tr('translate.sourceDirectory.enterFolderExample', {}, 'Enter an existing folder, for example ./locales/en.'));
137
144
  continue;
138
145
  }
139
146
  const stats = SecurityUtils.safeStatSync(resolved, path.dirname(resolved));
140
147
  if (!stats || !stats.isDirectory()) {
141
- console.log(` Not a directory: ${resolved}`);
148
+ console.log(' ' + this.tr('translate.sourceDirectory.notDirectory', { dir: resolved }, `Not a directory: ${resolved}`));
142
149
  continue;
143
150
  }
144
- console.log(` Using source directory: ${resolved}`);
151
+ console.log(' ' + this.tr('translate.sourceDirectory.using', { dir: resolved }, `Using source directory: ${resolved}`));
145
152
  return resolved;
146
153
  }
147
154
  }
148
155
 
149
156
  async promptSourceLang(ask) {
150
157
  while (true) {
151
- console.log('\n Source language code');
152
- console.log(` Default: ${this.sourceLang}`);
153
- console.log(' This should match the language of the source JSON values.');
154
- console.log(' Example: en');
155
- console.log(' Press Enter to use the default.');
158
+ console.log('\n ' + this.tr('translate.sourceLanguage.title', {}, 'Source language code'));
159
+ console.log(' ' + this.tr('translate.common.default', { value: this.sourceLang }, `Default: ${this.sourceLang}`));
160
+ console.log(' ' + this.tr('translate.sourceLanguage.hint', {}, 'This should match the language of the source JSON values.'));
161
+ console.log(' ' + this.tr('translate.common.exampleValue', { value: 'en' }, 'Example: en'));
162
+ console.log(' ' + this.tr('translate.common.pressEnterDefault', {}, 'Press Enter to use the default.'));
156
163
  const input = await ask(' > ');
157
164
 
158
165
  if (!input.trim()) {
159
- console.log(` Using source language: ${this.sourceLang}`);
166
+ console.log(' ' + this.tr('translate.sourceLanguage.using', { lang: this.sourceLang }, `Using source language: ${this.sourceLang}`));
160
167
  return this.sourceLang;
161
168
  }
162
169
 
@@ -164,46 +171,46 @@ class TranslateCommand {
164
171
  if (lang.length >= 2) {
165
172
  return lang;
166
173
  }
167
- console.log(' Invalid language code. Use 2+ characters (e.g. en, de, fr).');
174
+ console.log(' ' + this.tr('translate.sourceLanguage.invalid', {}, 'Invalid language code. Use 2+ characters (e.g. en, de, fr).'));
168
175
  }
169
176
  }
170
177
 
171
178
  async interactiveFlow(jsonFiles, ask) {
172
179
  await this.maybeConfigureProtection(ask);
173
180
 
174
- console.log('\n Target language(s)');
181
+ console.log('\n ' + this.tr('translate.targetLanguages.title', {}, 'Target language(s)'));
175
182
  if (this.configuredTargetLangs.length > 0) {
176
- console.log(` a) All configured target languages: ${this.configuredTargetLangs.join(', ')}`);
183
+ console.log(' ' + this.tr('translate.targetLanguages.allConfigured', { languages: this.configuredTargetLangs.join(', ') }, `a) All configured target languages: ${this.configuredTargetLangs.join(', ')}`));
177
184
  } else {
178
- console.log(' a) All configured target languages: none configured');
185
+ console.log(' ' + this.tr('translate.targetLanguages.noneConfigured', {}, 'a) All configured target languages: none configured'));
179
186
  }
180
- console.log(' Or enter one or more comma/space-separated language codes.');
181
- console.log(' Examples: de, es, fr or de es fr or zh');
182
- console.log(` Source language "${this.sourceLang}" will be excluded automatically.`);
187
+ console.log(' ' + this.tr('translate.targetLanguages.enterCodes', {}, 'Or enter one or more comma/space-separated language codes.'));
188
+ console.log(' ' + this.tr('translate.common.examplesInline', { examples: 'de, es, fr or de es fr or zh' }, 'Examples: de, es, fr or de es fr or zh'));
189
+ console.log(' ' + this.tr('translate.targetLanguages.sourceExcluded', { lang: this.sourceLang }, `Source language "${this.sourceLang}" will be excluded automatically.`));
183
190
  const langInput = await ask(' > ');
184
191
 
185
192
  const targetLangs = this.parseTargetLanguages(langInput);
186
193
 
187
194
  if (targetLangs.length === 0) {
188
- console.log(' No valid target languages selected. Aborting.');
195
+ console.log(' ' + this.tr('translate.targetLanguages.noneSelected', {}, 'No valid target languages selected. Aborting.'));
189
196
  if (this.configuredTargetLangs.length === 0) {
190
- console.log(' Configure defaultLanguages in .i18ntk-config, or enter target codes manually.');
197
+ console.log(' ' + this.tr('translate.targetLanguages.configureHint', {}, 'Configure defaultLanguages in .i18ntk-config, or enter target codes manually.'));
191
198
  }
192
199
  return { success: false, error: 'Invalid language code' };
193
200
  }
194
201
 
195
- console.log(`\n Target languages: ${targetLangs.join(', ')}`);
202
+ console.log('\n ' + this.tr('translate.targetLanguages.selected', { languages: targetLangs.join(', ') }, `Target languages: ${targetLangs.join(', ')}`));
196
203
 
197
- console.log(`\n Which file(s) to translate?`);
204
+ console.log('\n ' + this.tr('translate.files.title', {}, 'Which file(s) to translate?'));
198
205
  const filePreview = jsonFiles.length <= 6
199
206
  ? jsonFiles.join(', ')
200
207
  : `${jsonFiles.slice(0, 6).join(', ')}, ...`;
201
- console.log(` a) All JSON files (${jsonFiles.length}: ${filePreview})`);
208
+ console.log(' ' + this.tr('translate.files.all', { count: jsonFiles.length, files: filePreview }, `a) All JSON files (${jsonFiles.length}: ${filePreview})`));
202
209
  jsonFiles.forEach((f, i) => {
203
210
  console.log(` ${i + 1}) ${f}`);
204
211
  });
205
212
 
206
- const fileChoice = await ask('\n Choice [a/all or file number]: ');
213
+ const fileChoice = await ask('\n ' + this.tr('translate.files.choicePrompt', {}, 'Choice [a/all or file number]: '));
207
214
  let sourceFiles;
208
215
 
209
216
  if (['a', 'all', '*'].includes(fileChoice.trim().toLowerCase())) {
@@ -211,7 +218,7 @@ class TranslateCommand {
211
218
  } else {
212
219
  const idx = parseInt(fileChoice, 10) - 1;
213
220
  if (isNaN(idx) || idx < 0 || idx >= jsonFiles.length) {
214
- console.log(' Invalid choice. Aborting.');
221
+ console.log(' ' + this.tr('translate.files.invalidChoice', {}, 'Invalid choice. Aborting.'));
215
222
  return { success: false, error: 'Invalid file choice' };
216
223
  }
217
224
  sourceFiles = [path.join(this.sourceDir, jsonFiles[idx])];
@@ -220,39 +227,39 @@ class TranslateCommand {
220
227
  if (this.autoTranslateSettings.dryRunFirst !== false) {
221
228
  // Dry-run for first language only (all languages use same source so same keys)
222
229
  const firstLang = targetLangs[0];
223
- console.log(`\n Dry-run preview for "${firstLang}"...\n`);
230
+ console.log('\n ' + this.tr('translate.dryRun.previewFor', { lang: firstLang }, `Dry-run preview for "${firstLang}"...`) + '\n');
224
231
  await this.runTranslate(sourceFiles, firstLang, { dryRun: true });
225
232
  }
226
233
 
227
- console.log('\n Proceed with actual translation?');
228
- const answer = await ask(' [y]es / [n]o: ');
229
- if (!/^y|yes$/i.test(answer.trim())) {
230
- console.log(' Translation cancelled.');
234
+ console.log('\n ' + this.tr('translate.confirm.proceed', {}, 'Proceed with actual translation?'));
235
+ const answer = await ask(' ' + this.tr('translate.confirm.yesNoPrompt', {}, '[y]es / [n]o: '));
236
+ if (!parseConfirmation(answer, { language: this.config.uiLanguage || this.config.language || 'en', defaultValue: false })) {
237
+ console.log(' ' + this.tr('translate.confirm.cancelled', {}, 'Translation cancelled.'));
231
238
  return { success: true, cancelled: true };
232
239
  }
233
240
 
234
241
  let results = [];
235
242
  for (const lang of targetLangs) {
236
- console.log(`\n Translating to "${lang}"...\n`);
243
+ console.log('\n ' + this.tr('translate.run.translatingTo', { lang }, `Translating to "${lang}"...`) + '\n');
237
244
  try {
238
245
  await this.runTranslate(sourceFiles, lang, { dryRun: false });
239
246
  results.push({ lang, ok: true });
240
247
  } catch (e) {
241
- console.error(` Failed for "${lang}": ${e.message}`);
248
+ console.error(' ' + this.tr('translate.run.failedFor', { lang, error: e.message }, `Failed for "${lang}": ${e.message}`));
242
249
  results.push({ lang, ok: false, error: e.message });
243
250
  }
244
251
  }
245
252
 
246
- console.log('\n Summary:');
253
+ console.log('\n ' + this.tr('translate.summary.title', {}, 'Summary:'));
247
254
  for (const r of results) {
248
255
  console.log(` ${r.ok ? '\u{2705}' : '\u{274C}'} ${r.lang}${r.error ? ' (' + r.error + ')' : ''}`);
249
256
  }
250
- console.log('\n Translation complete!');
257
+ console.log('\n ' + this.tr('translate.summary.complete', {}, 'Translation complete!'));
251
258
  return { success: true, results };
252
259
  }
253
260
 
254
261
  async nonInteractiveFlow(jsonFiles) {
255
- console.log('\n Non-interactive mode. Use direct CLI instead:');
262
+ console.log('\n ' + this.tr('translate.nonInteractive.useDirect', {}, 'Non-interactive mode. Use direct CLI instead:'));
256
263
  console.log(' i18ntk-translate <source> <lang> [options]');
257
264
  return { success: false, error: 'Non-interactive mode not supported from menu' };
258
265
  }
@@ -278,8 +285,8 @@ class TranslateCommand {
278
285
  const languageJsonFiles = this.getJsonFiles(languageDir);
279
286
  if (languageJsonFiles.length > 0) {
280
287
  if (shouldLog) {
281
- console.log(` No JSON files found directly in: ${selectedDir}`);
282
- console.log(` Using source language folder: ${languageDir}`);
288
+ console.log(' ' + this.tr('translate.sourceDirectory.noJsonDirect', { dir: selectedDir }, `No JSON files found directly in: ${selectedDir}`));
289
+ console.log(' ' + this.tr('translate.sourceDirectory.usingLanguageFolder', { dir: languageDir }, `Using source language folder: ${languageDir}`));
283
290
  }
284
291
  return { ok: true, sourceDir: languageDir, jsonFiles: languageJsonFiles };
285
292
  }
@@ -288,8 +295,8 @@ class TranslateCommand {
288
295
 
289
296
  const checkedLanguageDir = cleanLang ? path.join(selectedDir, cleanLang) : null;
290
297
  const message = checkedLanguageDir
291
- ? `No JSON files found in: ${selectedDir}\nAlso checked source language folder: ${checkedLanguageDir}`
292
- : `No JSON files found in: ${selectedDir}`;
298
+ ? this.tr('translate.sourceDirectory.noJsonWithLanguageFolder', { dir: selectedDir, languageDir: checkedLanguageDir }, `No JSON files found in: ${selectedDir}\nAlso checked source language folder: ${checkedLanguageDir}`)
299
+ : this.tr('translate.sourceDirectory.noJson', { dir: selectedDir }, `No JSON files found in: ${selectedDir}`);
293
300
  return { ok: false, sourceDir: selectedDir, jsonFiles: [], message };
294
301
  }
295
302
 
@@ -343,13 +350,14 @@ class TranslateCommand {
343
350
  placeholderMode: ['preserve', 'skip', 'send'].includes(settings.placeholderMode)
344
351
  ? settings.placeholderMode
345
352
  : 'preserve',
346
- concurrency: this.toInt(settings.concurrency, 6, 1, 25),
353
+ concurrency: this.toInt(settings.concurrency, DEFAULT_CONCURRENCY, 1, getProviderConcurrencyLimit(settings.provider || 'google')),
347
354
  batchSize: this.toInt(settings.batchSize, 100, 1, 10000),
348
355
  progressInterval: this.toInt(settings.progressInterval, 25, 1, 10000),
349
356
  retryCount: this.toInt(settings.retryCount, 3, 0, 10),
350
357
  retryDelay: this.toInt(settings.retryDelay, 1000, 0, 30000),
351
358
  timeout: this.toInt(settings.timeout, 15000, 1000, 120000),
352
359
  dryRunFirst: settings.dryRunFirst !== false,
360
+ onlyMissingOrEnglish: settings.onlyMissingOrEnglish !== false,
353
361
  reportStdout: settings.reportStdout !== false,
354
362
  bom: settings.bom === true,
355
363
  protectionEnabled: settings.protectionEnabled !== false,
@@ -480,6 +488,7 @@ class TranslateCommand {
480
488
  args.noConfirm = true;
481
489
  args.sourceLang = this.sourceLang || 'en';
482
490
  args.dryRun = opts.dryRun === true;
491
+ args.onlyMissingOrEnglish = settings.onlyMissingOrEnglish;
483
492
  args.reportStdout = settings.reportStdout;
484
493
  args.bom = settings.bom;
485
494
  args.concurrency = settings.concurrency;
@@ -37,8 +37,11 @@ class ValidateCommand {
37
37
  this.warnings = [];
38
38
  this.rl = null;
39
39
  this.sourceDir = null;
40
- this.i18nDir = null;
41
- this.sourceLanguageDir = null;
40
+ this.i18nDir = null;
41
+ this.sourceLanguageDir = null;
42
+ this.initialized = false;
43
+ this.pathsDisplayed = false;
44
+ this.validationBannerDisplayed = false;
42
45
  }
43
46
 
44
47
  /**
@@ -53,8 +56,12 @@ class ValidateCommand {
53
56
  /**
54
57
  * Initialize the validator with configuration
55
58
  */
56
- async initialize() {
57
- try {
59
+ async initialize() {
60
+ try {
61
+ if (this.initialized) {
62
+ return;
63
+ }
64
+
58
65
  // Initialize i18n with UI language first
59
66
  const args = this.parseArgs();
60
67
  if (args.help) {
@@ -101,7 +108,9 @@ class ValidateCommand {
101
108
  }
102
109
  }
103
110
 
104
- displayPaths({ sourceDir: this.sourceDir, i18nDir: this.i18nDir, outputDir: this.config.outputDir });
111
+ displayPaths({ sourceDir: this.sourceDir, i18nDir: this.i18nDir, outputDir: this.config.outputDir });
112
+ this.pathsDisplayed = true;
113
+ this.initialized = true;
105
114
 
106
115
  SecurityUtils.logSecurityEvent(
107
116
  'I18n validator initialized successfully',
@@ -673,9 +682,12 @@ class ValidateCommand {
673
682
  const args = this.parseArgs();
674
683
  const jsonOutput = new JsonOutput('validate');
675
684
 
676
- if (!args.json) {
677
- console.log(t('validate.title'));
678
- console.log(t('validate.message'));
685
+ if (!args.json) {
686
+ console.log('');
687
+ console.log(t('validate.title'));
688
+ if (!this.validationBannerDisplayed) {
689
+ console.log(t('validate.message'));
690
+ }
679
691
 
680
692
  // Delete old validation report if it exists
681
693
  const reportPath = path.join(process.cwd(), 'validation-report.txt');
@@ -705,11 +717,14 @@ class ValidateCommand {
705
717
  this.config.strictMode = true;
706
718
  }
707
719
 
708
- if (!args.json) {
709
- console.log(t('validate.sourceDirectory', { dir: this.sourceDir }));
710
- console.log(t('validate.sourceLanguage', { sourceLanguage: this.config.sourceLanguage }));
711
- console.log(t('validate.strictMode', { mode: this.config.strictMode ? 'ON' : 'OFF' }));
712
- }
720
+ if (!args.json) {
721
+ if (!this.pathsDisplayed) {
722
+ console.log(t('validate.sourceDirectory', { dir: this.sourceDir }));
723
+ }
724
+ console.log(t('validate.sourceLanguage', { sourceLanguage: this.config.sourceLanguage }));
725
+ console.log(t('validate.strictMode', { mode: this.config.strictMode ? 'ON' : 'OFF' }));
726
+ console.log('');
727
+ }
713
728
 
714
729
  // Validate source language directory exists
715
730
  SecurityUtils.validatePath(this.sourceLanguageDir);
@@ -933,17 +948,9 @@ class ValidateCommand {
933
948
  this.config = {};
934
949
  }
935
950
 
936
- // Initialize configuration properly when called from menu
937
- if (fromMenu && !this.sourceDir) {
938
- const baseConfig = await getUnifiedConfig('validate', args);
939
- this.config = { ...baseConfig, ...this.config };
940
-
941
- const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
942
- loadTranslations(uiLanguage, path.resolve(__dirname, '../../../resources', 'i18n', 'ui-locales'));this.sourceDir = this.config.sourceDir;
943
- this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
944
- } else {
945
- await this.initialize();
946
- }
951
+ if (!this.initialized) {
952
+ await this.initialize();
953
+ }
947
954
 
948
955
  // Skip admin authentication when called from menu
949
956
  if (!fromMenu) {
@@ -972,8 +979,9 @@ class ValidateCommand {
972
979
  }
973
980
  const execute = async () => {
974
981
 
975
- console.log(t('validate.startingValidationProcess'));
976
- SecurityUtils.logSecurityEvent(
982
+ console.log('\n' + t('validate.startingValidationProcess'));
983
+ this.validationBannerDisplayed = true;
984
+ SecurityUtils.logSecurityEvent(
977
985
  t('validate.runStarted'),
978
986
  'info',
979
987
  { message: 'Starting validation run' }