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 +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
package/src/handler.ts
CHANGED
|
@@ -58,7 +58,6 @@ export async function handleTranslationRequest(
|
|
|
58
58
|
const targetLang = body.language.toUpperCase();
|
|
59
59
|
|
|
60
60
|
const projectContextHash = hash(cfg.projectContext);
|
|
61
|
-
const apiKey = process.env[cfg.apiKeyEnv];
|
|
62
61
|
|
|
63
62
|
const translations: Record<string, string> = {};
|
|
64
63
|
const plurals: Record<string, Record<string, string>> = {};
|
|
@@ -84,17 +83,14 @@ export async function handleTranslationRequest(
|
|
|
84
83
|
|
|
85
84
|
// Backend call
|
|
86
85
|
try {
|
|
87
|
-
const translated = await backendTranslate(
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
const translated = await backendTranslate(
|
|
87
|
+
item.text,
|
|
88
|
+
cfg.sourceLang,
|
|
90
89
|
targetLang,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
timeout: cfg.backendTimeout,
|
|
96
|
-
maxRetries: cfg.backendMaxRetries,
|
|
97
|
-
});
|
|
90
|
+
cfg.projectContext,
|
|
91
|
+
item.context ?? null,
|
|
92
|
+
cfg,
|
|
93
|
+
);
|
|
98
94
|
|
|
99
95
|
if (validateTranslation(item.text, translated)) {
|
|
100
96
|
await store.insert({
|
|
@@ -138,18 +134,15 @@ export async function handleTranslationRequest(
|
|
|
138
134
|
|
|
139
135
|
// Backend call
|
|
140
136
|
try {
|
|
141
|
-
const forms = await backendTranslatePlural(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
137
|
+
const forms = await backendTranslatePlural(
|
|
138
|
+
item.one,
|
|
139
|
+
item.other,
|
|
140
|
+
cfg.sourceLang,
|
|
145
141
|
targetLang,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
timeout: cfg.backendTimeout,
|
|
151
|
-
maxRetries: cfg.backendMaxRetries,
|
|
152
|
-
});
|
|
142
|
+
cfg.projectContext,
|
|
143
|
+
item.context ?? null,
|
|
144
|
+
cfg,
|
|
145
|
+
);
|
|
153
146
|
|
|
154
147
|
for (const [cat, translatedText] of Object.entries(forms)) {
|
|
155
148
|
await store.insertPlural({
|
package/src/index.ts
CHANGED
|
@@ -92,14 +92,11 @@ export async function ait(
|
|
|
92
92
|
});
|
|
93
93
|
if (rechecked !== null) return rechecked;
|
|
94
94
|
|
|
95
|
-
const apiKey = process.env[cfg.apiKeyEnv];
|
|
96
95
|
try {
|
|
97
|
-
const translated = await backendTranslate(
|
|
98
|
-
sourceText,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
|
|
102
|
-
});
|
|
96
|
+
const translated = await backendTranslate(
|
|
97
|
+
sourceText, cfg.sourceLang, state.targetLang!,
|
|
98
|
+
cfg.projectContext, context ?? null, cfg,
|
|
99
|
+
);
|
|
103
100
|
|
|
104
101
|
if (!validateTranslation(sourceText, translated)) {
|
|
105
102
|
console.warn(`[transduck] Validation failed for: ${sourceText} -> ${translated}`);
|
|
@@ -192,15 +189,12 @@ export async function aitPlural(
|
|
|
192
189
|
}
|
|
193
190
|
|
|
194
191
|
// Cache miss — call backend
|
|
195
|
-
const apiKey = process.env[cfg.apiKeyEnv];
|
|
196
192
|
try {
|
|
197
|
-
const forms = await backendTranslatePlural(
|
|
193
|
+
const forms = await backendTranslatePlural(
|
|
198
194
|
one, other,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
timeout: cfg.backendTimeout, maxRetries: cfg.backendMaxRetries,
|
|
203
|
-
});
|
|
195
|
+
cfg.sourceLang, state.targetLang,
|
|
196
|
+
cfg.projectContext, context ?? null, cfg,
|
|
197
|
+
);
|
|
204
198
|
|
|
205
199
|
// Validate and store each form
|
|
206
200
|
const validCategories = new Set(['zero', 'one', 'two', 'few', 'many', 'other']);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude API translation provider.
|
|
3
|
+
* Uses the @anthropic-ai/sdk package (optional peer dependency, lazy imported).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { TransduckConfig } from '../config.js';
|
|
7
|
+
import { buildMessages, buildPluralMessages } from './prompts.js';
|
|
8
|
+
|
|
9
|
+
async function getClient(config: TransduckConfig) {
|
|
10
|
+
let Anthropic: any;
|
|
11
|
+
try {
|
|
12
|
+
// @ts-ignore — optional peer dependency, may not be installed
|
|
13
|
+
const mod = await import('@anthropic-ai/sdk');
|
|
14
|
+
Anthropic = mod.default ?? mod.Anthropic;
|
|
15
|
+
} catch {
|
|
16
|
+
throw new Error(
|
|
17
|
+
'Install the required package: npm install @anthropic-ai/sdk'
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const apiKey = process.env[config.apiKeyEnv];
|
|
22
|
+
if (!apiKey) {
|
|
23
|
+
throw new Error(`Set ${config.apiKeyEnv} environment variable`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return new Anthropic({ apiKey });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function translate(
|
|
30
|
+
sourceText: string,
|
|
31
|
+
sourceLang: string,
|
|
32
|
+
targetLang: string,
|
|
33
|
+
projectContext: string,
|
|
34
|
+
stringContext: string | null,
|
|
35
|
+
config: TransduckConfig,
|
|
36
|
+
): Promise<string> {
|
|
37
|
+
const client = await getClient(config);
|
|
38
|
+
const messages = buildMessages({
|
|
39
|
+
sourceText, sourceLang, targetLang, projectContext, stringContext,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const response = await client.messages.create({
|
|
43
|
+
model: config.backendModel,
|
|
44
|
+
max_tokens: 1024,
|
|
45
|
+
temperature: 0.3,
|
|
46
|
+
system: messages[0].content,
|
|
47
|
+
messages: [{ role: 'user', content: messages[1].content }],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return response.content[0].text.trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function translatePlural(
|
|
54
|
+
one: string,
|
|
55
|
+
other: string,
|
|
56
|
+
sourceLang: string,
|
|
57
|
+
targetLang: string,
|
|
58
|
+
projectContext: string,
|
|
59
|
+
stringContext: string | null,
|
|
60
|
+
config: TransduckConfig,
|
|
61
|
+
): Promise<Record<string, string>> {
|
|
62
|
+
const client = await getClient(config);
|
|
63
|
+
const messages = buildPluralMessages({
|
|
64
|
+
one, other, sourceLang, targetLang, projectContext, stringContext,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const response = await client.messages.create({
|
|
68
|
+
model: config.backendModel,
|
|
69
|
+
max_tokens: 2048,
|
|
70
|
+
temperature: 0.3,
|
|
71
|
+
system: messages[0].content,
|
|
72
|
+
messages: [{ role: 'user', content: messages[1].content }],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const raw = response.content[0].text.trim();
|
|
76
|
+
return JSON.parse(raw);
|
|
77
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code translation provider (uses Claude Agent SDK with subscription auth).
|
|
3
|
+
* Uses the @anthropic-ai/claude-agent-sdk package (optional peer dependency, lazy imported).
|
|
4
|
+
* Already async in JS — no asyncio bridge needed like Python.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { TransduckConfig } from '../config.js';
|
|
8
|
+
import { buildSinglePrompt, buildPluralSinglePrompt } from './prompts.js';
|
|
9
|
+
|
|
10
|
+
function ensureToken(config: TransduckConfig): void {
|
|
11
|
+
const token = process.env[config.tokenEnv];
|
|
12
|
+
if (!token) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`Set ${config.tokenEnv} \u2014 run 'claude setup-token' to get your OAuth token`
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function translateWithSdk(prompt: string): Promise<string> {
|
|
20
|
+
let query: any;
|
|
21
|
+
try {
|
|
22
|
+
// @ts-ignore — optional peer dependency, may not be installed
|
|
23
|
+
const mod = await import('@anthropic-ai/claude-agent-sdk');
|
|
24
|
+
query = mod.query;
|
|
25
|
+
} catch {
|
|
26
|
+
throw new Error(
|
|
27
|
+
'Install the required package: npm install @anthropic-ai/claude-agent-sdk'
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for await (const message of query({
|
|
32
|
+
prompt,
|
|
33
|
+
options: { allowedTools: [] },
|
|
34
|
+
})) {
|
|
35
|
+
if ('result' in message) {
|
|
36
|
+
return (message as any).result.trim();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
throw new Error('No result from Claude Agent SDK');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function translate(
|
|
43
|
+
sourceText: string,
|
|
44
|
+
sourceLang: string,
|
|
45
|
+
targetLang: string,
|
|
46
|
+
projectContext: string,
|
|
47
|
+
stringContext: string | null,
|
|
48
|
+
config: TransduckConfig,
|
|
49
|
+
): Promise<string> {
|
|
50
|
+
ensureToken(config);
|
|
51
|
+
const prompt = buildSinglePrompt({
|
|
52
|
+
sourceText, sourceLang, targetLang, projectContext, stringContext,
|
|
53
|
+
});
|
|
54
|
+
return translateWithSdk(prompt);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function translatePlural(
|
|
58
|
+
one: string,
|
|
59
|
+
other: string,
|
|
60
|
+
sourceLang: string,
|
|
61
|
+
targetLang: string,
|
|
62
|
+
projectContext: string,
|
|
63
|
+
stringContext: string | null,
|
|
64
|
+
config: TransduckConfig,
|
|
65
|
+
): Promise<Record<string, string>> {
|
|
66
|
+
ensureToken(config);
|
|
67
|
+
const prompt = buildPluralSinglePrompt({
|
|
68
|
+
one, other, sourceLang, targetLang, projectContext, stringContext,
|
|
69
|
+
});
|
|
70
|
+
const raw = await translateWithSdk(prompt);
|
|
71
|
+
return JSON.parse(raw);
|
|
72
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation provider abstraction.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { TransduckConfig } from '../config.js';
|
|
6
|
+
|
|
7
|
+
export interface TranslationProvider {
|
|
8
|
+
translate(
|
|
9
|
+
sourceText: string,
|
|
10
|
+
sourceLang: string,
|
|
11
|
+
targetLang: string,
|
|
12
|
+
projectContext: string,
|
|
13
|
+
stringContext: string | null,
|
|
14
|
+
config: TransduckConfig,
|
|
15
|
+
_clientOverride?: any,
|
|
16
|
+
): Promise<string>;
|
|
17
|
+
|
|
18
|
+
translatePlural(
|
|
19
|
+
one: string,
|
|
20
|
+
other: string,
|
|
21
|
+
sourceLang: string,
|
|
22
|
+
targetLang: string,
|
|
23
|
+
projectContext: string,
|
|
24
|
+
stringContext: string | null,
|
|
25
|
+
config: TransduckConfig,
|
|
26
|
+
_clientOverride?: any,
|
|
27
|
+
): Promise<Record<string, string>>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Return the provider module for the configured provider.
|
|
32
|
+
*/
|
|
33
|
+
export async function getProvider(config: TransduckConfig): Promise<TranslationProvider> {
|
|
34
|
+
if (config.provider === 'openai') {
|
|
35
|
+
return await import('./openai-provider.js');
|
|
36
|
+
} else if (config.provider === 'claude_api') {
|
|
37
|
+
return await import('./claude-api.js');
|
|
38
|
+
} else if (config.provider === 'claude_code') {
|
|
39
|
+
return await import('./claude-code.js');
|
|
40
|
+
} else {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Unknown provider: ${config.provider}. Valid: openai, claude_api, claude_code`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI translation provider.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import OpenAI from 'openai';
|
|
6
|
+
import type { TransduckConfig } from '../config.js';
|
|
7
|
+
import { buildMessages, buildPluralMessages } from './prompts.js';
|
|
8
|
+
|
|
9
|
+
export async function translate(
|
|
10
|
+
sourceText: string,
|
|
11
|
+
sourceLang: string,
|
|
12
|
+
targetLang: string,
|
|
13
|
+
projectContext: string,
|
|
14
|
+
stringContext: string | null,
|
|
15
|
+
config: TransduckConfig,
|
|
16
|
+
_clientOverride?: any,
|
|
17
|
+
): Promise<string> {
|
|
18
|
+
const apiKey = process.env[config.apiKeyEnv];
|
|
19
|
+
const client = _clientOverride ?? new OpenAI({
|
|
20
|
+
apiKey,
|
|
21
|
+
timeout: config.backendTimeout * 1000,
|
|
22
|
+
maxRetries: config.backendMaxRetries,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const messages = buildMessages({
|
|
26
|
+
sourceText, sourceLang, targetLang, projectContext, stringContext,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const response = await client.chat.completions.create({
|
|
30
|
+
model: config.backendModel,
|
|
31
|
+
messages,
|
|
32
|
+
temperature: 0.3,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return response.choices[0].message.content.trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function translatePlural(
|
|
39
|
+
one: string,
|
|
40
|
+
other: string,
|
|
41
|
+
sourceLang: string,
|
|
42
|
+
targetLang: string,
|
|
43
|
+
projectContext: string,
|
|
44
|
+
stringContext: string | null,
|
|
45
|
+
config: TransduckConfig,
|
|
46
|
+
_clientOverride?: any,
|
|
47
|
+
): Promise<Record<string, string>> {
|
|
48
|
+
const apiKey = process.env[config.apiKeyEnv];
|
|
49
|
+
const client = _clientOverride ?? new OpenAI({
|
|
50
|
+
apiKey,
|
|
51
|
+
timeout: config.backendTimeout * 1000,
|
|
52
|
+
maxRetries: config.backendMaxRetries,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const messages = buildPluralMessages({
|
|
56
|
+
one, other, sourceLang, targetLang, projectContext, stringContext,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const response = await client.chat.completions.create({
|
|
60
|
+
model: config.backendModel,
|
|
61
|
+
messages,
|
|
62
|
+
temperature: 0.3,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const raw = response.choices[0].message.content.trim();
|
|
66
|
+
return JSON.parse(raw);
|
|
67
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/* eslint-disable no-template-curly-in-string */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared prompt templates for all translation providers.
|
|
5
|
+
*
|
|
6
|
+
* IMPORTANT: Uses string concatenation (NOT template literals) because the
|
|
7
|
+
* prompt text contains ${value} patterns that must be preserved literally.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const SYSTEM_TEMPLATE =
|
|
11
|
+
'You are a professional translator. Translate the given text from {source_lang} ' +
|
|
12
|
+
'to {target_lang}. Return ONLY the translated text, nothing else. Preserve any ' +
|
|
13
|
+
'placeholders like {name}, {{count}}, %s, ${value} exactly as they appear. ' +
|
|
14
|
+
'Preserve brand names. Match the tone and formality of the original.\n\n' +
|
|
15
|
+
'Project context: {project_context}';
|
|
16
|
+
|
|
17
|
+
const USER_TEMPLATE =
|
|
18
|
+
'Translate: "{source_text}"\nString context: {string_context}';
|
|
19
|
+
|
|
20
|
+
const PLURAL_SYSTEM_TEMPLATE =
|
|
21
|
+
'You are a professional translator. You will be given two plural forms ' +
|
|
22
|
+
'(one and other) in {source_lang}. Generate ALL plural forms needed in ' +
|
|
23
|
+
'{target_lang} according to CLDR plural rules. Return ONLY a JSON object ' +
|
|
24
|
+
'mapping plural categories to translated strings. Do not include explanation.\n\n' +
|
|
25
|
+
'CRITICAL: Placeholders like {count}, {name}, %s, ${value} are SOFTWARE VARIABLES ' +
|
|
26
|
+
'that will be replaced by code at runtime. You MUST include them exactly as written ' +
|
|
27
|
+
'in EVERY plural form, even for zero, one, and two categories where the language ' +
|
|
28
|
+
'would not normally use a numeral. For example, for Arabic zero: use "{count} ..." ' +
|
|
29
|
+
'not "\u0644\u0627 \u062a\u0648\u062c\u062f ...". For Arabic one: use "{count} ..." not "\u0648\u0627\u062d\u062f\u0629 ...".\n\n' +
|
|
30
|
+
'Preserve brand names. Match the tone and formality of the original.\n\n' +
|
|
31
|
+
'CLDR plural categories are: zero, one, two, few, many, other.\n' +
|
|
32
|
+
'Only include categories that {target_lang} actually uses.\n\n' +
|
|
33
|
+
'Project context: {project_context}';
|
|
34
|
+
|
|
35
|
+
const PLURAL_USER_TEMPLATE =
|
|
36
|
+
'Source one form: "{one}"\n' +
|
|
37
|
+
'Source other form: "{other}"\n' +
|
|
38
|
+
'String context: {string_context}';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Safe string replacement that only replaces known keys.
|
|
42
|
+
* Does NOT use str.replace with regex — avoids issues with ${value} patterns.
|
|
43
|
+
*/
|
|
44
|
+
export function safeRender(template: string, vars: Record<string, string>): string {
|
|
45
|
+
let result = template;
|
|
46
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
47
|
+
result = result.replaceAll('{' + key + '}', value);
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface BuildMessagesParams {
|
|
53
|
+
sourceText: string;
|
|
54
|
+
sourceLang: string;
|
|
55
|
+
targetLang: string;
|
|
56
|
+
projectContext: string;
|
|
57
|
+
stringContext: string | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface BuildPluralMessagesParams {
|
|
61
|
+
one: string;
|
|
62
|
+
other: string;
|
|
63
|
+
sourceLang: string;
|
|
64
|
+
targetLang: string;
|
|
65
|
+
projectContext: string;
|
|
66
|
+
stringContext: string | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build system + user messages for chat-based providers (OpenAI, Claude API).
|
|
71
|
+
*/
|
|
72
|
+
export function buildMessages(params: BuildMessagesParams): Array<{ role: string; content: string }> {
|
|
73
|
+
const systemMsg = SYSTEM_TEMPLATE
|
|
74
|
+
.replace('{source_lang}', params.sourceLang)
|
|
75
|
+
.replace('{target_lang}', params.targetLang)
|
|
76
|
+
.replace('{project_context}', params.projectContext);
|
|
77
|
+
|
|
78
|
+
const userMsg = USER_TEMPLATE
|
|
79
|
+
.replace('{source_text}', params.sourceText)
|
|
80
|
+
.replace('{string_context}', params.stringContext || 'none');
|
|
81
|
+
|
|
82
|
+
return [
|
|
83
|
+
{ role: 'system', content: systemMsg },
|
|
84
|
+
{ role: 'user', content: userMsg },
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build system + user messages for plural chat-based providers.
|
|
90
|
+
*/
|
|
91
|
+
export function buildPluralMessages(params: BuildPluralMessagesParams): Array<{ role: string; content: string }> {
|
|
92
|
+
const systemMsg = safeRender(PLURAL_SYSTEM_TEMPLATE, {
|
|
93
|
+
source_lang: params.sourceLang,
|
|
94
|
+
target_lang: params.targetLang,
|
|
95
|
+
project_context: params.projectContext,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const userMsg = safeRender(PLURAL_USER_TEMPLATE, {
|
|
99
|
+
one: params.one,
|
|
100
|
+
other: params.other,
|
|
101
|
+
string_context: params.stringContext || 'none',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return [
|
|
105
|
+
{ role: 'system', content: systemMsg },
|
|
106
|
+
{ role: 'user', content: userMsg },
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Build a single combined prompt for non-chat providers (Claude Code).
|
|
112
|
+
*/
|
|
113
|
+
export function buildSinglePrompt(params: BuildMessagesParams): string {
|
|
114
|
+
const messages = buildMessages(params);
|
|
115
|
+
return messages[0].content + '\n\n' + messages[1].content;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Build a single combined plural prompt for non-chat providers.
|
|
120
|
+
*/
|
|
121
|
+
export function buildPluralSinglePrompt(params: BuildPluralMessagesParams): string {
|
|
122
|
+
const messages = buildPluralMessages(params);
|
|
123
|
+
return messages[0].content + '\n\n' + messages[1].content;
|
|
124
|
+
}
|
package/tests/ait.test.ts
CHANGED
|
@@ -16,10 +16,13 @@ function makeConfig(tmpDir: string): TransduckConfig {
|
|
|
16
16
|
sourceLang: 'EN',
|
|
17
17
|
targetLangs: ['DE', 'ES'],
|
|
18
18
|
storagePath: join(tmpDir, 'test.duckdb'),
|
|
19
|
+
provider: 'openai',
|
|
19
20
|
apiKeyEnv: 'OPENAI_API_KEY',
|
|
21
|
+
tokenEnv: 'CLAUDE_CODE_OAUTH_TOKEN',
|
|
20
22
|
backendModel: 'gpt-4.1-mini',
|
|
21
23
|
backendTimeout: 10,
|
|
22
24
|
backendMaxRetries: 2,
|
|
25
|
+
readOnly: false,
|
|
23
26
|
};
|
|
24
27
|
}
|
|
25
28
|
|
package/tests/backend.test.ts
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { buildMessages,
|
|
2
|
+
import { buildMessages, buildPluralMessages, translate, translatePlural } from '../src/backend.js';
|
|
3
|
+
import type { TransduckConfig } from '../src/config.js';
|
|
4
|
+
|
|
5
|
+
function makeConfig(): TransduckConfig {
|
|
6
|
+
return {
|
|
7
|
+
projectName: 'test',
|
|
8
|
+
projectContext: 'A travel site about Mallorca',
|
|
9
|
+
sourceLang: 'EN',
|
|
10
|
+
targetLangs: ['DE'],
|
|
11
|
+
storagePath: '/tmp/test.duckdb',
|
|
12
|
+
provider: 'openai',
|
|
13
|
+
apiKeyEnv: 'OPENAI_API_KEY',
|
|
14
|
+
tokenEnv: 'CLAUDE_CODE_OAUTH_TOKEN',
|
|
15
|
+
backendModel: 'gpt-4.1-mini',
|
|
16
|
+
backendTimeout: 10,
|
|
17
|
+
backendMaxRetries: 2,
|
|
18
|
+
readOnly: false,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
3
21
|
|
|
4
22
|
describe('buildMessages', () => {
|
|
5
23
|
it('builds system and user messages', () => {
|
|
@@ -38,18 +56,14 @@ describe('translate', () => {
|
|
|
38
56
|
choices: [{ message: { content: 'Unsere Veranstaltungen' } }],
|
|
39
57
|
});
|
|
40
58
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
timeout: 10,
|
|
50
|
-
maxRetries: 2,
|
|
51
|
-
_clientOverride: { chat: { completions: { create: mockCreate } } } as any,
|
|
52
|
-
});
|
|
59
|
+
const cfg = makeConfig();
|
|
60
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
61
|
+
|
|
62
|
+
const result = await translate(
|
|
63
|
+
'Our Events', 'EN', 'DE',
|
|
64
|
+
'A travel site', null, cfg,
|
|
65
|
+
{ chat: { completions: { create: mockCreate } } } as any,
|
|
66
|
+
);
|
|
53
67
|
expect(result).toBe('Unsere Veranstaltungen');
|
|
54
68
|
});
|
|
55
69
|
|
|
@@ -58,18 +72,14 @@ describe('translate', () => {
|
|
|
58
72
|
choices: [{ message: { content: ' Hallo \n' } }],
|
|
59
73
|
});
|
|
60
74
|
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
timeout: 10,
|
|
70
|
-
maxRetries: 2,
|
|
71
|
-
_clientOverride: { chat: { completions: { create: mockCreate } } } as any,
|
|
72
|
-
});
|
|
75
|
+
const cfg = makeConfig();
|
|
76
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
77
|
+
|
|
78
|
+
const result = await translate(
|
|
79
|
+
'Hello', 'EN', 'DE',
|
|
80
|
+
'test', null, cfg,
|
|
81
|
+
{ chat: { completions: { create: mockCreate } } } as any,
|
|
82
|
+
);
|
|
73
83
|
expect(result).toBe('Hallo');
|
|
74
84
|
});
|
|
75
85
|
});
|
|
@@ -132,19 +142,15 @@ describe('translatePlural', () => {
|
|
|
132
142
|
choices: [{ message: { content: '{"one": "{count} Nachricht", "other": "{count} Nachrichten"}' } }],
|
|
133
143
|
});
|
|
134
144
|
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
timeout: 10,
|
|
145
|
-
maxRetries: 2,
|
|
146
|
-
_clientOverride: { chat: { completions: { create: mockCreate } } } as any,
|
|
147
|
-
});
|
|
145
|
+
const cfg = makeConfig();
|
|
146
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
147
|
+
|
|
148
|
+
const result = await translatePlural(
|
|
149
|
+
'{count} message', '{count} messages',
|
|
150
|
+
'EN', 'DE',
|
|
151
|
+
'test', null, cfg,
|
|
152
|
+
{ chat: { completions: { create: mockCreate } } } as any,
|
|
153
|
+
);
|
|
148
154
|
|
|
149
155
|
expect(result).toEqual({
|
|
150
156
|
one: '{count} Nachricht',
|
|
@@ -155,30 +161,26 @@ describe('translatePlural', () => {
|
|
|
155
161
|
it('returns multiple categories for Russian', async () => {
|
|
156
162
|
const mockCreate = vi.fn().mockResolvedValue({
|
|
157
163
|
choices: [{ message: { content: JSON.stringify({
|
|
158
|
-
one: '{count}
|
|
159
|
-
few: '{count}
|
|
160
|
-
many: '{count}
|
|
161
|
-
other: '{count}
|
|
164
|
+
one: '{count} \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435',
|
|
165
|
+
few: '{count} \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f',
|
|
166
|
+
many: '{count} \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439',
|
|
167
|
+
other: '{count} \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439',
|
|
162
168
|
}) } }],
|
|
163
169
|
});
|
|
164
170
|
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
timeout: 10,
|
|
175
|
-
maxRetries: 2,
|
|
176
|
-
_clientOverride: { chat: { completions: { create: mockCreate } } } as any,
|
|
177
|
-
});
|
|
171
|
+
const cfg = makeConfig();
|
|
172
|
+
process.env.OPENAI_API_KEY = 'test-key';
|
|
173
|
+
|
|
174
|
+
const result = await translatePlural(
|
|
175
|
+
'{count} message', '{count} messages',
|
|
176
|
+
'EN', 'RU',
|
|
177
|
+
'test', null, cfg,
|
|
178
|
+
{ chat: { completions: { create: mockCreate } } } as any,
|
|
179
|
+
);
|
|
178
180
|
|
|
179
|
-
expect(result.one).toBe('{count}
|
|
180
|
-
expect(result.few).toBe('{count}
|
|
181
|
-
expect(result.many).toBe('{count}
|
|
182
|
-
expect(result.other).toBe('{count}
|
|
181
|
+
expect(result.one).toBe('{count} \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435');
|
|
182
|
+
expect(result.few).toBe('{count} \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f');
|
|
183
|
+
expect(result.many).toBe('{count} \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439');
|
|
184
|
+
expect(result.other).toBe('{count} \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439');
|
|
183
185
|
});
|
|
184
186
|
});
|