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,978 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * I18NTK VALIDATE COMMAND
5
+ *
6
+ * Handles translation validation logic.
7
+ * Contains embedded business logic from I18nValidator.
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { loadTranslations, t } = require('../../../utils/i18n-helper');
13
+ const configManager = require('../../../utils/config-manager');
14
+ const SecurityUtils = require('../../../utils/security');
15
+ const AdminCLI = require('../../../utils/admin-cli');
16
+ const watchLocales = require('../../../utils/watch-locales');
17
+ const { getGlobalReadline, closeGlobalReadline } = require('../../../utils/cli');
18
+ const { getUnifiedConfig, parseCommonArgs, displayHelp, validateSourceDir, displayPaths } = require('../../../utils/config-helper');
19
+ const I18nInitializer = require('../../i18ntk-init');
20
+ const JsonOutput = require('../../../utils/json-output');
21
+ const ExitCodes = require('../../../utils/exit-codes');
22
+
23
+ loadTranslations('en', path.resolve(__dirname, '../../../resources', 'i18n', 'ui-locales'));
24
+
25
+ class ValidateCommand {
26
+ constructor(config = {}, ui = null) {
27
+ this.config = config;
28
+ this.ui = ui;
29
+ this.prompt = null;
30
+ this.isNonInteractiveMode = false;
31
+ this.safeClose = null;
32
+
33
+ // Initialize validation properties
34
+ this.errors = [];
35
+ this.warnings = [];
36
+ this.rl = null;
37
+ this.sourceDir = null;
38
+ this.i18nDir = null;
39
+ this.sourceLanguageDir = null;
40
+ }
41
+
42
+ /**
43
+ * Set runtime dependencies for interactive operations
44
+ */
45
+ setRuntimeDependencies(prompt, isNonInteractiveMode, safeClose) {
46
+ this.prompt = prompt;
47
+ this.isNonInteractiveMode = isNonInteractiveMode;
48
+ this.safeClose = safeClose;
49
+ }
50
+
51
+ /**
52
+ * Initialize the validator with configuration
53
+ */
54
+ async initialize() {
55
+ try {
56
+ // Initialize i18n with UI language first
57
+ const args = this.parseArgs();
58
+ if (args.help) {
59
+ displayHelp('i18ntk-validate', {
60
+ 'setup-admin': 'Configure admin PIN protection',
61
+ 'disable-admin': 'Disable admin PIN protection',
62
+ 'admin-status': 'Check admin PIN status'
63
+ });
64
+ process.exit(0);
65
+ }
66
+
67
+ const baseConfig = await getUnifiedConfig('validate', args);
68
+ this.config = { ...baseConfig, ...(this.config || {}) };
69
+
70
+ const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
71
+ loadTranslations(uiLanguage, path.resolve(__dirname, '../../../resources', 'i18n', 'ui-locales'));
72
+
73
+ SecurityUtils.logSecurityEvent(
74
+ 'I18n validator initializing',
75
+ 'info',
76
+ { message: 'Initializing I18n validator' }
77
+ );
78
+
79
+ // Use the i18n directory for language files
80
+ this.sourceDir = this.config.i18nDir || this.config.sourceDir;
81
+ this.i18nDir = this.config.i18nDir || this.config.sourceDir;
82
+ this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
83
+
84
+ try {
85
+ validateSourceDir(this.sourceDir, 'i18ntk-validate');
86
+ } catch (err) {
87
+ console.log(t('init.requiredTitle'));
88
+ console.log(t('init.requiredBody'));
89
+ const answer = await this.prompt(t('init.promptRunNow'));
90
+ if (answer.trim().toLowerCase() === 'y') {
91
+ const initializer = new I18nInitializer(this.config);
92
+ await initializer.run({ fromMenu: true });
93
+ } else {
94
+ console.warn(t('config.dirFallbackWarning', { dir: this.sourceDir, fallback: this.sourceLanguageDir }) ||
95
+ `Warning: Directory ${this.sourceDir} not found. Using ${this.sourceLanguageDir}.`);
96
+ if (!SecurityUtils.safeExistsSync(this.sourceLanguageDir)) {
97
+ fs.mkdirSync(this.sourceLanguageDir, { recursive: true });
98
+ }
99
+ }
100
+ }
101
+
102
+ displayPaths({ sourceDir: this.sourceDir, i18nDir: this.i18nDir, outputDir: this.config.outputDir });
103
+
104
+ SecurityUtils.logSecurityEvent(
105
+ 'I18n validator initialized successfully',
106
+ 'info',
107
+ { message: 'I18n validator initialized successfully' }
108
+ );
109
+ } catch (error) {
110
+ SecurityUtils.logSecurityEvent(
111
+ 'I18n validator initialization error',
112
+ 'error',
113
+ { message: `Validator initialization error: ${error.message}` }
114
+ );
115
+ throw error;
116
+ }
117
+ }
118
+
119
+ initReadline() {
120
+ return getGlobalReadline();
121
+ }
122
+
123
+ prompt(question) {
124
+ return new Promise(resolve => {
125
+ const rl = getGlobalReadline();
126
+ rl.question(question, answer => {
127
+ resolve(answer);
128
+ });
129
+ });
130
+ }
131
+
132
+ closeReadline() {
133
+ closeGlobalReadline();
134
+ }
135
+
136
+ // Parse command line arguments
137
+ parseArgs() {
138
+ try {
139
+ const baseArgs = parseCommonArgs(process.argv.slice(2));
140
+
141
+ // Handle shorthand language flags
142
+ const args = process.argv.slice(2);
143
+ args.forEach(arg => {
144
+ const sanitizedArg = SecurityUtils.sanitizeInput(arg);
145
+ if (sanitizedArg.startsWith('--') && !sanitizedArg.includes('=')) {
146
+ const key = sanitizedArg.substring(2);
147
+ if (['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(key)) {
148
+ baseArgs.uiLanguage = key;
149
+ }
150
+ }
151
+ });
152
+
153
+ return baseArgs;
154
+ } catch (error) {
155
+ throw error;
156
+ }
157
+ }
158
+
159
+ // Add error
160
+ addError(message, details = {}) {
161
+ this.errors.push({ message, details, type: 'error' });
162
+ }
163
+
164
+ // Add warning
165
+ addWarning(message, details = {}) {
166
+ this.warnings.push({ message, details, type: 'warning' });
167
+ }
168
+
169
+ // Get all available languages
170
+ getAvailableLanguages() {
171
+ try {
172
+ if (!SecurityUtils.safeExistsSync(this.sourceDir)) {
173
+ throw new Error(`Source directory not found: ${this.sourceDir}`);
174
+ }
175
+
176
+ const languages = fs.readdirSync(this.sourceDir)
177
+ .filter(item => {
178
+ const itemPath = path.join(this.sourceDir, item);
179
+ return fs.statSync(itemPath).isDirectory() && item !== this.config.sourceLanguage;
180
+ });
181
+
182
+ return languages;
183
+ } catch (error) {
184
+ throw error;
185
+ }
186
+ }
187
+
188
+ // Get all JSON files from a language directory
189
+ getLanguageFiles(language) {
190
+ try {
191
+ const sanitizedLanguage = SecurityUtils.sanitizeInput(language);
192
+ const languageDir = path.join(this.sourceDir, sanitizedLanguage);
193
+
194
+ if (!SecurityUtils.safeExistsSync(languageDir)) {
195
+ return [];
196
+ }
197
+
198
+ const files = fs.readdirSync(languageDir)
199
+ .filter(file => {
200
+ return file.endsWith('.json') &&
201
+ !this.config.excludeFiles.includes(file);
202
+ });
203
+
204
+ return files;
205
+ } catch (error) {
206
+ throw error;
207
+ }
208
+ }
209
+
210
+ // Get all keys recursively from an object
211
+ getAllKeys(obj, prefix = '') {
212
+ const keys = new Set();
213
+
214
+ for (const [key, value] of Object.entries(obj)) {
215
+ const fullKey = prefix ? `${prefix}.${key}` : key;
216
+ keys.add(fullKey);
217
+
218
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
219
+ const nestedKeys = this.getAllKeys(value, fullKey);
220
+ nestedKeys.forEach(k => keys.add(k));
221
+ }
222
+ }
223
+
224
+ return keys;
225
+ }
226
+
227
+ // Get value by key path
228
+ getValueByPath(obj, keyPath) {
229
+ // Ensure keyPath is a string
230
+ const keyPathStr = String(keyPath || '');
231
+ const keys = keyPathStr.split('.');
232
+ let current = obj;
233
+
234
+ for (const key of keys) {
235
+ if (current && typeof current === 'object' && key in current) {
236
+ current = current[key];
237
+ } else {
238
+ return undefined;
239
+ }
240
+ }
241
+
242
+ return current;
243
+ }
244
+
245
+ // Validate JSON file syntax
246
+ async validateJsonSyntax(filePath) {
247
+ try {
248
+ const content = SecurityUtils.safeReadFileSync(filePath, path.dirname(filePath), 'utf8');
249
+ const parsed = SecurityUtils.safeParseJSON(content);
250
+
251
+ SecurityUtils.logSecurityEvent(
252
+ t('validate.jsonValidated'),
253
+ 'info',
254
+ { message: `JSON syntax validated: ${filePath}` }
255
+ );
256
+ return { valid: true, data: parsed };
257
+ } catch (error) {
258
+ SecurityUtils.logSecurityEvent(
259
+ t('validate.jsonValidationError'),
260
+ 'error',
261
+ { message: `JSON validation error: ${error.message}` }
262
+ );
263
+ return {
264
+ valid: false,
265
+ error: error.message,
266
+ line: error.message.match(/line (\d+)/)?.[1] || 'unknown'
267
+ };
268
+ }
269
+ }
270
+
271
+ // Validate structural consistency
272
+ validateStructure(sourceObj, targetObj, language, fileName) {
273
+ const sourceKeys = this.getAllKeys(sourceObj);
274
+ const targetKeys = this.getAllKeys(targetObj);
275
+
276
+ const missingKeys = [...sourceKeys].filter(key => !targetKeys.has(key));
277
+ const extraKeys = [...targetKeys].filter(key => !sourceKeys.has(key));
278
+
279
+ // Report missing keys as errors
280
+ missingKeys.forEach(key => {
281
+ this.addError(
282
+ `Missing key in ${language}/${fileName}`,
283
+ { key, language, fileName }
284
+ );
285
+ });
286
+
287
+ // Report extra keys as warnings
288
+ extraKeys.forEach(key => {
289
+ this.addWarning(
290
+ `Extra key in ${language}/${fileName}`,
291
+ { key, language, fileName }
292
+ );
293
+ });
294
+
295
+ return {
296
+ isConsistent: missingKeys.length === 0 && extraKeys.length === 0,
297
+ missingKeys,
298
+ extraKeys,
299
+ sourceKeyCount: sourceKeys.size,
300
+ targetKeyCount: targetKeys.size
301
+ };
302
+ }
303
+
304
+ extractPlaceholders(value, patterns = []) {
305
+ const placeholders = new Set();
306
+ if (value === null || value === undefined) return placeholders;
307
+ const valueStr = String(value);
308
+ patterns.forEach(p => {
309
+ try {
310
+ const reg = new RegExp(p, 'g');
311
+ let m;
312
+ while ((m = reg.exec(valueStr)) !== null) {
313
+ placeholders.add(m[0]);
314
+ }
315
+ } catch (e) {
316
+ // skip invalid patterns
317
+ }
318
+ });
319
+ return placeholders;
320
+ }
321
+
322
+ getGenericPlaceholders(value) {
323
+ if (value === null || value === undefined) return new Set();
324
+ const valueStr = String(value);
325
+ return new Set(valueStr.match(/%s|\{\d+\}|\{\{[^}]+\}\}|\{[^}]+\}/g) || []);
326
+ }
327
+
328
+ checkPlaceholders(source, target, language, fileName, prefix = '') {
329
+ if (typeof source === 'string' && typeof target === 'string') {
330
+ const srcPatterns = (this.config.placeholderStyles && this.config.placeholderStyles[this.config.sourceLanguage]) || [];
331
+ const tgtPatterns = (this.config.placeholderStyles && this.config.placeholderStyles[language]) || [];
332
+ const srcPH = new Set([
333
+ ...this.extractPlaceholders(source, srcPatterns),
334
+ ...this.getGenericPlaceholders(source)
335
+ ]);
336
+ const tgtPH = new Set([
337
+ ...this.extractPlaceholders(target, tgtPatterns),
338
+ ...this.getGenericPlaceholders(target)
339
+ ]);
340
+
341
+ if (tgtPatterns.length) {
342
+ const allowed = this.extractPlaceholders(target, tgtPatterns);
343
+ this.getGenericPlaceholders(target).forEach(ph => {
344
+ if (!allowed.has(ph)) {
345
+ this.addError(`Disallowed placeholder style in ${language}/${fileName}`, { key: prefix, placeholder: ph });
346
+ }
347
+ });
348
+ }
349
+
350
+ if (srcPH.size !== tgtPH.size || [...srcPH].some(p => !tgtPH.has(p))) {
351
+ this.addError(`Placeholder style mismatch in ${language}/${fileName}`, { key: prefix });
352
+ }
353
+ } else if (source && typeof source === 'object' && !Array.isArray(source)) {
354
+ for (const key of Object.keys(source)) {
355
+ if (target && Object.prototype.hasOwnProperty.call(target, key)) {
356
+ this.checkPlaceholders(
357
+ source[key],
358
+ target[key],
359
+ language,
360
+ fileName,
361
+ prefix ? `${prefix}.${key}` : key
362
+ );
363
+ }
364
+ }
365
+ }
366
+ }
367
+
368
+ detectRiskyKeys(obj, language, fileName, prefix = '') {
369
+ for (const [key, value] of Object.entries(obj || {})) {
370
+ const fullKey = prefix ? `${prefix}.${key}` : key;
371
+ if (typeof value === 'string') {
372
+ if (/https?:\/\//.test(value) || /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/.test(value) || /(api[_-]?key|secret|token)/i.test(value)) {
373
+ const reporter = this.config.strictMode ? this.addError.bind(this) : this.addWarning.bind(this);
374
+ reporter(`Risky content in ${language}/${fileName}`, { key: fullKey, value });
375
+ }
376
+ } else if (value && typeof value === 'object' && !Array.isArray(value)) {
377
+ this.detectRiskyKeys(value, language, fileName, fullKey);
378
+ }
379
+ }
380
+ }
381
+
382
+ // Validate translation completeness
383
+ validateTranslation(obj, language, fileName, prefix = '') {
384
+ let totalKeys = 0;
385
+ let translatedKeys = 0;
386
+ let issues = [];
387
+
388
+ for (const [key, value] of Object.entries(obj)) {
389
+ const fullKey = prefix ? `${prefix}.${key}` : key;
390
+
391
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
392
+ const nested = this.validateTranslation(value, language, fileName, fullKey);
393
+ totalKeys += nested.totalKeys;
394
+ translatedKeys += nested.translatedKeys;
395
+ issues.push(...nested.issues);
396
+ } else if (typeof value === 'string') {
397
+ totalKeys++;
398
+
399
+ const markers = this.config.notTranslatedMarkers || [this.config.notTranslatedMarker];
400
+ if (markers.some(m => value === m)) {
401
+ issues.push({
402
+ type: 'not_translated',
403
+ key: fullKey,
404
+ value,
405
+ language,
406
+ fileName
407
+ });
408
+ } else if (value === '') {
409
+ issues.push({
410
+ type: 'empty_value',
411
+ key: fullKey,
412
+ value,
413
+ language,
414
+ fileName
415
+ });
416
+ } else if (markers.some(m => value.includes(m))) {
417
+ issues.push({
418
+ type: 'partial_translation',
419
+ key: fullKey,
420
+ value,
421
+ language,
422
+ fileName
423
+ });
424
+ } else {
425
+ translatedKeys++;
426
+ }
427
+ }
428
+ }
429
+
430
+ return { totalKeys, translatedKeys, issues };
431
+ }
432
+
433
+ // Validate a single language
434
+ async validateLanguage(language) {
435
+ try {
436
+ SecurityUtils.logSecurityEvent(
437
+ t('validate.languageValidation'),
438
+ 'info',
439
+ { message: `Validating language: ${language}` }
440
+ );
441
+
442
+ const sanitizedLanguage = SecurityUtils.sanitizeInput(language);
443
+ const languageDir = path.join(this.sourceDir, sanitizedLanguage);
444
+ const sourceFiles = this.getLanguageFiles(this.config.sourceLanguage);
445
+ const targetFiles = this.getLanguageFiles(sanitizedLanguage);
446
+
447
+ const validation = {
448
+ language: sanitizedLanguage,
449
+ files: {},
450
+ summary: {
451
+ totalFiles: sourceFiles.length,
452
+ validFiles: 0,
453
+ totalKeys: 0,
454
+ translatedKeys: 0,
455
+ missingFiles: [],
456
+ syntaxErrors: [],
457
+ structuralIssues: [],
458
+ translationIssues: []
459
+ }
460
+ };
461
+
462
+ // Check for missing language directory
463
+ if (!SecurityUtils.safeExistsSync(languageDir)) {
464
+ this.addError(
465
+ `Language directory missing: ${sanitizedLanguage}`,
466
+ { language: sanitizedLanguage, expectedPath: languageDir }
467
+ );
468
+ return validation;
469
+ }
470
+
471
+ // Validate each file
472
+ for (const fileName of sourceFiles) {
473
+ const sourceFilePath = path.join(this.sourceLanguageDir, fileName);
474
+ const targetFilePath = path.join(languageDir, fileName);
475
+
476
+ // Check if source file exists
477
+ if (!SecurityUtils.safeExistsSync(sourceFilePath)) {
478
+ this.addWarning(
479
+ `Source file missing: ${this.config.sourceLanguage}/${fileName}`,
480
+ { fileName, language: this.config.sourceLanguage }
481
+ );
482
+ continue;
483
+ }
484
+
485
+ // Check if target file exists
486
+ if (!SecurityUtils.safeExistsSync(targetFilePath)) {
487
+ this.addError(
488
+ `Translation file missing: ${language}/${fileName}`,
489
+ { fileName, language, expectedPath: targetFilePath }
490
+ );
491
+ validation.summary.missingFiles.push(fileName);
492
+ continue;
493
+ }
494
+
495
+ // Validate JSON syntax for both files
496
+ const sourceValidation = await this.validateJsonSyntax(sourceFilePath);
497
+ const targetValidation = await this.validateJsonSyntax(targetFilePath);
498
+
499
+ if (!sourceValidation.valid) {
500
+ this.addError(
501
+ `Invalid JSON syntax in source file: ${this.config.sourceLanguage}/${fileName}`,
502
+ { fileName, language: this.config.sourceLanguage, error: sourceValidation.error }
503
+ );
504
+ validation.summary.syntaxErrors.push({ fileName, type: 'source', error: sourceValidation.error });
505
+ continue;
506
+ }
507
+
508
+ if (!targetValidation.valid) {
509
+ this.addError(
510
+ `Invalid JSON syntax in target file: ${sanitizedLanguage}/${fileName}`,
511
+ { fileName, language: sanitizedLanguage, error: targetValidation.error }
512
+ );
513
+ validation.summary.syntaxErrors.push({ fileName, type: 'target', error: targetValidation.error });
514
+ continue;
515
+ }
516
+
517
+ // Use parsed data from validation
518
+ const sourceContent = sourceValidation.data;
519
+ const targetContent = targetValidation.data;
520
+ // Validate structure
521
+ const structural = this.validateStructure(sourceContent, targetContent, language, fileName);
522
+
523
+ // Validate translations
524
+ const translations = this.validateTranslation(targetContent, language, fileName);
525
+ this.checkPlaceholders(sourceContent, targetContent, language, fileName);
526
+ this.detectRiskyKeys(targetContent, language, fileName);
527
+
528
+ // Store file validation results
529
+ validation.files[fileName] = {
530
+ status: 'validated',
531
+ structural,
532
+ translations,
533
+ sourceFilePath,
534
+ targetFilePath
535
+ };
536
+
537
+ // Update summary
538
+ validation.summary.validFiles++;
539
+ validation.summary.totalKeys += translations.totalKeys;
540
+ validation.summary.translatedKeys += translations.translatedKeys;
541
+
542
+ if (!structural.isConsistent) {
543
+ validation.summary.structuralIssues.push({
544
+ fileName,
545
+ missingKeys: structural.missingKeys.length,
546
+ extraKeys: structural.extraKeys.length
547
+ });
548
+ }
549
+
550
+ validation.summary.translationIssues.push(...translations.issues);
551
+ }
552
+
553
+ // Calculate completion percentage
554
+ validation.summary.percentage = validation.summary.totalKeys > 0
555
+ ? Math.round((validation.summary.translatedKeys / validation.summary.totalKeys) * 100)
556
+ : 0;
557
+
558
+ return validation;
559
+ } catch (error) {
560
+ SecurityUtils.logSecurityEvent(t('validate.languageValidationError'), 'error', {
561
+ language: language,
562
+ error: error.message,
563
+ timestamp: new Date().toISOString()
564
+ });
565
+
566
+ this.addError(
567
+ `Language validation failed for ${language}: ${error.message}`,
568
+ { language, error: error.message }
569
+ );
570
+
571
+ return {
572
+ language: language,
573
+ files: {},
574
+ summary: {
575
+ totalFiles: 0,
576
+ validFiles: 0,
577
+ totalKeys: 0,
578
+ translatedKeys: 0,
579
+ missingFiles: [],
580
+ syntaxErrors: [],
581
+ structuralIssues: [],
582
+ translationIssues: [],
583
+ percentage: 0
584
+ }
585
+ };
586
+ }
587
+ }
588
+
589
+ // Check for unused translation keys (basic implementation)
590
+ checkUnusedKeys(language) {
591
+ // Note: For comprehensive unused key detection, use the dedicated
592
+ // usage analysis script: i18ntk-usage.js
593
+ const warnings = [];
594
+
595
+ // This method provides basic validation only
596
+ // For detailed usage analysis, run: node i18ntk-usage.js
597
+
598
+ return warnings;
599
+ }
600
+
601
+ // Show help message
602
+ showHelp() {
603
+ console.log(t('validate.help_message'));
604
+ }
605
+
606
+ // Main validation process
607
+ async validate() {
608
+ try {
609
+ const args = this.parseArgs();
610
+ const jsonOutput = new JsonOutput('validate');
611
+
612
+ if (!args.json) {
613
+ console.log(t('validate.title'));
614
+ console.log(t('validate.message'));
615
+
616
+ // Delete old validation report if it exists
617
+ const reportPath = path.join(process.cwd(), 'validation-report.txt');
618
+ SecurityUtils.validatePath(reportPath);
619
+
620
+ if (SecurityUtils.safeExistsSync(reportPath)) {
621
+ fs.unlinkSync(reportPath);
622
+ console.log(t('validate.deletedOldReport'));
623
+
624
+ SecurityUtils.logSecurityEvent(t('validate.fileDeleted'), 'info', {
625
+ path: reportPath,
626
+ timestamp: new Date().toISOString()
627
+ });
628
+ }
629
+ }
630
+
631
+ // Handle UI language change
632
+ if (args.uiLanguage) {
633
+ loadTranslations(args.uiLanguage, path.resolve(__dirname, '../../../ui-locales'));}
634
+
635
+ if (args.sourceDir) {
636
+ this.config.sourceDir = args.sourceDir;
637
+ this.sourceDir = path.resolve(this.config.sourceDir);
638
+ this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
639
+ }
640
+ if (args.strictMode) {
641
+ this.config.strictMode = true;
642
+ }
643
+
644
+ if (!args.json) {
645
+ console.log(t('validate.sourceDirectory', { dir: this.sourceDir }));
646
+ console.log(t('validate.sourceLanguage', { sourceLanguage: this.config.sourceLanguage }));
647
+ console.log(t('validate.strictMode', { mode: this.config.strictMode ? 'ON' : 'OFF' }));
648
+ }
649
+
650
+ // Validate source language directory exists
651
+ SecurityUtils.validatePath(this.sourceLanguageDir);
652
+
653
+ if (!SecurityUtils.safeExistsSync(this.sourceLanguageDir)) {
654
+ const error = t('validate.sourceLanguageDirectoryNotFound', { sourceDir: this.sourceLanguageDir }) || 'Source language directory not found';
655
+ this.addError(error, { sourceLanguage: this.config.sourceLanguage });
656
+
657
+ SecurityUtils.logSecurityEvent(t('validate.validationError'), 'error', {
658
+ error: 'Source language directory not found',
659
+ path: this.sourceLanguageDir,
660
+ timestamp: new Date().toISOString()
661
+ });
662
+
663
+ if (args.json) {
664
+ jsonOutput.setStatus('error', error);
665
+ console.log(JSON.stringify(jsonOutput.getOutput(), null, args.indent || 2));
666
+ return { success: false, error };
667
+ }
668
+ throw new Error(error);
669
+ }
670
+
671
+ // Get available languages including source language
672
+ const availableLanguages = this.getAvailableLanguages();
673
+
674
+ // Filter languages if specified
675
+ const targetLanguages = args.language
676
+ ? [args.language].filter(lang => availableLanguages.includes(lang))
677
+ : availableLanguages;
678
+
679
+ if (args.language && targetLanguages.length === 0) {
680
+ const error = `Specified language '${args.language}' not found`;
681
+ this.addError(error, { requestedLanguage: args.language, availableLanguages });
682
+ if (args.json) {
683
+ jsonOutput.setStatus('error', error);
684
+ console.log(JSON.stringify(jsonOutput.getOutput(), null, args.indent || 2));
685
+ return { success: false, error };
686
+ }
687
+ throw new Error(error);
688
+ }
689
+
690
+ if (!args.language && targetLanguages.length === 0) {
691
+ const message = t('validate.noTargetLanguages') || 'No target languages configured; skipping target validation.';
692
+ if (args.json) {
693
+ jsonOutput.setStatus('ok', message);
694
+ console.log(JSON.stringify(jsonOutput.getOutput(), null, args.indent || 2));
695
+ return { success: true, message };
696
+ }
697
+ console.log(message);
698
+ return { success: true, message };
699
+ }
700
+
701
+ if (!args.json) {
702
+ console.log(t('validate.validatingLanguages', { langs: targetLanguages.join(', ') }));
703
+ console.log('');
704
+ }
705
+
706
+ const results = {};
707
+ let totalErrors = 0;
708
+ let totalWarnings = 0;
709
+
710
+ // Validate each language
711
+ for (const language of targetLanguages) {
712
+ if (!args.json) {
713
+ console.log(t('validate.validatingLanguage', { lang: language }));
714
+ }
715
+
716
+ const validation = await this.validateLanguage(language);
717
+ results[language] = validation;
718
+
719
+ if (!args.json) {
720
+ // Display brief progress indicator
721
+ const { summary } = validation;
722
+ const status = summary.syntaxErrors.length > 0 ? '❌' :
723
+ summary.missingFiles.length > 0 ? '⚠️' : '✅';
724
+ console.log(` ${status} ${language}: ${summary.percentage}% (${summary.translatedKeys}/${summary.totalKeys} keys)`);
725
+ }
726
+
727
+ // Aggregate issues for JSON output
728
+ totalErrors += validation.errors?.length || 0;
729
+ totalWarnings += validation.warnings?.length || 0;
730
+ }
731
+
732
+ // Prepare JSON output
733
+ if (args.json) {
734
+ const hasErrors = this.errors.length > 0;
735
+ const hasWarnings = this.warnings.length > 0;
736
+
737
+ jsonOutput.setStatus(
738
+ hasErrors ? 'error' : hasWarnings ? 'warn' : 'ok',
739
+ hasErrors ? 'Validation failed' : hasWarnings ? 'Validation completed with warnings' : 'Validation passed'
740
+ );
741
+
742
+ jsonOutput.addStats({
743
+ errors: this.errors.length,
744
+ warnings: this.warnings.length,
745
+ languages: targetLanguages.length,
746
+ files: Object.values(results).reduce((sum, lang) => sum + (lang.files?.length || 0), 0)
747
+ });
748
+
749
+ // Add issues from errors and warnings
750
+ [...this.errors, ...this.warnings].forEach(issue => {
751
+ jsonOutput.addIssue({
752
+ type: issue.message.includes('not found') ? 'missing' :
753
+ issue.message.includes('syntax') ? 'syntax' : 'warning',
754
+ message: issue.message,
755
+ details: issue.details
756
+ });
757
+ });
758
+
759
+ // Add per-language results
760
+ jsonOutput.addData({ results });
761
+
762
+ console.log(JSON.stringify(jsonOutput.getOutput(), null, args.indent || 2));
763
+
764
+ return {
765
+ success: !hasErrors,
766
+ errors: this.errors.length,
767
+ warnings: this.warnings.length,
768
+ results
769
+ };
770
+ }
771
+
772
+ console.log('');
773
+ console.log(t('validate.separator'));
774
+
775
+ // Overall summary
776
+ const hasErrors = this.errors.length > 0;
777
+ const hasWarnings = this.warnings.length > 0;
778
+
779
+ console.log(t('validate.validationSummary'));
780
+ console.log(t('validate.totalErrors', { count: this.errors.length }));
781
+ console.log(t('validate.totalWarnings', { count: this.warnings.length }));
782
+
783
+ // Show errors
784
+ if (hasErrors) {
785
+ console.log('');
786
+ console.log(t('validate.separator'));
787
+ console.log(t('validate.errorsSection'));
788
+ console.log('');
789
+ this.errors.forEach((error, index) => {
790
+ console.log(` ❌ ${error.message}`);
791
+ if (error.details && Object.keys(error.details).length > 0) {
792
+ console.log(` 📄 Details: ${JSON.stringify(error.details, null, 2)}`);
793
+ }
794
+ console.log('');
795
+ });
796
+ }
797
+
798
+ // Show warnings
799
+ if (hasWarnings) {
800
+ console.log('');
801
+ console.log(t('validate.separator'));
802
+ console.log(t('validate.warningsSection'));
803
+ console.log('');
804
+ this.warnings.forEach((warning, index) => {
805
+ console.log(` ⚠️ ${warning.message}`);
806
+ if (warning.details && Object.keys(warning.details).length > 0) {
807
+ console.log(` 📄 Details: ${JSON.stringify(warning.details, null, 2)}`);
808
+ }
809
+ console.log('');
810
+ });
811
+ }
812
+
813
+ // Recommendations
814
+ console.log('');
815
+ console.log(t('validate.separator'));
816
+ console.log(t('validate.recommendationsSection'));
817
+
818
+ if (hasErrors) {
819
+ console.log('');
820
+ console.log(t('validate.resolveMissingFilesAndSyntaxErrors'));
821
+ console.log(t('validate.fixStructuralInconsistencies'));
822
+ console.log(t('validate.completeMissingTranslations'));
823
+ console.log(t('validate.rerunValidation'));
824
+ } else if (hasWarnings) {
825
+ console.log('');
826
+ console.log(t('validate.addressWarnings'));
827
+ console.log(t('validate.reviewWarnings'));
828
+ console.log(t('validate.considerRunningWithStrict'));
829
+ } else {
830
+ console.log('');
831
+ console.log(t('validate.allValidationsPassed'));
832
+ console.log(t('validate.considerRunningUsageAnalysis'));
833
+ }
834
+
835
+ // Exit with appropriate code
836
+ const success = !hasErrors && (!hasWarnings || !this.config.strictMode);
837
+
838
+ return {
839
+ success,
840
+ errors: this.errors.length,
841
+ warnings: this.warnings.length,
842
+ results
843
+ };
844
+
845
+ } catch (error) {
846
+ console.error(t("validate.validation_failed", { error: error.message }));
847
+ return {
848
+ success: false,
849
+ error: error.message
850
+ };
851
+ }
852
+ }
853
+
854
+ /**
855
+ * Run method for compatibility with manager
856
+ */
857
+ async run(options = {}) {
858
+ const { fromMenu = false } = options;
859
+
860
+ const args = this.parseArgs();
861
+
862
+ // Ensure config is always initialized
863
+ if (!this.config) {
864
+ this.config = {};
865
+ }
866
+
867
+ // Initialize configuration properly when called from menu
868
+ if (fromMenu && !this.sourceDir) {
869
+ const baseConfig = await getUnifiedConfig('validate', args);
870
+ this.config = { ...baseConfig, ...this.config };
871
+
872
+ const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
873
+ loadTranslations(uiLanguage, path.resolve(__dirname, '../../../resources', 'i18n', 'ui-locales'));this.sourceDir = this.config.sourceDir;
874
+ this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
875
+ } else {
876
+ await this.initialize();
877
+ }
878
+
879
+ // Skip admin authentication when called from menu
880
+ if (!fromMenu) {
881
+ // Check admin authentication for sensitive operations (only when called directly and not in no-prompt mode)
882
+ const AdminAuth = require('../../../utils/admin-auth');
883
+ const adminAuth = new AdminAuth();
884
+ await adminAuth.initialize();
885
+
886
+ const isCalledDirectly = require.main === module;
887
+ const isRequired = await adminAuth.isAuthRequired();
888
+ if (isRequired && isCalledDirectly && !args.noPrompt) {
889
+ console.log('\n' + t('adminCli.authRequiredForOperation', { operation: 'validate translations' }));
890
+
891
+ const cliHelper = require('../../../utils/cli-helper');
892
+ const pin = await cliHelper.promptPin(t('adminCli.enterPin'));
893
+
894
+ const isValid = await adminAuth.verifyPin(pin);
895
+ this.closeReadline();
896
+
897
+ if (!isValid) {
898
+ console.log(t('adminCli.invalidPin'));
899
+ if (!fromMenu) process.exit(ExitCodes.SECURITY_VIOLATION);
900
+ return { success: false, error: 'Authentication failed' };
901
+ }
902
+
903
+ console.log(t('adminCli.authenticationSuccess'));
904
+ }
905
+ }
906
+ const execute = async () => {
907
+
908
+ console.log(t('validate.startingValidationProcess'));
909
+ SecurityUtils.logSecurityEvent(
910
+ t('validate.runStarted'),
911
+ 'info',
912
+ { message: 'Starting validation run' }
913
+ );
914
+
915
+ const result = await this.validate();
916
+
917
+ console.log(t('validate.validationProcessCompletedSuccessfully'));
918
+ SecurityUtils.logSecurityEvent(
919
+ t('validate.runCompleted'),
920
+ 'info',
921
+ { message: 'Validation run completed successfully' }
922
+ );
923
+ return result;
924
+ };
925
+
926
+ if (args.watch) {
927
+ await execute();
928
+ let running = false;
929
+ watchLocales(this.sourceDir, async () => {
930
+ if (running) return;
931
+ running = true;
932
+ try {
933
+ await execute();
934
+ } finally {
935
+ running = false;
936
+ }
937
+ });
938
+ console.log('👀 Watching for translation changes. Press Ctrl+C to exit.');
939
+ return { watching: true };
940
+ }
941
+
942
+ return await execute();
943
+ }
944
+
945
+ /**
946
+ * Execute the validate command
947
+ */
948
+ async execute(options = {}) {
949
+ try {
950
+ await this.initialize();
951
+ await this.run(options);
952
+ return { success: true, command: 'validate' };
953
+ } catch (error) {
954
+ console.error(`Validate command failed: ${error.message}`);
955
+ throw error;
956
+ }
957
+ }
958
+
959
+ /**
960
+ * Get command metadata
961
+ */
962
+ getMetadata() {
963
+ return {
964
+ name: 'validate',
965
+ description: 'Validate translations for errors and consistency',
966
+ category: 'analysis',
967
+ aliases: [],
968
+ usage: 'validate [options]',
969
+ examples: [
970
+ 'validate',
971
+ 'validate --source-dir=./src/locales',
972
+ 'validate --strict'
973
+ ]
974
+ };
975
+ }
976
+ }
977
+
978
+ module.exports = ValidateCommand;