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/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 config = {
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
- return `Created ${configPath}\nCreated ${dbPath}`;
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 apiKey = process.env[cfg.apiKeyEnv];
75
- const translated = await backendTranslate({
76
- sourceText: opts.text, sourceLang: cfg.sourceLang, targetLang,
77
- projectContext: cfg.projectContext, stringContext: opts.stringContext ?? null,
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 apiKey = process.env[cfg.apiKeyEnv];
145
- const forms = await backendTranslatePlural({
146
- one: opts.one, other: opts.other,
147
- sourceLang: cfg.sourceLang, targetLang,
148
- projectContext: cfg.projectContext, stringContext: opts.stringContext ?? null,
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
- one: entry.one, other: entry.other,
260
- sourceLang: cfg.sourceLang, targetLang: lang,
261
- projectContext: cfg.projectContext, stringContext: entry.context ?? null,
262
- apiKey: apiKey!, model: cfg.backendModel,
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
- sourceText: entry.text, sourceLang: cfg.sourceLang, targetLang: lang,
309
- projectContext: cfg.projectContext, stringContext: entry.context ?? null,
310
- apiKey: apiKey!, model: cfg.backendModel,
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
- one: entry.one!, other: entry.other!,
426
- sourceLang: cfg.sourceLang, targetLang: lang,
427
- projectContext: cfg.projectContext, stringContext: entry.context ?? null,
428
- apiKey: apiKey!, model: cfg.backendModel,
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
- sourceText: entry.text!, sourceLang: cfg.sourceLang, targetLang: lang,
462
- projectContext: cfg.projectContext, stringContext: entry.context ?? null,
463
- apiKey: apiKey!, model: cfg.backendModel,
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
- apiKeyEnv: raw.backend.api_key_env,
59
- backendModel: raw.backend.model,
60
- backendTimeout: raw.backend.timeout_seconds,
61
- backendMaxRetries: raw.backend.max_retries,
62
- readOnly: raw.runtime?.read_only ?? false,
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
- sourceText: item.text,
89
- sourceLang: cfg.sourceLang,
86
+ const translated = await backendTranslate(
87
+ item.text,
88
+ cfg.sourceLang,
90
89
  targetLang,
91
- projectContext: cfg.projectContext,
92
- stringContext: item.context ?? null,
93
- apiKey: apiKey!,
94
- model: cfg.backendModel,
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
- one: item.one,
143
- other: item.other,
144
- sourceLang: cfg.sourceLang,
137
+ const forms = await backendTranslatePlural(
138
+ item.one,
139
+ item.other,
140
+ cfg.sourceLang,
145
141
  targetLang,
146
- projectContext: cfg.projectContext,
147
- stringContext: item.context ?? null,
148
- apiKey: apiKey!,
149
- model: cfg.backendModel,
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, sourceLang: cfg.sourceLang, targetLang: state.targetLang!,
99
- projectContext: cfg.projectContext, stringContext: context ?? null,
100
- apiKey: apiKey!, model: cfg.backendModel,
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
- sourceLang: cfg.sourceLang, targetLang: state.targetLang,
200
- projectContext: cfg.projectContext, stringContext: context ?? null,
201
- apiKey: apiKey!, model: cfg.backendModel,
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
+ }