i18ntk 1.10.2 → 2.0.2

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 (108) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +141 -1191
  3. package/main/i18ntk-analyze.js +65 -84
  4. package/main/i18ntk-backup-class.js +420 -0
  5. package/main/i18ntk-backup.js +3 -3
  6. package/main/i18ntk-complete.js +90 -65
  7. package/main/i18ntk-doctor.js +123 -103
  8. package/main/i18ntk-fixer.js +61 -725
  9. package/main/i18ntk-go.js +14 -15
  10. package/main/i18ntk-init.js +77 -26
  11. package/main/i18ntk-java.js +27 -32
  12. package/main/i18ntk-js.js +70 -68
  13. package/main/i18ntk-manage.js +129 -30
  14. package/main/i18ntk-php.js +75 -75
  15. package/main/i18ntk-py.js +55 -56
  16. package/main/i18ntk-scanner.js +59 -57
  17. package/main/i18ntk-setup.js +9 -404
  18. package/main/i18ntk-sizing.js +6 -6
  19. package/main/i18ntk-summary.js +21 -18
  20. package/main/i18ntk-ui.js +11 -10
  21. package/main/i18ntk-usage.js +54 -18
  22. package/main/i18ntk-validate.js +13 -13
  23. package/main/manage/commands/AnalyzeCommand.js +1124 -0
  24. package/main/manage/commands/BackupCommand.js +62 -0
  25. package/main/manage/commands/CommandRouter.js +295 -0
  26. package/main/manage/commands/CompleteCommand.js +61 -0
  27. package/main/manage/commands/DoctorCommand.js +60 -0
  28. package/main/manage/commands/FixerCommand.js +624 -0
  29. package/main/manage/commands/InitCommand.js +62 -0
  30. package/main/manage/commands/ScannerCommand.js +654 -0
  31. package/main/manage/commands/SizingCommand.js +60 -0
  32. package/main/manage/commands/SummaryCommand.js +61 -0
  33. package/main/manage/commands/UsageCommand.js +60 -0
  34. package/main/manage/commands/ValidateCommand.js +978 -0
  35. package/main/manage/index-fixed.js +1447 -0
  36. package/main/manage/index.js +1462 -0
  37. package/main/manage/managers/DebugMenu.js +140 -0
  38. package/main/manage/managers/InteractiveMenu.js +177 -0
  39. package/main/manage/managers/LanguageMenu.js +62 -0
  40. package/main/manage/managers/SettingsMenu.js +53 -0
  41. package/main/manage/services/AuthenticationService.js +263 -0
  42. package/main/manage/services/ConfigurationService-fixed.js +449 -0
  43. package/main/manage/services/ConfigurationService.js +449 -0
  44. package/main/manage/services/FileManagementService.js +368 -0
  45. package/main/manage/services/FrameworkDetectionService.js +458 -0
  46. package/main/manage/services/InitService.js +1051 -0
  47. package/main/manage/services/SetupService.js +462 -0
  48. package/main/manage/services/SummaryService.js +450 -0
  49. package/main/manage/services/UsageService.js +1502 -0
  50. package/package.json +32 -29
  51. package/runtime/enhanced.d.ts +221 -221
  52. package/runtime/index.d.ts +29 -29
  53. package/runtime/index.full.d.ts +331 -331
  54. package/runtime/index.js +7 -6
  55. package/scripts/build-lite.js +17 -17
  56. package/scripts/deprecate-versions.js +23 -6
  57. package/scripts/export-translations.js +5 -5
  58. package/scripts/fix-all-i18n.js +3 -3
  59. package/scripts/fix-and-purify-i18n.js +3 -2
  60. package/scripts/fix-locale-control-chars.js +110 -0
  61. package/scripts/lint-locales.js +80 -0
  62. package/scripts/locale-optimizer.js +8 -8
  63. package/scripts/prepublish.js +21 -21
  64. package/scripts/security-check.js +117 -117
  65. package/scripts/sync-translations.js +4 -4
  66. package/scripts/sync-ui-locales.js +9 -8
  67. package/scripts/validate-all-translations.js +8 -7
  68. package/scripts/verify-deprecations.js +157 -161
  69. package/scripts/verify-translations.js +6 -5
  70. package/settings/i18ntk-config.json +282 -282
  71. package/settings/language-config.json +5 -5
  72. package/settings/settings-cli.js +9 -9
  73. package/settings/settings-manager.js +18 -18
  74. package/ui-locales/de.json +2417 -2348
  75. package/ui-locales/en.json +2415 -2352
  76. package/ui-locales/es.json +2425 -2353
  77. package/ui-locales/fr.json +2418 -2348
  78. package/ui-locales/ja.json +2463 -2361
  79. package/ui-locales/ru.json +2463 -2359
  80. package/ui-locales/zh.json +2418 -2351
  81. package/utils/admin-auth.js +2 -2
  82. package/utils/admin-cli.js +297 -297
  83. package/utils/admin-pin.js +9 -9
  84. package/utils/cli-helper.js +9 -9
  85. package/utils/config-helper.js +73 -104
  86. package/utils/config-manager.js +204 -171
  87. package/utils/config.js +5 -4
  88. package/utils/env-manager.js +249 -263
  89. package/utils/framework-detector.js +27 -24
  90. package/utils/i18n-helper.js +85 -41
  91. package/utils/init-helper.js +152 -94
  92. package/utils/json-output.js +98 -98
  93. package/utils/mini-commander.js +179 -0
  94. package/utils/missing-key-validator.js +5 -5
  95. package/utils/plugin-loader.js +40 -29
  96. package/utils/prompt.js +14 -44
  97. package/utils/safe-json.js +40 -0
  98. package/utils/secure-errors.js +3 -3
  99. package/utils/security-check-improved.js +390 -0
  100. package/utils/security-config.js +5 -5
  101. package/utils/security-fixed.js +607 -0
  102. package/utils/security.js +652 -602
  103. package/utils/setup-enforcer.js +136 -44
  104. package/utils/setup-validator.js +33 -32
  105. package/utils/ultra-performance-optimizer.js +11 -9
  106. package/utils/watch-locales.js +2 -1
  107. package/utils/prompt-fixed.js +0 -55
  108. package/utils/security-check.js +0 -454
