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,1282 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* I18N USAGE ANALYSIS TOOLKIT - Version 1.4.3
|
|
4
|
+
*
|
|
5
|
+
* This script analyzes source code to find unused translation keys,
|
|
6
|
+
* missing translations, and provides comprehensive translation completeness analysis.
|
|
7
|
+
*
|
|
8
|
+
* NEW in v1.4.3:
|
|
9
|
+
* - Modular folder structure support
|
|
10
|
+
* - Recursive translation file discovery
|
|
11
|
+
* - NOT_TRANSLATED analysis
|
|
12
|
+
* - Enhanced reporting with completeness statistics
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* npm run i18ntk:usage
|
|
16
|
+
* npm run i18ntk:usage -- --source-dir=./src
|
|
17
|
+
* npm run i18ntk:usage -- --i18n-dir=./src/i18n/locales
|
|
18
|
+
* npm run i18ntk:usage -- --output-report
|
|
19
|
+
*
|
|
20
|
+
* Alternative direct usage:
|
|
21
|
+
* node i18ntk-usage.js
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const readline = require('readline');
|
|
27
|
+
const { loadTranslations, t } = require('../utils/i18n-helper');
|
|
28
|
+
const settingsManager = require('../settings/settings-manager');
|
|
29
|
+
const SecurityUtils = require('../utils/security');
|
|
30
|
+
const AdminCLI = require('../utils/admin-cli');
|
|
31
|
+
|
|
32
|
+
// Enhanced configuration with multiple source directory detection
|
|
33
|
+
async function getConfig() {
|
|
34
|
+
try {
|
|
35
|
+
const settings = settingsManager.getSettings();
|
|
36
|
+
|
|
37
|
+
// Multiple possible source directories to check
|
|
38
|
+
const possibleSourceDirs = [
|
|
39
|
+
'./main', // Primary source directory for this project
|
|
40
|
+
'./src',
|
|
41
|
+
'./app',
|
|
42
|
+
'./components',
|
|
43
|
+
'./pages',
|
|
44
|
+
'./views',
|
|
45
|
+
'./client',
|
|
46
|
+
'./frontend',
|
|
47
|
+
'./' // Current directory as fallback
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// Auto-detect source directory
|
|
51
|
+
let detectedSourceDir = './src'; // Default
|
|
52
|
+
for (const dir of possibleSourceDirs) {
|
|
53
|
+
if (fs.existsSync(dir)) {
|
|
54
|
+
// Check if directory contains code files
|
|
55
|
+
try {
|
|
56
|
+
const files = fs.readdirSync(dir);
|
|
57
|
+
const hasCodeFiles = files.some(file =>
|
|
58
|
+
['.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte'].includes(path.extname(file))
|
|
59
|
+
);
|
|
60
|
+
if (hasCodeFiles) {
|
|
61
|
+
detectedSourceDir = dir;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// Continue checking
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Multiple possible i18n directories
|
|
71
|
+
const possibleI18nDirs = [
|
|
72
|
+
'./locales',
|
|
73
|
+
'./src/locales',
|
|
74
|
+
'./src/i18n',
|
|
75
|
+
'./src/i18n/locales',
|
|
76
|
+
'./app/locales',
|
|
77
|
+
'./app/i18n',
|
|
78
|
+
'./public/locales',
|
|
79
|
+
'./assets/locales',
|
|
80
|
+
'./translations',
|
|
81
|
+
'./lang'
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// Auto-detect i18n directory
|
|
85
|
+
let detectedI18nDir = './locales'; // Default
|
|
86
|
+
for (const dir of possibleI18nDirs) {
|
|
87
|
+
if (fs.existsSync(dir)) {
|
|
88
|
+
// Check if directory contains language subdirectories or JSON files
|
|
89
|
+
try {
|
|
90
|
+
const items = fs.readdirSync(dir);
|
|
91
|
+
const hasLanguageDirs = items.some(item => {
|
|
92
|
+
const itemPath = path.join(dir, item);
|
|
93
|
+
if (fs.statSync(itemPath).isDirectory()) {
|
|
94
|
+
return ['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(item);
|
|
95
|
+
}
|
|
96
|
+
return item.endsWith('.json');
|
|
97
|
+
});
|
|
98
|
+
if (hasLanguageDirs) {
|
|
99
|
+
detectedI18nDir = dir;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
// Continue checking
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const config = {
|
|
109
|
+
sourceDir: settings.directories?.sourceDir || detectedSourceDir,
|
|
110
|
+
i18nDir: settings.directories?.i18nDir || detectedI18nDir,
|
|
111
|
+
sourceLanguage: settings.directories?.sourceLanguage || settings.sourceLanguage || 'en',
|
|
112
|
+
outputDir: settings.directories?.outputDir || settings.outputDir || './i18ntk-reports',
|
|
113
|
+
excludeDirs: settings.processing?.excludeDirs || [
|
|
114
|
+
'node_modules', '.git', 'dist', 'build', '.next', '.nuxt',
|
|
115
|
+
'i18ntk-reports', 'reports', 'dev', 'utils', 'test', 'tests'
|
|
116
|
+
],
|
|
117
|
+
includeExtensions: settings.processing?.includeExtensions || ['.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte'],
|
|
118
|
+
translationPatterns: settings.processing?.translationPatterns || [
|
|
119
|
+
/t\(['"`]([^'"`]+)['"`]\)/g,
|
|
120
|
+
/\$t\(['"`]([^'"`]+)['"`]\)/g,
|
|
121
|
+
/i18n\.t\(['"`]([^'"`]+)['"`]\)/g,
|
|
122
|
+
/useTranslation\(\).*t\(['"`]([^'"`]+)['"`]\)/g
|
|
123
|
+
]
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
console.log(`Detected source directory: ${config.sourceDir}`);
|
|
127
|
+
console.log(`Detected i18n directory: ${config.i18nDir}`);
|
|
128
|
+
|
|
129
|
+
return config;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
throw new Error(`Configuration error: ${error.message}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
class I18nUsageAnalyzer {
|
|
136
|
+
constructor(config = {}) {
|
|
137
|
+
this.config = config;
|
|
138
|
+
this.sourceDir = null;
|
|
139
|
+
this.i18nDir = null;
|
|
140
|
+
this.sourceLanguageDir = null;
|
|
141
|
+
|
|
142
|
+
// Initialize class properties
|
|
143
|
+
this.availableKeys = new Set();
|
|
144
|
+
this.usedKeys = new Set();
|
|
145
|
+
this.fileUsage = new Map();
|
|
146
|
+
this.translationFiles = new Map(); // New: Track all translation files
|
|
147
|
+
this.translationStats = new Map(); // New: Track translation completeness
|
|
148
|
+
|
|
149
|
+
// Initialize UI i18n for console messages
|
|
150
|
+
const UIi18n = require('./ui-i18n');
|
|
151
|
+
this.ui = new UIi18n();
|
|
152
|
+
this.t = this.ui.t.bind(this.ui);
|
|
153
|
+
|
|
154
|
+
// Initialize readline interface
|
|
155
|
+
this.rl = null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Initialize readline interface
|
|
159
|
+
initReadline() {
|
|
160
|
+
if (!this.rl) {
|
|
161
|
+
this.rl = readline.createInterface({
|
|
162
|
+
input: process.stdin,
|
|
163
|
+
output: process.stdout
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return this.rl;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Close readline interface
|
|
170
|
+
closeReadline() {
|
|
171
|
+
if (this.rl) {
|
|
172
|
+
this.rl.close();
|
|
173
|
+
this.rl = null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Prompt for user input
|
|
178
|
+
async prompt(question) {
|
|
179
|
+
const rl = this.initReadline();
|
|
180
|
+
return new Promise((resolve) => {
|
|
181
|
+
rl.question(question, resolve);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async initialize() {
|
|
186
|
+
try {
|
|
187
|
+
const defaultConfig = await getConfig();
|
|
188
|
+
this.config = { ...defaultConfig, ...this.config };
|
|
189
|
+
|
|
190
|
+
// Resolve paths
|
|
191
|
+
this.sourceDir = path.resolve(this.config.sourceDir);
|
|
192
|
+
this.i18nDir = path.resolve(this.config.i18nDir);
|
|
193
|
+
this.sourceLanguageDir = path.join(this.i18nDir, this.config.sourceLanguage);
|
|
194
|
+
|
|
195
|
+
// Verify translation function
|
|
196
|
+
if (typeof this.t !== 'function') {
|
|
197
|
+
throw new Error('Translation function not properly initialized');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
await SecurityUtils.logSecurityEvent('analyzer_initialized', { component: 'i18ntk-usage' });
|
|
201
|
+
} catch (error) {
|
|
202
|
+
await SecurityUtils.logSecurityEvent('analyzer_init_failed', { component: 'i18ntk-usage', error: error.message });
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Parse command line arguments
|
|
208
|
+
async parseArgs() {
|
|
209
|
+
try {
|
|
210
|
+
const args = process.argv.slice(2);
|
|
211
|
+
const parsed = {};
|
|
212
|
+
|
|
213
|
+
// Convert array to object for processing
|
|
214
|
+
const argsObj = {};
|
|
215
|
+
for (let i = 0; i < args.length; i++) {
|
|
216
|
+
const arg = args[i];
|
|
217
|
+
if (arg.startsWith('--')) {
|
|
218
|
+
const key = arg.substring(2);
|
|
219
|
+
if (key.includes('=')) {
|
|
220
|
+
const [k, v] = key.split('=', 2);
|
|
221
|
+
argsObj[k] = v;
|
|
222
|
+
} else {
|
|
223
|
+
argsObj[key] = args[i + 1] && !args[i + 1].startsWith('--') ? args[++i] : true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const validatedArgs = await SecurityUtils.validateCommandArgs(argsObj);
|
|
229
|
+
|
|
230
|
+
// Process validated arguments
|
|
231
|
+
for (const [key, value] of Object.entries(validatedArgs)) {
|
|
232
|
+
if (key === 'source-dir' && value) {
|
|
233
|
+
const sanitized = await SecurityUtils.sanitizeInput(value);
|
|
234
|
+
const validated = SecurityUtils.validatePath(sanitized, process.cwd());
|
|
235
|
+
if (validated) {
|
|
236
|
+
parsed.sourceDir = validated;
|
|
237
|
+
}
|
|
238
|
+
} else if (key === 'i18n-dir' && value) {
|
|
239
|
+
const sanitized = await SecurityUtils.sanitizeInput(value);
|
|
240
|
+
const validated = SecurityUtils.validatePath(sanitized, process.cwd());
|
|
241
|
+
if (validated) {
|
|
242
|
+
parsed.i18nDir = validated;
|
|
243
|
+
}
|
|
244
|
+
} else if (key === 'output-dir' && value) {
|
|
245
|
+
const sanitized = await SecurityUtils.sanitizeInput(value);
|
|
246
|
+
const validated = SecurityUtils.validatePath(sanitized, process.cwd());
|
|
247
|
+
if (validated) {
|
|
248
|
+
parsed.outputDir = validated;
|
|
249
|
+
}
|
|
250
|
+
} else if (key === 'help') {
|
|
251
|
+
parsed.help = true;
|
|
252
|
+
} else if (key === 'no-prompt') {
|
|
253
|
+
parsed.noPrompt = true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
await SecurityUtils.logSecurityEvent('args_parsed', { component: 'i18ntk-usage', args: parsed });
|
|
258
|
+
return parsed;
|
|
259
|
+
} catch (error) {
|
|
260
|
+
await SecurityUtils.logSecurityEvent('args_parse_failed', { component: 'i18ntk-usage', error: error.message });
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// NEW: Recursively discover all translation files in modular structure
|
|
266
|
+
async discoverTranslationFiles(baseDir, language = this.config.sourceLanguage) {
|
|
267
|
+
const translationFiles = [];
|
|
268
|
+
|
|
269
|
+
const traverse = async (currentDir) => {
|
|
270
|
+
try {
|
|
271
|
+
const absoluteDir = path.resolve(currentDir);
|
|
272
|
+
const validatedPath = SecurityUtils.validatePath(absoluteDir, process.cwd());
|
|
273
|
+
|
|
274
|
+
if (!validatedPath || !fs.existsSync(validatedPath)) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const items = fs.readdirSync(validatedPath);
|
|
279
|
+
|
|
280
|
+
for (const item of items) {
|
|
281
|
+
const itemPath = path.join(validatedPath, item);
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const stat = fs.statSync(itemPath);
|
|
285
|
+
|
|
286
|
+
if (stat.isDirectory()) {
|
|
287
|
+
// Skip excluded directories
|
|
288
|
+
if (!this.config.excludeDirs.includes(item)) {
|
|
289
|
+
await traverse(itemPath);
|
|
290
|
+
}
|
|
291
|
+
} else if (stat.isFile()) {
|
|
292
|
+
// Look for translation files:
|
|
293
|
+
// 1. Direct language files: en.json, de.json, etc.
|
|
294
|
+
// 2. Language directory files: en/common.json, de/auth.json, etc.
|
|
295
|
+
// 3. Nested modular files: components/en.json, features/auth/en.json, etc.
|
|
296
|
+
|
|
297
|
+
const fileName = path.basename(item, '.json');
|
|
298
|
+
const parentDir = path.basename(path.dirname(itemPath));
|
|
299
|
+
|
|
300
|
+
if (item.endsWith('.json')) {
|
|
301
|
+
// Case 1: Direct language files (en.json)
|
|
302
|
+
if (fileName === language) {
|
|
303
|
+
translationFiles.push({
|
|
304
|
+
filePath: itemPath,
|
|
305
|
+
namespace: path.relative(baseDir, path.dirname(itemPath)).replace(/[\\/]/g, '.') || 'root',
|
|
306
|
+
language: language,
|
|
307
|
+
type: 'direct'
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
// Case 2: Files in language directories (en/common.json)
|
|
311
|
+
else if (parentDir === language) {
|
|
312
|
+
translationFiles.push({
|
|
313
|
+
filePath: itemPath,
|
|
314
|
+
namespace: fileName,
|
|
315
|
+
language: language,
|
|
316
|
+
type: 'namespaced'
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
} catch (statError) {
|
|
322
|
+
// Skip files that can't be accessed
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} catch (error) {
|
|
327
|
+
await SecurityUtils.logSecurityEvent('translation_discovery_error', {
|
|
328
|
+
component: 'i18ntk-usage',
|
|
329
|
+
directory: currentDir,
|
|
330
|
+
error: error.message
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
await traverse(baseDir);
|
|
336
|
+
return translationFiles;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Get all files recursively from a directory with enhanced filtering
|
|
340
|
+
async getAllFiles(dir, extensions = this.config.includeExtensions) {
|
|
341
|
+
const files = [];
|
|
342
|
+
|
|
343
|
+
// Enhanced list of toolkit files to exclude from analysis
|
|
344
|
+
const excludeFiles = [
|
|
345
|
+
'i18ntk-analyze.js', 'i18ntk-autorun.js', 'i18ntk-complete.js',
|
|
346
|
+
'i18ntk-init.js', 'i18ntk-manage.js', 'i18ntk-sizing.js',
|
|
347
|
+
'i18ntk-summary.js', 'i18ntk-usage.js', 'i18ntk-validate.js',
|
|
348
|
+
'console-translations.js', 'console-key-checker.js',
|
|
349
|
+
'complete-console-translations.js', 'detect-language-mismatches.js',
|
|
350
|
+
'export-missing-keys.js', 'maintain-language-purity.js',
|
|
351
|
+
'native-translations.js', 'settings-cli.js', 'settings-manager.js',
|
|
352
|
+
'test-complete-system.js', 'test-console-i18n.js', 'test-features.js',
|
|
353
|
+
'translate-mismatches.js', 'ui-i18n.js', 'update-console-i18n.js',
|
|
354
|
+
'validate-language-purity.js', 'debugger.js', 'admin-auth.js',
|
|
355
|
+
'admin-cli.js', 'i18n-helper.js', 'security.js'
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
const traverse = async (currentDir) => {
|
|
359
|
+
try {
|
|
360
|
+
const absoluteDir = path.resolve(currentDir);
|
|
361
|
+
const validatedPath = SecurityUtils.validatePath(absoluteDir, process.cwd());
|
|
362
|
+
|
|
363
|
+
if (!validatedPath || !fs.existsSync(validatedPath)) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const items = fs.readdirSync(validatedPath);
|
|
368
|
+
|
|
369
|
+
for (const item of items) {
|
|
370
|
+
const itemPath = path.join(validatedPath, item);
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
const stat = fs.statSync(itemPath);
|
|
374
|
+
|
|
375
|
+
if (stat.isDirectory()) {
|
|
376
|
+
// Skip excluded directories
|
|
377
|
+
if (!this.config.excludeDirs.includes(item)) {
|
|
378
|
+
await traverse(itemPath);
|
|
379
|
+
}
|
|
380
|
+
} else if (stat.isFile()) {
|
|
381
|
+
// Include files with specified extensions, but exclude toolkit files
|
|
382
|
+
const ext = path.extname(item);
|
|
383
|
+
if (extensions.includes(ext) && !excludeFiles.includes(item)) {
|
|
384
|
+
files.push(itemPath);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
} catch (statError) {
|
|
388
|
+
// Skip files that can't be accessed
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
} catch (error) {
|
|
393
|
+
await SecurityUtils.logSecurityEvent('file_traversal_error', {
|
|
394
|
+
component: 'i18ntk-usage',
|
|
395
|
+
directory: currentDir,
|
|
396
|
+
error: error.message
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
await traverse(dir);
|
|
402
|
+
return files;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async run() {
|
|
406
|
+
try {
|
|
407
|
+
await this.initialize();
|
|
408
|
+
|
|
409
|
+
const args = await this.parseArgs();
|
|
410
|
+
|
|
411
|
+
if (args.help) {
|
|
412
|
+
this.showHelp();
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Override config with command line arguments
|
|
417
|
+
if (args.sourceDir) {
|
|
418
|
+
this.config.sourceDir = args.sourceDir;
|
|
419
|
+
this.sourceDir = path.resolve(args.sourceDir);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (args.i18nDir) {
|
|
423
|
+
this.config.i18nDir = args.i18nDir;
|
|
424
|
+
this.i18nDir = path.resolve(args.i18nDir);
|
|
425
|
+
this.sourceLanguageDir = path.join(this.i18nDir, this.config.sourceLanguage);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
console.log(this.t('checkUsage.source_directory_thissourcedir', { sourceDir: this.sourceDir }));
|
|
429
|
+
console.log(this.t('checkUsage.i18n_directory_thisi18ndir', { i18nDir: this.i18nDir }));
|
|
430
|
+
|
|
431
|
+
// Load available translation keys first
|
|
432
|
+
await this.loadAvailableKeys();
|
|
433
|
+
|
|
434
|
+
// Perform usage analysis
|
|
435
|
+
await this.analyzeUsage();
|
|
436
|
+
|
|
437
|
+
// NEW: Analyze translation completeness
|
|
438
|
+
await this.analyzeTranslationCompleteness();
|
|
439
|
+
|
|
440
|
+
// Generate and display results
|
|
441
|
+
const unusedKeys = this.findUnusedKeys();
|
|
442
|
+
const missingKeys = this.findMissingKeys();
|
|
443
|
+
const notTranslatedStats = this.getNotTranslatedStats();
|
|
444
|
+
|
|
445
|
+
console.log('\n' + this.t('usage.analysisResults'));
|
|
446
|
+
console.log(' ' + this.t('usage.availableKeysCount', { count: this.availableKeys.size }));
|
|
447
|
+
console.log(' ' + this.t('usage.usedKeysCount', { count: this.usedKeys.size }));
|
|
448
|
+
console.log(this.t('usage.unusedKeysCount', { count: unusedKeys.length }));
|
|
449
|
+
console.log(this.t('usage.missingKeysCount', { count: missingKeys.length }));
|
|
450
|
+
console.log(this.t('usage.notTranslatedKeysTotal', { total: notTranslatedStats.total }));
|
|
451
|
+
|
|
452
|
+
// Display translation completeness by language
|
|
453
|
+
console.log(this.t('usage.translationCompletenessTitle'));
|
|
454
|
+
for (const [language, stats] of this.translationStats) {
|
|
455
|
+
const completeness = ((stats.translated / stats.total) * 100).toFixed(1);
|
|
456
|
+
console.log(this.t('usage.languageCompletenessStats', { language, completeness, translated: stats.translated, total: stats.total }));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (args.outputReport) {
|
|
460
|
+
const report = this.generateUsageReport();
|
|
461
|
+
await this.saveReport(report, args.outputDir);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
console.log('\n' + this.t('usage.analysisCompletedSuccessfully'));
|
|
465
|
+
|
|
466
|
+
if (require.main === module) {
|
|
467
|
+
await this.prompt('\nPress Enter to continue...');
|
|
468
|
+
}
|
|
469
|
+
this.closeReadline();
|
|
470
|
+
|
|
471
|
+
} catch (error) {
|
|
472
|
+
console.error(this.t('usage.analysisFailedError'), error.message);
|
|
473
|
+
this.closeReadline();
|
|
474
|
+
await SecurityUtils.logSecurityEvent('usage_analysis_failed', {
|
|
475
|
+
component: 'i18ntk-usage',
|
|
476
|
+
error: error.message
|
|
477
|
+
});
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Show help message
|
|
483
|
+
showHelp() {
|
|
484
|
+
console.log(this.t('checkUsage.help_message'));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// NEW: Enhanced translation key loading with modular support
|
|
488
|
+
async getAllTranslationKeys() {
|
|
489
|
+
const keys = new Set();
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
// Discover all translation files in the i18n directory
|
|
493
|
+
const translationFiles = await this.discoverTranslationFiles(this.i18nDir, this.config.sourceLanguage);
|
|
494
|
+
|
|
495
|
+
console.log(this.t('usage.foundTranslationFiles', { count: translationFiles.length }));
|
|
496
|
+
|
|
497
|
+
for (const fileInfo of translationFiles) {
|
|
498
|
+
try {
|
|
499
|
+
await SecurityUtils.validatePath(fileInfo.filePath);
|
|
500
|
+
const content = await SecurityUtils.safeReadFile(fileInfo.filePath);
|
|
501
|
+
const jsonData = await SecurityUtils.safeParseJSON(content);
|
|
502
|
+
|
|
503
|
+
// Store file info for later analysis
|
|
504
|
+
this.translationFiles.set(fileInfo.filePath, fileInfo);
|
|
505
|
+
|
|
506
|
+
const fileKeys = this.extractKeysFromObject(jsonData, '', fileInfo.namespace);
|
|
507
|
+
fileKeys.forEach(key => keys.add(key));
|
|
508
|
+
|
|
509
|
+
console.log(` 📄 ${fileInfo.namespace}: ${fileKeys.length} keys`);
|
|
510
|
+
} catch (error) {
|
|
511
|
+
console.warn(this.t("checkUsage.failed_to_parse_filename_error", {
|
|
512
|
+
fileName: path.basename(fileInfo.filePath),
|
|
513
|
+
errorMessage: error.message
|
|
514
|
+
}));
|
|
515
|
+
await SecurityUtils.logSecurityEvent('translation_file_parse_error', {
|
|
516
|
+
component: 'i18ntk-usage',
|
|
517
|
+
file: fileInfo.filePath,
|
|
518
|
+
error: error.message
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
} catch (error) {
|
|
523
|
+
await SecurityUtils.logSecurityEvent('translation_keys_load_error', {
|
|
524
|
+
component: 'i18ntk-usage',
|
|
525
|
+
error: error.message
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return keys;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Extract keys recursively from translation object
|
|
533
|
+
extractKeysFromObject(obj, prefix = '', namespace = '') {
|
|
534
|
+
const keys = [];
|
|
535
|
+
|
|
536
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
537
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
538
|
+
|
|
539
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
540
|
+
keys.push(...this.extractKeysFromObject(value, fullKey, namespace));
|
|
541
|
+
} else {
|
|
542
|
+
// Add dot notation key (e.g., "pagination.showing")
|
|
543
|
+
keys.push(fullKey);
|
|
544
|
+
|
|
545
|
+
// If we have a namespace, also add the namespace:key format
|
|
546
|
+
if (namespace && namespace !== 'root') {
|
|
547
|
+
keys.push(`${namespace}:${fullKey}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return keys;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Extract translation keys from source code with enhanced patterns
|
|
556
|
+
extractKeysFromFile(filePath) {
|
|
557
|
+
try {
|
|
558
|
+
const content = SecurityUtils.safeReadFileSync(filePath);
|
|
559
|
+
if (!content) return [];
|
|
560
|
+
|
|
561
|
+
const keys = [];
|
|
562
|
+
|
|
563
|
+
// Ensure patterns are RegExp objects with better error handling
|
|
564
|
+
const patterns = this.config.translationPatterns.map(pattern => {
|
|
565
|
+
try {
|
|
566
|
+
if (typeof pattern === 'string') {
|
|
567
|
+
return new RegExp(pattern, 'g');
|
|
568
|
+
}
|
|
569
|
+
return new RegExp(pattern.source, 'g');
|
|
570
|
+
} catch (patternError) {
|
|
571
|
+
console.warn(`${this.t('usage.invalidPattern')} ${pattern}`);
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
}).filter(Boolean);
|
|
575
|
+
|
|
576
|
+
patterns.forEach(pattern => {
|
|
577
|
+
try {
|
|
578
|
+
let match;
|
|
579
|
+
let matchCount = 0;
|
|
580
|
+
const maxMatches = 10000; // Safety limit to prevent infinite loops
|
|
581
|
+
|
|
582
|
+
// Reset regex lastIndex to ensure clean start
|
|
583
|
+
pattern.lastIndex = 0;
|
|
584
|
+
|
|
585
|
+
while ((match = pattern.exec(content)) !== null && matchCount < maxMatches) {
|
|
586
|
+
if (match && match[1]) {
|
|
587
|
+
keys.push(match[1]);
|
|
588
|
+
}
|
|
589
|
+
matchCount++;
|
|
590
|
+
|
|
591
|
+
// Additional safety: if lastIndex doesn't advance, break to prevent infinite loop
|
|
592
|
+
if (pattern.lastIndex === 0) {
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (matchCount >= maxMatches) {
|
|
598
|
+
console.warn(`${this.t('usage.patternMatchLimitReached')} ${filePath}`);
|
|
599
|
+
}
|
|
600
|
+
} catch (execError) {
|
|
601
|
+
// Skip patterns that fail to execute
|
|
602
|
+
console.warn(`${this.t('usage.patternExecutionFailed')} ${filePath}: ${execError.message}`);
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
return keys;
|
|
607
|
+
} catch (error) {
|
|
608
|
+
console.warn(`${this.t('usage.failedToExtractKeys')} ${filePath}: ${error.message}`);
|
|
609
|
+
return [];
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Analyze usage in source files
|
|
614
|
+
async analyzeUsage() {
|
|
615
|
+
try {
|
|
616
|
+
console.log(this.t('checkUsage.analyzing_source_files'));
|
|
617
|
+
|
|
618
|
+
// Check if source directory exists
|
|
619
|
+
if (!fs.existsSync(this.sourceDir)) {
|
|
620
|
+
throw new Error(`Source directory not found: ${this.sourceDir}`);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const sourceFiles = await this.getAllFiles(this.sourceDir);
|
|
624
|
+
console.log(this.t('checkUsage.found_files_in_source', { numFiles: sourceFiles.length }));
|
|
625
|
+
|
|
626
|
+
// If no files found, exit gracefully
|
|
627
|
+
if (sourceFiles.length === 0) {
|
|
628
|
+
console.warn(t('hardcodedTexts.noSourceFilesFound'));
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
let totalKeysFound = 0;
|
|
633
|
+
let processedFiles = 0;
|
|
634
|
+
|
|
635
|
+
for (const filePath of sourceFiles) {
|
|
636
|
+
try {
|
|
637
|
+
const keys = this.extractKeysFromFile(filePath);
|
|
638
|
+
|
|
639
|
+
if (keys.length > 0) {
|
|
640
|
+
const relativePath = path.relative(this.sourceDir, filePath);
|
|
641
|
+
this.fileUsage.set(relativePath, keys);
|
|
642
|
+
|
|
643
|
+
keys.forEach(key => {
|
|
644
|
+
this.usedKeys.add(key);
|
|
645
|
+
totalKeysFound++;
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
processedFiles++;
|
|
650
|
+
|
|
651
|
+
// Progress indicator for large numbers of files
|
|
652
|
+
if (sourceFiles.length > 10 && processedFiles % Math.ceil(sourceFiles.length / 10) === 0) {
|
|
653
|
+
console.log(t('hardcodedTexts.processedFiles', { processedFiles, totalFiles: sourceFiles.length }));
|
|
654
|
+
}
|
|
655
|
+
} catch (fileError) {
|
|
656
|
+
console.warn(`${this.t('usage.failedToProcessFile')} ${filePath}: ${fileError.message}`);
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
console.log(this.t("checkUsage.found_thisusedkeyssize_unique_", { usedKeysSize: this.usedKeys.size }));
|
|
662
|
+
console.log(this.t("checkUsage.total_key_usages_totalkeysfoun", { totalKeysFound }));
|
|
663
|
+
|
|
664
|
+
} catch (error) {
|
|
665
|
+
console.error(`❌ Failed to analyze usage: ${error.message}`);
|
|
666
|
+
throw error;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Load available translation keys
|
|
671
|
+
async loadAvailableKeys() {
|
|
672
|
+
console.log(this.t("checkUsage.loading_available_translation_"));
|
|
673
|
+
|
|
674
|
+
this.availableKeys = await this.getAllTranslationKeys();
|
|
675
|
+
console.log(this.t("checkUsage.found_thisavailablekeyssize_av", { availableKeysSize: this.availableKeys.size }));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// NEW: Analyze translation completeness across all languages
|
|
679
|
+
async analyzeTranslationCompleteness() {
|
|
680
|
+
try {
|
|
681
|
+
console.log('\n' + t('hardcodedTexts.analyzingTranslationCompleteness'));
|
|
682
|
+
|
|
683
|
+
// Check if i18n directory exists
|
|
684
|
+
if (!fs.existsSync(this.i18nDir)) {
|
|
685
|
+
console.warn(t('hardcodedTexts.i18nDirectoryNotFound', { i18nDir: this.i18nDir }));
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Get all available languages
|
|
690
|
+
const languages = new Set();
|
|
691
|
+
|
|
692
|
+
try {
|
|
693
|
+
// Discover translation files for all languages
|
|
694
|
+
const allLanguageDirs = fs.readdirSync(this.i18nDir)
|
|
695
|
+
.filter(item => {
|
|
696
|
+
try {
|
|
697
|
+
const itemPath = path.join(this.i18nDir, item);
|
|
698
|
+
return fs.existsSync(itemPath) && fs.statSync(itemPath).isDirectory();
|
|
699
|
+
} catch (error) {
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
for (const lang of allLanguageDirs) {
|
|
705
|
+
if (['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(lang)) {
|
|
706
|
+
languages.add(lang);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Also check for direct language files (en.json, de.json, etc.)
|
|
711
|
+
const directFiles = fs.readdirSync(this.i18nDir)
|
|
712
|
+
.filter(file => file.endsWith('.json'))
|
|
713
|
+
.map(file => path.basename(file, '.json'))
|
|
714
|
+
.filter(lang => ['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(lang));
|
|
715
|
+
|
|
716
|
+
directFiles.forEach(lang => languages.add(lang));
|
|
717
|
+
} catch (error) {
|
|
718
|
+
console.warn(`${this.t('usage.errorReadingI18nDirectory')} ${error.message}`);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// If no languages found, exit gracefully
|
|
723
|
+
if (languages.size === 0) {
|
|
724
|
+
console.warn(t('hardcodedTexts.noTranslationLanguagesFound'));
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Analyze each language
|
|
729
|
+
for (const language of languages) {
|
|
730
|
+
try {
|
|
731
|
+
const translationFiles = await this.discoverTranslationFiles(this.i18nDir, language);
|
|
732
|
+
let totalKeys = 0;
|
|
733
|
+
let translatedKeys = 0;
|
|
734
|
+
|
|
735
|
+
for (const fileInfo of translationFiles) {
|
|
736
|
+
try {
|
|
737
|
+
if (!fs.existsSync(fileInfo.filePath)) {
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const content = await SecurityUtils.safeReadFile(fileInfo.filePath);
|
|
742
|
+
const jsonData = await SecurityUtils.safeParseJSON(content);
|
|
743
|
+
|
|
744
|
+
const stats = this.analyzeFileCompleteness(jsonData);
|
|
745
|
+
totalKeys += stats.total;
|
|
746
|
+
translatedKeys += stats.translated;
|
|
747
|
+
} catch (error) {
|
|
748
|
+
console.warn(t('hardcodedTexts.failedToAnalyzeFile', { filePath: fileInfo.filePath, error: error.message }));
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
this.translationStats.set(language, {
|
|
754
|
+
total: totalKeys,
|
|
755
|
+
translated: translatedKeys,
|
|
756
|
+
notTranslated: totalKeys - translatedKeys
|
|
757
|
+
});
|
|
758
|
+
} catch (error) {
|
|
759
|
+
console.warn(t('hardcodedTexts.failedToAnalyzeLanguage', { language, error: error.message }));
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
} catch (error) {
|
|
764
|
+
console.warn(t('hardcodedTexts.translationCompletenessAnalysisFailed', { error: error.message }));
|
|
765
|
+
// Don't throw error, just continue with the rest of the analysis
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// NEW: Analyze completeness of a single translation file
|
|
770
|
+
analyzeFileCompleteness(obj) {
|
|
771
|
+
let total = 0;
|
|
772
|
+
let translated = 0;
|
|
773
|
+
|
|
774
|
+
const traverse = (current) => {
|
|
775
|
+
for (const [key, value] of Object.entries(current)) {
|
|
776
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
777
|
+
traverse(value);
|
|
778
|
+
} else {
|
|
779
|
+
total++;
|
|
780
|
+
if (value !== 'NOT_TRANSLATED' && value !== '(NOT TRANSLATED)' &&
|
|
781
|
+
value !== 'TRANSLATED' && value !== '(TRANSLATED)' &&
|
|
782
|
+
value && value.toString().trim() !== '') {
|
|
783
|
+
translated++;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
traverse(obj);
|
|
790
|
+
return { total, translated };
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// NEW: Get statistics about NOT_TRANSLATED values
|
|
794
|
+
getNotTranslatedStats() {
|
|
795
|
+
let total = 0;
|
|
796
|
+
const byLanguage = new Map();
|
|
797
|
+
|
|
798
|
+
for (const [language, stats] of this.translationStats) {
|
|
799
|
+
const notTranslated = stats.notTranslated;
|
|
800
|
+
total += notTranslated;
|
|
801
|
+
byLanguage.set(language, notTranslated);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
return { total, byLanguage };
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Find unused keys
|
|
808
|
+
findUnusedKeys() {
|
|
809
|
+
const unused = [];
|
|
810
|
+
|
|
811
|
+
for (const key of this.availableKeys) {
|
|
812
|
+
let isUsed = false;
|
|
813
|
+
|
|
814
|
+
// Check exact match
|
|
815
|
+
if (this.usedKeys.has(key)) {
|
|
816
|
+
isUsed = true;
|
|
817
|
+
} else {
|
|
818
|
+
// Check if any dynamic key could match this
|
|
819
|
+
for (const usedKey of this.usedKeys) {
|
|
820
|
+
if (usedKey.endsWith('*')) {
|
|
821
|
+
const prefix = usedKey.slice(0, -1);
|
|
822
|
+
if (key.startsWith(prefix)) {
|
|
823
|
+
isUsed = true;
|
|
824
|
+
break;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (!isUsed) {
|
|
831
|
+
unused.push(key);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
return unused;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Find missing keys (used but not available)
|
|
839
|
+
findMissingKeys() {
|
|
840
|
+
const missing = [];
|
|
841
|
+
|
|
842
|
+
for (const key of this.usedKeys) {
|
|
843
|
+
// Skip dynamic keys for missing check
|
|
844
|
+
if (key.endsWith('*')) {
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (!this.availableKeys.has(key)) {
|
|
849
|
+
missing.push(key);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
return missing;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Find files that use specific keys
|
|
857
|
+
findKeyUsage(searchKey) {
|
|
858
|
+
const usage = [];
|
|
859
|
+
|
|
860
|
+
for (const [filePath, keys] of this.fileUsage) {
|
|
861
|
+
const matchingKeys = keys.filter(key => {
|
|
862
|
+
if (key.endsWith('*')) {
|
|
863
|
+
const prefix = key.slice(0, -1);
|
|
864
|
+
return searchKey.startsWith(prefix);
|
|
865
|
+
}
|
|
866
|
+
return key === searchKey;
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
if (matchingKeys.length > 0) {
|
|
870
|
+
usage.push({ filePath, keys: matchingKeys });
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return usage;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Enhanced usage report generation
|
|
878
|
+
generateUsageReport() {
|
|
879
|
+
const unusedKeys = this.findUnusedKeys();
|
|
880
|
+
const missingKeys = this.findMissingKeys();
|
|
881
|
+
const dynamicKeys = Array.from(this.usedKeys).filter(key => key.endsWith('*'));
|
|
882
|
+
const notTranslatedStats = this.getNotTranslatedStats();
|
|
883
|
+
|
|
884
|
+
const timestamp = new Date().toISOString();
|
|
885
|
+
|
|
886
|
+
let report = `${this.t('summary.usageReportTitle')}\n`;
|
|
887
|
+
report += `${this.t('summary.usageReportGenerated', { timestamp })}\n`;
|
|
888
|
+
report += `${this.t('summary.usageReportSourceDir', { sourceDir: this.sourceDir })}\n`;
|
|
889
|
+
report += `${this.t('summary.usageReportI18nDir', { i18nDir: this.i18nDir })}\n\n`;
|
|
890
|
+
|
|
891
|
+
// Summary
|
|
892
|
+
report += `${this.t('summary.usageReportSummary')}\n`;
|
|
893
|
+
report += `${'='.repeat(50)}\n`;
|
|
894
|
+
report += `${this.t('summary.usageReportSourceFilesScanned', { count: this.fileUsage.size })}\n`;
|
|
895
|
+
report += `${this.t('summary.usageReportTranslationFilesFound', { count: this.translationFiles.size })}\n`;
|
|
896
|
+
report += `${this.t('summary.usageReportAvailableKeys', { count: this.availableKeys.size })}\n`;
|
|
897
|
+
report += `${this.t('summary.usageReportUsedKeys', { count: this.usedKeys.size - dynamicKeys.length })}\n`;
|
|
898
|
+
report += `${this.t('summary.usageReportDynamicKeys', { count: dynamicKeys.length })}\n`;
|
|
899
|
+
report += `${this.t('summary.usageReportUnusedKeys', { count: unusedKeys.length })}\n`;
|
|
900
|
+
report += `${this.t('summary.usageReportMissingKeys', { count: missingKeys.length })}\n`;
|
|
901
|
+
report += `${this.t('summary.usageReportNotTranslatedKeys', { count: notTranslatedStats.total })}\n\n`;
|
|
902
|
+
|
|
903
|
+
// Translation completeness
|
|
904
|
+
report += `${this.t('summary.usageReportTranslationCompleteness')}\n`;
|
|
905
|
+
report += `${'='.repeat(50)}\n`;
|
|
906
|
+
for (const [language, stats] of this.translationStats) {
|
|
907
|
+
const completeness = ((stats.translated / stats.total) * 100).toFixed(1);
|
|
908
|
+
report += `${this.t('summary.usageReportLanguageCompleteness', { language: language.toUpperCase(), completeness, translated: stats.translated, total: stats.total })}\n`;
|
|
909
|
+
if (stats.notTranslated > 0) {
|
|
910
|
+
report += `${this.t('summary.usageReportNotTranslatedInLanguage', { count: stats.notTranslated })}\n`;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
report += `\n`;
|
|
914
|
+
|
|
915
|
+
// Translation files discovered
|
|
916
|
+
report += `${this.t('summary.usageReportTranslationFilesDiscovered')}\n`;
|
|
917
|
+
report += `${'='.repeat(50)}\n`;
|
|
918
|
+
for (const [filePath, fileInfo] of this.translationFiles) {
|
|
919
|
+
const relativePath = path.relative(this.i18nDir, filePath);
|
|
920
|
+
report += `${this.t('summary.usageReportFileInfo', { relativePath, namespace: fileInfo.namespace, type: fileInfo.type })}\n`;
|
|
921
|
+
}
|
|
922
|
+
report += `\n`;
|
|
923
|
+
|
|
924
|
+
// Unused keys
|
|
925
|
+
if (unusedKeys.length > 0) {
|
|
926
|
+
report += `${this.t('summary.usageReportUnusedTranslationKeys')}\n`;
|
|
927
|
+
report += `${'='.repeat(50)}\n`;
|
|
928
|
+
report += `${this.t('summary.usageReportUnusedKeysDescription')}\n\n`;
|
|
929
|
+
|
|
930
|
+
unusedKeys.slice(0, 100).forEach(key => {
|
|
931
|
+
report += `${this.t('summary.usageReportUnusedKey', { key })}\n`;
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
if (unusedKeys.length > 100) {
|
|
935
|
+
report += `${this.t('summary.usageReportMoreUnusedKeys', { count: unusedKeys.length - 100 })}\n`;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
report += `\n`;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Missing keys
|
|
942
|
+
if (missingKeys.length > 0) {
|
|
943
|
+
report += `${this.t('summary.usageReportMissingTranslationKeys')}\n`;
|
|
944
|
+
report += `${'='.repeat(50)}\n`;
|
|
945
|
+
report += `${this.t('summary.usageReportMissingKeysDescription')}\n\n`;
|
|
946
|
+
|
|
947
|
+
missingKeys.forEach(key => {
|
|
948
|
+
report += `${this.t('summary.usageReportMissingKey', { key })}\n`;
|
|
949
|
+
|
|
950
|
+
// Show where it's used
|
|
951
|
+
const usage = this.findKeyUsage(key);
|
|
952
|
+
usage.slice(0, 3).forEach(({ filePath }) => {
|
|
953
|
+
report += ` ${this.t('summary.usageReportUsedIn', { filePath })}\n`;
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
if (usage.length > 3) {
|
|
957
|
+
report += ` ${this.t('summary.usageReportMoreFiles', { count: usage.length - 3 })}\n`;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
report += `\n`;
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Dynamic keys
|
|
965
|
+
if (dynamicKeys.length > 0) {
|
|
966
|
+
report += `${this.t('summary.usageReportDynamicTranslationKeys')}\n`;
|
|
967
|
+
report += `${'='.repeat(50)}\n`;
|
|
968
|
+
report += `${this.t('summary.usageReportDynamicKeysDescription')}\n\n`;
|
|
969
|
+
|
|
970
|
+
dynamicKeys.forEach(key => {
|
|
971
|
+
report += `${this.t('summary.usageReportDynamicKey', { key })}\n`;
|
|
972
|
+
|
|
973
|
+
// Show where it's used
|
|
974
|
+
const usage = this.findKeyUsage(key);
|
|
975
|
+
usage.slice(0, 2).forEach(({ filePath }) => {
|
|
976
|
+
report += ` ${this.t('summary.usageReportUsedIn', { filePath })}\n`;
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
report += `\n`;
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// File usage breakdown
|
|
984
|
+
report += `${this.t('summary.usageReportFileUsageBreakdown')}\n`;
|
|
985
|
+
report += `${'='.repeat(50)}\n`;
|
|
986
|
+
|
|
987
|
+
const sortedFiles = Array.from(this.fileUsage.entries())
|
|
988
|
+
.sort(([,a], [,b]) => b.length - a.length)
|
|
989
|
+
.slice(0, 20);
|
|
990
|
+
|
|
991
|
+
sortedFiles.forEach(([filePath, keys]) => {
|
|
992
|
+
report += `${this.t('summary.usageReportFileUsage', { filePath, count: keys.length })}\n`;
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
if (this.fileUsage.size > 20) {
|
|
996
|
+
report += `${this.t('summary.usageReportMoreFiles', { count: this.fileUsage.size - 20 })}\n`;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
return report;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Save report to file
|
|
1003
|
+
async saveReport(report, outputDir = './i18ntk-reports/usage') {
|
|
1004
|
+
try {
|
|
1005
|
+
// Ensure output directory exists
|
|
1006
|
+
if (!fs.existsSync(outputDir)) {
|
|
1007
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1011
|
+
const filename = `usage-analysis-${timestamp}.txt`;
|
|
1012
|
+
const filepath = path.join(outputDir, filename);
|
|
1013
|
+
|
|
1014
|
+
await SecurityUtils.safeWriteFile(filepath, report);
|
|
1015
|
+
console.log(this.t('usage.reportSavedTo', { reportPath: filepath }));
|
|
1016
|
+
return filepath;
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
console.error(this.t('usage.failedToSaveReport', { error: error.message }));
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Main analysis process
|
|
1023
|
+
async analyze() {
|
|
1024
|
+
try {
|
|
1025
|
+
// Initialize if not already done
|
|
1026
|
+
if (!this.sourceDir || !this.t) {
|
|
1027
|
+
await this.initialize();
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
await SecurityUtils.logSecurityEvent('analysis_started', { component: 'i18ntk-usage' });
|
|
1031
|
+
|
|
1032
|
+
console.log(this.t('checkUsage.title'));
|
|
1033
|
+
console.log(this.t("checkUsage.message"));
|
|
1034
|
+
|
|
1035
|
+
// Parse command line arguments
|
|
1036
|
+
const args = await this.parseArgs();
|
|
1037
|
+
|
|
1038
|
+
// Show help if requested
|
|
1039
|
+
if (args.help) {
|
|
1040
|
+
this.showHelp();
|
|
1041
|
+
return { success: true, help: true };
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
if (args.sourceDir) {
|
|
1045
|
+
this.config.sourceDir = args.sourceDir;
|
|
1046
|
+
this.sourceDir = path.resolve(this.config.sourceDir);
|
|
1047
|
+
}
|
|
1048
|
+
if (args.i18nDir) {
|
|
1049
|
+
this.config.i18nDir = args.i18nDir;
|
|
1050
|
+
this.i18nDir = path.resolve(this.config.i18nDir);
|
|
1051
|
+
this.sourceLanguageDir = path.join(this.i18nDir, this.config.sourceLanguage);
|
|
1052
|
+
}
|
|
1053
|
+
if (args.outputDir) {
|
|
1054
|
+
this.config.outputDir = args.outputDir;
|
|
1055
|
+
this.outputDir = path.resolve(this.config.outputDir);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
console.log(this.t("checkUsage.source_directory_thissourcedir", { sourceDir: this.sourceDir }));
|
|
1059
|
+
console.log(this.t("checkUsage.i18n_directory_thisi18ndir", { i18nDir: this.i18nDir }));
|
|
1060
|
+
|
|
1061
|
+
// Validate directories
|
|
1062
|
+
await SecurityUtils.validatePath(this.sourceDir);
|
|
1063
|
+
await SecurityUtils.validatePath(this.i18nDir);
|
|
1064
|
+
|
|
1065
|
+
if (!fs.existsSync(this.sourceDir)) {
|
|
1066
|
+
throw new Error(`Source directory not found: ${this.sourceDir}`);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (!fs.existsSync(this.i18nDir)) {
|
|
1070
|
+
throw new Error(`I18n directory not found: ${this.i18nDir}`);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Load available keys
|
|
1074
|
+
await this.loadAvailableKeys();
|
|
1075
|
+
|
|
1076
|
+
// Analyze usage
|
|
1077
|
+
await this.analyzeUsage();
|
|
1078
|
+
|
|
1079
|
+
// NEW: Analyze translation completeness
|
|
1080
|
+
await this.analyzeTranslationCompleteness();
|
|
1081
|
+
|
|
1082
|
+
// Generate analysis results
|
|
1083
|
+
const unusedKeys = this.findUnusedKeys();
|
|
1084
|
+
const missingKeys = this.findMissingKeys();
|
|
1085
|
+
const dynamicKeys = Array.from(this.usedKeys).filter(key => key.endsWith('*'));
|
|
1086
|
+
const notTranslatedStats = this.getNotTranslatedStats();
|
|
1087
|
+
|
|
1088
|
+
// Display results
|
|
1089
|
+
console.log(this.t("checkUsage.n"));
|
|
1090
|
+
console.log(this.t("checkUsage.usage_analysis_results"));
|
|
1091
|
+
console.log(this.t("checkUsage.message"));
|
|
1092
|
+
|
|
1093
|
+
console.log(this.t("checkUsage.source_files_scanned_thisfileu", { fileUsageSize: this.fileUsage.size }));
|
|
1094
|
+
console.log(this.t("checkUsage.available_translation_keys_thi", { availableKeysSize: this.availableKeys.size }));
|
|
1095
|
+
console.log(this.t("checkUsage.used_translation_keys_thisused", { usedKeysSize: this.usedKeys.size - dynamicKeys.length }));
|
|
1096
|
+
console.log(this.t("checkUsage.dynamic_keys_detected_dynamick", { dynamicKeysLength: dynamicKeys.length }));
|
|
1097
|
+
console.log(this.t("checkUsage.unused_keys_unusedkeyslength", { unusedKeysLength: unusedKeys.length }));
|
|
1098
|
+
console.log(this.t("checkUsage.missing_keys_missingkeyslength", { missingKeysLength: missingKeys.length }));
|
|
1099
|
+
console.log(this.t('usage.notTranslatedKeysTotal', { total: notTranslatedStats.total }));
|
|
1100
|
+
|
|
1101
|
+
// Removed redundant hardcoded console output to avoid duplication
|
|
1102
|
+
// The translation completeness and not translated keys count are already logged below
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
// Display translation completeness
|
|
1106
|
+
console.log(this.t("checkUsage.translation_completeness_title"));
|
|
1107
|
+
for (const [language, stats] of this.translationStats) {
|
|
1108
|
+
const completeness = ((stats.translated / stats.total) * 100).toFixed(1);
|
|
1109
|
+
console.log(this.t("checkUsage.language_completeness_stats", {
|
|
1110
|
+
language: language.toUpperCase(),
|
|
1111
|
+
completeness,
|
|
1112
|
+
translated: stats.translated,
|
|
1113
|
+
total: stats.total
|
|
1114
|
+
}));
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Show some examples
|
|
1118
|
+
if (unusedKeys.length > 0) {
|
|
1119
|
+
console.log(this.t("checkUsage.n_sample_unused_keys"));
|
|
1120
|
+
unusedKeys.slice(0, 5).forEach(key => {
|
|
1121
|
+
console.log(this.t("checkUsage.key", { key }));
|
|
1122
|
+
});
|
|
1123
|
+
if (unusedKeys.length > 5) {
|
|
1124
|
+
console.log(this.t("checkUsage.and_unusedkeyslength_5_more", { count: unusedKeys.length - 5 }));
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
if (missingKeys.length > 0) {
|
|
1129
|
+
console.log(this.t("checkUsage.n_sample_missing_keys"));
|
|
1130
|
+
missingKeys.slice(0, 5).forEach(key => {
|
|
1131
|
+
console.log(this.t("checkUsage.key", { key }));
|
|
1132
|
+
});
|
|
1133
|
+
if (missingKeys.length > 5) {
|
|
1134
|
+
console.log(this.t("checkUsage.and_missingkeyslength_5_more", { count: missingKeys.length - 5 }));
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// Generate and save report if requested
|
|
1139
|
+
if (args.outputReport) {
|
|
1140
|
+
console.log(this.t("checkUsage.n_generating_detailed_report"));
|
|
1141
|
+
const report = this.generateUsageReport();
|
|
1142
|
+
const reportPath = await this.saveReport(report);
|
|
1143
|
+
console.log(this.t("checkUsage.report_saved_reportpath", { reportPath }));
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// Recommendations
|
|
1147
|
+
console.log(this.t("checkUsage.n_recommendations"));
|
|
1148
|
+
console.log(this.t("checkUsage.message"));
|
|
1149
|
+
|
|
1150
|
+
if (unusedKeys.length > 0) {
|
|
1151
|
+
console.log(this.t("checkUsage.consider_removing_unused_trans"));
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
if (missingKeys.length > 0) {
|
|
1155
|
+
console.log(this.t("checkUsage.add_missing_translation_keys_t"));
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
if (dynamicKeys.length > 0) {
|
|
1159
|
+
console.log(this.t("checkUsage.review_dynamic_keys_manually_t"));
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
if (notTranslatedStats.total > 0) {
|
|
1163
|
+
console.log(this.t('usage.reviewNotTranslatedKeys', { total: notTranslatedStats.total }));
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
if (unusedKeys.length === 0 && missingKeys.length === 0 && notTranslatedStats.total === 0) {
|
|
1167
|
+
console.log(this.t("checkUsage.all_translation_keys_are_prope"));
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
console.log(this.t("checkUsage.n_next_steps"));
|
|
1171
|
+
console.log(this.t("checkUsage.1_review_the_analysis_results"));
|
|
1172
|
+
if (args.outputReport) {
|
|
1173
|
+
console.log(this.t("checkUsage.2_check_the_detailed_report_fo"));
|
|
1174
|
+
} else {
|
|
1175
|
+
console.log(this.t("checkUsage.2_run_with_outputreport_for_de"));
|
|
1176
|
+
}
|
|
1177
|
+
console.log(this.t("checkUsage.3_remove_unused_keys_or_add_mi"));
|
|
1178
|
+
console.log(this.t("checkUsage.4_rerun_analysis_to_verify_imp"));
|
|
1179
|
+
|
|
1180
|
+
await SecurityUtils.logSecurityEvent('analysis_completed', {
|
|
1181
|
+
component: 'i18ntk-usage',
|
|
1182
|
+
stats: {
|
|
1183
|
+
availableKeys: this.availableKeys.size,
|
|
1184
|
+
usedKeys: this.usedKeys.size - dynamicKeys.length,
|
|
1185
|
+
dynamicKeys: dynamicKeys.length,
|
|
1186
|
+
unusedKeys: unusedKeys.length,
|
|
1187
|
+
missingKeys: missingKeys.length,
|
|
1188
|
+
filesScanned: this.fileUsage.size,
|
|
1189
|
+
notTranslatedKeys: notTranslatedStats.total
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
// Close readline interface to prevent hanging
|
|
1194
|
+
this.closeReadline();
|
|
1195
|
+
|
|
1196
|
+
return {
|
|
1197
|
+
success: true,
|
|
1198
|
+
stats: {
|
|
1199
|
+
availableKeys: this.availableKeys.size,
|
|
1200
|
+
usedKeys: this.usedKeys.size - dynamicKeys.length,
|
|
1201
|
+
dynamicKeys: dynamicKeys.length,
|
|
1202
|
+
unusedKeys: unusedKeys.length,
|
|
1203
|
+
missingKeys: missingKeys.length,
|
|
1204
|
+
filesScanned: this.fileUsage.size,
|
|
1205
|
+
notTranslatedKeys: notTranslatedStats.total,
|
|
1206
|
+
translationCompleteness: Object.fromEntries(this.translationStats)
|
|
1207
|
+
},
|
|
1208
|
+
unusedKeys,
|
|
1209
|
+
missingKeys,
|
|
1210
|
+
dynamicKeys,
|
|
1211
|
+
notTranslatedStats
|
|
1212
|
+
};
|
|
1213
|
+
|
|
1214
|
+
} catch (error) {
|
|
1215
|
+
console.error(this.t("checkUsage.usage_analysis_failed"));
|
|
1216
|
+
console.error(error.message);
|
|
1217
|
+
|
|
1218
|
+
await SecurityUtils.logSecurityEvent('analysis_failed', {
|
|
1219
|
+
component: 'i18ntk-usage',
|
|
1220
|
+
error: error.message
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
// Close readline interface to prevent hanging
|
|
1224
|
+
this.closeReadline();
|
|
1225
|
+
|
|
1226
|
+
return {
|
|
1227
|
+
success: false,
|
|
1228
|
+
error: error.message
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Run if called directly
|
|
1235
|
+
if (require.main === module) {
|
|
1236
|
+
const analyzer = new I18nUsageAnalyzer();
|
|
1237
|
+
|
|
1238
|
+
// Check if we're being called from the menu system (stdin has data)
|
|
1239
|
+
// In that case, we should run with default settings without prompting
|
|
1240
|
+
const hasStdinData = !process.stdin.isTTY;
|
|
1241
|
+
|
|
1242
|
+
if (hasStdinData) {
|
|
1243
|
+
// When called from menu, consume stdin data and run with defaults
|
|
1244
|
+
process.stdin.resume();
|
|
1245
|
+
process.stdin.on('data', () => {});
|
|
1246
|
+
process.stdin.on('end', () => {
|
|
1247
|
+
// Run analysis with default settings (no prompts)
|
|
1248
|
+
analyzer.analyze()
|
|
1249
|
+
.then((result) => {
|
|
1250
|
+
if (result.success) {
|
|
1251
|
+
console.log(analyzer.t('usage.analysisCompletedSuccessfully'));
|
|
1252
|
+
process.exit(0);
|
|
1253
|
+
} else {
|
|
1254
|
+
console.error(analyzer.t('usage.analysisFailed', { error: result.error }));
|
|
1255
|
+
process.exit(1);
|
|
1256
|
+
}
|
|
1257
|
+
})
|
|
1258
|
+
.catch((error) => {
|
|
1259
|
+
console.error(analyzer.t('usage.analysisFailed', { error: error.message }));
|
|
1260
|
+
process.exit(1);
|
|
1261
|
+
});
|
|
1262
|
+
});
|
|
1263
|
+
} else {
|
|
1264
|
+
// Normal direct execution
|
|
1265
|
+
analyzer.analyze()
|
|
1266
|
+
.then((result) => {
|
|
1267
|
+
if (result.success) {
|
|
1268
|
+
console.log('\n' + analyzer.t('usage.analysisCompletedSuccessfully'));
|
|
1269
|
+
process.exit(0);
|
|
1270
|
+
} else {
|
|
1271
|
+
console.error('\n' + analyzer.t('usage.analysisFailed', { error: result.error }));
|
|
1272
|
+
process.exit(1);
|
|
1273
|
+
}
|
|
1274
|
+
})
|
|
1275
|
+
.catch((error) => {
|
|
1276
|
+
console.error('\n' + analyzer.t('usage.analysisFailed', { error: error.message }));
|
|
1277
|
+
process.exit(1);
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
module.exports = I18nUsageAnalyzer;
|