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