i18nsmith 0.4.3 → 0.5.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/build.mjs +5 -0
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/scan.d.ts.map +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/transform.d.ts.map +1 -1
- package/dist/index.cjs +6354 -2910
- package/package.json +1 -1
- package/src/commands/check.ts +5 -1
- package/src/commands/config.ts +126 -0
- package/src/commands/init.test.ts +80 -0
- package/src/commands/init.ts +33 -22
- package/src/commands/scan.ts +8 -1
- package/src/commands/sync.ts +43 -4
- package/src/commands/transform.ts +8 -1
- package/src/index.ts +1 -1
- package/src/integration.test.ts +145 -12
package/src/integration.test.ts
CHANGED
|
@@ -23,16 +23,25 @@ function runCli(
|
|
|
23
23
|
args: string[],
|
|
24
24
|
options: { cwd?: string } = {}
|
|
25
25
|
): { stdout: string; stderr: string; output: string; exitCode: number } {
|
|
26
|
+
// Clear known debug env vars so the test output is deterministic even when
|
|
27
|
+
// developer environments set DEBUG_* flags. Preserve essential env vars.
|
|
28
|
+
const env: NodeJS.ProcessEnv = {
|
|
29
|
+
...process.env,
|
|
30
|
+
CI: 'true',
|
|
31
|
+
NO_COLOR: '1',
|
|
32
|
+
FORCE_COLOR: '0',
|
|
33
|
+
};
|
|
34
|
+
// Remove any DEBUG_* flags that may leak into the spawned CLI process
|
|
35
|
+
delete env.DEBUG_VUE_PARSER;
|
|
36
|
+
delete env.DEBUG_REFEXT;
|
|
37
|
+
delete env.DEBUG_SYNC_REF;
|
|
38
|
+
delete env.DEBUG_VUE_MUTATE;
|
|
39
|
+
|
|
26
40
|
const result = spawnSync('node', [CLI_PATH, ...args], {
|
|
27
41
|
cwd: options.cwd ?? process.cwd(),
|
|
28
42
|
encoding: 'utf8',
|
|
29
43
|
timeout: 30000,
|
|
30
|
-
env
|
|
31
|
-
...process.env,
|
|
32
|
-
CI: 'true',
|
|
33
|
-
NO_COLOR: '1',
|
|
34
|
-
FORCE_COLOR: '0',
|
|
35
|
-
},
|
|
44
|
+
env,
|
|
36
45
|
});
|
|
37
46
|
|
|
38
47
|
// Log errors for debugging
|
|
@@ -53,11 +62,48 @@ function runCli(
|
|
|
53
62
|
|
|
54
63
|
// Helper to extract JSON from CLI output (may contain log messages before JSON)
|
|
55
64
|
function extractJson<T>(output: string): T {
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
// Use brace-counting to find all complete top-level JSON objects in the output.
|
|
66
|
+
// This correctly handles nested braces (arrays/objects inside the top-level object).
|
|
67
|
+
const candidates: string[] = [];
|
|
68
|
+
let i = 0;
|
|
69
|
+
while (i < output.length) {
|
|
70
|
+
if (output[i] === '{') {
|
|
71
|
+
let depth = 0;
|
|
72
|
+
let inString = false;
|
|
73
|
+
let escape = false;
|
|
74
|
+
let j = i;
|
|
75
|
+
for (; j < output.length; j++) {
|
|
76
|
+
const ch = output[j];
|
|
77
|
+
if (escape) { escape = false; continue; }
|
|
78
|
+
if (ch === '\\' && inString) { escape = true; continue; }
|
|
79
|
+
if (ch === '"') { inString = !inString; continue; }
|
|
80
|
+
if (inString) continue;
|
|
81
|
+
if (ch === '{') depth++;
|
|
82
|
+
else if (ch === '}') {
|
|
83
|
+
depth--;
|
|
84
|
+
if (depth === 0) { candidates.push(output.slice(i, j + 1)); break; }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
i = j + 1;
|
|
88
|
+
} else {
|
|
89
|
+
i++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!candidates.length) {
|
|
58
94
|
throw new Error(`No JSON found in output: ${output.slice(0, 200)}...`);
|
|
59
95
|
}
|
|
60
|
-
|
|
96
|
+
|
|
97
|
+
// Prefer the largest candidate (most likely the top-level summary).
|
|
98
|
+
candidates.sort((a, b) => b.length - a.length);
|
|
99
|
+
for (const candidate of candidates) {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(candidate);
|
|
102
|
+
} catch (_err) {
|
|
103
|
+
// try next candidate
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
throw new Error(`No valid JSON block found in output: ${output.slice(0, 400)}...`);
|
|
61
107
|
}
|
|
62
108
|
|
|
63
109
|
describe('CLI Integration Tests', () => {
|
|
@@ -188,7 +234,7 @@ export function App() {
|
|
|
188
234
|
);
|
|
189
235
|
|
|
190
236
|
const result = runCli(['scan', '--json'], { cwd: tmpDir });
|
|
191
|
-
|
|
237
|
+
const parsed = extractJson<{ filesScanned: number; candidates: unknown[] }>(result.output);
|
|
192
238
|
|
|
193
239
|
expect(parsed).toHaveProperty('filesScanned');
|
|
194
240
|
expect(parsed).toHaveProperty('candidates');
|
|
@@ -333,6 +379,61 @@ export function App() {
|
|
|
333
379
|
expect(backupExists).toBe(true);
|
|
334
380
|
});
|
|
335
381
|
|
|
382
|
+
it('CLI end-to-end: detects nested $t inside template object args (I18nDemo.vue)', async () => {
|
|
383
|
+
// Setup Vue SFC that uses a nested $t(...) inside an interpolation object
|
|
384
|
+
const vue = `
|
|
385
|
+
<template>
|
|
386
|
+
<p v-if="name">{{ $t('common.components.i18ndemo.arg0-name.4ac48a', { arg0: $t('demo.card.greeting'), name }) }}</p>
|
|
387
|
+
</template>
|
|
388
|
+
`;
|
|
389
|
+
|
|
390
|
+
await fs.mkdir(path.join(tmpDir, 'src', 'components'), { recursive: true });
|
|
391
|
+
await fs.writeFile(path.join(tmpDir, 'src', 'components', 'I18nDemo.vue'), vue);
|
|
392
|
+
|
|
393
|
+
const en = {
|
|
394
|
+
common: { components: { i18ndemo: { 'arg0-name': '{arg0} {name}' } } },
|
|
395
|
+
demo: { card: { greeting: 'Hello' } }
|
|
396
|
+
};
|
|
397
|
+
const es = {
|
|
398
|
+
common: { components: { i18ndemo: { 'arg0-name': '{arg0} {name}' } } },
|
|
399
|
+
demo: { card: { greeting: 'Hola' } }
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
await fs.writeFile(path.join(tmpDir, 'locales', 'en.json'), JSON.stringify(en, null, 2));
|
|
403
|
+
await fs.writeFile(path.join(tmpDir, 'locales', 'fr.json'), JSON.stringify(es, null, 2));
|
|
404
|
+
|
|
405
|
+
// Update config to include .vue files for this test
|
|
406
|
+
const cfgPath = path.join(tmpDir, 'i18n.config.json');
|
|
407
|
+
const cfg = JSON.parse(await fs.readFile(cfgPath, 'utf8'));
|
|
408
|
+
cfg.include = ['src/**/*.vue', 'src/**/*.{ts,js,tsx}'];
|
|
409
|
+
await fs.writeFile(cfgPath, JSON.stringify(cfg, null, 2));
|
|
410
|
+
|
|
411
|
+
const result = runCli(['sync', '--json'], { cwd: tmpDir });
|
|
412
|
+
expect(result.exitCode).toBe(0);
|
|
413
|
+
|
|
414
|
+
// Sanity check: CLI output should contain the quoted key somewhere so
|
|
415
|
+
// editors/CI that scan the output can detect the reference even when
|
|
416
|
+
// there are additional log lines present.
|
|
417
|
+
expect(result.output).toContain('"demo.card.greeting"');
|
|
418
|
+
|
|
419
|
+
// Also try to parse JSON summary if possible and validate references.
|
|
420
|
+
// If parsing fails during CI or debugging, print raw output for diagnosis.
|
|
421
|
+
try {
|
|
422
|
+
const parsed = extractJson<any>(result.output);
|
|
423
|
+
const referenced = parsed.references.map((r: any) => r.key);
|
|
424
|
+
const unused = parsed.unusedKeys.map((u: any) => u.key);
|
|
425
|
+
|
|
426
|
+
expect(referenced).toContain('demo.card.greeting');
|
|
427
|
+
expect(unused).not.toContain('demo.card.greeting');
|
|
428
|
+
} catch (err) {
|
|
429
|
+
// Dump output for debugging in test logs then rethrow so CI shows failure
|
|
430
|
+
// (this helps capture the raw CLI output when test fails).
|
|
431
|
+
// eslint-disable-next-line no-console
|
|
432
|
+
console.error('CLI raw output (truncated):', result.output.slice(0, 4000));
|
|
433
|
+
throw err;
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
336
437
|
it('should skip backup with --no-backup', async () => {
|
|
337
438
|
await fs.writeFile(
|
|
338
439
|
path.join(tmpDir, 'src', 'App.tsx'),
|
|
@@ -427,6 +528,38 @@ export function App() {
|
|
|
427
528
|
expect(result.output).toContain('Planning transform (dry-run)');
|
|
428
529
|
expect(result.exitCode).toBe(0);
|
|
429
530
|
});
|
|
531
|
+
|
|
532
|
+
it('extraction should not include structural opening punctuation (Items ({count}) case)', async () => {
|
|
533
|
+
const content = `export function App() {
|
|
534
|
+
const count = 5;
|
|
535
|
+
const items = ['a','b'];
|
|
536
|
+
return (
|
|
537
|
+
<>
|
|
538
|
+
<p>Items ({count}): {items.join(', ')}</p>
|
|
539
|
+
<p>{'Items (' + count + ')'}</p>
|
|
540
|
+
</>
|
|
541
|
+
);
|
|
542
|
+
}`;
|
|
543
|
+
|
|
544
|
+
await fs.writeFile(path.join(tmpDir, 'src', 'App.tsx'), content);
|
|
545
|
+
|
|
546
|
+
const result = runCli(['transform', '--json'], { cwd: tmpDir });
|
|
547
|
+
expect(result.exitCode).toBe(0);
|
|
548
|
+
const parsed = extractJson<any>(result.output);
|
|
549
|
+
const candidates = parsed.candidates || [];
|
|
550
|
+
|
|
551
|
+
// No candidate text should include the structural '(' token before the placeholder
|
|
552
|
+
const hasBadText = candidates.some((c: any) => typeof c.text === 'string' && c.text.includes('Items ('));
|
|
553
|
+
expect(hasBadText).toBe(false);
|
|
554
|
+
|
|
555
|
+
// Find the Items-related candidate and assert its suggestedKey is derived from the static "Items"
|
|
556
|
+
const itemsCandidate = candidates.find((c: any) => typeof c.text === 'string' && /Items/.test(c.text));
|
|
557
|
+
expect(itemsCandidate).toBeDefined();
|
|
558
|
+
expect(itemsCandidate.text).not.toContain('(');
|
|
559
|
+
if (itemsCandidate.interpolation) {
|
|
560
|
+
expect(itemsCandidate.interpolation.template).not.toMatch(/Items \(/);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
430
563
|
});
|
|
431
564
|
|
|
432
565
|
describe('check command', () => {
|
|
@@ -477,7 +610,7 @@ export function App() {
|
|
|
477
610
|
);
|
|
478
611
|
|
|
479
612
|
const result = runCli(['check', '--json'], { cwd: tmpDir });
|
|
480
|
-
|
|
613
|
+
const parsed = extractJson<{ diagnostics: unknown; sync: unknown }>(result.output);
|
|
481
614
|
|
|
482
615
|
expect(parsed).toHaveProperty('diagnostics');
|
|
483
616
|
expect(parsed).toHaveProperty('sync');
|
|
@@ -524,7 +657,7 @@ export function App() {
|
|
|
524
657
|
);
|
|
525
658
|
|
|
526
659
|
const result = runCli(['check', '--audit', '--json'], { cwd: tmpDir });
|
|
527
|
-
|
|
660
|
+
const parsed = extractJson<{ audit?: { totalQualityIssues: number } }>(result.output);
|
|
528
661
|
|
|
529
662
|
expect(parsed).toHaveProperty('audit');
|
|
530
663
|
expect(parsed.audit?.totalQualityIssues ?? 0).toBeGreaterThan(0);
|