i18ntk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +401 -0
- package/LICENSE +21 -0
- package/README.md +507 -0
- package/dev/README.md +37 -0
- package/dev/debug/README.md +30 -0
- package/dev/debug/complete-console-translations.js +295 -0
- package/dev/debug/console-key-checker.js +408 -0
- package/dev/debug/console-translations.js +335 -0
- package/dev/debug/debugger.js +408 -0
- package/dev/debug/export-missing-keys.js +432 -0
- package/dev/debug/final-normalize.js +236 -0
- package/dev/debug/find-extra-keys.js +68 -0
- package/dev/debug/normalize-locales.js +153 -0
- package/dev/debug/refactor-locales.js +240 -0
- package/dev/debug/reorder-locales.js +85 -0
- package/dev/debug/replace-hardcoded-console.js +378 -0
- package/docs/INSTALLATION.md +449 -0
- package/docs/README.md +222 -0
- package/docs/TODO_ROADMAP.md +279 -0
- package/docs/api/API_REFERENCE.md +377 -0
- package/docs/api/COMPONENTS.md +492 -0
- package/docs/api/CONFIGURATION.md +651 -0
- package/docs/api/NPM_PUBLISHING_GUIDE.md +434 -0
- package/docs/debug/DEBUG_README.md +30 -0
- package/docs/debug/DEBUG_TOOLS.md +494 -0
- package/docs/development/AGENTS.md +351 -0
- package/docs/development/DEVELOPMENT_RULES.md +165 -0
- package/docs/development/DEV_README.md +37 -0
- package/docs/release-notes/RELEASE_NOTES_v1.0.0.md +173 -0
- package/docs/release-notes/RELEASE_NOTES_v1.6.0.md +141 -0
- package/docs/release-notes/RELEASE_NOTES_v1.6.1.md +185 -0
- package/docs/release-notes/RELEASE_NOTES_v1.6.3.md +199 -0
- package/docs/reports/ANALYSIS_README.md +17 -0
- package/docs/reports/CONSOLE_MISMATCH_BUG_REPORT_v1.5.0.md +181 -0
- package/docs/reports/SIZING_README.md +18 -0
- package/docs/reports/SUMMARY_README.md +18 -0
- package/docs/reports/TRANSLATION_BUG_REPORT_v1.5.0.md +129 -0
- package/docs/reports/USAGE_README.md +18 -0
- package/docs/reports/VALIDATION_README.md +18 -0
- package/locales/de/auth.json +3 -0
- package/locales/de/common.json +16 -0
- package/locales/de/pagination.json +6 -0
- package/locales/en/auth.json +3 -0
- package/locales/en/common.json +16 -0
- package/locales/en/pagination.json +6 -0
- package/locales/es/auth.json +3 -0
- package/locales/es/common.json +16 -0
- package/locales/es/pagination.json +6 -0
- package/locales/fr/auth.json +3 -0
- package/locales/fr/common.json +16 -0
- package/locales/fr/pagination.json +6 -0
- package/locales/ru/auth.json +3 -0
- package/locales/ru/common.json +16 -0
- package/locales/ru/pagination.json +6 -0
- package/main/i18ntk-analyze.js +625 -0
- package/main/i18ntk-autorun.js +461 -0
- package/main/i18ntk-complete.js +494 -0
- package/main/i18ntk-init.js +686 -0
- package/main/i18ntk-manage.js +848 -0
- package/main/i18ntk-sizing.js +557 -0
- package/main/i18ntk-summary.js +671 -0
- package/main/i18ntk-usage.js +1282 -0
- package/main/i18ntk-validate.js +762 -0
- package/main/ui-i18n.js +332 -0
- package/package.json +152 -0
- package/scripts/fix-missing-translation-keys.js +214 -0
- package/scripts/verify-package.js +168 -0
- package/ui-locales/de.json +637 -0
- package/ui-locales/en.json +688 -0
- package/ui-locales/es.json +637 -0
- package/ui-locales/fr.json +637 -0
- package/ui-locales/ja.json +637 -0
- package/ui-locales/ru.json +637 -0
- package/ui-locales/zh.json +637 -0
- package/utils/admin-auth.js +317 -0
- package/utils/admin-cli.js +353 -0
- package/utils/admin-pin.js +409 -0
- package/utils/detect-language-mismatches.js +454 -0
- package/utils/i18n-helper.js +128 -0
- package/utils/maintain-language-purity.js +433 -0
- package/utils/native-translations.js +478 -0
- package/utils/security.js +384 -0
- package/utils/test-complete-system.js +356 -0
- package/utils/test-console-i18n.js +402 -0
- package/utils/translate-mismatches.js +571 -0
- package/utils/validate-language-purity.js +531 -0
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* I18N TRANSLATION VALIDATION TOOLKIT
|
|
4
|
+
*
|
|
5
|
+
* This script validates translation files for completeness, consistency,
|
|
6
|
+
* and structural integrity across all languages.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npm run i18ntk:validate
|
|
10
|
+
* npm run i18ntk:validate -- --strict
|
|
11
|
+
* npm run i18ntk:validate -- --language=de
|
|
12
|
+
* npm run i18ntk:validate -- --source-dir=./src/i18n/locales
|
|
13
|
+
*
|
|
14
|
+
* Alternative direct usage:
|
|
15
|
+
* node i18ntk-validate.js
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { loadTranslations, t } = require('../utils/i18n-helper');
|
|
21
|
+
const settingsManager = require('../settings/settings-manager');
|
|
22
|
+
const SecurityUtils = require('../utils/security');
|
|
23
|
+
const AdminCLI = require('../utils/admin-cli');
|
|
24
|
+
|
|
25
|
+
// Get configuration from settings manager
|
|
26
|
+
async function getConfig() {
|
|
27
|
+
try {
|
|
28
|
+
SecurityUtils.logSecurityEvent('config_access', 'info', 'Accessing configuration for validation');
|
|
29
|
+
const settings = settingsManager.getSettings();
|
|
30
|
+
const config = {
|
|
31
|
+
sourceDir: settings.directories?.sourceDir || settings.sourceDir || './locales',
|
|
32
|
+
sourceLanguage: settings.directories?.sourceLanguage || settings.sourceLanguage || 'en',
|
|
33
|
+
notTranslatedMarker: settings.processing?.notTranslatedMarker || 'NOT_TRANSLATED',
|
|
34
|
+
excludeFiles: settings.processing?.excludeFiles || ['.DS_Store', 'Thumbs.db'],
|
|
35
|
+
strictMode: settings.processing?.strictMode || false,
|
|
36
|
+
uiLanguage: settings.language || 'en'
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Validate configuration
|
|
40
|
+
SecurityUtils.validateConfig(config);
|
|
41
|
+
SecurityUtils.logSecurityEvent('config_validated', 'info', 'Configuration validated successfully');
|
|
42
|
+
|
|
43
|
+
return config;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
SecurityUtils.logSecurityEvent('config_error', 'error', `Configuration error: ${error.message}`);
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class I18nValidator {
|
|
51
|
+
constructor(config = {}) {
|
|
52
|
+
this.config = config;
|
|
53
|
+
this.errors = [];
|
|
54
|
+
this.warnings = [];
|
|
55
|
+
this.t = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async initialize() {
|
|
59
|
+
try {
|
|
60
|
+
SecurityUtils.logSecurityEvent('validator_init', 'info', 'Initializing I18n validator');
|
|
61
|
+
|
|
62
|
+
const defaultConfig = await getConfig();
|
|
63
|
+
this.config = { ...defaultConfig, ...this.config };
|
|
64
|
+
|
|
65
|
+
// Validate configuration values
|
|
66
|
+
if (!this.config.sourceDir) {
|
|
67
|
+
throw new Error('Source directory not configured');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!this.config.sourceLanguage) {
|
|
71
|
+
throw new Error('Source language not configured');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Validate and resolve paths
|
|
75
|
+
const resolvedSourceDir = path.resolve(this.config.sourceDir);
|
|
76
|
+
this.sourceDir = resolvedSourceDir;
|
|
77
|
+
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
78
|
+
|
|
79
|
+
// Initialize i18n with UI language - FIX: Properly initialize translation function
|
|
80
|
+
const uiLanguage = SecurityUtils.sanitizeInput(this.config.uiLanguage || 'en');
|
|
81
|
+
loadTranslations(uiLanguage);
|
|
82
|
+
this.t = t; // Assign the translation function
|
|
83
|
+
|
|
84
|
+
// Verify translation function is working
|
|
85
|
+
if (typeof this.t !== 'function') {
|
|
86
|
+
throw new Error('Translation function not properly initialized');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
SecurityUtils.logSecurityEvent('validator_initialized', 'info', 'I18n validator initialized successfully');
|
|
90
|
+
} catch (error) {
|
|
91
|
+
SecurityUtils.logSecurityEvent('validator_init_error', 'error', `Validator initialization error: ${error.message}`);
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Parse command line arguments
|
|
97
|
+
parseArgs() {
|
|
98
|
+
try {
|
|
99
|
+
SecurityUtils.logSecurityEvent('args_parsing', 'info', 'Parsing command line arguments');
|
|
100
|
+
|
|
101
|
+
const args = process.argv.slice(2);
|
|
102
|
+
const parsed = {};
|
|
103
|
+
|
|
104
|
+
args.forEach(arg => {
|
|
105
|
+
const sanitizedArg = SecurityUtils.sanitizeInput(arg);
|
|
106
|
+
|
|
107
|
+
if (sanitizedArg.startsWith('--')) {
|
|
108
|
+
const [key, value] = sanitizedArg.substring(2).split('=');
|
|
109
|
+
const sanitizedKey = SecurityUtils.sanitizeInput(key);
|
|
110
|
+
const sanitizedValue = value ? SecurityUtils.sanitizeInput(value) : true;
|
|
111
|
+
|
|
112
|
+
if (sanitizedKey === 'language') {
|
|
113
|
+
parsed.language = sanitizedValue;
|
|
114
|
+
} else if (sanitizedKey === 'source-dir') {
|
|
115
|
+
parsed.sourceDir = sanitizedValue;
|
|
116
|
+
} else if (sanitizedKey === 'strict') {
|
|
117
|
+
parsed.strictMode = true;
|
|
118
|
+
} else if (sanitizedKey === 'ui-language') {
|
|
119
|
+
parsed.uiLanguage = sanitizedValue;
|
|
120
|
+
} else if (sanitizedKey === 'help') {
|
|
121
|
+
parsed.help = true;
|
|
122
|
+
} else if (sanitizedKey === 'setup-admin') {
|
|
123
|
+
parsed.setupAdmin = true;
|
|
124
|
+
} else if (sanitizedKey === 'disable-admin') {
|
|
125
|
+
parsed.disableAdmin = true;
|
|
126
|
+
} else if (sanitizedKey === 'admin-status') {
|
|
127
|
+
parsed.adminStatus = true;
|
|
128
|
+
} else if (['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(sanitizedKey)) {
|
|
129
|
+
// Support shorthand language flags like --de, --fr, etc.
|
|
130
|
+
parsed.uiLanguage = sanitizedKey;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
SecurityUtils.logSecurityEvent('args_parsed', 'info', 'Command line arguments parsed successfully');
|
|
136
|
+
return parsed;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
SecurityUtils.logSecurityEvent('args_parse_error', 'error', `Argument parsing error: ${error.message}`);
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Add error
|
|
144
|
+
addError(message, details = {}) {
|
|
145
|
+
this.errors.push({ message, details, type: 'error' });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Add warning
|
|
149
|
+
addWarning(message, details = {}) {
|
|
150
|
+
this.warnings.push({ message, details, type: 'warning' });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Get all available languages
|
|
154
|
+
getAvailableLanguages() {
|
|
155
|
+
try {
|
|
156
|
+
SecurityUtils.logSecurityEvent('languages_scan', 'info', 'Scanning available languages');
|
|
157
|
+
|
|
158
|
+
if (!fs.existsSync(this.sourceDir)) {
|
|
159
|
+
throw new Error(`Source directory not found: ${this.sourceDir}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const languages = fs.readdirSync(this.sourceDir)
|
|
163
|
+
.filter(item => {
|
|
164
|
+
const itemPath = path.join(this.sourceDir, item);
|
|
165
|
+
return fs.statSync(itemPath).isDirectory() && item !== this.config.sourceLanguage;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
SecurityUtils.logSecurityEvent('languages_found', 'info', `Found ${languages.length} languages`);
|
|
169
|
+
return languages;
|
|
170
|
+
} catch (error) {
|
|
171
|
+
SecurityUtils.logSecurityEvent('languages_scan_error', 'error', `Language scanning error: ${error.message}`);
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Get all JSON files from a language directory
|
|
177
|
+
getLanguageFiles(language) {
|
|
178
|
+
try {
|
|
179
|
+
const sanitizedLanguage = SecurityUtils.sanitizeInput(language);
|
|
180
|
+
const languageDir = path.join(this.sourceDir, sanitizedLanguage);
|
|
181
|
+
|
|
182
|
+
if (!fs.existsSync(languageDir)) {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const files = fs.readdirSync(languageDir)
|
|
187
|
+
.filter(file => {
|
|
188
|
+
return file.endsWith('.json') &&
|
|
189
|
+
!this.config.excludeFiles.includes(file);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
SecurityUtils.logSecurityEvent('files_scan', 'info', `Found ${files.length} files in ${sanitizedLanguage}`);
|
|
193
|
+
return files;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
SecurityUtils.logSecurityEvent('files_scan_error', 'error', `File scanning error: ${error.message}`);
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Get all keys recursively from an object
|
|
201
|
+
getAllKeys(obj, prefix = '') {
|
|
202
|
+
const keys = new Set();
|
|
203
|
+
|
|
204
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
205
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
206
|
+
keys.add(fullKey);
|
|
207
|
+
|
|
208
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
209
|
+
const nestedKeys = this.getAllKeys(value, fullKey);
|
|
210
|
+
nestedKeys.forEach(k => keys.add(k));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return keys;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Get value by key path
|
|
218
|
+
getValueByPath(obj, keyPath) {
|
|
219
|
+
const keys = keyPath.split('.');
|
|
220
|
+
let current = obj;
|
|
221
|
+
|
|
222
|
+
for (const key of keys) {
|
|
223
|
+
if (current && typeof current === 'object' && key in current) {
|
|
224
|
+
current = current[key];
|
|
225
|
+
} else {
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return current;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Validate JSON file syntax
|
|
234
|
+
async validateJsonSyntax(filePath) {
|
|
235
|
+
try {
|
|
236
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
237
|
+
const parsed = SecurityUtils.safeParseJSON(content);
|
|
238
|
+
|
|
239
|
+
SecurityUtils.logSecurityEvent('json_validated', 'info', `JSON syntax validated: ${filePath}`);
|
|
240
|
+
return { valid: true, data: parsed };
|
|
241
|
+
} catch (error) {
|
|
242
|
+
SecurityUtils.logSecurityEvent('json_validation_error', 'error', `JSON validation error: ${error.message}`);
|
|
243
|
+
return {
|
|
244
|
+
valid: false,
|
|
245
|
+
error: error.message,
|
|
246
|
+
line: error.message.match(/line (\d+)/)?.[1] || 'unknown'
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Validate structural consistency
|
|
252
|
+
validateStructure(sourceObj, targetObj, language, fileName) {
|
|
253
|
+
const sourceKeys = this.getAllKeys(sourceObj);
|
|
254
|
+
const targetKeys = this.getAllKeys(targetObj);
|
|
255
|
+
|
|
256
|
+
const missingKeys = [...sourceKeys].filter(key => !targetKeys.has(key));
|
|
257
|
+
const extraKeys = [...targetKeys].filter(key => !sourceKeys.has(key));
|
|
258
|
+
|
|
259
|
+
// Report missing keys as errors
|
|
260
|
+
missingKeys.forEach(key => {
|
|
261
|
+
this.addError(
|
|
262
|
+
`Missing key in ${language}/${fileName}`,
|
|
263
|
+
{ key, language, fileName }
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Report extra keys as warnings
|
|
268
|
+
extraKeys.forEach(key => {
|
|
269
|
+
this.addWarning(
|
|
270
|
+
`Extra key in ${language}/${fileName}`,
|
|
271
|
+
{ key, language, fileName }
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
isConsistent: missingKeys.length === 0 && extraKeys.length === 0,
|
|
277
|
+
missingKeys,
|
|
278
|
+
extraKeys,
|
|
279
|
+
sourceKeyCount: sourceKeys.size,
|
|
280
|
+
targetKeyCount: targetKeys.size
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Validate translation completeness
|
|
285
|
+
validateTranslations(obj, language, fileName, prefix = '') {
|
|
286
|
+
let totalKeys = 0;
|
|
287
|
+
let translatedKeys = 0;
|
|
288
|
+
let issues = [];
|
|
289
|
+
|
|
290
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
291
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
292
|
+
|
|
293
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
294
|
+
const nested = this.validateTranslations(value, language, fileName, fullKey);
|
|
295
|
+
totalKeys += nested.totalKeys;
|
|
296
|
+
translatedKeys += nested.translatedKeys;
|
|
297
|
+
issues.push(...nested.issues);
|
|
298
|
+
} else if (typeof value === 'string') {
|
|
299
|
+
totalKeys++;
|
|
300
|
+
|
|
301
|
+
if (value === this.config.notTranslatedMarker) {
|
|
302
|
+
issues.push({
|
|
303
|
+
type: 'not_translated',
|
|
304
|
+
key: fullKey,
|
|
305
|
+
value,
|
|
306
|
+
language,
|
|
307
|
+
fileName
|
|
308
|
+
});
|
|
309
|
+
} else if (value === '') {
|
|
310
|
+
issues.push({
|
|
311
|
+
type: 'empty_value',
|
|
312
|
+
key: fullKey,
|
|
313
|
+
value,
|
|
314
|
+
language,
|
|
315
|
+
fileName
|
|
316
|
+
});
|
|
317
|
+
} else if (value.includes(this.config.notTranslatedMarker)) {
|
|
318
|
+
issues.push({
|
|
319
|
+
type: 'partial_translation',
|
|
320
|
+
key: fullKey,
|
|
321
|
+
value,
|
|
322
|
+
language,
|
|
323
|
+
fileName
|
|
324
|
+
});
|
|
325
|
+
} else {
|
|
326
|
+
translatedKeys++;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return { totalKeys, translatedKeys, issues };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Validate a single language
|
|
335
|
+
async validateLanguage(language) {
|
|
336
|
+
try {
|
|
337
|
+
SecurityUtils.logSecurityEvent('language_validation', 'info', `Validating language: ${language}`);
|
|
338
|
+
|
|
339
|
+
const sanitizedLanguage = SecurityUtils.sanitizeInput(language);
|
|
340
|
+
const languageDir = path.join(this.sourceDir, sanitizedLanguage);
|
|
341
|
+
const sourceFiles = this.getLanguageFiles(this.config.sourceLanguage);
|
|
342
|
+
const targetFiles = this.getLanguageFiles(sanitizedLanguage);
|
|
343
|
+
|
|
344
|
+
const validation = {
|
|
345
|
+
language: sanitizedLanguage,
|
|
346
|
+
files: {},
|
|
347
|
+
summary: {
|
|
348
|
+
totalFiles: sourceFiles.length,
|
|
349
|
+
validFiles: 0,
|
|
350
|
+
totalKeys: 0,
|
|
351
|
+
translatedKeys: 0,
|
|
352
|
+
missingFiles: [],
|
|
353
|
+
syntaxErrors: [],
|
|
354
|
+
structuralIssues: [],
|
|
355
|
+
translationIssues: []
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// Check for missing language directory
|
|
360
|
+
if (!fs.existsSync(languageDir)) {
|
|
361
|
+
this.addError(
|
|
362
|
+
`Language directory missing: ${sanitizedLanguage}`,
|
|
363
|
+
{ language: sanitizedLanguage, expectedPath: languageDir }
|
|
364
|
+
);
|
|
365
|
+
return validation;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Validate each file
|
|
369
|
+
for (const fileName of sourceFiles) {
|
|
370
|
+
const sourceFilePath = path.join(this.sourceLanguageDir, fileName);
|
|
371
|
+
const targetFilePath = path.join(languageDir, fileName);
|
|
372
|
+
|
|
373
|
+
// Check if source file exists
|
|
374
|
+
if (!fs.existsSync(sourceFilePath)) {
|
|
375
|
+
this.addWarning(
|
|
376
|
+
`Source file missing: ${this.config.sourceLanguage}/${fileName}`,
|
|
377
|
+
{ fileName, language: this.config.sourceLanguage }
|
|
378
|
+
);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Check if target file exists
|
|
383
|
+
if (!fs.existsSync(targetFilePath)) {
|
|
384
|
+
this.addError(
|
|
385
|
+
`Translation file missing: ${language}/${fileName}`,
|
|
386
|
+
{ fileName, language, expectedPath: targetFilePath }
|
|
387
|
+
);
|
|
388
|
+
validation.summary.missingFiles.push(fileName);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Validate JSON syntax for both files
|
|
393
|
+
const sourceValidation = await this.validateJsonSyntax(sourceFilePath);
|
|
394
|
+
const targetValidation = await this.validateJsonSyntax(targetFilePath);
|
|
395
|
+
|
|
396
|
+
if (!sourceValidation.valid) {
|
|
397
|
+
this.addError(
|
|
398
|
+
`Invalid JSON syntax in source file: ${this.config.sourceLanguage}/${fileName}`,
|
|
399
|
+
{ fileName, language: this.config.sourceLanguage, error: sourceValidation.error }
|
|
400
|
+
);
|
|
401
|
+
validation.summary.syntaxErrors.push({ fileName, type: 'source', error: sourceValidation.error });
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (!targetValidation.valid) {
|
|
406
|
+
this.addError(
|
|
407
|
+
`Invalid JSON syntax in target file: ${sanitizedLanguage}/${fileName}`,
|
|
408
|
+
{ fileName, language: sanitizedLanguage, error: targetValidation.error }
|
|
409
|
+
);
|
|
410
|
+
validation.summary.syntaxErrors.push({ fileName, type: 'target', error: targetValidation.error });
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Use parsed data from validation
|
|
415
|
+
const sourceContent = sourceValidation.data;
|
|
416
|
+
const targetContent = targetValidation.data;
|
|
417
|
+
|
|
418
|
+
// Validate structure
|
|
419
|
+
const structural = this.validateStructure(sourceContent, targetContent, language, fileName);
|
|
420
|
+
|
|
421
|
+
// Validate translations
|
|
422
|
+
const translations = this.validateTranslations(targetContent, language, fileName);
|
|
423
|
+
|
|
424
|
+
// Store file validation results
|
|
425
|
+
validation.files[fileName] = {
|
|
426
|
+
status: 'validated',
|
|
427
|
+
structural,
|
|
428
|
+
translations,
|
|
429
|
+
sourceFilePath,
|
|
430
|
+
targetFilePath
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// Update summary
|
|
434
|
+
validation.summary.validFiles++;
|
|
435
|
+
validation.summary.totalKeys += translations.totalKeys;
|
|
436
|
+
validation.summary.translatedKeys += translations.translatedKeys;
|
|
437
|
+
|
|
438
|
+
if (!structural.isConsistent) {
|
|
439
|
+
validation.summary.structuralIssues.push({
|
|
440
|
+
fileName,
|
|
441
|
+
missingKeys: structural.missingKeys.length,
|
|
442
|
+
extraKeys: structural.extraKeys.length
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
validation.summary.translationIssues.push(...translations.issues);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Calculate completion percentage
|
|
450
|
+
validation.summary.percentage = validation.summary.totalKeys > 0
|
|
451
|
+
? Math.round((validation.summary.translatedKeys / validation.summary.totalKeys) * 100)
|
|
452
|
+
: 0;
|
|
453
|
+
|
|
454
|
+
return validation;
|
|
455
|
+
} catch (error) {
|
|
456
|
+
SecurityUtils.logSecurityEvent('language_validation_error', 'error', {
|
|
457
|
+
language: language,
|
|
458
|
+
error: error.message,
|
|
459
|
+
timestamp: new Date().toISOString()
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
this.addError(
|
|
463
|
+
`Language validation failed for ${language}: ${error.message}`,
|
|
464
|
+
{ language, error: error.message }
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
return {
|
|
468
|
+
language: language,
|
|
469
|
+
files: {},
|
|
470
|
+
summary: {
|
|
471
|
+
totalFiles: 0,
|
|
472
|
+
validFiles: 0,
|
|
473
|
+
totalKeys: 0,
|
|
474
|
+
translatedKeys: 0,
|
|
475
|
+
missingFiles: [],
|
|
476
|
+
syntaxErrors: [],
|
|
477
|
+
structuralIssues: [],
|
|
478
|
+
translationIssues: [],
|
|
479
|
+
percentage: 0
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Check for unused translation keys (basic implementation)
|
|
486
|
+
checkUnusedKeys(language) {
|
|
487
|
+
// Note: For comprehensive unused key detection, use the dedicated
|
|
488
|
+
// usage analysis script: i18ntk-usage.js
|
|
489
|
+
const warnings = [];
|
|
490
|
+
|
|
491
|
+
// This method provides basic validation only
|
|
492
|
+
// For detailed usage analysis, run: node i18ntk-usage.js
|
|
493
|
+
|
|
494
|
+
return warnings;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Show help message
|
|
498
|
+
showHelp() {
|
|
499
|
+
console.log(this.t('validateTranslations.help_message'));
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Main validation process
|
|
503
|
+
async validate() {
|
|
504
|
+
try {
|
|
505
|
+
console.log(this.t('validateTranslations.title'));
|
|
506
|
+
console.log(this.t("validateTranslations.message"));
|
|
507
|
+
|
|
508
|
+
// Delete old validation report if it exists
|
|
509
|
+
const reportPath = path.join(process.cwd(), 'validation-report.txt');
|
|
510
|
+
SecurityUtils.validatePath(reportPath);
|
|
511
|
+
|
|
512
|
+
if (fs.existsSync(reportPath)) {
|
|
513
|
+
fs.unlinkSync(reportPath);
|
|
514
|
+
console.log(this.t('validateTranslations.deletedOldReport'));
|
|
515
|
+
|
|
516
|
+
SecurityUtils.logSecurityEvent('file_deleted', 'info', {
|
|
517
|
+
path: reportPath,
|
|
518
|
+
timestamp: new Date().toISOString()
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Parse command line arguments
|
|
523
|
+
const args = this.parseArgs();
|
|
524
|
+
|
|
525
|
+
// Handle UI language change
|
|
526
|
+
if (args.uiLanguage) {
|
|
527
|
+
loadTranslations(args.uiLanguage);
|
|
528
|
+
this.t = t;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (args.sourceDir) {
|
|
532
|
+
this.config.sourceDir = args.sourceDir;
|
|
533
|
+
this.sourceDir = path.resolve(this.config.sourceDir);
|
|
534
|
+
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
535
|
+
}
|
|
536
|
+
if (args.strictMode) {
|
|
537
|
+
this.config.strictMode = true;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
console.log(this.t('validateTranslations.sourceDirectory', { dir: this.sourceDir }));
|
|
541
|
+
console.log(this.t("validateTranslations.source_language_thisconfigsour", { sourceLanguage: this.config.sourceLanguage }));
|
|
542
|
+
console.log(this.t('validateTranslations.strictMode', { mode: this.config.strictMode ? 'ON' : 'OFF' }));
|
|
543
|
+
|
|
544
|
+
// Validate source language directory exists
|
|
545
|
+
SecurityUtils.validatePath(this.sourceLanguageDir);
|
|
546
|
+
|
|
547
|
+
if (!fs.existsSync(this.sourceLanguageDir)) {
|
|
548
|
+
this.addError(
|
|
549
|
+
`Source language directory not found: ${this.sourceLanguageDir}`,
|
|
550
|
+
{ sourceLanguage: this.config.sourceLanguage }
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
SecurityUtils.logSecurityEvent('validation_error', 'error', {
|
|
554
|
+
error: 'Source language directory not found',
|
|
555
|
+
path: this.sourceLanguageDir,
|
|
556
|
+
timestamp: new Date().toISOString()
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
throw new Error('Source language directory not found');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Get available languages
|
|
563
|
+
const availableLanguages = this.getAvailableLanguages();
|
|
564
|
+
|
|
565
|
+
if (availableLanguages.length === 0) {
|
|
566
|
+
console.log(this.t('validateTranslations.noTargetLanguages'));
|
|
567
|
+
return { success: true, message: 'No languages to validate' };
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Filter languages if specified
|
|
571
|
+
const targetLanguages = args.language
|
|
572
|
+
? [args.language].filter(lang => availableLanguages.includes(lang))
|
|
573
|
+
: availableLanguages;
|
|
574
|
+
|
|
575
|
+
if (targetLanguages.length === 0) {
|
|
576
|
+
this.addError(
|
|
577
|
+
`Specified language '${args.language}' not found`,
|
|
578
|
+
{ requestedLanguage: args.language, availableLanguages }
|
|
579
|
+
);
|
|
580
|
+
throw new Error('Specified language not found');
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
console.log(this.t('validateTranslations.validatingLanguages', { languages: targetLanguages.join(', ') }));
|
|
584
|
+
|
|
585
|
+
const results = {};
|
|
586
|
+
|
|
587
|
+
// Validate each language
|
|
588
|
+
for (const language of targetLanguages) {
|
|
589
|
+
console.log(this.t('validateTranslations.validatingLanguage', { language: language }));
|
|
590
|
+
|
|
591
|
+
const validation = await this.validateLanguage(language);
|
|
592
|
+
results[language] = validation;
|
|
593
|
+
|
|
594
|
+
// Display summary
|
|
595
|
+
const { summary } = validation;
|
|
596
|
+
console.log(this.t('validateTranslations.filesCount', { filesCount: summary.validFiles }));
|
|
597
|
+
console.log(this.t('validateTranslations.keysCount', { keysCount: summary.totalKeys }));
|
|
598
|
+
console.log(this.t('validateTranslations.missingFilesCount', { missingFilesCount: summary.missingFiles.length }));
|
|
599
|
+
console.log(this.t('validateTranslations.syntaxErrorsCount', { syntaxErrorsCount: summary.syntaxErrors.length }));
|
|
600
|
+
console.log(this.t('validateTranslations.translationPercentage', { translationPercentage: summary.percentage, translated: summary.translatedKeys, total: summary.totalKeys }));
|
|
601
|
+
console.log('');
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Overall summary
|
|
605
|
+
const hasErrors = this.errors.length > 0;
|
|
606
|
+
const hasWarnings = this.warnings.length > 0;
|
|
607
|
+
|
|
608
|
+
console.log(this.t("validateTranslations.n_validation_summary"));
|
|
609
|
+
console.log(this.t("validateTranslations.total_errors", { totalErrors: this.errors.length }));
|
|
610
|
+
console.log(this.t("validateTranslations.total_warnings", { totalWarnings: this.warnings.length }));
|
|
611
|
+
|
|
612
|
+
// Show errors
|
|
613
|
+
if (hasErrors) {
|
|
614
|
+
console.log(this.t("validateTranslations.n_errors"));
|
|
615
|
+
this.errors.forEach((error, index) => {
|
|
616
|
+
console.log(`${index + 1}. ${error.message}`);
|
|
617
|
+
if (error.details && Object.keys(error.details).length > 0) {
|
|
618
|
+
console.log(` Details: ${JSON.stringify(error.details, null, 2)}`);
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
console.log('');
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Show warnings
|
|
625
|
+
if (hasWarnings) {
|
|
626
|
+
console.log(this.t("validateTranslations.n_warnings"));
|
|
627
|
+
this.warnings.forEach((warning, index) => {
|
|
628
|
+
console.log(`${index + 1}. ${warning.message}`);
|
|
629
|
+
if (warning.details && Object.keys(warning.details).length > 0) {
|
|
630
|
+
console.log(` Details: ${JSON.stringify(warning.details, null, 2)}`);
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
console.log('');
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Recommendations
|
|
637
|
+
console.log(this.t("validateTranslations.n_recommendations"));
|
|
638
|
+
console.log(this.t("validateTranslations.message"));
|
|
639
|
+
|
|
640
|
+
if (hasErrors) {
|
|
641
|
+
console.log(this.t("validateTranslations.fix_errors_first"));
|
|
642
|
+
console.log(this.t("validateTranslations.1_resolve_missing_files_and_sy"));
|
|
643
|
+
console.log(this.t("validateTranslations.2_fix_structural_inconsistenci"));
|
|
644
|
+
console.log(this.t("validateTranslations.3_complete_missing_translation"));
|
|
645
|
+
console.log(this.t("validateTranslations.4_rerun_validation"));
|
|
646
|
+
} else if (hasWarnings) {
|
|
647
|
+
console.log(this.t("validateTranslations.address_warnings"));
|
|
648
|
+
console.log(this.t("validateTranslations.review_warnings"));
|
|
649
|
+
console.log(this.t("validateTranslations.2_consider_running_with_strict"));
|
|
650
|
+
} else {
|
|
651
|
+
console.log(this.t("validateTranslations.all_validations_passed"));
|
|
652
|
+
console.log(this.t("validateTranslations.consider_running_usage_analysi"));
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Exit with appropriate code
|
|
656
|
+
const success = !hasErrors && (!hasWarnings || !this.config.strictMode);
|
|
657
|
+
|
|
658
|
+
return {
|
|
659
|
+
success,
|
|
660
|
+
errors: this.errors.length,
|
|
661
|
+
warnings: this.warnings.length,
|
|
662
|
+
results
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
} catch (error) {
|
|
666
|
+
console.error(this.t("validateTranslations.validation_failed", { error: error.message }));
|
|
667
|
+
return {
|
|
668
|
+
success: false,
|
|
669
|
+
error: error.message
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Run method for compatibility with manager
|
|
676
|
+
*/
|
|
677
|
+
async run() {
|
|
678
|
+
try {
|
|
679
|
+
console.log('🔍 Starting validation process...');
|
|
680
|
+
SecurityUtils.logSecurityEvent('run_started', 'info', 'Starting validation run');
|
|
681
|
+
|
|
682
|
+
await this.initialize();
|
|
683
|
+
const result = await this.validate();
|
|
684
|
+
|
|
685
|
+
console.log('✅ Validation process completed successfully');
|
|
686
|
+
SecurityUtils.logSecurityEvent('run_completed', 'info', 'Validation run completed successfully');
|
|
687
|
+
return result;
|
|
688
|
+
} catch (error) {
|
|
689
|
+
console.error('❌ Validation Error:', error.message);
|
|
690
|
+
console.error('Stack trace:', error.stack);
|
|
691
|
+
SecurityUtils.logSecurityEvent('run_error', 'error', `Validation run failed: ${error.message}`);
|
|
692
|
+
throw error;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
module.exports = I18nValidator;
|
|
698
|
+
|
|
699
|
+
if (require.main === module) {
|
|
700
|
+
(async () => {
|
|
701
|
+
try {
|
|
702
|
+
SecurityUtils.logSecurityEvent('script_execution', 'info', {
|
|
703
|
+
script: 'i18ntk-validate.js',
|
|
704
|
+
timestamp: new Date().toISOString()
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
const validator = new I18nValidator();
|
|
708
|
+
await validator.initialize();
|
|
709
|
+
|
|
710
|
+
const args = validator.parseArgs();
|
|
711
|
+
|
|
712
|
+
if (args.help) {
|
|
713
|
+
validator.showHelp();
|
|
714
|
+
process.exit(0);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (args.setupAdmin) {
|
|
718
|
+
await AdminCLI.setupAdmin();
|
|
719
|
+
process.exit(0);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (args.disableAdmin) {
|
|
723
|
+
await AdminCLI.disableAdmin();
|
|
724
|
+
process.exit(0);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (args.adminStatus) {
|
|
728
|
+
await AdminCLI.showStatus();
|
|
729
|
+
process.exit(0);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (AdminCLI.requiresAuth('validate')) {
|
|
733
|
+
const authenticated = await AdminCLI.authenticate();
|
|
734
|
+
if (!authenticated) {
|
|
735
|
+
console.log('❌ Authentication failed. Access denied.');
|
|
736
|
+
process.exit(1);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const result = await validator.validate();
|
|
741
|
+
|
|
742
|
+
SecurityUtils.logSecurityEvent('validation_completed', 'info', {
|
|
743
|
+
success: result.success,
|
|
744
|
+
errors: result.errors || 0,
|
|
745
|
+
warnings: result.warnings || 0,
|
|
746
|
+
timestamp: new Date().toISOString()
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
process.exit(result.success ? 0 : 1);
|
|
750
|
+
} catch (error) {
|
|
751
|
+
console.error('❌ Fatal validation error:', error.message);
|
|
752
|
+
console.error('Stack trace:', error.stack);
|
|
753
|
+
SecurityUtils.logSecurityEvent('validation_error', 'error', {
|
|
754
|
+
error: error.message,
|
|
755
|
+
stack: error.stack,
|
|
756
|
+
timestamp: new Date().toISOString()
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
process.exit(1);
|
|
760
|
+
}
|
|
761
|
+
})();
|
|
762
|
+
}
|