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 CHANGED
@@ -1,40 +1,7 @@
1
- interface BuildMessagesParams {
2
- sourceText: string;
3
- sourceLang: string;
4
- targetLang: string;
5
- projectContext: string;
6
- stringContext: string | null;
7
- }
8
- interface TranslateParams extends BuildMessagesParams {
9
- apiKey: string;
10
- model: string;
11
- timeout: number;
12
- maxRetries: number;
13
- _clientOverride?: any;
14
- }
15
- interface BuildPluralMessagesParams {
16
- one: string;
17
- other: string;
18
- sourceLang: string;
19
- targetLang: string;
20
- projectContext: string;
21
- stringContext: string | null;
22
- }
23
- interface TranslatePluralParams extends BuildPluralMessagesParams {
24
- apiKey: string;
25
- model: string;
26
- timeout: number;
27
- maxRetries: number;
28
- _clientOverride?: any;
29
- }
30
- export declare function buildMessages(params: BuildMessagesParams): Array<{
31
- role: string;
32
- content: string;
33
- }>;
34
- export declare function translate(params: TranslateParams): Promise<string>;
35
- export declare function buildPluralMessages(params: BuildPluralMessagesParams): Array<{
36
- role: string;
37
- content: string;
38
- }>;
39
- export declare function translatePlural(params: TranslatePluralParams): Promise<Record<string, string>>;
40
- export {};
1
+ /**
2
+ * Translation backend router -- delegates to the configured provider.
3
+ */
4
+ import type { TransduckConfig } from './config.js';
5
+ export { buildMessages, buildPluralMessages } from './providers/prompts.js';
6
+ export declare function translate(sourceText: string, sourceLang: string, targetLang: string, projectContext: string, stringContext: string | null, config: TransduckConfig, _clientOverride?: any): Promise<string>;
7
+ export declare function translatePlural(one: string, other: string, sourceLang: string, targetLang: string, projectContext: string, stringContext: string | null, config: TransduckConfig, _clientOverride?: any): Promise<Record<string, string>>;
package/dist/backend.js CHANGED
@@ -1,89 +1,14 @@
1
- import OpenAI from 'openai';
2
- /* eslint-disable no-template-curly-in-string */
3
- const SYSTEM_TEMPLATE = 'You are a professional translator. Translate the given text from {source_lang} ' +
4
- 'to {target_lang}. Return ONLY the translated text, nothing else. Preserve any ' +
5
- 'placeholders like {name}, {{count}}, %s, ${value} exactly as they appear. ' +
6
- 'Preserve brand names. Match the tone and formality of the original.\n\n' +
7
- 'Project context: {project_context}';
8
- const USER_TEMPLATE = `Translate: "{source_text}"\nString context: {string_context}`;
9
- const PLURAL_SYSTEM_TEMPLATE = 'You are a professional translator. You will be given two plural forms ' +
10
- '(one and other) in {source_lang}. Generate ALL plural forms needed in ' +
11
- '{target_lang} according to CLDR plural rules. Return ONLY a JSON object ' +
12
- 'mapping plural categories to translated strings. Do not include explanation.\n\n' +
13
- 'CRITICAL: Placeholders like {count}, {name}, %s, ${value} are SOFTWARE VARIABLES ' +
14
- 'that will be replaced by code at runtime. You MUST include them exactly as written ' +
15
- 'in EVERY plural form, even for zero, one, and two categories where the language ' +
16
- 'would not normally use a numeral. For example, for Arabic zero: use "{count} ..." ' +
17
- 'not "لا توجد ...". For Arabic one: use "{count} ..." not "واحدة ...".\n\n' +
18
- 'Preserve brand names. Match the tone and formality of the original.\n\n' +
19
- 'CLDR plural categories are: zero, one, two, few, many, other.\n' +
20
- 'Only include categories that {target_lang} actually uses.\n\n' +
21
- 'Project context: {project_context}';
22
- const PLURAL_USER_TEMPLATE = 'Source one form: "{one}"\n' +
23
- 'Source other form: "{other}"\n' +
24
- 'String context: {string_context}';
25
- function safeRender(template, vars) {
26
- let result = template;
27
- for (const [key, value] of Object.entries(vars)) {
28
- result = result.replaceAll(`{${key}}`, value);
29
- }
30
- return result;
31
- }
32
- export function buildMessages(params) {
33
- const systemMsg = SYSTEM_TEMPLATE
34
- .replace('{source_lang}', params.sourceLang)
35
- .replace('{target_lang}', params.targetLang)
36
- .replace('{project_context}', params.projectContext);
37
- const userMsg = USER_TEMPLATE
38
- .replace('{source_text}', params.sourceText)
39
- .replace('{string_context}', params.stringContext || 'none');
40
- return [
41
- { role: 'system', content: systemMsg },
42
- { role: 'user', content: userMsg },
43
- ];
44
- }
45
- export async function translate(params) {
46
- const client = params._clientOverride ?? new OpenAI({
47
- apiKey: params.apiKey,
48
- timeout: params.timeout * 1000,
49
- maxRetries: params.maxRetries,
50
- });
51
- const messages = buildMessages(params);
52
- const response = await client.chat.completions.create({
53
- model: params.model,
54
- messages,
55
- temperature: 0.3,
56
- });
57
- return response.choices[0].message.content.trim();
58
- }
59
- export function buildPluralMessages(params) {
60
- const systemMsg = safeRender(PLURAL_SYSTEM_TEMPLATE, {
61
- source_lang: params.sourceLang,
62
- target_lang: params.targetLang,
63
- project_context: params.projectContext,
64
- });
65
- const userMsg = safeRender(PLURAL_USER_TEMPLATE, {
66
- one: params.one,
67
- other: params.other,
68
- string_context: params.stringContext || 'none',
69
- });
70
- return [
71
- { role: 'system', content: systemMsg },
72
- { role: 'user', content: userMsg },
73
- ];
74
- }
75
- export async function translatePlural(params) {
76
- const client = params._clientOverride ?? new OpenAI({
77
- apiKey: params.apiKey,
78
- timeout: params.timeout * 1000,
79
- maxRetries: params.maxRetries,
80
- });
81
- const messages = buildPluralMessages(params);
82
- const response = await client.chat.completions.create({
83
- model: params.model,
84
- messages,
85
- temperature: 0.3,
86
- });
87
- const raw = response.choices[0].message.content.trim();
88
- return JSON.parse(raw);
1
+ /**
2
+ * Translation backend router -- delegates to the configured provider.
3
+ */
4
+ import { getProvider } from './providers/index.js';
5
+ // Re-export prompts for backward compat (tests import buildMessages from backend)
6
+ export { buildMessages, buildPluralMessages } from './providers/prompts.js';
7
+ export async function translate(sourceText, sourceLang, targetLang, projectContext, stringContext, config, _clientOverride) {
8
+ const provider = await getProvider(config);
9
+ return provider.translate(sourceText, sourceLang, targetLang, projectContext, stringContext, config, _clientOverride);
10
+ }
11
+ export async function translatePlural(one, other, sourceLang, targetLang, projectContext, stringContext, config, _clientOverride) {
12
+ const provider = await getProvider(config);
13
+ return provider.translatePlural(one, other, sourceLang, targetLang, projectContext, stringContext, config, _clientOverride);
89
14
  }
package/dist/cli.d.ts CHANGED
@@ -6,6 +6,7 @@ export interface InitOptions {
6
6
  context: string;
7
7
  sourceLang: string;
8
8
  targetLangs: string[];
9
+ provider?: number;
9
10
  }
10
11
  export declare function runInit(opts: InitOptions): Promise<string>;
11
12
  export interface TranslateOptions {
package/dist/cli.js CHANGED
@@ -14,19 +14,53 @@ function hash(text) {
14
14
  return createHash('sha256').update(text).digest('hex');
15
15
  }
16
16
  export async function runInit(opts) {
17
+ const providerChoice = opts.provider ?? 1;
17
18
  const config = {
18
19
  project: { name: opts.name, context: opts.context },
19
20
  languages: { source: opts.sourceLang.toUpperCase(), targets: opts.targetLangs.map(l => l.toUpperCase()) },
20
21
  storage: { path: './translations.duckdb' },
21
- backend: { api_key_env: 'OPENAI_API_KEY', model: 'gpt-4.1-mini', timeout_seconds: 10, max_retries: 2 },
22
22
  };
23
+ if (providerChoice === 2) {
24
+ config.backend = {
25
+ provider: 'claude_api',
26
+ api_key_env: 'ANTHROPIC_API_KEY',
27
+ model: 'claude-haiku-4-5-20251001',
28
+ timeout_seconds: 10,
29
+ max_retries: 2,
30
+ };
31
+ }
32
+ else if (providerChoice === 3) {
33
+ config.backend = {
34
+ provider: 'claude_code',
35
+ token_env: 'CLAUDE_CODE_OAUTH_TOKEN',
36
+ };
37
+ }
38
+ else {
39
+ config.backend = {
40
+ provider: 'openai',
41
+ api_key_env: 'OPENAI_API_KEY',
42
+ model: 'gpt-4.1-mini',
43
+ timeout_seconds: 10,
44
+ max_retries: 2,
45
+ };
46
+ }
23
47
  const configPath = join(opts.dir, 'transduck.yaml');
24
48
  writeFileSync(configPath, yamlStringify(config));
25
49
  const dbPath = join(opts.dir, 'translations.duckdb');
26
50
  const store = new TranslationStore(dbPath);
27
51
  await store.initialize();
28
52
  store.close();
29
- return `Created ${configPath}\nCreated ${dbPath}`;
53
+ const lines = [`Created ${configPath}`, `Created ${dbPath}`];
54
+ if (providerChoice === 2) {
55
+ lines.push('', 'Add to your .env file: ANTHROPIC_API_KEY=your-key-here');
56
+ }
57
+ else if (providerChoice === 3) {
58
+ lines.push('', "Run 'claude setup-token' to get your OAuth token, then add to your .env file:", ' CLAUDE_CODE_OAUTH_TOKEN=your-token-here', '', 'Note: claude_code works for CLI warming only. Your app will run in read-only mode.');
59
+ }
60
+ else {
61
+ lines.push('', 'Add to your .env file: OPENAI_API_KEY=your-key-here');
62
+ }
63
+ return lines.join('\n');
30
64
  }
31
65
  export async function runTranslate(opts) {
32
66
  const cfg = loadConfig(opts.configPath);
@@ -43,13 +77,7 @@ export async function runTranslate(opts) {
43
77
  store.close();
44
78
  return `[cached] ${interpolateVars(cached, opts.vars)}`;
45
79
  }
46
- const apiKey = process.env[cfg.apiKeyEnv];
47
- const translated = await backendTranslate({
48
- sourceText: opts.text, sourceLang: cfg.sourceLang, targetLang,
49
- projectContext: cfg.projectContext, stringContext: opts.stringContext ?? null,
50
- apiKey: apiKey, model: cfg.backendModel,
51
- timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
52
- });
80
+ const translated = await backendTranslate(opts.text, cfg.sourceLang, targetLang, cfg.projectContext, opts.stringContext ?? null, cfg);
53
81
  if (!validateTranslation(opts.text, translated)) {
54
82
  await store.insert({
55
83
  sourceText: opts.text, sourceLang: cfg.sourceLang, targetLang,
@@ -93,14 +121,7 @@ export async function runTranslatePlural(opts) {
93
121
  }
94
122
  // Cache miss — call backend
95
123
  try {
96
- const apiKey = process.env[cfg.apiKeyEnv];
97
- const forms = await backendTranslatePlural({
98
- one: opts.one, other: opts.other,
99
- sourceLang: cfg.sourceLang, targetLang,
100
- projectContext: cfg.projectContext, stringContext: opts.stringContext ?? null,
101
- apiKey: apiKey, model: cfg.backendModel,
102
- timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
103
- });
124
+ const forms = await backendTranslatePlural(opts.one, opts.other, cfg.sourceLang, targetLang, cfg.projectContext, opts.stringContext ?? null, cfg);
104
125
  // Validate each form
105
126
  const sourcePlaceholders = new Set([
106
127
  ...extractPlaceholders(opts.one),
@@ -181,7 +202,6 @@ export async function runWarm(opts) {
181
202
  else {
182
203
  entries = content.split('\n').filter(l => l.trim()).map(text => ({ text: text.trim() }));
183
204
  }
184
- const apiKey = process.env[cfg.apiKeyEnv];
185
205
  const projectContextHash = hash(cfg.projectContext);
186
206
  let translated = 0, skipped = 0, failed = 0;
187
207
  for (const entry of entries) {
@@ -199,13 +219,7 @@ export async function runWarm(opts) {
199
219
  continue;
200
220
  }
201
221
  try {
202
- const forms = await backendTranslatePlural({
203
- one: entry.one, other: entry.other,
204
- sourceLang: cfg.sourceLang, targetLang: lang,
205
- projectContext: cfg.projectContext, stringContext: entry.context ?? null,
206
- apiKey: apiKey, model: cfg.backendModel,
207
- timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
208
- });
222
+ const forms = await backendTranslatePlural(entry.one, entry.other, cfg.sourceLang, lang, cfg.projectContext, entry.context ?? null, cfg);
209
223
  const sourcePlaceholders = new Set([
210
224
  ...extractPlaceholders(entry.one),
211
225
  ...extractPlaceholders(entry.other),
@@ -254,12 +268,7 @@ export async function runWarm(opts) {
254
268
  continue;
255
269
  }
256
270
  try {
257
- const result = await backendTranslate({
258
- sourceText: entry.text, sourceLang: cfg.sourceLang, targetLang: lang,
259
- projectContext: cfg.projectContext, stringContext: entry.context ?? null,
260
- apiKey: apiKey, model: cfg.backendModel,
261
- timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
262
- });
271
+ const result = await backendTranslate(entry.text, cfg.sourceLang, lang, cfg.projectContext, entry.context ?? null, cfg);
263
272
  if (validateTranslation(entry.text, result)) {
264
273
  await store.insert({
265
274
  sourceText: entry.text, sourceLang: cfg.sourceLang, targetLang: lang,
@@ -333,7 +342,6 @@ export async function runScan(opts) {
333
342
  : cfg.targetLangs;
334
343
  const store = new TranslationStore(cfg.storagePath);
335
344
  await store.initialize();
336
- const apiKey = process.env[cfg.apiKeyEnv];
337
345
  const projectContextHash = hash(cfg.projectContext);
338
346
  let translated = 0;
339
347
  let skipped = 0;
@@ -352,13 +360,7 @@ export async function runScan(opts) {
352
360
  continue;
353
361
  }
354
362
  try {
355
- const forms = await backendTranslatePlural({
356
- one: entry.one, other: entry.other,
357
- sourceLang: cfg.sourceLang, targetLang: lang,
358
- projectContext: cfg.projectContext, stringContext: entry.context ?? null,
359
- apiKey: apiKey, model: cfg.backendModel,
360
- timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
361
- });
363
+ const forms = await backendTranslatePlural(entry.one, entry.other, cfg.sourceLang, lang, cfg.projectContext, entry.context ?? null, cfg);
362
364
  for (const [cat, translatedText] of Object.entries(forms)) {
363
365
  await store.insertPlural({
364
366
  sourceText: sourceKey, sourceLang: cfg.sourceLang, targetLang: lang,
@@ -386,12 +388,7 @@ export async function runScan(opts) {
386
388
  continue;
387
389
  }
388
390
  try {
389
- const result = await backendTranslate({
390
- sourceText: entry.text, sourceLang: cfg.sourceLang, targetLang: lang,
391
- projectContext: cfg.projectContext, stringContext: entry.context ?? null,
392
- apiKey: apiKey, model: cfg.backendModel,
393
- timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
394
- });
391
+ const result = await backendTranslate(entry.text, cfg.sourceLang, lang, cfg.projectContext, entry.context ?? null, cfg);
395
392
  if (validateTranslation(entry.text, result)) {
396
393
  await store.insert({
397
394
  sourceText: entry.text, sourceLang: cfg.sourceLang, targetLang: lang,
@@ -450,10 +447,17 @@ program.command('init')
450
447
  const context = await ask('Project context: ');
451
448
  const sourceLang = await ask('Source language (e.g. EN): ');
452
449
  const targetsRaw = await ask('Target languages (comma-separated): ');
450
+ console.log('\nTranslation provider:');
451
+ console.log(' 1. OpenAI (requires API key)');
452
+ console.log(' 2. Claude API (requires API key)');
453
+ console.log(' 3. Claude Code (uses your Claude Code subscription, warming only)');
454
+ const providerRaw = await ask('Select provider [1]: ');
455
+ const providerChoice = parseInt(providerRaw, 10) || 1;
453
456
  rl.close();
454
457
  const output = await runInit({
455
458
  dir, name, context, sourceLang,
456
459
  targetLangs: targetsRaw.split(',').map(s => s.trim()),
460
+ provider: providerChoice,
457
461
  });
458
462
  console.log(output);
459
463
  const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
package/dist/config.d.ts CHANGED
@@ -4,7 +4,9 @@ export interface TransduckConfig {
4
4
  sourceLang: string;
5
5
  targetLangs: string[];
6
6
  storagePath: string;
7
+ provider: string;
7
8
  apiKeyEnv: string;
9
+ tokenEnv: string;
8
10
  backendModel: string;
9
11
  backendTimeout: number;
10
12
  backendMaxRetries: number;
package/dist/config.js CHANGED
@@ -29,16 +29,29 @@ export function loadConfig(path) {
29
29
  const raw = parseYaml(readFileSync(configPath, 'utf-8'));
30
30
  const configDir = dirname(configPath);
31
31
  const storagePath = resolve(configDir, raw.storage.path);
32
+ const backend = raw.backend ?? {};
33
+ const provider = backend.provider ?? 'openai';
34
+ const apiKeyEnv = backend.api_key_env ?? 'OPENAI_API_KEY';
35
+ const tokenEnv = backend.token_env ?? 'CLAUDE_CODE_OAUTH_TOKEN';
36
+ const backendModel = backend.model ?? 'gpt-4.1-mini';
37
+ const backendTimeout = backend.timeout_seconds ?? 10;
38
+ const backendMaxRetries = backend.max_retries ?? 2;
39
+ let readOnly = raw.runtime?.read_only ?? false;
40
+ if (provider === 'claude_code') {
41
+ readOnly = true;
42
+ }
32
43
  return {
33
44
  projectName: raw.project.name,
34
45
  projectContext: raw.project.context,
35
46
  sourceLang: String(raw.languages.source).toUpperCase(),
36
47
  targetLangs: raw.languages.targets.map((l) => String(l).toUpperCase()),
37
48
  storagePath,
38
- apiKeyEnv: raw.backend.api_key_env,
39
- backendModel: raw.backend.model,
40
- backendTimeout: raw.backend.timeout_seconds,
41
- backendMaxRetries: raw.backend.max_retries,
42
- readOnly: raw.runtime?.read_only ?? false,
49
+ provider,
50
+ apiKeyEnv,
51
+ tokenEnv,
52
+ backendModel,
53
+ backendTimeout,
54
+ backendMaxRetries,
55
+ readOnly,
43
56
  };
44
57
  }
package/dist/handler.js CHANGED
@@ -27,7 +27,6 @@ export async function handleTranslationRequest(body, configPath) {
27
27
  const store = await getStore(configPath);
28
28
  const targetLang = body.language.toUpperCase();
29
29
  const projectContextHash = hash(cfg.projectContext);
30
- const apiKey = process.env[cfg.apiKeyEnv];
31
30
  const translations = {};
32
31
  const plurals = {};
33
32
  // Translate regular strings
@@ -48,17 +47,7 @@ export async function handleTranslationRequest(body, configPath) {
48
47
  }
49
48
  // Backend call
50
49
  try {
51
- const translated = await backendTranslate({
52
- sourceText: item.text,
53
- sourceLang: cfg.sourceLang,
54
- targetLang,
55
- projectContext: cfg.projectContext,
56
- stringContext: item.context ?? null,
57
- apiKey: apiKey,
58
- model: cfg.backendModel,
59
- timeout: cfg.backendTimeout,
60
- maxRetries: cfg.backendMaxRetries,
61
- });
50
+ const translated = await backendTranslate(item.text, cfg.sourceLang, targetLang, cfg.projectContext, item.context ?? null, cfg);
62
51
  if (validateTranslation(item.text, translated)) {
63
52
  await store.insert({
64
53
  sourceText: item.text,
@@ -99,18 +88,7 @@ export async function handleTranslationRequest(body, configPath) {
99
88
  }
100
89
  // Backend call
101
90
  try {
102
- const forms = await backendTranslatePlural({
103
- one: item.one,
104
- other: item.other,
105
- sourceLang: cfg.sourceLang,
106
- targetLang,
107
- projectContext: cfg.projectContext,
108
- stringContext: item.context ?? null,
109
- apiKey: apiKey,
110
- model: cfg.backendModel,
111
- timeout: cfg.backendTimeout,
112
- maxRetries: cfg.backendMaxRetries,
113
- });
91
+ const forms = await backendTranslatePlural(item.one, item.other, cfg.sourceLang, targetLang, cfg.projectContext, item.context ?? null, cfg);
114
92
  for (const [cat, translatedText] of Object.entries(forms)) {
115
93
  await store.insertPlural({
116
94
  sourceText: sourceKey,
package/dist/index.js CHANGED
@@ -67,14 +67,8 @@ export async function ait(sourceText, context, vars) {
67
67
  });
68
68
  if (rechecked !== null)
69
69
  return rechecked;
70
- const apiKey = process.env[cfg.apiKeyEnv];
71
70
  try {
72
- const translated = await backendTranslate({
73
- sourceText, sourceLang: cfg.sourceLang, targetLang: state.targetLang,
74
- projectContext: cfg.projectContext, stringContext: context ?? null,
75
- apiKey: apiKey, model: cfg.backendModel,
76
- timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
77
- });
71
+ const translated = await backendTranslate(sourceText, cfg.sourceLang, state.targetLang, cfg.projectContext, context ?? null, cfg);
78
72
  if (!validateTranslation(sourceText, translated)) {
79
73
  console.warn(`[transduck] Validation failed for: ${sourceText} -> ${translated}`);
80
74
  await state.store.insert({
@@ -153,15 +147,8 @@ export async function aitPlural(one, other, count, opts) {
153
147
  return interpolateVars(fallback, vars);
154
148
  }
155
149
  // Cache miss — call backend
156
- const apiKey = process.env[cfg.apiKeyEnv];
157
150
  try {
158
- const forms = await backendTranslatePlural({
159
- one, other,
160
- sourceLang: cfg.sourceLang, targetLang: state.targetLang,
161
- projectContext: cfg.projectContext, stringContext: context ?? null,
162
- apiKey: apiKey, model: cfg.backendModel,
163
- timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
164
- });
151
+ const forms = await backendTranslatePlural(one, other, cfg.sourceLang, state.targetLang, cfg.projectContext, context ?? null, cfg);
165
152
  // Validate and store each form
166
153
  const validCategories = new Set(['zero', 'one', 'two', 'few', 'many', 'other']);
167
154
  const sourcePlaceholders = new Set([
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Claude API translation provider.
3
+ * Uses the @anthropic-ai/sdk package (optional peer dependency, lazy imported).
4
+ */
5
+ import type { TransduckConfig } from '../config.js';
6
+ export declare function translate(sourceText: string, sourceLang: string, targetLang: string, projectContext: string, stringContext: string | null, config: TransduckConfig): Promise<string>;
7
+ export declare function translatePlural(one: string, other: string, sourceLang: string, targetLang: string, projectContext: string, stringContext: string | null, config: TransduckConfig): Promise<Record<string, string>>;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Claude API translation provider.
3
+ * Uses the @anthropic-ai/sdk package (optional peer dependency, lazy imported).
4
+ */
5
+ import { buildMessages, buildPluralMessages } from './prompts.js';
6
+ async function getClient(config) {
7
+ let Anthropic;
8
+ try {
9
+ // @ts-ignore — optional peer dependency, may not be installed
10
+ const mod = await import('@anthropic-ai/sdk');
11
+ Anthropic = mod.default ?? mod.Anthropic;
12
+ }
13
+ catch {
14
+ throw new Error('Install the required package: npm install @anthropic-ai/sdk');
15
+ }
16
+ const apiKey = process.env[config.apiKeyEnv];
17
+ if (!apiKey) {
18
+ throw new Error(`Set ${config.apiKeyEnv} environment variable`);
19
+ }
20
+ return new Anthropic({ apiKey });
21
+ }
22
+ export async function translate(sourceText, sourceLang, targetLang, projectContext, stringContext, config) {
23
+ const client = await getClient(config);
24
+ const messages = buildMessages({
25
+ sourceText, sourceLang, targetLang, projectContext, stringContext,
26
+ });
27
+ const response = await client.messages.create({
28
+ model: config.backendModel,
29
+ max_tokens: 1024,
30
+ temperature: 0.3,
31
+ system: messages[0].content,
32
+ messages: [{ role: 'user', content: messages[1].content }],
33
+ });
34
+ return response.content[0].text.trim();
35
+ }
36
+ export async function translatePlural(one, other, sourceLang, targetLang, projectContext, stringContext, config) {
37
+ const client = await getClient(config);
38
+ const messages = buildPluralMessages({
39
+ one, other, sourceLang, targetLang, projectContext, stringContext,
40
+ });
41
+ const response = await client.messages.create({
42
+ model: config.backendModel,
43
+ max_tokens: 2048,
44
+ temperature: 0.3,
45
+ system: messages[0].content,
46
+ messages: [{ role: 'user', content: messages[1].content }],
47
+ });
48
+ const raw = response.content[0].text.trim();
49
+ return JSON.parse(raw);
50
+ }
@@ -0,0 +1,8 @@
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
+ import type { TransduckConfig } from '../config.js';
7
+ export declare function translate(sourceText: string, sourceLang: string, targetLang: string, projectContext: string, stringContext: string | null, config: TransduckConfig): Promise<string>;
8
+ export declare function translatePlural(one: string, other: string, sourceLang: string, targetLang: string, projectContext: string, stringContext: string | null, config: TransduckConfig): Promise<Record<string, string>>;
@@ -0,0 +1,47 @@
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
+ import { buildSinglePrompt, buildPluralSinglePrompt } from './prompts.js';
7
+ function ensureToken(config) {
8
+ const token = process.env[config.tokenEnv];
9
+ if (!token) {
10
+ throw new Error(`Set ${config.tokenEnv} \u2014 run 'claude setup-token' to get your OAuth token`);
11
+ }
12
+ }
13
+ async function translateWithSdk(prompt) {
14
+ let query;
15
+ try {
16
+ // @ts-ignore — optional peer dependency, may not be installed
17
+ const mod = await import('@anthropic-ai/claude-agent-sdk');
18
+ query = mod.query;
19
+ }
20
+ catch {
21
+ throw new Error('Install the required package: npm install @anthropic-ai/claude-agent-sdk');
22
+ }
23
+ for await (const message of query({
24
+ prompt,
25
+ options: { allowedTools: [] },
26
+ })) {
27
+ if ('result' in message) {
28
+ return message.result.trim();
29
+ }
30
+ }
31
+ throw new Error('No result from Claude Agent SDK');
32
+ }
33
+ export async function translate(sourceText, sourceLang, targetLang, projectContext, stringContext, config) {
34
+ ensureToken(config);
35
+ const prompt = buildSinglePrompt({
36
+ sourceText, sourceLang, targetLang, projectContext, stringContext,
37
+ });
38
+ return translateWithSdk(prompt);
39
+ }
40
+ export async function translatePlural(one, other, sourceLang, targetLang, projectContext, stringContext, config) {
41
+ ensureToken(config);
42
+ const prompt = buildPluralSinglePrompt({
43
+ one, other, sourceLang, targetLang, projectContext, stringContext,
44
+ });
45
+ const raw = await translateWithSdk(prompt);
46
+ return JSON.parse(raw);
47
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Translation provider abstraction.
3
+ */
4
+ import type { TransduckConfig } from '../config.js';
5
+ export interface TranslationProvider {
6
+ translate(sourceText: string, sourceLang: string, targetLang: string, projectContext: string, stringContext: string | null, config: TransduckConfig, _clientOverride?: any): Promise<string>;
7
+ translatePlural(one: string, other: string, sourceLang: string, targetLang: string, projectContext: string, stringContext: string | null, config: TransduckConfig, _clientOverride?: any): Promise<Record<string, string>>;
8
+ }
9
+ /**
10
+ * Return the provider module for the configured provider.
11
+ */
12
+ export declare function getProvider(config: TransduckConfig): Promise<TranslationProvider>;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Translation provider abstraction.
3
+ */
4
+ /**
5
+ * Return the provider module for the configured provider.
6
+ */
7
+ export async function getProvider(config) {
8
+ if (config.provider === 'openai') {
9
+ return await import('./openai-provider.js');
10
+ }
11
+ else if (config.provider === 'claude_api') {
12
+ return await import('./claude-api.js');
13
+ }
14
+ else if (config.provider === 'claude_code') {
15
+ return await import('./claude-code.js');
16
+ }
17
+ else {
18
+ throw new Error(`Unknown provider: ${config.provider}. Valid: openai, claude_api, claude_code`);
19
+ }
20
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * OpenAI translation provider.
3
+ */
4
+ import type { TransduckConfig } from '../config.js';
5
+ export declare function translate(sourceText: string, sourceLang: string, targetLang: string, projectContext: string, stringContext: string | null, config: TransduckConfig, _clientOverride?: any): Promise<string>;
6
+ export declare function translatePlural(one: string, other: string, sourceLang: string, targetLang: string, projectContext: string, stringContext: string | null, config: TransduckConfig, _clientOverride?: any): Promise<Record<string, string>>;