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.
@@ -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>>;
@@ -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
+ }
@@ -1 +1,2 @@
1
+ 'use client';
1
2
  export { TransDuckProvider, t, ait, tPlural, aitPlural, useTransDuck, _resetReactState } from './provider.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "transduck",
3
- "version": "0.1.5",
3
+ "version": "0.2.2",
4
4
  "description": "AI-native translation tool using source text as keys",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -42,10 +42,14 @@
42
42
  },
43
43
  "peerDependencies": {
44
44
  "react": ">=18.0.0",
45
- "react-dom": ">=18.0.0"
45
+ "react-dom": ">=18.0.0",
46
+ "@anthropic-ai/sdk": ">=0.30.0",
47
+ "@anthropic-ai/claude-agent-sdk": ">=0.1.0"
46
48
  },
47
49
  "peerDependenciesMeta": {
48
50
  "react": { "optional": true },
49
- "react-dom": { "optional": true }
51
+ "react-dom": { "optional": true },
52
+ "@anthropic-ai/sdk": { "optional": true },
53
+ "@anthropic-ai/claude-agent-sdk": { "optional": true }
50
54
  }
51
55
  }
package/src/backend.ts CHANGED
@@ -1,142 +1,36 @@
1
- import OpenAI from 'openai';
2
-
3
- /* eslint-disable no-template-curly-in-string */
4
- const SYSTEM_TEMPLATE =
5
- 'You are a professional translator. Translate the given text from {source_lang} ' +
6
- 'to {target_lang}. Return ONLY the translated text, nothing else. Preserve any ' +
7
- 'placeholders like {name}, {{count}}, %s, ${value} exactly as they appear. ' +
8
- 'Preserve brand names. Match the tone and formality of the original.\n\n' +
9
- 'Project context: {project_context}';
10
-
11
- const USER_TEMPLATE = `Translate: "{source_text}"\nString context: {string_context}`;
12
-
13
- const PLURAL_SYSTEM_TEMPLATE =
14
- 'You are a professional translator. You will be given two plural forms ' +
15
- '(one and other) in {source_lang}. Generate ALL plural forms needed in ' +
16
- '{target_lang} according to CLDR plural rules. Return ONLY a JSON object ' +
17
- 'mapping plural categories to translated strings. Do not include explanation.\n\n' +
18
- 'CRITICAL: Placeholders like {count}, {name}, %s, ${value} are SOFTWARE VARIABLES ' +
19
- 'that will be replaced by code at runtime. You MUST include them exactly as written ' +
20
- 'in EVERY plural form, even for zero, one, and two categories where the language ' +
21
- 'would not normally use a numeral. For example, for Arabic zero: use "{count} ..." ' +
22
- 'not "لا توجد ...". For Arabic one: use "{count} ..." not "واحدة ...".\n\n' +
23
- 'Preserve brand names. Match the tone and formality of the original.\n\n' +
24
- 'CLDR plural categories are: zero, one, two, few, many, other.\n' +
25
- 'Only include categories that {target_lang} actually uses.\n\n' +
26
- 'Project context: {project_context}';
27
-
28
- const PLURAL_USER_TEMPLATE =
29
- 'Source one form: "{one}"\n' +
30
- 'Source other form: "{other}"\n' +
31
- 'String context: {string_context}';
32
-
33
- function safeRender(template: string, vars: Record<string, string>): string {
34
- let result = template;
35
- for (const [key, value] of Object.entries(vars)) {
36
- result = result.replaceAll(`{${key}}`, value);
37
- }
38
- return result;
39
- }
40
-
41
- interface BuildMessagesParams {
42
- sourceText: string;
43
- sourceLang: string;
44
- targetLang: string;
45
- projectContext: string;
46
- stringContext: string | null;
47
- }
48
-
49
- interface TranslateParams extends BuildMessagesParams {
50
- apiKey: string;
51
- model: string;
52
- timeout: number;
53
- maxRetries: number;
54
- _clientOverride?: any;
55
- }
56
-
57
- interface BuildPluralMessagesParams {
58
- one: string;
59
- other: string;
60
- sourceLang: string;
61
- targetLang: string;
62
- projectContext: string;
63
- stringContext: string | null;
64
- }
65
-
66
- interface TranslatePluralParams extends BuildPluralMessagesParams {
67
- apiKey: string;
68
- model: string;
69
- timeout: number;
70
- maxRetries: number;
71
- _clientOverride?: any;
72
- }
73
-
74
- export function buildMessages(params: BuildMessagesParams): Array<{ role: string; content: string }> {
75
- const systemMsg = SYSTEM_TEMPLATE
76
- .replace('{source_lang}', params.sourceLang)
77
- .replace('{target_lang}', params.targetLang)
78
- .replace('{project_context}', params.projectContext);
79
-
80
- const userMsg = USER_TEMPLATE
81
- .replace('{source_text}', params.sourceText)
82
- .replace('{string_context}', params.stringContext || 'none');
83
-
84
- return [
85
- { role: 'system', content: systemMsg },
86
- { role: 'user', content: userMsg },
87
- ];
88
- }
89
-
90
- export async function translate(params: TranslateParams): Promise<string> {
91
- const client = params._clientOverride ?? new OpenAI({
92
- apiKey: params.apiKey,
93
- timeout: params.timeout * 1000,
94
- maxRetries: params.maxRetries,
95
- });
96
-
97
- const messages = buildMessages(params);
98
- const response = await client.chat.completions.create({
99
- model: params.model,
100
- messages,
101
- temperature: 0.3,
102
- });
103
-
104
- return response.choices[0].message.content.trim();
105
- }
106
-
107
- export function buildPluralMessages(params: BuildPluralMessagesParams): Array<{ role: string; content: string }> {
108
- const systemMsg = safeRender(PLURAL_SYSTEM_TEMPLATE, {
109
- source_lang: params.sourceLang,
110
- target_lang: params.targetLang,
111
- project_context: params.projectContext,
112
- });
113
-
114
- const userMsg = safeRender(PLURAL_USER_TEMPLATE, {
115
- one: params.one,
116
- other: params.other,
117
- string_context: params.stringContext || 'none',
118
- });
119
-
120
- return [
121
- { role: 'system', content: systemMsg },
122
- { role: 'user', content: userMsg },
123
- ];
124
- }
125
-
126
- export async function translatePlural(params: TranslatePluralParams): Promise<Record<string, string>> {
127
- const client = params._clientOverride ?? new OpenAI({
128
- apiKey: params.apiKey,
129
- timeout: params.timeout * 1000,
130
- maxRetries: params.maxRetries,
131
- });
132
-
133
- const messages = buildPluralMessages(params);
134
- const response = await client.chat.completions.create({
135
- model: params.model,
136
- messages,
137
- temperature: 0.3,
138
- });
139
-
140
- const raw = response.choices[0].message.content.trim();
141
- return JSON.parse(raw);
1
+ /**
2
+ * Translation backend router -- delegates to the configured provider.
3
+ */
4
+
5
+ import type { TransduckConfig } from './config.js';
6
+ import { getProvider } from './providers/index.js';
7
+
8
+ // Re-export prompts for backward compat (tests import buildMessages from backend)
9
+ export { buildMessages, buildPluralMessages } from './providers/prompts.js';
10
+
11
+ export async function translate(
12
+ sourceText: string,
13
+ sourceLang: string,
14
+ targetLang: string,
15
+ projectContext: string,
16
+ stringContext: string | null,
17
+ config: TransduckConfig,
18
+ _clientOverride?: any,
19
+ ): Promise<string> {
20
+ const provider = await getProvider(config);
21
+ return provider.translate(sourceText, sourceLang, targetLang, projectContext, stringContext, config, _clientOverride);
22
+ }
23
+
24
+ export async function translatePlural(
25
+ one: string,
26
+ other: string,
27
+ sourceLang: string,
28
+ targetLang: string,
29
+ projectContext: string,
30
+ stringContext: string | null,
31
+ config: TransduckConfig,
32
+ _clientOverride?: any,
33
+ ): Promise<Record<string, string>> {
34
+ const provider = await getProvider(config);
35
+ return provider.translatePlural(one, other, sourceLang, targetLang, projectContext, stringContext, config, _clientOverride);
142
36
  }