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,557 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* I18n Sizing Analyzer
|
|
5
|
+
*
|
|
6
|
+
* Analyzes translation file sizes, character counts, and provides sizing statistics
|
|
7
|
+
* for different languages to help with UI layout planning and optimization.
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - File size analysis for all translation files
|
|
11
|
+
* - Character count statistics per language
|
|
12
|
+
* - Key-level size comparison across languages
|
|
13
|
+
* - UI layout impact assessment
|
|
14
|
+
* - Size optimization recommendations
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* i18ntk sizing [options]
|
|
18
|
+
*
|
|
19
|
+
* Options:
|
|
20
|
+
* --source-dir <dir> Source directory containing translation files (default: ./locales)
|
|
21
|
+
* --languages <langs> Comma-separated list of languages to analyze (default: all)
|
|
22
|
+
* --output-report Generate detailed sizing report
|
|
23
|
+
* --format <format> Output format: json, csv, table (default: table)
|
|
24
|
+
* --threshold <number> Size difference threshold for warnings (default: 50%)
|
|
25
|
+
* --detailed Generate detailed report with more information
|
|
26
|
+
* --help Show this help message
|
|
27
|
+
*
|
|
28
|
+
* Examples:
|
|
29
|
+
* i18ntk sizing --output-report
|
|
30
|
+
* i18ntk sizing --languages=en,de,fr --format=json
|
|
31
|
+
* i18ntk sizing --threshold=30 --output-report
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const fs = require('fs');
|
|
35
|
+
const path = require('path');
|
|
36
|
+
const { performance } = require('perf_hooks');
|
|
37
|
+
const { loadTranslations, t } = require('../utils/i18n-helper');
|
|
38
|
+
const settingsManager = require('../settings/settings-manager');
|
|
39
|
+
const SecurityUtils = require('../utils/security');
|
|
40
|
+
|
|
41
|
+
// Get configuration from settings manager
|
|
42
|
+
function getConfig() {
|
|
43
|
+
const settings = settingsManager.getSettings();
|
|
44
|
+
return {
|
|
45
|
+
sourceDir: settings.sourceDir || './locales',
|
|
46
|
+
outputDir: settings.outputDir || './i18ntk-reports',
|
|
47
|
+
threshold: settings.processing?.sizingThreshold || 50,
|
|
48
|
+
uiLanguage: settings.language || 'en'
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class I18nSizingAnalyzer {
|
|
53
|
+
constructor(options = {}) {
|
|
54
|
+
const config = getConfig();
|
|
55
|
+
this.sourceDir = options.sourceDir || config.sourceDir;
|
|
56
|
+
this.outputDir = options.outputDir || config.outputDir;
|
|
57
|
+
this.languages = options.languages || [];
|
|
58
|
+
this.threshold = options.threshold || config.threshold; // Size difference threshold in percentage
|
|
59
|
+
this.format = options.format || 'table';
|
|
60
|
+
this.outputReport = options.outputReport || false;
|
|
61
|
+
|
|
62
|
+
// Initialize i18n with UI language from config
|
|
63
|
+
const uiLanguage = options.uiLanguage || config.uiLanguage || 'en';
|
|
64
|
+
loadTranslations(uiLanguage);
|
|
65
|
+
this.t = t;
|
|
66
|
+
|
|
67
|
+
this.stats = {
|
|
68
|
+
files: {},
|
|
69
|
+
languages: {},
|
|
70
|
+
keys: {},
|
|
71
|
+
summary: {}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Get available language files
|
|
76
|
+
getLanguageFiles() {
|
|
77
|
+
const validatedSourceDir = SecurityUtils.validatePath(this.sourceDir, process.cwd());
|
|
78
|
+
if (!validatedSourceDir) {
|
|
79
|
+
throw new Error(`Invalid source directory path: ${this.sourceDir}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!fs.existsSync(validatedSourceDir)) {
|
|
83
|
+
throw new Error(`Source directory not found: ${validatedSourceDir}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const files = [];
|
|
87
|
+
const items = fs.readdirSync(validatedSourceDir);
|
|
88
|
+
|
|
89
|
+
// Check for nested language directories
|
|
90
|
+
for (const item of items) {
|
|
91
|
+
const itemPath = SecurityUtils.validatePath(path.join(validatedSourceDir, item), process.cwd());
|
|
92
|
+
if (!itemPath) continue;
|
|
93
|
+
|
|
94
|
+
const stat = fs.statSync(itemPath);
|
|
95
|
+
|
|
96
|
+
if (stat.isDirectory()) {
|
|
97
|
+
// This is a language directory, combine all JSON files
|
|
98
|
+
const langFiles = fs.readdirSync(itemPath)
|
|
99
|
+
.filter(file => file.endsWith('.json'))
|
|
100
|
+
.map(file => SecurityUtils.validatePath(path.join(itemPath, file), process.cwd()))
|
|
101
|
+
.filter(file => file !== null);
|
|
102
|
+
|
|
103
|
+
if (langFiles.length > 0) {
|
|
104
|
+
files.push({
|
|
105
|
+
language: item,
|
|
106
|
+
file: `${item}/*.json`,
|
|
107
|
+
path: itemPath,
|
|
108
|
+
files: langFiles
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
} else if (item.endsWith('.json')) {
|
|
112
|
+
// Direct JSON file in root
|
|
113
|
+
const lang = path.basename(item, '.json');
|
|
114
|
+
files.push({
|
|
115
|
+
language: lang,
|
|
116
|
+
file: item,
|
|
117
|
+
path: itemPath
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (this.languages.length > 0) {
|
|
123
|
+
return files.filter(f => this.languages.includes(f.language));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return files;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Analyze file sizes
|
|
130
|
+
analyzeFileSizes(files) {
|
|
131
|
+
console.log(this.t("sizing.analyzing_file_sizes"));
|
|
132
|
+
|
|
133
|
+
files.forEach(({ language, file, path: filePath, files: langFiles }) => {
|
|
134
|
+
if (langFiles) {
|
|
135
|
+
// Handle nested directory structure
|
|
136
|
+
let totalSize = 0;
|
|
137
|
+
let totalLines = 0;
|
|
138
|
+
let totalCharacters = 0;
|
|
139
|
+
let lastModified = new Date(0);
|
|
140
|
+
|
|
141
|
+
langFiles.forEach(langFile => {
|
|
142
|
+
const stats = fs.statSync(langFile);
|
|
143
|
+
let content = SecurityUtils.safeReadFileSync(langFile, process.cwd());
|
|
144
|
+
if (typeof content !== "string") content = "";
|
|
145
|
+
totalSize += stats.size;
|
|
146
|
+
totalLines += content.split('\n').length;
|
|
147
|
+
totalCharacters += content.length;
|
|
148
|
+
if (stats.mtime > lastModified) {
|
|
149
|
+
lastModified = stats.mtime;
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
this.stats.files[language] = {
|
|
154
|
+
file,
|
|
155
|
+
size: totalSize,
|
|
156
|
+
sizeKB: (totalSize / 1024).toFixed(2),
|
|
157
|
+
lines: totalLines,
|
|
158
|
+
characters: totalCharacters,
|
|
159
|
+
lastModified: lastModified,
|
|
160
|
+
fileCount: langFiles.length
|
|
161
|
+
};
|
|
162
|
+
} else {
|
|
163
|
+
// Handle single file structure
|
|
164
|
+
const stats = fs.statSync(filePath);
|
|
165
|
+
let content = SecurityUtils.safeReadFileSync(filePath, process.cwd());
|
|
166
|
+
if (typeof content !== "string") content = "";
|
|
167
|
+
this.stats.files[language] = {
|
|
168
|
+
file,
|
|
169
|
+
size: stats.size,
|
|
170
|
+
sizeKB: (stats.size / 1024).toFixed(2),
|
|
171
|
+
lines: content.split('\n').length,
|
|
172
|
+
characters: content.length,
|
|
173
|
+
lastModified: stats.mtime,
|
|
174
|
+
fileCount: 1
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Analyze translation content
|
|
181
|
+
analyzeTranslationContent(files) {
|
|
182
|
+
console.log(this.t("sizing.analyzing_translation_content"));
|
|
183
|
+
|
|
184
|
+
files.forEach(({ language, path: filePath, files: langFiles }) => {
|
|
185
|
+
try {
|
|
186
|
+
let combinedContent = {};
|
|
187
|
+
|
|
188
|
+
if (langFiles) {
|
|
189
|
+
// Handle nested directory structure - combine all JSON files
|
|
190
|
+
langFiles.forEach(langFile => {
|
|
191
|
+
const rawContent = SecurityUtils.safeReadFileSync(langFile, process.cwd());
|
|
192
|
+
const fileContent = SecurityUtils.safeParseJSON(rawContent);
|
|
193
|
+
if (fileContent) {
|
|
194
|
+
const fileName = path.basename(langFile, '.json');
|
|
195
|
+
combinedContent[fileName] = fileContent;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
} else {
|
|
199
|
+
// Handle single file structure
|
|
200
|
+
const rawContent = SecurityUtils.safeReadFileSync(filePath, process.cwd());
|
|
201
|
+
combinedContent = SecurityUtils.safeParseJSON(rawContent) || {};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const analysis = this.analyzeTranslationObject(combinedContent, '');
|
|
205
|
+
|
|
206
|
+
this.stats.languages[language] = {
|
|
207
|
+
totalKeys: analysis.keyCount,
|
|
208
|
+
totalCharacters: analysis.charCount,
|
|
209
|
+
averageKeyLength: analysis.keyCount > 0 ? analysis.charCount / analysis.keyCount : 0,
|
|
210
|
+
maxKeyLength: analysis.maxLength,
|
|
211
|
+
minKeyLength: analysis.minLength,
|
|
212
|
+
emptyKeys: analysis.emptyKeys,
|
|
213
|
+
longKeys: analysis.longKeys
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// Store individual key sizes for comparison
|
|
217
|
+
Object.entries(analysis.keys).forEach(([key, value]) => {
|
|
218
|
+
if (!this.stats.keys[key]) {
|
|
219
|
+
this.stats.keys[key] = {};
|
|
220
|
+
}
|
|
221
|
+
this.stats.keys[key][language] = {
|
|
222
|
+
length: value.length,
|
|
223
|
+
characters: value.length
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.error(this.t("sizing.failed_to_parse_language_error", { language, errorMessage: error.message }));
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Recursively analyze translation object
|
|
234
|
+
analyzeTranslationObject(obj, prefix = '') {
|
|
235
|
+
let keyCount = 0;
|
|
236
|
+
let charCount = 0;
|
|
237
|
+
let maxLength = 0;
|
|
238
|
+
let minLength = Infinity;
|
|
239
|
+
let emptyKeys = 0;
|
|
240
|
+
let longKeys = 0;
|
|
241
|
+
const keys = {};
|
|
242
|
+
|
|
243
|
+
const traverse = (current, currentPrefix) => {
|
|
244
|
+
Object.entries(current).forEach(([key, value]) => {
|
|
245
|
+
const fullKey = currentPrefix ? `${currentPrefix}.${key}` : key;
|
|
246
|
+
|
|
247
|
+
if (typeof value === 'string') {
|
|
248
|
+
keyCount++;
|
|
249
|
+
charCount += value.length;
|
|
250
|
+
maxLength = Math.max(maxLength, value.length);
|
|
251
|
+
minLength = Math.min(minLength, value.length);
|
|
252
|
+
|
|
253
|
+
if (value.length === 0) emptyKeys++;
|
|
254
|
+
if (value.length > 100) longKeys++;
|
|
255
|
+
|
|
256
|
+
keys[fullKey] = value;
|
|
257
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
258
|
+
traverse(value, fullKey);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
traverse(obj, prefix);
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
keyCount,
|
|
267
|
+
charCount,
|
|
268
|
+
maxLength: maxLength === 0 ? 0 : maxLength,
|
|
269
|
+
minLength: minLength === Infinity ? 0 : minLength,
|
|
270
|
+
emptyKeys,
|
|
271
|
+
longKeys,
|
|
272
|
+
keys
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Generate size comparison analysis
|
|
277
|
+
generateSizeComparison() {
|
|
278
|
+
console.log(this.t("sizing.generating_size_comparisons"));
|
|
279
|
+
|
|
280
|
+
const languages = Object.keys(this.stats.languages);
|
|
281
|
+
const baseLanguage = languages[0]; // Use first language as baseline
|
|
282
|
+
|
|
283
|
+
if (!baseLanguage) {
|
|
284
|
+
console.warn(this.t("sizing.no_languages_found_for_comparison"));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
this.stats.summary = {
|
|
289
|
+
baseLanguage,
|
|
290
|
+
totalLanguages: languages.length,
|
|
291
|
+
sizeVariations: {},
|
|
292
|
+
problematicKeys: [],
|
|
293
|
+
recommendations: []
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
// Compare each language to base language
|
|
297
|
+
languages.forEach(lang => {
|
|
298
|
+
if (lang === baseLanguage) return;
|
|
299
|
+
|
|
300
|
+
const baseStats = this.stats.languages[baseLanguage];
|
|
301
|
+
const langStats = this.stats.languages[lang];
|
|
302
|
+
|
|
303
|
+
const sizeDiff = ((langStats.totalCharacters - baseStats.totalCharacters) / baseStats.totalCharacters) * 100;
|
|
304
|
+
|
|
305
|
+
this.stats.summary.sizeVariations[lang] = {
|
|
306
|
+
characterDifference: langStats.totalCharacters - baseStats.totalCharacters,
|
|
307
|
+
percentageDifference: sizeDiff.toFixed(2),
|
|
308
|
+
isProblematic: Math.abs(sizeDiff) > this.threshold
|
|
309
|
+
};
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Find problematic keys (significant size differences)
|
|
313
|
+
Object.entries(this.stats.keys).forEach(([key, langData]) => {
|
|
314
|
+
const baseLang = langData[baseLanguage];
|
|
315
|
+
if (!baseLang) return;
|
|
316
|
+
|
|
317
|
+
const variations = [];
|
|
318
|
+
Object.entries(langData).forEach(([lang, data]) => {
|
|
319
|
+
if (lang === baseLanguage) return;
|
|
320
|
+
|
|
321
|
+
const diff = ((data.length - baseLang.length) / baseLang.length) * 100;
|
|
322
|
+
if (Math.abs(diff) > this.threshold) {
|
|
323
|
+
variations.push({
|
|
324
|
+
language: lang,
|
|
325
|
+
difference: diff.toFixed(2),
|
|
326
|
+
baseLength: baseLang.length,
|
|
327
|
+
currentLength: data.length
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (variations.length > 0) {
|
|
333
|
+
this.stats.summary.problematicKeys.push({
|
|
334
|
+
key,
|
|
335
|
+
variations
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Generate recommendations
|
|
341
|
+
this.generateRecommendations();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Generate optimization recommendations
|
|
345
|
+
generateRecommendations() {
|
|
346
|
+
const recommendations = [];
|
|
347
|
+
|
|
348
|
+
// Check for large size variations
|
|
349
|
+
Object.entries(this.stats.summary.sizeVariations).forEach(([lang, data]) => {
|
|
350
|
+
if (data.isProblematic) {
|
|
351
|
+
if (data.percentageDifference > 0) {
|
|
352
|
+
recommendations.push(`Consider reviewing ${lang} translations - they are ${data.percentageDifference}% longer than baseline`);
|
|
353
|
+
} else {
|
|
354
|
+
recommendations.push(`Consider reviewing ${lang} translations - they are ${Math.abs(data.percentageDifference)}% shorter than baseline`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Check for problematic keys
|
|
360
|
+
if (this.stats.summary.problematicKeys.length > 0) {
|
|
361
|
+
recommendations.push(`${this.stats.summary.problematicKeys.length} keys have significant size variations across languages`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Check for very long translations
|
|
365
|
+
Object.entries(this.stats.languages).forEach(([lang, data]) => {
|
|
366
|
+
if (data.longKeys > 0) {
|
|
367
|
+
recommendations.push(`${lang} has ${data.longKeys} translations longer than 100 characters - consider breaking them down`);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
this.stats.summary.recommendations = recommendations;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Display results in table format
|
|
375
|
+
displayTable() {
|
|
376
|
+
console.log(this.t("sizing.sizing_analysis_results"));
|
|
377
|
+
console.log("=".repeat(80));
|
|
378
|
+
|
|
379
|
+
// File sizes table
|
|
380
|
+
console.log("\n" + this.t("sizing.file_sizes_title"));
|
|
381
|
+
console.log("-".repeat(80));
|
|
382
|
+
console.log(this.t("sizing.file_sizes_header"));
|
|
383
|
+
console.log("-".repeat(80));
|
|
384
|
+
|
|
385
|
+
Object.entries(this.stats.files).forEach(([lang, data]) => {
|
|
386
|
+
console.log(this.t("sizing.file_size_row", { lang, sizeKB: data.sizeKB, lines: data.lines, characters: data.characters }));
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Language statistics
|
|
390
|
+
console.log("\n" + this.t("sizing.language_statistics_title"));
|
|
391
|
+
console.log("-".repeat(80));
|
|
392
|
+
console.log(this.t("sizing.language_stats_header"));
|
|
393
|
+
console.log("-".repeat(80));
|
|
394
|
+
|
|
395
|
+
Object.entries(this.stats.languages).forEach(([lang, data]) => {
|
|
396
|
+
console.log(this.t("sizing.language_stats_row", { lang, totalKeys: data.totalKeys, totalCharacters: data.totalCharacters, averageKeyLength: data.averageKeyLength.toFixed(1), maxKeyLength: data.maxKeyLength, emptyKeys: data.emptyKeys }));
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Size variations
|
|
400
|
+
if (this.stats.summary.sizeVariations) {
|
|
401
|
+
console.log("\n" + this.t("sizing.size_variations_title"));
|
|
402
|
+
console.log("-".repeat(80));
|
|
403
|
+
console.log(this.t("sizing.size_variations_header"));
|
|
404
|
+
console.log("-".repeat(80));
|
|
405
|
+
|
|
406
|
+
Object.entries(this.stats.summary.sizeVariations).forEach(([lang, data]) => {
|
|
407
|
+
const problematic = data.isProblematic ? this.t("sizing.problematic_yes") : this.t("sizing.problematic_no");
|
|
408
|
+
console.log(this.t("sizing.size_variation_row", { lang, characterDifference: data.characterDifference, percentageDifference: data.percentageDifference, problematic }));
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Recommendations
|
|
413
|
+
if (this.stats.summary.recommendations.length > 0) {
|
|
414
|
+
console.log("\n" + this.t("sizing.recommendations_title"));
|
|
415
|
+
console.log("-".repeat(80));
|
|
416
|
+
this.stats.summary.recommendations.forEach((rec, index) => {
|
|
417
|
+
console.log(`${index + 1}. ${rec}`);
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Generate detailed report
|
|
423
|
+
async generateReport() {
|
|
424
|
+
if (!this.outputReport) return;
|
|
425
|
+
|
|
426
|
+
console.log(this.t("sizing.generating_detailed_report"));
|
|
427
|
+
|
|
428
|
+
const validatedOutputDir = SecurityUtils.validatePath(this.outputDir, process.cwd());
|
|
429
|
+
if (!validatedOutputDir) {
|
|
430
|
+
throw new Error(`Invalid output directory path: ${this.outputDir}`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Ensure output directory exists
|
|
434
|
+
if (!fs.existsSync(validatedOutputDir)) {
|
|
435
|
+
fs.mkdirSync(validatedOutputDir, { recursive: true });
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
439
|
+
const reportPath = SecurityUtils.validatePath(path.join(validatedOutputDir, `sizing-analysis-${timestamp}.json`), process.cwd());
|
|
440
|
+
|
|
441
|
+
if (!reportPath) {
|
|
442
|
+
throw new Error('Invalid report file path');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const report = {
|
|
446
|
+
timestamp: new Date().toISOString(),
|
|
447
|
+
configuration: {
|
|
448
|
+
sourceDir: this.sourceDir,
|
|
449
|
+
languages: this.languages,
|
|
450
|
+
threshold: this.threshold
|
|
451
|
+
},
|
|
452
|
+
analysis: this.stats,
|
|
453
|
+
metadata: {
|
|
454
|
+
totalFiles: Object.keys(this.stats.files).length,
|
|
455
|
+
totalLanguages: Object.keys(this.stats.languages).length,
|
|
456
|
+
totalKeys: Object.keys(this.stats.keys).length
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const success = SecurityUtils.safeWriteFileSync(reportPath, JSON.stringify(report, null, 2), process.cwd());
|
|
461
|
+
if (success) {
|
|
462
|
+
console.log(this.t("sizing.report_saved_to", { reportPath }));
|
|
463
|
+
SecurityUtils.logSecurityEvent('Sizing report saved', 'info', { reportPath });
|
|
464
|
+
} else {
|
|
465
|
+
throw new Error('Failed to save report securely');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Generate CSV if requested
|
|
469
|
+
if (this.format === 'csv') {
|
|
470
|
+
await this.generateCSVReport(timestamp);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Generate CSV report
|
|
475
|
+
async generateCSVReport(timestamp) {
|
|
476
|
+
const validatedOutputDir = SecurityUtils.validatePath(this.outputDir, process.cwd());
|
|
477
|
+
if (!validatedOutputDir) {
|
|
478
|
+
throw new Error(`Invalid output directory path: ${this.outputDir}`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const csvPath = SecurityUtils.validatePath(path.join(validatedOutputDir, `sizing-analysis-${timestamp}.csv`), process.cwd());
|
|
482
|
+
if (!csvPath) {
|
|
483
|
+
throw new Error('Invalid CSV file path');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
let csvContent = 'Language,File Size (KB),Lines,Characters,Total Keys,Avg Key Length,Max Key Length,Empty Keys,Long Keys\n';
|
|
487
|
+
|
|
488
|
+
Object.entries(this.stats.files).forEach(([lang]) => {
|
|
489
|
+
const fileData = this.stats.files[lang];
|
|
490
|
+
const langData = this.stats.languages[lang];
|
|
491
|
+
|
|
492
|
+
csvContent += `${lang},${fileData.sizeKB},${fileData.lines},${fileData.characters},${langData.totalKeys},${langData.averageKeyLength.toFixed(1)},${langData.maxKeyLength},${langData.emptyKeys},${langData.longKeys}\n`;
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const success = SecurityUtils.safeWriteFileSync(csvPath, csvContent, process.cwd());
|
|
496
|
+
if (success) {
|
|
497
|
+
console.log(this.t("sizing.csv_report_saved_to", { csvPath }));
|
|
498
|
+
SecurityUtils.logSecurityEvent('CSV report saved', 'info', { csvPath });
|
|
499
|
+
} else {
|
|
500
|
+
throw new Error('Failed to save CSV report securely');
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Main analysis method
|
|
505
|
+
async analyze() {
|
|
506
|
+
const startTime = performance.now();
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
console.log(this.t("sizing.starting_i18n_sizing_analysis"));
|
|
510
|
+
console.log(this.t("sizing.source_directory", { sourceDir: this.sourceDir }));
|
|
511
|
+
|
|
512
|
+
const files = this.getLanguageFiles();
|
|
513
|
+
|
|
514
|
+
if (files.length === 0) {
|
|
515
|
+
console.log(this.t("sizing.no_translation_files_found"));
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
console.log(this.t("sizing.found_languages", { languages: files.map(f => f.language).join(', ') }));
|
|
520
|
+
|
|
521
|
+
this.analyzeFileSizes(files);
|
|
522
|
+
this.analyzeTranslationContent(files);
|
|
523
|
+
this.generateSizeComparison();
|
|
524
|
+
|
|
525
|
+
if (this.format === 'table') {
|
|
526
|
+
this.displayTable();
|
|
527
|
+
} else if (this.format === 'json') {
|
|
528
|
+
console.log(JSON.stringify(this.stats, null, 2));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
await this.generateReport();
|
|
532
|
+
|
|
533
|
+
const endTime = performance.now();
|
|
534
|
+
console.log(this.t("sizing.analysis_completed", { duration: (endTime - startTime).toFixed(2) }));
|
|
535
|
+
|
|
536
|
+
} catch (error) {
|
|
537
|
+
console.error(this.t("sizing.analysis_failed", { errorMessage: error.message }));
|
|
538
|
+
process.exit(1);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Add run method for compatibility with manager
|
|
543
|
+
async run() {
|
|
544
|
+
return await this.analyze();
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Update the execution block at the end
|
|
549
|
+
if (require.main === module) {
|
|
550
|
+
const analyzer = new I18nSizingAnalyzer();
|
|
551
|
+
analyzer.analyze().catch(error => {
|
|
552
|
+
console.error('❌ Sizing analysis failed:', error.message);
|
|
553
|
+
process.exit(1);
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
module.exports = I18nSizingAnalyzer;
|