i18nsmith 0.2.0 → 0.3.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.
Files changed (55) hide show
  1. package/dist/commands/audit.d.ts.map +1 -1
  2. package/dist/commands/backup.d.ts.map +1 -1
  3. package/dist/commands/check.d.ts +25 -0
  4. package/dist/commands/check.d.ts.map +1 -1
  5. package/dist/commands/config.d.ts.map +1 -1
  6. package/dist/commands/debug-patterns.d.ts.map +1 -1
  7. package/dist/commands/diagnose.d.ts.map +1 -1
  8. package/dist/commands/init.d.ts.map +1 -1
  9. package/dist/commands/install-hooks.d.ts.map +1 -1
  10. package/dist/commands/preflight.d.ts.map +1 -1
  11. package/dist/commands/rename.d.ts.map +1 -1
  12. package/dist/commands/review.d.ts.map +1 -1
  13. package/dist/commands/scaffold-adapter.d.ts.map +1 -1
  14. package/dist/commands/scan.d.ts.map +1 -1
  15. package/dist/commands/sync.d.ts +1 -1
  16. package/dist/commands/sync.d.ts.map +1 -1
  17. package/dist/commands/transform.d.ts.map +1 -1
  18. package/dist/commands/translate/index.d.ts.map +1 -1
  19. package/dist/index.js +2574 -107704
  20. package/dist/rename-suspicious.test.d.ts +2 -0
  21. package/dist/rename-suspicious.test.d.ts.map +1 -0
  22. package/dist/utils/diff-utils.d.ts +5 -0
  23. package/dist/utils/diff-utils.d.ts.map +1 -1
  24. package/dist/utils/errors.d.ts +8 -0
  25. package/dist/utils/errors.d.ts.map +1 -0
  26. package/dist/utils/locale-audit.d.ts +39 -0
  27. package/dist/utils/locale-audit.d.ts.map +1 -0
  28. package/dist/utils/preview.d.ts.map +1 -1
  29. package/dist/utils/preview.test.d.ts +2 -0
  30. package/dist/utils/preview.test.d.ts.map +1 -0
  31. package/package.json +5 -5
  32. package/src/commands/audit.ts +18 -209
  33. package/src/commands/backup.ts +67 -63
  34. package/src/commands/check.ts +119 -68
  35. package/src/commands/config.ts +117 -95
  36. package/src/commands/debug-patterns.ts +25 -22
  37. package/src/commands/diagnose.ts +29 -26
  38. package/src/commands/init.ts +84 -79
  39. package/src/commands/install-hooks.ts +18 -15
  40. package/src/commands/preflight.ts +21 -13
  41. package/src/commands/rename.ts +86 -81
  42. package/src/commands/review.ts +81 -78
  43. package/src/commands/scaffold-adapter.ts +8 -4
  44. package/src/commands/scan.ts +61 -58
  45. package/src/commands/sync.ts +640 -203
  46. package/src/commands/transform.ts +117 -8
  47. package/src/commands/translate/index.ts +7 -4
  48. package/src/e2e.test.ts +78 -14
  49. package/src/integration.test.ts +86 -0
  50. package/src/rename-suspicious.test.ts +124 -0
  51. package/src/utils/diff-utils.ts +6 -0
  52. package/src/utils/errors.ts +34 -0
  53. package/src/utils/locale-audit.ts +219 -0
  54. package/src/utils/preview.test.ts +137 -0
  55. package/src/utils/preview.ts +2 -8
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=rename-suspicious.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rename-suspicious.test.d.ts","sourceRoot":"","sources":["../src/rename-suspicious.test.ts"],"names":[],"mappings":""}
@@ -1,3 +1,8 @@
1
+ /**
2
+ * CLI presentation layer for diff utilities.
3
+ * This module handles printing and writing locale diffs for CLI commands.
4
+ * Core diff building logic is in packages/core/src/diff-utils.ts.
5
+ */
1
6
  import type { SyncSummary } from '@i18nsmith/core';
