transduck 0.1.5 → 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.
@@ -0,0 +1,40 @@
1
+ /**
2
+ * OpenAI translation provider.
3
+ */
4
+ import OpenAI from 'openai';
5
+ import { buildMessages, buildPluralMessages } from './prompts.js';
6
+ export async function translate(sourceText, sourceLang, targetLang, projectContext, stringContext, config, _clientOverride) {
7
+ const apiKey = process.env[config.apiKeyEnv];
8
+ const client = _clientOverride ?? new OpenAI({
9
+ apiKey,
10
+ timeout: config.backendTimeout * 1000,
11
+ maxRetries: config.backendMaxRetries,
12
+ });
13
+ const messages = buildMessages({
14
+ sourceText, sourceLang, targetLang, projectContext, stringContext,
15
+ });
16
+ const response = await client.chat.completions.create({
17
+ model: config.backendModel,
18
+ messages,
19
+ temperature: 0.3,
20
+ });
21
+ return response.choices[0].message.content.trim();
22
+ }
23
+ export async function translatePlural(one, other, sourceLang, targetLang, projectContext, stringContext, config, _clientOverride) {
24
+ const apiKey = process.env[config.apiKeyEnv];
25
+ const client = _clientOverride ?? new OpenAI({
26
+ apiKey,
27
+ timeout: config.backendTimeout * 1000,
28
+ maxRetries: config.backendMaxRetries,
29
+ });
30
+ const messages = buildPluralMessages({
31
+ one, other, sourceLang, targetLang, projectContext, stringContext,
32
+ });
33
+ const response = await client.chat.completions.create({
34
+ model: config.backendModel,
35
+ messages,
36
+ temperature: 0.3,
37
+ });
38
+ const raw = response.choices[0].message.content.trim();
39
+ return JSON.parse(raw);
40
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Safe string replacement that only replaces known keys.
3
+ * Does NOT use str.replace with regex — avoids issues with ${value} patterns.
4
+ */
5
+ export declare function safeRender(template: string, vars: Record<string, string>): string;
6
+ export interface BuildMessagesParams {
7
+ sourceText: string;
8
+ sourceLang: string;
9
+ targetLang: string;
10
+ projectContext: string;
11
+ stringContext: string | null;
12
+ }
13
+ export interface BuildPluralMessagesParams {
14
+ one: string;
15
+ other: string;
16
+ sourceLang: string;
17
+ targetLang: string;
18
+ projectContext: string;
19
+ stringContext: string | null;
20
+ }
21
+ /**
22
+ * Build system + user messages for chat-based providers (OpenAI, Claude API).
23
+ */
24
+ export declare function buildMessages(params: BuildMessagesParams): Array<{
25
+ role: string;
26
+ content: string;
27
+ }>;
28
+ /**
29
+ * Build system + user messages for plural chat-based providers.
30
+ */
31
+ export declare function buildPluralMessages(params: BuildPluralMessagesParams): Array<{
32
+ role: string;
33
+ content: string;
34
+ }>;
35
+ /**
36
+ * Build a single combined prompt for non-chat providers (Claude Code).
37
+ */
38
+ export declare function buildSinglePrompt(params: BuildMessagesParams): string;
39
+ /**
40
+ * Build a single combined plural prompt for non-chat providers.
41
+ */
42
+ export declare function buildPluralSinglePrompt(params: BuildPluralMessagesParams): string;
@@ -0,0 +1,89 @@
1
+ /* eslint-disable no-template-curly-in-string */
2
+ /**
3
+ * Shared prompt templates for all translation providers.
4
+ *
5
+ * IMPORTANT: Uses string concatenation (NOT template literals) because the
6
+ * prompt text contains ${value} patterns that must be preserved literally.
7
+ */
8
+ const SYSTEM_TEMPLATE = 'You are a professional translator. Translate the given text from {source_lang} ' +
9
+ 'to {target_lang}. Return ONLY the translated text, nothing else. Preserve any ' +
10
+ 'placeholders like {name}, {{count}}, %s, ${value} exactly as they appear. ' +
11
+ 'Preserve brand names. Match the tone and formality of the original.\n\n' +
12
+ 'Project context: {project_context}';
13
+ const USER_TEMPLATE = 'Translate: "{source_text}"\nString context: {string_context}';
14
+ const PLURAL_SYSTEM_TEMPLATE = 'You are a professional translator. You will be given two plural forms ' +
15
+ '(one and other) in {source_lang}. Generate ALL plural forms needed in ' +
16
+ '{target_lang} according to CLDR plural rules. Return ONLY a JSON object ' +
17
+ 'mapping plural categories to translated strings. Do not include explanation.\n\n' +
18
+ 'CRITICAL: Placeholders like {count}, {name}, %s, ${value} are SOFTWARE VARIABLES ' +
19
+ 'that will be replaced by code at runtime. You MUST include them exactly as written ' +
20
+ 'in EVERY plural form, even for zero, one, and two categories where the language ' +
21
+ 'would not normally use a numeral. For example, for Arabic zero: use "{count} ..." ' +
22
+ 'not "\u0644\u0627 \u062a\u0648\u062c\u062f ...". For Arabic one: use "{count} ..." not "\u0648\u0627\u062d\u062f\u0629 ...".\n\n' +
23
+ 'Preserve brand names. Match the tone and formality of the original.\n\n' +
24
+ 'CLDR plural categories are: zero, one, two, few, many, other.\n' +
25
+ 'Only include categories that {target_lang} actually uses.\n\n' +
26
+ 'Project context: {project_context}';
27
+ const PLURAL_USER_TEMPLATE = 'Source one form: "{one}"\n' +
28
+ 'Source other form: "{other}"\n' +
29
+ 'String context: {string_context}';
30
+ /**
31
+ * Safe string replacement that only replaces known keys.
32
+ * Does NOT use str.replace with regex — avoids issues with ${value} patterns.
33
+ */
34
+ export function safeRender(template, vars) {
35
+ let result = template;
36
+ for (const [key, value] of Object.entries(vars)) {
37
+ result = result.replaceAll('{' + key + '}', value);
38
+ }
39
+ return result;
40
+ }
41
+ /**
42
+ * Build system + user messages for chat-based providers (OpenAI, Claude API).
43
+ */
44
+ export function buildMessages(params) {
45
+ const systemMsg = SYSTEM_TEMPLATE
46
+ .replace('{source_lang}', params.sourceLang)
47
+ .replace('{target_lang}', params.targetLang)
48
+ .replace('{project_context}', params.projectContext);
49
+ const userMsg = USER_TEMPLATE
50
+ .replace('{source_text}', params.sourceText)
51
+ .replace('{string_context}', params.stringContext || 'none');
52
+ return [
53
+ { role: 'system', content: systemMsg },
54
+ { role: 'user', content: userMsg },
55
+ ];
56
+ }
57
+ /**
58
+ * Build system + user messages for plural chat-based providers.
59
+ */
60
+ export function buildPluralMessages(params) {
61
+ const systemMsg = safeRender(PLURAL_SYSTEM_TEMPLATE, {
62
+ source_lang: params.sourceLang,
63
+ target_lang: params.targetLang,
64
+ project_context: params.projectContext,
65
+ });
66
+ const userMsg = safeRender(PLURAL_USER_TEMPLATE, {
67
+ one: params.one,
68
+ other: params.other,
69
+ string_context: params.stringContext || 'none',
70
+ });
71
+ return [
72
+ { role: 'system', content: systemMsg },
73
+ { role: 'user', content: userMsg },
74
+ ];
75
+ }
76
+ /**
77
+ * Build a single combined prompt for non-chat providers (Claude Code).
78
+ */
79
+ export function buildSinglePrompt(params) {
80
+ const messages = buildMessages(params);
81
+ return messages[0].content + '\n\n' + messages[1].content;
82
+ }
83
+ /**
84
+ * Build a single combined plural prompt for non-chat providers.
85
+ */
86
+ export function buildPluralSinglePrompt(params) {
87
+ const messages = buildPluralMessages(params);
88
+ return messages[0].content + '\n\n' + messages[1].content;
89
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "transduck",
3
- "version": "0.1.5",
3
+ "version": "0.2.1",
4
4
  "description": "AI-native translation tool using source text as keys",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -42,10 +42,14 @@
42
42
  },
43
43
  "peerDependencies": {
44
44
  "react": ">=18.0.0",
45
- "react-dom": ">=18.0.0"
45
+ "react-dom": ">=18.0.0",
46
+ "@anthropic-ai/sdk": ">=0.30.0",
47
+ "@anthropic-ai/claude-agent-sdk": ">=0.1.0"
46
48
  },
47
49
  "peerDependenciesMeta": {
48
50
  "react": { "optional": true },
49
- "react-dom": { "optional": true }
51
+ "react-dom": { "optional": true },
52
+ "@anthropic-ai/sdk": { "optional": true },
53
+ "@anthropic-ai/claude-agent-sdk": { "optional": true }
50
54
  }
51
55
  }
