transduck 0.0.4 → 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.
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +167 -2
- package/dist/config.d.ts +1 -0
- package/dist/config.js +1 -0
- package/dist/handler.d.ts +23 -0
- package/dist/handler.js +153 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +11 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +1 -0
- package/dist/react/provider.d.ts +23 -0
- package/dist/react/provider.js +218 -0
- package/dist/scanner.d.ts +21 -0
- package/dist/scanner.js +180 -0
- package/package.json +25 -1
- package/src/cli.ts +195 -2
- package/src/config.ts +2 -0
- package/src/handler.ts +193 -0
- package/src/index.ts +14 -0
- package/src/react/index.ts +1 -0
- package/src/react/provider.tsx +287 -0
- package/src/scanner.ts +215 -0
- package/tests/cli.test.ts +63 -2
- package/tests/handler.test.ts +136 -0
- package/tests/react-provider.test.tsx +162 -0
- package/tests/scanner.test.ts +191 -0
- package/tsconfig.json +2 -1
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { writeFileSync, mkdirSync, mkdtempSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { extractStrings, scanDirectory } from '../src/scanner.js';
|
|
6
|
+
|
|
7
|
+
function makeTmpDir(): string {
|
|
8
|
+
return mkdtempSync(join(tmpdir(), 'transduck-scanner-test-'));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('extractStrings', () => {
|
|
12
|
+
it('extracts ait with double quotes', () => {
|
|
13
|
+
const result = extractStrings('ait("Our Events")', 'test.py');
|
|
14
|
+
expect(result).toHaveLength(1);
|
|
15
|
+
expect(result[0].text).toBe('Our Events');
|
|
16
|
+
expect(result[0].plural).toBeUndefined();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('extracts ait with single quotes', () => {
|
|
20
|
+
const result = extractStrings("ait('Our Events')", 'test.py');
|
|
21
|
+
expect(result).toHaveLength(1);
|
|
22
|
+
expect(result[0].text).toBe('Our Events');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('extracts ait with keyword context', () => {
|
|
26
|
+
const result = extractStrings('ait("Book Now", context="Hotel booking")', 'test.py');
|
|
27
|
+
expect(result).toHaveLength(1);
|
|
28
|
+
expect(result[0].text).toBe('Book Now');
|
|
29
|
+
expect(result[0].context).toBe('Hotel booking');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('extracts ait with positional context (JS style)', () => {
|
|
33
|
+
const result = extractStrings('ait("Book Now", "Hotel booking")', 'test.js');
|
|
34
|
+
expect(result).toHaveLength(1);
|
|
35
|
+
expect(result[0].text).toBe('Book Now');
|
|
36
|
+
expect(result[0].context).toBe('Hotel booking');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('extracts ait with no context', () => {
|
|
40
|
+
const result = extractStrings('ait("Hello")', 'test.py');
|
|
41
|
+
expect(result[0].context).toBeNull();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('extracts ait with vars but no context', () => {
|
|
45
|
+
const result = extractStrings('ait("Welcome {name}", vars={"name": user})', 'test.py');
|
|
46
|
+
expect(result[0].text).toBe('Welcome {name}');
|
|
47
|
+
expect(result[0].context).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('extracts ait_plural (Python style)', () => {
|
|
51
|
+
const result = extractStrings('ait_plural("{count} message", "{count} messages", count=n)', 'test.py');
|
|
52
|
+
expect(result).toHaveLength(1);
|
|
53
|
+
expect(result[0].plural).toBe(true);
|
|
54
|
+
expect(result[0].one).toBe('{count} message');
|
|
55
|
+
expect(result[0].other).toBe('{count} messages');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('extracts aitPlural (JS style)', () => {
|
|
59
|
+
const result = extractStrings("aitPlural('{count} night', '{count} nights', 5)", 'test.ts');
|
|
60
|
+
expect(result).toHaveLength(1);
|
|
61
|
+
expect(result[0].plural).toBe(true);
|
|
62
|
+
expect(result[0].one).toBe('{count} night');
|
|
63
|
+
expect(result[0].other).toBe('{count} nights');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('extracts Django template tag', () => {
|
|
67
|
+
const result = extractStrings('{% ait "Our Events" %}', 'test.html');
|
|
68
|
+
expect(result).toHaveLength(1);
|
|
69
|
+
expect(result[0].text).toBe('Our Events');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('extracts Django template tag with context', () => {
|
|
73
|
+
const result = extractStrings('{% ait "Book Now" context="Hotel booking" %}', 'test.html');
|
|
74
|
+
expect(result[0].text).toBe('Book Now');
|
|
75
|
+
expect(result[0].context).toBe('Hotel booking');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('extracts Jinja expression', () => {
|
|
79
|
+
const result = extractStrings('{{ ait("Our Events") }}', 'test.html');
|
|
80
|
+
expect(result).toHaveLength(1);
|
|
81
|
+
expect(result[0].text).toBe('Our Events');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('extracts multiple strings', () => {
|
|
85
|
+
const code = `
|
|
86
|
+
ait("Hello")
|
|
87
|
+
ait("World", context="greeting")
|
|
88
|
+
ait_plural("{count} item", "{count} items", count=n)
|
|
89
|
+
`;
|
|
90
|
+
const result = extractStrings(code, 'test.py');
|
|
91
|
+
expect(result).toHaveLength(3);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('returns empty for no matches', () => {
|
|
95
|
+
const result = extractStrings('print("Hello")', 'test.py');
|
|
96
|
+
expect(result).toHaveLength(0);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('includes line numbers', () => {
|
|
100
|
+
const result = extractStrings('line1\nait("Hello")\nline3', 'test.py');
|
|
101
|
+
expect(result[0].line).toBe(2);
|
|
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
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('scanDirectory', () => {
|
|
130
|
+
it('scans directory and deduplicates', () => {
|
|
131
|
+
const tmp = makeTmpDir();
|
|
132
|
+
writeFileSync(join(tmp, 'app.py'), 'ait("Hello")\nait("World")');
|
|
133
|
+
writeFileSync(join(tmp, 'views.py'), 'ait("Hello")'); // duplicate
|
|
134
|
+
writeFileSync(join(tmp, 'template.html'), '{% ait "Events" %}');
|
|
135
|
+
const result = scanDirectory([tmp]);
|
|
136
|
+
const texts = result.filter(e => !e.plural).map(e => e.text);
|
|
137
|
+
expect(texts).toContain('Hello');
|
|
138
|
+
expect(texts).toContain('World');
|
|
139
|
+
expect(texts).toContain('Events');
|
|
140
|
+
expect(result).toHaveLength(3);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('dedup tracks files', () => {
|
|
144
|
+
const tmp = makeTmpDir();
|
|
145
|
+
writeFileSync(join(tmp, 'a.py'), 'ait("Hello")');
|
|
146
|
+
writeFileSync(join(tmp, 'b.py'), 'ait("Hello")');
|
|
147
|
+
const result = scanDirectory([tmp]);
|
|
148
|
+
expect(result).toHaveLength(1);
|
|
149
|
+
expect(result[0].files).toHaveLength(2);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('skips node_modules', () => {
|
|
153
|
+
const tmp = makeTmpDir();
|
|
154
|
+
const nm = join(tmp, 'node_modules');
|
|
155
|
+
mkdirSync(nm);
|
|
156
|
+
writeFileSync(join(nm, 'dep.js'), 'ait("Hidden")');
|
|
157
|
+
writeFileSync(join(tmp, 'app.py'), 'ait("Visible")');
|
|
158
|
+
const result = scanDirectory([tmp]);
|
|
159
|
+
expect(result).toHaveLength(1);
|
|
160
|
+
expect(result[0].text).toBe('Visible');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('skips .venv', () => {
|
|
164
|
+
const tmp = makeTmpDir();
|
|
165
|
+
const venv = join(tmp, '.venv');
|
|
166
|
+
mkdirSync(venv);
|
|
167
|
+
writeFileSync(join(venv, 'lib.py'), 'ait("Hidden")');
|
|
168
|
+
writeFileSync(join(tmp, 'app.py'), 'ait("Visible")');
|
|
169
|
+
const result = scanDirectory([tmp]);
|
|
170
|
+
expect(result).toHaveLength(1);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('filters extensions', () => {
|
|
174
|
+
const tmp = makeTmpDir();
|
|
175
|
+
writeFileSync(join(tmp, 'readme.md'), 'ait("Not a code file")');
|
|
176
|
+
writeFileSync(join(tmp, 'app.py'), 'ait("Code file")');
|
|
177
|
+
const result = scanDirectory([tmp]);
|
|
178
|
+
expect(result).toHaveLength(1);
|
|
179
|
+
expect(result[0].text).toBe('Code file');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('deduplicates plural entries', () => {
|
|
183
|
+
const tmp = makeTmpDir();
|
|
184
|
+
writeFileSync(join(tmp, 'a.py'), 'ait_plural("{count} msg", "{count} msgs", count=n)');
|
|
185
|
+
writeFileSync(join(tmp, 'b.py'), 'ait_plural("{count} msg", "{count} msgs", count=n)');
|
|
186
|
+
const result = scanDirectory([tmp]);
|
|
187
|
+
expect(result).toHaveLength(1);
|
|
188
|
+
expect(result[0].plural).toBe(true);
|
|
189
|
+
expect(result[0].files).toHaveLength(2);
|
|
190
|
+
});
|
|
191
|
+
});
|
package/tsconfig.json
CHANGED