analyze-codebase 1.2.2 → 1.3.1

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.
@@ -0,0 +1,477 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.analyzeAndRemoveUnusedKeys = exports.findUnusedKeys = exports.flattenKeys = void 0;
30
+ const fs = __importStar(require("fs/promises"));
31
+ const path = __importStar(require("path"));
32
+ const chalk_1 = __importDefault(require("chalk"));
33
+ const fast_glob_1 = __importDefault(require("fast-glob"));
34
+ const readline = __importStar(require("readline"));
35
+ const progress_1 = require("../../utils/progress");
36
+ const spinner_1 = require("../../utils/spinner");
37
+ const cancellation_1 = require("../../utils/cancellation");
38
+ /**
39
+ * Flattens a nested JSON object into dot-notation paths
40
+ */
41
+ const flattenKeys = (obj, prefix = "", originalPath = []) => {
42
+ const keys = [];
43
+ for (const key in obj) {
44
+ if (obj.hasOwnProperty(key)) {
45
+ const currentPath = prefix ? `${prefix}.${key}` : key;
46
+ const currentOriginalPath = [...originalPath, key];
47
+ const value = obj[key];
48
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
49
+ // Recursively flatten nested objects
50
+ keys.push(...(0, exports.flattenKeys)(value, currentPath, currentOriginalPath));
51
+ }
52
+ else {
53
+ // Leaf node - this is a translation key
54
+ keys.push({
55
+ path: currentPath,
56
+ value,
57
+ originalPath: currentOriginalPath,
58
+ });
59
+ }
60
+ }
61
+ }
62
+ return keys;
63
+ };
64
+ exports.flattenKeys = flattenKeys;
65
+ /**
66
+ * Checks if a key is a parent of another key
67
+ * e.g., "a.b" is a parent of "a.b.c"
68
+ */
69
+ const isParentKey = (parentKey, childKey) => {
70
+ if (!childKey.startsWith(parentKey))
71
+ return false;
72
+ // Check if the next character is a dot (to ensure it's a proper parent)
73
+ return childKey.length === parentKey.length || childKey[parentKey.length] === '.';
74
+ };
75
+ /**
76
+ * Searches for a translation key in file content
77
+ * Handles various i18n usage patterns:
78
+ * - t('key')
79
+ * - t("key")
80
+ * - t(`key`)
81
+ * - t(`a.b.${variable}`) - Dynamic keys with template literals
82
+ * - t('a.b.' + variable) - Dynamic keys with string concatenation
83
+ * - i18n.t('key')
84
+ * - $t('key')
85
+ * - {t('key')}
86
+ * - translate('key')
87
+ */
88
+ const searchKeyInContent = (content, key, allKeys) => {
89
+ // Escape special regex characters in the key
90
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
91
+ // Patterns to match static i18n usage:
92
+ // - t('key'), t("key"), t(`key`)
93
+ // - i18n.t('key')
94
+ // - $t('key')
95
+ // - translate('key')
96
+ // - ['key'], ["key"], [`key`] (for array access)
97
+ // - .key (for object access)
98
+ const staticPatterns = [
99
+ // Function calls: t('key'), i18n.t('key'), $t('key'), translate('key')
100
+ new RegExp(`(?:t|i18n\\.t|\\$t|translate)\\s*\\(\\s*['"\`]${escapedKey}['"\`]`, "g"),
101
+ // Array access: ['key'], ["key"], [`key`]
102
+ new RegExp(`\\[\\s*['"\`]${escapedKey}['"\`]\\s*\\]`, "g"),
103
+ // Object access: .key (but not as part of another key)
104
+ new RegExp(`\\.${escapedKey}(?:\\s|;|,|\\)|\\]|\\}|\\n|$)`, "g"),
105
+ // String contains the full key path
106
+ new RegExp(`['"\`]${escapedKey}['"\`]`, "g"),
107
+ ];
108
+ // Check for static matches first
109
+ if (staticPatterns.some((pattern) => pattern.test(content))) {
110
+ return { found: true, isDynamic: false };
111
+ }
112
+ // Check for dynamic patterns (template literals with interpolation)
113
+ // Pattern: t(`a.b.${variable}`) or t(`a.b.${var1}.${var2}`)
114
+ // We need to check if the key is a parent of any dynamically constructed key
115
+ const keyParts = key.split('.');
116
+ // Build patterns for dynamic key construction
117
+ // Match: t(`prefix.${variable}`) where prefix matches our key or is a parent
118
+ for (let i = 0; i < keyParts.length; i++) {
119
+ const partialKey = keyParts.slice(0, i + 1).join('.');
120
+ const escapedPartialKey = partialKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
121
+ // Pattern for template literals: t(`a.b.${...}`)
122
+ // Escape backticks properly in regex
123
+ const templateLiteralPattern = new RegExp("(?:t|i18n\\.t|\\$t|translate)\\s*\\(\\s*`[^`]*" + escapedPartialKey + "[^`]*\\$\\{[^}]+\\}[^`]*`", "g");
124
+ // Pattern for string concatenation: t('a.b.' + variable) or t("a.b." + variable)
125
+ const concatPattern1 = new RegExp(`(?:t|i18n\\.t|\\$t|translate)\\s*\\(\\s*['"\`]${escapedPartialKey}\\.?['"\`]\\s*\\+`, "g");
126
+ // Pattern for reverse concatenation: t(variable + '.a.b')
127
+ const concatPattern2 = new RegExp(`(?:t|i18n\\.t|\\$t|translate)\\s*\\([^)]*\\+\\s*['"\`]\\.?${escapedPartialKey}['"\`]`, "g");
128
+ if (templateLiteralPattern.test(content) ||
129
+ concatPattern1.test(content) ||
130
+ concatPattern2.test(content)) {
131
+ // If we found a dynamic pattern that matches this key or its parent,
132
+ // mark this key and all its children as potentially used
133
+ return { found: true, isDynamic: true };
134
+ }
135
+ }
136
+ // Check if this key is a child of any dynamically used parent key
137
+ // This handles cases where we have t(`a.b.${var}`) and need to mark a.b.c, a.b.d, etc. as used
138
+ // Check all possible parent keys of the current key
139
+ const currentKeyParts = key.split('.');
140
+ for (let i = 1; i < currentKeyParts.length; i++) {
141
+ const parentKey = currentKeyParts.slice(0, i).join('.');
142
+ const escapedParentKey = parentKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
143
+ // Check if parent key is used in dynamic patterns
144
+ const dynamicPattern = new RegExp("(?:t|i18n\\.t|\\$t|translate)\\s*\\(\\s*`[^`]*" + escapedParentKey + "[^`]*\\$\\{[^}]+\\}", "g");
145
+ const concatPattern = new RegExp("(?:t|i18n\\.t|\\$t|translate)\\s*\\(\\s*['\"`]" + escapedParentKey + "\\.?['\"`]\\s*\\+", "g");
146
+ if (dynamicPattern.test(content) || concatPattern.test(content)) {
147
+ return { found: true, isDynamic: true };
148
+ }
149
+ }
150
+ return { found: false, isDynamic: false };
151
+ };
152
+ /**
153
+ * Collects all files to analyze
154
+ */
155
+ const collectFiles = async (directory, extensions, exclude) => {
156
+ const patterns = [];
157
+ const ignorePatterns = [
158
+ "**/node_modules/**",
159
+ "**/dist/**",
160
+ "**/build/**",
161
+ "**/coverage/**",
162
+ "**/.git/**",
163
+ "**/.next/**",
164
+ "**/public/**",
165
+ "**/test/**",
166
+ "**/tests/**",
167
+ "**/mocks/**",
168
+ ];
169
+ if (exclude) {
170
+ ignorePatterns.push(...exclude.map((ex) => `**/${ex}/**`));
171
+ }
172
+ const fileExtensions = extensions || [".ts", ".tsx", ".js", ".jsx", ".vue"];
173
+ patterns.push(...fileExtensions.map((ext) => `**/*${ext}`));
174
+ const files = await (0, fast_glob_1.default)(patterns, {
175
+ cwd: directory,
176
+ ignore: ignorePatterns,
177
+ absolute: true,
178
+ onlyFiles: true,
179
+ dot: false,
180
+ });
181
+ return files;
182
+ };
183
+ /**
184
+ * Searches the codebase for usage of translation keys
185
+ */
186
+ const findUnusedKeys = async (options) => {
187
+ const { directory, i18nFile, extensions, exclude, maxConcurrency: providedConcurrency } = options;
188
+ const cancellationToken = new cancellation_1.CancellationToken();
189
+ // Read and parse the i18n file
190
+ const i18nFilePath = path.isAbsolute(i18nFile)
191
+ ? i18nFile
192
+ : path.join(directory, i18nFile);
193
+ const spinner = (0, spinner_1.createSpinner)("📖 Loading translation file...");
194
+ // Cleanup on cancellation
195
+ cancellationToken.onCancel(() => {
196
+ spinner.stop();
197
+ });
198
+ let i18nContent;
199
+ try {
200
+ const fileContent = await fs.readFile(i18nFilePath, "utf-8");
201
+ i18nContent = JSON.parse(fileContent);
202
+ spinner.succeed("✅ Translation file loaded");
203
+ }
204
+ catch (error) {
205
+ spinner.fail(`❌ Failed to read translation file`);
206
+ throw new Error(`Failed to read or parse i18n file: ${i18nFilePath}. ${error}`);
207
+ }
208
+ // Flatten all keys
209
+ const allKeys = (0, exports.flattenKeys)(i18nContent);
210
+ // Show key count with animation
211
+ const keySpinner = (0, spinner_1.createSpinner)(`🔍 Found ${chalk_1.default.cyan(allKeys.length)} translation keys to check...`);
212
+ await new Promise((resolve) => setTimeout(resolve, 500)); // Brief animation
213
+ keySpinner.succeed(`✅ Found ${chalk_1.default.cyan(allKeys.length)} translation keys to check`);
214
+ // Collect files
215
+ const fileSpinner = (0, spinner_1.createSpinner)("📁 Discovering files...");
216
+ const files = await collectFiles(directory, extensions, exclude);
217
+ fileSpinner.succeed(`✅ Found ${chalk_1.default.cyan(files.length)} files to analyze`);
218
+ // Track which keys are used
219
+ const usedKeys = new Set();
220
+ const dynamicKeys = new Set(); // Keys that are used dynamically
221
+ const keyUsageMap = new Map(); // key -> array of file paths where it's used
222
+ // Progress bar for file processing (primary indicator)
223
+ const fileProgressBar = new progress_1.ProgressBar({
224
+ total: files.length,
225
+ format: `${chalk_1.default.blue("{bar}")} | ${chalk_1.default.yellow("{percentage}%")} | ${chalk_1.default.white("Processing files: {value}/{total}")}`,
226
+ });
227
+ // Cleanup progress bar on cancellation
228
+ cancellationToken.onCancel(() => {
229
+ fileProgressBar.stop();
230
+ console.log(chalk_1.default.yellow("\n\n⚠️ Cancellation requested. Cleaning up...\n"));
231
+ });
232
+ // Track progress
233
+ let processedFiles = 0;
234
+ let totalKeyChecks = 0;
235
+ const keysCheckedPerFile = new Map();
236
+ // Process files in parallel batches with optimized concurrency
237
+ // For file I/O operations, optimal concurrency is lower to avoid I/O contention
238
+ // Too high concurrency can actually slow things down due to disk I/O bottlenecks
239
+ const fileCount = files.length;
240
+ // Optimal concurrency for file I/O: 20-30 is usually the sweet spot
241
+ // Higher concurrency doesn't help much with disk I/O and can cause contention
242
+ const defaultConcurrency = fileCount > 500 ? 30 : fileCount > 100 ? 25 : 20;
243
+ const maxConcurrency = providedConcurrency || defaultConcurrency;
244
+ // Show key checking progress as we process files
245
+ console.log(chalk_1.default.cyan(`\n🔍 Checking ${chalk_1.default.bold(allKeys.length)} translation keys across ${chalk_1.default.bold(files.length)} files...\n` +
246
+ chalk_1.default.gray(` Using ${maxConcurrency} concurrent file readers (optimal for I/O performance)\n`)));
247
+ // Pre-compile regex patterns for all keys to avoid recompiling
248
+ const keyPatterns = new Map();
249
+ const dynamicPatterns = new Map();
250
+ for (const keyData of allKeys) {
251
+ const escapedKey = keyData.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
252
+ const keyParts = keyData.path.split('.');
253
+ // Static patterns
254
+ const staticPatterns = [
255
+ new RegExp(`(?:t|i18n\\.t|\\$t|translate)\\s*\\(\\s*['"\`]${escapedKey}['"\`]`, "g"),
256
+ new RegExp(`\\[\\s*['"\`]${escapedKey}['"\`]\\s*\\]`, "g"),
257
+ new RegExp(`\\.${escapedKey}(?:\\s|;|,|\\)|\\]|\\}|\\n|$)`, "g"),
258
+ new RegExp(`['"\`]${escapedKey}['"\`]`, "g"),
259
+ ];
260
+ keyPatterns.set(keyData.path, staticPatterns);
261
+ // Dynamic patterns for parent keys
262
+ const dynPatterns = [];
263
+ for (let i = 0; i < keyParts.length; i++) {
264
+ const partialKey = keyParts.slice(0, i + 1).join('.');
265
+ const escapedPartialKey = partialKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
266
+ dynPatterns.push(new RegExp("(?:t|i18n\\.t|\\$t|translate)\\s*\\(\\s*`[^`]*" + escapedPartialKey + "[^`]*\\$\\{[^}]+\\}[^`]*`", "g"), new RegExp("(?:t|i18n\\.t|\\$t|translate)\\s*\\(\\s*['\"`]" + escapedPartialKey + "\\.?['\"`]\\s*\\+", "g"), new RegExp("(?:t|i18n\\.t|\\$t|translate)\\s*\\([^)]*\\+\\s*['\"`]\\.?" + escapedPartialKey + "['\"`]", "g"));
267
+ }
268
+ dynamicPatterns.set(keyData.path, dynPatterns);
269
+ }
270
+ // Optimized: Process files in large batches with optimal concurrency
271
+ const processFile = async (filePath) => {
272
+ // Check cancellation before processing
273
+ if (cancellationToken.isCancelled()) {
274
+ throw new Error("Operation cancelled by user");
275
+ }
276
+ try {
277
+ const content = await fs.readFile(filePath, "utf-8");
278
+ // Optimized: Check all unused keys in a single pass
279
+ // Only check keys that haven't been marked as used yet
280
+ const keysToCheck = allKeys.filter((key) => !usedKeys.has(key.path));
281
+ // Check cancellation periodically
282
+ if (cancellationToken.isCancelled()) {
283
+ throw new Error("Operation cancelled by user");
284
+ }
285
+ // Process keys in smaller batches to allow cancellation checks
286
+ const keyBatchSize = 100; // Check 100 keys at a time
287
+ for (let i = 0; i < keysToCheck.length; i += keyBatchSize) {
288
+ if (cancellationToken.isCancelled()) {
289
+ throw new Error("Operation cancelled by user");
290
+ }
291
+ const keyBatch = keysToCheck.slice(i, i + keyBatchSize);
292
+ for (const keyData of keyBatch) {
293
+ totalKeyChecks++;
294
+ // Check static patterns first (faster)
295
+ const staticPats = keyPatterns.get(keyData.path);
296
+ if (staticPats.some((pattern) => pattern.test(content))) {
297
+ usedKeys.add(keyData.path);
298
+ if (!keyUsageMap.has(keyData.path)) {
299
+ keyUsageMap.set(keyData.path, []);
300
+ }
301
+ keyUsageMap.get(keyData.path).push(filePath);
302
+ continue;
303
+ }
304
+ // Check dynamic patterns
305
+ const dynPats = dynamicPatterns.get(keyData.path);
306
+ if (dynPats.some((pattern) => pattern.test(content))) {
307
+ usedKeys.add(keyData.path);
308
+ dynamicKeys.add(keyData.path);
309
+ // If this is a parent key used dynamically, mark all children as potentially used
310
+ for (const otherKey of allKeys) {
311
+ if (isParentKey(keyData.path, otherKey.path)) {
312
+ usedKeys.add(otherKey.path);
313
+ dynamicKeys.add(otherKey.path);
314
+ }
315
+ }
316
+ if (!keyUsageMap.has(keyData.path)) {
317
+ keyUsageMap.set(keyData.path, []);
318
+ }
319
+ keyUsageMap.get(keyData.path).push(filePath);
320
+ }
321
+ }
322
+ }
323
+ }
324
+ catch (error) {
325
+ // Re-throw cancellation errors
326
+ if ((error === null || error === void 0 ? void 0 : error.message) === "Operation cancelled by user") {
327
+ throw error;
328
+ }
329
+ // Skip files that can't be read
330
+ // Silently skip to avoid cluttering output
331
+ }
332
+ if (!cancellationToken.isCancelled()) {
333
+ processedFiles++;
334
+ fileProgressBar.update(1);
335
+ }
336
+ };
337
+ // Process files in parallel batches
338
+ const batches = [];
339
+ for (let i = 0; i < files.length; i += maxConcurrency) {
340
+ batches.push(files.slice(i, i + maxConcurrency));
341
+ }
342
+ // Process all batches with cancellation support
343
+ try {
344
+ for (const batch of batches) {
345
+ // Check cancellation before each batch
346
+ if (cancellationToken.isCancelled()) {
347
+ fileProgressBar.stop();
348
+ throw new Error("Operation cancelled by user");
349
+ }
350
+ // Use Promise.allSettled to handle cancellation better
351
+ const results = await Promise.allSettled(batch.map(async (filePath) => {
352
+ if (cancellationToken.isCancelled()) {
353
+ throw new Error("Operation cancelled by user");
354
+ }
355
+ return processFile(filePath);
356
+ }));
357
+ // Check if any promise was cancelled
358
+ const cancelled = results.some((result) => {
359
+ var _a;
360
+ return result.status === "rejected" &&
361
+ ((_a = result.reason) === null || _a === void 0 ? void 0 : _a.message) === "Operation cancelled by user";
362
+ });
363
+ if (cancelled || cancellationToken.isCancelled()) {
364
+ fileProgressBar.stop();
365
+ throw new Error("Operation cancelled by user");
366
+ }
367
+ }
368
+ }
369
+ catch (error) {
370
+ fileProgressBar.stop();
371
+ if ((error === null || error === void 0 ? void 0 : error.message) === "Operation cancelled by user" || cancellationToken.isCancelled()) {
372
+ throw new Error("Operation cancelled by user");
373
+ }
374
+ throw error;
375
+ }
376
+ cancellationToken.throwIfCancelled();
377
+ fileProgressBar.complete();
378
+ // Show summary with dynamic key information
379
+ if (dynamicKeys.size > 0) {
380
+ console.log(chalk_1.default.yellow(`\n⚠️ Note: Found ${chalk_1.default.bold(dynamicKeys.size)} keys used dynamically (e.g., t(\`a.b.\${var}\`))\n` +
381
+ chalk_1.default.gray(` These keys and their children are marked as used to prevent false positives.\n`)));
382
+ }
383
+ console.log(chalk_1.default.green(`\n✅ Completed: Checked ${chalk_1.default.cyan(allKeys.length)} translation keys across ${chalk_1.default.cyan(processedFiles)} files\n`));
384
+ // Find unused keys
385
+ const unusedKeys = allKeys.filter((key) => !usedKeys.has(key.path));
386
+ return unusedKeys;
387
+ };
388
+ exports.findUnusedKeys = findUnusedKeys;
389
+ /**
390
+ * Removes a key from a nested object using the original path
391
+ */
392
+ const removeKeyFromObject = (obj, path) => {
393
+ if (path.length === 0)
394
+ return;
395
+ const [currentKey, ...remainingPath] = path;
396
+ if (remainingPath.length === 0) {
397
+ // This is the final key, delete it
398
+ delete obj[currentKey];
399
+ }
400
+ else {
401
+ // Navigate deeper
402
+ if (obj[currentKey] && typeof obj[currentKey] === "object") {
403
+ removeKeyFromObject(obj[currentKey], remainingPath);
404
+ // Clean up empty objects
405
+ if (Object.keys(obj[currentKey]).length === 0) {
406
+ delete obj[currentKey];
407
+ }
408
+ }
409
+ }
410
+ };
411
+ /**
412
+ * Prompts user for confirmation
413
+ */
414
+ const askConfirmation = (question) => {
415
+ const rl = readline.createInterface({
416
+ input: process.stdin,
417
+ output: process.stdout,
418
+ });
419
+ return new Promise((resolve) => {
420
+ rl.question(question, (answer) => {
421
+ rl.close();
422
+ resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
423
+ });
424
+ });
425
+ };
426
+ /**
427
+ * Analyzes and removes unused i18n keys
428
+ */
429
+ const analyzeAndRemoveUnusedKeys = async (options) => {
430
+ const { directory, i18nFile } = options;
431
+ const i18nFilePath = path.isAbsolute(i18nFile)
432
+ ? i18nFile
433
+ : path.join(directory, i18nFile);
434
+ console.log(chalk_1.default.bold(`\nAnalyzing unused translation keys in ${chalk_1.default.cyan(i18nFilePath)}\n`));
435
+ try {
436
+ // Find unused keys
437
+ const unusedKeys = await (0, exports.findUnusedKeys)(options);
438
+ // Check if cancelled
439
+ if (unusedKeys.length === 0 && !options.maxConcurrency) {
440
+ // This might be a cancellation, but we'll let it through for now
441
+ }
442
+ if (unusedKeys.length === 0) {
443
+ console.log(chalk_1.default.green("\n✓ No unused translation keys found!\n"));
444
+ return;
445
+ }
446
+ // Display unused keys
447
+ console.log(chalk_1.default.yellow(`\nFound ${chalk_1.default.bold(unusedKeys.length)} unused translation key(s):\n`));
448
+ unusedKeys.forEach((key, index) => {
449
+ console.log(chalk_1.default.gray(`${index + 1}. `) + chalk_1.default.white(key.path) + chalk_1.default.gray(` (${JSON.stringify(key.value)})`));
450
+ });
451
+ // Ask for confirmation
452
+ const confirmed = await askConfirmation(chalk_1.default.yellow(`\nDo you want to remove these ${unusedKeys.length} unused key(s)? (y/n): `));
453
+ if (!confirmed) {
454
+ console.log(chalk_1.default.gray("\nOperation cancelled.\n"));
455
+ return;
456
+ }
457
+ // Read the original file
458
+ const fileContent = await fs.readFile(i18nFilePath, "utf-8");
459
+ const i18nContent = JSON.parse(fileContent);
460
+ // Remove unused keys
461
+ for (const key of unusedKeys) {
462
+ removeKeyFromObject(i18nContent, key.originalPath);
463
+ }
464
+ // Write back to file
465
+ await fs.writeFile(i18nFilePath, JSON.stringify(i18nContent, null, 2) + "\n", "utf-8");
466
+ console.log(chalk_1.default.green(`\n✓ Successfully removed ${unusedKeys.length} unused translation key(s) from ${i18nFilePath}\n`));
467
+ }
468
+ catch (error) {
469
+ if ((error === null || error === void 0 ? void 0 : error.message) === "Operation cancelled by user") {
470
+ console.log(chalk_1.default.yellow("\n\n✅ Analysis cancelled by user\n"));
471
+ process.exit(130); // Standard exit code for SIGINT
472
+ }
473
+ console.error(chalk_1.default.red(`\nError: ${error}\n`));
474
+ process.exit(1);
475
+ }
476
+ };
477
+ exports.analyzeAndRemoveUnusedKeys = analyzeAndRemoveUnusedKeys;