i18ntk 1.10.2 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +141 -1191
  3. package/main/i18ntk-analyze.js +65 -84
  4. package/main/i18ntk-backup-class.js +420 -0
  5. package/main/i18ntk-backup.js +3 -3
  6. package/main/i18ntk-complete.js +90 -65
  7. package/main/i18ntk-doctor.js +123 -103
  8. package/main/i18ntk-fixer.js +61 -725
  9. package/main/i18ntk-go.js +14 -15
  10. package/main/i18ntk-init.js +77 -26
  11. package/main/i18ntk-java.js +27 -32
  12. package/main/i18ntk-js.js +70 -68
  13. package/main/i18ntk-manage.js +129 -30
  14. package/main/i18ntk-php.js +75 -75
  15. package/main/i18ntk-py.js +55 -56
  16. package/main/i18ntk-scanner.js +59 -57
  17. package/main/i18ntk-setup.js +9 -404
  18. package/main/i18ntk-sizing.js +6 -6
  19. package/main/i18ntk-summary.js +21 -18
  20. package/main/i18ntk-ui.js +11 -10
  21. package/main/i18ntk-usage.js +54 -18
  22. package/main/i18ntk-validate.js +13 -13
  23. package/main/manage/commands/AnalyzeCommand.js +1124 -0
  24. package/main/manage/commands/BackupCommand.js +62 -0
  25. package/main/manage/commands/CommandRouter.js +295 -0
  26. package/main/manage/commands/CompleteCommand.js +61 -0
  27. package/main/manage/commands/DoctorCommand.js +60 -0
  28. package/main/manage/commands/FixerCommand.js +624 -0
  29. package/main/manage/commands/InitCommand.js +62 -0
  30. package/main/manage/commands/ScannerCommand.js +654 -0
  31. package/main/manage/commands/SizingCommand.js +60 -0
  32. package/main/manage/commands/SummaryCommand.js +61 -0
  33. package/main/manage/commands/UsageCommand.js +60 -0
  34. package/main/manage/commands/ValidateCommand.js +978 -0
  35. package/main/manage/index-fixed.js +1447 -0
  36. package/main/manage/index.js +1462 -0
  37. package/main/manage/managers/DebugMenu.js +140 -0
  38. package/main/manage/managers/InteractiveMenu.js +177 -0
  39. package/main/manage/managers/LanguageMenu.js +62 -0
  40. package/main/manage/managers/SettingsMenu.js +53 -0
  41. package/main/manage/services/AuthenticationService.js +263 -0
  42. package/main/manage/services/ConfigurationService-fixed.js +449 -0
  43. package/main/manage/services/ConfigurationService.js +449 -0
  44. package/main/manage/services/FileManagementService.js +368 -0
  45. package/main/manage/services/FrameworkDetectionService.js +458 -0
  46. package/main/manage/services/InitService.js +1051 -0
  47. package/main/manage/services/SetupService.js +462 -0
  48. package/main/manage/services/SummaryService.js +450 -0
  49. package/main/manage/services/UsageService.js +1502 -0
  50. package/package.json +32 -29
  51. package/runtime/enhanced.d.ts +221 -221
  52. package/runtime/index.d.ts +29 -29
  53. package/runtime/index.full.d.ts +331 -331
  54. package/runtime/index.js +7 -6
  55. package/scripts/build-lite.js +17 -17
  56. package/scripts/deprecate-versions.js +23 -6
  57. package/scripts/export-translations.js +5 -5
  58. package/scripts/fix-all-i18n.js +3 -3
  59. package/scripts/fix-and-purify-i18n.js +3 -2
  60. package/scripts/fix-locale-control-chars.js +110 -0
  61. package/scripts/lint-locales.js +80 -0
  62. package/scripts/locale-optimizer.js +8 -8
  63. package/scripts/prepublish.js +21 -21
  64. package/scripts/security-check.js +117 -117
  65. package/scripts/sync-translations.js +4 -4
  66. package/scripts/sync-ui-locales.js +9 -8
  67. package/scripts/validate-all-translations.js +8 -7
  68. package/scripts/verify-deprecations.js +157 -161
  69. package/scripts/verify-translations.js +6 -5
  70. package/settings/i18ntk-config.json +282 -282
  71. package/settings/language-config.json +5 -5
  72. package/settings/settings-cli.js +9 -9
  73. package/settings/settings-manager.js +18 -18
  74. package/ui-locales/de.json +2417 -2348
  75. package/ui-locales/en.json +2415 -2352
  76. package/ui-locales/es.json +2425 -2353
  77. package/ui-locales/fr.json +2418 -2348
  78. package/ui-locales/ja.json +2463 -2361
  79. package/ui-locales/ru.json +2463 -2359
  80. package/ui-locales/zh.json +2418 -2351
  81. package/utils/admin-auth.js +2 -2
  82. package/utils/admin-cli.js +297 -297
  83. package/utils/admin-pin.js +9 -9
  84. package/utils/cli-helper.js +9 -9
  85. package/utils/config-helper.js +73 -104
  86. package/utils/config-manager.js +204 -171
  87. package/utils/config.js +5 -4
  88. package/utils/env-manager.js +249 -263
  89. package/utils/framework-detector.js +27 -24
  90. package/utils/i18n-helper.js +85 -41
  91. package/utils/init-helper.js +152 -94
  92. package/utils/json-output.js +98 -98
  93. package/utils/mini-commander.js +179 -0
  94. package/utils/missing-key-validator.js +5 -5
  95. package/utils/plugin-loader.js +40 -29
  96. package/utils/prompt.js +14 -44
  97. package/utils/safe-json.js +40 -0
  98. package/utils/secure-errors.js +3 -3
  99. package/utils/security-check-improved.js +390 -0
  100. package/utils/security-config.js +5 -5
  101. package/utils/security-fixed.js +607 -0
  102. package/utils/security.js +652 -602
  103. package/utils/setup-enforcer.js +136 -44
  104. package/utils/setup-validator.js +33 -32
  105. package/utils/ultra-performance-optimizer.js +11 -9
  106. package/utils/watch-locales.js +2 -1
  107. package/utils/prompt-fixed.js +0 -55
  108. package/utils/security-check.js +0 -454
