i18nsmith 0.2.1 → 0.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.
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/check.d.ts +25 -0
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/debug-patterns.d.ts.map +1 -1
- package/dist/commands/diagnose.d.ts.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/install-hooks.d.ts.map +1 -1
- package/dist/commands/preflight.d.ts.map +1 -1
- package/dist/commands/rename.d.ts.map +1 -1
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/scaffold-adapter.d.ts.map +1 -1
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/sync.d.ts +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/transform.d.ts.map +1 -1
- package/dist/commands/translate/index.d.ts.map +1 -1
- package/dist/index.js +2536 -107783
- package/dist/rename-suspicious.test.d.ts +2 -0
- package/dist/rename-suspicious.test.d.ts.map +1 -0
- package/dist/utils/diff-utils.d.ts +5 -0
- package/dist/utils/diff-utils.d.ts.map +1 -1
- package/dist/utils/errors.d.ts +8 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/locale-audit.d.ts +39 -0
- package/dist/utils/locale-audit.d.ts.map +1 -0
- package/dist/utils/preview.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/commands/audit.ts +18 -209
- package/src/commands/backup.ts +67 -63
- package/src/commands/check.ts +119 -68
- package/src/commands/config.ts +117 -95
- package/src/commands/debug-patterns.ts +25 -22
- package/src/commands/diagnose.ts +29 -26
- package/src/commands/init.ts +84 -79
- package/src/commands/install-hooks.ts +18 -15
- package/src/commands/preflight.ts +21 -13
- package/src/commands/rename.ts +86 -81
- package/src/commands/review.ts +81 -78
- package/src/commands/scaffold-adapter.ts +8 -4
- package/src/commands/scan.ts +61 -58
- package/src/commands/sync.ts +640 -203
- package/src/commands/transform.ts +46 -18
- package/src/commands/translate/index.ts +7 -4
- package/src/e2e.test.ts +34 -14
- package/src/integration.test.ts +86 -0
- package/src/rename-suspicious.test.ts +124 -0
- package/src/utils/diff-utils.ts +6 -0
- package/src/utils/errors.ts +34 -0
- package/src/utils/locale-audit.ts +219 -0
- package/src/utils/preview.test.ts +43 -0
- package/src/utils/preview.ts +2 -8
|
@@ -7,6 +7,7 @@ import { Transformer } from '@i18nsmith/transformer';
|
|
|
7
7
|
import type { TransformProgress, TransformSummary } from '@i18nsmith/transformer';
|
|
8
8
|
import { printLocaleDiffs, writeLocaleDiffPatches } from '../utils/diff-utils.js';
|
|
9
9
|
import { applyPreviewFile, writePreviewFile } from '../utils/preview.js';
|
|
10
|
+
import { CliError, withErrorHandling } from '../utils/errors.js';
|
|
10
11
|
|
|
11
12
|
interface TransformOptions {
|
|
12
13
|
config?: string;
|
|
@@ -102,16 +103,26 @@ function createProgressLogger() {
|
|
|
102
103
|
let lastPercent = -1;
|
|
103
104
|
let lastLogTime = 0;
|
|
104
105
|
let pendingCarriageReturn = false;
|
|
106
|
+
let lastPlainLog = 0;
|
|
107
|
+
let emittedZero = false;
|
|
108
|
+
const isTTY = Boolean(process.stdout.isTTY);
|
|
109
|
+
const minPercentStep = isTTY ? 5 : 15;
|
|
110
|
+
const minIntervalMs = isTTY ? 1000 : 4000;
|
|
105
111
|
|
|
106
112
|
const writeLine = (line: string, final = false) => {
|
|
107
|
-
if (
|
|
113
|
+
if (isTTY) {
|
|
108
114
|
process.stdout.write(`\r${line}`);
|
|
109
115
|
pendingCarriageReturn = !final;
|
|
110
116
|
if (final) {
|
|
111
117
|
process.stdout.write('\n');
|
|
112
118
|
}
|
|
113
|
-
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
if (final || now - lastPlainLog >= minIntervalMs) {
|
|
114
124
|
console.log(line);
|
|
125
|
+
lastPlainLog = now;
|
|
115
126
|
}
|
|
116
127
|
};
|
|
117
128
|
|
|
@@ -122,25 +133,40 @@ function createProgressLogger() {
|
|
|
122
133
|
}
|
|
123
134
|
|
|
124
135
|
const now = Date.now();
|
|
125
|
-
const
|
|
126
|
-
progress.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
136
|
+
const computedPercent =
|
|
137
|
+
typeof progress.percent === 'number'
|
|
138
|
+
? Math.max(0, Math.min(100, Math.round(progress.percent)))
|
|
139
|
+
: progress.total
|
|
140
|
+
? Math.round((progress.processed / progress.total) * 100)
|
|
141
|
+
: 0;
|
|
142
|
+
const reachedEnd = progress.processed === progress.total;
|
|
143
|
+
const percentAdvanced =
|
|
144
|
+
(!emittedZero && computedPercent === 0) || computedPercent >= lastPercent + minPercentStep || reachedEnd;
|
|
145
|
+
const timedOut = now - lastLogTime >= minIntervalMs;
|
|
130
146
|
|
|
131
|
-
if (!
|
|
147
|
+
if (!percentAdvanced && !timedOut) {
|
|
132
148
|
return;
|
|
133
149
|
}
|
|
134
150
|
|
|
135
|
-
|
|
151
|
+
if (!emittedZero && computedPercent === 0) {
|
|
152
|
+
emittedZero = true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
lastPercent = Math.max(lastPercent, computedPercent);
|
|
136
156
|
lastLogTime = now;
|
|
157
|
+
|
|
158
|
+
const remaining =
|
|
159
|
+
typeof progress.remaining === 'number'
|
|
160
|
+
? progress.remaining
|
|
161
|
+
: Math.max(progress.total - progress.processed, 0);
|
|
162
|
+
|
|
137
163
|
const line =
|
|
138
|
-
`Applying transforms ${progress.processed}/${progress.total} (${
|
|
164
|
+
`Applying transforms ${progress.processed}/${progress.total} (${computedPercent}%)` +
|
|
139
165
|
` | applied: ${progress.applied ?? 0}` +
|
|
140
166
|
` | skipped: ${progress.skipped ?? 0}` +
|
|
141
167
|
(progress.errors ? ` | errors: ${progress.errors}` : '') +
|
|
142
|
-
` | remaining: ${
|
|
143
|
-
writeLine(line,
|
|
168
|
+
` | remaining: ${remaining}`;
|
|
169
|
+
writeLine(line, reachedEnd);
|
|
144
170
|
},
|
|
145
171
|
flush() {
|
|
146
172
|
if (pendingCarriageReturn && process.stdout.isTTY) {
|
|
@@ -166,7 +192,8 @@ export function registerTransform(program: Command) {
|
|
|
166
192
|
.option('--migrate-text-keys', 'Migrate existing t("Text") calls to structured keys')
|
|
167
193
|
.option('--preview-output <path>', 'Write preview summary (JSON) to a file (implies dry-run)')
|
|
168
194
|
.option('--apply-preview <path>', 'Apply a previously saved transform preview JSON file safely')
|
|
169
|
-
.action(
|
|
195
|
+
.action(
|
|
196
|
+
withErrorHandling(async (options: TransformOptions) => {
|
|
170
197
|
if (options.applyPreview) {
|
|
171
198
|
await applyPreviewFile('transform', options.applyPreview);
|
|
172
199
|
return;
|
|
@@ -247,13 +274,14 @@ export function registerTransform(program: Command) {
|
|
|
247
274
|
console.log(chalk.yellow('Run again with --write to apply these changes.'));
|
|
248
275
|
}
|
|
249
276
|
} catch (error) {
|
|
250
|
-
const errorMessage =
|
|
277
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
251
278
|
if (options.json) {
|
|
252
279
|
console.log(JSON.stringify({ ok: false, error: { message: errorMessage } }, null, 2));
|
|
253
|
-
|
|
254
|
-
|
|
280
|
+
process.exitCode = 1;
|
|
281
|
+
return;
|
|
255
282
|
}
|
|
256
|
-
|
|
283
|
+
throw new CliError(`Transform failed: ${errorMessage}`);
|
|
257
284
|
}
|
|
258
|
-
})
|
|
285
|
+
})
|
|
286
|
+
);
|
|
259
287
|
}
|
|
@@ -21,6 +21,7 @@ import { emitTranslateOutput, maybePrintEstimate } from './reporter.js';
|
|
|
21
21
|
import { handleCsvExport, handleCsvImport } from './csv-handler.js';
|
|
22
22
|
import { executeTranslations } from './executor.js';
|
|
23
23
|
import { applyPreviewFile, writePreviewFile } from '../../utils/preview.js';
|
|
24
|
+
import { CliError, withErrorHandling } from '../../utils/errors.js';
|
|
24
25
|
|
|
25
26
|
// Re-export types for external use
|
|
26
27
|
export * from './types.js';
|
|
@@ -102,7 +103,8 @@ export function registerTranslate(program: Command): void {
|
|
|
102
103
|
.option('--import <path>', 'Import translations from a CSV file and merge into locale files')
|
|
103
104
|
.option('--preview-output <path>', 'Write preview summary (JSON) to a file (implies dry-run)')
|
|
104
105
|
.option('--apply-preview <path>', 'Apply a previously saved translate preview JSON file safely')
|
|
105
|
-
.action(
|
|
106
|
+
.action(
|
|
107
|
+
withErrorHandling(async (options: TranslateCommandOptions) => {
|
|
106
108
|
if (options.applyPreview) {
|
|
107
109
|
await applyPreviewFile('translate', options.applyPreview, ['--yes']);
|
|
108
110
|
return;
|
|
@@ -233,8 +235,9 @@ export function registerTranslate(program: Command): void {
|
|
|
233
235
|
? error
|
|
234
236
|
: new Error(typeof error === 'string' ? error : JSON.stringify(error));
|
|
235
237
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
+
const message = translatorError?.message ?? normalizedError.message;
|
|
239
|
+
throw new CliError(`Translate failed: ${message}`);
|
|
238
240
|
}
|
|
239
|
-
})
|
|
241
|
+
})
|
|
242
|
+
);
|
|
240
243
|
}
|
package/src/e2e.test.ts
CHANGED
|
@@ -85,6 +85,17 @@ function extractJson<T>(output: string): T {
|
|
|
85
85
|
return JSON.parse(jsonMatch[0]);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
function parseRenameMap(content: string): Record<string, string> {
|
|
89
|
+
const mapping: Record<string, string> = {};
|
|
90
|
+
for (const line of content.split('\n')) {
|
|
91
|
+
const match = line.trim().match(/^"([^"]+)"\s*=\s*"([^"]+)"/);
|
|
92
|
+
if (match) {
|
|
93
|
+
mapping[match[1]] = match[2];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return mapping;
|
|
97
|
+
}
|
|
98
|
+
|
|
88
99
|
beforeAll(async () => {
|
|
89
100
|
await ensureCliBuilt(CLI_PATH);
|
|
90
101
|
});
|
|
@@ -258,20 +269,29 @@ describe('E2E Fixture Tests', () => {
|
|
|
258
269
|
|
|
259
270
|
const enLocalePath = path.join(fixtureDir, 'locales', 'en.json');
|
|
260
271
|
const frLocalePath = path.join(fixtureDir, 'locales', 'fr.json');
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
272
|
+
const enLocale = JSON.parse(await fs.readFile(enLocalePath, 'utf8'));
|
|
273
|
+
const frLocale = JSON.parse(await fs.readFile(frLocalePath, 'utf8'));
|
|
274
|
+
|
|
275
|
+
expect(enLocale).not.toHaveProperty('Hello World');
|
|
276
|
+
expect(frLocale).not.toHaveProperty('Hello World');
|
|
277
|
+
|
|
278
|
+
const mapPath = path.join(fixtureDir, 'auto-rename-map.txt');
|
|
279
|
+
const mapContents = await fs.readFile(mapPath, 'utf8');
|
|
280
|
+
expect(mapContents).toContain('"Hello World" = ');
|
|
281
|
+
const renameMap = parseRenameMap(mapContents);
|
|
282
|
+
const helloKey = renameMap['Hello World'];
|
|
283
|
+
expect(helloKey).toBeDefined();
|
|
284
|
+
const submitKey = renameMap['buttons.submit'];
|
|
285
|
+
expect(submitKey).toBeDefined();
|
|
286
|
+
|
|
287
|
+
expect(enLocale).toHaveProperty(helloKey!);
|
|
288
|
+
expect(frLocale).toHaveProperty(helloKey!);
|
|
289
|
+
expect(enLocale).toHaveProperty(submitKey!);
|
|
290
|
+
expect(frLocale).toHaveProperty(submitKey!);
|
|
291
|
+
|
|
292
|
+
const sourceFile = await fs.readFile(path.join(fixtureDir, 'src', 'BadKeys.tsx'), 'utf8');
|
|
293
|
+
expect(sourceFile).toContain(`t('${helloKey!}')`);
|
|
294
|
+
expect(sourceFile).not.toContain("t('Hello World')");
|
|
275
295
|
});
|
|
276
296
|
});
|
|
277
297
|
|
package/src/integration.test.ts
CHANGED
|
@@ -281,6 +281,45 @@ export function App() {
|
|
|
281
281
|
expect(enData).not.toHaveProperty('existing.key');
|
|
282
282
|
});
|
|
283
283
|
|
|
284
|
+
it('should delegate --check to the health check command', async () => {
|
|
285
|
+
await fs.writeFile(
|
|
286
|
+
path.join(tmpDir, 'src', 'App.tsx'),
|
|
287
|
+
`import { useTranslation } from 'react-i18next';
|
|
288
|
+
export function App() {
|
|
289
|
+
const { t } = useTranslation();
|
|
290
|
+
return <div>{t('existing.key')}</div>;
|
|
291
|
+
}`
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const result = runCli(['sync', '--check'], { cwd: tmpDir });
|
|
295
|
+
|
|
296
|
+
expect(result.exitCode).toBe(0);
|
|
297
|
+
expect(result.output).toContain('guided repository health check');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should fail via delegated check when using --check --strict and audit issues exist', async () => {
|
|
301
|
+
const duplicateLocale = {
|
|
302
|
+
'existing.key': 'Existing Value',
|
|
303
|
+
'buttons.submit': 'Submit',
|
|
304
|
+
'cta.submit': 'Submit',
|
|
305
|
+
};
|
|
306
|
+
await fs.writeFile(path.join(tmpDir, 'locales', 'en.json'), JSON.stringify(duplicateLocale, null, 2));
|
|
307
|
+
await fs.writeFile(path.join(tmpDir, 'locales', 'fr.json'), JSON.stringify(duplicateLocale, null, 2));
|
|
308
|
+
await fs.writeFile(
|
|
309
|
+
path.join(tmpDir, 'src', 'App.tsx'),
|
|
310
|
+
`import { useTranslation } from 'react-i18next';
|
|
311
|
+
export function App() {
|
|
312
|
+
const { t } = useTranslation();
|
|
313
|
+
return <div>{t('existing.key')}</div>;
|
|
314
|
+
}`
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
const result = runCli(['sync', '--check', '--strict'], { cwd: tmpDir });
|
|
318
|
+
|
|
319
|
+
expect(result.exitCode).toBe(10);
|
|
320
|
+
expect(result.output).toContain('Audit detected');
|
|
321
|
+
});
|
|
322
|
+
|
|
284
323
|
it('should create backup when pruning', async () => {
|
|
285
324
|
await fs.writeFile(
|
|
286
325
|
path.join(tmpDir, 'src', 'App.tsx'),
|
|
@@ -445,5 +484,52 @@ export function App() {
|
|
|
445
484
|
expect(parsed).toHaveProperty('diagnostics');
|
|
446
485
|
expect(parsed).toHaveProperty('sync');
|
|
447
486
|
});
|
|
487
|
+
|
|
488
|
+
it('should surface locale audit results and fail with --audit-strict', async () => {
|
|
489
|
+
const duplicateLocale = {
|
|
490
|
+
'hello': 'Hello',
|
|
491
|
+
'buttons.submit': 'Submit',
|
|
492
|
+
'cta.submit': 'Submit',
|
|
493
|
+
};
|
|
494
|
+
await fs.writeFile(path.join(tmpDir, 'locales', 'en.json'), JSON.stringify(duplicateLocale, null, 2));
|
|
495
|
+
await fs.writeFile(path.join(tmpDir, 'locales', 'fr.json'), JSON.stringify(duplicateLocale, null, 2));
|
|
496
|
+
await fs.writeFile(
|
|
497
|
+
path.join(tmpDir, 'src', 'App.tsx'),
|
|
498
|
+
`import { useTranslation } from 'react-i18next';
|
|
499
|
+
export function App() {
|
|
500
|
+
const { t } = useTranslation();
|
|
501
|
+
return <div>{t('hello')}</div>;
|
|
502
|
+
}`
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
const result = runCli(['check', '--audit', '--audit-strict'], { cwd: tmpDir });
|
|
506
|
+
|
|
507
|
+
expect(result.exitCode).toBe(10);
|
|
508
|
+
expect(result.output).toContain('Locale quality audit');
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
it('should include audit payload in JSON output when --audit is set', async () => {
|
|
512
|
+
const duplicateLocale = {
|
|
513
|
+
'hello': 'Hello',
|
|
514
|
+
'buttons.submit': 'Submit',
|
|
515
|
+
'cta.submit': 'Submit',
|
|
516
|
+
};
|
|
517
|
+
await fs.writeFile(path.join(tmpDir, 'locales', 'en.json'), JSON.stringify(duplicateLocale, null, 2));
|
|
518
|
+
await fs.writeFile(path.join(tmpDir, 'locales', 'fr.json'), JSON.stringify(duplicateLocale, null, 2));
|
|
519
|
+
await fs.writeFile(
|
|
520
|
+
path.join(tmpDir, 'src', 'App.tsx'),
|
|
521
|
+
`import { useTranslation } from 'react-i18next';
|
|
522
|
+
export function App() {
|
|
523
|
+
const { t } = useTranslation();
|
|
524
|
+
return <div>{t('hello')}</div>;
|
|
525
|
+
}`
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
const result = runCli(['check', '--audit', '--json'], { cwd: tmpDir });
|
|
529
|
+
const parsed = extractJson<{ audit?: { totalQualityIssues: number } }>(result.stdout);
|
|
530
|
+
|
|
531
|
+
expect(parsed).toHaveProperty('audit');
|
|
532
|
+
expect(parsed.audit?.totalQualityIssues ?? 0).toBeGreaterThan(0);
|
|
533
|
+
});
|
|
448
534
|
});
|
|
449
535
|
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
|
|
2
|
+
import { describe, it, expect, beforeAll, afterEach } from 'vitest';
|
|
3
|
+
import fs from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import { spawnSync } from 'child_process';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { ensureCliBuilt } from './test-helpers/ensure-cli-built';
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
|
|
13
|
+
const CLI_PATH = path.resolve(__dirname, '../dist/index.js');
|
|
14
|
+
const FIXTURES_DIR = path.resolve(__dirname, './fixtures');
|
|
15
|
+
|
|
16
|
+
function runCli(
|
|
17
|
+
args: string[],
|
|
18
|
+
options: { cwd?: string } = {}
|
|
19
|
+
): { stdout: string; stderr: string; output: string; exitCode: number } {
|
|
20
|
+
const result = spawnSync('node', [CLI_PATH, ...args], {
|
|
21
|
+
cwd: options.cwd ?? process.cwd(),
|
|
22
|
+
encoding: 'utf8',
|
|
23
|
+
timeout: 30000,
|
|
24
|
+
env: {
|
|
25
|
+
...process.env,
|
|
26
|
+
CI: 'true',
|
|
27
|
+
NO_COLOR: '1',
|
|
28
|
+
FORCE_COLOR: '0',
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
stdout: result.stdout ?? '',
|
|
34
|
+
stderr: result.stderr ?? '',
|
|
35
|
+
output: (result.stdout ?? '') + (result.stderr ?? ''),
|
|
36
|
+
exitCode: result.status ?? 1,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function setupFixture(fixtureName: string): Promise<string> {
|
|
41
|
+
const fixtureSource = path.join(FIXTURES_DIR, fixtureName);
|
|
42
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), `i18nsmith-e2e-${fixtureName}-`));
|
|
43
|
+
await fs.cp(fixtureSource, tmpDir, { recursive: true });
|
|
44
|
+
return tmpDir;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function cleanupFixture(fixtureDir: string): Promise<void> {
|
|
48
|
+
await fs.rm(fixtureDir, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('Rename Suspicious Keys E2E', () => {
|
|
52
|
+
let tmpDir: string;
|
|
53
|
+
|
|
54
|
+
beforeAll(async () => {
|
|
55
|
+
await ensureCliBuilt(CLI_PATH);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterEach(async () => {
|
|
59
|
+
if (tmpDir) {
|
|
60
|
+
await cleanupFixture(tmpDir);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should generate rename diffs when running sync with auto-rename and preview output', async () => {
|
|
65
|
+
tmpDir = await setupFixture('suspicious-keys');
|
|
66
|
+
const previewPath = path.join(tmpDir, 'preview.json');
|
|
67
|
+
|
|
68
|
+
const { output, exitCode } = runCli(
|
|
69
|
+
['sync', '--diff', '--auto-rename-suspicious', '--preview-output', previewPath],
|
|
70
|
+
{ cwd: tmpDir }
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
expect(exitCode).toBe(0);
|
|
74
|
+
expect(output).toContain('Preview written to');
|
|
75
|
+
|
|
76
|
+
const previewContent = await fs.readFile(previewPath, 'utf8');
|
|
77
|
+
const preview = JSON.parse(previewContent);
|
|
78
|
+
|
|
79
|
+
expect(preview.summary).toBeDefined();
|
|
80
|
+
expect(preview.summary.suspiciousKeys.length).toBeGreaterThan(0);
|
|
81
|
+
|
|
82
|
+
// Check for renameDiffs
|
|
83
|
+
expect(preview.summary.renameDiffs).toBeDefined();
|
|
84
|
+
expect(preview.summary.renameDiffs.length).toBeGreaterThan(0);
|
|
85
|
+
|
|
86
|
+
// Check for localeDiffs
|
|
87
|
+
expect(preview.summary.localeDiffs).toBeDefined();
|
|
88
|
+
expect(preview.summary.localeDiffs.length).toBeGreaterThan(0);
|
|
89
|
+
|
|
90
|
+
// Verify specific rename proposal
|
|
91
|
+
const renameDiff = preview.summary.renameDiffs.find((d: any) => d.relativePath.includes('BadKeys.tsx'));
|
|
92
|
+
expect(renameDiff).toBeDefined();
|
|
93
|
+
// console.log('Actual diff:', renameDiff.diff);
|
|
94
|
+
expect(renameDiff.diff).toContain('- <h1>{t(\'Hello World\')}</h1>');
|
|
95
|
+
// Key generation includes hash and file slug
|
|
96
|
+
expect(renameDiff.diff).toMatch(/\+ <h1>{t\('common\.badkeys\.hello-world\.[a-f0-9]+'\)}<\/h1>/);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should handle existing keys that are suspicious (key-equals-value or contains-spaces)', async () => {
|
|
100
|
+
// This covers the user's specific scenario: key exists in locale but is suspicious
|
|
101
|
+
tmpDir = await setupFixture('suspicious-keys');
|
|
102
|
+
const previewPath = path.join(tmpDir, 'preview.json');
|
|
103
|
+
|
|
104
|
+
// "Hello World" exists in en.json and is suspicious
|
|
105
|
+
const { output, exitCode } = runCli(
|
|
106
|
+
['sync', '--diff', '--auto-rename-suspicious', '--preview-output', previewPath],
|
|
107
|
+
{ cwd: tmpDir }
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
expect(exitCode).toBe(0);
|
|
111
|
+
|
|
112
|
+
const preview = JSON.parse(await fs.readFile(previewPath, 'utf8'));
|
|
113
|
+
|
|
114
|
+
// Verify "Hello World" is in suspiciousKeys
|
|
115
|
+
const helloWorldSuspicious = preview.summary.suspiciousKeys.find((k: any) => k.key === 'Hello World');
|
|
116
|
+
expect(helloWorldSuspicious).toBeDefined();
|
|
117
|
+
// It might be 'contains-spaces' or 'key-equals-value' depending on priority
|
|
118
|
+
expect(['key-equals-value', 'contains-spaces']).toContain(helloWorldSuspicious.reason);
|
|
119
|
+
|
|
120
|
+
// Verify it has a rename diff
|
|
121
|
+
const renameDiff = preview.summary.renameDiffs.find((d: any) => d.relativePath.includes('BadKeys.tsx'));
|
|
122
|
+
expect(renameDiff).toBeDefined();
|
|
123
|
+
});
|
|
124
|
+
});
|
package/src/utils/diff-utils.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
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
|
+
*/
|
|
6
|
+
|
|
1
7
|
import fs from 'fs/promises';
|
|
2
8
|
import path from 'path';
|
|
3
9
|
import chalk from 'chalk';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
export class CliError extends Error {
|
|
4
|
+
constructor(message: string, public exitCode = 1) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'CliError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
11
|
+
|
|
12
|
+
export function withErrorHandling<A extends unknown[], R>(
|
|
13
|
+
action: (...args: A) => MaybePromise<R>
|
|
14
|
+
): (...args: A) => Promise<R | undefined> {
|
|
15
|
+
return async (...args: A): Promise<R | undefined> => {
|
|
16
|
+
try {
|
|
17
|
+
return (await action(...args)) as R;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (error instanceof CliError) {
|
|
20
|
+
console.error(chalk.red(error.message));
|
|
21
|
+
process.exitCode = error.exitCode;
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
26
|
+
console.error(chalk.red(`Unexpected error: ${message}`));
|
|
27
|
+
if (error instanceof Error && error.stack) {
|
|
28
|
+
console.error(chalk.gray(error.stack));
|
|
29
|
+
}
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|