transduck 0.1.4 → 0.2.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/backend.d.ts +7 -40
- package/dist/backend.js +13 -88
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +49 -45
- package/dist/config.d.ts +2 -0
- package/dist/config.js +18 -5
- package/dist/handler.js +2 -24
- package/dist/index.js +2 -15
- package/dist/providers/claude-api.d.ts +7 -0
- package/dist/providers/claude-api.js +50 -0
- package/dist/providers/claude-code.d.ts +8 -0
- package/dist/providers/claude-code.js +47 -0
- package/dist/providers/index.d.ts +12 -0
- package/dist/providers/index.js +20 -0
- package/dist/providers/openai-provider.d.ts +6 -0
- package/dist/providers/openai-provider.js +40 -0
- package/dist/providers/prompts.d.ts +42 -0
- package/dist/providers/prompts.js +89 -0
- package/package.json +7 -3
- package/src/backend.ts +35 -141
- package/src/cli.ts +79 -46
- package/src/config.ts +22 -5
- package/src/handler.ts +15 -22
- package/src/index.ts +8 -14
- package/src/providers/claude-api.ts +77 -0
- package/src/providers/claude-code.ts +72 -0
- package/src/providers/index.ts +45 -0
- package/src/providers/openai-provider.ts +67 -0
- package/src/providers/prompts.ts +124 -0
- package/tests/ait.test.ts +3 -0
- package/tests/backend.test.ts +61 -59
- package/tests/providers.test.ts +289 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
buildMessages,
|
|
4
|
+
buildPluralMessages,
|
|
5
|
+
buildSinglePrompt,
|
|
6
|
+
buildPluralSinglePrompt,
|
|
7
|
+
safeRender,
|
|
8
|
+
} from '../src/providers/prompts.js';
|
|
9
|
+
import { getProvider } from '../src/providers/index.js';
|
|
10
|
+
import type { TransduckConfig } from '../src/config.js';
|
|
11
|
+
import { loadConfig } from '../src/config.js';
|
|
12
|
+
import { writeFileSync } from 'fs';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
import { mkdtempSync } from 'fs';
|
|
15
|
+
import { tmpdir } from 'os';
|
|
16
|
+
|
|
17
|
+
function makeConfig(overrides: Partial<TransduckConfig> = {}): TransduckConfig {
|
|
18
|
+
return {
|
|
19
|
+
projectName: 'test',
|
|
20
|
+
projectContext: 'A test site',
|
|
21
|
+
sourceLang: 'EN',
|
|
22
|
+
targetLangs: ['DE'],
|
|
23
|
+
storagePath: '/tmp/test.duckdb',
|
|
24
|
+
provider: 'openai',
|
|
25
|
+
apiKeyEnv: 'OPENAI_API_KEY',
|
|
26
|
+
tokenEnv: 'CLAUDE_CODE_OAUTH_TOKEN',
|
|
27
|
+
backendModel: 'gpt-4.1-mini',
|
|
28
|
+
backendTimeout: 10,
|
|
29
|
+
backendMaxRetries: 2,
|
|
30
|
+
readOnly: false,
|
|
31
|
+
...overrides,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---- Prompts ----
|
|
36
|
+
|
|
37
|
+
describe('prompts', () => {
|
|
38
|
+
describe('buildMessages', () => {
|
|
39
|
+
it('builds system and user messages', () => {
|
|
40
|
+
const messages = buildMessages({
|
|
41
|
+
sourceText: 'Hello',
|
|
42
|
+
sourceLang: 'EN',
|
|
43
|
+
targetLang: 'DE',
|
|
44
|
+
projectContext: 'A travel site',
|
|
45
|
+
stringContext: 'greeting',
|
|
46
|
+
});
|
|
47
|
+
expect(messages).toHaveLength(2);
|
|
48
|
+
expect(messages[0].role).toBe('system');
|
|
49
|
+
expect(messages[0].content).toContain('EN');
|
|
50
|
+
expect(messages[0].content).toContain('DE');
|
|
51
|
+
expect(messages[1].role).toBe('user');
|
|
52
|
+
expect(messages[1].content).toContain('Hello');
|
|
53
|
+
expect(messages[1].content).toContain('greeting');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('uses "none" for null string context', () => {
|
|
57
|
+
const messages = buildMessages({
|
|
58
|
+
sourceText: 'Hello',
|
|
59
|
+
sourceLang: 'EN',
|
|
60
|
+
targetLang: 'DE',
|
|
61
|
+
projectContext: 'test',
|
|
62
|
+
stringContext: null,
|
|
63
|
+
});
|
|
64
|
+
expect(messages[1].content).toContain('none');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('buildPluralMessages', () => {
|
|
69
|
+
it('builds plural messages with CLDR info', () => {
|
|
70
|
+
const messages = buildPluralMessages({
|
|
71
|
+
one: '{count} item',
|
|
72
|
+
other: '{count} items',
|
|
73
|
+
sourceLang: 'EN',
|
|
74
|
+
targetLang: 'DE',
|
|
75
|
+
projectContext: 'test',
|
|
76
|
+
stringContext: null,
|
|
77
|
+
});
|
|
78
|
+
expect(messages).toHaveLength(2);
|
|
79
|
+
expect(messages[0].content).toContain('CLDR');
|
|
80
|
+
expect(messages[1].content).toContain('{count} item');
|
|
81
|
+
expect(messages[1].content).toContain('{count} items');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('buildSinglePrompt', () => {
|
|
86
|
+
it('combines system and user into one string', () => {
|
|
87
|
+
const prompt = buildSinglePrompt({
|
|
88
|
+
sourceText: 'Hello',
|
|
89
|
+
sourceLang: 'EN',
|
|
90
|
+
targetLang: 'DE',
|
|
91
|
+
projectContext: 'A travel site',
|
|
92
|
+
stringContext: 'greeting',
|
|
93
|
+
});
|
|
94
|
+
expect(prompt).toContain('EN');
|
|
95
|
+
expect(prompt).toContain('DE');
|
|
96
|
+
expect(prompt).toContain('Hello');
|
|
97
|
+
expect(prompt).toContain('greeting');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('buildPluralSinglePrompt', () => {
|
|
102
|
+
it('combines plural system and user into one string', () => {
|
|
103
|
+
const prompt = buildPluralSinglePrompt({
|
|
104
|
+
one: '{count} item',
|
|
105
|
+
other: '{count} items',
|
|
106
|
+
sourceLang: 'EN',
|
|
107
|
+
targetLang: 'DE',
|
|
108
|
+
projectContext: 'test',
|
|
109
|
+
stringContext: null,
|
|
110
|
+
});
|
|
111
|
+
expect(prompt).toContain('CLDR');
|
|
112
|
+
expect(prompt).toContain('{count} item');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('safeRender', () => {
|
|
117
|
+
it('replaces known keys only', () => {
|
|
118
|
+
const result = safeRender('Hello {name}, you have {count} items', {
|
|
119
|
+
name: 'Tim',
|
|
120
|
+
});
|
|
121
|
+
expect(result).toBe('Hello Tim, you have {count} items');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ---- Provider router ----
|
|
127
|
+
|
|
128
|
+
describe('getProvider', () => {
|
|
129
|
+
it('returns openai provider for openai config', async () => {
|
|
130
|
+
const openaiProvider = await import('../src/providers/openai-provider.js');
|
|
131
|
+
const provider = await getProvider(makeConfig({ provider: 'openai' }));
|
|
132
|
+
expect(provider.translate).toBe(openaiProvider.translate);
|
|
133
|
+
expect(provider.translatePlural).toBe(openaiProvider.translatePlural);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('returns claude_api provider for claude_api config', async () => {
|
|
137
|
+
const claudeApiProvider = await import('../src/providers/claude-api.js');
|
|
138
|
+
const provider = await getProvider(makeConfig({ provider: 'claude_api' }));
|
|
139
|
+
expect(provider.translate).toBe(claudeApiProvider.translate);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('returns claude_code provider for claude_code config', async () => {
|
|
143
|
+
const claudeCodeProvider = await import('../src/providers/claude-code.js');
|
|
144
|
+
const provider = await getProvider(makeConfig({ provider: 'claude_code' }));
|
|
145
|
+
expect(provider.translate).toBe(claudeCodeProvider.translate);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('throws for invalid provider', async () => {
|
|
149
|
+
await expect(
|
|
150
|
+
getProvider(makeConfig({ provider: 'invalid' }))
|
|
151
|
+
).rejects.toThrow('Unknown provider');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ---- OpenAI provider ----
|
|
156
|
+
|
|
157
|
+
describe('openai-provider', () => {
|
|
158
|
+
it('translate calls OpenAI and returns result', async () => {
|
|
159
|
+
const { translate } = await import('../src/providers/openai-provider.js');
|
|
160
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
161
|
+
choices: [{ message: { content: 'Hallo' } }],
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const cfg = makeConfig();
|
|
165
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
166
|
+
|
|
167
|
+
const result = await translate(
|
|
168
|
+
'Hello', 'EN', 'DE', 'test', null, cfg,
|
|
169
|
+
{ chat: { completions: { create: mockCreate } } } as any,
|
|
170
|
+
);
|
|
171
|
+
expect(result).toBe('Hallo');
|
|
172
|
+
expect(mockCreate).toHaveBeenCalledOnce();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('translatePlural calls OpenAI and returns parsed JSON', async () => {
|
|
176
|
+
const { translatePlural } = await import('../src/providers/openai-provider.js');
|
|
177
|
+
const mockCreate = vi.fn().mockResolvedValue({
|
|
178
|
+
choices: [{ message: { content: '{"one": "{count} Nachricht", "other": "{count} Nachrichten"}' } }],
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const cfg = makeConfig();
|
|
182
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
183
|
+
|
|
184
|
+
const result = await translatePlural(
|
|
185
|
+
'{count} message', '{count} messages',
|
|
186
|
+
'EN', 'DE', 'test', null, cfg,
|
|
187
|
+
{ chat: { completions: { create: mockCreate } } } as any,
|
|
188
|
+
);
|
|
189
|
+
expect(result).toEqual({
|
|
190
|
+
one: '{count} Nachricht',
|
|
191
|
+
other: '{count} Nachrichten',
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ---- Config with provider fields ----
|
|
197
|
+
|
|
198
|
+
describe('config provider fields', () => {
|
|
199
|
+
it('defaults to openai provider', () => {
|
|
200
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'transduck-cfg-'));
|
|
201
|
+
const configPath = join(tmpDir, 'transduck.yaml');
|
|
202
|
+
writeFileSync(configPath, `
|
|
203
|
+
project:
|
|
204
|
+
name: test
|
|
205
|
+
context: "test"
|
|
206
|
+
languages:
|
|
207
|
+
source: EN
|
|
208
|
+
targets: [DE]
|
|
209
|
+
storage:
|
|
210
|
+
path: ./translations.duckdb
|
|
211
|
+
backend:
|
|
212
|
+
api_key_env: OPENAI_API_KEY
|
|
213
|
+
model: gpt-4.1-mini
|
|
214
|
+
timeout_seconds: 10
|
|
215
|
+
max_retries: 2
|
|
216
|
+
`);
|
|
217
|
+
const cfg = loadConfig(configPath);
|
|
218
|
+
expect(cfg.provider).toBe('openai');
|
|
219
|
+
expect(cfg.tokenEnv).toBe('CLAUDE_CODE_OAUTH_TOKEN');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('loads claude_api provider', () => {
|
|
223
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'transduck-cfg-'));
|
|
224
|
+
const configPath = join(tmpDir, 'transduck.yaml');
|
|
225
|
+
writeFileSync(configPath, `
|
|
226
|
+
project:
|
|
227
|
+
name: test
|
|
228
|
+
context: "test"
|
|
229
|
+
languages:
|
|
230
|
+
source: EN
|
|
231
|
+
targets: [DE]
|
|
232
|
+
storage:
|
|
233
|
+
path: ./translations.duckdb
|
|
234
|
+
backend:
|
|
235
|
+
provider: claude_api
|
|
236
|
+
api_key_env: ANTHROPIC_API_KEY
|
|
237
|
+
model: claude-haiku-4-5-20251001
|
|
238
|
+
timeout_seconds: 15
|
|
239
|
+
max_retries: 3
|
|
240
|
+
`);
|
|
241
|
+
const cfg = loadConfig(configPath);
|
|
242
|
+
expect(cfg.provider).toBe('claude_api');
|
|
243
|
+
expect(cfg.apiKeyEnv).toBe('ANTHROPIC_API_KEY');
|
|
244
|
+
expect(cfg.backendModel).toBe('claude-haiku-4-5-20251001');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('claude_code forces readOnly', () => {
|
|
248
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'transduck-cfg-'));
|
|
249
|
+
const configPath = join(tmpDir, 'transduck.yaml');
|
|
250
|
+
writeFileSync(configPath, `
|
|
251
|
+
project:
|
|
252
|
+
name: test
|
|
253
|
+
context: "test"
|
|
254
|
+
languages:
|
|
255
|
+
source: EN
|
|
256
|
+
targets: [DE]
|
|
257
|
+
storage:
|
|
258
|
+
path: ./translations.duckdb
|
|
259
|
+
backend:
|
|
260
|
+
provider: claude_code
|
|
261
|
+
token_env: CLAUDE_CODE_OAUTH_TOKEN
|
|
262
|
+
`);
|
|
263
|
+
const cfg = loadConfig(configPath);
|
|
264
|
+
expect(cfg.provider).toBe('claude_code');
|
|
265
|
+
expect(cfg.tokenEnv).toBe('CLAUDE_CODE_OAUTH_TOKEN');
|
|
266
|
+
expect(cfg.readOnly).toBe(true);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('claude_code minimal config uses defaults', () => {
|
|
270
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'transduck-cfg-'));
|
|
271
|
+
const configPath = join(tmpDir, 'transduck.yaml');
|
|
272
|
+
writeFileSync(configPath, `
|
|
273
|
+
project:
|
|
274
|
+
name: test
|
|
275
|
+
context: "test"
|
|
276
|
+
languages:
|
|
277
|
+
source: EN
|
|
278
|
+
targets: [DE]
|
|
279
|
+
storage:
|
|
280
|
+
path: ./translations.duckdb
|
|
281
|
+
backend:
|
|
282
|
+
provider: claude_code
|
|
283
|
+
`);
|
|
284
|
+
const cfg = loadConfig(configPath);
|
|
285
|
+
expect(cfg.provider).toBe('claude_code');
|
|
286
|
+
expect(cfg.readOnly).toBe(true);
|
|
287
|
+
expect(cfg.backendModel).toBe('gpt-4.1-mini');
|
|
288
|
+
});
|
|
289
|
+
});
|