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,671 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const settingsManager = require('../settings/settings-manager');
|
|
6
|
+
const SecurityUtils = require('../utils/security');
|
|
7
|
+
const UIi18n = require('./ui-i18n');
|
|
8
|
+
|
|
9
|
+
// Get configuration from settings manager
|
|
10
|
+
function getConfig() {
|
|
11
|
+
const settings = settingsManager.getSettings();
|
|
12
|
+
return {
|
|
13
|
+
sourceLanguage: settings.directories?.sourceLanguage || 'en',
|
|
14
|
+
excludeFiles: settings.processing?.excludeFiles || ['index.js', 'index.ts', '.DS_Store'],
|
|
15
|
+
supportedExtensions: settings.processing?.supportedExtensions || ['.json', '.js', '.ts'],
|
|
16
|
+
sourceDir: settings.directories?.sourceDir || null,
|
|
17
|
+
uiLanguage: settings.language || 'en'
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* I18N SUMMARY REPORT GENERATOR
|
|
23
|
+
*
|
|
24
|
+
* Analyzes the i18n folder structure and generates a comprehensive summary report
|
|
25
|
+
* including key statistics, file structure analysis, and validation checks.
|
|
26
|
+
*
|
|
27
|
+
* This script is designed to be generic and work with any i18n project structure.
|
|
28
|
+
*/
|
|
29
|
+
class I18nSummaryReporter {
|
|
30
|
+
constructor() {
|
|
31
|
+
this.config = getConfig();
|
|
32
|
+
this.ui = new UIi18n();
|
|
33
|
+
this.t = this.ui.t.bind(this.ui);
|
|
34
|
+
this.stats = {
|
|
35
|
+
languages: [],
|
|
36
|
+
totalFiles: 0,
|
|
37
|
+
totalKeys: 0,
|
|
38
|
+
keysByLanguage: {},
|
|
39
|
+
filesByLanguage: {},
|
|
40
|
+
missingFiles: [],
|
|
41
|
+
inconsistentKeys: [],
|
|
42
|
+
emptyFiles: [],
|
|
43
|
+
malformedFiles: [],
|
|
44
|
+
duplicateKeys: []
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Parse command line arguments
|
|
49
|
+
parseArgs() {
|
|
50
|
+
const args = process.argv.slice(2);
|
|
51
|
+
const parsed = {
|
|
52
|
+
sourceDir: null,
|
|
53
|
+
outputFile: null,
|
|
54
|
+
verbose: false,
|
|
55
|
+
help: false,
|
|
56
|
+
keepReports: false,
|
|
57
|
+
deleteReports: false
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < args.length; i++) {
|
|
61
|
+
const arg = args[i];
|
|
62
|
+
|
|
63
|
+
if (arg === '--help' || arg === '-h') {
|
|
64
|
+
parsed.help = true;
|
|
65
|
+
} else if (arg === '--verbose' || arg === '-v') {
|
|
66
|
+
parsed.verbose = true;
|
|
67
|
+
} else if (arg === '--source-dir' || arg === '-s') {
|
|
68
|
+
parsed.sourceDir = args[++i];
|
|
69
|
+
} else if (arg === '--output' || arg === '-o') {
|
|
70
|
+
parsed.outputFile = args[++i];
|
|
71
|
+
} else if (arg === '--keep-reports') {
|
|
72
|
+
parsed.keepReports = true;
|
|
73
|
+
} else if (arg === '--delete-reports') {
|
|
74
|
+
parsed.deleteReports = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return parsed;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Show help information
|
|
82
|
+
showHelp() {
|
|
83
|
+
console.log(`\n${this.t('summary.helpTitle')}\n`);
|
|
84
|
+
console.log(this.t('summary.helpDescription') + '\n');
|
|
85
|
+
console.log(this.t('summary.helpUsage') + '\n');
|
|
86
|
+
console.log(this.t('summary.helpOptions'));
|
|
87
|
+
console.log(this.t('summary.helpSourceDir'));
|
|
88
|
+
console.log(this.t('summary.helpOutput'));
|
|
89
|
+
console.log(this.t('summary.helpVerbose'));
|
|
90
|
+
console.log(this.t('summary.helpKeepReports'));
|
|
91
|
+
console.log(this.t('summary.helpDeleteReports'));
|
|
92
|
+
console.log(this.t('summary.helpHelp') + '\n');
|
|
93
|
+
console.log(this.t('summary.helpExamples'));
|
|
94
|
+
console.log(this.t('summary.helpExample1'));
|
|
95
|
+
console.log(this.t('summary.helpExample2'));
|
|
96
|
+
console.log(this.t('summary.helpExample3'));
|
|
97
|
+
console.log(this.t('summary.helpExample4'));
|
|
98
|
+
console.log(this.t('summary.helpExample5') + '\n');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Auto-detect i18n directory
|
|
102
|
+
detectI18nDirectory() {
|
|
103
|
+
const possiblePaths = [
|
|
104
|
+
'locales',
|
|
105
|
+
'i18ntk-reports',
|
|
106
|
+
'src/i18n/locales',
|
|
107
|
+
'src/locales',
|
|
108
|
+
'src/i18n',
|
|
109
|
+
'i18n/locales',
|
|
110
|
+
'i18n',
|
|
111
|
+
'public/locales',
|
|
112
|
+
'assets/i18n',
|
|
113
|
+
'translations'
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
for (const possiblePath of possiblePaths) {
|
|
117
|
+
const fullPath = path.resolve(possiblePath);
|
|
118
|
+
if (fs.existsSync(fullPath)) {
|
|
119
|
+
const contents = fs.readdirSync(fullPath);
|
|
120
|
+
// Check if it contains language directories
|
|
121
|
+
const hasLanguageDirs = contents.some(item => {
|
|
122
|
+
const itemPath = path.join(fullPath, item);
|
|
123
|
+
return fs.statSync(itemPath).isDirectory() &&
|
|
124
|
+
item.length === 2 && // Language codes are typically 2 characters
|
|
125
|
+
/^[a-z]{2}$/.test(item);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (hasLanguageDirs) {
|
|
129
|
+
return fullPath;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Get all available languages
|
|
138
|
+
getAvailableLanguages() {
|
|
139
|
+
if (!fs.existsSync(this.config.sourceDir)) {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return fs.readdirSync(this.config.sourceDir)
|
|
144
|
+
.filter(item => {
|
|
145
|
+
const itemPath = path.join(this.config.sourceDir, item);
|
|
146
|
+
return fs.statSync(itemPath).isDirectory() &&
|
|
147
|
+
!item.startsWith('.') &&
|
|
148
|
+
item !== 'node_modules';
|
|
149
|
+
})
|
|
150
|
+
.sort();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Get all translation files for a language
|
|
154
|
+
getLanguageFiles(language) {
|
|
155
|
+
const languageDir = path.join(this.config.sourceDir, language);
|
|
156
|
+
|
|
157
|
+
if (!fs.existsSync(languageDir)) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return fs.readdirSync(languageDir)
|
|
162
|
+
.filter(file => {
|
|
163
|
+
return this.config.supportedExtensions.some(ext => file.endsWith(ext)) &&
|
|
164
|
+
!this.config.excludeFiles.includes(file);
|
|
165
|
+
})
|
|
166
|
+
.sort();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Extract keys from a translation file
|
|
170
|
+
async extractKeysFromFile(filePath) {
|
|
171
|
+
try {
|
|
172
|
+
const content = await SecurityUtils.safeReadFile(filePath, this.config.sourceDir);
|
|
173
|
+
if (!content) {
|
|
174
|
+
console.warn(this.t('summary.couldNotReadFile', { filePath }));
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (filePath.endsWith('.json')) {
|
|
179
|
+
const data = JSON.parse(content);
|
|
180
|
+
return this.extractKeysFromObject(data);
|
|
181
|
+
} else if (filePath.endsWith('.js') || filePath.endsWith('.ts')) {
|
|
182
|
+
// Basic extraction for JS/TS files (assumes export default or module.exports)
|
|
183
|
+
const match = content.match(/(?:export\s+default|module\.exports\s*=)\s*({[\s\S]*})/);;
|
|
184
|
+
if (match) {
|
|
185
|
+
const objStr = match[1];
|
|
186
|
+
// This is a simplified approach - in production, you might want to use a proper JS parser
|
|
187
|
+
try {
|
|
188
|
+
const data = eval(`(${objStr})`);
|
|
189
|
+
return this.extractKeysFromObject(data);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
console.warn(this.t('summary.couldNotParseJSFile', { filePath }));
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return [];
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.warn(this.t('summary.errorReadingFile', { filePath, error: error.message }));
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Extract keys recursively from an object
|
|
205
|
+
extractKeysFromObject(obj, prefix = '') {
|
|
206
|
+
const keys = [];
|
|
207
|
+
|
|
208
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
209
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
210
|
+
|
|
211
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
212
|
+
keys.push(...this.extractKeysFromObject(value, fullKey));
|
|
213
|
+
} else {
|
|
214
|
+
keys.push(fullKey);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return keys;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Check if file is empty or has no meaningful content
|
|
222
|
+
async isFileEmpty(filePath) {
|
|
223
|
+
try {
|
|
224
|
+
const content = await SecurityUtils.safeReadFile(filePath, this.config.sourceDir);
|
|
225
|
+
if (!content) return true;
|
|
226
|
+
const trimmedContent = content.trim();
|
|
227
|
+
if (!trimmedContent) return true;
|
|
228
|
+
|
|
229
|
+
if (filePath.endsWith('.json')) {
|
|
230
|
+
const data = SecurityUtils.safeParseJSON(trimmedContent);
|
|
231
|
+
if (!data) return true;
|
|
232
|
+
return Object.keys(data).length === 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return false;
|
|
236
|
+
} catch (error) {
|
|
237
|
+
return false; // If we can't parse it, it's not empty, just malformed
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Check if file is malformed
|
|
242
|
+
async isFileMalformed(filePath) {
|
|
243
|
+
try {
|
|
244
|
+
const content = await SecurityUtils.safeReadFile(filePath, this.config.sourceDir);
|
|
245
|
+
if (!content) return true;
|
|
246
|
+
|
|
247
|
+
if (filePath.endsWith('.json')) {
|
|
248
|
+
const parsed = SecurityUtils.safeParseJSON(content);
|
|
249
|
+
if (!parsed) return true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return false;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Find duplicate keys within a file
|
|
259
|
+
async findDuplicateKeys(filePath) {
|
|
260
|
+
try {
|
|
261
|
+
const content = await SecurityUtils.safeReadFile(filePath, this.config.sourceDir);
|
|
262
|
+
if (!content) return [];
|
|
263
|
+
|
|
264
|
+
if (filePath.endsWith('.json')) {
|
|
265
|
+
// For JSON files, check for duplicate keys in the raw content
|
|
266
|
+
const keys = [];
|
|
267
|
+
const keyRegex = /"([^"]+)"\s*:/g;
|
|
268
|
+
let match;
|
|
269
|
+
|
|
270
|
+
while ((match = keyRegex.exec(content)) !== null) {
|
|
271
|
+
keys.push(match[1]);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const duplicates = keys.filter((key, index) => keys.indexOf(key) !== index);
|
|
275
|
+
return [...new Set(duplicates)];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return [];
|
|
279
|
+
} catch (error) {
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Analyze folder structure and collect statistics
|
|
285
|
+
async analyzeStructure() {
|
|
286
|
+
console.log(this.t('summary.analyzingFolder'));
|
|
287
|
+
|
|
288
|
+
this.stats.languages = this.getAvailableLanguages();
|
|
289
|
+
|
|
290
|
+
if (this.stats.languages.length === 0) {
|
|
291
|
+
console.log(this.t('summary.noLanguageDirectoriesFound'));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
console.log(this.t('summary.foundLanguages', {count: this.stats.languages.length, languages: this.stats.languages.join(', ')}));
|
|
296
|
+
|
|
297
|
+
// Use first language as reference for file structure
|
|
298
|
+
const referenceLanguage = this.stats.languages[0];
|
|
299
|
+
const referenceFiles = this.getLanguageFiles(referenceLanguage);
|
|
300
|
+
|
|
301
|
+
console.log(this.t('summary.referenceLanguageFiles', {language: referenceLanguage, count: referenceFiles.length}));
|
|
302
|
+
|
|
303
|
+
// Analyze each language
|
|
304
|
+
for (const language of this.stats.languages) {
|
|
305
|
+
console.log(this.t('summary.analyzingLanguage', {language}));
|
|
306
|
+
|
|
307
|
+
const files = this.getLanguageFiles(language);
|
|
308
|
+
this.stats.filesByLanguage[language] = files;
|
|
309
|
+
this.stats.keysByLanguage[language] = {};
|
|
310
|
+
|
|
311
|
+
let totalKeysForLanguage = 0;
|
|
312
|
+
|
|
313
|
+
// Check for missing files compared to reference
|
|
314
|
+
const missingFiles = referenceFiles.filter(file => !files.includes(file));
|
|
315
|
+
if (missingFiles.length > 0) {
|
|
316
|
+
this.stats.missingFiles.push({
|
|
317
|
+
language,
|
|
318
|
+
files: missingFiles
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Analyze each file
|
|
323
|
+
for (const file of files) {
|
|
324
|
+
const filePath = path.join(this.config.sourceDir, language, file);
|
|
325
|
+
|
|
326
|
+
// Check if file is empty
|
|
327
|
+
if (await this.isFileEmpty(filePath)) {
|
|
328
|
+
this.stats.emptyFiles.push({ language, file });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Check if file is malformed
|
|
332
|
+
if (await this.isFileMalformed(filePath)) {
|
|
333
|
+
this.stats.malformedFiles.push({ language, file });
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Find duplicate keys
|
|
338
|
+
const duplicates = await this.findDuplicateKeys(filePath);
|
|
339
|
+
if (duplicates.length > 0) {
|
|
340
|
+
this.stats.duplicateKeys.push({ language, file, duplicates });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Extract keys
|
|
344
|
+
const keys = await this.extractKeysFromFile(filePath);
|
|
345
|
+
this.stats.keysByLanguage[language][file] = keys;
|
|
346
|
+
totalKeysForLanguage += keys.length;
|
|
347
|
+
|
|
348
|
+
this.stats.totalFiles++;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
this.stats.totalKeys += totalKeysForLanguage;
|
|
352
|
+
console.log(this.t('summary.keysInFiles', {keys: totalKeysForLanguage, files: files.length}));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Find inconsistent keys across languages
|
|
356
|
+
this.findInconsistentKeys();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Find keys that are inconsistent across languages
|
|
360
|
+
findInconsistentKeys() {
|
|
361
|
+
console.log(this.t('summary.checkingInconsistentKeys'));
|
|
362
|
+
|
|
363
|
+
const referenceLanguage = this.stats.languages[0];
|
|
364
|
+
const referenceKeys = this.stats.keysByLanguage[referenceLanguage];
|
|
365
|
+
|
|
366
|
+
for (const file of Object.keys(referenceKeys)) {
|
|
367
|
+
const refKeys = new Set(referenceKeys[file]);
|
|
368
|
+
|
|
369
|
+
for (const language of this.stats.languages.slice(1)) {
|
|
370
|
+
const langKeys = this.stats.keysByLanguage[language][file] || [];
|
|
371
|
+
const langKeySet = new Set(langKeys);
|
|
372
|
+
|
|
373
|
+
// Find missing keys in this language
|
|
374
|
+
const missingInLang = [...refKeys].filter(key => !langKeySet.has(key));
|
|
375
|
+
// Find extra keys in this language
|
|
376
|
+
const extraInLang = [...langKeySet].filter(key => !refKeys.has(key));
|
|
377
|
+
|
|
378
|
+
if (missingInLang.length > 0 || extraInLang.length > 0) {
|
|
379
|
+
this.stats.inconsistentKeys.push({
|
|
380
|
+
file,
|
|
381
|
+
language,
|
|
382
|
+
missing: missingInLang,
|
|
383
|
+
extra: extraInLang
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Generate summary report
|
|
391
|
+
generateReport() {
|
|
392
|
+
const report = [];
|
|
393
|
+
const timestamp = new Date().toISOString();
|
|
394
|
+
|
|
395
|
+
report.push(this.t('summary.reportTitle'));
|
|
396
|
+
report.push('='.repeat(50));
|
|
397
|
+
report.push(this.t('summary.generated', {timestamp}));
|
|
398
|
+
report.push(this.t('summary.sourceDirectory', {dir: this.config.sourceDir}));
|
|
399
|
+
report.push('');
|
|
400
|
+
// Overview
|
|
401
|
+
report.push(this.t('summary.overview'));
|
|
402
|
+
report.push('='.repeat(30));
|
|
403
|
+
report.push(this.t('summary.languagesCount', {count: this.stats.languages.length}));
|
|
404
|
+
report.push(this.t('summary.totalFiles', {count: this.stats.totalFiles}));
|
|
405
|
+
report.push(this.t('summary.totalKeys', {count: this.stats.totalKeys}));
|
|
406
|
+
report.push(this.t('summary.avgKeysPerLanguage', {count: Math.round(this.stats.totalKeys / this.stats.languages.length)}));
|
|
407
|
+
report.push('');
|
|
408
|
+
// Languages breakdown
|
|
409
|
+
report.push(this.t('summary.languagesBreakdown'));
|
|
410
|
+
report.push('='.repeat(30));
|
|
411
|
+
for (const language of this.stats.languages) {
|
|
412
|
+
const files = this.stats.filesByLanguage[language];
|
|
413
|
+
const totalKeys = Object.values(this.stats.keysByLanguage[language])
|
|
414
|
+
.reduce((sum, keys) => sum + keys.length, 0);
|
|
415
|
+
report.push(this.t('summary.languageBreakdown', {language, files: files.length, keys: totalKeys}));
|
|
416
|
+
}
|
|
417
|
+
report.push('');
|
|
418
|
+
// File structure
|
|
419
|
+
report.push(this.t('summary.fileStructure'));
|
|
420
|
+
report.push('='.repeat(30));
|
|
421
|
+
const referenceLanguage = this.stats.languages[0];
|
|
422
|
+
const referenceFiles = this.stats.filesByLanguage[referenceLanguage] || [];
|
|
423
|
+
for (const file of referenceFiles) {
|
|
424
|
+
const keysInFile = this.stats.keysByLanguage[referenceLanguage][file]?.length || 0;
|
|
425
|
+
report.push(this.t('summary.fileKeys', {file, keys: keysInFile}));
|
|
426
|
+
// Show which languages have this file
|
|
427
|
+
const languagesWithFile = this.stats.languages.filter(lang =>
|
|
428
|
+
this.stats.filesByLanguage[lang].includes(file)
|
|
429
|
+
);
|
|
430
|
+
if (languagesWithFile.length < this.stats.languages.length) {
|
|
431
|
+
const missingIn = this.stats.languages.filter(lang =>
|
|
432
|
+
!languagesWithFile.includes(lang)
|
|
433
|
+
);
|
|
434
|
+
report.push(this.t('summary.missingInLanguages', {languages: missingIn.join(', ')}));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
report.push('');
|
|
438
|
+
|
|
439
|
+
// Issues
|
|
440
|
+
if (this.stats.missingFiles.length > 0 ||
|
|
441
|
+
this.stats.emptyFiles.length > 0 ||
|
|
442
|
+
this.stats.malformedFiles.length > 0 ||
|
|
443
|
+
this.stats.duplicateKeys.length > 0 ||
|
|
444
|
+
this.stats.inconsistentKeys.length > 0) {
|
|
445
|
+
|
|
446
|
+
report.push(this.t('summary.issuesFound'));
|
|
447
|
+
report.push('='.repeat(30));
|
|
448
|
+
|
|
449
|
+
if (this.stats.missingFiles.length > 0) {
|
|
450
|
+
report.push(this.t('summary.missingFiles'));
|
|
451
|
+
for (const item of this.stats.missingFiles) {
|
|
452
|
+
report.push(` ${item.language}: ${item.files.join(', ')}`);
|
|
453
|
+
}
|
|
454
|
+
report.push('');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (this.stats.emptyFiles.length > 0) {
|
|
458
|
+
report.push(this.t('summary.emptyFiles'));
|
|
459
|
+
for (const item of this.stats.emptyFiles) {
|
|
460
|
+
report.push(` ${item.language}/${item.file}`);
|
|
461
|
+
}
|
|
462
|
+
report.push('');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (this.stats.malformedFiles.length > 0) {
|
|
466
|
+
report.push(this.t('summary.malformedFiles'));
|
|
467
|
+
for (const item of this.stats.malformedFiles) {
|
|
468
|
+
report.push(` ${item.language}/${item.file}`);
|
|
469
|
+
}
|
|
470
|
+
report.push('');
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (this.stats.duplicateKeys.length > 0) {
|
|
474
|
+
report.push(this.t('summary.duplicateKeys'));
|
|
475
|
+
for (const item of this.stats.duplicateKeys) {
|
|
476
|
+
report.push(` ${item.language}/${item.file}: ${item.duplicates.join(', ')}`);
|
|
477
|
+
}
|
|
478
|
+
report.push('');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (this.stats.inconsistentKeys.length > 0) {
|
|
482
|
+
report.push(this.t('summary.inconsistentKeys'));
|
|
483
|
+
for (const item of this.stats.inconsistentKeys) {
|
|
484
|
+
report.push(` ${item.language}/${item.file}:`);
|
|
485
|
+
if (item.missing.length > 0) {
|
|
486
|
+
report.push(this.t('summary.missingKeys', {keys: item.missing.slice(0, 5).join(', '), more: item.missing.length > 5 ? '...' : ''}));
|
|
487
|
+
}
|
|
488
|
+
if (item.extra.length > 0) {
|
|
489
|
+
report.push(this.t('summary.extraKeys', {keys: item.extra.slice(0, 5).join(', '), more: item.extra.length > 5 ? '...' : ''}));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
report.push('');
|
|
493
|
+
}
|
|
494
|
+
} else {
|
|
495
|
+
report.push(this.t('summary.noIssuesFound'));
|
|
496
|
+
report.push('='.repeat(30));
|
|
497
|
+
report.push(this.t('summary.allFilesConsistent'));
|
|
498
|
+
report.push('');
|
|
499
|
+
}
|
|
500
|
+
// Recommendations
|
|
501
|
+
report.push(this.t('summary.recommendations'));
|
|
502
|
+
report.push('='.repeat(30));
|
|
503
|
+
if (this.stats.missingFiles.length > 0) {
|
|
504
|
+
report.push(this.t('summary.createMissingFiles'));
|
|
505
|
+
}
|
|
506
|
+
if (this.stats.emptyFiles.length > 0) {
|
|
507
|
+
report.push(this.t('summary.addContentToEmptyFiles'));
|
|
508
|
+
}
|
|
509
|
+
if (this.stats.malformedFiles.length > 0) {
|
|
510
|
+
report.push(this.t('summary.fixMalformedFiles'));
|
|
511
|
+
}
|
|
512
|
+
if (this.stats.duplicateKeys.length > 0) {
|
|
513
|
+
report.push(this.t('summary.removeDuplicateKeys'));
|
|
514
|
+
}
|
|
515
|
+
if (this.stats.inconsistentKeys.length > 0) {
|
|
516
|
+
report.push(this.t('summary.synchronizeKeys'));
|
|
517
|
+
}
|
|
518
|
+
if (this.stats.totalKeys > 10000) {
|
|
519
|
+
report.push(this.t('summary.splitLargeFiles'));
|
|
520
|
+
}
|
|
521
|
+
if (this.stats.languages.length === 1) {
|
|
522
|
+
report.push(this.t('summary.addMoreLanguages'));
|
|
523
|
+
}
|
|
524
|
+
report.push('');
|
|
525
|
+
report.push(this.t('summary.nextSteps'));
|
|
526
|
+
report.push(this.t('summary.nextStep1'));
|
|
527
|
+
report.push(this.t('summary.nextStep2'));
|
|
528
|
+
report.push(this.t('summary.nextStep3'));
|
|
529
|
+
report.push(this.t('summary.nextStep4'));
|
|
530
|
+
return report.join('\n');
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Run the analysis and generate report
|
|
534
|
+
async run() {
|
|
535
|
+
const args = this.parseArgs();
|
|
536
|
+
|
|
537
|
+
if (args.help) {
|
|
538
|
+
this.showHelp();
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Determine source directory
|
|
543
|
+
if (args.sourceDir) {
|
|
544
|
+
this.config.sourceDir = path.resolve(args.sourceDir);
|
|
545
|
+
} else {
|
|
546
|
+
this.config.sourceDir = this.detectI18nDirectory();
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (!this.config.sourceDir) {
|
|
550
|
+
console.error(this.t('summary.couldNotFindI18nDirectory'));
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (!fs.existsSync(this.config.sourceDir)) {
|
|
555
|
+
console.error(this.t('summary.sourceDirectoryDoesNotExist', { sourceDir: this.config.sourceDir }));
|
|
556
|
+
process.exit(1);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
console.log(this.t('summary.i18nSummaryReportGenerator'));
|
|
560
|
+
console.log('============================================================');
|
|
561
|
+
console.log(this.t('summary.sourceDirectory', {dir: this.config.sourceDir}));
|
|
562
|
+
|
|
563
|
+
if (args.verbose) {
|
|
564
|
+
console.log(`🔧 Configuration:`);
|
|
565
|
+
console.log(` Source language: ${this.config.sourceLanguage}`);
|
|
566
|
+
console.log(` Supported extensions: ${this.config.supportedExtensions.join(', ')}`);
|
|
567
|
+
console.log(` Excluded files: ${this.config.excludeFiles.join(', ')}`);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
try {
|
|
571
|
+
// Analyze structure
|
|
572
|
+
await this.analyzeStructure();
|
|
573
|
+
|
|
574
|
+
// Generate report
|
|
575
|
+
console.log(this.t('summary.generatingSummaryReport'));
|
|
576
|
+
const report = this.generateReport();
|
|
577
|
+
|
|
578
|
+
// Output report
|
|
579
|
+
if (args.outputFile) {
|
|
580
|
+
// Always save summary reports to i18ntk-reports
|
|
581
|
+
const reportsDir = path.resolve(process.cwd(), 'i18ntk-reports');
|
|
582
|
+
if (!fs.existsSync(reportsDir)) {
|
|
583
|
+
fs.mkdirSync(reportsDir, { recursive: true });
|
|
584
|
+
}
|
|
585
|
+
const outputFileName = args.outputFile ? path.basename(args.outputFile) : `summary-report-${new Date().toISOString().slice(0,10)}.txt`;
|
|
586
|
+
const outputPath = path.join(reportsDir, outputFileName);
|
|
587
|
+
const success = await SecurityUtils.safeWriteFile(outputPath, report, reportsDir);
|
|
588
|
+
if (success) {
|
|
589
|
+
console.log(this.t('summary.reportSaved', { reportPath: outputPath }));
|
|
590
|
+
} else {
|
|
591
|
+
console.log(this.t('summary.reportSaveFailed', { reportPath: outputPath }));
|
|
592
|
+
}
|
|
593
|
+
} else {
|
|
594
|
+
console.log('\n' + report);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Handle report file management
|
|
598
|
+
if (args.deleteReports && !args.keepReports) {
|
|
599
|
+
console.log(this.t('summary.cleaningUpReportFiles'));
|
|
600
|
+
try {
|
|
601
|
+
const reportsDir = path.join(this.config.sourceDir, 'scripts', 'i18n', 'reports');
|
|
602
|
+
if (fs.existsSync(reportsDir)) {
|
|
603
|
+
const files = fs.readdirSync(reportsDir);
|
|
604
|
+
const reportFiles = files.filter(file =>
|
|
605
|
+
(file.endsWith('.txt') || file.endsWith('.json') || file.endsWith('.log')) &&
|
|
606
|
+
file !== path.basename(args.outputFile || '')
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
let deletedCount = 0;
|
|
610
|
+
for (const file of reportFiles) {
|
|
611
|
+
try {
|
|
612
|
+
fs.unlinkSync(path.join(reportsDir, file));
|
|
613
|
+
deletedCount++;
|
|
614
|
+
} catch (error) {
|
|
615
|
+
console.log(this.t('summary.couldNotDelete', { file, error: error.message }));
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (deletedCount > 0) {
|
|
620
|
+
console.log(this.t('summary.deletedOldReportFiles', { count: deletedCount }));
|
|
621
|
+
} else {
|
|
622
|
+
console.log(this.t('summary.noOldReportFilesToDelete'));
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
} catch (error) {
|
|
626
|
+
console.log(this.t('summary.errorCleaningUpReports', { error: error.message }));
|
|
627
|
+
}
|
|
628
|
+
} else if (args.keepReports) {
|
|
629
|
+
console.log(this.t('summary.reportFilesPreserved'));
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Summary
|
|
633
|
+
console.log('\n============================================================');
|
|
634
|
+
console.log(this.t('summary.analysisComplete'));
|
|
635
|
+
console.log('============================================================');
|
|
636
|
+
console.log(this.t('summary.analyzedLanguages', { count: this.stats.languages.length }));
|
|
637
|
+
console.log(this.t('summary.processedFiles', { count: this.stats.totalFiles }));
|
|
638
|
+
console.log(this.t('summary.foundTranslationKeys', { count: this.stats.totalKeys }));
|
|
639
|
+
|
|
640
|
+
const totalIssues = this.stats.missingFiles.length +
|
|
641
|
+
this.stats.emptyFiles.length +
|
|
642
|
+
this.stats.malformedFiles.length +
|
|
643
|
+
this.stats.duplicateKeys.length +
|
|
644
|
+
this.stats.inconsistentKeys.length;
|
|
645
|
+
|
|
646
|
+
if (totalIssues > 0) {
|
|
647
|
+
console.log(this.t('summary.foundIssues', {count: totalIssues}));
|
|
648
|
+
} else {
|
|
649
|
+
console.log(this.t('summary.noIssuesConsole'));
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
} catch (error) {
|
|
653
|
+
console.error(this.t('summary.errorDuringAnalysis', { error: error.message }));
|
|
654
|
+
if (args.verbose) {
|
|
655
|
+
console.error(error.stack);
|
|
656
|
+
}
|
|
657
|
+
process.exit(1);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Run if called directly
|
|
663
|
+
if (require.main === module) {
|
|
664
|
+
const reporter = new I18nSummaryReporter();
|
|
665
|
+
reporter.run().catch(error => {
|
|
666
|
+
console.error('❌ Fatal error:', error.message);
|
|
667
|
+
process.exit(1);
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
module.exports = I18nSummaryReporter;
|