i18ntk 1.10.2 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +141 -1191
  3. package/main/i18ntk-analyze.js +65 -84
  4. package/main/i18ntk-backup-class.js +420 -0
  5. package/main/i18ntk-backup.js +3 -3
  6. package/main/i18ntk-complete.js +90 -65
  7. package/main/i18ntk-doctor.js +123 -103
  8. package/main/i18ntk-fixer.js +61 -725
  9. package/main/i18ntk-go.js +14 -15
  10. package/main/i18ntk-init.js +77 -26
  11. package/main/i18ntk-java.js +27 -32
  12. package/main/i18ntk-js.js +70 -68
  13. package/main/i18ntk-manage.js +129 -30
  14. package/main/i18ntk-php.js +75 -75
  15. package/main/i18ntk-py.js +55 -56
  16. package/main/i18ntk-scanner.js +59 -57
  17. package/main/i18ntk-setup.js +9 -404
  18. package/main/i18ntk-sizing.js +6 -6
  19. package/main/i18ntk-summary.js +21 -18
  20. package/main/i18ntk-ui.js +11 -10
  21. package/main/i18ntk-usage.js +54 -18
  22. package/main/i18ntk-validate.js +13 -13
  23. package/main/manage/commands/AnalyzeCommand.js +1124 -0
  24. package/main/manage/commands/BackupCommand.js +62 -0
  25. package/main/manage/commands/CommandRouter.js +295 -0
  26. package/main/manage/commands/CompleteCommand.js +61 -0
  27. package/main/manage/commands/DoctorCommand.js +60 -0
  28. package/main/manage/commands/FixerCommand.js +624 -0
  29. package/main/manage/commands/InitCommand.js +62 -0
  30. package/main/manage/commands/ScannerCommand.js +654 -0
  31. package/main/manage/commands/SizingCommand.js +60 -0
  32. package/main/manage/commands/SummaryCommand.js +61 -0
  33. package/main/manage/commands/UsageCommand.js +60 -0
  34. package/main/manage/commands/ValidateCommand.js +978 -0
  35. package/main/manage/index-fixed.js +1447 -0
  36. package/main/manage/index.js +1462 -0
  37. package/main/manage/managers/DebugMenu.js +140 -0
  38. package/main/manage/managers/InteractiveMenu.js +177 -0
  39. package/main/manage/managers/LanguageMenu.js +62 -0
  40. package/main/manage/managers/SettingsMenu.js +53 -0
  41. package/main/manage/services/AuthenticationService.js +263 -0
  42. package/main/manage/services/ConfigurationService-fixed.js +449 -0
  43. package/main/manage/services/ConfigurationService.js +449 -0
  44. package/main/manage/services/FileManagementService.js +368 -0
  45. package/main/manage/services/FrameworkDetectionService.js +458 -0
  46. package/main/manage/services/InitService.js +1051 -0
  47. package/main/manage/services/SetupService.js +462 -0
  48. package/main/manage/services/SummaryService.js +450 -0
  49. package/main/manage/services/UsageService.js +1502 -0
  50. package/package.json +32 -29
  51. package/runtime/enhanced.d.ts +221 -221
  52. package/runtime/index.d.ts +29 -29
  53. package/runtime/index.full.d.ts +331 -331
  54. package/runtime/index.js +7 -6
  55. package/scripts/build-lite.js +17 -17
  56. package/scripts/deprecate-versions.js +23 -6
  57. package/scripts/export-translations.js +5 -5
  58. package/scripts/fix-all-i18n.js +3 -3
  59. package/scripts/fix-and-purify-i18n.js +3 -2
  60. package/scripts/fix-locale-control-chars.js +110 -0
  61. package/scripts/lint-locales.js +80 -0
  62. package/scripts/locale-optimizer.js +8 -8
  63. package/scripts/prepublish.js +21 -21
  64. package/scripts/security-check.js +117 -117
  65. package/scripts/sync-translations.js +4 -4
  66. package/scripts/sync-ui-locales.js +9 -8
  67. package/scripts/validate-all-translations.js +8 -7
  68. package/scripts/verify-deprecations.js +157 -161
  69. package/scripts/verify-translations.js +6 -5
  70. package/settings/i18ntk-config.json +282 -282
  71. package/settings/language-config.json +5 -5
  72. package/settings/settings-cli.js +9 -9
  73. package/settings/settings-manager.js +18 -18
  74. package/ui-locales/de.json +2417 -2348
  75. package/ui-locales/en.json +2415 -2352
  76. package/ui-locales/es.json +2425 -2353
  77. package/ui-locales/fr.json +2418 -2348
  78. package/ui-locales/ja.json +2463 -2361
  79. package/ui-locales/ru.json +2463 -2359
  80. package/ui-locales/zh.json +2418 -2351
  81. package/utils/admin-auth.js +2 -2
  82. package/utils/admin-cli.js +297 -297
  83. package/utils/admin-pin.js +9 -9
  84. package/utils/cli-helper.js +9 -9
  85. package/utils/config-helper.js +73 -104
  86. package/utils/config-manager.js +204 -171
  87. package/utils/config.js +5 -4
  88. package/utils/env-manager.js +249 -263
  89. package/utils/framework-detector.js +27 -24
  90. package/utils/i18n-helper.js +85 -41
  91. package/utils/init-helper.js +152 -94
  92. package/utils/json-output.js +98 -98
  93. package/utils/mini-commander.js +179 -0
  94. package/utils/missing-key-validator.js +5 -5
  95. package/utils/plugin-loader.js +40 -29
  96. package/utils/prompt.js +14 -44
  97. package/utils/safe-json.js +40 -0
  98. package/utils/secure-errors.js +3 -3
  99. package/utils/security-check-improved.js +390 -0
  100. package/utils/security-config.js +5 -5
  101. package/utils/security-fixed.js +607 -0
  102. package/utils/security.js +652 -602
  103. package/utils/setup-enforcer.js +136 -44
  104. package/utils/setup-validator.js +33 -32
  105. package/utils/ultra-performance-optimizer.js +11 -9
  106. package/utils/watch-locales.js +2 -1
  107. package/utils/prompt-fixed.js +0 -55
  108. package/utils/security-check.js +0 -454