package/src/backend.ts CHANGED
@@ -1,142 +1,36 @@
1
- import OpenAI from 'openai';
2
-
3
- /* eslint-disable no-template-curly-in-string */
4
- const SYSTEM_TEMPLATE =
5
- 'You are a professional translator. Translate the given text from {source_lang} ' +
6
- 'to {target_lang}. Return ONLY the translated text, nothing else. Preserve any ' +
7
- 'placeholders like {name}, {{count}}, %s, ${value} exactly as they appear. ' +
8
- 'Preserve brand names. Match the tone and formality of the original.\n\n' +
9
- 'Project context: {project_context}';
10
-
11
- const USER_TEMPLATE = `Translate: "{source_text}"\nString context: {string_context}`;
12
-
13
- const PLURAL_SYSTEM_TEMPLATE =
14
- 'You are a professional translator. You will be given two plural forms ' +
15
- '(one and other) in {source_lang}. Generate ALL plural forms needed in ' +
16
- '{target_lang} according to CLDR plural rules. Return ONLY a JSON object ' +
17
- 'mapping plural categories to translated strings. Do not include explanation.\n\n' +
18
- 'CRITICAL: Placeholders like {count}, {name}, %s, ${value} are SOFTWARE VARIABLES ' +
19
- 'that will be replaced by code at runtime. You MUST include them exactly as written ' +
20
- 'in EVERY plural form, even for zero, one, and two categories where the language ' +
21
- 'would not normally use a numeral. For example, for Arabic zero: use "{count} ..." ' +
22
- 'not "لا توجد ...". For Arabic one: use "{count} ..." not "واحدة ...".\n\n' +
23
- 'Preserve brand names. Match the tone and formality of the original.\n\n' +
24
- 'CLDR plural categories are: zero, one, two, few, many, other.\n' +
25
- 'Only include categories that {target_lang} actually uses.\n\n' +
26
- 'Project context: {project_context}';
27
-
28
- const PLURAL_USER_TEMPLATE =
29
- 'Source one form: "{one}"\n' +
30
- 'Source other form: "{other}"\n' +
31
- 'String context: {string_context}';
32
-
33
- function safeRender(template: string, vars: Record<string, string>): string {
34
- let result = template;
35
- for (const [key, value] of Object.entries(vars)) {
36
- result = result.replaceAll(`{${key}}`, value);
37
- }
38
- return result;
39
- }
40
-
41
- interface BuildMessagesParams {
42
- sourceText: string;
43
- sourceLang: string;
44
- targetLang: string;
45
- projectContext: string;
46
- stringContext: string | null;
47
- }
48
-
49
- interface TranslateParams extends BuildMessagesParams {
50
- apiKey: string;
51
- model: string;
52
- timeout: number;
53
- maxRetries: number;
54
- _clientOverride?: any;
55
- }
56
-
57
- interface BuildPluralMessagesParams {
58
- one: string;
59
- other: string;
60
- sourceLang: string;
61
- targetLang: string;
62
- projectContext: string;
63
- stringContext: string | null;
64
- }
65
-
66
- interface TranslatePluralParams extends BuildPluralMessagesParams {
67
- apiKey: string;
68
- model: string;
69
- timeout: number;
70
- maxRetries: number;
71
- _clientOverride?: any;
72
- }
73
-
74
- export function buildMessages(params: BuildMessagesParams): Array<{ role: string; content: string }> {
75
- const systemMsg = SYSTEM_TEMPLATE
76
- .replace('{source_lang}', params.sourceLang)
77
- .replace('{target_lang}', params.targetLang)
78
- .replace('{project_context}', params.projectContext);
79
-
80
- const userMsg = USER_TEMPLATE
81
- .replace('{source_text}', params.sourceText)
82
- .replace('{string_context}', params.stringContext || 'none');
83
-
84
- return [
85
- { role: 'system', content: systemMsg },
86
- { role: 'user', content: userMsg },
87
- ];
88
- }
89
-
90
- export async function translate(params: TranslateParams): Promise<string> {
91
- const client = params._clientOverride ?? new OpenAI({
92
- apiKey: params.apiKey,
93
- timeout: params.timeout * 1000,
94
- maxRetries: params.maxRetries,
95
- });
96
-
97
- const messages = buildMessages(params);
98
- const response = await client.chat.completions.create({
99
- model: params.model,
100
- messages,
101
- temperature: 0.3,
102
- });
103
-
104
- return response.choices[0].message.content.trim();
105
- }
106
-
107
- export function buildPluralMessages(params: BuildPluralMessagesParams): Array<{ role: string; content: string }> {
108
- const systemMsg = safeRender(PLURAL_SYSTEM_TEMPLATE, {
109
- source_lang: params.sourceLang,
110
- target_lang: params.targetLang,
111
- project_context: params.projectContext,
112
- });
113
-
114
- const userMsg = safeRender(PLURAL_USER_TEMPLATE, {
115
- one: params.one,
116
- other: params.other,
117
- string_context: params.stringContext || 'none',
118
- });
119
-
120
- return [
121
- { role: 'system', content: systemMsg },
122
- { role: 'user', content: userMsg },
123
- ];
124
- }
125
-
126
- export async function translatePlural(params: TranslatePluralParams): Promise<Record<string, string>> {
127
- const client = params._clientOverride ?? new OpenAI({
128
- apiKey: params.apiKey,
129
- timeout: params.timeout * 1000,
130
- maxRetries: params.maxRetries,
131
- });
132
-
133
- const messages = buildPluralMessages(params);
134
- const response = await client.chat.completions.create({
135
- model: params.model,
136
- messages,
137
- temperature: 0.3,
138
- });
139
-
140
- const raw = response.choices[0].message.content.trim();
141
- return JSON.parse(raw);
1
+ /**
2
+ * Translation backend router -- delegates to the configured provider.
3
+ */
4
+
5
+ import type { TransduckConfig } from './config.js';
6
+ import { getProvider } from './providers/index.js';
7
+
8
+ // Re-export prompts for backward compat (tests import buildMessages from backend)
9
+ export { buildMessages, buildPluralMessages } from './providers/prompts.js';
10
+
11
+ export async function translate(
12
+ sourceText: string,
13
+ sourceLang: string,
14
+ targetLang: string,
15
+ projectContext: string,
16
+ stringContext: string | null,
17
+ config: TransduckConfig,
18
+ _clientOverride?: any,
19
+ ): Promise<string> {
20
+ const provider = await getProvider(config);
21
+ return provider.translate(sourceText, sourceLang, targetLang, projectContext, stringContext, config, _clientOverride);
22
+ }
23
+
24
+ export async function translatePlural(
25
+ one: string,
26
+ other: string,
27
+ sourceLang: string,
28
+ targetLang: string,
29
+ projectContext: string,
30
+ stringContext: string | null,
31
+ config: TransduckConfig,
32
+ _clientOverride?: any,
33
+ ): Promise<Record<string, string>> {
34
+ const provider = await getProvider(config);
35
+ return provider.translatePlural(one, other, sourceLang, targetLang, projectContext, stringContext, config, _clientOverride);
142
36
  }
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({
@@ -397,7 +427,6 @@ export async function runScan(opts: ScanOptions): Promise<string> {
397
427
 
398
428
  const store = new TranslationStore(cfg.storagePath);
399
429
  await store.initialize();
400
- const apiKey = process.env[cfg.apiKeyEnv];
401
430
  const projectContextHash = hash(cfg.projectContext);
402
431
 
403
432
  let translated = 0;
@@ -421,13 +450,11 @@ export async function runScan(opts: ScanOptions): Promise<string> {
421
450
  }
422
451
 
423
452
  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
- });
453
+ const forms = await backendTranslatePlural(
454
+ entry.one!, entry.other!,
455
+ cfg.sourceLang, lang,
456
+ cfg.projectContext, entry.context ?? null, cfg,
457
+ );
431
458
 
432
459
  for (const [cat, translatedText] of Object.entries(forms)) {
433
460
  await store.insertPlural({
@@ -457,12 +484,10 @@ export async function runScan(opts: ScanOptions): Promise<string> {
457
484
  }
458
485
 
459
486
  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
- });
487
+ const result = await backendTranslate(
488
+ entry.text!, cfg.sourceLang, lang,
489
+ cfg.projectContext, entry.context ?? null, cfg,
490
+ );
466
491
 
467
492
  if (validateTranslation(entry.text!, result)) {
468
493
  await store.insert({
@@ -533,11 +558,19 @@ program.command('init')
533
558
  const context = await ask('Project context: ');
534
559
  const sourceLang = await ask('Source language (e.g. EN): ');
535
560
  const targetsRaw = await ask('Target languages (comma-separated): ');
561
+
562
+ console.log('\nTranslation provider:');
563
+ console.log(' 1. OpenAI (requires API key)');
564
+ console.log(' 2. Claude API (requires API key)');
565
+ console.log(' 3. Claude Code (uses your Claude Code subscription, warming only)');
566
+ const providerRaw = await ask('Select provider [1]: ');
567
+ const providerChoice = parseInt(providerRaw, 10) || 1;
536
568
  rl.close();
537
569
 
538
570
  const output = await runInit({
539
571
  dir, name, context, sourceLang,
540
572
  targetLangs: targetsRaw.split(',').map(s => s.trim()),
573
+ provider: providerChoice,
541
574
  });
542
575
  console.log(output);
543
576
 
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
  }