2
7
  export declare function printLocaleDiffs(diffs: SyncSummary['diffs']): void;
3
8
  export declare function writeLocaleDiffPatches(diffs: SyncSummary['diffs'], directory: string): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"diff-utils.d.ts","sourceRoot":"","sources":["../../src/utils/diff-utils.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,QAW3D;AAED,wBAAsB,sBAAsB,CAAC,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,MAAM,iBAuB1F"}
1
+ {"version":3,"file":"diff-utils.d.ts","sourceRoot":"","sources":["../../src/utils/diff-utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAEnD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,QAW3D;AAED,wBAAsB,sBAAsB,CAAC,KAAK,EAAE,WAAW,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,MAAM,iBAuB1F"}
@@ -0,0 +1,8 @@
1
+ export declare class CliError extends Error {
2
+ exitCode: number;
3
+ constructor(message: string, exitCode?: number);
4
+ }
5
+ type MaybePromise<T> = T | Promise<T>;
6
+ export declare function withErrorHandling<A extends unknown[], R>(action: (...args: A) => MaybePromise<R>): (...args: A) => Promise<R | undefined>;
7
+ export {};
8
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/utils/errors.ts"],"names":[],"mappings":"AAEA,qBAAa,QAAS,SAAQ,KAAK;IACG,QAAQ;gBAAhC,OAAO,EAAE,MAAM,EAAS,QAAQ,SAAI;CAIjD;AAED,KAAK,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAEtC,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,OAAO,EAAE,EAAE,CAAC,EACtD,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,YAAY,CAAC,CAAC,CAAC,GACtC,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAoBxC"}
@@ -0,0 +1,39 @@
1
+ import { type I18nConfig } from '@i18nsmith/core';
2
+ export interface AuditIssue {
3
+ key: string;
4
+ value: string;
5
+ reason: string;
6
+ description: string;
7
+ suggestion?: string;
8
+ }
9
+ export interface QualityIssue {
10
+ type: 'duplicate-value' | 'inconsistent-key' | 'orphaned-namespace';
11
+ description: string;
12
+ keys?: string[];
13
+ suggestion?: string;
14
+ }
15
+ export interface LocaleAuditResult {
16
+ locale: string;
17
+ totalKeys: number;
18
+ issues: AuditIssue[];
19
+ qualityIssues: QualityIssue[];
20
+ }
21
+ export interface LocaleAuditSummary {
22
+ results: LocaleAuditResult[];
23
+ totalIssues: number;
24
+ totalQualityIssues: number;
25
+ }
26
+ export interface LocaleAuditContext {
27
+ config: I18nConfig;
28
+ projectRoot: string;
29
+ }
30
+ export interface LocaleAuditOptions {
31
+ locales?: string[];
32
+ checkDuplicates?: boolean;
33
+ checkInconsistent?: boolean;
34
+ checkOrphaned?: boolean;
35
+ }
36
+ export declare function runLocaleAudit(context: LocaleAuditContext, options?: LocaleAuditOptions): Promise<LocaleAuditSummary>;
37
+ export declare function printLocaleAuditResults(summary: LocaleAuditSummary): void;
38
+ export declare function hasAuditFindings(summary: LocaleAuditSummary): boolean;
39
+ //# sourceMappingURL=locale-audit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locale-audit.d.ts","sourceRoot":"","sources":["../../src/utils/locale-audit.ts"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,UAAU,EAChB,MAAM,iBAAiB,CAAC;AAEzB,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,iBAAiB,GAAG,kBAAkB,GAAG,oBAAoB,CAAC;IACpE,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,aAAa,EAAE,YAAY,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,UAAU,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,wBAAsB,cAAc,CAClC,OAAO,EAAE,kBAAkB,EAC3B,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,kBAAkB,CAAC,CAkH7B;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,kBAAkB,QA6ClE;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAErE"}
@@ -1 +1 @@
1
- {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../src/utils/preview.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,cAAc,CAAC,QAAQ;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,QAAQ,CAAC;CACnB;AAED,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,WAAW,GAAG,YAAY,GAAG,WAAW,CAAC;AAE5E,wBAAsB,gBAAgB,CAAC,QAAQ,EAC7C,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,QAAQ,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAcjB;AAED,wBAAsB,eAAe,CAAC,QAAQ,EAC5C,YAAY,EAAE,WAAW,EACzB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAWnC;AAED,wBAAsB,gBAAgB,CACpC,YAAY,EAAE,WAAW,EACzB,WAAW,EAAE,MAAM,EACnB,SAAS,GAAE,MAAM,EAAO,GACvB,OAAO,CAAC,IAAI,CAAC,CAuBf"}
1
+ {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../src/utils/preview.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,cAAc,CAAC,QAAQ;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,QAAQ,CAAC;CACnB;AAED,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,WAAW,GAAG,YAAY,GAAG,WAAW,CAAC;AAE5E,wBAAsB,gBAAgB,CAAC,QAAQ,EAC7C,IAAI,EAAE,WAAW,EACjB,OAAO,EAAE,QAAQ,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAcjB;AAED,wBAAsB,eAAe,CAAC,QAAQ,EAC5C,YAAY,EAAE,WAAW,EACzB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAWnC;AAED,wBAAsB,gBAAgB,CACpC,YAAY,EAAE,WAAW,EACzB,WAAW,EAAE,MAAM,EACnB,SAAS,GAAE,MAAM,EAAO,GACvB,OAAO,CAAC,IAAI,CAAC,CAiBf"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=preview.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview.test.d.ts","sourceRoot":"","sources":["../../src/utils/preview.test.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18nsmith",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "CLI for i18nsmith",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -21,6 +21,10 @@
21
21
  "test": "vitest run"
22
22
  },
23
23
  "dependencies": {
24
+ "@i18nsmith/core": "workspace:*",
25
+ "@i18nsmith/transformer": "workspace:*",
26
+ "@i18nsmith/translation": "workspace:*",
27
+ "@i18nsmith/translator-mock": "workspace:*",
24
28
  "chalk": "^5.0.0",
25
29
  "commander": "^11.0.0",
26
30
  "diff": "^8.0.2",
@@ -31,10 +35,6 @@
31
35
  "ts-morph": "^21.0.0"
32
36
  },
33
37
  "devDependencies": {
34
- "@i18nsmith/core": "workspace:*",
35
- "@i18nsmith/transformer": "workspace:*",
36
- "@i18nsmith/translation": "workspace:*",
37
- "@i18nsmith/translator-mock": "workspace:*",
38
38
  "esbuild": "^0.25.0"
39
39
  }
40
40
  }
@@ -1,14 +1,7 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
1
  import chalk from 'chalk';
4
2
  import type { Command } from 'commander';
5
- import {
6
- loadConfig,
7
- LocaleStore,
8
- KeyValidator,
9
- LocaleValidator,
10
- SUSPICIOUS_KEY_REASON_DESCRIPTIONS,
11
- } from '@i18nsmith/core';
3
+ import { runCheck } from './check.js';
4
+ import { withErrorHandling } from '../utils/errors.js';
12
5
 
13
6
  interface AuditCommandOptions {
14
7
  config?: string;
@@ -21,28 +14,6 @@ interface AuditCommandOptions {
21
14
  orphaned?: boolean;
22
15
  }
23
16
 
24
- interface AuditIssue {
25
- key: string;
26
- value: string;
27
- reason: string;
28
- description: string;
29
- suggestion?: string;
30
- }
31
-
32
- interface QualityIssue {
33
- type: 'duplicate-value' | 'inconsistent-key' | 'orphaned-namespace';
34
- description: string;
35
- keys?: string[];
36
- suggestion?: string;
37
- }
38
-
39
- interface LocaleAuditResult {
40
- locale: string;
41
- totalKeys: number;
42
- issues: AuditIssue[];
43
- qualityIssues: QualityIssue[];
44
- }
45
-
46
17
  export function registerAudit(program: Command) {
47
18
  program
48
19
  .command('audit')
@@ -53,182 +24,20 @@ export function registerAudit(program: Command) {
53
24
  .option('--report <path>', 'Write JSON report to a file')
54
25
  .option('--strict', 'Exit with error code if any issues found', false)
55
26
  .option('--duplicates', 'Check for duplicate values (consolidation opportunities)', false)
56
- .option('--inconsistent', 'Check for inconsistent key naming patterns', false)
57
- .option('--orphaned', 'Check for orphaned namespaces with few keys', false)
58
- .action(async (options: AuditCommandOptions) => {
59
- console.log(chalk.blue('Auditing locale files...'));
60
- try {
61
- const config = await loadConfig(options.config);
62
- const localesDir = path.resolve(process.cwd(), config.localesDir ?? 'locales');
63
- const localeStore = new LocaleStore(localesDir, {
64
- sortKeys: config.locales?.sortKeys ?? 'alphabetical',
65
- });
66
- const keyValidator = new KeyValidator(config.sync?.suspiciousKeyPolicy ?? 'skip');
67
- const localeValidator = new LocaleValidator({
68
- delimiter: config.locales?.delimiter ?? '.',
69
- });
70
-
71
- // Determine which locales to audit
72
- let localesToAudit = options.locale ?? [];
73
- if (localesToAudit.length === 0) {
74
- localesToAudit = [config.sourceLanguage ?? 'en', ...(config.targetLanguages ?? [])];
75
- }
76
-
77
- // Enable all quality checks if none specified
78
- const runQualityChecks = options.duplicates || options.inconsistent || options.orphaned;
79
- const checkDuplicates = options.duplicates || !runQualityChecks;
80
- const checkInconsistent = options.inconsistent;
81
- const checkOrphaned = options.orphaned;
82
-
83
- const results: LocaleAuditResult[] = [];
84
- const allKeys = new Set<string>();
85
-
86
- // First pass: collect all keys
87
- for (const locale of localesToAudit) {
88
- const data = await localeStore.get(locale);
89
- for (const key of Object.keys(data)) {
90
- allKeys.add(key);
91
- }
92
- }
93
-
94
- // Second pass: run audits
95
- for (const locale of localesToAudit) {
96
- const data = await localeStore.get(locale);
97
- const keys = Object.keys(data);
98
- const issues: AuditIssue[] = [];
99
- const qualityIssues: QualityIssue[] = [];
100
-
101
- // Suspicious key detection
102
- for (const key of keys) {
103
- const value = data[key];
104
- const analysis = keyValidator.analyzeWithValue(key, value);
105
-
106
- if (analysis.suspicious && analysis.reason) {
107
- issues.push({
108
- key,
109
- value: value.length > 50 ? `${value.slice(0, 47)}...` : value,
110
- reason: analysis.reason,
111
- description: SUSPICIOUS_KEY_REASON_DESCRIPTIONS[analysis.reason] ?? 'Unknown issue',
112
- suggestion: keyValidator.suggestFix(key, analysis.reason),
113
- });
114
- }
115
- }
116
-
117
- // Quality checks
118
- if (checkDuplicates) {
119
- const duplicates = localeValidator.detectDuplicateValues(locale, data);
120
- for (const dup of duplicates) {
121
- qualityIssues.push({
122
- type: 'duplicate-value',
123
- description: `Value "${dup.value.slice(0, 40)}${dup.value.length > 40 ? '...' : ''}" used by ${dup.keys.length} keys`,
124
- keys: dup.keys,
125
- suggestion: 'Consider consolidating to a single key',
126
- });
127
- }
128
- }
129
-
130
- if (checkInconsistent && locale === localesToAudit[0]) {
131
- // Only check inconsistent keys once (using all keys)
132
- const inconsistent = localeValidator.detectInconsistentKeys(Array.from(allKeys));
133
- for (const inc of inconsistent) {
134
- qualityIssues.push({
135
- type: 'inconsistent-key',
136
- description: inc.pattern,
137
- keys: inc.variants,
138
- suggestion: inc.suggestion,
139
- });
140
- }
141
- }
142
-
143
- if (checkOrphaned && locale === localesToAudit[0]) {
144
- // Only check orphaned namespaces once (using all keys)
145
- const orphaned = localeValidator.detectOrphanedNamespaces(Array.from(allKeys));
146
- for (const orph of orphaned) {
147
- qualityIssues.push({
148
- type: 'orphaned-namespace',
149
- description: `Namespace "${orph.namespace}" has only ${orph.keyCount} key(s)`,
150
- keys: orph.keys,
151
- suggestion: 'Consider merging into a related namespace',
152
- });
153
- }
154
- }
155
-
156
- results.push({
157
- locale,
158
- totalKeys: keys.length,
159
- issues,
160
- qualityIssues,
161
- });
162
- }
163
-
164
- const totalIssues = results.reduce((sum, r) => sum + r.issues.length, 0);
165
- const totalQualityIssues = results.reduce((sum, r) => sum + r.qualityIssues.length, 0);
166
-
167
- if (options.report) {
168
- const outputPath = path.resolve(process.cwd(), options.report);
169
- await fs.mkdir(path.dirname(outputPath), { recursive: true });
170
- await fs.writeFile(outputPath, JSON.stringify({ results, totalIssues, totalQualityIssues }, null, 2));
171
- console.log(chalk.green(`Audit report written to ${outputPath}`));
172
- }
173
-
174
- if (options.json) {
175
- console.log(JSON.stringify({ results, totalIssues, totalQualityIssues }, null, 2));
176
- } else {
177
- // Pretty print results
178
- for (const result of results) {
179
- const hasIssues = result.issues.length > 0 || result.qualityIssues.length > 0;
180
- if (!hasIssues) {
181
- console.log(chalk.green(`✓ ${result.locale}.json: ${result.totalKeys} keys, no issues`));
182
- } else {
183
- console.log(chalk.yellow(`⚠ ${result.locale}.json: ${result.totalKeys} keys`));
184
-
185
- // Suspicious keys
186
- if (result.issues.length > 0) {
187
- console.log(chalk.yellow(` Suspicious keys: ${result.issues.length}`));
188
- for (const issue of result.issues) {
189
- console.log(chalk.dim(` - "${issue.key}"`));
190
- console.log(chalk.dim(` ${issue.description}`));
191
- if (issue.suggestion) {
192
- console.log(chalk.dim(` Suggestion: ${issue.suggestion}`));
193
- }
194
- }
195
- }
196
-
197
- // Quality issues
198
- if (result.qualityIssues.length > 0) {
199
- console.log(chalk.cyan(` Quality checks: ${result.qualityIssues.length}`));
200
- for (const issue of result.qualityIssues) {
201
- const typeLabel = issue.type === 'duplicate-value' ? '📋' :
202
- issue.type === 'inconsistent-key' ? '🔀' : '📦';
203
- console.log(chalk.dim(` ${typeLabel} ${issue.description}`));
204
- if (issue.suggestion) {
205
- console.log(chalk.dim(` ${issue.suggestion}`));
206
- }
207
- }
208
- }
209
- }
210
- }
211
-
212
- console.log();
213
- if (totalIssues === 0 && totalQualityIssues === 0) {
214
- console.log(chalk.green('✓ No issues found in locale files'));
215
- } else {
216
- if (totalIssues > 0) {
217
- console.log(chalk.yellow(`Found ${totalIssues} suspicious key(s)`));
218
- }
219
- if (totalQualityIssues > 0) {
220
- console.log(chalk.cyan(`Found ${totalQualityIssues} quality issue(s)`));
221
- }
222
- }
223
- }
224
-
225
- if (options.strict && totalIssues > 0) {
226
- console.error(chalk.red(`\nAudit failed with ${totalIssues} issue(s). Use --strict=false to allow.`));
227
- process.exitCode = 1;
228
- }
229
- } catch (error) {
230
- console.error(chalk.red('Audit failed:'), (error as Error).message);
231
- process.exitCode = 1;
232
- }
233
- });
27
+ .option('--inconsistent', 'Detect inconsistent key naming patterns', false)
28
+ .option('--orphaned', 'Detect orphaned namespaces that only contain a few keys', false)
29
+ .action(withErrorHandling(async (options: AuditCommandOptions) => {
30
+ console.log(chalk.yellow('⚠️ "i18nsmith audit" is deprecated. Proxying to "i18nsmith check --audit"...'));
31
+ await runCheck({
32
+ config: options.config,
33
+ json: options.json,
34
+ report: options.report,
35
+ audit: true,
36
+ auditStrict: options.strict,
37
+ auditLocales: options.locale,
38
+ auditDuplicates: options.duplicates,
39
+ auditInconsistent: options.inconsistent,
40
+ auditOrphaned: options.orphaned,
41
+ });
42
+ }));
234
43
  }
@@ -2,6 +2,7 @@ import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
3
  import inquirer from 'inquirer';
4
4
  import { listBackups, restoreBackup } from '@i18nsmith/core';
5
+ import { CliError, withErrorHandling } from '../utils/errors.js';
5
6
 
6
7
  /**
7
8
  * Registers backup-related commands (backup-list, backup-restore)
@@ -11,86 +12,89 @@ export function registerBackup(program: Command): void {
11
12
  .command('backup-list')
12
13
  .description('List available locale file backups')
13
14
  .option('--backup-dir <path>', 'Custom backup directory (default: .i18nsmith-backup)')
14
- .action(async (options: { backupDir?: string }) => {
15
- try {
16
- const workspaceRoot = process.cwd();
17
- const backups = await listBackups(workspaceRoot, { backupDir: options.backupDir });
15
+ .action(
16
+ withErrorHandling(async (options: { backupDir?: string }) => {
17
+ try {
18
+ const workspaceRoot = process.cwd();
19
+ const backups = await listBackups(workspaceRoot, { backupDir: options.backupDir });
18
20
 
19
- if (backups.length === 0) {
20
- console.log(chalk.yellow('No backups found.'));
21
- console.log(chalk.gray('Backups are created automatically when using --write --prune'));
22
- return;
23
- }
21
+ if (backups.length === 0) {
22
+ console.log(chalk.yellow('No backups found.'));
23
+ console.log(chalk.gray('Backups are created automatically when using --write --prune'));
24
+ return;
25
+ }
24
26
 
25
- console.log(chalk.blue(`Found ${backups.length} backup(s):\n`));
27
+ console.log(chalk.blue(`Found ${backups.length} backup(s):\n`));
26
28
 
27
- for (const backup of backups) {
28
- const date = new Date(backup.createdAt);
29
- const formattedDate = date.toLocaleString();
30
- console.log(` ${chalk.cyan(backup.timestamp)} ${formattedDate} (${backup.fileCount} files)`);
31
- }
29
+ for (const backup of backups) {
30
+ const date = new Date(backup.createdAt);
31
+ const formattedDate = date.toLocaleString();
32
+ console.log(` ${chalk.cyan(backup.timestamp)} ${formattedDate} (${backup.fileCount} files)`);
33
+ }
32
34
 
33
- console.log(chalk.gray(`\nRestore a backup with: i18nsmith backup-restore <timestamp>`));
34
- } catch (err) {
35
- console.error(chalk.red('Error listing backups:'), err instanceof Error ? err.message : err);
36
- process.exitCode = 1;
37
- }
38
- });
35
+ console.log(chalk.gray(`\nRestore a backup with: i18nsmith backup-restore <timestamp>`));
36
+ } catch (error) {
37
+ const message = error instanceof Error ? error.message : String(error);
38
+ throw new CliError(`Error listing backups: ${message}`);
39
+ }
40
+ })
41
+ );
39
42
 
40
43
  program
41
44
  .command('backup-restore')
42
45
  .description('Restore locale files from a previous backup')
43
46
  .argument('<timestamp>', 'Backup timestamp (from backup-list) or "latest" for most recent')
44
47
  .option('--backup-dir <path>', 'Custom backup directory (default: .i18nsmith-backup)')
45
- .action(async (timestamp: string, options: { backupDir?: string }) => {
46
- try {
47
- const workspaceRoot = process.cwd();
48
- const backups = await listBackups(workspaceRoot, { backupDir: options.backupDir });
48
+ .action(
49
+ withErrorHandling(async (timestamp: string, options: { backupDir?: string }) => {
50
+ try {
51
+ const workspaceRoot = process.cwd();
52
+ const backups = await listBackups(workspaceRoot, { backupDir: options.backupDir });
49
53
 
50
- if (backups.length === 0) {
51
- console.error(chalk.red('No backups found.'));
52
- process.exitCode = 1;
53
- return;
54
- }
54
+ if (backups.length === 0) {
55
+ throw new CliError('No backups found.');
56
+ }
55
57
 
56
- let targetBackup = timestamp === 'latest'
57
- ? backups[0]
58
- : backups.find((b) => b.timestamp === timestamp);
58
+ const targetBackup = timestamp === 'latest'
59
+ ? backups[0]
60
+ : backups.find((b) => b.timestamp === timestamp);
59
61
 
60
- if (!targetBackup) {
61
- console.error(chalk.red(`Backup not found: ${timestamp}`));
62
- console.log(chalk.gray('Available backups:'));
63
- for (const b of backups.slice(0, 5)) {
64
- console.log(` ${b.timestamp}`);
62
+ if (!targetBackup) {
63
+ const suggestion = backups
64
+ .slice(0, 5)
65
+ .map((b) => b.timestamp)
66
+ .join(', ');
67
+ throw new CliError(
68
+ suggestion
69
+ ? `Backup not found: ${timestamp}. Available backups: ${suggestion}`
70
+ : `Backup not found: ${timestamp}`
71
+ );
65
72
  }
66
- process.exitCode = 1;
67
- return;
68
- }
69
73
 
70
- // Confirm restore
71
- const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([
72
- {
73
- type: 'confirm',
74
- name: 'confirmed',
75
- message: `Restore ${targetBackup.fileCount} locale files from backup ${targetBackup.timestamp}? This will overwrite current locale files.`,
76
- default: false,
77
- },
78
- ]);
74
+ const { confirmed } = await inquirer.prompt<{ confirmed: boolean }>([
75
+ {
76
+ type: 'confirm',
77
+ name: 'confirmed',
78
+ message: `Restore ${targetBackup.fileCount} locale files from backup ${targetBackup.timestamp}? This will overwrite current locale files.`,
79
+ default: false,
80
+ },
81
+ ]);
79
82
 
80
- if (!confirmed) {
81
- console.log(chalk.yellow('Restore cancelled.'));
82
- return;
83
- }
83
+ if (!confirmed) {
84
+ console.log(chalk.yellow('Restore cancelled.'));
85
+ return;
86
+ }
84
87
 
85
- const result = await restoreBackup(targetBackup.path, workspaceRoot);
88
+ const result = await restoreBackup(targetBackup.path, workspaceRoot);
86
89
 
87
- console.log(chalk.green(`\n✅ ${result.summary}`));
88
- for (const file of result.restored) {
89
- console.log(chalk.gray(` Restored: ${file}`));
90
+ console.log(chalk.green(`\n✅ ${result.summary}`));
91
+ for (const file of result.restored) {
92
+ console.log(chalk.gray(` Restored: ${file}`));
93
+ }
94
+ } catch (error) {
95
+ const message = error instanceof Error ? error.message : String(error);
96
+ throw new CliError(`Error restoring backup: ${message}`);
90
97
  }
91
- } catch (err) {
92
- console.error(chalk.red('Error restoring backup:'), err instanceof Error ? err.message : err);
93
- process.exitCode = 1;
94
- }
95
- });
98
+ })
99
+ );
96
100
  }