@@ -0,0 +1,1502 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * I18NTK USAGE SERVICE
5
+ *
6
+ * Core business logic for i18n usage analysis and translation key management.
7
+ * Handles source code scanning, translation completeness analysis, and reporting.
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { loadTranslations, t } = require('../../../utils/i18n-helper');
13
+ const { getGlobalReadline, closeGlobalReadline, askHidden } = require('../../../utils/cli');
14
+ const { detectFramework } = require('../../../utils/framework-detector');
15
+ const { getExtractor } = require('../../../utils/extractor-manager');
16
+ const configManager = require('../../../utils/config-manager');
17
+ const SecurityUtils = require('../../../utils/security');
18
+ const AdminCLI = require('../../../utils/admin-cli');
19
+ const SettingsManager = require('../../../settings/settings-manager');
20
+ const settingsManager = new SettingsManager();
21
+ const { getUnifiedConfig, parseCommonArgs, displayHelp, validateSourceDir, displayPaths } = require('../../../utils/config-helper');
22
+ const I18nInitializer = require('../../i18ntk-init');
23
+ const JsonOutput = require('../../../utils/json-output');
24
+ const SetupEnforcer = require('../../../utils/setup-enforcer');
25
+
26
+ class UsageService {
27
+ constructor(config = {}) {
28
+ this.config = config;
29
+ this.sourceDir = null;
30
+ this.i18nDir = null;
31
+ this.sourceLanguageDir = null;
32
+
33
+ // Initialize class properties
34
+ this.availableKeys = new Set();
35
+ this.usedKeys = new Set();
36
+ this.fileUsage = new Map();
37
+ this.translationFiles = new Map(); // Track all translation files
38
+ this.translationStats = new Map(); // Track translation completeness
39
+ this.extractor = getExtractor(config.extractor);
40
+ this.placeholderKeys = new Set();
41
+ this.placeholderStyles = settingsManager.getDefaultSettings().placeholderStyles || {};
42
+
43
+ // Enhanced analysis properties
44
+ this.frameworkUsage = new Map(); // Track framework usage per file
45
+ this.keyComplexity = new Map(); // Track key complexity analysis
46
+ this.startTime = Date.now(); // Track performance metrics
47
+ this.version = '1.10.1'; // Version tracking
48
+
49
+ // Use global translation function
50
+ this.rl = null;
51
+ }
52
+
53
+ // Initialize readline interface
54
+ initReadline() {
55
+ if (!this.rl) {
56
+ return getGlobalReadline();
57
+ }
58
+ }
59
+
60
+ // Close readline interface
61
+ closeReadline() {
62
+ const { closeGlobalReadline } = require('../../../utils/cli');
63
+ closeGlobalReadline();
64
+ }
65
+
66
+ // Prompt for user input
67
+ async prompt(question) {
68
+ const rl = getGlobalReadline();
69
+ return new Promise(resolve => {
70
+ rl.question(question, (answer) => {
71
+ resolve(answer);
72
+ });
73
+ });
74
+ }
75
+
76
+ async initialize() {
77
+ try {
78
+ const cliArgs = parseCommonArgs(process.argv.slice(2));
79
+ const defaultConfig = await getUnifiedConfig('usage', cliArgs);
80
+ this.config = { ...defaultConfig, ...(this.config || {}) };
81
+
82
+ // Load translations for UI
83
+ const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
84
+ loadTranslations(uiLanguage, path.resolve(__dirname, '..', '..', '..', 'resources', 'i18n', 'ui-locales'));
85
+ const projectRoot = path.resolve(this.config.projectRoot || '.');
86
+ const detected = detectFramework(projectRoot);
87
+ if (detected) {
88
+ this.config.translationPatterns = detected.patterns;
89
+ if (!this.config.includeExtensions) {
90
+ this.config.includeExtensions = ['.js', '.jsx', '.ts', '.tsx', '.py', '.pyx', '.pyi'];
91
+ }
92
+ if (!this.config.excludeDirs) {
93
+ this.config.excludeDirs = [];
94
+ }
95
+ }
96
+ this.sourceDir = this.config.sourceDir;
97
+ this.i18nDir = this.config.i18nDir;
98
+ this.sourceLanguageDir = path.join(this.i18nDir, this.config.sourceLanguage);
99
+
100
+ if (!SecurityUtils.safeExistsSync(this.i18nDir, process.cwd())) {
101
+ console.warn(t('usage.i18nDirectoryNotFound', { i18nDir: this.i18nDir }));
102
+ this.i18nDir = this.sourceDir;
103
+ this.config.i18nDir = this.i18nDir;
104
+ await configManager.updateConfig({ i18nDir: configManager.toRelative(this.sourceDir) });
105
+ this.sourceLanguageDir = path.join(this.i18nDir, this.config.sourceLanguage);
106
+ }
107
+
108
+ displayPaths({ sourceDir: this.sourceDir, i18nDir: this.i18nDir, outputDir: this.config.outputDir });
109
+
110
+
111
+ // Ensure translation patterns are defined
112
+ this.config = this.config || {};
113
+ this.config.translationPatterns = this.config.translationPatterns || [
114
+ /t\(['"`]([^'"`]+)['"`]/g,
115
+ /i18n\.t\(['"`]([^'"`]+)['"`]/g,
116
+ /useTranslation\(\)\.t\(['"`]([^'"`]+)['"`]/g,
117
+ /t\(`([^`]+)`\)/g,
118
+ /i18nKey=['"`]([^'"`]+)['"`]/g,
119
+ /\$t\(['"`]([^'"`]+)['"`]/g,
120
+ /getTranslation\(['"`]([^'"`]+)['"`]/g
121
+ ];
122
+ this.extractor = getExtractor(this.config.extractor);
123
+
124
+ // Ensure defaults for other config values
125
+ this.config = this.config || {};
126
+ if (!Array.isArray(this.config.excludeDirs)) {
127
+ this.config.excludeDirs = ['node_modules', '.git'];
128
+ }
129
+ if (!Array.isArray(this.config.includeExtensions) && !Array.isArray(this.config.supportedExtensions)) {
130
+ this.config.includeExtensions = ['.js', '.jsx', '.ts', '.tsx', '.py', '.pyx', '.pyi'];
131
+ }
132
+
133
+ await SecurityUtils.logSecurityEvent(t('usage.analyzerInitialized'), { component: 'i18ntk-usage' });
134
+ } catch (error) {
135
+ await SecurityUtils.logSecurityEvent(t('usage.analyzerInitFailed'), { component: 'i18ntk-usage', error: error.message });
136
+ throw error;
137
+ }
138
+ }
139
+
140
+ // Normalize CLI arguments to handle both camelCase and hyphenated flags
141
+ normalizeArgs(a) {
142
+ return {
143
+ sourceDir: a.sourceDir ?? a['source-dir'],
144
+ i18nDir: a.i18nDir ?? a['i18n-dir'],
145
+ outputReport: a.outputReport ?? a['output-report'],
146
+ outputDir: a.outputDir ?? a['output-dir'],
147
+ uiLanguage: a.uiLanguage ?? a['ui-language'],
148
+ help: a.help || a.h,
149
+ noPrompt: a.noPrompt ?? a['no-prompt'],
150
+ strict: a.strict,
151
+ debug: a.debug
152
+ };
153
+ }
154
+
155
+ // Parse command line arguments
156
+ async parseArgs() {
157
+ const args = process.argv.slice(2);
158
+ const parsed = {};
159
+
160
+ for (const arg of args) {
161
+ if (arg.startsWith('--')) {
162
+ const [key, value] = arg.substring(2).split('=');
163
+ if (value !== undefined) {
164
+ parsed[key] = value;
165
+ } else {
166
+ parsed[key] = true;
167
+ }
168
+ } else if (arg.startsWith('-')) {
169
+ const key = arg.substring(1);
170
+ parsed[key] = true;
171
+ }
172
+ }
173
+
174
+ return this.normalizeArgs(parsed);
175
+ }
176
+
177
+ // Recursively discover all translation files in modular structure
178
+ async discoverTranslationFiles(baseDir, language = (this.config && this.config.sourceLanguage) || 'en') {
179
+ const translationFiles = [];
180
+
181
+ const traverse = async (currentDir) => {
182
+ try {
183
+ const absoluteDir = path.resolve(currentDir);
184
+ const validatedPath = SecurityUtils.validatePath(absoluteDir, process.cwd());
185
+
186
+ if (!validatedPath || !SecurityUtils.safeExistsSync(validatedPath)) {
187
+ return;
188
+ }
189
+
190
+ const items = fs.readdirSync(validatedPath);
191
+
192
+ for (const item of items) {
193
+ const itemPath = path.join(validatedPath, item);
194
+
195
+ try {
196
+ const stat = fs.statSync(itemPath);
197
+
198
+ if (stat.isDirectory()) {
199
+ // Skip excluded directories with null-safety
200
+ const excludes = Array.isArray(this.config.excludeDirs) ? this.config.excludeDirs : [];
201
+ if (!excludes.includes(item)) {
202
+ await traverse(itemPath);
203
+ }
204
+ } else if (stat.isFile()) {
205
+ // Look for translation files:
206
+ // 1. Direct language files: en.json, de.json, etc.
207
+ // 2. Language directory files: en/common.json, de/auth.json, etc.
208
+ // 3. Nested modular files: components/en.json, features/auth/en.json, etc.
209
+
210
+ const fileName = path.basename(item, '.json');
211
+ const parentDir = path.basename(path.dirname(itemPath));
212
+
213
+ if (item.endsWith('.json')) {
214
+ // Case 1: Direct language files (en.json)
215
+ if (fileName === language) {
216
+ translationFiles.push({
217
+ filePath: itemPath,
218
+ namespace: path.relative(baseDir, path.dirname(itemPath)).replace(/[\\/]/g, '.') || 'root',
219
+ language: language,
220
+ type: 'direct'
221
+ });
222
+ }
223
+ // Case 2: Files in language directories (en/common.json)
224
+ else if (parentDir === language) {
225
+ translationFiles.push({
226
+ filePath: itemPath,
227
+ namespace: fileName,
228
+ language: language,
229
+ type: 'namespaced'
230
+ });
231
+ }
232
+ }
233
+ }
234
+ } catch (statError) {
235
+ // Skip files that can't be accessed
236
+ continue;
237
+ }
238
+ }
239
+ } catch (error) {
240
+ await SecurityUtils.logSecurityEvent(t('usage.translationDiscoveryError'), {
241
+ component: 'i18ntk-usage',
242
+ directory: currentDir,
243
+ error: error.message
244
+ });
245
+ }
246
+ };
247
+
248
+ await traverse(baseDir);
249
+ return translationFiles;
250
+ }
251
+
252
+ // Get all files recursively from a directory with enhanced filtering
253
+ async getAllFiles(dir, extensions = (this.config && (this.config.includeExtensions || this.config.supportedExtensions)) || ['.js', '.jsx', '.ts', '.tsx', '.py', '.pyx', '.pyi']) {
254
+ const files = [];
255
+
256
+ // Enhanced list of toolkit files to exclude from analysis
257
+ const excludeFiles = [
258
+ 'console-translations.js', 'console-key-checker.js',
259
+ 'complete-console-translations.js', 'detect-language-mismatches.js',
260
+ 'export-missing-keys.js', 'maintain-language-purity.js',
261
+ 'native-translations.js', 'settings-cli.js', 'settings-manager.js',
262
+ 'test-complete-system.js', 'test-console-i18n.js', 'test-features.js',
263
+ 'translate-mismatches.js', 'i18ntk-ui.js', 'update-console-i18n.js',
264
+ 'validate-language-purity.js', 'debugger.js', 'admin-auth.js',
265
+ 'admin-cli.js'
266
+ ];
267
+
268
+ // Null-safe extensions handling
269
+ const safeExtensions = Array.isArray(extensions) ? extensions : ['.js', '.jsx', '.ts', '.tsx', '.py', '.pyx', '.pyi'];
270
+ const skipRoot = path.resolve(this.i18nDir || '');
271
+ const traverse = async (currentDir) => {
272
+ try {
273
+ const absoluteDir = path.resolve(currentDir);
274
+ const validatedPath = SecurityUtils.validatePath(absoluteDir, process.cwd());
275
+
276
+ if (!validatedPath || !SecurityUtils.safeExistsSync(validatedPath)) {
277
+ return;
278
+ }
279
+
280
+ const items = fs.readdirSync(validatedPath);
281
+
282
+ for (const item of items) {
283
+ const itemPath = path.join(validatedPath, item);
284
+
285
+ try {
286
+ const stat = fs.statSync(itemPath);
287
+
288
+ if (stat.isDirectory()) {
289
+ const excludes = Array.isArray(this.config.excludeDirs) ? this.config.excludeDirs : [];
290
+ if (!excludes.includes(item)) {
291
+ // hard-skip the locales root to avoid reading JSON
292
+ if (skipRoot && path.resolve(itemPath).startsWith(skipRoot)) continue;
293
+ await traverse(itemPath);
294
+ }
295
+ } else if (stat.isFile()) {
296
+ // Skip JSON files entirely to prevent scanning translation files
297
+ if (itemPath.endsWith('.json')) continue;
298
+
299
+ // Include files with specified extensions, but exclude toolkit files
300
+ const ext = path.extname(item);
301
+ // Ensure extension has dot prefix
302
+ const normalizedExt = ext.startsWith('.') ? ext : `.${ext}`;
303
+ const normalizedExtensions = safeExtensions.map(ext => ext.startsWith('.') ? ext : `.${ext}`);
304
+ if (normalizedExtensions.includes(normalizedExt) && !excludeFiles.includes(item)) {
305
+ files.push(itemPath);
306
+ }
307
+ }
308
+ } catch (statError) {
309
+ // Skip files that can't be accessed
310
+ continue;
311
+ }
312
+ }
313
+ } catch (error) {
314
+ await SecurityUtils.logSecurityEvent(t('usage.fileTraversalError'), {
315
+ component: 'i18ntk-usage',
316
+ directory: currentDir,
317
+ error: error.message
318
+ });
319
+ }
320
+ };
321
+
322
+ await traverse(dir);
323
+ return files;
324
+ }
325
+
326
+ // Analyze usage in source files
327
+ async analyzeUsage() {
328
+ try {
329
+ console.log(t('usage.checkUsage.analyzing_source_files'));
330
+
331
+ // Check if source directory exists
332
+ if (!SecurityUtils.safeExistsSync(this.sourceDir)) {
333
+ throw new Error(this.t('usage.sourceDirectoryDoesNotExist', { dir: this.sourceDir }) || `Source directory not found: ${this.sourceDir}`);
334
+ }
335
+
336
+ const sourceFiles = await this.getAllFiles(this.sourceDir);
337
+ console.log(t('usage.checkUsage.found_files_in_source', { numFiles: sourceFiles.length }));
338
+
339
+ // If no files found, exit gracefully
340
+ if (sourceFiles.length === 0) {
341
+ console.warn(t('usage.noSourceFilesFound'));
342
+ return;
343
+ }
344
+
345
+ let totalKeysFound = 0;
346
+ let processedFiles = 0;
347
+
348
+ for (const filePath of sourceFiles) {
349
+ try {
350
+ const keys = this.extractKeysFromFile(filePath);
351
+
352
+ if (keys.length > 0) {
353
+ const relativePath = path.relative(this.sourceDir, filePath);
354
+ this.fileUsage.set(relativePath, keys);
355
+
356
+ keys.forEach(key => {
357
+ this.usedKeys.add(key);
358
+ totalKeysFound++;
359
+ });
360
+ }
361
+
362
+ processedFiles++;
363
+
364
+ // Progress indicator for large numbers of files
365
+ if (sourceFiles.length > 10 && processedFiles % Math.ceil(sourceFiles.length / 10) === 0) {
366
+ console.log(t('usage.processedFiles', { processedFiles, totalFiles: sourceFiles.length }));
367
+ }
368
+ } catch (fileError) {
369
+ console.warn(`${t('usage.failedToProcessFile')} ${filePath}: ${fileError.message}`);
370
+ continue;
371
+ }
372
+ }
373
+
374
+ console.log(t("usage.checkUsage.found_thisusedkeyssize_unique_", { usedKeysSize: this.usedKeys.size }));
375
+ console.log(t("usage.checkUsage.total_key_usages_totalkeysfoun", { totalKeysFound }));
376
+
377
+ } catch (error) {
378
+ console.error(t('usage.failedToAnalyzeUsage', { error: error.message }));
379
+ throw error;
380
+ }
381
+ }
382
+
383
+ // Load available translation keys
384
+ async loadAvailableKeys() {
385
+ console.log(t("usage.checkUsage.loading_available_translation_"));
386
+
387
+ this.availableKeys = await this.getAllTranslationKeys();
388
+ console.log(t("usage.checkUsage.found_thisavailablekeyssize_av", { availableKeysSize: this.availableKeys.size }));
389
+ if (this.placeholderKeys.size > 0) {
390
+ console.log('Placeholder translation keys detected: ' + Array.from(this.placeholderKeys).join(', '));
391
+ }
392
+ }
393
+
394
+ // Get all translation keys recursively from translation object
395
+ async getAllTranslationKeys() {
396
+ const keys = new Set();
397
+ const isStrict = process.argv.includes('--strict');
398
+ const isDebug = process.argv.includes('--debug');
399
+
400
+ try {
401
+ // Discover all translation files in the i18n directory
402
+ const translationFiles = await this.discoverTranslationFiles(this.i18nDir, this.config.sourceLanguage);
403
+
404
+ console.log(t('usage.foundTranslationFiles', { count: translationFiles.length }));
405
+
406
+ for (const fileInfo of translationFiles) {
407
+ try {
408
+ await SecurityUtils.validatePath(fileInfo.filePath);
409
+
410
+ // Check if file exists and is readable
411
+ if (!SecurityUtils.safeExistsSync(fileInfo.filePath)) {
412
+ if (isDebug || isStrict) {
413
+ console.warn(`āš ļø File not found: ${path.basename(fileInfo.filePath)}`);
414
+ }
415
+ continue;
416
+ }
417
+
418
+ const content = await SecurityUtils.safeReadFile(fileInfo.filePath);
419
+
420
+ // Handle empty files
421
+ if (!content || content.trim() === '') {
422
+ if (isDebug || isStrict) {
423
+ console.warn(`āš ļø Empty file: ${path.basename(fileInfo.filePath)}`);
424
+ }
425
+ continue;
426
+ }
427
+
428
+ const jsonData = await SecurityUtils.safeParseJSON(content);
429
+
430
+ // Validate JSON structure before processing
431
+ if (jsonData === null || jsonData === undefined) {
432
+ if (isDebug || isStrict) {
433
+ console.warn(`āš ļø Null/undefined JSON data: ${path.basename(fileInfo.filePath)}`);
434
+ }
435
+ continue;
436
+ }
437
+
438
+ if (typeof jsonData !== 'object') {
439
+ if (isDebug || isStrict) {
440
+ console.warn(`āš ļø Invalid JSON structure (not an object): ${path.basename(fileInfo.filePath)}`);
441
+ }
442
+ continue;
443
+ }
444
+
445
+ if (Array.isArray(jsonData)) {
446
+ if (isDebug || isStrict) {
447
+ console.warn(`āš ļø Invalid JSON structure (array instead of object): ${path.basename(fileInfo.filePath)}`);
448
+ }
449
+ continue;
450
+ }
451
+
452
+ // Store file info for later analysis
453
+ this.translationFiles.set(fileInfo.filePath, fileInfo);
454
+
455
+ const fileKeys = this.extractKeysFromObject(jsonData, '', fileInfo.namespace);
456
+ fileKeys.forEach(key => keys.add(key));
457
+ this.collectPlaceholderKeys(jsonData, '', fileInfo.language);
458
+
459
+ if (isDebug) {
460
+ console.log(t('usage.fileInfo', { namespace: fileInfo.namespace, keys: fileKeys.length }));
461
+ }
462
+ } catch (error) {
463
+ if (isDebug || isStrict) {
464
+ console.warn(`āŒ Failed to extract keys from ${path.basename(fileInfo.filePath)}: ${error.message}`);
465
+ }
466
+ if (isDebug) {
467
+ console.error(error.stack);
468
+ }
469
+ await SecurityUtils.logSecurityEvent(t('usage.translationFileParseError'), {
470
+ component: 'i18ntk-usage',
471
+ file: fileInfo.filePath,
472
+ error: error.message
473
+ });
474
+ }
475
+ }
476
+ } catch (error) {
477
+ await SecurityUtils.logSecurityEvent(t('usage.translationKeysLoadError'), {
478
+ component: 'i18ntk-usage',
479
+ error: error.message
480
+ });
481
+ }
482
+
483
+ return keys;
484
+ }
485
+
486
+ // Extract keys recursively from translation object
487
+ extractKeysFromObject(obj, prefix = '', namespace = '') {
488
+ const keys = [];
489
+
490
+ // Validate input object before processing
491
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
492
+ return keys; // Return empty array for invalid input
493
+ }
494
+
495
+ try {
496
+ for (const [key, value] of Object.entries(obj)) {
497
+ const fullKey = prefix ? `${prefix}.${key}` : key;
498
+
499
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
500
+ keys.push(...this.extractKeysFromObject(value, fullKey, namespace));
501
+ } else {
502
+ // Add dot notation key (e.g., "pagination.showing")
503
+ keys.push(fullKey);
504
+ }
505
+ }
506
+ } catch (error) {
507
+ // Handle any unexpected errors during key extraction
508
+ console.warn(`āš ļø Error during key extraction: ${error.message}`);
509
+ return keys;
510
+ }
511
+
512
+ return keys;
513
+ }
514
+
515
+ collectPlaceholderKeys(obj, prefix = '', language) {
516
+ const patterns = this.placeholderStyles[language] || [];
517
+ const regexes = patterns.map(p => new RegExp(p));
518
+ if (typeof obj !== 'object' || obj === null) return;
519
+
520
+ for (const [key, value] of Object.entries(obj)) {
521
+ const fullKey = prefix ? `${prefix}.${key}` : key;
522
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
523
+ this.collectPlaceholderKeys(value, fullKey, language);
524
+ } else if (typeof value === 'string' && regexes.some(r => r.test(value))) {
525
+ this.placeholderKeys.add(fullKey);
526
+ }
527
+ }
528
+ }
529
+
530
+ // Extract translation keys from source code with enhanced patterns
531
+ extractKeysFromFile(filePath) {
532
+ try {
533
+ const content = SecurityUtils.safeReadFileSync(filePath, path.dirname(filePath), 'utf8');
534
+ if (!content) return [];
535
+
536
+ // Skip JSON files entirely to prevent scanning translation files
537
+ if (filePath.endsWith('.json')) return [];
538
+ const rawPatterns = Array.isArray(this.config.translationPatterns) ? this.config.translationPatterns : [];
539
+ if (rawPatterns.length === 0) return [];
540
+
541
+ return this.extractor.extract(content, rawPatterns);
542
+
543
+ // Null-safe translation patterns handling
544
+ } catch (error) {
545
+ console.warn(`${t('usage.failedToExtractKeys')} ${filePath}: ${error.message}`);
546
+ return [];
547
+ }
548
+ }
549
+
550
+ // Analyze translation completeness across all languages
551
+ async analyzeTranslationCompleteness() {
552
+ try {
553
+ console.log('\nšŸ“Š Analyzing translation completeness...');
554
+
555
+ const isDebug = process.argv.includes('--debug');
556
+ const isStrict = process.argv.includes('--strict');
557
+
558
+ // Check if i18n directory exists
559
+ if (!SecurityUtils.safeExistsSync(this.i18nDir, process.cwd())) {
560
+ console.warn(t('usage.i18nDirectoryNotFound', { i18nDir: this.i18nDir }));
561
+ return;
562
+ }
563
+
564
+ // Get all available languages
565
+ const languages = new Set();
566
+
567
+ try {
568
+ // Discover translation files for all languages
569
+ const allLanguageDirs = fs.readdirSync(this.i18nDir)
570
+ .filter(item => {
571
+ try {
572
+ const itemPath = path.join(this.i18nDir, item);
573
+ return SecurityUtils.safeExistsSync(itemPath) && fs.statSync(itemPath).isDirectory();
574
+ } catch (error) {
575
+ return false;
576
+ }
577
+ });
578
+
579
+ for (const lang of allLanguageDirs) {
580
+ if (['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(lang)) {
581
+ languages.add(lang);
582
+ }
583
+ }
584
+
585
+ // Also check for direct language files (en.json, de.json, etc.)
586
+ const directFiles = fs.readdirSync(this.i18nDir)
587
+ .filter(file => file.endsWith('.json'))
588
+ .map(file => path.basename(file, '.json'))
589
+ .filter(lang => ['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(lang));
590
+
591
+ directFiles.forEach(lang => languages.add(lang));
592
+ } catch (error) {
593
+ console.warn(`${t('usage.errorReadingI18nDirectory')} ${error.message}`);
594
+ return;
595
+ }
596
+
597
+ // If no languages found, exit gracefully
598
+ if (languages.size === 0) {
599
+ console.warn(t('usage.checkUsage.noTranslationLanguagesFound'));
600
+ return;
601
+ }
602
+
603
+ // Analyze each language
604
+ for (const language of languages) {
605
+ try {
606
+ const translationFiles = await this.discoverTranslationFiles(this.i18nDir, language);
607
+ let totalKeys = 0;
608
+ let translatedKeys = 0;
609
+
610
+ for (const fileInfo of translationFiles) {
611
+ try {
612
+ if (!SecurityUtils.safeExistsSync(fileInfo.filePath)) {
613
+ if (isDebug || isStrict) {
614
+ console.warn(`āš ļø File not found: ${path.basename(fileInfo.filePath)}`);
615
+ }
616
+ continue;
617
+ }
618
+
619
+ const content = await SecurityUtils.safeReadFile(fileInfo.filePath);
620
+
621
+ // Handle empty files
622
+ if (!content || content.trim() === '') {
623
+ if (isDebug || isStrict) {
624
+ console.warn(`āš ļø Empty file: ${path.basename(fileInfo.filePath)}`);
625
+ }
626
+ continue;
627
+ }
628
+
629
+ const jsonData = await SecurityUtils.safeParseJSON(content);
630
+
631
+ // Validate JSON structure before processing
632
+ if (jsonData === null || jsonData === undefined) {
633
+ if (isDebug || isStrict) {
634
+ console.warn(`āš ļø Null/undefined JSON data: ${path.basename(fileInfo.filePath)}`);
635
+ }
636
+ continue;
637
+ }
638
+
639
+ if (typeof jsonData !== 'object') {
640
+ if (isDebug || isStrict) {
641
+ console.warn(`āš ļø Invalid JSON structure (not an object): ${path.basename(fileInfo.filePath)}`);
642
+ }
643
+ continue;
644
+ }
645
+
646
+ if (Array.isArray(jsonData)) {
647
+ if (isDebug || isStrict) {
648
+ console.warn(`āš ļø Invalid JSON structure (array instead of object): ${path.basename(fileInfo.filePath)}`);
649
+ }
650
+ continue;
651
+ }
652
+
653
+ const stats = this.analyzeFileCompleteness(jsonData);
654
+ totalKeys += stats.total;
655
+ translatedKeys += stats.translated;
656
+ } catch (error) {
657
+ if (isDebug || isStrict) {
658
+ console.warn(`āŒ Failed to analyze file ${path.basename(fileInfo.filePath)}: ${error.message}`);
659
+ }
660
+ if (isDebug) {
661
+ console.error(error.stack);
662
+ }
663
+ continue;
664
+ }
665
+ }
666
+
667
+ this.translationStats.set(language, {
668
+ total: totalKeys,
669
+ translated: translatedKeys,
670
+ notTranslated: totalKeys - translatedKeys
671
+ });
672
+ } catch (error) {
673
+ if (isDebug || isStrict) {
674
+ console.warn(t('usage.failedToAnalyzeLanguage', { language, error: error.message }));
675
+ }
676
+ continue;
677
+ }
678
+ }
679
+ } catch (error) {
680
+ console.warn(t('usage.translationCompletenessAnalysisFailed', { error: error.message }));
681
+ // Don't throw error, just continue with the rest of the analysis
682
+ }
683
+ }
684
+
685
+ // Analyze completeness of a single translation file
686
+ analyzeFileCompleteness(obj) {
687
+ let total = 0;
688
+ let translated = 0;
689
+
690
+ // Validate input object before processing
691
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
692
+ return { total: 0, translated: 0 }; // Return empty stats for invalid input
693
+ }
694
+
695
+ const traverse = (current) => {
696
+ // Validate current object before processing
697
+ if (typeof current !== 'object' || current === null || Array.isArray(current)) {
698
+ return;
699
+ }
700
+
701
+ for (const [key, value] of Object.entries(current)) {
702
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
703
+ traverse(value);
704
+ } else {
705
+ total++;
706
+ if (value !== 'NOT_TRANSLATED' && value !== '(NOT TRANSLATED)' &&
707
+ value !== 'TRANSLATED' && value !== '(TRANSLATED)' &&
708
+ value && value.toString().trim() !== '') {
709
+ translated++;
710
+ }
711
+ }
712
+ }
713
+ };
714
+
715
+ traverse(obj);
716
+ return { total, translated };
717
+ }
718
+
719
+ // Get statistics about NOT_TRANSLATED values
720
+ getNotTranslatedStats() {
721
+ let total = 0;
722
+ const byLanguage = new Map();
723
+
724
+ for (const [language, stats] of this.translationStats) {
725
+ const notTranslated = stats.notTranslated;
726
+ total += notTranslated;
727
+ byLanguage.set(language, notTranslated);
728
+ }
729
+
730
+ return { total, byLanguage };
731
+ }
732
+
733
+ // Find unused keys
734
+ findUnusedKeys() {
735
+ const unused = [];
736
+
737
+ for (const key of this.availableKeys) {
738
+ let isUsed = false;
739
+
740
+ // Check exact match
741
+ if (this.usedKeys.has(key)) {
742
+ isUsed = true;
743
+ } else {
744
+ // Check if any dynamic key could match this
745
+ for (const usedKey of this.usedKeys) {
746
+ if (usedKey.endsWith('*')) {
747
+ const prefix = usedKey.slice(0, -1);
748
+ if (key.startsWith(prefix)) {
749
+ isUsed = true;
750
+ break;
751
+ }
752
+ }
753
+ }
754
+ }
755
+
756
+ if (!isUsed) {
757
+ unused.push(key);
758
+ }
759
+ }
760
+
761
+ return unused;
762
+ }
763
+
764
+ // Find missing keys (used but not available)
765
+ findMissingKeys() {
766
+ const missing = [];
767
+
768
+ for (const key of this.usedKeys) {
769
+ // Skip dynamic keys for missing check
770
+ if (key.endsWith('*')) {
771
+ continue;
772
+ }
773
+
774
+ if (!this.availableKeys.has(key)) {
775
+ missing.push(key);
776
+ }
777
+ }
778
+
779
+ return missing;
780
+ }
781
+
782
+ // Find files that use specific keys
783
+ findKeyUsage(searchKey) {
784
+ const usage = [];
785
+
786
+ for (const [filePath, keys] of this.fileUsage) {
787
+ const matchingKeys = keys.filter(key => {
788
+ if (key.endsWith('*')) {
789
+ const prefix = key.slice(0, -1);
790
+ return searchKey.startsWith(prefix);
791
+ }
792
+ return key === searchKey;
793
+ });
794
+
795
+ if (matchingKeys.length > 0) {
796
+ usage.push({ filePath, keys: matchingKeys });
797
+ }
798
+ }
799
+
800
+ return usage;
801
+ }
802
+
803
+ // Enhanced usage report generation
804
+ generateUsageReport() {
805
+ const unusedKeys = this.findUnusedKeys();
806
+ const missingKeys = this.findMissingKeys();
807
+ const dynamicKeys = Array.from(this.usedKeys).filter(key => key.endsWith('*'));
808
+ const notTranslatedStats = this.getNotTranslatedStats();
809
+
810
+ const timestamp = new Date().toISOString();
811
+
812
+ let report = `${t('summary.usageReportTitle')}\n`;
813
+ report += `${t('summary.usageReportGenerated', { timestamp })}\n`;
814
+ report += `${t('summary.usageReportSourceDir', { sourceDir: this.sourceDir })}\n`;
815
+ report += `${t('summary.usageReportI18nDir', { i18nDir: this.i18nDir })}\n`;
816
+ report += `Version: ${this.version}\n\n`;
817
+
818
+ // Performance metrics
819
+ const analysisTime = Date.now() - this.startTime;
820
+ report += `⚔ Performance Metrics:\n`;
821
+ report += ` Analysis completed in: ${analysisTime}ms\n`;
822
+ report += ` Memory usage: ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB\n\n`;
823
+
824
+ // Summary
825
+ report += `${t('summary.usageReportSummary')}\n`;
826
+ report += `${'='.repeat(50)}\n`;
827
+ report += `${t('summary.usageReportSourceFilesScanned', { count: this.fileUsage.size })}\n`;
828
+ report += `${t('summary.usageReportTranslationFilesFound', { count: this.translationFiles.size })}\n`;
829
+ report += `${t('summary.usageReportAvailableKeys', { count: this.availableKeys.size })}\n`;
830
+ report += `${t('summary.usageReportUsedKeys', { count: this.usedKeys.size - dynamicKeys.length })}\n`;
831
+ report += `${t('summary.usageReportDynamicKeys', { count: dynamicKeys.length })}\n`;
832
+ report += `${t('summary.usageReportUnusedKeys', { count: unusedKeys.length })}\n`;
833
+ report += `${t('summary.usageReportMissingKeys', { count: missingKeys.length })}\n`;
834
+ report += `${t('summary.usageReportNotTranslatedKeys', { count: notTranslatedStats.total })}\n\n`;
835
+
836
+ // Framework usage analysis
837
+ if (this.frameworkUsage && this.frameworkUsage.size > 0) {
838
+ report += `šŸ—ļø Framework Usage Analysis:\n`;
839
+ const frameworkCounts = {};
840
+ this.frameworkUsage.forEach((data, filePath) => {
841
+ const framework = data.framework;
842
+ if (!frameworkCounts[framework]) frameworkCounts[framework] = 0;
843
+ frameworkCounts[framework]++;
844
+ });
845
+
846
+ Object.entries(frameworkCounts).forEach(([framework, count]) => {
847
+ report += ` ${framework}: ${count} files\n`;
848
+ });
849
+ report += `\n`;
850
+ }
851
+
852
+ // Translation completeness with advanced scoring
853
+ report += `${t('summary.usageReportTranslationCompleteness')}\n`;
854
+ report += `${'='.repeat(50)}\n`;
855
+ for (const [language, stats] of this.translationStats) {
856
+ const translations = this.translationsByLanguage[language] || {};
857
+ const score = this.calculateTranslationScore ? this.calculateTranslationScore(language, translations) : {
858
+ completeness: ((stats.translated / stats.total) * 100).toFixed(1),
859
+ quality: ((stats.translated / stats.total) * 100).toFixed(1),
860
+ placeholderAccuracy: 100
861
+ };
862
+
863
+ report += `${t('summary.usageReportLanguageCompleteness', { language: language.toUpperCase(), completeness: score.completeness, translated: stats.translated, total: stats.total })}\n`;
864
+ report += ` Quality: ${score.quality}%\n`;
865
+ report += ` Placeholder Accuracy: ${score.placeholderAccuracy}%\n`;
866
+
867
+ if (stats.notTranslated > 0) {
868
+ report += `${t('summary.usageReportNotTranslatedInLanguage', { count: stats.notTranslated })}\n`;
869
+ }
870
+ report += `\n`;
871
+ }
872
+
873
+ // Translation files discovered
874
+ report += `${t('summary.usageReportTranslationFilesDiscovered')}\n`;
875
+ report += `${'='.repeat(50)}\n`;
876
+ for (const [filePath, fileInfo] of this.translationFiles) {
877
+ const relativePath = path.relative(this.i18nDir, filePath);
878
+ report += `${t('summary.usageReportFileInfo', { relativePath, namespace: fileInfo.namespace, type: fileInfo.type })}\n`;
879
+ }
880
+ report += `\n`;
881
+
882
+ // Key complexity analysis
883
+ if (this.keyComplexity && this.keyComplexity.size > 0) {
884
+ report += `šŸ” Key Complexity Analysis:\n`;
885
+ const complexityStats = { simple: 0, moderate: 0, complex: 0 };
886
+ this.keyComplexity.forEach((data, key) => {
887
+ complexityStats[data.level]++;
888
+ });
889
+
890
+ report += ` Simple keys: ${complexityStats.simple}\n`;
891
+ report += ` Moderate keys: ${complexityStats.moderate}\n`;
892
+ report += ` Complex keys: ${complexityStats.complex}\n\n`;
893
+ }
894
+
895
+ // Unused keys with complexity
896
+ if (unusedKeys.length > 0) {
897
+ report += `${t('summary.usageReportUnusedTranslationKeys')}\n`;
898
+ report += `${'='.repeat(50)}\n`;
899
+ report += `${t('summary.usageReportUnusedKeysDescription')}\n\n`;
900
+
901
+ unusedKeys.slice(0, 100).forEach(key => {
902
+ const complexity = this.keyComplexity && this.keyComplexity.get(key);
903
+ const complexityLevel = complexity ? ` (${complexity.level})` : '';
904
+ report += `${t('summary.usageReportUnusedKey', { key: key + complexityLevel })}\n`;
905
+ });
906
+
907
+ if (unusedKeys.length > 100) {
908
+ report += `${t('summary.usageReportMoreUnusedKeys', { count: unusedKeys.length - 100 })}\n`;
909
+ }
910
+
911
+ report += `\n`;
912
+ }
913
+
914
+ // Missing keys with location and framework
915
+ if (missingKeys.length > 0) {
916
+ report += `${t('summary.usageReportMissingTranslationKeys')}\n`;
917
+ report += `${'='.repeat(50)}\n`;
918
+ report += `${t('summary.usageReportMissingKeysDescription')}\n\n`;
919
+
920
+ missingKeys.forEach(key => {
921
+ report += `${t('summary.usageReportMissingKey', { key })}\n`;
922
+
923
+ // Show where it's used
924
+ const usage = this.findKeyUsage(key);
925
+ usage.slice(0, 3).forEach(({ filePath }) => {
926
+ const framework = this.frameworkUsage && this.frameworkUsage.get(filePath);
927
+ const frameworkInfo = framework ? ` [${framework.framework}]` : '';
928
+ report += ` ${t('summary.usageReportUsedIn', { filePath: filePath + frameworkInfo })}\n`;
929
+ });
930
+
931
+ if (usage.length > 3) {
932
+ report += ` ${t('summary.usageReportMoreFiles', { count: usage.length - 3 })}\n`;
933
+ }
934
+
935
+ report += `\n`;
936
+ });
937
+ }
938
+
939
+ // Dynamic keys
940
+ if (dynamicKeys.length > 0) {
941
+ report += `${t('summary.usageReportDynamicTranslationKeys')}\n`;
942
+ report += `${'='.repeat(50)}\n`;
943
+ report += `${t('summary.usageReportDynamicKeysDescription')}\n\n`;
944
+
945
+ dynamicKeys.forEach(key => {
946
+ const complexity = this.keyComplexity && this.keyComplexity.get(key);
947
+ const complexityLevel = complexity ? ` (${complexity.level})` : '';
948
+ report += `${t('summary.usageReportDynamicKey', { key: key + complexityLevel })}\n`;
949
+
950
+ // Show where it's used
951
+ const usage = this.findKeyUsage(key);
952
+ usage.slice(0, 2).forEach(({ filePath }) => {
953
+ const framework = this.frameworkUsage && this.frameworkUsage.get(filePath);
954
+ const frameworkInfo = framework ? ` [${framework.framework}]` : '';
955
+ report += ` ${t('summary.usageReportUsedIn', { filePath: filePath + frameworkInfo })}\n`;
956
+ });
957
+
958
+ report += `\n`;
959
+ });
960
+ }
961
+
962
+ // Placeholder validation results
963
+ const placeholderValidations = [];
964
+ Object.entries(this.translationsByLanguage || {}).forEach(([lang, translations]) => {
965
+ Object.entries(translations).forEach(([key, value]) => {
966
+ if (this.validatePlaceholderKeys) {
967
+ const validation = this.validatePlaceholderKeys(key, value);
968
+ if (validation.hasPlaceholders) {
969
+ placeholderValidations.push({ lang, key, validation });
970
+ }
971
+ }
972
+ });
973
+ });
974
+
975
+ if (placeholderValidations.length > 0) {
976
+ report += `šŸ”§ Placeholder Validation Results:\n`;
977
+ placeholderValidations.forEach(({ lang, key, validation }) => {
978
+ const status = validation.isValid ? 'āœ…' : 'āŒ';
979
+ report += ` ${status} ${lang}.${key}: ${validation.placeholders.join(', ')}\n`;
980
+ if (!validation.isValid) {
981
+ validation.errors.forEach(error => {
982
+ report += ` - ${error}\n`;
983
+ });
984
+ }
985
+ });
986
+ report += `\n`;
987
+ }
988
+
989
+ // File usage breakdown with framework info
990
+ report += `${t('summary.usageReportFileUsageBreakdown')}\n`;
991
+ report += `${'='.repeat(50)}\n`;
992
+
993
+ const sortedFiles = Array.from(this.fileUsage.entries())
994
+ .sort(([,a], [,b]) => b.length - a.length)
995
+ .slice(0, 20);
996
+
997
+ sortedFiles.forEach(([filePath, keys]) => {
998
+ const framework = this.frameworkUsage && this.frameworkUsage.get(filePath);
999
+ const frameworkInfo = framework ? ` [${framework.framework}]` : '';
1000
+ report += `${t('summary.usageReportFileUsage', { filePath: filePath + frameworkInfo, count: keys.length })}\n`;
1001
+ });
1002
+
1003
+ if (this.fileUsage.size > 20) {
1004
+ report += `${t('summary.usageReportMoreFiles', { count: this.fileUsage.size - 20 })}\n`;
1005
+ }
1006
+
1007
+ return report;
1008
+ }
1009
+
1010
+ // Save report to file
1011
+ async saveReport(report, outputDir = './i18ntk-reports/usage') {
1012
+ try {
1013
+ // Ensure output directory exists
1014
+ if (!SecurityUtils.safeExistsSync(outputDir)) {
1015
+ fs.mkdirSync(outputDir, { recursive: true });
1016
+ }
1017
+
1018
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1019
+ const filename = `usage-analysis-${timestamp}.txt`;
1020
+ const filepath = path.join(outputDir, filename);
1021
+
1022
+ await SecurityUtils.safeWriteFile(filepath, report);
1023
+ console.log(t('usage.reportSavedTo', { reportPath: filepath }));
1024
+ return filepath;
1025
+ } catch (error) {
1026
+ console.error(t('usage.failedToSaveReport', { error: error.message }));
1027
+ }
1028
+ }
1029
+
1030
+ // Enhanced placeholder key detection with validation
1031
+ validatePlaceholderKeys(key, value) {
1032
+ if (typeof value !== 'string') {
1033
+ return {
1034
+ key,
1035
+ hasPlaceholders: false,
1036
+ placeholders: [],
1037
+ isValid: true,
1038
+ errors: []
1039
+ };
1040
+ }
1041
+
1042
+ const placeholderRegex = /\{\{[^}]+\}\}|\{[^}]+\}|\$\{[^}]+\}/g;
1043
+ const placeholders = (typeof value === 'string' ? value : String(value || '')).match(placeholderRegex) || [];
1044
+
1045
+ const validation = {
1046
+ key,
1047
+ hasPlaceholders: placeholders.length > 0,
1048
+ placeholders,
1049
+ isValid: true,
1050
+ errors: []
1051
+ };
1052
+
1053
+ // Check for common placeholder issues
1054
+ placeholders.forEach(placeholder => {
1055
+ if (typeof placeholder === 'string') {
1056
+ if (placeholder.includes('undefined') || placeholder.includes('null')) {
1057
+ validation.isValid = false;
1058
+ validation.errors.push(`Invalid placeholder: ${placeholder}`);
1059
+ }
1060
+
1061
+ // Check for matching opening/closing brackets
1062
+ const placeholderStr = String(placeholder || '');
1063
+ const openCount = (placeholderStr.match(/\{/g) || []).length;
1064
+ const closeCount = (placeholderStr.match(/\}/g) || []).length;
1065
+ if (openCount !== closeCount) {
1066
+ validation.isValid = false;
1067
+ validation.errors.push(`Mismatched brackets in: ${placeholder}`);
1068
+ }
1069
+ }
1070
+ });
1071
+
1072
+ return validation;
1073
+ }
1074
+
1075
+ // Framework-specific pattern recognition
1076
+ detectFrameworkPatterns(content, filePath) {
1077
+ const frameworkPatterns = {
1078
+ react: {
1079
+ patterns: [
1080
+ /useTranslation\(\)/g,
1081
+ /Trans\s+component/g,
1082
+ /i18nKey\s*=/g,
1083
+ /withTranslation\(/g
1084
+ ],
1085
+ score: 0
1086
+ },
1087
+ vue: {
1088
+ patterns: [
1089
+ /\$t\(/g,
1090
+ /this\.\$t\(/g,
1091
+ /v-t\s*=/g,
1092
+ /\$i18n/g
1093
+ ],
1094
+ score: 0
1095
+ },
1096
+ angular: {
1097
+ patterns: [
1098
+ /translate\s*\|/g,
1099
+ /ngx-translate/g,
1100
+ /TranslateService/g,
1101
+ /\.instant\(/g
1102
+ ],
1103
+ score: 0
1104
+ }
1105
+ };
1106
+
1107
+ const contentStr = String(content || '');
1108
+ Object.keys(frameworkPatterns).forEach(framework => {
1109
+ const config = frameworkPatterns[framework];
1110
+ config.patterns.forEach(pattern => {
1111
+ const matches = contentStr.match(pattern);
1112
+ if (matches) {
1113
+ config.score += matches.length;
1114
+ }
1115
+ });
1116
+ });
1117
+
1118
+ // Find dominant framework
1119
+ let dominantFramework = 'generic';
1120
+ let maxScore = 0;
1121
+
1122
+ Object.keys(frameworkPatterns).forEach(framework => {
1123
+ if (frameworkPatterns[framework].score > maxScore) {
1124
+ maxScore = frameworkPatterns[framework].score;
1125
+ dominantFramework = framework;
1126
+ }
1127
+ });
1128
+
1129
+ this.frameworkUsage.set(filePath, {
1130
+ framework: dominantFramework,
1131
+ score: maxScore,
1132
+ patterns: frameworkPatterns[dominantFramework]?.patterns || []
1133
+ });
1134
+
1135
+ return dominantFramework;
1136
+ }
1137
+
1138
+ // Advanced translation completeness scoring
1139
+ calculateTranslationScore(language, translations) {
1140
+ const score = {
1141
+ completeness: 0,
1142
+ quality: 0,
1143
+ consistency: 0,
1144
+ placeholderAccuracy: 0
1145
+ };
1146
+
1147
+ const totalKeys = Object.keys(translations).length;
1148
+ const translatedKeys = Object.keys(translations).filter(key =>
1149
+ translations[key] &&
1150
+ translations[key] !== 'NOT_TRANSLATED' &&
1151
+ translations[key] !== key
1152
+ ).length;
1153
+
1154
+ score.completeness = totalKeys > 0 ? (translatedKeys / totalKeys) * 100 : 0;
1155
+
1156
+ // Quality scoring based on placeholder accuracy
1157
+ const placeholderScores = Object.entries(translations).map(([key, value]) => {
1158
+ const validation = this.validatePlaceholderKeys(key, value);
1159
+ return validation.isValid ? 1 : 0;
1160
+ });
1161
+
1162
+ score.placeholderAccuracy = placeholderScores.length > 0
1163
+ ? (placeholderScores.reduce((sum, score) => sum + score, 0) / placeholderScores.length) * 100
1164
+ : 0;
1165
+
1166
+ score.quality = (score.completeness + score.placeholderAccuracy) / 2;
1167
+
1168
+ return score;
1169
+ }
1170
+
1171
+ // Key complexity analysis
1172
+ analyzeKeyComplexity(key) {
1173
+ // Ensure key is a string
1174
+ const keyStr = String(key || '');
1175
+
1176
+ const complexity = {
1177
+ level: 'simple',
1178
+ segments: keyStr.split('.').length,
1179
+ length: keyStr.length,
1180
+ hasPlaceholders: false,
1181
+ patterns: []
1182
+ };
1183
+
1184
+ if (complexity.segments > 3) complexity.level = 'complex';
1185
+ else if (complexity.segments > 1) complexity.level = 'moderate';
1186
+
1187
+ if (keyStr.includes('{{') || keyStr.includes('${')) {
1188
+ complexity.hasPlaceholders = true;
1189
+ complexity.level = 'complex';
1190
+ }
1191
+
1192
+ this.keyComplexity.set(keyStr, complexity);
1193
+ return complexity;
1194
+ }
1195
+
1196
+ // Main analysis process
1197
+ async run(options = {}) {
1198
+ const { fromMenu = false } = options;
1199
+
1200
+ // Parse command line arguments for strict/debug flags
1201
+ const args = await this.parseArgs();
1202
+ const cliOptions = {
1203
+ strict: process.argv.includes('--strict'),
1204
+ debug: process.argv.includes('--debug')
1205
+ };
1206
+
1207
+ if (cliOptions.debug) {
1208
+ console.log('šŸ” Debug mode enabled');
1209
+ }
1210
+
1211
+ try {
1212
+ // Ensure config is always initialized
1213
+ if (!this.config) {
1214
+ this.config = {};
1215
+ }
1216
+
1217
+ // Ensure configuration is loaded - no need for .i18ntk directory check
1218
+ if (!this.config) {
1219
+ this.config = {};
1220
+ }
1221
+
1222
+ // Initialize configuration properly when called from menu
1223
+ if (fromMenu && !this.sourceDir) {
1224
+ const baseConfig = await getUnifiedConfig('usage', args);
1225
+ this.config = { ...baseConfig, ...(this.config || {}) };
1226
+
1227
+ const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
1228
+ loadTranslations(uiLanguage, path.resolve(__dirname, '..', '..', '..', 'resources', 'i18n', 'ui-locales'));
1229
+ if (!Array.isArray(this.config.translationPatterns)) {
1230
+ this.config.translationPatterns = [
1231
+ /t\(['"`]([^'"`]+)['"`]/g,
1232
+ /i18n\.t\(['"`]([^'"`]+)['"`]/g,
1233
+ /useTranslation\(\)\.t\(['"`]([^'"`]+)['"`]/g,
1234
+ /t\(`([^`]+)`\)/g,
1235
+ /i18nKey=['"`]([^'"`]+)['"`]/g,
1236
+ /\$t\(['"`]([^'"`]+)['"`]/g,
1237
+ /getTranslation\(['"`]([^'"`]+)['"`]/g
1238
+ ];
1239
+ }
1240
+ if (!Array.isArray(this.config.excludeDirs)) {
1241
+ this.config.excludeDirs = ['node_modules', '.git'];
1242
+ }
1243
+ if (!Array.isArray(this.config.includeExtensions) && !Array.isArray(this.config.supportedExtensions)) {
1244
+ this.config.includeExtensions = ['.js', '.jsx', '.ts', '.tsx', '.py', '.pyx', '.pyi'];
1245
+ }
1246
+
1247
+ this.sourceDir = this.config.sourceDir;
1248
+ this.i18nDir = this.config.i18nDir;
1249
+ this.sourceLanguageDir = path.join(this.i18nDir, this.config.sourceLanguage);
1250
+ if (fromMenu && (!this.config.sourceDir || this.config.sourceDir === this.config.i18nDir)) {
1251
+ console.log('āš ļø Go to Settings → Directory Settings or run with --source-dir');
1252
+ }
1253
+ } else {
1254
+ await this.initialize();
1255
+ }
1256
+
1257
+ // Skip admin authentication when called from menu
1258
+ if (!fromMenu) {
1259
+ const isCalledDirectly = require.main === module;
1260
+ if (isCalledDirectly && !args.noPrompt) {
1261
+ // Only check admin authentication when running directly and not in no-prompt mode
1262
+ const AdminAuth = require('../../../utils/admin-auth');
1263
+ const adminAuth = new AdminAuth();
1264
+ await adminAuth.initialize();
1265
+
1266
+ const isRequired = await adminAuth.isAuthRequired();
1267
+ if (isRequired) {
1268
+ console.log('\n' + t('adminCli.authRequiredForOperation', { operation: 'analyze usage' }));
1269
+
1270
+ const pin = await askHidden(t('adminCli.enterPin'));
1271
+
1272
+ const isValid = await adminAuth.verifyPin(pin);
1273
+
1274
+ if (!isValid) {
1275
+ console.log(t('adminCli.invalidPin'));
1276
+ if (!fromMenu) process.exit(1);
1277
+ return { success: false, error: 'Authentication failed' };
1278
+ }
1279
+
1280
+ console.log(t('adminCli.authenticationSuccess'));
1281
+ }
1282
+ }
1283
+ }
1284
+
1285
+ if (args.help) {
1286
+ this.showHelp();
1287
+ return;
1288
+ }
1289
+
1290
+ // Override config with command line arguments
1291
+ if (args.sourceDir) {
1292
+ this.config.sourceDir = args.sourceDir;
1293
+ this.sourceDir = path.resolve(args.sourceDir);
1294
+ }
1295
+
1296
+ if (args.i18nDir) {
1297
+ this.config.i18nDir = args.i18nDir;
1298
+ this.i18nDir = path.resolve(args.i18nDir);
1299
+ this.sourceLanguageDir = path.join(this.i18nDir, this.config.sourceLanguage);
1300
+ }
1301
+
1302
+ if (this.sourceDir || this.i18nDir) {
1303
+ await configManager.updateConfig({
1304
+ sourceDir: configManager.toRelative(this.sourceDir || this.config.sourceDir),
1305
+ i18nDir: configManager.toRelative(this.i18nDir || this.config.i18nDir)
1306
+ });
1307
+ }
1308
+
1309
+ // Ensure sourceDir points to source code, not locales
1310
+ if (!args.sourceDir && this.config.sourceDir === this.config.i18nDir) {
1311
+ // Default to common source directories if not explicitly provided
1312
+ const possibleSourceDirs = ['src', 'lib', 'app', 'source'];
1313
+
1314
+ const projectRoot = this.config.projectRoot || '.';
1315
+
1316
+ for (const dir of possibleSourceDirs) {
1317
+ const testPath = path.resolve(projectRoot, dir);
1318
+ if (SecurityUtils.safeExistsSync(testPath)) {
1319
+ this.config.sourceDir = testPath;
1320
+ this.sourceDir = testPath;
1321
+ break;
1322
+ }
1323
+ }
1324
+
1325
+ // If no common source directory found, use current directory
1326
+ if (this.config.sourceDir === this.config.i18nDir) {
1327
+ this.config.sourceDir = projectRoot;
1328
+ this.sourceDir = projectRoot;
1329
+ }
1330
+ }
1331
+
1332
+ // 🚧 prevent scanning locales as source
1333
+ if (path.resolve(this.sourceDir) === path.resolve(this.i18nDir)) {
1334
+ const fallback = path.resolve(this.config.projectRoot || '.', 'src');
1335
+ console.warn(t('usage.sourceEqualsI18nWarn') ||
1336
+ `āš ļø sourceDir equals i18nDir (${this.sourceDir}). Falling back to ${fallback} for source scanning.`);
1337
+ if (SecurityUtils.safeExistsSync(fallback)) {
1338
+ this.sourceDir = fallback;
1339
+ } else {
1340
+ console.warn(`āš ļø Fallback directory ${fallback} does not exist. Using project root for source scanning.`);
1341
+ this.sourceDir = path.resolve(this.config.projectRoot || '.');
1342
+ }
1343
+ this.config.sourceDir = this.sourceDir;
1344
+ await configManager.updateConfig({
1345
+ sourceDir: configManager.toRelative(this.sourceDir)
1346
+ });
1347
+ }
1348
+
1349
+ console.log(t('usage.detectedSourceDirectory', { sourceDir: this.sourceDir }));
1350
+ console.log(t('usage.detectedI18nDirectory', { i18nDir: this.i18nDir }));
1351
+
1352
+ // Load available translation keys first
1353
+ await this.loadAvailableKeys();
1354
+
1355
+ // NEW: Detect framework patterns before analysis
1356
+ await this.detectFrameworkPatterns();
1357
+
1358
+ // Perform usage analysis with enhanced features
1359
+ await this.analyzeUsage();
1360
+
1361
+ // NEW: Validate placeholder keys
1362
+ await this.validatePlaceholderKeys();
1363
+
1364
+ // Analyze translation completeness with enhanced scoring
1365
+ await this.analyzeTranslationCompleteness();
1366
+
1367
+ // Calculate key complexity analysis
1368
+ await this.analyzeKeyComplexity();
1369
+
1370
+ // Generate and display results
1371
+ const unusedKeys = this.findUnusedKeys();
1372
+ const missingKeys = this.findMissingKeys();
1373
+ const notTranslatedStats = this.getNotTranslatedStats();
1374
+
1375
+ // Calculate performance metrics
1376
+ const duration = Date.now() - this.startTime;
1377
+
1378
+ console.log('\n' + t('usage.analysisResults'));
1379
+ console.log(' ' + t('usage.availableKeysCount', { count: this.availableKeys.size }));
1380
+ console.log(' ' + t('usage.usedKeysCount', { count: this.usedKeys.size }));
1381
+ console.log(t('usage.unusedKeysCount', { count: unusedKeys.length }));
1382
+ console.log(t('usage.missingKeysCount', { count: missingKeys.length }));
1383
+ console.log(t('usage.notTranslatedKeysTotal', { total: notTranslatedStats.total }));
1384
+
1385
+ // NEW: Display performance metrics
1386
+ console.log(`\nšŸ“Š Performance: ${duration}ms (${this.availableKeys.size} keys processed)`);
1387
+
1388
+ // NEW: Display framework usage
1389
+ if (this.frameworkUsage.size > 0) {
1390
+ console.log('\nšŸ› ļø Framework Detection:');
1391
+ const frameworkCounts = new Map();
1392
+
1393
+ // Aggregate framework counts
1394
+ for (const [filePath, frameworkInfo] of this.frameworkUsage) {
1395
+ const frameworkName = frameworkInfo.framework || 'generic';
1396
+ frameworkCounts.set(frameworkName, (frameworkCounts.get(frameworkName) || 0) + 1);
1397
+ }
1398
+
1399
+ if (frameworkCounts.size > 0) {
1400
+ for (const [framework, count] of frameworkCounts) {
1401
+ console.log(` ${framework}: ${count} files`);
1402
+ }
1403
+ } else {
1404
+ console.log(' No Framework: 0 files');
1405
+ }
1406
+ } else {
1407
+ console.log('\nšŸ› ļø Framework Detection:');
1408
+ console.log(' No Framework: 0 files');
1409
+ }
1410
+
1411
+ // NEW: Display key complexity analysis
1412
+ const complexityValues = Array.from(this.keyComplexity.values()).map(c => c.segments || 0);
1413
+ const avgComplexity = complexityValues.length > 0 ?
1414
+ complexityValues.reduce((a, b) => a + b, 0) / complexityValues.length : 0;
1415
+ console.log(`\nšŸ” Key Complexity: ${avgComplexity.toFixed(2)} avg depth`);
1416
+
1417
+ // Sanity check: warn if 0 used keys but available keys exist
1418
+ if (this.availableKeys.size > 0 && this.usedKeys.size === 0) {
1419
+ console.warn('\nāš ļø ' + (t('operations.usage.noUsedKeysHint') || 'Found translations but no usage in source. Check --source-dir and translationPatterns.'));
1420
+ }
1421
+
1422
+ // Display translation completeness by language with enhanced scoring
1423
+ console.log(t('common.languageCompletenessTitle'));
1424
+ for (const [language, stats] of this.translationStats) {
1425
+ const completeness = ((stats.translated / stats.total) * 100).toFixed(1);
1426
+ const score = this.calculateTranslationScore(language, stats);
1427
+ console.log(t('summary.usageReportLanguageCompleteness', {
1428
+ language: language.toUpperCase(),
1429
+ completeness,
1430
+ translated: stats.translated,
1431
+ total: stats.total
1432
+ }));
1433
+ }
1434
+
1435
+ if (args.outputReport) {
1436
+ const report = this.generateUsageReport();
1437
+ await this.saveReport(report, args.outputDir);
1438
+ }
1439
+
1440
+ console.log('\n' + t('usage.analysisCompletedSuccessfully'));
1441
+
1442
+ if (require.main === module && !args.noPrompt) {
1443
+ await this.prompt('\nPress Enter to continue...');
1444
+ }
1445
+ this.closeReadline();
1446
+
1447
+ } catch (error) {
1448
+ console.error(t('usage.analysisFailedError'), error.message);
1449
+ this.closeReadline();
1450
+ SecurityUtils.logSecurityEvent(t('usage.usageAnalysisFailed'), {
1451
+ component: 'i18ntk-usage',
1452
+ error: error.message
1453
+ });
1454
+ throw error;
1455
+ }
1456
+ }
1457
+
1458
+ // Show help message
1459
+ showHelp() {
1460
+ console.log(`
1461
+ šŸ“Š i18ntk usage - Translation key usage analysis (v1.8.3)
1462
+
1463
+ Usage:
1464
+ node i18ntk-usage.js [options]
1465
+ npm run i18ntk:usage -- [options]
1466
+
1467
+ Options:
1468
+ --source-dir=<path> Source code directory to scan (default: ./src)
1469
+ --i18n-dir=<path> Directory containing translation files (default: ./src/i18n/locales)
1470
+ --output-report Generate detailed usage report
1471
+ --output-dir=<path> Directory for output reports (default: ./i18ntk-reports/usage)
1472
+ --strict Show all warnings and errors during analysis
1473
+ --debug Enable debug mode with stack traces
1474
+ --no-prompt Skip interactive prompts (useful for CI/CD)
1475
+ --validate-placeholders Enable placeholder key validation
1476
+ --framework-detect Enable framework-specific pattern detection
1477
+ --performance-mode Enable performance metrics tracking
1478
+ --help, -h Show this help message
1479
+
1480
+ Examples:
1481
+ node i18ntk-usage.js --source-dir=./src --i18n-dir=./translations --output-report
1482
+ npm run i18ntk:usage -- --strict --debug --validate-placeholders
1483
+ node i18ntk-usage.js --no-prompt --performance-mode --output-dir=./reports
1484
+
1485
+ Analysis Features (v1.8.3):
1486
+ • Detects unused translation keys
1487
+ • Identifies missing translation keys
1488
+ • Shows translation completeness by language
1489
+ • Reports NOT_TRANSLATED values
1490
+ • Supports modular folder structures
1491
+ • Enhanced placeholder key detection
1492
+ • Framework-specific pattern recognition (React, Vue, Angular)
1493
+ • Advanced translation completeness scoring
1494
+ • Performance metrics and optimization tracking
1495
+ • Key complexity analysis
1496
+ • Security-enhanced path validation
1497
+ • Detailed reporting with validation errors
1498
+ `);
1499
+ }
1500
+ }
1501
+
1502
+ module.exports = UsageService;