transduck 0.0.5 → 0.1.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,162 @@
1
+ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
2
+ import React from 'react';
3
+ import { render, screen, cleanup, waitFor } from '@testing-library/react';
4
+ import { TransDuckProvider, t, ait, tPlural, aitPlural, useTransDuck, _resetReactState } from '../src/react/index.js';
5
+
6
+ // Mock fetch globally
7
+ const mockFetch = vi.fn();
8
+ global.fetch = mockFetch;
9
+
10
+ // Mock localStorage
11
+ const localStorageStore: Record<string, string> = {};
12
+ const mockLocalStorage = {
13
+ getItem: vi.fn((key: string) => localStorageStore[key] ?? null),
14
+ setItem: vi.fn((key: string, value: string) => { localStorageStore[key] = value; }),
15
+ removeItem: vi.fn((key: string) => { delete localStorageStore[key]; }),
16
+ clear: vi.fn(() => { for (const k in localStorageStore) delete localStorageStore[k]; }),
17
+ };
18
+ Object.defineProperty(globalThis, 'localStorage', { value: mockLocalStorage, writable: true });
19
+
20
+ describe('TransDuckProvider + t()', () => {
21
+ beforeEach(() => {
22
+ _resetReactState();
23
+ mockFetch.mockReset();
24
+ mockLocalStorage.clear();
25
+ });
26
+
27
+ afterEach(() => {
28
+ cleanup();
29
+ });
30
+
31
+ it('t() returns source text before translation loads', () => {
32
+ mockFetch.mockResolvedValue({
33
+ ok: true,
34
+ json: async () => ({ translations: { 'Hello||': 'Hallo' }, plurals: {} }),
35
+ });
36
+
37
+ function TestComp() {
38
+ useTransDuck();
39
+ return <span data-testid="text">{t('Hello')}</span>;
40
+ }
41
+
42
+ render(
43
+ <TransDuckProvider language="DE">
44
+ <TestComp />
45
+ </TransDuckProvider>
46
+ );
47
+
48
+ // Before fetch resolves, shows source text
49
+ expect(screen.getByTestId('text').textContent).toBe('Hello');
50
+ });
51
+
52
+ it('t() and ait() are the same function', () => {
53
+ expect(t).toBe(ait);
54
+ });
55
+
56
+ it('tPlural() and aitPlural() are the same function', () => {
57
+ expect(tPlural).toBe(aitPlural);
58
+ });
59
+
60
+ it('t() returns source text when language matches source', () => {
61
+ function TestComp() {
62
+ useTransDuck();
63
+ return <span data-testid="text">{t('Hello')}</span>;
64
+ }
65
+
66
+ render(
67
+ <TransDuckProvider language="EN" sourceLang="EN">
68
+ <TestComp />
69
+ </TransDuckProvider>
70
+ );
71
+
72
+ expect(screen.getByTestId('text').textContent).toBe('Hello');
73
+ expect(mockFetch).not.toHaveBeenCalled();
74
+ });
75
+
76
+ it('t() interpolates vars', () => {
77
+ function TestComp() {
78
+ useTransDuck();
79
+ return <span data-testid="text">{t('Hello {name}', undefined, { name: 'Tim' })}</span>;
80
+ }
81
+
82
+ render(
83
+ <TransDuckProvider language="EN" sourceLang="EN">
84
+ <TestComp />
85
+ </TransDuckProvider>
86
+ );
87
+
88
+ expect(screen.getByTestId('text').textContent).toBe('Hello Tim');
89
+ });
90
+
91
+ it('t() batches and fetches translations after render', async () => {
92
+ mockFetch.mockResolvedValue({
93
+ ok: true,
94
+ json: async () => ({
95
+ translations: { 'Hello||': 'Hallo', 'World||': 'Welt' },
96
+ plurals: {},
97
+ }),
98
+ });
99
+
100
+ function TestComp() {
101
+ useTransDuck();
102
+ return (
103
+ <div>
104
+ <span data-testid="a">{t('Hello')}</span>
105
+ <span data-testid="b">{t('World')}</span>
106
+ </div>
107
+ );
108
+ }
109
+
110
+ render(
111
+ <TransDuckProvider language="DE">
112
+ <TestComp />
113
+ </TransDuckProvider>
114
+ );
115
+
116
+ // After useEffect fires and fetch resolves
117
+ await waitFor(() => {
118
+ expect(screen.getByTestId('a').textContent).toBe('Hallo');
119
+ expect(screen.getByTestId('b').textContent).toBe('Welt');
120
+ });
121
+
122
+ // Should have made exactly one fetch call (batched)
123
+ expect(mockFetch).toHaveBeenCalledTimes(1);
124
+ });
125
+
126
+ it('t() does not refetch cached strings', async () => {
127
+ mockFetch.mockResolvedValue({
128
+ ok: true,
129
+ json: async () => ({
130
+ translations: { 'Hello||': 'Hallo' },
131
+ plurals: {},
132
+ }),
133
+ });
134
+
135
+ function TestComp() {
136
+ useTransDuck();
137
+ return <span data-testid="text">{t('Hello')}</span>;
138
+ }
139
+
140
+ const { rerender } = render(
141
+ <TransDuckProvider language="DE">
142
+ <TestComp />
143
+ </TransDuckProvider>
144
+ );
145
+
146
+ await waitFor(() => {
147
+ expect(screen.getByTestId('text').textContent).toBe('Hallo');
148
+ });
149
+
150
+ mockFetch.mockClear();
151
+
152
+ // Re-render — should not fetch again
153
+ rerender(
154
+ <TransDuckProvider language="DE">
155
+ <TestComp />
156
+ </TransDuckProvider>
157
+ );
158
+
159
+ expect(mockFetch).not.toHaveBeenCalled();
160
+ expect(screen.getByTestId('text').textContent).toBe('Hallo');
161
+ });
162
+ });
@@ -100,6 +100,30 @@ ait_plural("{count} item", "{count} items", count=n)
100
100
  const result = extractStrings('line1\nait("Hello")\nline3', 'test.py');
