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/dist/backend.d.ts
CHANGED
|
@@ -1,40 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
@@ -289,6 +298,7 @@ export async function runWarm(opts) {
|
|
|
289
298
|
export async function runScan(opts) {
|
|
290
299
|
const cfg = loadConfig(opts.configPath);
|
|
291
300
|
const scanDirs = opts.dirs.length > 0 ? opts.dirs : [process.cwd()];
|
|
301
|
+
console.log('Scanning...');
|
|
292
302
|
const entries = scanDirectory(scanDirs);
|
|
293
303
|
const regular = entries.filter(e => !e.plural);
|
|
294
304
|
const plurals = entries.filter(e => e.plural);
|
|
@@ -328,90 +338,96 @@ export async function runScan(opts) {
|
|
|
328
338
|
}
|
|
329
339
|
// Warm
|
|
330
340
|
if (opts.warm) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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,
|
|
341
|
+
if (entries.length === 0) {
|
|
342
|
+
lines.push('\nNothing to warm — no translatable strings found.');
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
const targetLangs = opts.langs && opts.langs.length > 0
|
|
346
|
+
? opts.langs.map(l => l.toUpperCase())
|
|
347
|
+
: cfg.targetLangs;
|
|
348
|
+
const store = new TranslationStore(cfg.storagePath);
|
|
349
|
+
await store.initialize();
|
|
350
|
+
const projectContextHash = hash(cfg.projectContext);
|
|
351
|
+
let translated = 0;
|
|
352
|
+
let skipped = 0;
|
|
353
|
+
let failed = 0;
|
|
354
|
+
const total = entries.length * targetLangs.length;
|
|
355
|
+
let done = 0;
|
|
356
|
+
for (const entry of entries) {
|
|
357
|
+
if (entry.plural) {
|
|
358
|
+
const sourceKey = entry.one + '\x00' + entry.other;
|
|
359
|
+
const stringContextHash = hash(entry.context ?? '');
|
|
360
|
+
const label = (entry.one ?? '').slice(0, 40);
|
|
361
|
+
for (const lang of targetLangs) {
|
|
362
|
+
done++;
|
|
363
|
+
const cachedForms = await store.lookupPlural({
|
|
364
|
+
sourceText: sourceKey, sourceLang: cfg.sourceLang, targetLang: lang,
|
|
365
|
+
projectContextHash, stringContextHash,
|
|
361
366
|
});
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
367
|
+
if (Object.keys(cachedForms).length > 0) {
|
|
368
|
+
skipped++;
|
|
369
|
+
console.log(` [${done}/${total}] ${lang} skipped (cached): ${label}`);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
const forms = await backendTranslatePlural(entry.one, entry.other, cfg.sourceLang, lang, cfg.projectContext, entry.context ?? null, cfg);
|
|
374
|
+
for (const [cat, translatedText] of Object.entries(forms)) {
|
|
375
|
+
await store.insertPlural({
|
|
376
|
+
sourceText: sourceKey, sourceLang: cfg.sourceLang, targetLang: lang,
|
|
377
|
+
projectContextHash, stringContextHash,
|
|
378
|
+
pluralCategory: cat, translatedText: translatedText,
|
|
379
|
+
model: cfg.backendModel, status: 'translated',
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
translated++;
|
|
383
|
+
console.log(` [${done}/${total}] ${lang} translated: ${label}`);
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
failed++;
|
|
387
|
+
console.log(` [${done}/${total}] ${lang} failed: ${label}`);
|
|
369
388
|
}
|
|
370
|
-
translated++;
|
|
371
|
-
}
|
|
372
|
-
catch {
|
|
373
|
-
failed++;
|
|
374
389
|
}
|
|
375
390
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
projectContextHash, stringContextHash,
|
|
383
|
-
});
|
|
384
|
-
if (cached !== null) {
|
|
385
|
-
skipped++;
|
|
386
|
-
continue;
|
|
387
|
-
}
|
|
388
|
-
try {
|
|
389
|
-
const result = await backendTranslate({
|
|
391
|
+
else {
|
|
392
|
+
const stringContextHash = hash(entry.context ?? '');
|
|
393
|
+
const label = (entry.text ?? '').slice(0, 40);
|
|
394
|
+
for (const lang of targetLangs) {
|
|
395
|
+
done++;
|
|
396
|
+
const cached = await store.lookup({
|
|
390
397
|
sourceText: entry.text, sourceLang: cfg.sourceLang, targetLang: lang,
|
|
391
|
-
|
|
392
|
-
apiKey: apiKey, model: cfg.backendModel,
|
|
393
|
-
timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
|
|
398
|
+
projectContextHash, stringContextHash,
|
|
394
399
|
});
|
|
395
|
-
if (
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
translatedText: result, model: cfg.backendModel, status: 'translated',
|
|
400
|
-
});
|
|
401
|
-
translated++;
|
|
400
|
+
if (cached !== null) {
|
|
401
|
+
skipped++;
|
|
402
|
+
console.log(` [${done}/${total}] ${lang} skipped (cached): ${label}`);
|
|
403
|
+
continue;
|
|
402
404
|
}
|
|
403
|
-
|
|
405
|
+
try {
|
|
406
|
+
const result = await backendTranslate(entry.text, cfg.sourceLang, lang, cfg.projectContext, entry.context ?? null, cfg);
|
|
407
|
+
if (validateTranslation(entry.text, result)) {
|
|
408
|
+
await store.insert({
|
|
409
|
+
sourceText: entry.text, sourceLang: cfg.sourceLang, targetLang: lang,
|
|
410
|
+
projectContextHash, stringContextHash,
|
|
411
|
+
translatedText: result, model: cfg.backendModel, status: 'translated',
|
|
412
|
+
});
|
|
413
|
+
translated++;
|
|
414
|
+
console.log(` [${done}/${total}] ${lang} translated: ${label}`);
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
failed++;
|
|
418
|
+
console.log(` [${done}/${total}] ${lang} failed: ${label}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
404
422
|
failed++;
|
|
423
|
+
console.log(` [${done}/${total}] ${lang} failed: ${label}`);
|
|
405
424
|
}
|
|
406
425
|
}
|
|
407
|
-
catch {
|
|
408
|
-
failed++;
|
|
409
|
-
}
|
|
410
426
|
}
|
|
411
427
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
428
|
+
store.close();
|
|
429
|
+
lines.push(`\nTranslated: ${translated} | Skipped: ${skipped} | Failed: ${failed}`);
|
|
430
|
+
} // end if entries.length > 0
|
|
415
431
|
}
|
|
416
432
|
if (!opts.warm && !opts.outputPath) {
|
|
417
433
|
lines.push(`\nRun 'transduck scan --warm --langs DE,ES' to translate all strings.`);
|
|
@@ -450,10 +466,17 @@ program.command('init')
|
|
|
450
466
|
const context = await ask('Project context: ');
|
|
451
467
|
const sourceLang = await ask('Source language (e.g. EN): ');
|
|
452
468
|
const targetsRaw = await ask('Target languages (comma-separated): ');
|
|
469
|
+
console.log('\nTranslation provider:');
|
|
470
|
+
console.log(' 1. OpenAI (requires API key)');
|
|
471
|
+
console.log(' 2. Claude API (requires API key)');
|
|
472
|
+
console.log(' 3. Claude Code (uses your Claude Code subscription, warming only)');
|
|
473
|
+
const providerRaw = await ask('Select provider [1]: ');
|
|
474
|
+
const providerChoice = parseInt(providerRaw, 10) || 1;
|
|
453
475
|
rl.close();
|
|
454
476
|
const output = await runInit({
|
|
455
477
|
dir, name, context, sourceLang,
|
|
456
478
|
targetLangs: targetsRaw.split(',').map(s => s.trim()),
|
|
479
|
+
provider: providerChoice,
|
|
457
480
|
});
|
|
458
481
|
console.log(output);
|
|
459
482
|
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
package/dist/config.d.ts
CHANGED
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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>>;
|