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.
@@ -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
- const jsonMatch = output.match(/\{[\s\S]*\}/);
57
- if (!jsonMatch) {
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
- return JSON.parse(jsonMatch[0]);
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
- const parsed = extractJson<{ filesScanned: number; candidates: unknown[] }>(result.stdout);
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
- const parsed = extractJson<{ diagnostics: unknown; sync: unknown }>(result.stdout);
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
- const parsed = extractJson<{ audit?: { totalQualityIssues: number } }>(result.stdout);
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);