101
101
  expect(result[0].line).toBe(2);
102
102
  });
103
+
104
+ it('extracts t() from files with transduck/react import', () => {
105
+ const code = `import { t } from 'transduck/react';\nt("Hello")\nt("Book", "Hotel booking")`;
106
+ const result = extractStrings(code, 'test.tsx');
107
+ expect(result.length).toBe(2);
108
+ expect(result[0].text).toBe('Hello');
109
+ expect(result[1].text).toBe('Book');
110
+ expect(result[1].context).toBe('Hotel booking');
111
+ });
112
+
113
+ it('does not extract t() from files without transduck/react import', () => {
114
+ const code = `import { t } from 'i18next';\nt("Hello")`;
115
+ const result = extractStrings(code, 'test.tsx');
116
+ expect(result.length).toBe(0);
117
+ });
118
+
119
+ it('extracts tPlural() from files with transduck/react import', () => {
120
+ const code = `import { tPlural } from 'transduck/react';\ntPlural("{count} item", "{count} items", 5)`;
121
+ const result = extractStrings(code, 'test.tsx');
122
+ expect(result.length).toBe(1);
123
+ expect(result[0].plural).toBe(true);
124
+ expect(result[0].one).toBe('{count} item');
125
+ expect(result[0].other).toBe('{count} items');
126
+ });
103
127
  });
104
128
 
105
129
  describe('scanDirectory', () => {
package/tsconfig.json CHANGED
@@ -10,7 +10,8 @@
10
10
  "esModuleInterop": true,
11
11
  "skipLibCheck": true,
12
12
  "forceConsistentCasingInFileNames": true,
13
- "resolveJsonModule": true
13
+ "resolveJsonModule": true,
14
+ "jsx": "react-jsx"
14
15
  },
15
16
  "include": ["src/**/*"]
16
17
  }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environmentMatchGlobs: [
6
+ ['tests/react-*.test.tsx', 'jsdom'],
7
+ ],
8
+ },
9
+ });