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.
- package/LICENSE +1 -1
- package/README.md +141 -1191
- package/main/i18ntk-analyze.js +65 -84
- package/main/i18ntk-backup-class.js +420 -0
- package/main/i18ntk-backup.js +3 -3
- package/main/i18ntk-complete.js +90 -65
- package/main/i18ntk-doctor.js +123 -103
- package/main/i18ntk-fixer.js +61 -725
- package/main/i18ntk-go.js +14 -15
- package/main/i18ntk-init.js +77 -26
- package/main/i18ntk-java.js +27 -32
- package/main/i18ntk-js.js +70 -68
- package/main/i18ntk-manage.js +129 -30
- package/main/i18ntk-php.js +75 -75
- package/main/i18ntk-py.js +55 -56
- package/main/i18ntk-scanner.js +59 -57
- package/main/i18ntk-setup.js +9 -404
- package/main/i18ntk-sizing.js +6 -6
- package/main/i18ntk-summary.js +21 -18
- package/main/i18ntk-ui.js +11 -10
- package/main/i18ntk-usage.js +54 -18
- package/main/i18ntk-validate.js +13 -13
- package/main/manage/commands/AnalyzeCommand.js +1124 -0
- package/main/manage/commands/BackupCommand.js +62 -0
- package/main/manage/commands/CommandRouter.js +295 -0
- package/main/manage/commands/CompleteCommand.js +61 -0
- package/main/manage/commands/DoctorCommand.js +60 -0
- package/main/manage/commands/FixerCommand.js +624 -0
- package/main/manage/commands/InitCommand.js +62 -0
- package/main/manage/commands/ScannerCommand.js +654 -0
- package/main/manage/commands/SizingCommand.js +60 -0
- package/main/manage/commands/SummaryCommand.js +61 -0
- package/main/manage/commands/UsageCommand.js +60 -0
- package/main/manage/commands/ValidateCommand.js +978 -0
- package/main/manage/index-fixed.js +1447 -0
- package/main/manage/index.js +1462 -0
- package/main/manage/managers/DebugMenu.js +140 -0
- package/main/manage/managers/InteractiveMenu.js +177 -0
- package/main/manage/managers/LanguageMenu.js +62 -0
- package/main/manage/managers/SettingsMenu.js +53 -0
- package/main/manage/services/AuthenticationService.js +263 -0
- package/main/manage/services/ConfigurationService-fixed.js +449 -0
- package/main/manage/services/ConfigurationService.js +449 -0
- package/main/manage/services/FileManagementService.js +368 -0
- package/main/manage/services/FrameworkDetectionService.js +458 -0
- package/main/manage/services/InitService.js +1051 -0
- package/main/manage/services/SetupService.js +462 -0
- package/main/manage/services/SummaryService.js +450 -0
- package/main/manage/services/UsageService.js +1502 -0
- package/package.json +32 -29
- package/runtime/enhanced.d.ts +221 -221
- package/runtime/index.d.ts +29 -29
- package/runtime/index.full.d.ts +331 -331
- package/runtime/index.js +7 -6
- package/scripts/build-lite.js +17 -17
- package/scripts/deprecate-versions.js +23 -6
- package/scripts/export-translations.js +5 -5
- package/scripts/fix-all-i18n.js +3 -3
- package/scripts/fix-and-purify-i18n.js +3 -2
- package/scripts/fix-locale-control-chars.js +110 -0
- package/scripts/lint-locales.js +80 -0
- package/scripts/locale-optimizer.js +8 -8
- package/scripts/prepublish.js +21 -21
- package/scripts/security-check.js +117 -117
- package/scripts/sync-translations.js +4 -4
- package/scripts/sync-ui-locales.js +9 -8
- package/scripts/validate-all-translations.js +8 -7
- package/scripts/verify-deprecations.js +157 -161
- package/scripts/verify-translations.js +6 -5
- package/settings/i18ntk-config.json +282 -282
- package/settings/language-config.json +5 -5
- package/settings/settings-cli.js +9 -9
- package/settings/settings-manager.js +18 -18
- package/ui-locales/de.json +2417 -2348
- package/ui-locales/en.json +2415 -2352
- package/ui-locales/es.json +2425 -2353
- package/ui-locales/fr.json +2418 -2348
- package/ui-locales/ja.json +2463 -2361
- package/ui-locales/ru.json +2463 -2359
- package/ui-locales/zh.json +2418 -2351
- package/utils/admin-auth.js +2 -2
- package/utils/admin-cli.js +297 -297
- package/utils/admin-pin.js +9 -9
- package/utils/cli-helper.js +9 -9
- package/utils/config-helper.js +73 -104
- package/utils/config-manager.js +204 -171
- package/utils/config.js +5 -4
- package/utils/env-manager.js +249 -263
- package/utils/framework-detector.js +27 -24
- package/utils/i18n-helper.js +85 -41
- package/utils/init-helper.js +152 -94
- package/utils/json-output.js +98 -98
- package/utils/mini-commander.js +179 -0
- package/utils/missing-key-validator.js +5 -5
- package/utils/plugin-loader.js +40 -29
- package/utils/prompt.js +14 -44
- package/utils/safe-json.js +40 -0
- package/utils/secure-errors.js +3 -3
- package/utils/security-check-improved.js +390 -0
- package/utils/security-config.js +5 -5
- package/utils/security-fixed.js +607 -0
- package/utils/security.js +652 -602
- package/utils/setup-enforcer.js +136 -44
- package/utils/setup-validator.js +33 -32
- package/utils/ultra-performance-optimizer.js +11 -9
- package/utils/watch-locales.js +2 -1
- package/utils/prompt-fixed.js +0 -55
- 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;
|