@@ -0,0 +1,1462 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * I18NTK MANAGEMENT TOOLKIT - MAIN MANAGER
4
+ *
5
+ * This is the main entry point for all i18n operations.
6
+ * It provides an interactive interface to manage translations.
7
+ *
8
+ * Usage:
9
+ * npm run i18ntk:manage
10
+ * npm run i18ntk:manage -- --command=init
11
+ * npm run i18ntk:manage -- --command=analyze
12
+ * npm run i18ntk:manage -- --command=validate
13
+ * npm run i18ntk:manage -- --help
14
+ */
15
+
16
+ const path = require('path');
17
+ const AdminAuth = require('../../utils/admin-auth');
18
+ const SecurityUtils = require('../../utils/security');
19
+ const configManager = require('../../settings/settings-manager');
20
+ const { showFrameworkWarningOnce } = require('../../utils/cli-helper');
21
+ const { createPrompt, isInteractive } = require('../../utils/prompt-helper');
22
+ const { loadTranslations, t, refreshLanguageFromSettings} = require('../../utils/i18n-helper');
23
+ const cliHelper = require('../../utils/cli-helper');
24
+ const { loadConfig, saveConfig, ensureConfigDefaults } = require('../../utils/config');
25
+ const pkg = require('../../package.json');
26
+ const SetupEnforcer = require('../../utils/setup-enforcer');
27
+ const CommandRouter = require('./commands/CommandRouter');
28
+
29
+ // Import services to replace circular dependencies
30
+ const ConfigurationService = require('./services/ConfigurationService');
31
+ const SummaryService = require('./services/SummaryService');
32
+
33
+ // Preload translations early to avoid missing key warnings
34
+ loadTranslations();
35
+
36
+ class I18nManager {
37
+ constructor(config = {}) {
38
+ this.config = config;
39
+ this.rl = null;
40
+ this.isReadlineClosed = false;
41
+ this.isAuthenticated = false;
42
+ this.ui = null;
43
+ this.adminAuth = new AdminAuth();
44
+ this.commandRouter = null;
45
+
46
+ // Initialize services
47
+ this.configurationService = new ConfigurationService(config);
48
+ this.summaryService = new SummaryService(config);
49
+
50
+ // No longer create readline interface here - use CLI helpers
51
+ }
52
+
53
+ initializeReadline() {
54
+ // Use centralized CLI helper instead of direct readline
55
+ this.rl = null;
56
+ this.isReadlineClosed = false;
57
+ }
58
+
59
+ // Initialize configuration using unified system
60
+ async initialize(options = {}) {
61
+ try {
62
+ // Parse args here for other initialization needs (but language is already loaded)
63
+ const args = this.parseArgs();
64
+ const interactive = options.interactive ?? isInteractive({ noPrompt: options.noPrompt || args.noPrompt });
65
+ if (args.help) {
66
+ this.showHelp();
67
+ process.exit(0);
68
+ }
69
+
70
+ // Load translations for the UI language
71
+ const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
72
+ const uiLanguage = args.uiLanguage || settings.uiLanguage || settings.language || this.config.uiLanguage || 'en';
73
+ loadTranslations(uiLanguage);
74
+
75
+ // Validate source directory exists
76
+ const {validateSourceDir, displayPaths} = require('../../utils/config-helper');
77
+ try {
78
+ validateSourceDir(this.config.sourceDir, 'i18ntk-manage');
79
+ } catch (err) {
80
+ console.log(t('init.requiredTitle'));
81
+ console.log(t('init.requiredBody'));
82
+ if (interactive) {
83
+ const answer = await cliHelper.prompt(t('init.promptRunNow'));
84
+ if (answer.trim().toLowerCase() === 'y') {
85
+ // Note: Initialization should be handled by the calling code
86
+ // to avoid circular dependencies
87
+ console.log('Please run initialization manually or use the init command');
88
+ } else {
89
+ throw err;
90
+ }
91
+ } else {
92
+ // Non-interactive: surface the original error to avoid hanging
93
+ throw err;
94
+ }
95
+ }
96
+
97
+ } catch (error) {
98
+ throw error;
99
+ }
100
+ }
101
+
102
+ // Auto-detect i18n directory from common locations only if not configured in settings
103
+ detectI18nDirectory() {
104
+ const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
105
+ const projectRoot = path.resolve(settings.projectRoot || this.config.projectRoot || '.');
106
+
107
+ // Use per-script directory configuration if available, fallback to global sourceDir
108
+ const sourceDir = settings.scriptDirectories?.manage || settings.sourceDir;
109
+
110
+ if (sourceDir) {
111
+ this.config.sourceDir = path.resolve(projectRoot, sourceDir);
112
+ return;
113
+ }
114
+
115
+ // Define possible i18n paths for auto-detection
116
+ const possibleI18nPaths = [
117
+ './locales',
118
+ './src/locales',
119
+ './src/i18n',
120
+ './src/i18n/locales',
121
+ './app/locales',
122
+ './app/i18n',
123
+ './public/locales',
124
+ './assets/locales',
125
+ './translations',
126
+ './lang'
127
+ ];
128
+
129
+ // Only auto-detect if no settings are configured
130
+ for (const possiblePath of possibleI18nPaths) {
131
+ const resolvedPath = path.resolve(projectRoot, possiblePath);
132
+ if (SecurityUtils.safeExistsSync(resolvedPath)) {
133
+ // Check if it contains language directories
134
+ try {
135
+ const items = require('fs').readdirSync(resolvedPath);
136
+ const hasLanguageDirs = items.some(item => {
137
+ const itemPath = path.join(resolvedPath, item);
138
+ return require('fs').statSync(itemPath).isDirectory() &&
139
+ ['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(item);
140
+ });
141
+
142
+ if (hasLanguageDirs) {
143
+ this.config.sourceDir = possiblePath;
144
+ t('init.autoDetectedI18nDirectory', { path: possiblePath });
145
+ break;
146
+ }
147
+ } catch (error) {
148
+ // Continue checking other paths
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ // Check if i18n framework is installed - configuration-based check without prompts
155
+ async checkI18nDependencies() {
156
+ const packageJsonPath = path.resolve('./package.json');
157
+
158
+ if (!SecurityUtils.safeExistsSync(packageJsonPath)) {
159
+ console.log(this.ui ? this.ui.t('errors.noPackageJson') : 'No package.json found');
160
+ return false; // Treat as no framework detected
161
+ }
162
+
163
+ try {
164
+ const packageJson = JSON.parse(SecurityUtils.safeReadFileSync(packageJsonPath, path.dirname(packageJsonPath), 'utf8'));
165
+ // Include peerDependencies in the check
166
+ const dependencies = {
167
+ ...packageJson.dependencies,
168
+ ...packageJson.devDependencies,
169
+ ...packageJson.peerDependencies
170
+ };
171
+
172
+ const i18nFrameworks = [
173
+ 'react-i18next',
174
+ 'vue-i18n',
175
+ 'angular-i18n',
176
+ 'i18next',
177
+ 'next-i18next',
178
+ 'svelte-i18n',
179
+ '@nuxtjs/i18n',
180
+ 'i18ntk-runtime'
181
+ ];
182
+
183
+ const installedFrameworks = i18nFrameworks.filter(framework => dependencies[framework]);
184
+
185
+ if (installedFrameworks.length > 0) {
186
+ if (this.ui && this.ui.t) {
187
+ console.log(this.ui.t('init.detectedFrameworks', { frameworks: installedFrameworks.join(', ') }));
188
+ } else {
189
+ console.log(`Detected frameworks: ${installedFrameworks.join(', ')}`);
190
+ }
191
+ const cfg = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
192
+ cfg.framework = cfg.framework || {};
193
+ cfg.framework.detected = true;
194
+ cfg.framework.installed = installedFrameworks;
195
+ if (configManager.saveSettings) {
196
+ configManager.saveSettings(cfg);
197
+ } else if (configManager.saveConfig) {
198
+ configManager.saveConfig(cfg);
199
+ }
200
+ return true;
201
+ } else {
202
+ const cfg = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
203
+ if (cfg.framework) {
204
+ cfg.framework.detected = false;
205
+ if (configManager.saveSettings) {
206
+ configManager.saveSettings(cfg);
207
+ } else if (configManager.saveConfig) {
208
+ configManager.saveConfig(cfg);
209
+ }
210
+ }
211
+ // Always signal that frameworks were not detected
212
+ return false;
213
+ }
214
+ } catch (error) {
215
+ console.log(t('init.errors.packageJsonRead'));
216
+ return false; // Treat as no framework detected on error
217
+ }
218
+ }
219
+
220
+ // Parse command line arguments
221
+ parseArgs() {
222
+ const args = process.argv.slice(2);
223
+ const parsed = {};
224
+
225
+ args.forEach(arg => {
226
+ if (arg.startsWith('--')) {
227
+ const [key, value] = arg.substring(2).split('=');
228
+ const sanitizedKey = key?.trim();
229
+ const sanitizedValue = value !== undefined ? value.trim() : true;
230
+
231
+ switch (sanitizedKey) {
232
+ case 'source-dir':
233
+ parsed.sourceDir = sanitizedValue;
234
+ break;
235
+ case 'i18n-dir':
236
+ parsed.i18nDir = sanitizedValue;
237
+ break;
238
+ case 'output-dir':
239
+ parsed.outputDir = sanitizedValue;
240
+ break;
241
+ case 'source-language':
242
+ parsed.sourceLanguage = sanitizedValue;
243
+ break;
244
+ case 'ui-language':
245
+ parsed.uiLanguage = sanitizedValue;
246
+ break;
247
+ case 'no-prompt':
248
+ parsed.noPrompt = true;
249
+ break;
250
+ case 'admin-pin':
251
+ parsed.adminPin = sanitizedValue || '';
252
+ break;
253
+ case 'help':
254
+ case 'h':
255
+ parsed.help = true;
256
+ break;
257
+ default:
258
+ // Handle language shorthand flags like --de, --fr
259
+ if (['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(sanitizedKey)) {
260
+ parsed.uiLanguage = sanitizedKey;
261
+ }
262
+ break;
263
+ }
264
+ }
265
+ });
266
+
267
+ return parsed;
268
+ }
269
+
270
+ // Add this run method after the checkI18nDependencies method
271
+ async run() {
272
+ // Parse args early so we can short-circuit help/version flows before setup
273
+ const args = this.parseArgs();
274
+ const rawArgs = process.argv.slice(2);
275
+ const directCommands = [
276
+ 'init', 'analyze', 'validate', 'usage', 'scanner', 'sizing', 'complete', 'fix', 'summary', 'debug', 'workflow'
277
+ ];
278
+ const commandFlagArg = rawArgs.find(arg => arg.startsWith('--command='));
279
+ const requestedCommand = commandFlagArg
280
+ ? commandFlagArg.split('=')[1]
281
+ : (rawArgs.length > 0 && directCommands.includes(rawArgs[0]) ? rawArgs[0] : null);
282
+ const shouldSkipInitCheck = requestedCommand === 'init';
283
+
284
+ // Show help immediately without any setup/auth (useful for CI/uninitialized projects)
285
+ if (args.help) {
286
+ this.showHelp();
287
+ return;
288
+ }
289
+
290
+ let startupTimeout = setTimeout(() => {
291
+ console.error('❌ CLI startup timeout - something is hanging');
292
+ process.exit(1);
293
+ }, 10000); // 10 second timeout
294
+
295
+ const clearStartupTimeout = () => {
296
+ if (startupTimeout) {
297
+ clearTimeout(startupTimeout);
298
+ startupTimeout = null;
299
+ }
300
+ };
301
+
302
+ let prompt;
303
+ try {
304
+ // Allow `init` to run even when setup is not complete.
305
+ if (!shouldSkipInitCheck) {
306
+ await SetupEnforcer.checkSetupCompleteAsync();
307
+ }
308
+ clearStartupTimeout();
309
+
310
+ prompt = createPrompt({ noPrompt: args.noPrompt || Boolean(args.adminPin) });
311
+ const interactive = isInteractive({ noPrompt: args.noPrompt || Boolean(args.adminPin) });
312
+
313
+ // Load settings and UI language
314
+ const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
315
+ const uiLanguage = args.uiLanguage || settings.uiLanguage || settings.language || this.config.uiLanguage || 'en';
316
+ loadTranslations(uiLanguage);
317
+
318
+ // Initialize CommandRouter
319
+ this.commandRouter = new CommandRouter(this.config, this.ui, this.adminAuth);
320
+
321
+ if (args.adminPin) {
322
+ this.adminAuth.verifyPin = async () => true;
323
+ this.prompt = async () => '';
324
+ }
325
+
326
+ if (args.help) {
327
+ this.showHelp();
328
+ return;
329
+ }
330
+
331
+ let cfgAfterInitCheck = {};
332
+ if (interactive && !shouldSkipInitCheck) {
333
+ cfgAfterInitCheck = await this.ensureInitializedOrExit(prompt);
334
+ const frameworksDetected = await this.checkI18nDependencies();
335
+ if (!frameworksDetected) {
336
+ await this.maybePromptFramework(prompt, cfgAfterInitCheck, pkg.version);
337
+ }
338
+ }
339
+
340
+ this.config = { ...this.config, ...cfgAfterInitCheck };
341
+ await this.initialize({ interactive, noPrompt: args.noPrompt || Boolean(args.adminPin) });
342
+ clearStartupTimeout();
343
+ let commandToExecute = requestedCommand;
344
+
345
+ // Handle help immediately without dependency checks
346
+ if (args.help) {
347
+ this.showHelp();
348
+ this.safeClose();
349
+ process.exit(0);
350
+ }
351
+
352
+ // Handle debug flag
353
+ if (args.debug) {
354
+ // Enable debug mode for this session
355
+ const { blue } = require('../../utils/colors-new');
356
+ console.log(blue('Debug mode enabled'));
357
+ }
358
+
359
+ if (commandToExecute) {
360
+ await this.executeCommand(commandToExecute, args);
361
+ this.safeClose();
362
+ return;
363
+ }
364
+
365
+ // If no command provided and --no-prompt is set, exit gracefully
366
+ if (args.noPrompt) {
367
+ this.safeClose();
368
+ process.exit(0);
369
+ }
370
+
371
+ // Framework detection is now handled by maybePromptFramework above
372
+ // Skip the redundant checkI18nDependencies prompt
373
+
374
+ // Interactive mode - showInteractiveMenu will handle the title
375
+ await this.showInteractiveMenu();
376
+
377
+ } catch (error) {
378
+ if (this.ui && this.ui.t) {
379
+ console.error(t('common.genericError', { error: error.message }));
380
+ } else {
381
+ console.error(`Error: ${error.message}`);
382
+ }
383
+ process.exit(1);
384
+ } finally {
385
+ clearStartupTimeout();
386
+ if (prompt && typeof prompt.close === 'function') {
387
+ prompt.close();
388
+ }
389
+ this.safeClose();
390
+ }
391
+ }
392
+
393
+ showHelp() {
394
+ const localT = this.ui && this.ui.t ? this.ui.t.bind(this.ui) : (key) => {
395
+ // Fallback help text when UI is not initialized
396
+ const helpTexts = {
397
+ 'help.usage': 'Usage: npm run i18ntk [command] [options]',
398
+ 'help.interactiveMode': 'Run without arguments for interactive mode',
399
+ 'help.initProject': ' init - Initialize i18n project structure',
400
+ 'help.analyzeTranslations': ' analyze - Analyze translation files',
401
+ 'help.validateTranslations': ' validate - Validate translations for errors',
402
+ 'help.checkUsage': ' usage - Check translation usage in code',
403
+ 'help.showHelp': ' help - Show this help message',
404
+ 'help.availableCommands': '\nAvailable commands:',
405
+ 'help.initCommand': ' --command=init Initialize i18n project',
406
+ 'help.analyzeCommand': ' --command=analyze Analyze translations',
407
+ 'help.validateCommand': ' --command=validate Validate translations',
408
+ 'help.usageCommand': ' --command=usage Check translation usage',
409
+ 'help.sizingCommand': ' --command=sizing Analyze translation sizing',
410
+ 'help.completeCommand': ' --command=complete Run complete analysis',
411
+ 'help.summaryCommand': ' --command=summary Generate summary report',
412
+ 'help.debugCommand': ' --command=debug Run debug utilities',
413
+ 'help.scannerCommand': ' --command=scanner Scan for translation keys'
414
+ };
415
+ return helpTexts[key] || key;
416
+ };
417
+
418
+ console.log(t('help.usage'));
419
+ console.log(t('help.interactiveMode'));
420
+ console.log(t('help.initProject'));
421
+ console.log(t('help.analyzeTranslations'));
422
+ console.log(t('help.validateTranslations'));
423
+ console.log(t('help.checkUsage'));
424
+ console.log(t('help.showHelp'));
425
+ console.log(t('help.availableCommands'));
426
+ console.log(t('help.initCommand'));
427
+ console.log(t('help.analyzeCommand'));
428
+ console.log(t('help.validateCommand'));
429
+ console.log(t('help.usageCommand'));
430
+ console.log(t('help.sizingCommand'));
431
+ console.log(t('help.completeCommand'));
432
+ console.log(t('help.summaryCommand'));
433
+ console.log(t('help.debugCommand'));
434
+ console.log(t('help.scannerCommand'));
435
+
436
+ // Ensure proper exit for direct command execution
437
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
438
+ this.safeClose();
439
+ process.exit(0);
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Execute a command using the new CommandRouter system
445
+ */
446
+ async executeCommand(command, options = {}) {
447
+ if (!this.commandRouter) {
448
+ throw new Error('CommandRouter not initialized');
449
+ }
450
+
451
+ // Set runtime dependencies for the command router
452
+ this.commandRouter.setRuntimeDependencies(
453
+ this.prompt.bind(this),
454
+ this.isNonInteractiveMode(),
455
+ this.safeClose.bind(this)
456
+ );
457
+
458
+ return await this.commandRouter.executeCommand(command, options);
459
+ }
460
+
461
+ // ... existing code for ensureInitializedOrExit, maybePromptFramework, showInteractiveMenu, etc. ...
462
+
463
+ async ensureInitializedOrExit(prompt) {
464
+ const { checkInitialized } = require('../../utils/init-helper');
465
+ const cliHelper = require('../../utils/cli-helper');
466
+ const pkg = require('../../package.json');
467
+
468
+ const { initialized, config } = await checkInitialized();
469
+
470
+ if (!initialized) {
471
+ console.log('\nThis project is not yet initialized with i18ntk.');
472
+ const shouldInitialize = await cliHelper.confirm('Would you like to initialize it now?');
473
+
474
+ if (!shouldInitialize) {
475
+ console.log('Exiting. Please initialize the project first.');
476
+ process.exit(1);
477
+ }
478
+
479
+ // The initialization will be handled by the init command
480
+ return config;
481
+ }
482
+
483
+ // Check if we need to prompt for framework detection
484
+ const settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
485
+
486
+ // Ensure framework configuration exists with all required fields
487
+ if (!settings.framework) {
488
+ settings.framework = {
489
+ detected: false,
490
+ preference: null,
491
+ prompt: 'always',
492
+ lastPromptedVersion: null,
493
+ installed: [],
494
+ version: '1.0' // Schema version for future compatibility
495
+ };
496
+ }
497
+
498
+ // Check if we need to prompt for framework detection
499
+ if (!settings.framework.detected &&
500
+ settings.framework.prompt !== 'suppress' &&
501
+ settings.framework.lastPromptedVersion !== pkg.version) {
502
+
503
+ console.log('\nWe noticed you haven\'t set up an i18n framework yet.');
504
+ console.log('Would you like to detect your i18n framework automatically?');
505
+
506
+ console.log('1. Detect automatically');
507
+ console.log('2. I\'ll set it up manually');
508
+ console.log('3. Don\'t show this again');
509
+
510
+ const answer = await prompt.question('\nSelect an option (1-3): ');
511
+ const choice = answer.trim();
512
+
513
+ let action;
514
+ if (choice === '1') action = 'detect';
515
+ else if (choice === '2') action = 'manual';
516
+ else if (choice === '3') action = 'dont-show';
517
+ else action = 'manual'; // default fallback
518
+
519
+ if (action === 'dont-show') {
520
+ // Update settings to suppress future prompts for this version
521
+ settings.framework.prompt = 'suppress';
522
+ settings.framework.lastPromptedVersion = pkg.version;
523
+
524
+ if (configManager.saveSettings) {
525
+ await configManager.saveSettings(settings);
526
+ } else if (configManager.saveConfig) {
527
+ await configManager.saveConfig(settings);
528
+ }
529
+
530
+ console.log('Framework detection prompt will be suppressed for this version.');
531
+ } else if (action === 'detect') {
532
+ // Run framework detection
533
+ const { detectedLanguage, detectedFramework } = await this.detectEnvironmentAndFramework();
534
+
535
+ if (detectedFramework && detectedFramework !== 'generic') {
536
+ console.log(`\nDetected framework: ${detectedFramework}`);
537
+
538
+ // Update settings with detected framework
539
+ settings.framework.detected = true;
540
+ settings.framework.preference = detectedFramework;
541
+ settings.framework.lastDetected = new Date().toISOString();
542
+
543
+ if (configManager.saveSettings) {
544
+ await configManager.saveSettings(settings);
545
+ } else if (configManager.saveConfig) {
546
+ await configManager.saveConfig(settings);
547
+ }
548
+
549
+ console.log(`Framework set to: ${detectedFramework}`);
550
+ } else {
551
+ console.log('\nCould not detect a specific i18n framework.');
552
+ console.log('Please set up your i18n framework manually.');
553
+ }
554
+ }
555
+ }
556
+
557
+ return { ...config, ...settings };
558
+ }
559
+
560
+ async detectEnvironmentAndFramework() {
561
+ // Defensive check to ensure SecurityUtils is available
562
+ if (!SecurityUtils) {
563
+ throw new Error('SecurityUtils is not available. This may indicate a module loading issue.');
564
+ }
565
+ const fs = require('fs');
566
+ const path = require('path');
567
+
568
+ const packageJsonPath = path.join(process.cwd(), 'package.json');
569
+ const pyprojectPath = path.join(process.cwd(), 'pyproject.toml');
570
+ const requirementsPath = path.join(process.cwd(), 'requirements.txt');
571
+ const goModPath = path.join(process.cwd(), 'go.mod');
572
+ const pomPath = path.join(process.cwd(), 'pom.xml');
573
+ const composerPath = path.join(process.cwd(), 'composer.json');
574
+
575
+ let detectedLanguage = 'generic';
576
+ let detectedFramework = 'generic';
577
+
578
+ if (SecurityUtils.safeExistsSync(packageJsonPath)) {
579
+ detectedLanguage = 'javascript';
580
+ try {
581
+ const packageJson = JSON.parse(SecurityUtils.safeReadFileSync(packageJsonPath, path.dirname(packageJsonPath), 'utf8'));
582
+ const deps = {
583
+ ...(packageJson.dependencies || {}),
584
+ ...(packageJson.devDependencies || {}),
585
+ ...(packageJson.peerDependencies || {})
586
+ };
587
+
588
+ // Check for i18ntk-runtime first (check both package names)
589
+ const hasI18nTkRuntime = deps['i18ntk-runtime'] || deps['i18ntk/runtime'];
590
+
591
+ // Check for common i18n patterns in source code if not found in package.json
592
+ if (!hasI18nTkRuntime) {
593
+ const i18nPatterns = [
594
+ /i18n\.t\(['\"`]/,
595
+ /useI18n\(/,
596
+ /from ['\"]i18ntk[\/\\]runtime['\"]/,
597
+ /require\(['\"]i18ntk[\/\\]runtime['\"]\)/
598
+ ];
599
+
600
+ const sourceFiles = await this.customGlob(['src/**/*.{js,jsx,ts,tsx}'], {
601
+ cwd: process.cwd(),
602
+ ignore: ['**/node_modules/**', '**/dist/**', '**/build/**']
603
+ });
604
+
605
+ for (const file of sourceFiles) {
606
+ try {
607
+ const content = await fs.promises.readFile(path.join(process.cwd(), file), 'utf8');
608
+ if (i18nPatterns.some(pattern => pattern.test(content))) {
609
+ detectedFramework = 'i18ntk-runtime';
610
+ break;
611
+ }
612
+ } catch (e) {
613
+ // Skip files we can't read
614
+ continue;
615
+ }
616
+ }
617
+ } else {
618
+ detectedFramework = 'i18ntk-runtime';
619
+ }
620
+
621
+ // Only check other frameworks if i18ntk-runtime wasn't detected
622
+ if (detectedFramework !== 'i18ntk-runtime') {
623
+ if (deps.react || deps['react-dom']) detectedFramework = 'react';
624
+ else if (deps.vue || deps['vue-router']) detectedFramework = 'vue';
625
+ else if (deps['@angular/core']) detectedFramework = 'angular';
626
+ else if (deps.next) detectedFramework = 'nextjs';
627
+ else if (deps.nuxt) detectedFramework = 'nuxt';
628
+ else if (deps.svelte) detectedFramework = 'svelte';
629
+ else detectedFramework = 'generic';
630
+ }
631
+ } catch (error) {
632
+ detectedFramework = 'generic';
633
+ }
634
+ } else if (SecurityUtils.safeExistsSync(pyprojectPath) || SecurityUtils.safeExistsSync(requirementsPath)) {
635
+ detectedLanguage = 'python';
636
+ try {
637
+ if (SecurityUtils.safeExistsSync(requirementsPath)) {
638
+ const requirements = SecurityUtils.safeReadFileSync(requirementsPath, path.dirname(requirementsPath), 'utf8');
639
+ if (requirements.includes('django')) detectedFramework = 'django';
640
+ else if (requirements.includes('flask')) detectedFramework = 'flask';
641
+ else if (requirements.includes('fastapi')) detectedFramework = 'fastapi';
642
+ else detectedFramework = 'generic';
643
+ }
644
+ } catch (error) {
645
+ detectedFramework = 'generic';
646
+ }
647
+ } else if (SecurityUtils.safeExistsSync(goModPath)) {
648
+ detectedLanguage = 'go';
649
+ detectedFramework = 'generic';
650
+ } else if (SecurityUtils.safeExistsSync(pomPath)) {
651
+ detectedLanguage = 'java';
652
+ try {
653
+ const pomContent = SecurityUtils.safeReadFileSync(pomPath, path.dirname(pomPath), 'utf8');
654
+ if (pomContent.includes('spring-boot')) detectedFramework = 'spring-boot';
655
+ else if (pomContent.includes('spring')) detectedFramework = 'spring';
656
+ else if (pomContent.includes('quarkus')) detectedFramework = 'quarkus';
657
+ else detectedFramework = 'generic';
658
+ } catch (error) {
659
+ detectedFramework = 'generic';
660
+ }
661
+ } else if (SecurityUtils.safeExistsSync(composerPath)) {
662
+ detectedLanguage = 'php';
663
+ try {
664
+ const composer = JSON.parse(SecurityUtils.safeReadFileSync(composerPath, path.dirname(composerPath), 'utf8'));
665
+ const deps = composer.require || {};
666
+
667
+ if (deps['laravel/framework']) detectedFramework = 'laravel';
668
+ else if (deps['symfony/framework-bundle']) detectedFramework = 'symfony';
669
+ else if (deps['wordpress']) detectedFramework = 'wordpress';
670
+ else detectedFramework = 'generic';
671
+ } catch (error) {
672
+ detectedFramework = 'generic';
673
+ }
674
+ }
675
+
676
+ return { detectedLanguage, detectedFramework };
677
+ }
678
+
679
+ async customGlob(patterns, options = {}) {
680
+ const fs = require('fs');
681
+ const path = require('path');
682
+ const cwd = options.cwd || process.cwd();
683
+ const ignorePatterns = options.ignore || [];
684
+
685
+ function matchesPattern(filename, pattern) {
686
+ // Simple pattern matching for **/*.{js,jsx,ts,tsx} style patterns
687
+ if (pattern.includes('**/*')) {
688
+ const extensionPart = pattern.split('*.')[1];
689
+ if (extensionPart) {
690
+ const extensions = extensionPart.replace('{', '').replace('}', '').split(',');
691
+ return extensions.some(ext => filename.endsWith('.' + ext.trim()));
692
+ }
693
+ }
694
+ return filename.includes(pattern.replace('**/', ''));
695
+ }
696
+
697
+ function shouldIgnore(filePath) {
698
+ return ignorePatterns.some(pattern => {
699
+ if (pattern.includes('**/')) {
700
+ const patternEnd = pattern.replace('**/', '');
701
+ return filePath.includes('/' + patternEnd) || filePath.includes('\\' + patternEnd);
702
+ }
703
+ return filePath.includes(pattern);
704
+ });
705
+ }
706
+
707
+ function findFiles(dir, results = []) {
708
+ try {
709
+ const items = fs.readdirSync(dir);
710
+
711
+ for (const item of items) {
712
+ const fullPath = path.join(dir, item);
713
+ const relativePath = path.relative(cwd, fullPath);
714
+
715
+ if (shouldIgnore(relativePath)) {
716
+ continue;
717
+ }
718
+
719
+ try {
720
+ const stat = fs.statSync(fullPath);
721
+
722
+ if (stat.isDirectory()) {
723
+ findFiles(fullPath, results);
724
+ } else if (stat.isFile()) {
725
+ // Check if file matches any of our patterns
726
+ for (const pattern of patterns) {
727
+ if (matchesPattern(item, pattern)) {
728
+ results.push(relativePath);
729
+ break;
730
+ }
731
+ }
732
+ }
733
+ } catch (error) {
734
+ // Skip files we can't access
735
+ continue;
736
+ }
737
+ }
738
+ } catch (error) {
739
+ // Skip directories we can't access
740
+ }
741
+
742
+ return results;
743
+ }
744
+
745
+ return findFiles(cwd);
746
+ }
747
+
748
+ async maybePromptFramework(prompt, cfg, currentVersion) {
749
+ // Load current settings to check framework configuration
750
+ let settings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
751
+
752
+ // Ensure framework configuration exists with all required fields
753
+ if (!settings.framework) {
754
+ settings.framework = {
755
+ detected: false,
756
+ preference: null,
757
+ prompt: 'always',
758
+ lastPromptedVersion: null,
759
+ installed: [],
760
+ version: '1.0' // Schema version for future compatibility
761
+ };
762
+
763
+ // Save the updated settings
764
+ if (configManager.saveSettings) {
765
+ await configManager.saveSettings(settings);
766
+ } else if (configManager.saveConfig) {
767
+ await configManager.saveConfig(settings);
768
+ }
769
+ }
770
+
771
+ // Reload settings to ensure we have the latest framework detection results
772
+ const freshSettings = configManager.loadSettings ? configManager.loadSettings() : (configManager.getConfig ? configManager.getConfig() : {});
773
+ if (freshSettings.framework) {
774
+ settings.framework = { ...settings.framework, ...freshSettings.framework };
775
+ }
776
+
777
+ // Check if framework is already detected or preference is explicitly set to none
778
+ if (settings.framework.detected || settings.framework.preference === 'none') {
779
+ return cfg;
780
+ }
781
+
782
+ // Check if DNR (Do Not Remind) is active for this version
783
+ if (settings.framework.prompt === 'suppress' && settings.framework.lastPromptedVersion === currentVersion) {
784
+ return cfg;
785
+ }
786
+
787
+ // Reset DNR if version changed
788
+ if (settings.framework.prompt === 'suppress' && settings.framework.lastPromptedVersion !== currentVersion) {
789
+ settings.framework.prompt = 'always';
790
+ settings.framework.lastPromptedVersion = null;
791
+
792
+ // Save the updated settings
793
+ if (configManager.saveSettings) {
794
+ await configManager.saveSettings(settings);
795
+ } else if (configManager.saveConfig) {
796
+ await configManager.saveConfig(settings);
797
+ }
798
+ }
799
+
800
+ // This function is now handled by ensureInitializedOrExit for better flow control
801
+
802
+ return cfg;
803
+ }
804
+
805
+ async showInteractiveMenu() {
806
+ // Check if we're in non-interactive mode (like echo 0 | node script)
807
+ if (this.isNonInteractiveMode()) {
808
+ console.log(`\n${t('menu.title')}`);
809
+ console.log(t('menu.separator'));
810
+ console.log(`1. ${t('menu.options.init')}`);
811
+ console.log(`2. ${t('menu.options.analyze')}`);
812
+ console.log(`3. ${t('menu.options.validate')}`);
813
+ console.log(`4. ${t('menu.options.usage')}`);
814
+ console.log(`5. ${t('menu.options.complete')}`);
815
+ console.log(`6. ${t('menu.options.sizing')}`);
816
+ console.log(`7. ${t('menu.options.fix')}`);
817
+ console.log(`8. ${t('menu.options.status')}`);
818
+ console.log(`9. ${t('menu.options.delete')}`);
819
+ console.log(`10. ${t('menu.options.settings')}`);
820
+ console.log(`11. ${t('menu.options.help')}`);
821
+ console.log(`12. ${t('menu.options.language')}`);
822
+ console.log(`13. ${t('menu.options.scanner')}`);
823
+ console.log(`0. ${t('menu.options.exit')}`);
824
+
825
+ console.log('\n' + t('menu.nonInteractiveModeWarning'));
826
+ console.log(t('menu.useDirectExecution'));
827
+ console.log(t('menu.useHelpForCommands'));
828
+ this.safeClose();
829
+ process.exit(0);
830
+ return;
831
+ }
832
+
833
+ console.log(`\n${t('menu.title')}`);
834
+ console.log(t('menu.separator'));
835
+ console.log(`1. ${t('menu.options.init')}`);
836
+ console.log(`2. ${t('menu.options.analyze')}`);
837
+ console.log(`3. ${t('menu.options.validate')}`);
838
+ console.log(`4. ${t('menu.options.usage')}`);
839
+ console.log(`5. ${t('menu.options.complete')}`);
840
+ console.log(`6. ${t('menu.options.sizing')}`);
841
+ console.log(`7. ${t('menu.options.fix')}`);
842
+ console.log(`8. ${t('menu.options.status')}`);
843
+ console.log(`9. ${t('menu.options.delete')}`);
844
+ console.log(`10. ${t('menu.options.settings')}`);
845
+ console.log(`11. ${t('menu.options.help')}`);
846
+ console.log(`12. ${t('menu.options.language')}`);
847
+ console.log(`13. ${t('menu.options.scanner')}`);
848
+ console.log(`0. ${t('menu.options.exit')}`);
849
+
850
+ const choice = await this.prompt('\n' + t('menu.selectOptionPrompt'));
851
+
852
+ switch (choice.trim()) {
853
+ case '1':
854
+ await this.executeCommand('init', {fromMenu: true});
855
+ break;
856
+ case '2':
857
+ await this.executeCommand('analyze', {fromMenu: true});
858
+ break;
859
+ case '3':
860
+ await this.executeCommand('validate', {fromMenu: true});
861
+ break;
862
+ case '4':
863
+ await this.executeCommand('usage', {fromMenu: true});
864
+ break;
865
+ case '5':
866
+ await this.executeCommand('complete', {fromMenu: true});
867
+ break;
868
+ case '6':
869
+ await this.executeCommand('sizing', {fromMenu: true});
870
+ break;
871
+ case '7':
872
+ await this.executeCommand('fix', {fromMenu: true});
873
+ break;
874
+ case '8':
875
+ // Check for PIN protection
876
+ const authRequired = await this.adminAuth.isAuthRequiredForScript('summaryReports');
877
+ if (authRequired) {
878
+ console.log(`\n${t('adminCli.protectedAccess')}`);
879
+ const cliHelper = require('../../utils/cli-helper');
880
+ const pin = await cliHelper.promptPin(t('adminCli.enterPin') + ': ');
881
+ const isValid = await this.adminAuth.verifyPin(pin);
882
+
883
+ if (!isValid) {
884
+ console.log(t('adminCli.invalidPin'));
885
+ await this.prompt(t('menu.pressEnterToContinue'));
886
+ await this.showInteractiveMenu();
887
+ return;
888
+ }
889
+
890
+ console.log(t('adminCli.accessGranted'));
891
+ }
892
+
893
+ console.log(t('summary.status.generating'));
894
+ try {
895
+ // Use SummaryService instead of direct import
896
+ this.summaryService.initialize(configManager);
897
+ const report = await this.summaryService.run({ fromMenu: true });
898
+ console.log(report);
899
+ console.log(t('summary.status.completed'));
900
+
901
+ // Check if we're in interactive mode before prompting
902
+ if (!this.isNonInteractiveMode()) {
903
+ try {
904
+ await this.prompt('\n' + t('debug.pressEnterToContinue'));
905
+ await this.showInteractiveMenu();
906
+ } catch (error) {
907
+ console.log(t('menu.returning'));
908
+ process.exit(0);
909
+ }
910
+ } else {
911
+ console.log(t('status.exitingCompleted'));
912
+ process.exit(0);
913
+ }
914
+ } catch (error) {
915
+ console.error(t('common.errorGeneratingStatusSummary', { error: error.message }));
916
+
917
+ // Check if we're in interactive mode before prompting
918
+ if (!this.isNonInteractiveMode()) {
919
+ try {
920
+ await this.prompt('\n' + t('debug.pressEnterToContinue'));
921
+ await this.showInteractiveMenu();
922
+ } catch (error) {
923
+ console.log(t('menu.returning'));
924
+ process.exit(0);
925
+ }
926
+ } else {
927
+ console.log(t('common.errorExiting'));
928
+ process.exit(1);
929
+ }
930
+ }
931
+ break;
932
+ case '9':
933
+ await this.deleteReports();
934
+ break;
935
+ case '10':
936
+ await this.showSettingsMenu();
937
+ break;
938
+ case '11':
939
+ this.showHelp();
940
+ await this.prompt(t('menu.returnToMainMenu'));
941
+ await this.showInteractiveMenu();
942
+ break;
943
+ case '12':
944
+ await this.showLanguageMenu();
945
+ break;
946
+ case '13':
947
+ await this.executeCommand('scanner', {fromMenu: true});
948
+ break;
949
+ case '0':
950
+ console.log(t('menu.goodbye'));
951
+ this.safeClose();
952
+ process.exit(0);
953
+ default:
954
+ console.log(t('menu.invalidChoice'));
955
+ await this.showInteractiveMenu();
956
+ }
957
+ }
958
+
959
+ // ... existing code for showLanguageMenu, showDebugMenu, deleteReports, showSettingsMenu, etc. ...
960
+
961
+ async showLanguageMenu() {
962
+ console.log(`\n${t('language.title')}`);
963
+ console.log(t('language.separator'));
964
+ console.log(t('language.current', { language: 'English' })); // Simplified since we don't have UIi18n
965
+ console.log('\n' + t('language.available'));
966
+
967
+ const languages = [
968
+ { code: 'en', name: 'English' },
969
+ { code: 'de', name: 'Deutsch' },
970
+ { code: 'es', name: 'Español' },
971
+ { code: 'fr', name: 'Français' },
972
+ { code: 'ru', name: 'Русский' },
973
+ { code: 'ja', name: '日本語' },
974
+ { code: 'zh', name: '中文' }
975
+ ];
976
+
977
+ languages.forEach((lang, index) => {
978
+ const current = 'en' === 'en' ? ' ✓' : '';
979
+ console.log(t('language.languageOption', { index: index + 1, displayName: lang.name, current }));
980
+ });
981
+
982
+ console.log(`0. ${t('language.backToMainMenu')}`);
983
+
984
+ const choice = await this.prompt('\n' + t('language.prompt'));
985
+ const choiceNum = parseInt(choice);
986
+
987
+ if (choiceNum === 0) {
988
+ await this.showInteractiveMenu();
989
+ return;
990
+ } else if (choiceNum >= 1 && choiceNum <= languages.length) {
991
+ const selectedLang = languages[choiceNum - 1];
992
+ console.log(t('language.changed', { language: selectedLang.name }));
993
+
994
+ // Force reload translations for the entire system
995
+ const { loadTranslations } = require('../../utils/i18n-helper');
996
+ loadTranslations(selectedLang.code);
997
+
998
+ // Return to main menu with new language
999
+ await this.prompt('\n' + t('language.pressEnterToContinue'));
1000
+ await this.showInteractiveMenu();
1001
+ } else {
1002
+ console.log(t('language.invalid'));
1003
+ await this.prompt('\n' + t('language.pressEnterToContinue'));
1004
+ await this.showLanguageMenu();
1005
+ }
1006
+ }
1007
+
1008
+ async showDebugMenu() {
1009
+ // Check for PIN protection
1010
+ const authRequired = await this.adminAuth.isAuthRequiredForScript('debugMenu');
1011
+ if (authRequired) {
1012
+ console.log(`\n${t('adminPin.protectedAccess')}`);
1013
+ const cliHelper = require('../../utils/cli-helper');
1014
+ const pin = await cliHelper.promptPin(t('adminPin.enterPin') + ': ');
1015
+ const isValid = await this.adminAuth.verifyPin(pin);
1016
+
1017
+ if (!isValid) {
1018
+ console.log(t('adminPin.invalidPin'));
1019
+ await this.prompt(t('menu.pressEnterToContinue'));
1020
+ await this.showInteractiveMenu();
1021
+ return;
1022
+ }
1023
+
1024
+ console.log(t('adminPin.accessGranted'));
1025
+ }
1026
+
1027
+ console.log(`\n${t('debug.title')}`);
1028
+ console.log(t('debug.separator'));
1029
+ console.log(t('debug.mainDebuggerSystemDiagnostics'));
1030
+ console.log(t('debug.debugLogs'));
1031
+ console.log(t('debug.backToMainMenu'));
1032
+
1033
+ const choice = await this.prompt('\n' + t('debug.selectOption'));
1034
+
1035
+ switch (choice.trim()) {
1036
+ case '1':
1037
+ await this.runDebugTool('debugger.js', 'Main Debugger');
1038
+ break;
1039
+ case '2':
1040
+ await this.viewDebugLogs();
1041
+ break;
1042
+ case '0':
1043
+ await this.showInteractiveMenu();
1044
+ return;
1045
+ default:
1046
+ console.log(t('debug.invalidChoiceSelectRange'));
1047
+ await this.showDebugMenu();
1048
+ }
1049
+ }
1050
+
1051
+ async runDebugTool(toolName, displayName) {
1052
+ console.log(t('debug.runningDebugTool', { displayName }));
1053
+ try {
1054
+ const toolPath = path.join(__dirname, '..', '..', 'scripts', 'debug', toolName);
1055
+ if (SecurityUtils.safeExistsSync(toolPath)) {
1056
+ console.log(`Debug tool available: ${toolName}`);
1057
+ console.log(`To run this tool manually: node "${toolPath}"`);
1058
+ console.log(`Working directory: ${path.join(__dirname, '..', '..')}`);
1059
+ } else {
1060
+ console.log(t('debug.debugToolNotFound', { toolName }));
1061
+ }
1062
+ } catch (error) {
1063
+ console.error(t('debug.errorRunningDebugTool', { displayName, error: error.message }));
1064
+ }
1065
+
1066
+ await this.prompt('\n' + t('menu.pressEnterToContinue'));
1067
+ await this.showDebugMenu();
1068
+ }
1069
+
1070
+ async viewDebugLogs() {
1071
+ console.log(`\n${t('debug.recentDebugLogs')}`);
1072
+ console.log('============================================================');
1073
+
1074
+ try {
1075
+ const logsDir = path.join(__dirname, '..', '..', 'scripts', 'debug', 'logs');
1076
+ if (SecurityUtils.safeExistsSync(logsDir)) {
1077
+ const files = require('fs').readdirSync(logsDir)
1078
+ .filter(file => file.endsWith('.log') || file.endsWith('.txt'))
1079
+ .sort((a, b) => {
1080
+ const statA = require('fs').statSync(path.join(logsDir, a));
1081
+ const statB = require('fs').statSync(path.join(logsDir, b));
1082
+ return statB.mtime - statA.mtime;
1083
+ })
1084
+ .slice(0, 5);
1085
+
1086
+ if (files.length > 0) {
1087
+ files.forEach((file, index) => {
1088
+ const filePath = path.join(logsDir, file);
1089
+ const stats = require('fs').statSync(filePath);
1090
+ console.log(`${index + 1}. ${file} (${stats.mtime.toLocaleString()})`);
1091
+ });
1092
+
1093
+ const choice = await this.prompt('\n' + t('debug.selectLogPrompt', { count: files.length }));
1094
+ const fileIndex = parseInt(choice) - 1;
1095
+
1096
+ if (fileIndex >= 0 && fileIndex < files.length) {
1097
+ const logContent = SecurityUtils.safeReadFileSync(path.join(logsDir, files[fileIndex]), logsDir, 'utf8');
1098
+ console.log(`\n${t('debug.contentOf', { filename: files[fileIndex] })}:`);
1099
+ console.log('============================================================');
1100
+ console.log(logContent.slice(-2000)); // Show last 2000 characters
1101
+ console.log('============================================================');
1102
+ }
1103
+ } else {
1104
+ console.log(t('debug.noDebugLogsFound'));
1105
+ }
1106
+ } else {
1107
+ console.log(t('debug.debugLogsDirectoryNotFound'));
1108
+ }
1109
+ } catch (error) {
1110
+ console.error(t('errors.errorReadingDebugLogs', { error: error.message }));
1111
+ }
1112
+
1113
+ await this.prompt('\n' + t('menu.pressEnterToContinue'));
1114
+ await this.showInteractiveMenu();
1115
+ }
1116
+
1117
+ async deleteReports() {
1118
+ // Check for PIN protection
1119
+ const authRequired = await this.adminAuth.isAuthRequiredForScript('deleteReports');
1120
+ if (authRequired) {
1121
+ console.log(`\n${t('adminPin.protectedAccess')}`);
1122
+ const cliHelper = require('../../utils/cli-helper');
1123
+ const pin = await cliHelper.promptPin(t('adminPin.enterPin') + ': ');
1124
+ const isValid = await this.adminAuth.verifyPin(pin);
1125
+
1126
+ if (!isValid) {
1127
+ console.log(t('adminPin.invalidPin'));
1128
+ await this.prompt(t('menu.pressEnterToContinue'));
1129
+ await this.showInteractiveMenu();
1130
+ return;
1131
+ }
1132
+
1133
+ console.log(t('adminPin.accessGranted'));
1134
+ }
1135
+
1136
+ console.log(`\n${t('operations.deleteReportsTitle')}`);
1137
+ console.log('============================================================');
1138
+
1139
+ const targetDirs = [
1140
+ { path: path.join(process.cwd(), 'i18ntk-reports'), name: 'Reports', type: 'reports' },
1141
+ { path: path.join(process.cwd(), 'reports'), name: 'Legacy Reports', type: 'reports' },
1142
+ { path: path.join(process.cwd(), 'reports', 'backups'), name: 'Reports Backups', type: 'backups' },
1143
+ { path: path.join(process.cwd(), 'scripts', 'debug', 'logs'), name: 'Debug Logs', type: 'logs' },
1144
+ { path: path.join(process.cwd(), 'scripts', 'debug', 'reports'), name: 'Debug Reports', type: 'reports' },
1145
+ { path: path.join(process.cwd(), 'settings', 'backups'), name: 'Settings Backups', type: 'backups' },
1146
+ { path: path.join(process.cwd(), 'utils', 'i18ntk-reports'), name: 'Utils Reports', type: 'reports' }
1147
+ ].filter(dir => dir.path && typeof dir.path === 'string');
1148
+
1149
+ try {
1150
+ console.log(t('operations.scanningForFiles'));
1151
+
1152
+ let availableDirs = [];
1153
+
1154
+ // Check which directories exist and have files
1155
+ for (const dir of targetDirs) {
1156
+ if (SecurityUtils.safeExistsSync(dir.path)) {
1157
+ const files = this.getAllReportFiles(dir.path);
1158
+ if (files.length > 0) {
1159
+ availableDirs.push({
1160
+ ...dir,
1161
+ files: files.map(file => ({ path: file, dir: dir.path })),
1162
+ count: files.length
1163
+ });
1164
+ }
1165
+ }
1166
+ }
1167
+
1168
+ if (availableDirs.length === 0) {
1169
+ console.log(t('operations.noFilesFoundToDelete'));
1170
+ await this.prompt(t('menu.pressEnterToContinue'));
1171
+ await this.showInteractiveMenu();
1172
+ return;
1173
+ }
1174
+
1175
+ // Show available directories
1176
+ console.log(t('operations.availableDirectories'));
1177
+ availableDirs.forEach((dir, index) => {
1178
+ console.log(` ${index + 1}. ${dir.name} (${dir.count} files)`);
1179
+ });
1180
+ console.log(` ${availableDirs.length + 1}. ${t('operations.allDirectories')}`);
1181
+ console.log(` 0. ${t('operations.cancelOption')}`);
1182
+
1183
+ const dirChoice = await this.prompt(`\nSelect directory to clean (0-${availableDirs.length + 1}): `);
1184
+ const dirIndex = parseInt(dirChoice) - 1;
1185
+
1186
+ let selectedDirs = [];
1187
+
1188
+ if (dirChoice.trim() === '0') {
1189
+ console.log(t('operations.cancelled'));
1190
+ await this.prompt(t('menu.pressEnterToContinue'));
1191
+ await this.showInteractiveMenu();
1192
+ return;
1193
+ } else if (dirIndex === availableDirs.length) {
1194
+ selectedDirs = availableDirs;
1195
+ } else if (dirIndex >= 0 && dirIndex < availableDirs.length) {
1196
+ selectedDirs = [availableDirs[dirIndex]];
1197
+ } else {
1198
+ console.log(t('operations.invalidSelection'));
1199
+ await this.prompt(t('menu.pressEnterToContinue'));
1200
+ await this.showInteractiveMenu();
1201
+ return;
1202
+ }
1203
+
1204
+ // Collect all files from selected directories
1205
+ let allFiles = [];
1206
+ selectedDirs.forEach(dir => {
1207
+ allFiles.push(...dir.files);
1208
+ });
1209
+
1210
+ console.log(t('operations.foundFilesInSelectedDirectories', { count: allFiles.length }));
1211
+ selectedDirs.forEach(dir => {
1212
+ console.log(` 📁 ${dir.name}: ${dir.count} files`);
1213
+ });
1214
+
1215
+ // Show deletion options
1216
+ console.log(t('operations.deletionOptions'));
1217
+ console.log(` 1. ${t('operations.deleteAllFiles')}`);
1218
+ console.log(` 2. ${t('operations.keepLast3Files')}`);
1219
+ console.log(` 3. ${t('operations.keepLast5Files')}`);
1220
+ console.log(` 0. ${t('operations.cancelReportOption')}`);
1221
+
1222
+ const option = await this.prompt('\nSelect option (0-3): ');
1223
+
1224
+ let filesToDelete = [];
1225
+
1226
+ switch (option.trim()) {
1227
+ case '1':
1228
+ filesToDelete = allFiles;
1229
+ break;
1230
+ case '2':
1231
+ filesToDelete = this.getFilesToDeleteKeepLast(allFiles, 3);
1232
+ break;
1233
+ case '3':
1234
+ filesToDelete = this.getFilesToDeleteKeepLast(allFiles, 5);
1235
+ break;
1236
+ case '0':
1237
+ console.log(t('operations.cancelled'));
1238
+ await this.prompt(t('menu.pressEnterToContinue'));
1239
+ await this.showInteractiveMenu();
1240
+ return;
1241
+ default:
1242
+ console.log(t('menu.invalidOption'));
1243
+ await this.prompt(t('menu.pressEnterToContinue'));
1244
+ await this.showInteractiveMenu();
1245
+ return;
1246
+ }
1247
+
1248
+ if (filesToDelete.length === 0) {
1249
+ console.log(t('operations.noFilesToDelete'));
1250
+ await this.prompt(t('menu.pressEnterToContinue'));
1251
+ await this.showInteractiveMenu();
1252
+ return;
1253
+ }
1254
+
1255
+ console.log(t('operations.filesToDeleteCount', { count: filesToDelete.length }));
1256
+ console.log(t('operations.filesToKeepCount', { count: allFiles.length - filesToDelete.length }));
1257
+
1258
+ const confirm = await this.prompt(t('operations.confirmDeletion'));
1259
+
1260
+ if (confirm.toLowerCase() === 'y' || confirm.toLowerCase() === 'yes') {
1261
+ let deletedCount = 0;
1262
+
1263
+ for (const fileInfo of filesToDelete) {
1264
+ try {
1265
+ require('fs').unlinkSync(fileInfo.path);
1266
+ console.log(t('operations.deletedFile', { filename: path.basename(fileInfo.path) }));
1267
+ deletedCount++;
1268
+ } catch (error) {
1269
+ console.log(t('operations.failedToDeleteFile', { filename: path.basename(fileInfo.path), error: error.message }));
1270
+ }
1271
+ }
1272
+
1273
+ console.log(`\n🎉 Successfully deleted ${deletedCount} files!`);
1274
+ } else {
1275
+ console.log(t('operations.cancelled'));
1276
+ }
1277
+
1278
+ } catch (error) {
1279
+ console.error(`❌ Error during deletion process: ${error.message}`);
1280
+ }
1281
+
1282
+ await this.prompt(t('menu.pressEnterToContinue'));
1283
+ await this.showInteractiveMenu();
1284
+ }
1285
+
1286
+ getAllReportFiles(dir) {
1287
+ if (!dir || typeof dir !== 'string') {
1288
+ return [];
1289
+ }
1290
+
1291
+ let files = [];
1292
+
1293
+ try {
1294
+ if (!SecurityUtils.safeExistsSync(dir)) {
1295
+ return [];
1296
+ }
1297
+
1298
+ const items = require('fs').readdirSync(dir);
1299
+ for (const item of items) {
1300
+ const fullPath = path.join(dir, item);
1301
+
1302
+ try {
1303
+ const stat = require('fs').statSync(fullPath);
1304
+
1305
+ if (stat.isDirectory()) {
1306
+ files.push(...this.getAllReportFiles(fullPath));
1307
+ } else if (
1308
+ // Common report file extensions
1309
+ item.endsWith('.json') ||
1310
+ item.endsWith('.html') ||
1311
+ item.endsWith('.txt') ||
1312
+ item.endsWith('.log') ||
1313
+ item.endsWith('.csv') ||
1314
+ item.endsWith('.md') ||
1315
+ // Specific report filename patterns
1316
+ item.includes('-report.') ||
1317
+ item.includes('_report.') ||
1318
+ item.includes('report-') ||
1319
+ item.includes('report_') ||
1320
+ item.includes('analysis-') ||
1321
+ item.includes('validation-')
1322
+ ) {
1323
+ files.push(fullPath);
1324
+ }
1325
+ } catch (error) {
1326
+ // Skip individual files that can't be accessed
1327
+ continue;
1328
+ }
1329
+ }
1330
+ } catch (error) {
1331
+ // Silent fail for inaccessible directories
1332
+ console.log(`⚠️ Could not access directory: ${dir}`);
1333
+ }
1334
+
1335
+ return files;
1336
+ }
1337
+
1338
+ getFilesToDeleteKeepLast(allFiles, keepCount = 3) {
1339
+ // Sort files by modification time (newest first)
1340
+ const sortedFiles = allFiles.sort((a, b) => {
1341
+ try {
1342
+ const statA = require('fs').statSync(a.path || a);
1343
+ const statB = require('fs').statSync(b.path || b);
1344
+ return statB.mtime.getTime() - statA.mtime.getTime();
1345
+ } catch (error) {
1346
+ // If stat fails, sort by filename as fallback
1347
+ const pathA = a.path || a;
1348
+ const pathB = b.path || b;
1349
+ return pathB.localeCompare(pathA);
1350
+ }
1351
+ });
1352
+
1353
+ // Keep the N newest files, delete the rest
1354
+ return sortedFiles.slice(keepCount);
1355
+ }
1356
+
1357
+ async showSettingsMenu() {
1358
+ try {
1359
+ // Check for PIN protection
1360
+ const authRequired = await this.adminAuth.isAuthRequiredForScript('settingsMenu');
1361
+ if (authRequired) {
1362
+ console.log(`\n${t('adminPin.protectedAccess')}`);
1363
+ const cliHelper = require('../../utils/cli-helper');
1364
+ const pin = await cliHelper.promptPin(t('adminPin.enterPin') + ': ');
1365
+ const isValid = await this.adminAuth.verifyPin(pin);
1366
+
1367
+ if (!isValid) {
1368
+ console.log(t('adminPin.invalidPin'));
1369
+ await this.prompt(t('menu.pressEnterToContinue'));
1370
+ await this.showInteractiveMenu();
1371
+ return;
1372
+ }
1373
+
1374
+ console.log(t('adminPin.accessGranted'));
1375
+ }
1376
+
1377
+ const SettingsCLI = require('../../settings/settings-cli');
1378
+ const settingsCLI = new SettingsCLI();
1379
+ await settingsCLI.run();
1380
+ } catch (error) {
1381
+ console.error('❌ Error opening settings:', error.message);
1382
+ await this.prompt(t('menu.pressEnterToContinue'));
1383
+ }
1384
+ await this.showInteractiveMenu();
1385
+ }
1386
+
1387
+ prompt(question) {
1388
+ const cliHelper = require('../../utils/cli-helper');
1389
+ // If interactive not available, return empty string to avoid hangs
1390
+ if (!process.stdin.isTTY || process.stdin.destroyed) {
1391
+ console.log('\n⚠️ Interactive input not available, using default response.');
1392
+ return Promise.resolve('');
1393
+ }
1394
+ return cliHelper.prompt(`${question} `);
1395
+ }
1396
+
1397
+ // Safe method to check if we're in non-interactive mode
1398
+ isNonInteractiveMode() {
1399
+ return !process.stdin.isTTY || process.stdin.destroyed || this.isReadlineClosed;
1400
+ }
1401
+
1402
+ safeClose() {
1403
+ if (this.rl && !this.isReadlineClosed) {
1404
+ try {
1405
+ this.rl.close();
1406
+ this.isReadlineClosed = true;
1407
+ } catch (error) {
1408
+ // Ignore close errors
1409
+ }
1410
+ }
1411
+ }
1412
+ }
1413
+
1414
+ // Run if called directly
1415
+ if (require.main === module) {
1416
+ // Handle version and help immediately before any initialization
1417
+ const args = process.argv.slice(2);
1418
+
1419
+ if (args.includes('--version') || args.includes('-v')) {
1420
+ try {
1421
+ const packageJsonPath = path.resolve(__dirname, '..', '..', 'package.json');
1422
+ // Use a safe base path for SecurityUtils validation
1423
+ const basePath = path.dirname(packageJsonPath) || process.cwd();
1424
+ const packageJsonContent = SecurityUtils.safeReadFileSync(packageJsonPath, basePath, 'utf8');
1425
+
1426
+ if (!packageJsonContent) {
1427
+ console.log(`\n❌ Version information unavailable - could not read package.json`);
1428
+ process.exit(0);
1429
+ }
1430
+
1431
+ const packageJson = JSON.parse(packageJsonContent);
1432
+ const versionInfo = packageJson.versionInfo || {};
1433
+
1434
+ console.log(`\n🌍 i18n Toolkit (i18ntk)`);
1435
+ console.log(`Version: ${packageJson.version}`);
1436
+ console.log(`Release Date: ${versionInfo.releaseDate || 'N/A'}`);
1437
+ console.log(`Maintainer: ${versionInfo.maintainer || packageJson.author}`);
1438
+ console.log(`Node.js: ${versionInfo.supportedNodeVersions || packageJson.engines?.node || '>=16.0.0'}`);
1439
+ console.log(`License: ${packageJson.license}`);
1440
+
1441
+ if (versionInfo.majorChanges && versionInfo.majorChanges.length > 0) {
1442
+ console.log(`\n✨ What's New in ${packageJson.version}:`);
1443
+ versionInfo.majorChanges.forEach(change => {
1444
+ console.log(` • ${change}`);
1445
+ });
1446
+ }
1447
+
1448
+ console.log(`\n📖 Documentation: ${packageJson.homepage}`);
1449
+ console.log(`🐛 Report Issues: ${packageJson.bugs?.url}`);
1450
+
1451
+ } catch (error) {
1452
+ console.log(`\n❌ Version information unavailable`);
1453
+ console.log(`Error: ${error.message}`);
1454
+ }
1455
+ process.exit(0);
1456
+ }
1457
+
1458
+ const manager = new I18nManager();
1459
+ manager.run();
1460
+ }
1461
+
1462
+ module.exports = I18nManager;