@@ -0,0 +1,1124 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * I18NTK ANALYZE COMMAND
5
+ *
6
+ * Handles translation analysis functionality.
7
+ * Contains embedded business logic from I18nAnalyzer.
8
+ */
9
+
10
+ const path = require('path');
11
+ const cliHelper = require('../../../utils/cli-helper');
12
+ const { loadTranslations, t } = require('../../../utils/i18n-helper');
13
+ const { getUnifiedConfig, parseCommonArgs, displayHelp } = require('../../../utils/config-helper');
14
+ const SecurityUtils = require('../../../utils/security');
15
+ const AdminCLI = require('../../../utils/admin-cli');
16
+ const watchLocales = require('../../../utils/watch-locales');
17
+ const JsonOutput = require('../../../utils/json-output');
18
+
19
+ loadTranslations('en', path.resolve(__dirname, '../../../resources', 'i18n', 'ui-locales'));
20
+
21
+ const PROJECT_ROOT = process.cwd();
22
+
23
+ class AnalyzeCommand {
24
+ constructor(config = {}, ui = null) {
25
+ this.config = config;
26
+ this.ui = ui;
27
+ this.prompt = null;
28
+ this.isNonInteractiveMode = false;
29
+ this.safeClose = null;
30
+
31
+ // Initialize analysis properties
32
+ this.sourceDir = null;
33
+ this.sourceLanguageDir = null;
34
+ this.outputDir = null;
35
+ }
36
+
37
+ /**
38
+ * Set runtime dependencies for interactive operations
39
+ */
40
+ setRuntimeDependencies(prompt, isNonInteractiveMode, safeClose) {
41
+ this.prompt = prompt;
42
+ this.isNonInteractiveMode = isNonInteractiveMode;
43
+ this.safeClose = safeClose;
44
+ }
45
+
46
+ /**
47
+ * Initialize the analyzer with configuration
48
+ */
49
+ async initialize() {
50
+ try {
51
+ const args = this.parseArgs();
52
+ if (args.help) {
53
+ displayHelp('i18ntk-analyze', {
54
+ 'language': 'Analyze specific language only',
55
+ 'output-reports': 'Generate detailed reports',
56
+ 'setup-admin': 'Configure admin PIN protection',
57
+ 'disable-admin': 'Disable admin PIN protection',
58
+ 'admin-status': 'Check admin PIN status',
59
+ 'setup-wizard': 'Interactive setup wizard for analysis configuration',
60
+ 'wizard': 'Alias for --setup-wizard',
61
+ 'source': 'Source directory path',
62
+ 'output': 'Output directory for reports',
63
+ 'json': 'Output results as JSON',
64
+ 'exclude-files': 'Comma-separated list of files to exclude',
65
+ 'exclude': 'Pattern to exclude files'
66
+ });
67
+ process.exit(0);
68
+ }
69
+
70
+ // Configuration is handled by getUnifiedConfig - no need for .i18ntk directory check
71
+
72
+ // Initialize i18n with UI language first
73
+ const baseConfig = await getUnifiedConfig('analyze', args);
74
+ this.config = { ...baseConfig, ...(this.config || {}) };
75
+
76
+ const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
77
+ loadTranslations(uiLanguage, path.resolve(__dirname, '../../../resources', 'i18n', 'ui-locales'));
78
+
79
+ this.sourceDir = this.config.sourceDir;
80
+ this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
81
+ this.outputDir = this.config.outputDir;
82
+
83
+ // Validate source directory exists
84
+ const { validateSourceDir } = require('../../../utils/config-helper');
85
+ validateSourceDir(this.sourceDir, 'i18ntk-analyze');
86
+
87
+ } catch (error) {
88
+ console.error(`Fatal analysis error: ${error.message}`);
89
+ throw error;
90
+ }
91
+ }
92
+
93
+ // Initialize readline interface (deprecated - use cliHelper directly)
94
+ initReadline() {
95
+ return cliHelper.getInterface();
96
+ }
97
+
98
+ // Close readline interface (deprecated - use cliHelper.close directly)
99
+ closeReadline() {
100
+ cliHelper.close();
101
+ }
102
+
103
+ // Prompt for user input
104
+ async prompt(question) {
105
+ return cliHelper.prompt(question);
106
+ }
107
+
108
+ // Parse command line arguments
109
+ parseArgs() {
110
+ try {
111
+ const args = process.argv.slice(2);
112
+ const parsed = parseCommonArgs(args);
113
+
114
+ // Add script-specific arguments
115
+ args.forEach(arg => {
116
+ if (arg.startsWith('--')) {
117
+ const [key, value] = arg.substring(2).split('=');
118
+ const sanitizedKey = SecurityUtils.sanitizeInput(key);
119
+ const sanitizedValue = value ? SecurityUtils.sanitizeInput(value) : true;
120
+
121
+ if (sanitizedKey === 'language') {
122
+ parsed.language = sanitizedValue;
123
+ } else if (sanitizedKey === 'output-reports') {
124
+ parsed.outputReports = true;
125
+ } else if (sanitizedKey === 'setup-admin') {
126
+ parsed.setupAdmin = true;
127
+ } else if (sanitizedKey === 'disable-admin') {
128
+ parsed.disableAdmin = true;
129
+ } else if (sanitizedKey === 'admin-status') {
130
+ parsed.adminStatus = true;
131
+ } else if (sanitizedKey === 'json') {
132
+ parsed.json = true;
133
+ } else if (sanitizedKey === 'sort-keys') {
134
+ parsed.sortKeys = true;
135
+ } else if (sanitizedKey === 'indent') {
136
+ parsed.indent = parseInt(value) || 2;
137
+ } else if (sanitizedKey === 'newline') {
138
+ parsed.newline = value || 'lf';
139
+ } else if (sanitizedKey === 'setup-wizard' || sanitizedKey === 'wizard') {
140
+ parsed['setup-wizard'] = true;
141
+ parsed.wizard = true;
142
+ }
143
+ }
144
+ });
145
+
146
+ return parsed;
147
+ } catch (error) {
148
+ throw error;
149
+ }
150
+ }
151
+
152
+ // Get all available languages
153
+ getAvailableLanguages() {
154
+ try {
155
+ const items = SecurityUtils.safeReaddirSync(this.sourceDir, process.cwd(), { withFileTypes: true });
156
+ if (!items) {
157
+ console.error('Error reading source directory: Unable to access directory');
158
+ return [];
159
+ }
160
+
161
+ const languages = [];
162
+
163
+ // Check for directory-based structure
164
+ const directories = items
165
+ .filter(item => item.isDirectory())
166
+ .map(item => item.name)
167
+ .filter(name => name !== 'node_modules' && !name.startsWith('.') && name !== this.config.sourceLanguage);
168
+
169
+ // Check for monolith files (language.json files)
170
+ const files = items
171
+ .filter(item => item.isFile() && item.name.endsWith('.json'))
172
+ .map(item => item.name);
173
+
174
+ // Add directories as languages
175
+ languages.push(...directories);
176
+
177
+ // Add monolith files as languages (without .json extension)
178
+ const monolithLanguages = files
179
+ .map(file => file.replace('.json', ''))
180
+ .filter(lang => !languages.includes(lang) && lang !== this.config.sourceLanguage);
181
+ languages.push(...monolithLanguages);
182
+
183
+ // Check for nested structures
184
+ for (const dir of directories) {
185
+ const dirPath = path.join(this.sourceDir, dir);
186
+ try {
187
+ const dirItems = SecurityUtils.safeReaddirSync(dirPath, process.cwd(), { withFileTypes: true });
188
+ if (dirItems) {
189
+ const jsonFiles = dirItems
190
+ .filter(item => item.isFile() && item.name.endsWith('.json'))
191
+ .map(item => item.name.replace('.json', ''));
192
+
193
+ // If directory contains JSON files, it's likely a language directory
194
+ if (jsonFiles.length > 0) {
195
+ if (!languages.includes(dir)) {
196
+ languages.push(dir);
197
+ }
198
+ }
199
+ }
200
+ } catch (error) {
201
+ // Skip directories we can't read
202
+ }
203
+ }
204
+
205
+ return [...new Set(languages)].sort();
206
+ } catch (error) {
207
+ console.error('Error reading source directory:', error.message);
208
+ return [];
209
+ }
210
+ }
211
+
212
+ // Get all JSON files from a language directory
213
+ getLanguageFiles(language) {
214
+ if (!this.sourceDir) {
215
+ console.warn('Source directory not set');
216
+ return [];
217
+ }
218
+
219
+ const languageDir = path.resolve(this.sourceDir, language);
220
+ const languageFile = path.resolve(this.sourceDir, `${language}.json`);
221
+ const files = [];
222
+
223
+ // Handle monolith file structure
224
+ const languageFileStat = SecurityUtils.safeStatSync(languageFile, this.sourceDir);
225
+ if (languageFileStat && languageFileStat.isFile()) {
226
+ return [path.basename(languageFile)];
227
+ }
228
+
229
+ // Handle directory-based structure
230
+ const languageDirStat = SecurityUtils.safeStatSync(languageDir, this.sourceDir);
231
+ if (languageDirStat && languageDirStat.isDirectory()) {
232
+ try {
233
+ // Ensure the path is within the source directory for security
234
+ const validatedPath = SecurityUtils.validatePath(languageDir, this.sourceDir);
235
+ if (!validatedPath) {
236
+ console.warn(`Language directory not found or invalid: ${languageDir}`);
237
+ return [];
238
+ }
239
+
240
+ const findJsonFiles = (dir) => {
241
+ const results = [];
242
+ const items = SecurityUtils.safeReaddirSync(dir, this.sourceDir, { withFileTypes: true });
243
+
244
+ if (!items) return results;
245
+
246
+ for (const item of items) {
247
+ const fullPath = path.join(dir, item.name);
248
+
249
+ if (item.isDirectory() && !item.name.startsWith('.') && item.name !== 'node_modules') {
250
+ // Recursively search subdirectories
251
+ results.push(...findJsonFiles(fullPath));
252
+ } else if (item.isFile() && item.name.endsWith('.json')) {
253
+ // Check exclusion patterns
254
+ const relativePath = path.relative(this.sourceDir, fullPath);
255
+ const shouldExclude = (this.config.excludeFiles || []).some(pattern => {
256
+ if (typeof pattern === 'string') {
257
+ return relativePath === pattern || relativePath.endsWith(path.sep + pattern);
258
+ }
259
+ if (pattern instanceof RegExp) {
260
+ return pattern.test(relativePath);
261
+ }
262
+ return false;
263
+ });
264
+
265
+ if (!shouldExclude && !item.name.startsWith('.')) {
266
+ results.push(path.relative(languageDir, fullPath));
267
+ }
268
+ }
269
+ }
270
+
271
+ return results;
272
+ };
273
+
274
+ return findJsonFiles(validatedPath);
275
+ } catch (error) {
276
+ console.error(`Error reading language directory ${languageDir}:`, error.message);
277
+ return [];
278
+ }
279
+ }
280
+
281
+ // Check for namespace-based structure (language.json files in any subdirectory)
282
+ const findNamespaceFiles = () => {
283
+ const results = [];
284
+ const searchDir = this.sourceDir;
285
+
286
+ try {
287
+ const searchDirExists = SecurityUtils.safeExistsSync(searchDir, this.sourceDir);
288
+ if (searchDirExists) {
289
+ const items = SecurityUtils.safeReaddirSync(searchDir, this.sourceDir, { withFileTypes: true });
290
+
291
+ if (items) {
292
+ for (const item of items) {
293
+ if (item.isDirectory() && !item.name.startsWith('.') && item.name !== 'node_modules') {
294
+ const namespaceDir = path.join(searchDir, item.name);
295
+ const namespaceFile = path.join(namespaceDir, `${language}.json`);
296
+
297
+ const namespaceFileExists = SecurityUtils.safeExistsSync(namespaceFile, this.sourceDir);
298
+ if (namespaceFileExists) {
299
+ results.push(path.relative(path.join(this.sourceDir, item.name), namespaceFile));
300
+ }
301
+ }
302
+ }
303
+ }
304
+ }
305
+ } catch (error) {
306
+ console.warn(`Error searching for namespace files: ${error.message}`);
307
+ }
308
+
309
+ return results;
310
+ };
311
+
312
+ const namespaceFiles = findNamespaceFiles();
313
+ if (namespaceFiles.length > 0) {
314
+ return namespaceFiles;
315
+ }
316
+
317
+ return files;
318
+ }
319
+
320
+ // Get all keys recursively from an object
321
+ getAllKeys(obj, prefix = '') {
322
+ const keys = new Set();
323
+
324
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
325
+ // Log a warning instead of crashing
326
+ console.warn(`⚠️ Skipping invalid translation object at prefix '${prefix}'`);
327
+ return keys;
328
+ }
329
+
330
+ for (const [key, value] of Object.entries(obj)) {
331
+ const fullKey = prefix ? `${prefix}.${key}` : key;
332
+ keys.add(fullKey);
333
+
334
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
335
+ const nestedKeys = this.getAllKeys(value, fullKey);
336
+ nestedKeys.forEach(k => keys.add(k));
337
+ }
338
+ }
339
+
340
+ return keys;
341
+ }
342
+
343
+ // Get value by key path
344
+ getValueByPath(obj, keyPath) {
345
+ // Ensure keyPath is a string
346
+ const keyPathStr = String(keyPath || '');
347
+ const keys = keyPathStr.split('.');
348
+ let current = obj;
349
+
350
+ for (const key of keys) {
351
+ if (current && typeof current === 'object' && key in current) {
352
+ current = current[key];
353
+ } else {
354
+ return undefined;
355
+ }
356
+ }
357
+
358
+ return current;
359
+ }
360
+
361
+ // Analyze translation issues in an object
362
+ analyzeTranslationIssues(obj, sourceObj = null, prefix = '') {
363
+ const issues = [];
364
+
365
+ for (const [key, value] of Object.entries(obj)) {
366
+ const fullKey = prefix ? `${prefix}.${key}` : key;
367
+ const sourceValue = sourceObj ? this.getValueByPath(sourceObj, fullKey) : null;
368
+
369
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
370
+ issues.push(...this.analyzeTranslationIssues(value, sourceObj, fullKey));
371
+ } else if (typeof value === 'string') {
372
+ const markers = this.config.notTranslatedMarkers || [this.config.notTranslatedMarker];
373
+ if (markers.some(m => value === m)) {
374
+ issues.push({
375
+ type: 'not_translated',
376
+ key: fullKey,
377
+ value,
378
+ sourceValue: sourceValue || 'N/A'
379
+ });
380
+ } else if (value === '') {
381
+ issues.push({
382
+ type: 'empty_value',
383
+ key: fullKey,
384
+ value,
385
+ sourceValue: sourceValue || 'N/A'
386
+ });
387
+ } else if (markers.some(m => value.includes(m))) {
388
+ issues.push({
389
+ type: 'partial_translation',
390
+ key: fullKey,
391
+ value,
392
+ sourceValue: sourceValue || 'N/A'
393
+ });
394
+ } else if (sourceValue && value === sourceValue) {
395
+ issues.push({
396
+ type: 'same_as_source',
397
+ key: fullKey,
398
+ value,
399
+ sourceValue
400
+ });
401
+ }
402
+ }
403
+ }
404
+
405
+ return issues;
406
+ }
407
+
408
+ // Get translation statistics for an object
409
+ getTranslationStats(obj) {
410
+ let total = 0;
411
+ let translated = 0;
412
+ let notTranslated = 0;
413
+ let empty = 0;
414
+ let partial = 0;
415
+
416
+ const markers = this.config.notTranslatedMarkers || [this.config.notTranslatedMarker];
417
+ const count = (item) => {
418
+ if (typeof item === 'string') {
419
+ total++;
420
+ if (markers.some(m => item === m)) {
421
+ notTranslated++;
422
+ } else if (item === '') {
423
+ empty++;
424
+ } else if (markers.some(m => item.includes(m))) {
425
+ partial++;
426
+ } else {
427
+ translated++;
428
+ }
429
+ } else if (Array.isArray(item)) {
430
+ item.forEach(count);
431
+ } else if (item && typeof item === 'object') {
432
+ Object.values(item).forEach(count);
433
+ }
434
+ };
435
+
436
+ count(obj);
437
+
438
+ return {
439
+ total,
440
+ translated,
441
+ notTranslated,
442
+ empty,
443
+ partial,
444
+ percentage: total > 0 ? Math.round((translated / total) * 100) : 0,
445
+ missing: notTranslated + empty + partial
446
+ };
447
+ }
448
+
449
+ // Check structural consistency between source and target
450
+ checkStructuralConsistency(sourceObj, targetObj) {
451
+ const sourceKeys = this.getAllKeys(sourceObj);
452
+ const targetKeys = this.getAllKeys(targetObj);
453
+
454
+ const missingKeys = [...sourceKeys].filter(key => !targetKeys.has(key));
455
+ const extraKeys = [...targetKeys].filter(key => !sourceKeys.has(key));
456
+
457
+ return {
458
+ isConsistent: missingKeys.length === 0 && extraKeys.length === 0,
459
+ missingKeys,
460
+ extraKeys,
461
+ sourceKeyCount: sourceKeys.size,
462
+ targetKeyCount: targetKeys.size
463
+ };
464
+ }
465
+
466
+ // Analyze a single language
467
+ analyzeLanguage(language) {
468
+ const languageDir = path.join(this.sourceDir, language);
469
+ const sourceFiles = this.getLanguageFiles(this.config.sourceLanguage);
470
+ const targetFiles = this.getLanguageFiles(language);
471
+
472
+ const analysis = {
473
+ language,
474
+ files: {},
475
+ summary: {
476
+ totalFiles: sourceFiles.length,
477
+ analyzedFiles: 0,
478
+ totalKeys: 0,
479
+ translatedKeys: 0,
480
+ missingKeys: 0,
481
+ issues: []
482
+ }
483
+ };
484
+
485
+ for (const fileName of sourceFiles) {
486
+ const sourceFilePath = path.join(this.config.sourceLanguage, fileName);
487
+ const targetFilePath = path.join(language, fileName);
488
+
489
+ const sourceFullPath = path.join(this.sourceDir, sourceFilePath);
490
+ const targetFullPath = path.join(this.sourceDir, targetFilePath);
491
+
492
+ const sourceExists = SecurityUtils.safeExistsSync(sourceFullPath, this.sourceDir);
493
+ if (!sourceExists) {
494
+ continue;
495
+ }
496
+
497
+ let sourceContent, targetContent;
498
+
499
+ try {
500
+ const sourceFileContent = SecurityUtils.safeReadFileSync(sourceFullPath, this.sourceDir, 'utf8');
501
+ if (!sourceFileContent) {
502
+ analysis.files[fileName] = {
503
+ error: `Failed to read source file: File not accessible or empty`
504
+ };
505
+ continue;
506
+ }
507
+ sourceContent = SecurityUtils.safeParseJSON(sourceFileContent);
508
+ if (!sourceContent) {
509
+ analysis.files[fileName] = {
510
+ error: `Failed to parse source file: Invalid JSON format`
511
+ };
512
+ continue;
513
+ }
514
+ } catch (error) {
515
+ analysis.files[fileName] = {
516
+ error: `Failed to parse source file: ${error.message}`
517
+ };
518
+ continue;
519
+ }
520
+
521
+ const targetExists = SecurityUtils.safeExistsSync(targetFullPath, this.sourceDir);
522
+ if (!targetExists) {
523
+ analysis.files[fileName] = {
524
+ status: 'missing',
525
+ sourceKeys: this.getAllKeys(sourceContent).size
526
+ };
527
+ continue;
528
+ }
529
+
530
+ try {
531
+ const targetFileContent = SecurityUtils.safeReadFileSync(targetFullPath, this.sourceDir, 'utf8');
532
+ if (!targetFileContent) {
533
+ analysis.files[fileName] = {
534
+ error: `Failed to read target file: File not accessible or empty`
535
+ };
536
+ continue;
537
+ }
538
+
539
+ const parsed = SecurityUtils.safeParseJSON(targetFileContent);
540
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
541
+ analysis.files[fileName] = {
542
+ error: `Invalid structure in target file: must be a plain object (not array/null/type)`
543
+ };
544
+ continue;
545
+ }
546
+
547
+ targetContent = parsed;
548
+
549
+ } catch (error) {
550
+ analysis.files[fileName] = {
551
+ error: `Failed to parse target file: ${error.message}`
552
+ };
553
+ continue;
554
+ }
555
+
556
+ // Analyze this file
557
+ const stats = this.getTranslationStats(targetContent);
558
+ const structural = this.checkStructuralConsistency(sourceContent, targetContent);
559
+ const issues = this.analyzeTranslationIssues(targetContent, sourceContent);
560
+
561
+ analysis.files[fileName] = {
562
+ status: 'analyzed',
563
+ stats,
564
+ structural,
565
+ issues,
566
+ sourceFilePath,
567
+ targetFilePath
568
+ };
569
+
570
+ // Update summary
571
+ analysis.summary.analyzedFiles++;
572
+ analysis.summary.totalKeys += stats.total;
573
+ analysis.summary.translatedKeys += stats.translated;
574
+ analysis.summary.missingKeys += stats.missing;
575
+ analysis.summary.issues.push(...issues);
576
+ }
577
+
578
+ // Calculate overall percentage
579
+ analysis.summary.percentage = analysis.summary.totalKeys > 0
580
+ ? Math.round((analysis.summary.translatedKeys / analysis.summary.totalKeys) * 100)
581
+ : 0;
582
+
583
+ return analysis;
584
+ }
585
+
586
+ // Generate detailed report for a language
587
+ generateLanguageReport(analysis) {
588
+ const { language } = analysis;
589
+ const timestamp = new Date().toISOString();
590
+
591
+ let report = `${t('analyze.reportTitle', { language: language.toUpperCase() })}
592
+ `;
593
+ report += `${t('analyze.generated', { timestamp })}
594
+ `;
595
+ report += `${t('analyze.status', { translated: analysis.summary.translatedKeys, total: analysis.summary.totalKeys, percentage: analysis.summary.percentage })}
596
+ `;
597
+ report += `${t('analyze.filesAnalyzed', { analyzed: analysis.summary.analyzedFiles, total: analysis.summary.totalFiles })}
598
+ `;
599
+ report += `${t('analyze.keysNeedingTranslation', { count: analysis.summary.missingKeys })}
600
+
601
+ `;
602
+
603
+ report += `${t('analyze.fileBreakdown')}
604
+ `;
605
+ report += `${'='.repeat(50)}\n\n`;
606
+
607
+ Object.entries(analysis.files).forEach(([fileName, fileData]) => {
608
+ report += `\uD83D\uDCC4 ${fileName}\n`;
609
+
610
+ if (fileData.error) {
611
+ report += ` \u274C ${t('analyze.error')}: ${fileData.error}\n\n`;
612
+ return;
613
+ }
614
+
615
+ if (fileData.status === 'missing') {
616
+ report += ` \u274C ${t('analyze.statusFileMissing')}\n`;
617
+ report += ` 📊 ${t('analyze.sourceKeys', { count: fileData.sourceKeys })}\n\n`;
618
+ return;
619
+ }
620
+
621
+ const { stats, structural, issues } = fileData;
622
+
623
+ report += ` \uD83D\uDCCA ${t('analyze.translation', { translated: stats.translated, total: stats.total, percentage: stats.percentage })}\n`;
624
+ report += ` \uD83C\uDFD7️ ${t('analyze.structure', { status: structural.isConsistent ? t('analyze.consistent') : t('analyze.inconsistent') })}\n`;
625
+
626
+ if (!structural.isConsistent) {
627
+ if (structural.missingKeys.length > 0) {
628
+ report += ` ${t('analyze.missingKeys', { count: structural.missingKeys.length })}\n`;
629
+ }
630
+ if (structural.extraKeys.length > 0) {
631
+ report += ` ${t('analyze.extraKeys', { count: structural.extraKeys.length })}\n`;
632
+ }
633
+ }
634
+
635
+ if (issues.length > 0) {
636
+ report += ` ⚠️ ${t('analyze.issues', { count: issues.length })}\n`;
637
+
638
+ const issueTypes = {
639
+ not_translated: issues.filter(i => i.type === 'not_translated').length,
640
+ empty_value: issues.filter(i => i.type === 'empty_value').length,
641
+ partial_translation: issues.filter(i => i.type === 'partial_translation').length,
642
+ same_as_source: issues.filter(i => i.type === 'same_as_source').length
643
+ };
644
+
645
+ Object.entries(issueTypes).forEach(([type, count]) => {
646
+ if (count > 0) {
647
+ report += ` ${t('analyze.issueType.' + type, { count })}\n`;
648
+ }
649
+ });
650
+ }
651
+
652
+ report += `\n`;
653
+ });
654
+
655
+ // Keys needing translation
656
+ const notTranslatedIssues = analysis.summary.issues.filter(issue =>
657
+ issue.type === 'not_translated' || issue.type === 'empty_value'
658
+ );
659
+
660
+ if (notTranslatedIssues.length > 0) {
661
+ report += `${t('analyze.keysToTranslate')}\n`;
662
+ report += `${'='.repeat(50)}\n\n`;
663
+
664
+ notTranslatedIssues.slice(0, 50).forEach(issue => {
665
+ report += `${t('analyze.key')}: ${issue.key}\n`;
666
+ report += `${t('analyze.english')}: "${issue.sourceValue}"\n`;
667
+ report += `${language}: [${t('analyze.needsTranslation')}]\n\n`;
668
+ });
669
+
670
+ if (notTranslatedIssues.length > 50) {
671
+ report += `${t('analyze.andMoreKeys', { count: notTranslatedIssues.length - 50 })}\n\n`;
672
+ }
673
+ }
674
+
675
+ return report;
676
+ }
677
+
678
+ // Save analysis report to a file
679
+ async saveReport(language, report) {
680
+ try {
681
+ // Ensure we have a valid output directory
682
+ if (!this.outputDir) {
683
+ this.outputDir = path.join(process.cwd(), 'i18n-reports');
684
+ console.warn(`No output directory specified, using default: ${this.outputDir}`);
685
+ }
686
+
687
+ // Ensure the output directory exists
688
+ const dirCreated = SecurityUtils.safeMkdirSync(this.outputDir, process.cwd(), { recursive: true });
689
+ if (!dirCreated) {
690
+ console.error(`Failed to create output directory ${this.outputDir}`);
691
+ return null;
692
+ }
693
+
694
+ // Validate the output directory is within the project
695
+ const validatedOutputDir = SecurityUtils.validatePath(this.outputDir, process.cwd());
696
+ if (!validatedOutputDir) {
697
+ console.error(`Invalid output directory: ${this.outputDir} is outside project root`);
698
+ return null;
699
+ }
700
+
701
+ // Create a safe filename
702
+ const safeLanguage = language.replace(/[^\w-]/g, '_');
703
+ const reportPath = path.resolve(validatedOutputDir, `translation-report-${safeLanguage}.json`);
704
+
705
+ // Ensure the final path is still within the output directory
706
+ if (!reportPath.startsWith(validatedOutputDir)) {
707
+ console.error('Invalid report path detected, potential directory traversal attack');
708
+ return null;
709
+ }
710
+
711
+ // Use safeWriteFile for secure file writing
712
+ const success = await SecurityUtils.safeWriteFile(reportPath, JSON.stringify(report, null, 2), process.cwd(), 'utf8');
713
+ if (!success) {
714
+ throw new Error(t('analyze.failedToWriteReportFile') || 'Failed to write report file securely');
715
+ }
716
+
717
+ console.log(`Report saved to: ${reportPath}`);
718
+ return reportPath;
719
+
720
+ } catch (error) {
721
+ console.error(`Failed to save report for ${language}:`, error.message);
722
+ return null;
723
+ }
724
+ }
725
+
726
+ // Show help message
727
+ showHelp() {
728
+ console.log(t('analyze.help_message'));
729
+ }
730
+
731
+ // Main analyze method
732
+ async analyze() {
733
+ try {
734
+ const results = [];
735
+ const args = this.parseArgs();
736
+ const jsonOutput = new JsonOutput('analyze');
737
+
738
+ if (!args.json) {
739
+ console.log(t('analyze.starting') || '🔍 Starting translation analysis...');
740
+ console.log(t('analyze.sourceDirectoryLabel', { sourceDir: path.resolve(this.sourceDir) }));
741
+ console.log(t('analyze.sourceLanguageLabel', { sourceLanguage: this.config.sourceLanguage }));
742
+ console.log(t('analyze.strictModeLabel', { mode: this.config.processing?.strictMode || this.config.strictMode ? 'ON' : 'OFF' }));
743
+ }
744
+
745
+ // Ensure output directory exists
746
+ const outputDirExists = SecurityUtils.safeExistsSync(this.outputDir, process.cwd());
747
+ if (!outputDirExists) {
748
+ SecurityUtils.safeMkdirSync(this.outputDir, process.cwd(), { recursive: true });
749
+ }
750
+
751
+ const languages = this.getAvailableLanguages();
752
+
753
+ if (languages.length === 0) {
754
+ const error = t('analyze.noLanguages') || '⚠️ No target languages found.';
755
+ const guidance = this.provideSetupGuidance();
756
+
757
+ if (args.json) {
758
+ jsonOutput.setStatus('error', error);
759
+ jsonOutput.setOutput({
760
+ error,
761
+ guidance,
762
+ structure: this.detectStructureType()
763
+ });
764
+ console.log(JSON.stringify(jsonOutput.data, null, args.indent || 2));
765
+ return;
766
+ }
767
+ console.log(error);
768
+ console.log('\n' + guidance);
769
+ return;
770
+ }
771
+
772
+ if (!args.json) {
773
+ console.log(t('analyze.foundLanguages', { count: languages.length, languages: languages.join(', ') }) || `📋 Found ${languages.length} languages to analyze: ${languages.join(', ')}`);
774
+ }
775
+
776
+ let totalMissing = 0;
777
+ let totalExtra = 0;
778
+ let totalFiles = 0;
779
+
780
+ for (const language of languages) {
781
+ if (!args.json) {
782
+ console.log(t('analyze.analyzing', { language }) || `\n🔄 Analyzing ${language}...`);
783
+ }
784
+
785
+ const analysis = this.analyzeLanguage(language);
786
+ const report = this.generateLanguageReport(analysis);
787
+
788
+ // Save report
789
+ const reportPath = await this.saveReport(language, report);
790
+
791
+ if (!args.json) {
792
+ console.log(t('analyze.completed', { language }) || `✅ Analysis completed for ${language}`);
793
+ console.log(t('analyze.progress', {
794
+ translated: results.length,
795
+ total: languages.length
796
+ }) || ` Progress: ${results.length}/${languages.length} languages processed`);
797
+ console.log(t('analyze.reportSaved', { reportPath }) || ` Report saved: ${reportPath}`);
798
+ }
799
+
800
+ results.push({
801
+ language,
802
+ analysis,
803
+ reportPath
804
+ });
805
+
806
+ // Add issues to JSON output
807
+ Object.values(analysis.files).forEach(fileData => {
808
+ if (fileData.structural) {
809
+ fileData.structural.missingKeys?.forEach(key => {
810
+ jsonOutput.addIssue('missing', key, language);
811
+ totalMissing++;
812
+ });
813
+ fileData.structural.extraKeys?.forEach(key => {
814
+ jsonOutput.addIssue('extra', key, language);
815
+ totalExtra++;
816
+ });
817
+ }
818
+ });
819
+ totalFiles += analysis.summary.analyzedFiles;
820
+ }
821
+
822
+ // Set JSON output
823
+ jsonOutput.setStats({
824
+ missing: totalMissing,
825
+ extra: totalExtra,
826
+ files: totalFiles,
827
+ languages: languages.length
828
+ });
829
+
830
+ if (totalMissing > 0 || totalExtra > 0) {
831
+ jsonOutput.setStatus('warn');
832
+ } else {
833
+ jsonOutput.setStatus('ok');
834
+ }
835
+
836
+ if (args.json) {
837
+ jsonOutput.output();
838
+ return results;
839
+ }
840
+
841
+ // Summary
842
+ console.log(t('analyze.summary') || '\n📊 ANALYSIS SUMMARY');
843
+ console.log('='.repeat(50));
844
+
845
+ results.forEach(({ language, analysis }) => {
846
+ console.log(t('analyze.languageStats', {
847
+ language,
848
+ percentage: analysis.summary.percentage,
849
+ translated: analysis.summary.translatedKeys,
850
+ total: analysis.summary.totalKeys
851
+ }) || `${language}: ${analysis.summary.percentage}% complete (${analysis.summary.translatedKeys}/${analysis.summary.totalKeys} keys)`);
852
+ });
853
+
854
+ console.log(t('analyze.finished') || '\n✅ Analysis completed successfully!');
855
+
856
+ // Only prompt for input if running standalone and not in no-prompt mode
857
+ if (require.main === module && !this.noPrompt) {
858
+ await this.prompt('\nPress Enter to continue...');
859
+ }
860
+ this.closeReadline();
861
+
862
+ return results;
863
+
864
+ } catch (error) {
865
+ console.error(t('analyze.error') || '❌ Analysis failed:', error.message);
866
+ this.closeReadline();
867
+ throw error;
868
+ }
869
+ }
870
+
871
+ // Main analysis process
872
+ async run(options = {}) {
873
+ const fromMenu = options.fromMenu || false;
874
+
875
+ try {
876
+ const args = this.parseArgs();
877
+
878
+ if (args.help) {
879
+ this.showHelp();
880
+ return;
881
+ }
882
+
883
+ // Handle setup wizard
884
+ if (args['setup-wizard'] || args.wizard) {
885
+ return await this.runSetupWizard();
886
+ }
887
+
888
+ // Initialize configuration properly when called from menu
889
+ if (fromMenu && !this.sourceDir) {
890
+ const baseConfig = await getUnifiedConfig('analyze', args);
891
+ this.config = { ...baseConfig, ...this.config };
892
+
893
+ const uiLanguage = this.config.uiLanguage || 'en';
894
+ loadTranslations(uiLanguage, path.resolve(__dirname, '../../../resources', 'i18n', 'ui-locales'));
895
+
896
+ this.sourceDir = this.config.sourceDir;
897
+ this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
898
+ this.outputDir = this.config.outputDir;
899
+ }
900
+
901
+ // Skip admin authentication when called from menu system (i18ntk-manage.js) or when --no-prompt is used
902
+ // Authentication is handled by the menu system
903
+ const isCalledDirectly = require.main === module;
904
+ if (isCalledDirectly && !args.noPrompt && !fromMenu) {
905
+ // Only check admin authentication when running directly and not in no-prompt mode
906
+ const AdminAuth = require('../../../utils/admin-auth');
907
+ const adminAuth = new AdminAuth();
908
+ await adminAuth.initialize();
909
+
910
+ const isRequired = await adminAuth.isAuthRequired();
911
+ if (isRequired) {
912
+ console.log('\n' + t('adminCli.authRequiredForOperation', { operation: 'analyze translations' }));
913
+ const cliHelper = require('../../../utils/cli-helper');
914
+ const pin = await cliHelper.promptPin(t('adminCli.enterPin'));
915
+ const isValid = await this.adminAuth.verifyPin(pin);
916
+
917
+ if (!isValid) {
918
+ console.log(t('adminCli.invalidPin'));
919
+ this.closeReadline();
920
+ if (!fromMenu) process.exit(1);
921
+ return;
922
+ }
923
+
924
+ console.log(t('adminCli.authenticationSuccess'));
925
+ }
926
+ }
927
+
928
+ // Set noPrompt flag - skip prompts when called from menu
929
+ this.noPrompt = args.noPrompt || fromMenu;
930
+
931
+ // Handle UI language change
932
+ if (args.uiLanguage) {
933
+ loadTranslations(args.uiLanguage, path.resolve(__dirname, '../../../ui-locales'));}
934
+
935
+ // Update config if source directory is provided
936
+ if (args.sourceDir) {
937
+ this.config.sourceDir = args.sourceDir;
938
+ this.sourceDir = path.resolve(PROJECT_ROOT, this.config.sourceDir);
939
+ this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
940
+ }
941
+
942
+ if (args.outputDir) {
943
+ this.config.outputDir = args.outputDir;
944
+ this.outputDir = path.resolve(this.config.outputDir);
945
+ }
946
+ const execute = async () => {
947
+ await this.analyze();
948
+ };
949
+
950
+ if (args.watch) {
951
+ await execute();
952
+ let running = false;
953
+ watchLocales(this.sourceDir, async () => {
954
+ if (running) return;
955
+ running = true;
956
+ try {
957
+ await execute();
958
+ } finally {
959
+ running = false;
960
+ }
961
+ });
962
+ console.log('��� Watching for translation changes. Press Ctrl+C to exit.');
963
+ } else {
964
+ await execute();
965
+ if (!fromMenu && require.main === module) {
966
+ process.exit(0);
967
+ }
968
+ }
969
+ } catch (error) {
970
+ console.error(t('analyze.error') || '❌ Analysis failed:', error.message);
971
+ this.closeReadline();
972
+ if (!fromMenu && require.main === module) {
973
+ process.exit(1);
974
+ }
975
+ }
976
+ }
977
+
978
+ async runSetupWizard() {
979
+ console.log('Translation Analysis Setup Wizard');
980
+ console.log('='.repeat(50));
981
+
982
+ try {
983
+ const structure = this.detectStructureType();
984
+ console.log(`Current structure detected: ${structure.type}`);
985
+
986
+ const structureOptions = ['monolith', 'directory', 'namespace', 'mixed'];
987
+ const defaultStructureIndex = structureOptions.indexOf(structure.type);
988
+ const defaultStructureChoice = defaultStructureIndex >= 0 ? String(defaultStructureIndex + 1) : '4';
989
+
990
+ console.log('Choose your translation file structure:');
991
+ console.log('1) Monolith files (en.json, de.json, etc.)');
992
+ console.log('2) Directory structure (en/common.json, de/common.json)');
993
+ console.log('3) Namespace structure (common/en.json, forms/en.json)');
994
+ console.log('4) Mixed structure (auto-detect)');
995
+ const structureChoiceInput = await this.prompt(`Select option [${defaultStructureChoice}]: `);
996
+ const structureChoice = structureChoiceInput.trim() || defaultStructureChoice;
997
+ const structureType = structureOptions[Number(structureChoice) - 1] || 'mixed';
998
+
999
+ const sourceDirInput = await this.prompt(`Enter source directory path [${this.sourceDir}]: `);
1000
+ const sourceDir = sourceDirInput.trim() || this.sourceDir;
1001
+ if (!SecurityUtils.safeExistsSync(sourceDir, process.cwd())) {
1002
+ console.log('Setup cancelled: directory does not exist.');
1003
+ return { success: false, cancelled: true };
1004
+ }
1005
+
1006
+ const languagesInput = await this.prompt('Enter languages to analyze (comma-separated) [de,fr,es,ja,ru]: ');
1007
+ const languagesValue = languagesInput.trim() || 'de,fr,es,ja,ru';
1008
+ const languages = languagesValue.split(',').map(lang => lang.trim()).filter(Boolean);
1009
+ if (languages.length === 0) {
1010
+ console.log('Setup cancelled: no languages provided.');
1011
+ return { success: false, cancelled: true };
1012
+ }
1013
+
1014
+ const outputReportsInput = await this.prompt('Generate detailed reports for each language? (Y/n): ');
1015
+ const outputReports = !['n', 'no'].includes(outputReportsInput.trim().toLowerCase());
1016
+
1017
+ const outputDirInput = await this.prompt(`Enter output directory for reports [${this.outputDir}]: `);
1018
+ const outputDir = outputDirInput.trim() || this.outputDir;
1019
+
1020
+ const response = {
1021
+ structureType,
1022
+ sourceDir,
1023
+ languages: languagesValue,
1024
+ outputReports,
1025
+ outputDir
1026
+ };
1027
+
1028
+ this.sourceDir = path.resolve(response.sourceDir);
1029
+ this.outputDir = path.resolve(response.outputDir);
1030
+ this.outputReports = response.outputReports;
1031
+
1032
+ console.log('\nConfiguration Summary:');
1033
+ console.log(`Source: ${this.sourceDir}`);
1034
+ console.log(`Output: ${this.outputDir}`);
1035
+ console.log(`Languages: ${languages.join(", ")}`);
1036
+ console.log(`Structure: ${response.structureType}`);
1037
+
1038
+ const proceedInput = await this.prompt('Proceed with analysis? (Y/n): ');
1039
+ const proceed = !['n', 'no'].includes(proceedInput.trim().toLowerCase());
1040
+ if (!proceed) {
1041
+ console.log('Setup cancelled.');
1042
+ return { success: false, cancelled: true };
1043
+ }
1044
+
1045
+ const results = [];
1046
+ for (const language of languages) {
1047
+ try {
1048
+ console.log(`\nAnalyzing ${language}...`);
1049
+ const result = await this.analyzeLanguage(language);
1050
+ results.push({ language, ...result });
1051
+
1052
+ if (this.outputReports) {
1053
+ await this.saveReport(language, result);
1054
+ console.log(`Report saved: ${language}.json`);
1055
+ }
1056
+ } catch (error) {
1057
+ console.error(`Error analyzing ${language}:`, error.message);
1058
+ results.push({ language, error: error.message });
1059
+ }
1060
+ }
1061
+
1062
+ const summary = {
1063
+ totalLanguages: results.length,
1064
+ successful: results.filter(r => !r.error).length,
1065
+ failed: results.filter(r => r.error).length,
1066
+ results
1067
+ };
1068
+
1069
+ await this.saveReport('wizard-summary', {
1070
+ summary,
1071
+ configuration: response,
1072
+ timestamp: new Date().toISOString()
1073
+ });
1074
+
1075
+ console.log('\nSetup complete.');
1076
+ console.log(`Analyzed ${summary.successful}/${summary.totalLanguages} languages successfully`);
1077
+
1078
+ return {
1079
+ success: true,
1080
+ summary,
1081
+ configuration: response
1082
+ };
1083
+
1084
+ } catch (error) {
1085
+ console.error('Setup wizard error:', error.message);
1086
+ return { success: false, error: error.message };
1087
+ }
1088
+ }
1089
+
1090
+ /**
1091
+ * Execute the analyze command
1092
+ */
1093
+ async execute(options = {}) {
1094
+ try {
1095
+ await this.initialize();
1096
+ await this.run(options);
1097
+ return { success: true, command: 'analyze' };
1098
+ } catch (error) {
1099
+ console.error(`Analyze command failed: ${error.message}`);
1100
+ throw error;
1101
+ }
1102
+ }
1103
+
1104
+ /**
1105
+ * Get command metadata
1106
+ */
1107
+ getMetadata() {
1108
+ return {
1109
+ name: 'analyze',
1110
+ description: 'Analyze translation files for issues and completeness',
1111
+ category: 'analysis',
1112
+ aliases: [],
1113
+ usage: 'analyze [options]',
1114
+ examples: [
1115
+ 'analyze',
1116
+ 'analyze --source-dir=./src/locales',
1117
+ 'analyze --output-dir=./reports'
1118
+ ]
1119
+ };
1120
+ }
1121
+ }
1122
+
1123
+ module.exports = AnalyzeCommand;
1124
+