i18ntk 1.0.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 (86) hide show
  1. package/CHANGELOG.md +401 -0
  2. package/LICENSE +21 -0
  3. package/README.md +507 -0
  4. package/dev/README.md +37 -0
  5. package/dev/debug/README.md +30 -0
  6. package/dev/debug/complete-console-translations.js +295 -0
  7. package/dev/debug/console-key-checker.js +408 -0
  8. package/dev/debug/console-translations.js +335 -0
  9. package/dev/debug/debugger.js +408 -0
  10. package/dev/debug/export-missing-keys.js +432 -0
  11. package/dev/debug/final-normalize.js +236 -0
  12. package/dev/debug/find-extra-keys.js +68 -0
  13. package/dev/debug/normalize-locales.js +153 -0
  14. package/dev/debug/refactor-locales.js +240 -0
  15. package/dev/debug/reorder-locales.js +85 -0
  16. package/dev/debug/replace-hardcoded-console.js +378 -0
  17. package/docs/INSTALLATION.md +449 -0
  18. package/docs/README.md +222 -0
  19. package/docs/TODO_ROADMAP.md +279 -0
  20. package/docs/api/API_REFERENCE.md +377 -0
  21. package/docs/api/COMPONENTS.md +492 -0
  22. package/docs/api/CONFIGURATION.md +651 -0
  23. package/docs/api/NPM_PUBLISHING_GUIDE.md +434 -0
  24. package/docs/debug/DEBUG_README.md +30 -0
  25. package/docs/debug/DEBUG_TOOLS.md +494 -0
  26. package/docs/development/AGENTS.md +351 -0
  27. package/docs/development/DEVELOPMENT_RULES.md +165 -0
  28. package/docs/development/DEV_README.md +37 -0
  29. package/docs/release-notes/RELEASE_NOTES_v1.0.0.md +173 -0
  30. package/docs/release-notes/RELEASE_NOTES_v1.6.0.md +141 -0
  31. package/docs/release-notes/RELEASE_NOTES_v1.6.1.md +185 -0
  32. package/docs/release-notes/RELEASE_NOTES_v1.6.3.md +199 -0
  33. package/docs/reports/ANALYSIS_README.md +17 -0
  34. package/docs/reports/CONSOLE_MISMATCH_BUG_REPORT_v1.5.0.md +181 -0
  35. package/docs/reports/SIZING_README.md +18 -0
  36. package/docs/reports/SUMMARY_README.md +18 -0
  37. package/docs/reports/TRANSLATION_BUG_REPORT_v1.5.0.md +129 -0
  38. package/docs/reports/USAGE_README.md +18 -0
  39. package/docs/reports/VALIDATION_README.md +18 -0
  40. package/locales/de/auth.json +3 -0
  41. package/locales/de/common.json +16 -0
  42. package/locales/de/pagination.json +6 -0
  43. package/locales/en/auth.json +3 -0
  44. package/locales/en/common.json +16 -0
  45. package/locales/en/pagination.json +6 -0
  46. package/locales/es/auth.json +3 -0
  47. package/locales/es/common.json +16 -0
  48. package/locales/es/pagination.json +6 -0
  49. package/locales/fr/auth.json +3 -0
  50. package/locales/fr/common.json +16 -0
  51. package/locales/fr/pagination.json +6 -0
  52. package/locales/ru/auth.json +3 -0
  53. package/locales/ru/common.json +16 -0
  54. package/locales/ru/pagination.json +6 -0
  55. package/main/i18ntk-analyze.js +625 -0
  56. package/main/i18ntk-autorun.js +461 -0
  57. package/main/i18ntk-complete.js +494 -0
  58. package/main/i18ntk-init.js +686 -0
  59. package/main/i18ntk-manage.js +848 -0
  60. package/main/i18ntk-sizing.js +557 -0
  61. package/main/i18ntk-summary.js +671 -0
  62. package/main/i18ntk-usage.js +1282 -0
  63. package/main/i18ntk-validate.js +762 -0
  64. package/main/ui-i18n.js +332 -0
  65. package/package.json +152 -0
  66. package/scripts/fix-missing-translation-keys.js +214 -0
  67. package/scripts/verify-package.js +168 -0
  68. package/ui-locales/de.json +637 -0
  69. package/ui-locales/en.json +688 -0
  70. package/ui-locales/es.json +637 -0
  71. package/ui-locales/fr.json +637 -0
  72. package/ui-locales/ja.json +637 -0
  73. package/ui-locales/ru.json +637 -0
  74. package/ui-locales/zh.json +637 -0
  75. package/utils/admin-auth.js +317 -0
  76. package/utils/admin-cli.js +353 -0
  77. package/utils/admin-pin.js +409 -0
  78. package/utils/detect-language-mismatches.js +454 -0
  79. package/utils/i18n-helper.js +128 -0
  80. package/utils/maintain-language-purity.js +433 -0
  81. package/utils/native-translations.js +478 -0
  82. package/utils/security.js +384 -0
  83. package/utils/test-complete-system.js +356 -0
  84. package/utils/test-console-i18n.js +402 -0
  85. package/utils/translate-mismatches.js +571 -0
  86. package/utils/validate-language-purity.js +531 -0
