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.
- package/dist/backend.d.ts +7 -40
- package/dist/backend.js +13 -88
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +49 -45
- 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/package.json +7 -3
- package/src/backend.ts +35 -141
- package/src/cli.ts +79 -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/tests/ait.test.ts +3 -0
- package/tests/backend.test.ts +61 -59
- package/tests/providers.test.ts +289 -0
|
@@ -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
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
29
|
+
const providerChoice = opts.provider ?? 1;
|
|
30
|
+
|
|
31
|
+
const config: Record<string, any> = {
|
|
29
32
|
project: { name: opts.name, context: opts.context },
|
|
30
33
|
languages: { source: opts.sourceLang.toUpperCase(), targets: opts.targetLangs.map(l => l.toUpperCase()) },
|
|
31
34
|
storage: { path: './translations.duckdb' },
|
|
32
|
-
backend: { api_key_env: 'OPENAI_API_KEY', model: 'gpt-4.1-mini', timeout_seconds: 10, max_retries: 2 },
|
|
33
35
|
};
|
|
34
36
|
|
|
37
|
+
if (providerChoice === 2) {
|
|
38
|
+
config.backend = {
|
|
39
|
+
provider: 'claude_api',
|
|
40
|
+
api_key_env: 'ANTHROPIC_API_KEY',
|
|
41
|
+
model: 'claude-haiku-4-5-20251001',
|
|
42
|
+
timeout_seconds: 10,
|
|
43
|
+
max_retries: 2,
|
|
44
|
+
};
|
|
45
|
+
} else if (providerChoice === 3) {
|
|
46
|
+
config.backend = {
|
|
47
|
+
provider: 'claude_code',
|
|
48
|
+
token_env: 'CLAUDE_CODE_OAUTH_TOKEN',
|
|
49
|
+
};
|
|
50
|
+
} else {
|
|
51
|
+
config.backend = {
|
|
52
|
+
provider: 'openai',
|
|
53
|
+
api_key_env: 'OPENAI_API_KEY',
|
|
54
|
+
model: 'gpt-4.1-mini',
|
|
55
|
+
timeout_seconds: 10,
|
|
56
|
+
max_retries: 2,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
35
60
|
const configPath = join(opts.dir, 'transduck.yaml');
|
|
36
61
|
writeFileSync(configPath, yamlStringify(config));
|
|
37
62
|
|
|
@@ -40,7 +65,23 @@ export async function runInit(opts: InitOptions): Promise<string> {
|
|
|
40
65
|
await store.initialize();
|
|
41
66
|
store.close();
|
|
42
67
|
|
|
43
|
-
|
|
68
|
+
const lines = [`Created ${configPath}`, `Created ${dbPath}`];
|
|
69
|
+
|
|
70
|
+
if (providerChoice === 2) {
|
|
71
|
+
lines.push('', 'Add to your .env file: ANTHROPIC_API_KEY=your-key-here');
|
|
72
|
+
} else if (providerChoice === 3) {
|
|
73
|
+
lines.push(
|
|
74
|
+
'',
|
|
75
|
+
"Run 'claude setup-token' to get your OAuth token, then add to your .env file:",
|
|
76
|
+
' CLAUDE_CODE_OAUTH_TOKEN=your-token-here',
|
|
77
|
+
'',
|
|
78
|
+
'Note: claude_code works for CLI warming only. Your app will run in read-only mode.',
|
|
79
|
+
);
|
|
80
|
+
} else {
|
|
81
|
+
lines.push('', 'Add to your .env file: OPENAI_API_KEY=your-key-here');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return lines.join('\n');
|
|
44
85
|
}
|
|
45
86
|
|
|
46
87
|
export interface TranslateOptions {
|
|
@@ -71,13 +112,10 @@ export async function runTranslate(opts: TranslateOptions): Promise<string> {
|
|
|
71
112
|
return `[cached] ${interpolateVars(cached, opts.vars)}`;
|
|
72
113
|
}
|
|
73
114
|
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
apiKey: apiKey!, model: cfg.backendModel,
|
|
79
|
-
timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
|
|
80
|
-
});
|
|
115
|
+
const translated = await backendTranslate(
|
|
116
|
+
opts.text, cfg.sourceLang, targetLang,
|
|
117
|
+
cfg.projectContext, opts.stringContext ?? null, cfg,
|
|
118
|
+
);
|
|
81
119
|
|
|
82
120
|
if (!validateTranslation(opts.text, translated)) {
|
|
83
121
|
await store.insert({
|
|
@@ -141,14 +179,11 @@ export async function runTranslatePlural(opts: TranslatePluralOptions): Promise<
|
|
|
141
179
|
|
|
142
180
|
// Cache miss — call backend
|
|
143
181
|
try {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
apiKey: apiKey!, model: cfg.backendModel,
|
|
150
|
-
timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
|
|
151
|
-
});
|
|
182
|
+
const forms = await backendTranslatePlural(
|
|
183
|
+
opts.one, opts.other,
|
|
184
|
+
cfg.sourceLang, targetLang,
|
|
185
|
+
cfg.projectContext, opts.stringContext ?? null, cfg,
|
|
186
|
+
);
|
|
152
187
|
|
|
153
188
|
// Validate each form
|
|
154
189
|
const sourcePlaceholders = new Set([
|
|
@@ -233,7 +268,6 @@ export async function runWarm(opts: WarmOptions): Promise<string> {
|
|
|
233
268
|
entries = content.split('\n').filter(l => l.trim()).map(text => ({ text: text.trim() }));
|
|
234
269
|
}
|
|
235
270
|
|
|
236
|
-
const apiKey = process.env[cfg.apiKeyEnv];
|
|
237
271
|
const projectContextHash = hash(cfg.projectContext);
|
|
238
272
|
let translated = 0, skipped = 0, failed = 0;
|
|
239
273
|
|
|
@@ -255,13 +289,11 @@ export async function runWarm(opts: WarmOptions): Promise<string> {
|
|
|
255
289
|
}
|
|
256
290
|
|
|
257
291
|
try {
|
|
258
|
-
const forms = await backendTranslatePlural(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
|
|
264
|
-
});
|
|
292
|
+
const forms = await backendTranslatePlural(
|
|
293
|
+
entry.one, entry.other,
|
|
294
|
+
cfg.sourceLang, lang,
|
|
295
|
+
cfg.projectContext, entry.context ?? null, cfg,
|
|
296
|
+
);
|
|
265
297
|
|
|
266
298
|
const sourcePlaceholders = new Set([
|
|
267
299
|
...extractPlaceholders(entry.one),
|
|
@@ -304,12 +336,10 @@ export async function runWarm(opts: WarmOptions): Promise<string> {
|
|
|
304
336
|
if (cached !== null) { skipped++; continue; }
|
|
305
337
|
|
|
306
338
|
try {
|
|
307
|
-
const result = await backendTranslate(
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
|
|
312
|
-
});
|
|
339
|
+
const result = await backendTranslate(
|
|
340
|
+
entry.text, cfg.sourceLang, lang,
|
|
341
|
+
cfg.projectContext, entry.context ?? null, cfg,
|
|
342
|
+
);
|
|
313
343
|
|
|
314
344
|
if (validateTranslation(entry.text, result)) {
|
|
315
345
|
await store.insert({
|
|
@@ -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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
provider,
|
|
74
|
+
apiKeyEnv,
|
|
75
|
+
tokenEnv,
|
|
76
|
+
backendModel,
|
|
77
|
+
backendTimeout,
|
|
78
|
+
backendMaxRetries,
|
|
79
|
+
readOnly,
|
|
63
80
|
};
|
|
64
81
|
}
|