transduck 0.1.5 → 0.2.2
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 +126 -103
- 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/dist/react/index.js +1 -0
- package/package.json +7 -3
- package/src/backend.ts +35 -141
- package/src/cli.ts +97 -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/src/react/index.ts +2 -0
- package/tests/ait.test.ts +3 -0
- package/tests/backend.test.ts +61 -59
- package/tests/providers.test.ts +289 -0
package/src/cli.ts
CHANGED
|
@@ -22,16 +22,41 @@ export interface InitOptions {
|
|
|
22
22
|
context: string;
|
|
23
23
|
sourceLang: string;
|
|
24
24
|
targetLangs: string[];
|
|
25
|
+
provider?: number;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export async function runInit(opts: InitOptions): Promise<string> {
|
|
28
|
-
const
|
|
29
|
+
const providerChoice = opts.provider ?? 1;
|
|
30
|
+
|
|
31
|
+
const config: Record<string, any> = {
|
|
29
32
|
project: { name: opts.name, context: opts.context },
|
|
30
33
|
languages: { source: opts.sourceLang.toUpperCase(), targets: opts.targetLangs.map(l => l.toUpperCase()) },
|
|
31
34
|
storage: { path: './translations.duckdb' },
|
|
32
|
-
backend: { api_key_env: 'OPENAI_API_KEY', model: 'gpt-4.1-mini', timeout_seconds: 10, max_retries: 2 },
|
|
33
35
|
};
|
|
34
36
|
|
|
37
|
+
if (providerChoice === 2) {
|
|
38
|
+
config.backend = {
|
|
39
|
+
provider: 'claude_api',
|
|
40
|
+
api_key_env: 'ANTHROPIC_API_KEY',
|
|
41
|
+
model: 'claude-haiku-4-5-20251001',
|
|
42
|
+
timeout_seconds: 10,
|
|
43
|
+
max_retries: 2,
|
|
44
|
+
};
|
|
45
|
+
} else if (providerChoice === 3) {
|
|
46
|
+
config.backend = {
|
|
47
|
+
provider: 'claude_code',
|
|
48
|
+
token_env: 'CLAUDE_CODE_OAUTH_TOKEN',
|
|
49
|
+
};
|
|
50
|
+
} else {
|
|
51
|
+
config.backend = {
|
|
52
|
+
provider: 'openai',
|
|
53
|
+
api_key_env: 'OPENAI_API_KEY',
|
|
54
|
+
model: 'gpt-4.1-mini',
|
|
55
|
+
timeout_seconds: 10,
|
|
56
|
+
max_retries: 2,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
35
60
|
const configPath = join(opts.dir, 'transduck.yaml');
|
|
36
61
|
writeFileSync(configPath, yamlStringify(config));
|
|
37
62
|
|
|
@@ -40,7 +65,23 @@ export async function runInit(opts: InitOptions): Promise<string> {
|
|
|
40
65
|
await store.initialize();
|
|
41
66
|
store.close();
|
|
42
67
|
|
|
43
|
-
|
|
68
|
+
const lines = [`Created ${configPath}`, `Created ${dbPath}`];
|
|
69
|
+
|
|
70
|
+
if (providerChoice === 2) {
|
|
71
|
+
lines.push('', 'Add to your .env file: ANTHROPIC_API_KEY=your-key-here');
|
|
72
|
+
} else if (providerChoice === 3) {
|
|
73
|
+
lines.push(
|
|
74
|
+
'',
|
|
75
|
+
"Run 'claude setup-token' to get your OAuth token, then add to your .env file:",
|
|
76
|
+
' CLAUDE_CODE_OAUTH_TOKEN=your-token-here',
|
|
77
|
+
'',
|
|
78
|
+
'Note: claude_code works for CLI warming only. Your app will run in read-only mode.',
|
|
79
|
+
);
|
|
80
|
+
} else {
|
|
81
|
+
lines.push('', 'Add to your .env file: OPENAI_API_KEY=your-key-here');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return lines.join('\n');
|
|
44
85
|
}
|
|
45
86
|
|
|
46
87
|
export interface TranslateOptions {
|
|
@@ -71,13 +112,10 @@ export async function runTranslate(opts: TranslateOptions): Promise<string> {
|
|
|
71
112
|
return `[cached] ${interpolateVars(cached, opts.vars)}`;
|
|
72
113
|
}
|
|
73
114
|
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
apiKey: apiKey!, model: cfg.backendModel,
|
|
79
|
-
timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
|
|
80
|
-
});
|
|
115
|
+
const translated = await backendTranslate(
|
|
116
|
+
opts.text, cfg.sourceLang, targetLang,
|
|
117
|
+
cfg.projectContext, opts.stringContext ?? null, cfg,
|
|
118
|
+
);
|
|
81
119
|
|
|
82
120
|
if (!validateTranslation(opts.text, translated)) {
|
|
83
121
|
await store.insert({
|
|
@@ -141,14 +179,11 @@ export async function runTranslatePlural(opts: TranslatePluralOptions): Promise<
|
|
|
141
179
|
|
|
142
180
|
// Cache miss — call backend
|
|
143
181
|
try {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
apiKey: apiKey!, model: cfg.backendModel,
|
|
150
|
-
timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
|
|
151
|
-
});
|
|
182
|
+
const forms = await backendTranslatePlural(
|
|
183
|
+
opts.one, opts.other,
|
|
184
|
+
cfg.sourceLang, targetLang,
|
|
185
|
+
cfg.projectContext, opts.stringContext ?? null, cfg,
|
|
186
|
+
);
|
|
152
187
|
|
|
153
188
|
// Validate each form
|
|
154
189
|
const sourcePlaceholders = new Set([
|
|
@@ -233,7 +268,6 @@ export async function runWarm(opts: WarmOptions): Promise<string> {
|
|
|
233
268
|
entries = content.split('\n').filter(l => l.trim()).map(text => ({ text: text.trim() }));
|
|
234
269
|
}
|
|
235
270
|
|
|
236
|
-
const apiKey = process.env[cfg.apiKeyEnv];
|
|
237
271
|
const projectContextHash = hash(cfg.projectContext);
|
|
238
272
|
let translated = 0, skipped = 0, failed = 0;
|
|
239
273
|
|
|
@@ -255,13 +289,11 @@ export async function runWarm(opts: WarmOptions): Promise<string> {
|
|
|
255
289
|
}
|
|
256
290
|
|
|
257
291
|
try {
|
|
258
|
-
const forms = await backendTranslatePlural(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
|
|
264
|
-
});
|
|
292
|
+
const forms = await backendTranslatePlural(
|
|
293
|
+
entry.one, entry.other,
|
|
294
|
+
cfg.sourceLang, lang,
|
|
295
|
+
cfg.projectContext, entry.context ?? null, cfg,
|
|
296
|
+
);
|
|
265
297
|
|
|
266
298
|
const sourcePlaceholders = new Set([
|
|
267
299
|
...extractPlaceholders(entry.one),
|
|
@@ -304,12 +336,10 @@ export async function runWarm(opts: WarmOptions): Promise<string> {
|
|
|
304
336
|
if (cached !== null) { skipped++; continue; }
|
|
305
337
|
|
|
306
338
|
try {
|
|
307
|
-
const result = await backendTranslate(
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
|
|
312
|
-
});
|
|
339
|
+
const result = await backendTranslate(
|
|
340
|
+
entry.text, cfg.sourceLang, lang,
|
|
341
|
+
cfg.projectContext, entry.context ?? null, cfg,
|
|
342
|
+
);
|
|
313
343
|
|
|
314
344
|
if (validateTranslation(entry.text, result)) {
|
|
315
345
|
await store.insert({
|
|
@@ -348,6 +378,7 @@ export interface ScanOptions {
|
|
|
348
378
|
export async function runScan(opts: ScanOptions): Promise<string> {
|
|
349
379
|
const cfg = loadConfig(opts.configPath);
|
|
350
380
|
const scanDirs = opts.dirs.length > 0 ? opts.dirs : [process.cwd()];
|
|
381
|
+
console.log('Scanning...');
|
|
351
382
|
const entries = scanDirectory(scanDirs);
|
|
352
383
|
|
|
353
384
|
const regular = entries.filter(e => !e.plural);
|
|
@@ -391,25 +422,31 @@ export async function runScan(opts: ScanOptions): Promise<string> {
|
|
|
391
422
|
|
|
392
423
|
// Warm
|
|
393
424
|
if (opts.warm) {
|
|
425
|
+
if (entries.length === 0) {
|
|
426
|
+
lines.push('\nNothing to warm — no translatable strings found.');
|
|
427
|
+
} else {
|
|
394
428
|
const targetLangs = opts.langs && opts.langs.length > 0
|
|
395
429
|
? opts.langs.map(l => l.toUpperCase())
|
|
396
430
|
: cfg.targetLangs;
|
|
397
431
|
|
|
398
432
|
const store = new TranslationStore(cfg.storagePath);
|
|
399
433
|
await store.initialize();
|
|
400
|
-
const apiKey = process.env[cfg.apiKeyEnv];
|
|
401
434
|
const projectContextHash = hash(cfg.projectContext);
|
|
402
435
|
|
|
403
436
|
let translated = 0;
|
|
404
437
|
let skipped = 0;
|
|
405
438
|
let failed = 0;
|
|
439
|
+
const total = entries.length * targetLangs.length;
|
|
440
|
+
let done = 0;
|
|
406
441
|
|
|
407
442
|
for (const entry of entries) {
|
|
408
443
|
if (entry.plural) {
|
|
409
444
|
const sourceKey = entry.one + '\x00' + entry.other;
|
|
410
445
|
const stringContextHash = hash(entry.context ?? '');
|
|
446
|
+
const label = (entry.one ?? '').slice(0, 40);
|
|
411
447
|
|
|
412
448
|
for (const lang of targetLangs) {
|
|
449
|
+
done++;
|
|
413
450
|
const cachedForms = await store.lookupPlural({
|
|
414
451
|
sourceText: sourceKey, sourceLang: cfg.sourceLang, targetLang: lang,
|
|
415
452
|
projectContextHash, stringContextHash,
|
|
@@ -417,17 +454,16 @@ export async function runScan(opts: ScanOptions): Promise<string> {
|
|
|
417
454
|
|
|
418
455
|
if (Object.keys(cachedForms).length > 0) {
|
|
419
456
|
skipped++;
|
|
457
|
+
console.log(` [${done}/${total}] ${lang} skipped (cached): ${label}`);
|
|
420
458
|
continue;
|
|
421
459
|
}
|
|
422
460
|
|
|
423
461
|
try {
|
|
424
|
-
const forms = await backendTranslatePlural(
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
|
|
430
|
-
});
|
|
462
|
+
const forms = await backendTranslatePlural(
|
|
463
|
+
entry.one!, entry.other!,
|
|
464
|
+
cfg.sourceLang, lang,
|
|
465
|
+
cfg.projectContext, entry.context ?? null, cfg,
|
|
466
|
+
);
|
|
431
467
|
|
|
432
468
|
for (const [cat, translatedText] of Object.entries(forms)) {
|
|
433
469
|
await store.insertPlural({
|
|
@@ -438,14 +474,18 @@ export async function runScan(opts: ScanOptions): Promise<string> {
|
|
|
438
474
|
});
|
|
439
475
|
}
|
|
440
476
|
translated++;
|
|
477
|
+
console.log(` [${done}/${total}] ${lang} translated: ${label}`);
|
|
441
478
|
} catch {
|
|
442
479
|
failed++;
|
|
480
|
+
console.log(` [${done}/${total}] ${lang} failed: ${label}`);
|
|
443
481
|
}
|
|
444
482
|
}
|
|
445
483
|
} else {
|
|
446
484
|
const stringContextHash = hash(entry.context ?? '');
|
|
485
|
+
const label = (entry.text ?? '').slice(0, 40);
|
|
447
486
|
|
|
448
487
|
for (const lang of targetLangs) {
|
|
488
|
+
done++;
|
|
449
489
|
const cached = await store.lookup({
|
|
450
490
|
sourceText: entry.text!, sourceLang: cfg.sourceLang, targetLang: lang,
|
|
451
491
|
projectContextHash, stringContextHash,
|
|
@@ -453,16 +493,15 @@ export async function runScan(opts: ScanOptions): Promise<string> {
|
|
|
453
493
|
|
|
454
494
|
if (cached !== null) {
|
|
455
495
|
skipped++;
|
|
496
|
+
console.log(` [${done}/${total}] ${lang} skipped (cached): ${label}`);
|
|
456
497
|
continue;
|
|
457
498
|
}
|
|
458
499
|
|
|
459
500
|
try {
|
|
460
|
-
const result = await backendTranslate(
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
|
|
465
|
-
});
|
|
501
|
+
const result = await backendTranslate(
|
|
502
|
+
entry.text!, cfg.sourceLang, lang,
|
|
503
|
+
cfg.projectContext, entry.context ?? null, cfg,
|
|
504
|
+
);
|
|
466
505
|
|
|
467
506
|
if (validateTranslation(entry.text!, result)) {
|
|
468
507
|
await store.insert({
|
|
@@ -471,11 +510,14 @@ export async function runScan(opts: ScanOptions): Promise<string> {
|
|
|
471
510
|
translatedText: result, model: cfg.backendModel, status: 'translated',
|
|
472
511
|
});
|
|
473
512
|
translated++;
|
|
513
|
+
console.log(` [${done}/${total}] ${lang} translated: ${label}`);
|
|
474
514
|
} else {
|
|
475
515
|
failed++;
|
|
516
|
+
console.log(` [${done}/${total}] ${lang} failed: ${label}`);
|
|
476
517
|
}
|
|
477
518
|
} catch {
|
|
478
519
|
failed++;
|
|
520
|
+
console.log(` [${done}/${total}] ${lang} failed: ${label}`);
|
|
479
521
|
}
|
|
480
522
|
}
|
|
481
523
|
}
|
|
@@ -483,6 +525,7 @@ export async function runScan(opts: ScanOptions): Promise<string> {
|
|
|
483
525
|
|
|
484
526
|
store.close();
|
|
485
527
|
lines.push(`\nTranslated: ${translated} | Skipped: ${skipped} | Failed: ${failed}`);
|
|
528
|
+
} // end if entries.length > 0
|
|
486
529
|
}
|
|
487
530
|
|
|
488
531
|
if (!opts.warm && !opts.outputPath) {
|
|
@@ -533,11 +576,19 @@ program.command('init')
|
|
|
533
576
|
const context = await ask('Project context: ');
|
|
534
577
|
const sourceLang = await ask('Source language (e.g. EN): ');
|
|
535
578
|
const targetsRaw = await ask('Target languages (comma-separated): ');
|
|
579
|
+
|
|
580
|
+
console.log('\nTranslation provider:');
|
|
581
|
+
console.log(' 1. OpenAI (requires API key)');
|
|
582
|
+
console.log(' 2. Claude API (requires API key)');
|
|
583
|
+
console.log(' 3. Claude Code (uses your Claude Code subscription, warming only)');
|
|
584
|
+
const providerRaw = await ask('Select provider [1]: ');
|
|
585
|
+
const providerChoice = parseInt(providerRaw, 10) || 1;
|
|
536
586
|
rl.close();
|
|
537
587
|
|
|
538
588
|
const output = await runInit({
|
|
539
589
|
dir, name, context, sourceLang,
|
|
540
590
|
targetLangs: targetsRaw.split(',').map(s => s.trim()),
|
|
591
|
+
provider: providerChoice,
|
|
541
592
|
});
|
|
542
593
|
console.log(output);
|
|
543
594
|
|
package/src/config.ts
CHANGED
|
@@ -10,7 +10,9 @@ export interface TransduckConfig {
|
|
|
10
10
|
sourceLang: string;
|
|
11
11
|
targetLangs: string[];
|
|
12
12
|
storagePath: string;
|
|
13
|
+
provider: string;
|
|
13
14
|
apiKeyEnv: string;
|
|
15
|
+
tokenEnv: string;
|
|
14
16
|
backendModel: string;
|
|
15
17
|
backendTimeout: number;
|
|
16
18
|
backendMaxRetries: number;
|
|
@@ -49,16 +51,31 @@ export function loadConfig(path?: string): TransduckConfig {
|
|
|
49
51
|
const configDir = dirname(configPath);
|
|
50
52
|
const storagePath = resolve(configDir, raw.storage.path);
|
|
51
53
|
|
|
54
|
+
const backend = raw.backend ?? {};
|
|
55
|
+
const provider = backend.provider ?? 'openai';
|
|
56
|
+
const apiKeyEnv = backend.api_key_env ?? 'OPENAI_API_KEY';
|
|
57
|
+
const tokenEnv = backend.token_env ?? 'CLAUDE_CODE_OAUTH_TOKEN';
|
|
58
|
+
const backendModel = backend.model ?? 'gpt-4.1-mini';
|
|
59
|
+
const backendTimeout = backend.timeout_seconds ?? 10;
|
|
60
|
+
const backendMaxRetries = backend.max_retries ?? 2;
|
|
61
|
+
|
|
62
|
+
let readOnly = raw.runtime?.read_only ?? false;
|
|
63
|
+
if (provider === 'claude_code') {
|
|
64
|
+
readOnly = true;
|
|
65
|
+
}
|
|
66
|
+
|
|
52
67
|
return {
|
|
53
68
|
projectName: raw.project.name,
|
|
54
69
|
projectContext: raw.project.context,
|
|
55
70
|
sourceLang: String(raw.languages.source).toUpperCase(),
|
|
56
71
|
targetLangs: raw.languages.targets.map((l: string) => String(l).toUpperCase()),
|
|
57
72
|
storagePath,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
provider,
|
|
74
|
+
apiKeyEnv,
|
|
75
|
+
tokenEnv,
|
|
76
|
+
backendModel,
|
|
77
|
+
backendTimeout,
|
|
78
|
+
backendMaxRetries,
|
|
79
|
+
readOnly,
|
|
63
80
|
};
|
|
64
81
|
}
|
package/src/handler.ts
CHANGED
|
@@ -58,7 +58,6 @@ export async function handleTranslationRequest(
|
|
|
58
58
|
const targetLang = body.language.toUpperCase();
|
|
59
59
|
|
|
60
60
|
const projectContextHash = hash(cfg.projectContext);
|
|
61
|
-
const apiKey = process.env[cfg.apiKeyEnv];
|
|
62
61
|
|
|
63
62
|
const translations: Record<string, string> = {};
|
|
64
63
|
const plurals: Record<string, Record<string, string>> = {};
|
|
@@ -84,17 +83,14 @@ export async function handleTranslationRequest(
|
|
|
84
83
|
|
|
85
84
|
// Backend call
|
|
86
85
|
try {
|
|
87
|
-
const translated = await backendTranslate(
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
const translated = await backendTranslate(
|
|
87
|
+
item.text,
|
|
88
|
+
cfg.sourceLang,
|
|
90
89
|
targetLang,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
timeout: cfg.backendTimeout,
|
|
96
|
-
maxRetries: cfg.backendMaxRetries,
|
|
97
|
-
});
|
|
90
|
+
cfg.projectContext,
|
|
91
|
+
item.context ?? null,
|
|
92
|
+
cfg,
|
|
93
|
+
);
|
|
98
94
|
|
|
99
95
|
if (validateTranslation(item.text, translated)) {
|
|
100
96
|
await store.insert({
|
|
@@ -138,18 +134,15 @@ export async function handleTranslationRequest(
|
|
|
138
134
|
|
|
139
135
|
// Backend call
|
|
140
136
|
try {
|
|
141
|
-
const forms = await backendTranslatePlural(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
137
|
+
const forms = await backendTranslatePlural(
|
|
138
|
+
item.one,
|
|
139
|
+
item.other,
|
|
140
|
+
cfg.sourceLang,
|
|
145
141
|
targetLang,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
timeout: cfg.backendTimeout,
|
|
151
|
-
maxRetries: cfg.backendMaxRetries,
|
|
152
|
-
});
|
|
142
|
+
cfg.projectContext,
|
|
143
|
+
item.context ?? null,
|
|
144
|
+
cfg,
|
|
145
|
+
);
|
|
153
146
|
|
|
154
147
|
for (const [cat, translatedText] of Object.entries(forms)) {
|
|
155
148
|
await store.insertPlural({
|
package/src/index.ts
CHANGED
|
@@ -92,14 +92,11 @@ export async function ait(
|
|
|
92
92
|
});
|
|
93
93
|
if (rechecked !== null) return rechecked;
|
|
94
94
|
|
|
95
|
-
const apiKey = process.env[cfg.apiKeyEnv];
|
|
96
95
|
try {
|
|
97
|
-
const translated = await backendTranslate(
|
|
98
|
-
sourceText,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
|
|
102
|
-
});
|
|
96
|
+
const translated = await backendTranslate(
|
|
97
|
+
sourceText, cfg.sourceLang, state.targetLang!,
|
|
98
|
+
cfg.projectContext, context ?? null, cfg,
|
|
99
|
+
);
|
|
103
100
|
|
|
104
101
|
if (!validateTranslation(sourceText, translated)) {
|
|
105
102
|
console.warn(`[transduck] Validation failed for: ${sourceText} -> ${translated}`);
|
|
@@ -192,15 +189,12 @@ export async function aitPlural(
|
|
|
192
189
|
}
|
|
193
190
|
|
|
194
191
|
// Cache miss — call backend
|
|
195
|
-
const apiKey = process.env[cfg.apiKeyEnv];
|
|
196
192
|
try {
|
|
197
|
-
const forms = await backendTranslatePlural(
|
|
193
|
+
const forms = await backendTranslatePlural(
|
|
198
194
|
one, other,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
|
|
203
|
-
});
|
|
195
|
+
cfg.sourceLang, state.targetLang,
|
|
196
|
+
cfg.projectContext, context ?? null, cfg,
|
|
197
|
+
);
|
|
204
198
|
|
|
205
199
|
// Validate and store each form
|
|
206
200
|
const validCategories = new Set(['zero', 'one', 'two', 'few', 'many', 'other']);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude API translation provider.
|
|
3
|
+
* Uses the @anthropic-ai/sdk package (optional peer dependency, lazy imported).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { TransduckConfig } from '../config.js';
|
|
7
|
+
import { buildMessages, buildPluralMessages } from './prompts.js';
|
|
8
|
+
|
|
9
|
+
async function getClient(config: TransduckConfig) {
|
|
10
|
+
let Anthropic: any;
|
|
11
|
+
try {
|
|
12
|
+
// @ts-ignore — optional peer dependency, may not be installed
|
|
13
|
+
const mod = await import('@anthropic-ai/sdk');
|
|
14
|
+
Anthropic = mod.default ?? mod.Anthropic;
|
|
15
|
+
} catch {
|
|
16
|
+
throw new Error(
|
|
17
|
+
'Install the required package: npm install @anthropic-ai/sdk'
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const apiKey = process.env[config.apiKeyEnv];
|
|
22
|
+
if (!apiKey) {
|
|
23
|
+
throw new Error(`Set ${config.apiKeyEnv} environment variable`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return new Anthropic({ apiKey });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function translate(
|
|
30
|
+
sourceText: string,
|
|
31
|
+
sourceLang: string,
|
|
32
|
+
targetLang: string,
|
|
33
|
+
projectContext: string,
|
|
34
|
+
stringContext: string | null,
|
|
35
|
+
config: TransduckConfig,
|
|
36
|
+
): Promise<string> {
|
|
37
|
+
const client = await getClient(config);
|
|
38
|
+
const messages = buildMessages({
|
|
39
|
+
sourceText, sourceLang, targetLang, projectContext, stringContext,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const response = await client.messages.create({
|
|
43
|
+
model: config.backendModel,
|
|
44
|
+
max_tokens: 1024,
|
|
45
|
+
temperature: 0.3,
|
|
46
|
+
system: messages[0].content,
|
|
47
|
+
messages: [{ role: 'user', content: messages[1].content }],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return response.content[0].text.trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function translatePlural(
|
|
54
|
+
one: string,
|
|
55
|
+
other: string,
|
|
56
|
+
sourceLang: string,
|
|
57
|
+
targetLang: string,
|
|
58
|
+
projectContext: string,
|
|
59
|
+
stringContext: string | null,
|
|
60
|
+
config: TransduckConfig,
|
|
61
|
+
): Promise<Record<string, string>> {
|
|
62
|
+
const client = await getClient(config);
|
|
63
|
+
const messages = buildPluralMessages({
|
|
64
|
+
one, other, sourceLang, targetLang, projectContext, stringContext,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const response = await client.messages.create({
|
|
68
|
+
model: config.backendModel,
|
|
69
|
+
max_tokens: 2048,
|
|
70
|
+
temperature: 0.3,
|
|
71
|
+
system: messages[0].content,
|
|
72
|
+
messages: [{ role: 'user', content: messages[1].content }],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const raw = response.content[0].text.trim();
|
|
76
|
+
return JSON.parse(raw);
|
|
77
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code translation provider (uses Claude Agent SDK with subscription auth).
|
|
3
|
+
* Uses the @anthropic-ai/claude-agent-sdk package (optional peer dependency, lazy imported).
|
|
4
|
+
* Already async in JS — no asyncio bridge needed like Python.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { TransduckConfig } from '../config.js';
|
|
8
|
+
import { buildSinglePrompt, buildPluralSinglePrompt } from './prompts.js';
|
|
9
|
+
|
|
10
|
+
function ensureToken(config: TransduckConfig): void {
|
|
11
|
+
const token = process.env[config.tokenEnv];
|
|
12
|
+
if (!token) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`Set ${config.tokenEnv} \u2014 run 'claude setup-token' to get your OAuth token`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function translateWithSdk(prompt: string): Promise<string> {
|
|
20
|
+
let query: any;
|
|
21
|
+
try {
|
|
22
|
+
// @ts-ignore — optional peer dependency, may not be installed
|
|
23
|
+
const mod = await import('@anthropic-ai/claude-agent-sdk');
|
|
24
|
+
query = mod.query;
|
|
25
|
+
} catch {
|
|
26
|
+
throw new Error(
|
|
27
|
+
'Install the required package: npm install @anthropic-ai/claude-agent-sdk'
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for await (const message of query({
|
|
32
|
+
prompt,
|
|
33
|
+
options: { allowedTools: [] },
|
|
34
|
+
})) {
|
|
35
|
+
if ('result' in message) {
|
|
36
|
+
return (message as any).result.trim();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
throw new Error('No result from Claude Agent SDK');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function translate(
|
|
43
|
+
sourceText: string,
|
|
44
|
+
sourceLang: string,
|
|
45
|
+
targetLang: string,
|
|
46
|
+
projectContext: string,
|
|
47
|
+
stringContext: string | null,
|
|
48
|
+
config: TransduckConfig,
|
|
49
|
+
): Promise<string> {
|
|
50
|
+
ensureToken(config);
|
|
51
|
+
const prompt = buildSinglePrompt({
|
|
52
|
+
sourceText, sourceLang, targetLang, projectContext, stringContext,
|
|
53
|
+
});
|
|
54
|
+
return translateWithSdk(prompt);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function translatePlural(
|
|
58
|
+
one: string,
|
|
59
|
+
other: string,
|
|
60
|
+
sourceLang: string,
|
|
61
|
+
targetLang: string,
|
|
62
|
+
projectContext: string,
|
|
63
|
+
stringContext: string | null,
|
|
64
|
+
config: TransduckConfig,
|
|
65
|
+
): Promise<Record<string, string>> {
|
|
66
|
+
ensureToken(config);
|
|
67
|
+
const prompt = buildPluralSinglePrompt({
|
|
68
|
+
one, other, sourceLang, targetLang, projectContext, stringContext,
|
|
69
|
+
});
|
|
70
|
+
const raw = await translateWithSdk(prompt);
|
|
71
|
+
return JSON.parse(raw);
|
|
72
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation provider abstraction.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { TransduckConfig } from '../config.js';
|
|
6
|
+
|
|
7
|
+
export interface TranslationProvider {
|
|
8
|
+
translate(
|
|
9
|
+
sourceText: string,
|
|
10
|
+
sourceLang: string,
|
|
11
|
+
targetLang: string,
|
|
12
|
+
projectContext: string,
|
|
13
|
+
stringContext: string | null,
|
|
14
|
+
config: TransduckConfig,
|
|
15
|
+
_clientOverride?: any,
|
|
16
|
+
): Promise<string>;
|
|
17
|
+
|
|
18
|
+
translatePlural(
|
|
19
|
+
one: string,
|
|
20
|
+
other: string,
|
|
21
|
+
sourceLang: string,
|
|
22
|
+
targetLang: string,
|
|
23
|
+
projectContext: string,
|
|
24
|
+
stringContext: string | null,
|
|
25
|
+
config: TransduckConfig,
|
|
26
|
+
_clientOverride?: any,
|
|
27
|
+
): Promise<Record<string, string>>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Return the provider module for the configured provider.
|
|
32
|
+
*/
|
|
33
|
+
export async function getProvider(config: TransduckConfig): Promise<TranslationProvider> {
|
|
34
|
+
if (config.provider === 'openai') {
|
|
35
|
+
return await import('./openai-provider.js');
|
|
36
|
+
} else if (config.provider === 'claude_api') {
|
|
37
|
+
return await import('./claude-api.js');
|
|
38
|
+
} else if (config.provider === 'claude_code') {
|
|
39
|
+
return await import('./claude-code.js');
|
|
40
|
+
} else {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Unknown provider: ${config.provider}. Valid: openai, claude_api, claude_code`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|