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,625 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* I18N TRANSLATION ANALYSIS SCRIPT
|
|
4
|
+
*
|
|
5
|
+
* This script analyzes translation files to identify missing translations,
|
|
6
|
+
* inconsistencies, and provides detailed reports for each language.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node scripts/i18n/02-analyze-translations.js
|
|
10
|
+
* node scripts/i18n/02-analyze-translations.js --language=de
|
|
11
|
+
* node scripts/i18n/02-analyze-translations.js --source-dir=./src/i18n/locales
|
|
12
|
+
* node scripts/i18n/02-analyze-translations.js --output-reports
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const readline = require('readline');
|
|
18
|
+
const { loadTranslations, t } = require('../utils/i18n-helper');
|
|
19
|
+
const settingsManager = require('../settings/settings-manager');
|
|
20
|
+
const SecurityUtils = require('../utils/security');
|
|
21
|
+
|
|
22
|
+
// Get configuration from settings manager
|
|
23
|
+
function getConfig() {
|
|
24
|
+
const settings = settingsManager.getSettings();
|
|
25
|
+
return {
|
|
26
|
+
sourceDir: settings.directories?.sourceDir || './locales',
|
|
27
|
+
sourceLanguage: settings.directories?.sourceLanguage || 'en',
|
|
28
|
+
notTranslatedMarker: settings.processing?.notTranslatedMarker || 'NOT_TRANSLATED',
|
|
29
|
+
outputDir: settings.directories?.outputDir || './i18ntk-reports',
|
|
30
|
+
excludeFiles: settings.processing?.excludeFiles || ['.DS_Store', 'Thumbs.db'],
|
|
31
|
+
uiLanguage: settings.language || 'en'
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class I18nAnalyzer {
|
|
36
|
+
constructor(config = {}) {
|
|
37
|
+
this.config = { ...getConfig(), ...config };
|
|
38
|
+
this.sourceDir = path.resolve(this.config.sourceDir);
|
|
39
|
+
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
40
|
+
this.outputDir = path.resolve(this.config.outputDir);
|
|
41
|
+
|
|
42
|
+
// Initialize i18n with UI language
|
|
43
|
+
const uiLanguage = this.config.uiLanguage || 'en';
|
|
44
|
+
loadTranslations(uiLanguage);
|
|
45
|
+
this.t = t;
|
|
46
|
+
|
|
47
|
+
// Initialize readline interface
|
|
48
|
+
this.rl = null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Initialize readline interface
|
|
52
|
+
initReadline() {
|
|
53
|
+
if (!this.rl) {
|
|
54
|
+
this.rl = readline.createInterface({
|
|
55
|
+
input: process.stdin,
|
|
56
|
+
output: process.stdout
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return this.rl;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Close readline interface
|
|
63
|
+
closeReadline() {
|
|
64
|
+
if (this.rl) {
|
|
65
|
+
this.rl.close();
|
|
66
|
+
this.rl = null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Prompt for user input
|
|
71
|
+
async prompt(question) {
|
|
72
|
+
const rl = this.initReadline();
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
rl.question(question, resolve);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Parse command line arguments
|
|
79
|
+
parseArgs() {
|
|
80
|
+
const args = process.argv.slice(2);
|
|
81
|
+
const parsed = {};
|
|
82
|
+
|
|
83
|
+
args.forEach(arg => {
|
|
84
|
+
if (arg.startsWith('--')) {
|
|
85
|
+
const [key, value] = arg.substring(2).split('=');
|
|
86
|
+
if (key === 'language') {
|
|
87
|
+
parsed.language = value;
|
|
88
|
+
} else if (key === 'source-dir') {
|
|
89
|
+
parsed.sourceDir = value;
|
|
90
|
+
} else if (key === 'output-reports') {
|
|
91
|
+
parsed.outputReports = true;
|
|
92
|
+
} else if (key === 'output-dir') {
|
|
93
|
+
parsed.outputDir = value;
|
|
94
|
+
} else if (key === 'ui-language') {
|
|
95
|
+
parsed.uiLanguage = value;
|
|
96
|
+
} else if (key === 'help') {
|
|
97
|
+
parsed.help = true;
|
|
98
|
+
} else if (key === 'no-prompt') {
|
|
99
|
+
parsed.noPrompt = true;
|
|
100
|
+
} else if (['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(key)) {
|
|
101
|
+
// Support shorthand language flags like --de, --fr, etc.
|
|
102
|
+
parsed.uiLanguage = key;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return parsed;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Get all available languages
|
|
111
|
+
getAvailableLanguages() {
|
|
112
|
+
if (!fs.existsSync(this.sourceDir)) {
|
|
113
|
+
throw new Error(`Source directory not found: ${this.sourceDir}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return fs.readdirSync(this.sourceDir)
|
|
117
|
+
.filter(item => {
|
|
118
|
+
const itemPath = path.join(this.sourceDir, item);
|
|
119
|
+
return fs.statSync(itemPath).isDirectory() && item !== this.config.sourceLanguage;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Get all JSON files from a language directory
|
|
124
|
+
getLanguageFiles(language) {
|
|
125
|
+
const languageDir = path.join(this.sourceDir, language);
|
|
126
|
+
|
|
127
|
+
const validatedPath = SecurityUtils.validatePath(languageDir, this.sourceDir);
|
|
128
|
+
if (!validatedPath || !fs.existsSync(validatedPath)) {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return fs.readdirSync(validatedPath)
|
|
133
|
+
.filter(file => {
|
|
134
|
+
return file.endsWith('.json') &&
|
|
135
|
+
!this.config.excludeFiles.includes(file);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Get all keys recursively from an object
|
|
140
|
+
getAllKeys(obj, prefix = '') {
|
|
141
|
+
const keys = new Set();
|
|
142
|
+
|
|
143
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
144
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
145
|
+
keys.add(fullKey);
|
|
146
|
+
|
|
147
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
148
|
+
const nestedKeys = this.getAllKeys(value, fullKey);
|
|
149
|
+
nestedKeys.forEach(k => keys.add(k));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return keys;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Get value by key path
|
|
157
|
+
getValueByPath(obj, keyPath) {
|
|
158
|
+
const keys = keyPath.split('.');
|
|
159
|
+
let current = obj;
|
|
160
|
+
|
|
161
|
+
for (const key of keys) {
|
|
162
|
+
if (current && typeof current === 'object' && key in current) {
|
|
163
|
+
current = current[key];
|
|
164
|
+
} else {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return current;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Analyze translation issues in an object
|
|
173
|
+
analyzeTranslationIssues(obj, sourceObj = null, prefix = '') {
|
|
174
|
+
const issues = [];
|
|
175
|
+
|
|
176
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
177
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
178
|
+
const sourceValue = sourceObj ? this.getValueByPath(sourceObj, fullKey) : null;
|
|
179
|
+
|
|
180
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
181
|
+
issues.push(...this.analyzeTranslationIssues(value, sourceObj, fullKey));
|
|
182
|
+
} else if (typeof value === 'string') {
|
|
183
|
+
if (value === this.config.notTranslatedMarker) {
|
|
184
|
+
issues.push({
|
|
185
|
+
type: 'not_translated',
|
|
186
|
+
key: fullKey,
|
|
187
|
+
value,
|
|
188
|
+
sourceValue: sourceValue || 'N/A'
|
|
189
|
+
});
|
|
190
|
+
} else if (value === '') {
|
|
191
|
+
issues.push({
|
|
192
|
+
type: 'empty_value',
|
|
193
|
+
key: fullKey,
|
|
194
|
+
value,
|
|
195
|
+
sourceValue: sourceValue || 'N/A'
|
|
196
|
+
});
|
|
197
|
+
} else if (value.includes(this.config.notTranslatedMarker)) {
|
|
198
|
+
issues.push({
|
|
199
|
+
type: 'partial_translation',
|
|
200
|
+
key: fullKey,
|
|
201
|
+
value,
|
|
202
|
+
sourceValue: sourceValue || 'N/A'
|
|
203
|
+
});
|
|
204
|
+
} else if (sourceValue && value === sourceValue) {
|
|
205
|
+
issues.push({
|
|
206
|
+
type: 'same_as_source',
|
|
207
|
+
key: fullKey,
|
|
208
|
+
value,
|
|
209
|
+
sourceValue
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return issues;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Get translation statistics for an object
|
|
219
|
+
getTranslationStats(obj) {
|
|
220
|
+
let total = 0;
|
|
221
|
+
let translated = 0;
|
|
222
|
+
let notTranslated = 0;
|
|
223
|
+
let empty = 0;
|
|
224
|
+
let partial = 0;
|
|
225
|
+
|
|
226
|
+
const count = (item) => {
|
|
227
|
+
if (typeof item === 'string') {
|
|
228
|
+
total++;
|
|
229
|
+
if (item === this.config.notTranslatedMarker) {
|
|
230
|
+
notTranslated++;
|
|
231
|
+
} else if (item === '') {
|
|
232
|
+
empty++;
|
|
233
|
+
} else if (item.includes(this.config.notTranslatedMarker)) {
|
|
234
|
+
partial++;
|
|
235
|
+
} else {
|
|
236
|
+
translated++;
|
|
237
|
+
}
|
|
238
|
+
} else if (Array.isArray(item)) {
|
|
239
|
+
item.forEach(count);
|
|
240
|
+
} else if (item && typeof item === 'object') {
|
|
241
|
+
Object.values(item).forEach(count);
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
count(obj);
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
total,
|
|
249
|
+
translated,
|
|
250
|
+
notTranslated,
|
|
251
|
+
empty,
|
|
252
|
+
partial,
|
|
253
|
+
percentage: total > 0 ? Math.round((translated / total) * 100) : 0,
|
|
254
|
+
missing: notTranslated + empty + partial
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Check structural consistency between source and target
|
|
259
|
+
checkStructuralConsistency(sourceObj, targetObj) {
|
|
260
|
+
const sourceKeys = this.getAllKeys(sourceObj);
|
|
261
|
+
const targetKeys = this.getAllKeys(targetObj);
|
|
262
|
+
|
|
263
|
+
const missingKeys = [...sourceKeys].filter(key => !targetKeys.has(key));
|
|
264
|
+
const extraKeys = [...targetKeys].filter(key => !sourceKeys.has(key));
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
isConsistent: missingKeys.length === 0 && extraKeys.length === 0,
|
|
268
|
+
missingKeys,
|
|
269
|
+
extraKeys,
|
|
270
|
+
sourceKeyCount: sourceKeys.size,
|
|
271
|
+
targetKeyCount: targetKeys.size
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Analyze a single language
|
|
276
|
+
analyzeLanguage(language) {
|
|
277
|
+
const languageDir = path.join(this.sourceDir, language);
|
|
278
|
+
const sourceFiles = this.getLanguageFiles(this.config.sourceLanguage);
|
|
279
|
+
const targetFiles = this.getLanguageFiles(language);
|
|
280
|
+
|
|
281
|
+
const analysis = {
|
|
282
|
+
language,
|
|
283
|
+
files: {},
|
|
284
|
+
summary: {
|
|
285
|
+
totalFiles: sourceFiles.length,
|
|
286
|
+
analyzedFiles: 0,
|
|
287
|
+
totalKeys: 0,
|
|
288
|
+
translatedKeys: 0,
|
|
289
|
+
missingKeys: 0,
|
|
290
|
+
issues: []
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
for (const fileName of sourceFiles) {
|
|
295
|
+
const sourceFilePath = path.join(this.sourceLanguageDir, fileName);
|
|
296
|
+
const targetFilePath = path.join(languageDir, fileName);
|
|
297
|
+
|
|
298
|
+
if (!fs.existsSync(sourceFilePath)) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let sourceContent, targetContent;
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
const validatedSourcePath = SecurityUtils.validatePath(sourceFilePath, this.sourceDir);
|
|
306
|
+
if (!validatedSourcePath) {
|
|
307
|
+
analysis.files[fileName] = {
|
|
308
|
+
error: 'Invalid source file path'
|
|
309
|
+
};
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const sourceFileContent = SecurityUtils.safeReadFile(validatedSourcePath, this.sourceDir);
|
|
313
|
+
if (!sourceFileContent) {
|
|
314
|
+
analysis.files[fileName] = {
|
|
315
|
+
error: 'Failed to read source file securely'
|
|
316
|
+
};
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
sourceContent = SecurityUtils.safeParseJSON(sourceFileContent);
|
|
320
|
+
if (!sourceContent) {
|
|
321
|
+
analysis.files[fileName] = {
|
|
322
|
+
error: 'Failed to parse source file JSON'
|
|
323
|
+
};
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
} catch (error) {
|
|
327
|
+
analysis.files[fileName] = {
|
|
328
|
+
error: `Failed to parse source file: ${error.message}`
|
|
329
|
+
};
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (!fs.existsSync(targetFilePath)) {
|
|
334
|
+
analysis.files[fileName] = {
|
|
335
|
+
status: 'missing',
|
|
336
|
+
sourceKeys: this.getAllKeys(sourceContent).size
|
|
337
|
+
};
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
const validatedTargetPath = SecurityUtils.validatePath(targetFilePath, this.sourceDir);
|
|
343
|
+
if (!validatedTargetPath) {
|
|
344
|
+
analysis.files[fileName] = {
|
|
345
|
+
error: 'Invalid target file path'
|
|
346
|
+
};
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const targetFileContent = SecurityUtils.safeReadFile(validatedTargetPath, this.sourceDir);
|
|
350
|
+
if (!targetFileContent) {
|
|
351
|
+
analysis.files[fileName] = {
|
|
352
|
+
error: 'Failed to read target file securely'
|
|
353
|
+
};
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
targetContent = SecurityUtils.safeParseJSON(targetFileContent);
|
|
357
|
+
if (!targetContent) {
|
|
358
|
+
analysis.files[fileName] = {
|
|
359
|
+
error: 'Failed to parse target file JSON'
|
|
360
|
+
};
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
} catch (error) {
|
|
364
|
+
analysis.files[fileName] = {
|
|
365
|
+
error: `Failed to parse target file: ${error.message}`
|
|
366
|
+
};
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Analyze this file
|
|
371
|
+
const stats = this.getTranslationStats(targetContent);
|
|
372
|
+
const structural = this.checkStructuralConsistency(sourceContent, targetContent);
|
|
373
|
+
const issues = this.analyzeTranslationIssues(targetContent, sourceContent);
|
|
374
|
+
|
|
375
|
+
analysis.files[fileName] = {
|
|
376
|
+
status: 'analyzed',
|
|
377
|
+
stats,
|
|
378
|
+
structural,
|
|
379
|
+
issues,
|
|
380
|
+
sourceFilePath,
|
|
381
|
+
targetFilePath
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// Update summary
|
|
385
|
+
analysis.summary.analyzedFiles++;
|
|
386
|
+
analysis.summary.totalKeys += stats.total;
|
|
387
|
+
analysis.summary.translatedKeys += stats.translated;
|
|
388
|
+
analysis.summary.missingKeys += stats.missing;
|
|
389
|
+
analysis.summary.issues.push(...issues);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Calculate overall percentage
|
|
393
|
+
analysis.summary.percentage = analysis.summary.totalKeys > 0
|
|
394
|
+
? Math.round((analysis.summary.translatedKeys / analysis.summary.totalKeys) * 100)
|
|
395
|
+
: 0;
|
|
396
|
+
|
|
397
|
+
return analysis;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Generate detailed report for a language
|
|
401
|
+
generateLanguageReport(analysis) {
|
|
402
|
+
const { language } = analysis;
|
|
403
|
+
const timestamp = new Date().toISOString();
|
|
404
|
+
|
|
405
|
+
let report = `${this.t('analyzeTranslations.reportTitle', { language: language.toUpperCase() })}\n`;
|
|
406
|
+
report += `${this.t('analyzeTranslations.generated', { timestamp })}\n`;
|
|
407
|
+
report += `${this.t('analyzeTranslations.status', { translated: analysis.summary.translatedKeys, total: analysis.summary.totalKeys, percentage: analysis.summary.percentage })}\n`;
|
|
408
|
+
report += `${this.t('analyzeTranslations.filesAnalyzed', { analyzed: analysis.summary.analyzedFiles, total: analysis.summary.totalFiles })}\n`;
|
|
409
|
+
report += `${this.t('analyzeTranslations.keysNeedingTranslation', { count: analysis.summary.missingKeys })}\n\n`;
|
|
410
|
+
|
|
411
|
+
report += `${this.t('analyzeTranslations.fileBreakdown')}\n`;
|
|
412
|
+
report += `${'='.repeat(50)}\n\n`;
|
|
413
|
+
|
|
414
|
+
Object.entries(analysis.files).forEach(([fileName, fileData]) => {
|
|
415
|
+
report += `\uD83D\uDCC4 ${fileName}\n`;
|
|
416
|
+
|
|
417
|
+
if (fileData.error) {
|
|
418
|
+
report += ` \u274C ${this.t('analyzeTranslations.error')}: ${fileData.error}\n\n`;
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (fileData.status === 'missing') {
|
|
423
|
+
report += ` \u274C ${this.t('analyzeTranslations.statusFileMissing')}\n`;
|
|
424
|
+
report += ` \uD83D\uDCCA ${this.t('analyzeTranslations.sourceKeys', { count: fileData.sourceKeys })}\n\n`;
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const { stats, structural, issues } = fileData;
|
|
429
|
+
|
|
430
|
+
report += ` \uD83D\uDCCA ${this.t('analyzeTranslations.translation', { translated: stats.translated, total: stats.total, percentage: stats.percentage })}\n`;
|
|
431
|
+
report += ` \uD83C\uDFD7ļø ${this.t('analyzeTranslations.structure', { status: structural.isConsistent ? this.t('analyzeTranslations.consistent') : this.t('analyzeTranslations.inconsistent') })}\n`;
|
|
432
|
+
|
|
433
|
+
if (!structural.isConsistent) {
|
|
434
|
+
if (structural.missingKeys.length > 0) {
|
|
435
|
+
report += ` ${this.t('analyzeTranslations.missingKeys', { count: structural.missingKeys.length })}\n`;
|
|
436
|
+
}
|
|
437
|
+
if (structural.extraKeys.length > 0) {
|
|
438
|
+
report += ` ${this.t('analyzeTranslations.extraKeys', { count: structural.extraKeys.length })}\n`;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (issues.length > 0) {
|
|
443
|
+
report += ` \u26A0ļø ${this.t('analyzeTranslations.issues', { count: issues.length })}\n`;
|
|
444
|
+
|
|
445
|
+
const issueTypes = {
|
|
446
|
+
not_translated: issues.filter(i => i.type === 'not_translated').length,
|
|
447
|
+
empty_value: issues.filter(i => i.type === 'empty_value').length,
|
|
448
|
+
partial_translation: issues.filter(i => i.type === 'partial_translation').length,
|
|
449
|
+
same_as_source: issues.filter(i => i.type === 'same_as_source').length
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
Object.entries(issueTypes).forEach(([type, count]) => {
|
|
453
|
+
if (count > 0) {
|
|
454
|
+
report += ` ${this.t('analyzeTranslations.issueType.' + type, { count })}\n`;
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
report += `\n`;
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Keys needing translation
|
|
463
|
+
const notTranslatedIssues = analysis.summary.issues.filter(issue =>
|
|
464
|
+
issue.type === 'not_translated' || issue.type === 'empty_value'
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
if (notTranslatedIssues.length > 0) {
|
|
468
|
+
report += `${this.t('analyzeTranslations.keysToTranslate')}\n`;
|
|
469
|
+
report += `${'='.repeat(50)}\n\n`;
|
|
470
|
+
|
|
471
|
+
notTranslatedIssues.slice(0, 50).forEach(issue => {
|
|
472
|
+
report += `${this.t('analyzeTranslations.key')}: ${issue.key}\n`;
|
|
473
|
+
report += `${this.t('analyzeTranslations.english')}: \"${issue.sourceValue}\"\n`;
|
|
474
|
+
report += `${language}: [${this.t('analyzeTranslations.needsTranslation')}]\n\n`;
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
if (notTranslatedIssues.length > 50) {
|
|
478
|
+
report += `${this.t('analyzeTranslations.andMoreKeys', { count: notTranslatedIssues.length - 50 })}\n\n`;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return report;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Save report to file
|
|
486
|
+
async saveReport(language, report) {
|
|
487
|
+
const reportPath = path.join(this.outputDir, `analysis-${language}.txt`);
|
|
488
|
+
const validatedPath = SecurityUtils.validatePath(reportPath, this.outputDir);
|
|
489
|
+
|
|
490
|
+
if (!validatedPath) {
|
|
491
|
+
throw new Error('Invalid report file path');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const success = await SecurityUtils.safeWriteFile(validatedPath, report, this.outputDir);
|
|
495
|
+
if (!success) {
|
|
496
|
+
throw new Error('Failed to write report file securely');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return validatedPath;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Show help message
|
|
503
|
+
showHelp() {
|
|
504
|
+
console.log(this.t('analyzeTranslations.help_message'));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Main analyze method
|
|
508
|
+
async analyze() {
|
|
509
|
+
try {
|
|
510
|
+
const results = []; // Add this line to declare the results array
|
|
511
|
+
|
|
512
|
+
console.log(this.t('analyzeTranslations.starting') || 'š Starting translation analysis...');
|
|
513
|
+
|
|
514
|
+
// Ensure output directory exists
|
|
515
|
+
if (!fs.existsSync(this.outputDir)) {
|
|
516
|
+
fs.mkdirSync(this.outputDir, { recursive: true });
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const languages = this.getAvailableLanguages();
|
|
520
|
+
|
|
521
|
+
if (languages.length === 0) {
|
|
522
|
+
console.log(this.t('analyzeTranslations.noLanguages') || 'ā ļø No target languages found.');
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
console.log(this.t('analyzeTranslations.foundLanguages', { count: languages.length, languages: languages.join(', ') }) || `š Found ${languages.length} languages to analyze: ${languages.join(', ')}`);
|
|
527
|
+
|
|
528
|
+
for (const language of languages) {
|
|
529
|
+
console.log(this.t('analyzeTranslations.analyzing', { language }) || `\nš Analyzing ${language}...`);
|
|
530
|
+
|
|
531
|
+
const analysis = this.analyzeLanguage(language);
|
|
532
|
+
const report = this.generateLanguageReport(analysis);
|
|
533
|
+
|
|
534
|
+
// Save report
|
|
535
|
+
const reportPath = await this.saveReport(language, report);
|
|
536
|
+
|
|
537
|
+
console.log(this.t('analyzeTranslations.completed', { language }) || `ā
Analysis completed for ${language}`);
|
|
538
|
+
console.log(this.t('analyzeTranslations.progress', {
|
|
539
|
+
percentage: analysis.summary.percentage,
|
|
540
|
+
translatedKeys: analysis.summary.translatedKeys,
|
|
541
|
+
totalKeys: analysis.summary.totalKeys
|
|
542
|
+
}) || ` Progress: ${analysis.summary.percentage}% (${analysis.summary.translatedKeys}/${analysis.summary.totalKeys} keys)`);
|
|
543
|
+
console.log(this.t('analyzeTranslations.reportSaved', { reportPath }) || ` Report saved: ${reportPath}`);
|
|
544
|
+
|
|
545
|
+
results.push({
|
|
546
|
+
language,
|
|
547
|
+
analysis,
|
|
548
|
+
reportPath
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Summary
|
|
553
|
+
console.log(this.t('analyzeTranslations.summary') || '\nš ANALYSIS SUMMARY');
|
|
554
|
+
console.log('='.repeat(50));
|
|
555
|
+
|
|
556
|
+
results.forEach(({ language, analysis }) => {
|
|
557
|
+
console.log(`${language}: ${analysis.summary.percentage}% complete (${analysis.summary.translatedKeys}/${analysis.summary.totalKeys} keys)`);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
console.log(this.t('analyzeTranslations.finished') || '\nā
Analysis completed successfully!');
|
|
561
|
+
|
|
562
|
+
// Only prompt for input if running standalone (not from menu or workflow)
|
|
563
|
+
if (require.main === module && !this.noPrompt) {
|
|
564
|
+
await this.prompt('\nPress Enter to continue...');
|
|
565
|
+
}
|
|
566
|
+
this.closeReadline();
|
|
567
|
+
|
|
568
|
+
return results;
|
|
569
|
+
|
|
570
|
+
} catch (error) {
|
|
571
|
+
console.error(this.t('analyzeTranslations.error') || 'ā Analysis failed:', error.message);
|
|
572
|
+
this.closeReadline();
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Main analysis process
|
|
578
|
+
async run() {
|
|
579
|
+
try {
|
|
580
|
+
const args = this.parseArgs();
|
|
581
|
+
|
|
582
|
+
if (args.help) {
|
|
583
|
+
this.showHelp();
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Set noPrompt flag
|
|
588
|
+
this.noPrompt = args.noPrompt;
|
|
589
|
+
|
|
590
|
+
// Handle UI language change
|
|
591
|
+
if (args.uiLanguage) {
|
|
592
|
+
loadTranslations(args.uiLanguage);
|
|
593
|
+
this.t = t;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Update config if source directory is provided
|
|
597
|
+
if (args.sourceDir) {
|
|
598
|
+
this.config.sourceDir = args.sourceDir;
|
|
599
|
+
this.sourceDir = path.resolve(this.config.sourceDir);
|
|
600
|
+
this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (args.outputDir) {
|
|
604
|
+
this.config.outputDir = args.outputDir;
|
|
605
|
+
this.outputDir = path.resolve(this.config.outputDir);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
await this.analyze();
|
|
609
|
+
} catch (error) {
|
|
610
|
+
this.closeReadline();
|
|
611
|
+
throw error;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Run if called directly
|
|
617
|
+
if (require.main === module) {
|
|
618
|
+
const analyzer = new I18nAnalyzer();
|
|
619
|
+
analyzer.run().catch(error => {
|
|
620
|
+
console.error('ā Analysis failed:', error.message);
|
|
621
|
+
process.exit(1);
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
module.exports = I18nAnalyzer;
|