@@ -0,0 +1,1282 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * I18N USAGE ANALYSIS TOOLKIT - Version 1.4.3
4
+ *
5
+ * This script analyzes source code to find unused translation keys,
6
+ * missing translations, and provides comprehensive translation completeness analysis.
7
+ *
8
+ * NEW in v1.4.3:
9
+ * - Modular folder structure support
10
+ * - Recursive translation file discovery
11
+ * - NOT_TRANSLATED analysis
12
+ * - Enhanced reporting with completeness statistics
13
+ *
14
+ * Usage:
15
+ * npm run i18ntk:usage
16
+ * npm run i18ntk:usage -- --source-dir=./src
17
+ * npm run i18ntk:usage -- --i18n-dir=./src/i18n/locales
18
+ * npm run i18ntk:usage -- --output-report
19
+ *
20
+ * Alternative direct usage:
21
+ * node i18ntk-usage.js
22
+ */
23
+
24
+ const fs = require('fs');
25
+ const path = require('path');
26
+ const readline = require('readline');
27
+ const { loadTranslations, t } = require('../utils/i18n-helper');
28
+ const settingsManager = require('../settings/settings-manager');
29
+ const SecurityUtils = require('../utils/security');
30
+ const AdminCLI = require('../utils/admin-cli');
31
+
32
+ // Enhanced configuration with multiple source directory detection
33
+ async function getConfig() {
34
+ try {
35
+ const settings = settingsManager.getSettings();
36
+
37
+ // Multiple possible source directories to check
38
+ const possibleSourceDirs = [
39
+ './main', // Primary source directory for this project
40
+ './src',
41
+ './app',
42
+ './components',
43
+ './pages',
44
+ './views',
45
+ './client',
46
+ './frontend',
47
+ './' // Current directory as fallback
48
+ ];
49
+
50
+ // Auto-detect source directory
51
+ let detectedSourceDir = './src'; // Default
52
+ for (const dir of possibleSourceDirs) {
53
+ if (fs.existsSync(dir)) {
54
+ // Check if directory contains code files
55
+ try {
56
+ const files = fs.readdirSync(dir);
57
+ const hasCodeFiles = files.some(file =>
58
+ ['.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte'].includes(path.extname(file))
59
+ );
60
+ if (hasCodeFiles) {
61
+ detectedSourceDir = dir;
62
+ break;
63
+ }
64
+ } catch (error) {
65
+ // Continue checking
66
+ }
67
+ }
68
+ }
69
+
70
+ // Multiple possible i18n directories
71
+ const possibleI18nDirs = [
72
+ './locales',
73
+ './src/locales',
74
+ './src/i18n',
75
+ './src/i18n/locales',
76
+ './app/locales',
77
+ './app/i18n',
78
+ './public/locales',
79
+ './assets/locales',
80
+ './translations',
81
+ './lang'
82
+ ];
83
+
84
+ // Auto-detect i18n directory
85
+ let detectedI18nDir = './locales'; // Default
86
+ for (const dir of possibleI18nDirs) {
87
+ if (fs.existsSync(dir)) {
88
+ // Check if directory contains language subdirectories or JSON files
89
+ try {
90
+ const items = fs.readdirSync(dir);
91
+ const hasLanguageDirs = items.some(item => {
92
+ const itemPath = path.join(dir, item);
93
+ if (fs.statSync(itemPath).isDirectory()) {
94
+ return ['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(item);
95
+ }
96
+ return item.endsWith('.json');
97
+ });
98
+ if (hasLanguageDirs) {
99
+ detectedI18nDir = dir;
100
+ break;
101
+ }
102
+ } catch (error) {
103
+ // Continue checking
104
+ }
105
+ }
106
+ }
107
+
108
+ const config = {
109
+ sourceDir: settings.directories?.sourceDir || detectedSourceDir,
110
+ i18nDir: settings.directories?.i18nDir || detectedI18nDir,
111
+ sourceLanguage: settings.directories?.sourceLanguage || settings.sourceLanguage || 'en',
112
+ outputDir: settings.directories?.outputDir || settings.outputDir || './i18ntk-reports',
113
+ excludeDirs: settings.processing?.excludeDirs || [
114
+ 'node_modules', '.git', 'dist', 'build', '.next', '.nuxt',
115
+ 'i18ntk-reports', 'reports', 'dev', 'utils', 'test', 'tests'
116
+ ],
117
+ includeExtensions: settings.processing?.includeExtensions || ['.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte'],
118
+ translationPatterns: settings.processing?.translationPatterns || [
119
+ /t\(['"`]([^'"`]+)['"`]\)/g,
120
+ /\$t\(['"`]([^'"`]+)['"`]\)/g,
121
+ /i18n\.t\(['"`]([^'"`]+)['"`]\)/g,
122
+ /useTranslation\(\).*t\(['"`]([^'"`]+)['"`]\)/g
123
+ ]
124
+ };
125
+
126
+ console.log(`Detected source directory: ${config.sourceDir}`);
127
+ console.log(`Detected i18n directory: ${config.i18nDir}`);
128
+
129
+ return config;
130
+ } catch (error) {
131
+ throw new Error(`Configuration error: ${error.message}`);
132
+ }
133
+ }
134
+
135
+ class I18nUsageAnalyzer {
136
+ constructor(config = {}) {
137
+ this.config = config;
138
+ this.sourceDir = null;
139
+ this.i18nDir = null;
140
+ this.sourceLanguageDir = null;
141
+
142
+ // Initialize class properties
143
+ this.availableKeys = new Set();
144
+ this.usedKeys = new Set();
145
+ this.fileUsage = new Map();
146
+ this.translationFiles = new Map(); // New: Track all translation files
147
+ this.translationStats = new Map(); // New: Track translation completeness
148
+
149
+ // Initialize UI i18n for console messages
150
+ const UIi18n = require('./ui-i18n');
151
+ this.ui = new UIi18n();
152
+ this.t = this.ui.t.bind(this.ui);
153
+
154
+ // Initialize readline interface
155
+ this.rl = null;
156
+ }
157
+
158
+ // Initialize readline interface
159
+ initReadline() {
160
+ if (!this.rl) {
161
+ this.rl = readline.createInterface({
162
+ input: process.stdin,
163
+ output: process.stdout
164
+ });
165
+ }
166
+ return this.rl;
167
+ }
168
+
169
+ // Close readline interface
170
+ closeReadline() {
171
+ if (this.rl) {
172
+ this.rl.close();
173
+ this.rl = null;
174
+ }
175
+ }
176
+
177
+ // Prompt for user input
178
+ async prompt(question) {
179
+ const rl = this.initReadline();
180
+ return new Promise((resolve) => {
181
+ rl.question(question, resolve);
182
+ });
183
+ }
184
+
185
+ async initialize() {
186
+ try {
187
+ const defaultConfig = await getConfig();
188
+ this.config = { ...defaultConfig, ...this.config };
189
+
190
+ // Resolve paths
191
+ this.sourceDir = path.resolve(this.config.sourceDir);
192
+ this.i18nDir = path.resolve(this.config.i18nDir);
193
+ this.sourceLanguageDir = path.join(this.i18nDir, this.config.sourceLanguage);
194
+
195
+ // Verify translation function
196
+ if (typeof this.t !== 'function') {
197
+ throw new Error('Translation function not properly initialized');
198
+ }
199
+
200
+ await SecurityUtils.logSecurityEvent('analyzer_initialized', { component: 'i18ntk-usage' });
201
+ } catch (error) {
202
+ await SecurityUtils.logSecurityEvent('analyzer_init_failed', { component: 'i18ntk-usage', error: error.message });
203
+ throw error;
204
+ }
205
+ }
206
+
207
+ // Parse command line arguments
208
+ async parseArgs() {
209
+ try {
210
+ const args = process.argv.slice(2);
211
+ const parsed = {};
212
+
213
+ // Convert array to object for processing
214
+ const argsObj = {};
215
+ for (let i = 0; i < args.length; i++) {
216
+ const arg = args[i];
217
+ if (arg.startsWith('--')) {
218
+ const key = arg.substring(2);
219
+ if (key.includes('=')) {
220
+ const [k, v] = key.split('=', 2);
221
+ argsObj[k] = v;
222
+ } else {
223
+ argsObj[key] = args[i + 1] && !args[i + 1].startsWith('--') ? args[++i] : true;
224
+ }
225
+ }
226
+ }
227
+
228
+ const validatedArgs = await SecurityUtils.validateCommandArgs(argsObj);
229
+
230
+ // Process validated arguments
231
+ for (const [key, value] of Object.entries(validatedArgs)) {
232
+ if (key === 'source-dir' && value) {
233
+ const sanitized = await SecurityUtils.sanitizeInput(value);
234
+ const validated = SecurityUtils.validatePath(sanitized, process.cwd());
235
+ if (validated) {
236
+ parsed.sourceDir = validated;
237
+ }
238
+ } else if (key === 'i18n-dir' && value) {
239
+ const sanitized = await SecurityUtils.sanitizeInput(value);
240
+ const validated = SecurityUtils.validatePath(sanitized, process.cwd());
241
+ if (validated) {
242
+ parsed.i18nDir = validated;
243
+ }
244
+ } else if (key === 'output-dir' && value) {
245
+ const sanitized = await SecurityUtils.sanitizeInput(value);
246
+ const validated = SecurityUtils.validatePath(sanitized, process.cwd());
247
+ if (validated) {
248
+ parsed.outputDir = validated;
249
+ }
250
+ } else if (key === 'help') {
251
+ parsed.help = true;
252
+ } else if (key === 'no-prompt') {
253
+ parsed.noPrompt = true;
254
+ }
255
+ }
256
+
257
+ await SecurityUtils.logSecurityEvent('args_parsed', { component: 'i18ntk-usage', args: parsed });
258
+ return parsed;
259
+ } catch (error) {
260
+ await SecurityUtils.logSecurityEvent('args_parse_failed', { component: 'i18ntk-usage', error: error.message });
261
+ throw error;
262
+ }
263
+ }
264
+
265
+ // NEW: Recursively discover all translation files in modular structure
266
+ async discoverTranslationFiles(baseDir, language = this.config.sourceLanguage) {
267
+ const translationFiles = [];
268
+
269
+ const traverse = async (currentDir) => {
270
+ try {
271
+ const absoluteDir = path.resolve(currentDir);
272
+ const validatedPath = SecurityUtils.validatePath(absoluteDir, process.cwd());
273
+
274
+ if (!validatedPath || !fs.existsSync(validatedPath)) {
275
+ return;
276
+ }
277
+
278
+ const items = fs.readdirSync(validatedPath);
279
+
280
+ for (const item of items) {
281
+ const itemPath = path.join(validatedPath, item);
282
+
283
+ try {
284
+ const stat = fs.statSync(itemPath);
285
+
286
+ if (stat.isDirectory()) {
287
+ // Skip excluded directories
288
+ if (!this.config.excludeDirs.includes(item)) {
289
+ await traverse(itemPath);
290
+ }
291
+ } else if (stat.isFile()) {
292
+ // Look for translation files:
293
+ // 1. Direct language files: en.json, de.json, etc.
294
+ // 2. Language directory files: en/common.json, de/auth.json, etc.
295
+ // 3. Nested modular files: components/en.json, features/auth/en.json, etc.
296
+
297
+ const fileName = path.basename(item, '.json');
298
+ const parentDir = path.basename(path.dirname(itemPath));
299
+
300
+ if (item.endsWith('.json')) {
301
+ // Case 1: Direct language files (en.json)
302
+ if (fileName === language) {
303
+ translationFiles.push({
304
+ filePath: itemPath,
305
+ namespace: path.relative(baseDir, path.dirname(itemPath)).replace(/[\\/]/g, '.') || 'root',
306
+ language: language,
307
+ type: 'direct'
308
+ });
309
+ }
310
+ // Case 2: Files in language directories (en/common.json)
311
+ else if (parentDir === language) {
312
+ translationFiles.push({
313
+ filePath: itemPath,
314
+ namespace: fileName,
315
+ language: language,
316
+ type: 'namespaced'
317
+ });
318
+ }
319
+ }
320
+ }
321
+ } catch (statError) {
322
+ // Skip files that can't be accessed
323
+ continue;
324
+ }
325
+ }
326
+ } catch (error) {
327
+ await SecurityUtils.logSecurityEvent('translation_discovery_error', {
328
+ component: 'i18ntk-usage',
329
+ directory: currentDir,
330
+ error: error.message
331
+ });
332
+ }
333
+ };
334
+
335
+ await traverse(baseDir);
336
+ return translationFiles;
337
+ }
338
+
339
+ // Get all files recursively from a directory with enhanced filtering
340
+ async getAllFiles(dir, extensions = this.config.includeExtensions) {
341
+ const files = [];
342
+
343
+ // Enhanced list of toolkit files to exclude from analysis
344
+ const excludeFiles = [
345
+ 'i18ntk-analyze.js', 'i18ntk-autorun.js', 'i18ntk-complete.js',
346
+ 'i18ntk-init.js', 'i18ntk-manage.js', 'i18ntk-sizing.js',
347
+ 'i18ntk-summary.js', 'i18ntk-usage.js', 'i18ntk-validate.js',
348
+ 'console-translations.js', 'console-key-checker.js',
349
+ 'complete-console-translations.js', 'detect-language-mismatches.js',
350
+ 'export-missing-keys.js', 'maintain-language-purity.js',
351
+ 'native-translations.js', 'settings-cli.js', 'settings-manager.js',
352
+ 'test-complete-system.js', 'test-console-i18n.js', 'test-features.js',
353
+ 'translate-mismatches.js', 'ui-i18n.js', 'update-console-i18n.js',
354
+ 'validate-language-purity.js', 'debugger.js', 'admin-auth.js',
355
+ 'admin-cli.js', 'i18n-helper.js', 'security.js'
356
+ ];
357
+
358
+ const traverse = async (currentDir) => {
359
+ try {
360
+ const absoluteDir = path.resolve(currentDir);
361
+ const validatedPath = SecurityUtils.validatePath(absoluteDir, process.cwd());
362
+
363
+ if (!validatedPath || !fs.existsSync(validatedPath)) {
364
+ return;
365
+ }
366
+
367
+ const items = fs.readdirSync(validatedPath);
368
+
369
+ for (const item of items) {
370
+ const itemPath = path.join(validatedPath, item);
371
+
372
+ try {
373
+ const stat = fs.statSync(itemPath);
374
+
375
+ if (stat.isDirectory()) {
376
+ // Skip excluded directories
377
+ if (!this.config.excludeDirs.includes(item)) {
378
+ await traverse(itemPath);
379
+ }
380
+ } else if (stat.isFile()) {
381
+ // Include files with specified extensions, but exclude toolkit files
382
+ const ext = path.extname(item);
383
+ if (extensions.includes(ext) && !excludeFiles.includes(item)) {
384
+ files.push(itemPath);
385
+ }
386
+ }
387
+ } catch (statError) {
388
+ // Skip files that can't be accessed
389
+ continue;
390
+ }
391
+ }
392
+ } catch (error) {
393
+ await SecurityUtils.logSecurityEvent('file_traversal_error', {
394
+ component: 'i18ntk-usage',
395
+ directory: currentDir,
396
+ error: error.message
397
+ });
398
+ }
399
+ };
400
+
401
+ await traverse(dir);
402
+ return files;
403
+ }
404
+
405
+ async run() {
406
+ try {
407
+ await this.initialize();
408
+
409
+ const args = await this.parseArgs();
410
+
411
+ if (args.help) {
412
+ this.showHelp();
413
+ return;
414
+ }
415
+
416
+ // Override config with command line arguments
417
+ if (args.sourceDir) {
418
+ this.config.sourceDir = args.sourceDir;
419
+ this.sourceDir = path.resolve(args.sourceDir);
420
+ }
421
+
422
+ if (args.i18nDir) {
423
+ this.config.i18nDir = args.i18nDir;
424
+ this.i18nDir = path.resolve(args.i18nDir);
425
+ this.sourceLanguageDir = path.join(this.i18nDir, this.config.sourceLanguage);
426
+ }
427
+
428
+ console.log(this.t('checkUsage.source_directory_thissourcedir', { sourceDir: this.sourceDir }));
429
+ console.log(this.t('checkUsage.i18n_directory_thisi18ndir', { i18nDir: this.i18nDir }));
430
+
431
+ // Load available translation keys first
432
+ await this.loadAvailableKeys();
433
+
434
+ // Perform usage analysis
435
+ await this.analyzeUsage();
436
+
437
+ // NEW: Analyze translation completeness
438
+ await this.analyzeTranslationCompleteness();
439
+
440
+ // Generate and display results
441
+ const unusedKeys = this.findUnusedKeys();
442
+ const missingKeys = this.findMissingKeys();
443
+ const notTranslatedStats = this.getNotTranslatedStats();
444
+
445
+ console.log('\n' + this.t('usage.analysisResults'));
446
+ console.log(' ' + this.t('usage.availableKeysCount', { count: this.availableKeys.size }));
447
+ console.log(' ' + this.t('usage.usedKeysCount', { count: this.usedKeys.size }));
448
+ console.log(this.t('usage.unusedKeysCount', { count: unusedKeys.length }));
449
+ console.log(this.t('usage.missingKeysCount', { count: missingKeys.length }));
450
+ console.log(this.t('usage.notTranslatedKeysTotal', { total: notTranslatedStats.total }));
451
+
452
+ // Display translation completeness by language
453
+ console.log(this.t('usage.translationCompletenessTitle'));
454
+ for (const [language, stats] of this.translationStats) {
455
+ const completeness = ((stats.translated / stats.total) * 100).toFixed(1);
456
+ console.log(this.t('usage.languageCompletenessStats', { language, completeness, translated: stats.translated, total: stats.total }));
457
+ }
458
+
459
+ if (args.outputReport) {
460
+ const report = this.generateUsageReport();
461
+ await this.saveReport(report, args.outputDir);
462
+ }
463
+
464
+ console.log('\n' + this.t('usage.analysisCompletedSuccessfully'));
465
+
466
+ if (require.main === module) {
467
+ await this.prompt('\nPress Enter to continue...');
468
+ }
469
+ this.closeReadline();
470
+
471
+ } catch (error) {
472
+ console.error(this.t('usage.analysisFailedError'), error.message);
473
+ this.closeReadline();
474
+ await SecurityUtils.logSecurityEvent('usage_analysis_failed', {
475
+ component: 'i18ntk-usage',
476
+ error: error.message
477
+ });
478
+ throw error;
479
+ }
480
+ }
481
+
482
+ // Show help message
483
+ showHelp() {
484
+ console.log(this.t('checkUsage.help_message'));
485
+ }
486
+
487
+ // NEW: Enhanced translation key loading with modular support
488
+ async getAllTranslationKeys() {
489
+ const keys = new Set();
490
+
491
+ try {
492
+ // Discover all translation files in the i18n directory
493
+ const translationFiles = await this.discoverTranslationFiles(this.i18nDir, this.config.sourceLanguage);
494
+
495
+ console.log(this.t('usage.foundTranslationFiles', { count: translationFiles.length }));
496
+
497
+ for (const fileInfo of translationFiles) {
498
+ try {
499
+ await SecurityUtils.validatePath(fileInfo.filePath);
500
+ const content = await SecurityUtils.safeReadFile(fileInfo.filePath);
501
+ const jsonData = await SecurityUtils.safeParseJSON(content);
502
+
503
+ // Store file info for later analysis
504
+ this.translationFiles.set(fileInfo.filePath, fileInfo);
505
+
506
+ const fileKeys = this.extractKeysFromObject(jsonData, '', fileInfo.namespace);
507
+ fileKeys.forEach(key => keys.add(key));
508
+
509
+ console.log(` 📄 ${fileInfo.namespace}: ${fileKeys.length} keys`);
510
+ } catch (error) {
511
+ console.warn(this.t("checkUsage.failed_to_parse_filename_error", {
512
+ fileName: path.basename(fileInfo.filePath),
513
+ errorMessage: error.message
514
+ }));
515
+ await SecurityUtils.logSecurityEvent('translation_file_parse_error', {
516
+ component: 'i18ntk-usage',
517
+ file: fileInfo.filePath,
518
+ error: error.message
519
+ });
520
+ }
521
+ }
522
+ } catch (error) {
523
+ await SecurityUtils.logSecurityEvent('translation_keys_load_error', {
524
+ component: 'i18ntk-usage',
525
+ error: error.message
526
+ });
527
+ }
528
+
529
+ return keys;
530
+ }
531
+
532
+ // Extract keys recursively from translation object
533
+ extractKeysFromObject(obj, prefix = '', namespace = '') {
534
+ const keys = [];
535
+
536
+ for (const [key, value] of Object.entries(obj)) {
537
+ const fullKey = prefix ? `${prefix}.${key}` : key;
538
+
539
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
540
+ keys.push(...this.extractKeysFromObject(value, fullKey, namespace));
541
+ } else {
542
+ // Add dot notation key (e.g., "pagination.showing")
543
+ keys.push(fullKey);
544
+
545
+ // If we have a namespace, also add the namespace:key format
546
+ if (namespace && namespace !== 'root') {
547
+ keys.push(`${namespace}:${fullKey}`);
548
+ }
549
+ }
550
+ }
551
+
552
+ return keys;
553
+ }
554
+
555
+ // Extract translation keys from source code with enhanced patterns
556
+ extractKeysFromFile(filePath) {
557
+ try {
558
+ const content = SecurityUtils.safeReadFileSync(filePath);
559
+ if (!content) return [];
560
+
561
+ const keys = [];
562
+
563
+ // Ensure patterns are RegExp objects with better error handling
564
+ const patterns = this.config.translationPatterns.map(pattern => {
565
+ try {
566
+ if (typeof pattern === 'string') {
567
+ return new RegExp(pattern, 'g');
568
+ }
569
+ return new RegExp(pattern.source, 'g');
570
+ } catch (patternError) {
571
+ console.warn(`${this.t('usage.invalidPattern')} ${pattern}`);
572
+ return null;
573
+ }
574
+ }).filter(Boolean);
575
+
576
+ patterns.forEach(pattern => {
577
+ try {
578
+ let match;
579
+ let matchCount = 0;
580
+ const maxMatches = 10000; // Safety limit to prevent infinite loops
581
+
582
+ // Reset regex lastIndex to ensure clean start
583
+ pattern.lastIndex = 0;
584
+
585
+ while ((match = pattern.exec(content)) !== null && matchCount < maxMatches) {
586
+ if (match && match[1]) {
587
+ keys.push(match[1]);
588
+ }
589
+ matchCount++;
590
+
591
+ // Additional safety: if lastIndex doesn't advance, break to prevent infinite loop
592
+ if (pattern.lastIndex === 0) {
593
+ break;
594
+ }
595
+ }
596
+
597
+ if (matchCount >= maxMatches) {
598
+ console.warn(`${this.t('usage.patternMatchLimitReached')} ${filePath}`);
599
+ }
600
+ } catch (execError) {
601
+ // Skip patterns that fail to execute
602
+ console.warn(`${this.t('usage.patternExecutionFailed')} ${filePath}: ${execError.message}`);
603
+ }
604
+ });
605
+
606
+ return keys;
607
+ } catch (error) {
608
+ console.warn(`${this.t('usage.failedToExtractKeys')} ${filePath}: ${error.message}`);
609
+ return [];
610
+ }
611
+ }
612
+
613
+ // Analyze usage in source files
614
+ async analyzeUsage() {
615
+ try {
616
+ console.log(this.t('checkUsage.analyzing_source_files'));
617
+
618
+ // Check if source directory exists
619
+ if (!fs.existsSync(this.sourceDir)) {
620
+ throw new Error(`Source directory not found: ${this.sourceDir}`);
621
+ }
622
+
623
+ const sourceFiles = await this.getAllFiles(this.sourceDir);
624
+ console.log(this.t('checkUsage.found_files_in_source', { numFiles: sourceFiles.length }));
625
+
626
+ // If no files found, exit gracefully
627
+ if (sourceFiles.length === 0) {
628
+ console.warn(t('hardcodedTexts.noSourceFilesFound'));
629
+ return;
630
+ }
631
+
632
+ let totalKeysFound = 0;
633
+ let processedFiles = 0;
634
+
635
+ for (const filePath of sourceFiles) {
636
+ try {
637
+ const keys = this.extractKeysFromFile(filePath);
638
+
639
+ if (keys.length > 0) {
640
+ const relativePath = path.relative(this.sourceDir, filePath);
641
+ this.fileUsage.set(relativePath, keys);
642
+
643
+ keys.forEach(key => {
644
+ this.usedKeys.add(key);
645
+ totalKeysFound++;
646
+ });
647
+ }
648
+
649
+ processedFiles++;
650
+
651
+ // Progress indicator for large numbers of files
652
+ if (sourceFiles.length > 10 && processedFiles % Math.ceil(sourceFiles.length / 10) === 0) {
653
+ console.log(t('hardcodedTexts.processedFiles', { processedFiles, totalFiles: sourceFiles.length }));
654
+ }
655
+ } catch (fileError) {
656
+ console.warn(`${this.t('usage.failedToProcessFile')} ${filePath}: ${fileError.message}`);
657
+ continue;
658
+ }
659
+ }
660
+
661
+ console.log(this.t("checkUsage.found_thisusedkeyssize_unique_", { usedKeysSize: this.usedKeys.size }));
662
+ console.log(this.t("checkUsage.total_key_usages_totalkeysfoun", { totalKeysFound }));
663
+
664
+ } catch (error) {
665
+ console.error(`❌ Failed to analyze usage: ${error.message}`);
666
+ throw error;
667
+ }
668
+ }
669
+
670
+ // Load available translation keys
671
+ async loadAvailableKeys() {
672
+ console.log(this.t("checkUsage.loading_available_translation_"));
673
+
674
+ this.availableKeys = await this.getAllTranslationKeys();
675
+ console.log(this.t("checkUsage.found_thisavailablekeyssize_av", { availableKeysSize: this.availableKeys.size }));
676
+ }
677
+
678
+ // NEW: Analyze translation completeness across all languages
679
+ async analyzeTranslationCompleteness() {
680
+ try {
681
+ console.log('\n' + t('hardcodedTexts.analyzingTranslationCompleteness'));
682
+
683
+ // Check if i18n directory exists
684
+ if (!fs.existsSync(this.i18nDir)) {
685
+ console.warn(t('hardcodedTexts.i18nDirectoryNotFound', { i18nDir: this.i18nDir }));
686
+ return;
687
+ }
688
+
689
+ // Get all available languages
690
+ const languages = new Set();
691
+
692
+ try {
693
+ // Discover translation files for all languages
694
+ const allLanguageDirs = fs.readdirSync(this.i18nDir)
695
+ .filter(item => {
696
+ try {
697
+ const itemPath = path.join(this.i18nDir, item);
698
+ return fs.existsSync(itemPath) && fs.statSync(itemPath).isDirectory();
699
+ } catch (error) {
700
+ return false;
701
+ }
702
+ });
703
+
704
+ for (const lang of allLanguageDirs) {
705
+ if (['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(lang)) {
706
+ languages.add(lang);
707
+ }
708
+ }
709
+
710
+ // Also check for direct language files (en.json, de.json, etc.)
711
+ const directFiles = fs.readdirSync(this.i18nDir)
712
+ .filter(file => file.endsWith('.json'))
713
+ .map(file => path.basename(file, '.json'))
714
+ .filter(lang => ['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(lang));
715
+
716
+ directFiles.forEach(lang => languages.add(lang));
717
+ } catch (error) {
718
+ console.warn(`${this.t('usage.errorReadingI18nDirectory')} ${error.message}`);
719
+ return;
720
+ }
721
+
722
+ // If no languages found, exit gracefully
723
+ if (languages.size === 0) {
724
+ console.warn(t('hardcodedTexts.noTranslationLanguagesFound'));
725
+ return;
726
+ }
727
+
728
+ // Analyze each language
729
+ for (const language of languages) {
730
+ try {
731
+ const translationFiles = await this.discoverTranslationFiles(this.i18nDir, language);
732
+ let totalKeys = 0;
733
+ let translatedKeys = 0;
734
+
735
+ for (const fileInfo of translationFiles) {
736
+ try {
737
+ if (!fs.existsSync(fileInfo.filePath)) {
738
+ continue;
739
+ }
740
+
741
+ const content = await SecurityUtils.safeReadFile(fileInfo.filePath);
742
+ const jsonData = await SecurityUtils.safeParseJSON(content);
743
+
744
+ const stats = this.analyzeFileCompleteness(jsonData);
745
+ totalKeys += stats.total;
746
+ translatedKeys += stats.translated;
747
+ } catch (error) {
748
+ console.warn(t('hardcodedTexts.failedToAnalyzeFile', { filePath: fileInfo.filePath, error: error.message }));
749
+ continue;
750
+ }
751
+ }
752
+
753
+ this.translationStats.set(language, {
754
+ total: totalKeys,
755
+ translated: translatedKeys,
756
+ notTranslated: totalKeys - translatedKeys
757
+ });
758
+ } catch (error) {
759
+ console.warn(t('hardcodedTexts.failedToAnalyzeLanguage', { language, error: error.message }));
760
+ continue;
761
+ }
762
+ }
763
+ } catch (error) {
764
+ console.warn(t('hardcodedTexts.translationCompletenessAnalysisFailed', { error: error.message }));
765
+ // Don't throw error, just continue with the rest of the analysis
766
+ }
767
+ }
768
+
769
+ // NEW: Analyze completeness of a single translation file
770
+ analyzeFileCompleteness(obj) {
771
+ let total = 0;
772
+ let translated = 0;
773
+
774
+ const traverse = (current) => {
775
+ for (const [key, value] of Object.entries(current)) {
776
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
777
+ traverse(value);
778
+ } else {
779
+ total++;
780
+ if (value !== 'NOT_TRANSLATED' && value !== '(NOT TRANSLATED)' &&
781
+ value !== 'TRANSLATED' && value !== '(TRANSLATED)' &&
782
+ value && value.toString().trim() !== '') {
783
+ translated++;
784
+ }
785
+ }
786
+ }
787
+ };
788
+
789
+ traverse(obj);
790
+ return { total, translated };
791
+ }
792
+
793
+ // NEW: Get statistics about NOT_TRANSLATED values
794
+ getNotTranslatedStats() {
795
+ let total = 0;
796
+ const byLanguage = new Map();
797
+
798
+ for (const [language, stats] of this.translationStats) {
799
+ const notTranslated = stats.notTranslated;
800
+ total += notTranslated;
801
+ byLanguage.set(language, notTranslated);
802
+ }
803
+
804
+ return { total, byLanguage };
805
+ }
806
+
807
+ // Find unused keys
808
+ findUnusedKeys() {
809
+ const unused = [];
810
+
811
+ for (const key of this.availableKeys) {
812
+ let isUsed = false;
813
+
814
+ // Check exact match
815
+ if (this.usedKeys.has(key)) {
816
+ isUsed = true;
817
+ } else {
818
+ // Check if any dynamic key could match this
819
+ for (const usedKey of this.usedKeys) {
820
+ if (usedKey.endsWith('*')) {
821
+ const prefix = usedKey.slice(0, -1);
822
+ if (key.startsWith(prefix)) {
823
+ isUsed = true;
824
+ break;
825
+ }
826
+ }
827
+ }
828
+ }
829
+
830
+ if (!isUsed) {
831
+ unused.push(key);
832
+ }
833
+ }
834
+
835
+ return unused;
836
+ }
837
+
838
+ // Find missing keys (used but not available)
839
+ findMissingKeys() {
840
+ const missing = [];
841
+
842
+ for (const key of this.usedKeys) {
843
+ // Skip dynamic keys for missing check
844
+ if (key.endsWith('*')) {
845
+ continue;
846
+ }
847
+
848
+ if (!this.availableKeys.has(key)) {
849
+ missing.push(key);
850
+ }
851
+ }
852
+
853
+ return missing;
854
+ }
855
+
856
+ // Find files that use specific keys
857
+ findKeyUsage(searchKey) {
858
+ const usage = [];
859
+
860
+ for (const [filePath, keys] of this.fileUsage) {
861
+ const matchingKeys = keys.filter(key => {
862
+ if (key.endsWith('*')) {
863
+ const prefix = key.slice(0, -1);
864
+ return searchKey.startsWith(prefix);
865
+ }
866
+ return key === searchKey;
867
+ });
868
+
869
+ if (matchingKeys.length > 0) {
870
+ usage.push({ filePath, keys: matchingKeys });
871
+ }
872
+ }
873
+
874
+ return usage;
875
+ }
876
+
877
+ // Enhanced usage report generation
878
+ generateUsageReport() {
879
+ const unusedKeys = this.findUnusedKeys();
880
+ const missingKeys = this.findMissingKeys();
881
+ const dynamicKeys = Array.from(this.usedKeys).filter(key => key.endsWith('*'));
882
+ const notTranslatedStats = this.getNotTranslatedStats();
883
+
884
+ const timestamp = new Date().toISOString();
885
+
886
+ let report = `${this.t('summary.usageReportTitle')}\n`;
887
+ report += `${this.t('summary.usageReportGenerated', { timestamp })}\n`;
888
+ report += `${this.t('summary.usageReportSourceDir', { sourceDir: this.sourceDir })}\n`;
889
+ report += `${this.t('summary.usageReportI18nDir', { i18nDir: this.i18nDir })}\n\n`;
890
+
891
+ // Summary
892
+ report += `${this.t('summary.usageReportSummary')}\n`;
893
+ report += `${'='.repeat(50)}\n`;
894
+ report += `${this.t('summary.usageReportSourceFilesScanned', { count: this.fileUsage.size })}\n`;
895
+ report += `${this.t('summary.usageReportTranslationFilesFound', { count: this.translationFiles.size })}\n`;
896
+ report += `${this.t('summary.usageReportAvailableKeys', { count: this.availableKeys.size })}\n`;
897
+ report += `${this.t('summary.usageReportUsedKeys', { count: this.usedKeys.size - dynamicKeys.length })}\n`;
898
+ report += `${this.t('summary.usageReportDynamicKeys', { count: dynamicKeys.length })}\n`;
899
+ report += `${this.t('summary.usageReportUnusedKeys', { count: unusedKeys.length })}\n`;
900
+ report += `${this.t('summary.usageReportMissingKeys', { count: missingKeys.length })}\n`;
901
+ report += `${this.t('summary.usageReportNotTranslatedKeys', { count: notTranslatedStats.total })}\n\n`;
902
+
903
+ // Translation completeness
904
+ report += `${this.t('summary.usageReportTranslationCompleteness')}\n`;
905
+ report += `${'='.repeat(50)}\n`;
906
+ for (const [language, stats] of this.translationStats) {
907
+ const completeness = ((stats.translated / stats.total) * 100).toFixed(1);
908
+ report += `${this.t('summary.usageReportLanguageCompleteness', { language: language.toUpperCase(), completeness, translated: stats.translated, total: stats.total })}\n`;
909
+ if (stats.notTranslated > 0) {
910
+ report += `${this.t('summary.usageReportNotTranslatedInLanguage', { count: stats.notTranslated })}\n`;
911
+ }
912
+ }
913
+ report += `\n`;
914
+
915
+ // Translation files discovered
916
+ report += `${this.t('summary.usageReportTranslationFilesDiscovered')}\n`;
917
+ report += `${'='.repeat(50)}\n`;
918
+ for (const [filePath, fileInfo] of this.translationFiles) {
919
+ const relativePath = path.relative(this.i18nDir, filePath);
920
+ report += `${this.t('summary.usageReportFileInfo', { relativePath, namespace: fileInfo.namespace, type: fileInfo.type })}\n`;
921
+ }
922
+ report += `\n`;
923
+
924
+ // Unused keys
925
+ if (unusedKeys.length > 0) {
926
+ report += `${this.t('summary.usageReportUnusedTranslationKeys')}\n`;
927
+ report += `${'='.repeat(50)}\n`;
928
+ report += `${this.t('summary.usageReportUnusedKeysDescription')}\n\n`;
929
+
930
+ unusedKeys.slice(0, 100).forEach(key => {
931
+ report += `${this.t('summary.usageReportUnusedKey', { key })}\n`;
932
+ });
933
+
934
+ if (unusedKeys.length > 100) {
935
+ report += `${this.t('summary.usageReportMoreUnusedKeys', { count: unusedKeys.length - 100 })}\n`;
936
+ }
937
+
938
+ report += `\n`;
939
+ }
940
+
941
+ // Missing keys
942
+ if (missingKeys.length > 0) {
943
+ report += `${this.t('summary.usageReportMissingTranslationKeys')}\n`;
944
+ report += `${'='.repeat(50)}\n`;
945
+ report += `${this.t('summary.usageReportMissingKeysDescription')}\n\n`;
946
+
947
+ missingKeys.forEach(key => {
948
+ report += `${this.t('summary.usageReportMissingKey', { key })}\n`;
949
+
950
+ // Show where it's used
951
+ const usage = this.findKeyUsage(key);
952
+ usage.slice(0, 3).forEach(({ filePath }) => {
953
+ report += ` ${this.t('summary.usageReportUsedIn', { filePath })}\n`;
954
+ });
955
+
956
+ if (usage.length > 3) {
957
+ report += ` ${this.t('summary.usageReportMoreFiles', { count: usage.length - 3 })}\n`;
958
+ }
959
+
960
+ report += `\n`;
961
+ });
962
+ }
963
+
964
+ // Dynamic keys
965
+ if (dynamicKeys.length > 0) {
966
+ report += `${this.t('summary.usageReportDynamicTranslationKeys')}\n`;
967
+ report += `${'='.repeat(50)}\n`;
968
+ report += `${this.t('summary.usageReportDynamicKeysDescription')}\n\n`;
969
+
970
+ dynamicKeys.forEach(key => {
971
+ report += `${this.t('summary.usageReportDynamicKey', { key })}\n`;
972
+
973
+ // Show where it's used
974
+ const usage = this.findKeyUsage(key);
975
+ usage.slice(0, 2).forEach(({ filePath }) => {
976
+ report += ` ${this.t('summary.usageReportUsedIn', { filePath })}\n`;
977
+ });
978
+
979
+ report += `\n`;
980
+ });
981
+ }
982
+
983
+ // File usage breakdown
984
+ report += `${this.t('summary.usageReportFileUsageBreakdown')}\n`;
985
+ report += `${'='.repeat(50)}\n`;
986
+
987
+ const sortedFiles = Array.from(this.fileUsage.entries())
988
+ .sort(([,a], [,b]) => b.length - a.length)
989
+ .slice(0, 20);
990
+
991
+ sortedFiles.forEach(([filePath, keys]) => {
992
+ report += `${this.t('summary.usageReportFileUsage', { filePath, count: keys.length })}\n`;
993
+ });
994
+
995
+ if (this.fileUsage.size > 20) {
996
+ report += `${this.t('summary.usageReportMoreFiles', { count: this.fileUsage.size - 20 })}\n`;
997
+ }
998
+
999
+ return report;
1000
+ }
1001
+
1002
+ // Save report to file
1003
+ async saveReport(report, outputDir = './i18ntk-reports/usage') {
1004
+ try {
1005
+ // Ensure output directory exists
1006
+ if (!fs.existsSync(outputDir)) {
1007
+ fs.mkdirSync(outputDir, { recursive: true });
1008
+ }
1009
+
1010
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1011
+ const filename = `usage-analysis-${timestamp}.txt`;
1012
+ const filepath = path.join(outputDir, filename);
1013
+
1014
+ await SecurityUtils.safeWriteFile(filepath, report);
1015
+ console.log(this.t('usage.reportSavedTo', { reportPath: filepath }));
1016
+ return filepath;
1017
+ } catch (error) {
1018
+ console.error(this.t('usage.failedToSaveReport', { error: error.message }));
1019
+ }
1020
+ }
1021
+
1022
+ // Main analysis process
1023
+ async analyze() {
1024
+ try {
1025
+ // Initialize if not already done
1026
+ if (!this.sourceDir || !this.t) {
1027
+ await this.initialize();
1028
+ }
1029
+
1030
+ await SecurityUtils.logSecurityEvent('analysis_started', { component: 'i18ntk-usage' });
1031
+
1032
+ console.log(this.t('checkUsage.title'));
1033
+ console.log(this.t("checkUsage.message"));
1034
+
1035
+ // Parse command line arguments
1036
+ const args = await this.parseArgs();
1037
+
1038
+ // Show help if requested
1039
+ if (args.help) {
1040
+ this.showHelp();
1041
+ return { success: true, help: true };
1042
+ }
1043
+
1044
+ if (args.sourceDir) {
1045
+ this.config.sourceDir = args.sourceDir;
1046
+ this.sourceDir = path.resolve(this.config.sourceDir);
1047
+ }
1048
+ if (args.i18nDir) {
1049
+ this.config.i18nDir = args.i18nDir;
1050
+ this.i18nDir = path.resolve(this.config.i18nDir);
1051
+ this.sourceLanguageDir = path.join(this.i18nDir, this.config.sourceLanguage);
1052
+ }
1053
+ if (args.outputDir) {
1054
+ this.config.outputDir = args.outputDir;
1055
+ this.outputDir = path.resolve(this.config.outputDir);
1056
+ }
1057
+
1058
+ console.log(this.t("checkUsage.source_directory_thissourcedir", { sourceDir: this.sourceDir }));
1059
+ console.log(this.t("checkUsage.i18n_directory_thisi18ndir", { i18nDir: this.i18nDir }));
1060
+
1061
+ // Validate directories
1062
+ await SecurityUtils.validatePath(this.sourceDir);
1063
+ await SecurityUtils.validatePath(this.i18nDir);
1064
+
1065
+ if (!fs.existsSync(this.sourceDir)) {
1066
+ throw new Error(`Source directory not found: ${this.sourceDir}`);
1067
+ }
1068
+
1069
+ if (!fs.existsSync(this.i18nDir)) {
1070
+ throw new Error(`I18n directory not found: ${this.i18nDir}`);
1071
+ }
1072
+
1073
+ // Load available keys
1074
+ await this.loadAvailableKeys();
1075
+
1076
+ // Analyze usage
1077
+ await this.analyzeUsage();
1078
+
1079
+ // NEW: Analyze translation completeness
1080
+ await this.analyzeTranslationCompleteness();
1081
+
1082
+ // Generate analysis results
1083
+ const unusedKeys = this.findUnusedKeys();
1084
+ const missingKeys = this.findMissingKeys();
1085
+ const dynamicKeys = Array.from(this.usedKeys).filter(key => key.endsWith('*'));
1086
+ const notTranslatedStats = this.getNotTranslatedStats();
1087
+
1088
+ // Display results
1089
+ console.log(this.t("checkUsage.n"));
1090
+ console.log(this.t("checkUsage.usage_analysis_results"));
1091
+ console.log(this.t("checkUsage.message"));
1092
+
1093
+ console.log(this.t("checkUsage.source_files_scanned_thisfileu", { fileUsageSize: this.fileUsage.size }));
1094
+ console.log(this.t("checkUsage.available_translation_keys_thi", { availableKeysSize: this.availableKeys.size }));
1095
+ console.log(this.t("checkUsage.used_translation_keys_thisused", { usedKeysSize: this.usedKeys.size - dynamicKeys.length }));
1096
+ console.log(this.t("checkUsage.dynamic_keys_detected_dynamick", { dynamicKeysLength: dynamicKeys.length }));
1097
+ console.log(this.t("checkUsage.unused_keys_unusedkeyslength", { unusedKeysLength: unusedKeys.length }));
1098
+ console.log(this.t("checkUsage.missing_keys_missingkeyslength", { missingKeysLength: missingKeys.length }));
1099
+ console.log(this.t('usage.notTranslatedKeysTotal', { total: notTranslatedStats.total }));
1100
+
1101
+ // Removed redundant hardcoded console output to avoid duplication
1102
+ // The translation completeness and not translated keys count are already logged below
1103
+
1104
+
1105
+ // Display translation completeness
1106
+ console.log(this.t("checkUsage.translation_completeness_title"));
1107
+ for (const [language, stats] of this.translationStats) {
1108
+ const completeness = ((stats.translated / stats.total) * 100).toFixed(1);
1109
+ console.log(this.t("checkUsage.language_completeness_stats", {
1110
+ language: language.toUpperCase(),
1111
+ completeness,
1112
+ translated: stats.translated,
1113
+ total: stats.total
1114
+ }));
1115
+ }
1116
+
1117
+ // Show some examples
1118
+ if (unusedKeys.length > 0) {
1119
+ console.log(this.t("checkUsage.n_sample_unused_keys"));
1120
+ unusedKeys.slice(0, 5).forEach(key => {
1121
+ console.log(this.t("checkUsage.key", { key }));
1122
+ });
1123
+ if (unusedKeys.length > 5) {
1124
+ console.log(this.t("checkUsage.and_unusedkeyslength_5_more", { count: unusedKeys.length - 5 }));
1125
+ }
1126
+ }
1127
+
1128
+ if (missingKeys.length > 0) {
1129
+ console.log(this.t("checkUsage.n_sample_missing_keys"));
1130
+ missingKeys.slice(0, 5).forEach(key => {
1131
+ console.log(this.t("checkUsage.key", { key }));
1132
+ });
1133
+ if (missingKeys.length > 5) {
1134
+ console.log(this.t("checkUsage.and_missingkeyslength_5_more", { count: missingKeys.length - 5 }));
1135
+ }
1136
+ }
1137
+
1138
+ // Generate and save report if requested
1139
+ if (args.outputReport) {
1140
+ console.log(this.t("checkUsage.n_generating_detailed_report"));
1141
+ const report = this.generateUsageReport();
1142
+ const reportPath = await this.saveReport(report);
1143
+ console.log(this.t("checkUsage.report_saved_reportpath", { reportPath }));
1144
+ }
1145
+
1146
+ // Recommendations
1147
+ console.log(this.t("checkUsage.n_recommendations"));
1148
+ console.log(this.t("checkUsage.message"));
1149
+
1150
+ if (unusedKeys.length > 0) {
1151
+ console.log(this.t("checkUsage.consider_removing_unused_trans"));
1152
+ }
1153
+
1154
+ if (missingKeys.length > 0) {
1155
+ console.log(this.t("checkUsage.add_missing_translation_keys_t"));
1156
+ }
1157
+
1158
+ if (dynamicKeys.length > 0) {
1159
+ console.log(this.t("checkUsage.review_dynamic_keys_manually_t"));
1160
+ }
1161
+
1162
+ if (notTranslatedStats.total > 0) {
1163
+ console.log(this.t('usage.reviewNotTranslatedKeys', { total: notTranslatedStats.total }));
1164
+ }
1165
+
1166
+ if (unusedKeys.length === 0 && missingKeys.length === 0 && notTranslatedStats.total === 0) {
1167
+ console.log(this.t("checkUsage.all_translation_keys_are_prope"));
1168
+ }
1169
+
1170
+ console.log(this.t("checkUsage.n_next_steps"));
1171
+ console.log(this.t("checkUsage.1_review_the_analysis_results"));
1172
+ if (args.outputReport) {
1173
+ console.log(this.t("checkUsage.2_check_the_detailed_report_fo"));
1174
+ } else {
1175
+ console.log(this.t("checkUsage.2_run_with_outputreport_for_de"));
1176
+ }
1177
+ console.log(this.t("checkUsage.3_remove_unused_keys_or_add_mi"));
1178
+ console.log(this.t("checkUsage.4_rerun_analysis_to_verify_imp"));
1179
+
1180
+ await SecurityUtils.logSecurityEvent('analysis_completed', {
1181
+ component: 'i18ntk-usage',
1182
+ stats: {
1183
+ availableKeys: this.availableKeys.size,
1184
+ usedKeys: this.usedKeys.size - dynamicKeys.length,
1185
+ dynamicKeys: dynamicKeys.length,
1186
+ unusedKeys: unusedKeys.length,
1187
+ missingKeys: missingKeys.length,
1188
+ filesScanned: this.fileUsage.size,
1189
+ notTranslatedKeys: notTranslatedStats.total
1190
+ }
1191
+ });
1192
+
1193
+ // Close readline interface to prevent hanging
1194
+ this.closeReadline();
1195
+
1196
+ return {
1197
+ success: true,
1198
+ stats: {
1199
+ availableKeys: this.availableKeys.size,
1200
+ usedKeys: this.usedKeys.size - dynamicKeys.length,
1201
+ dynamicKeys: dynamicKeys.length,
1202
+ unusedKeys: unusedKeys.length,
1203
+ missingKeys: missingKeys.length,
1204
+ filesScanned: this.fileUsage.size,
1205
+ notTranslatedKeys: notTranslatedStats.total,
1206
+ translationCompleteness: Object.fromEntries(this.translationStats)
1207
+ },
1208
+ unusedKeys,
1209
+ missingKeys,
1210
+ dynamicKeys,
1211
+ notTranslatedStats
1212
+ };
1213
+
1214
+ } catch (error) {
1215
+ console.error(this.t("checkUsage.usage_analysis_failed"));
1216
+ console.error(error.message);
1217
+
1218
+ await SecurityUtils.logSecurityEvent('analysis_failed', {
1219
+ component: 'i18ntk-usage',
1220
+ error: error.message
1221
+ });
1222
+
1223
+ // Close readline interface to prevent hanging
1224
+ this.closeReadline();
1225
+
1226
+ return {
1227
+ success: false,
1228
+ error: error.message
1229
+ };
1230
+ }
1231
+ }
1232
+ }
1233
+
1234
+ // Run if called directly
1235
+ if (require.main === module) {
1236
+ const analyzer = new I18nUsageAnalyzer();
1237
+
1238
+ // Check if we're being called from the menu system (stdin has data)
1239
+ // In that case, we should run with default settings without prompting
1240
+ const hasStdinData = !process.stdin.isTTY;
1241
+
1242
+ if (hasStdinData) {
1243
+ // When called from menu, consume stdin data and run with defaults
1244
+ process.stdin.resume();
1245
+ process.stdin.on('data', () => {});
1246
+ process.stdin.on('end', () => {
1247
+ // Run analysis with default settings (no prompts)
1248
+ analyzer.analyze()
1249
+ .then((result) => {
1250
+ if (result.success) {
1251
+ console.log(analyzer.t('usage.analysisCompletedSuccessfully'));
1252
+ process.exit(0);
1253
+ } else {
1254
+ console.error(analyzer.t('usage.analysisFailed', { error: result.error }));
1255
+ process.exit(1);
1256
+ }
1257
+ })
1258
+ .catch((error) => {
1259
+ console.error(analyzer.t('usage.analysisFailed', { error: error.message }));
1260
+ process.exit(1);
1261
+ });
1262
+ });
1263
+ } else {
1264
+ // Normal direct execution
1265
+ analyzer.analyze()
1266
+ .then((result) => {
1267
+ if (result.success) {
1268
+ console.log('\n' + analyzer.t('usage.analysisCompletedSuccessfully'));
1269
+ process.exit(0);
1270
+ } else {
1271
+ console.error('\n' + analyzer.t('usage.analysisFailed', { error: result.error }));
1272
+ process.exit(1);
1273
+ }
1274
+ })
1275
+ .catch((error) => {
1276
+ console.error('\n' + analyzer.t('usage.analysisFailed', { error: error.message }));
1277
+ process.exit(1);
1278
+ });
1279
+ }
1280
+ }
1281
+
1282
+ module.exports = I18nUsageAnalyzer;