transduck 0.5.3 → 0.6.0
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 +1 -0
- package/dist/backend.js +6 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +64 -12
- package/dist/config.d.ts +1 -0
- package/dist/config.js +2 -0
- package/dist/handler.d.ts +2 -0
- package/dist/handler.js +79 -41
- package/dist/index.d.ts +17 -2
- package/dist/index.js +191 -92
- package/dist/providers/claude-api.d.ts +1 -0
- package/dist/providers/claude-api.js +11 -0
- package/dist/providers/claude-code.d.ts +1 -0
- package/dist/providers/claude-code.js +6 -0
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/openai-provider.d.ts +1 -0
- package/dist/providers/openai-provider.js +17 -0
- package/dist/result.d.ts +19 -0
- package/dist/result.js +26 -0
- package/dist/shared-store.d.ts +18 -0
- package/dist/shared-store.js +126 -0
- package/package.json +5 -1
- package/src/backend.ts +10 -0
- package/src/cli.ts +64 -12
- package/src/config.ts +4 -0
- package/src/handler.ts +81 -54
- package/src/index.ts +277 -98
- package/src/providers/claude-api.ts +16 -0
- package/src/providers/claude-code.ts +10 -0
- package/src/providers/index.ts +6 -0
- package/src/providers/openai-provider.ts +24 -0
- package/src/result.ts +30 -0
- package/src/shared-store.ts +157 -0
- package/tests/ait.test.ts +152 -14
- package/tests/backend.test.ts +34 -1
- package/tests/cli.test.ts +33 -0
- package/tests/config.test.ts +40 -0
- package/tests/result.test.ts +62 -0
- package/tests/shared-store.test.ts +210 -0
package/dist/index.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
import { loadConfig } from './config.js';
|
|
3
3
|
import { TranslationStore } from './storage.js';
|
|
4
|
-
import {
|
|
4
|
+
import { SharedStore } from './shared-store.js';
|
|
5
|
+
import { TranslationResult } from './result.js';
|
|
6
|
+
import { translate as backendTranslate, translatePlural as backendTranslatePlural, detectLanguage as backendDetectLanguage } from './backend.js';
|
|
5
7
|
import { validateTranslation, extractPlaceholders } from './validation.js';
|
|
6
8
|
import { getPluralCategory, getPluralCategories, interpolateVars } from './plural.js';
|
|
7
9
|
let state = {
|
|
8
10
|
config: null,
|
|
9
11
|
store: null,
|
|
12
|
+
sharedStore: null,
|
|
10
13
|
targetLang: null,
|
|
11
14
|
pendingTranslations: new Map(),
|
|
15
|
+
backgroundKeys: new Set(),
|
|
12
16
|
};
|
|
13
17
|
function hash(text) {
|
|
14
18
|
return createHash('sha256').update(text).digest('hex');
|
|
@@ -19,85 +23,156 @@ export async function initialize(config) {
|
|
|
19
23
|
await store.initialize();
|
|
20
24
|
state.config = cfg;
|
|
21
25
|
state.store = store;
|
|
26
|
+
if (cfg.sharedUrl) {
|
|
27
|
+
try {
|
|
28
|
+
const shared = new SharedStore(cfg.sharedUrl);
|
|
29
|
+
await shared.initialize();
|
|
30
|
+
state.sharedStore = shared;
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
console.warn(`[transduck] Could not connect to shared store: ${err.message}`);
|
|
34
|
+
state.sharedStore = null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
22
37
|
}
|
|
23
38
|
export function setLanguage(lang) {
|
|
24
39
|
state.targetLang = lang.toUpperCase();
|
|
25
40
|
}
|
|
26
41
|
export function _resetState() {
|
|
27
|
-
state = {
|
|
42
|
+
state = {
|
|
43
|
+
config: null, store: null, sharedStore: null, targetLang: null,
|
|
44
|
+
pendingTranslations: new Map(), backgroundKeys: new Set(),
|
|
45
|
+
};
|
|
28
46
|
}
|
|
29
47
|
export function _getStore() {
|
|
30
48
|
return state.store;
|
|
31
49
|
}
|
|
32
|
-
export
|
|
50
|
+
export function _getSharedStore() {
|
|
51
|
+
return state.sharedStore;
|
|
52
|
+
}
|
|
53
|
+
export function _setSharedStore(shared) {
|
|
54
|
+
state.sharedStore = shared;
|
|
55
|
+
}
|
|
56
|
+
export async function ait(sourceText, contextOrOpts, vars) {
|
|
33
57
|
if (!state.config || !state.store) {
|
|
34
58
|
throw new Error('transduck not initialized. Call initialize() first.');
|
|
35
59
|
}
|
|
36
60
|
if (!state.targetLang) {
|
|
37
61
|
throw new Error('Target language not set. Call setLanguage() first.');
|
|
38
62
|
}
|
|
63
|
+
// Parse overloaded args: ait(text, context?, vars?) OR ait(text, opts)
|
|
64
|
+
let context;
|
|
65
|
+
let resolvedVars;
|
|
66
|
+
let sourceLang;
|
|
67
|
+
let background = false;
|
|
68
|
+
if (typeof contextOrOpts === 'string') {
|
|
69
|
+
context = contextOrOpts;
|
|
70
|
+
resolvedVars = vars;
|
|
71
|
+
}
|
|
72
|
+
else if (contextOrOpts != null) {
|
|
73
|
+
context = contextOrOpts.context;
|
|
74
|
+
resolvedVars = contextOrOpts.vars;
|
|
75
|
+
sourceLang = contextOrOpts.sourceLang;
|
|
76
|
+
background = contextOrOpts.background ?? false;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
resolvedVars = vars;
|
|
80
|
+
}
|
|
39
81
|
const cfg = state.config;
|
|
40
|
-
|
|
41
|
-
|
|
82
|
+
const effectiveSourceLang = sourceLang?.toUpperCase() ?? cfg.sourceLang;
|
|
83
|
+
const targetLang = state.targetLang;
|
|
84
|
+
if (targetLang === effectiveSourceLang) {
|
|
85
|
+
return new TranslationResult(interpolateVars(sourceText, resolvedVars), { pending: false, sourceLang: effectiveSourceLang, lang: targetLang });
|
|
42
86
|
}
|
|
43
87
|
const projectContextHash = hash(cfg.projectContext);
|
|
44
88
|
const stringContextHash = hash(context ?? '');
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
sourceText, sourceLang: cfg.sourceLang, targetLang: state.targetLang,
|
|
89
|
+
const lookupParams = {
|
|
90
|
+
sourceText, sourceLang: effectiveSourceLang, targetLang,
|
|
48
91
|
projectContextHash, stringContextHash,
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
92
|
+
};
|
|
93
|
+
// Tier 1: local SQLite lookup
|
|
94
|
+
const cached = await state.store.lookup(lookupParams);
|
|
95
|
+
if (cached !== null) {
|
|
96
|
+
return new TranslationResult(interpolateVars(cached, resolvedVars), { pending: false, sourceLang: effectiveSourceLang, lang: targetLang });
|
|
97
|
+
}
|
|
98
|
+
// Tier 2: shared Postgres lookup
|
|
99
|
+
if (state.sharedStore) {
|
|
100
|
+
const sharedCached = await state.sharedStore.lookup(lookupParams);
|
|
101
|
+
if (sharedCached !== null) {
|
|
102
|
+
// Propagate to local store
|
|
103
|
+
await state.store.insert({
|
|
104
|
+
...lookupParams, stringContext: context ?? '',
|
|
105
|
+
translatedText: sharedCached, model: cfg.backendModel, status: 'translated',
|
|
106
|
+
});
|
|
107
|
+
return new TranslationResult(interpolateVars(sharedCached, resolvedVars), { pending: false, sourceLang: effectiveSourceLang, lang: targetLang });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
52
110
|
// Read-only mode: skip backend, return source text
|
|
53
111
|
if (cfg.readOnly) {
|
|
54
|
-
return interpolateVars(sourceText,
|
|
112
|
+
return new TranslationResult(interpolateVars(sourceText, resolvedVars), { pending: false, sourceLang: effectiveSourceLang, lang: targetLang });
|
|
113
|
+
}
|
|
114
|
+
// Background mode: return immediately with pending=true
|
|
115
|
+
if (background) {
|
|
116
|
+
const bgKey = `${sourceText}|${effectiveSourceLang}|${targetLang}|${projectContextHash}|${stringContextHash}`;
|
|
117
|
+
if (!state.backgroundKeys.has(bgKey)) {
|
|
118
|
+
state.backgroundKeys.add(bgKey);
|
|
119
|
+
_doTranslate(sourceText, effectiveSourceLang, targetLang, projectContextHash, stringContextHash, context ?? '', cfg)
|
|
120
|
+
.finally(() => state.backgroundKeys.delete(bgKey));
|
|
121
|
+
}
|
|
122
|
+
return new TranslationResult(interpolateVars(sourceText, resolvedVars), { pending: true, sourceLang: effectiveSourceLang, lang: targetLang });
|
|
55
123
|
}
|
|
56
124
|
// In-process dedup
|
|
57
|
-
const lockKey = `${sourceText}|${
|
|
125
|
+
const lockKey = `${sourceText}|${effectiveSourceLang}|${targetLang}|${projectContextHash}|${stringContextHash}`;
|
|
58
126
|
if (state.pendingTranslations.has(lockKey)) {
|
|
59
127
|
const pending = await state.pendingTranslations.get(lockKey);
|
|
60
|
-
return interpolateVars(pending,
|
|
128
|
+
return new TranslationResult(interpolateVars(pending, resolvedVars), { pending: false, sourceLang: effectiveSourceLang, lang: targetLang });
|
|
61
129
|
}
|
|
62
|
-
const translationPromise = (
|
|
63
|
-
// Double-check after getting in
|
|
64
|
-
const rechecked = await state.store.lookup({
|
|
65
|
-
sourceText, sourceLang: cfg.sourceLang, targetLang: state.targetLang,
|
|
66
|
-
projectContextHash, stringContextHash,
|
|
67
|
-
});
|
|
68
|
-
if (rechecked !== null)
|
|
69
|
-
return rechecked;
|
|
70
|
-
try {
|
|
71
|
-
const translated = await backendTranslate(sourceText, cfg.sourceLang, state.targetLang, cfg.projectContext, context ?? null, cfg);
|
|
72
|
-
if (!validateTranslation(sourceText, translated)) {
|
|
73
|
-
console.warn(`[transduck] Validation failed for: ${sourceText} -> ${translated}`);
|
|
74
|
-
await state.store.insert({
|
|
75
|
-
sourceText, sourceLang: cfg.sourceLang, targetLang: state.targetLang,
|
|
76
|
-
projectContextHash, stringContextHash, stringContext: context ?? '',
|
|
77
|
-
translatedText: translated, model: cfg.backendModel, status: 'failed',
|
|
78
|
-
});
|
|
79
|
-
return sourceText;
|
|
80
|
-
}
|
|
81
|
-
await state.store.insert({
|
|
82
|
-
sourceText, sourceLang: cfg.sourceLang, targetLang: state.targetLang,
|
|
83
|
-
projectContextHash, stringContextHash, stringContext: context ?? '',
|
|
84
|
-
translatedText: translated, model: cfg.backendModel, status: 'translated',
|
|
85
|
-
});
|
|
86
|
-
return translated;
|
|
87
|
-
}
|
|
88
|
-
catch (err) {
|
|
89
|
-
console.warn(`[transduck] Backend failed for: ${sourceText}`, err);
|
|
90
|
-
return sourceText;
|
|
91
|
-
}
|
|
92
|
-
finally {
|
|
93
|
-
state.pendingTranslations.delete(lockKey);
|
|
94
|
-
}
|
|
95
|
-
})();
|
|
130
|
+
const translationPromise = _doTranslate(sourceText, effectiveSourceLang, targetLang, projectContextHash, stringContextHash, context ?? '', cfg);
|
|
96
131
|
state.pendingTranslations.set(lockKey, translationPromise);
|
|
97
132
|
const result = await translationPromise;
|
|
98
|
-
|
|
133
|
+
state.pendingTranslations.delete(lockKey);
|
|
134
|
+
return new TranslationResult(interpolateVars(result, resolvedVars), { pending: false, sourceLang: effectiveSourceLang, lang: targetLang });
|
|
135
|
+
}
|
|
136
|
+
async function _doTranslate(sourceText, sourceLang, targetLang, projectContextHash, stringContextHash, stringContext, cfg) {
|
|
137
|
+
// Double-check local cache
|
|
138
|
+
const rechecked = await state.store.lookup({
|
|
139
|
+
sourceText, sourceLang, targetLang, projectContextHash, stringContextHash,
|
|
140
|
+
});
|
|
141
|
+
if (rechecked !== null)
|
|
142
|
+
return rechecked;
|
|
143
|
+
try {
|
|
144
|
+
const translated = await backendTranslate(sourceText, sourceLang, targetLang, cfg.projectContext, stringContext || null, cfg);
|
|
145
|
+
const insertParams = {
|
|
146
|
+
sourceText, sourceLang, targetLang,
|
|
147
|
+
projectContextHash, stringContextHash, stringContext,
|
|
148
|
+
translatedText: translated, model: cfg.backendModel,
|
|
149
|
+
};
|
|
150
|
+
if (!validateTranslation(sourceText, translated)) {
|
|
151
|
+
console.warn(`[transduck] Validation failed for: ${sourceText} -> ${translated}`);
|
|
152
|
+
await state.store.insert({ ...insertParams, status: 'failed' });
|
|
153
|
+
if (state.sharedStore)
|
|
154
|
+
await state.sharedStore.insert({ ...insertParams, status: 'failed' });
|
|
155
|
+
return sourceText;
|
|
156
|
+
}
|
|
157
|
+
await state.store.insert({ ...insertParams, status: 'translated' });
|
|
158
|
+
if (state.sharedStore)
|
|
159
|
+
await state.sharedStore.insert({ ...insertParams, status: 'translated' });
|
|
160
|
+
return translated;
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
console.warn(`[transduck] Backend failed for: ${sourceText}`, err);
|
|
164
|
+
return sourceText;
|
|
165
|
+
}
|
|
99
166
|
}
|
|
100
167
|
export { createTransDuckHandler } from './handler.js';
|
|
168
|
+
export { TranslationResult } from './result.js';
|
|
169
|
+
export { SharedStore } from './shared-store.js';
|
|
170
|
+
export async function detectLanguage(text) {
|
|
171
|
+
if (!state.config) {
|
|
172
|
+
throw new Error('transduck not initialized. Call initialize() first.');
|
|
173
|
+
}
|
|
174
|
+
return backendDetectLanguage(text, state.config);
|
|
175
|
+
}
|
|
101
176
|
export async function aitPlural(one, other, count, opts) {
|
|
102
177
|
if (!state.config || !state.store) {
|
|
103
178
|
throw new Error('transduck not initialized. Call initialize() first.');
|
|
@@ -107,6 +182,8 @@ export async function aitPlural(one, other, count, opts) {
|
|
|
107
182
|
}
|
|
108
183
|
const cfg = state.config;
|
|
109
184
|
const context = opts?.context;
|
|
185
|
+
const effectiveSourceLang = opts?.sourceLang?.toUpperCase() ?? cfg.sourceLang;
|
|
186
|
+
const background = opts?.background ?? false;
|
|
110
187
|
// Build vars with count
|
|
111
188
|
let vars;
|
|
112
189
|
if (!opts?.vars) {
|
|
@@ -118,37 +195,76 @@ export async function aitPlural(one, other, count, opts) {
|
|
|
118
195
|
else {
|
|
119
196
|
vars = { ...opts.vars };
|
|
120
197
|
}
|
|
198
|
+
const targetLang = state.targetLang;
|
|
121
199
|
// Same language, 2-form language: select directly from provided forms
|
|
122
|
-
if (
|
|
123
|
-
const categories = getPluralCategories(
|
|
200
|
+
if (targetLang === effectiveSourceLang) {
|
|
201
|
+
const categories = getPluralCategories(effectiveSourceLang);
|
|
124
202
|
if (categories.size <= 2) {
|
|
125
|
-
const category = getPluralCategory(
|
|
203
|
+
const category = getPluralCategory(effectiveSourceLang, count);
|
|
126
204
|
const form = category === 'one' ? one : other;
|
|
127
|
-
return interpolateVars(form, vars);
|
|
205
|
+
return new TranslationResult(interpolateVars(form, vars), { pending: false, sourceLang: effectiveSourceLang, lang: targetLang });
|
|
128
206
|
}
|
|
129
207
|
}
|
|
130
208
|
// Build cache key
|
|
131
209
|
const sourceKey = one + '\x00' + other;
|
|
132
210
|
const projectContextHash = hash(cfg.projectContext);
|
|
133
211
|
const stringContextHash = hash(context ?? '');
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
sourceText: sourceKey, sourceLang: cfg.sourceLang, targetLang: state.targetLang,
|
|
212
|
+
const lookupParams = {
|
|
213
|
+
sourceText: sourceKey, sourceLang: effectiveSourceLang, targetLang,
|
|
137
214
|
projectContextHash, stringContextHash,
|
|
138
|
-
}
|
|
139
|
-
const category = getPluralCategory(
|
|
215
|
+
};
|
|
216
|
+
const category = getPluralCategory(targetLang, count);
|
|
217
|
+
// Tier 1: local cache lookup
|
|
218
|
+
const cached = await state.store.lookupPlural(lookupParams);
|
|
140
219
|
if (category in cached) {
|
|
141
|
-
return interpolateVars(cached[category], vars);
|
|
220
|
+
return new TranslationResult(interpolateVars(cached[category], vars), { pending: false, sourceLang: effectiveSourceLang, lang: targetLang });
|
|
221
|
+
}
|
|
222
|
+
// Tier 2: shared store lookup
|
|
223
|
+
if (state.sharedStore) {
|
|
224
|
+
const sharedCached = await state.sharedStore.lookupPlural(lookupParams);
|
|
225
|
+
if (category in sharedCached) {
|
|
226
|
+
// Propagate all forms to local store
|
|
227
|
+
for (const [cat, text] of Object.entries(sharedCached)) {
|
|
228
|
+
await state.store.insertPlural({
|
|
229
|
+
...lookupParams, stringContext: context ?? '',
|
|
230
|
+
pluralCategory: cat, translatedText: text,
|
|
231
|
+
model: cfg.backendModel, status: 'translated',
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return new TranslationResult(interpolateVars(sharedCached[category], vars), { pending: false, sourceLang: effectiveSourceLang, lang: targetLang });
|
|
235
|
+
}
|
|
142
236
|
}
|
|
143
237
|
// Read-only mode: skip backend, fall back to source forms
|
|
144
238
|
if (cfg.readOnly) {
|
|
145
|
-
const fallbackCategory = getPluralCategory(
|
|
239
|
+
const fallbackCategory = getPluralCategory(effectiveSourceLang, count);
|
|
146
240
|
const fallback = fallbackCategory === 'one' ? one : other;
|
|
147
|
-
return interpolateVars(fallback, vars);
|
|
241
|
+
return new TranslationResult(interpolateVars(fallback, vars), { pending: false, sourceLang: effectiveSourceLang, lang: targetLang });
|
|
242
|
+
}
|
|
243
|
+
// Background mode
|
|
244
|
+
if (background) {
|
|
245
|
+
const bgKey = `plural|${sourceKey}|${effectiveSourceLang}|${targetLang}|${projectContextHash}|${stringContextHash}`;
|
|
246
|
+
if (!state.backgroundKeys.has(bgKey)) {
|
|
247
|
+
state.backgroundKeys.add(bgKey);
|
|
248
|
+
_doPluralTranslate(one, other, sourceKey, effectiveSourceLang, targetLang, projectContextHash, stringContextHash, context ?? '', cfg)
|
|
249
|
+
.finally(() => state.backgroundKeys.delete(bgKey));
|
|
250
|
+
}
|
|
251
|
+
const fallbackCategory = getPluralCategory(effectiveSourceLang, count);
|
|
252
|
+
const fallback = fallbackCategory === 'one' ? one : other;
|
|
253
|
+
return new TranslationResult(interpolateVars(fallback, vars), { pending: true, sourceLang: effectiveSourceLang, lang: targetLang });
|
|
148
254
|
}
|
|
149
255
|
// Cache miss — call backend
|
|
256
|
+
const translatedText = await _doPluralTranslate(one, other, sourceKey, effectiveSourceLang, targetLang, projectContextHash, stringContextHash, context ?? '', cfg);
|
|
257
|
+
if (translatedText !== null && category in translatedText) {
|
|
258
|
+
return new TranslationResult(interpolateVars(translatedText[category], vars), { pending: false, sourceLang: effectiveSourceLang, lang: targetLang });
|
|
259
|
+
}
|
|
260
|
+
// Fallback
|
|
261
|
+
const fallbackCategory = getPluralCategory(effectiveSourceLang, count);
|
|
262
|
+
const fallback = fallbackCategory === 'one' ? one : other;
|
|
263
|
+
return new TranslationResult(interpolateVars(fallback, vars), { pending: false, sourceLang: effectiveSourceLang, lang: targetLang });
|
|
264
|
+
}
|
|
265
|
+
async function _doPluralTranslate(one, other, sourceKey, sourceLang, targetLang, projectContextHash, stringContextHash, stringContext, cfg) {
|
|
150
266
|
try {
|
|
151
|
-
const forms = await backendTranslatePlural(one, other,
|
|
267
|
+
const forms = await backendTranslatePlural(one, other, sourceLang, targetLang, cfg.projectContext, stringContext || null, cfg);
|
|
152
268
|
// Validate and store each form
|
|
153
269
|
const validCategories = new Set(['zero', 'one', 'two', 'few', 'many', 'other']);
|
|
154
270
|
const sourcePlaceholders = new Set([
|
|
@@ -157,14 +273,15 @@ export async function aitPlural(one, other, count, opts) {
|
|
|
157
273
|
]);
|
|
158
274
|
if (typeof forms !== 'object' || forms === null || !('other' in forms)) {
|
|
159
275
|
console.warn(`[transduck] Invalid plural response for: ${one} / ${other}`);
|
|
160
|
-
|
|
161
|
-
const fallback = fallbackCategory === 'one' ? one : other;
|
|
162
|
-
return interpolateVars(fallback, vars);
|
|
276
|
+
return null;
|
|
163
277
|
}
|
|
278
|
+
const lookupParams = {
|
|
279
|
+
sourceText: sourceKey, sourceLang, targetLang,
|
|
280
|
+
projectContextHash, stringContextHash,
|
|
281
|
+
};
|
|
164
282
|
for (const [cat, text] of Object.entries(forms)) {
|
|
165
283
|
if (!validCategories.has(cat) || !text)
|
|
166
284
|
continue;
|
|
167
|
-
// Validate placeholders
|
|
168
285
|
const translatedPlaceholders = extractPlaceholders(text);
|
|
169
286
|
let allPresent = true;
|
|
170
287
|
for (const p of sourcePlaceholders) {
|
|
@@ -174,37 +291,19 @@ export async function aitPlural(one, other, count, opts) {
|
|
|
174
291
|
}
|
|
175
292
|
}
|
|
176
293
|
const status = allPresent ? 'translated' : 'failed';
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
projectContextHash, stringContextHash, stringContext: context ?? '',
|
|
294
|
+
const insertParams = {
|
|
295
|
+
...lookupParams, stringContext,
|
|
180
296
|
pluralCategory: cat, translatedText: text,
|
|
181
297
|
model: cfg.backendModel, status,
|
|
182
|
-
}
|
|
298
|
+
};
|
|
299
|
+
await state.store.insertPlural(insertParams);
|
|
300
|
+
if (state.sharedStore)
|
|
301
|
+
await state.sharedStore.insertPlural(insertParams);
|
|
183
302
|
}
|
|
184
|
-
|
|
185
|
-
if (category in forms) {
|
|
186
|
-
const text = forms[category];
|
|
187
|
-
const tp = extractPlaceholders(text);
|
|
188
|
-
let allPresent = true;
|
|
189
|
-
for (const p of sourcePlaceholders) {
|
|
190
|
-
if (!tp.has(p)) {
|
|
191
|
-
allPresent = false;
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
if (allPresent) {
|
|
196
|
-
return interpolateVars(text, vars);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
// Fallback
|
|
200
|
-
const fallbackCategory = getPluralCategory(cfg.sourceLang, count);
|
|
201
|
-
const fallback = fallbackCategory === 'one' ? one : other;
|
|
202
|
-
return interpolateVars(fallback, vars);
|
|
303
|
+
return forms;
|
|
203
304
|
}
|
|
204
305
|
catch (err) {
|
|
205
306
|
console.warn(`[transduck] Backend failed for plural: ${one} / ${other}`, err);
|
|
206
|
-
|
|
207
|
-
const fallback = fallbackCategory === 'one' ? one : other;
|
|
208
|
-
return interpolateVars(fallback, vars);
|
|
307
|
+
return null;
|
|
209
308
|
}
|
|
210
309
|
}
|
|
@@ -4,4 +4,5 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { TransduckConfig } from '../config.js';
|
|
6
6
|
export declare function translate(sourceText: string, sourceLang: string, targetLang: string, projectContext: string, stringContext: string | null, config: TransduckConfig): Promise<string>;
|
|
7
|
+
export declare function detectLanguage(text: string, config: TransduckConfig): Promise<string>;
|
|
7
8
|
export declare function translatePlural(one: string, other: string, sourceLang: string, targetLang: string, projectContext: string, stringContext: string | null, config: TransduckConfig): Promise<Record<string, string>>;
|
|
@@ -33,6 +33,17 @@ export async function translate(sourceText, sourceLang, targetLang, projectConte
|
|
|
33
33
|
});
|
|
34
34
|
return response.content[0].text.trim();
|
|
35
35
|
}
|
|
36
|
+
export async function detectLanguage(text, config) {
|
|
37
|
+
const client = await getClient(config);
|
|
38
|
+
const response = await client.messages.create({
|
|
39
|
+
model: config.backendModel,
|
|
40
|
+
max_tokens: 16,
|
|
41
|
+
temperature: 0.0,
|
|
42
|
+
system: 'What language is this text written in? Return only the uppercase ISO 639-1 code.',
|
|
43
|
+
messages: [{ role: 'user', content: text }],
|
|
44
|
+
});
|
|
45
|
+
return response.content[0].text.trim().toUpperCase();
|
|
46
|
+
}
|
|
36
47
|
export async function translatePlural(one, other, sourceLang, targetLang, projectContext, stringContext, config) {
|
|
37
48
|
const client = await getClient(config);
|
|
38
49
|
const messages = buildPluralMessages({
|
|
@@ -5,4 +5,5 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { TransduckConfig } from '../config.js';
|
|
7
7
|
export declare function translate(sourceText: string, sourceLang: string, targetLang: string, projectContext: string, stringContext: string | null, config: TransduckConfig): Promise<string>;
|
|
8
|
+
export declare function detectLanguage(text: string, config: TransduckConfig): Promise<string>;
|
|
8
9
|
export declare function translatePlural(one: string, other: string, sourceLang: string, targetLang: string, projectContext: string, stringContext: string | null, config: TransduckConfig): Promise<Record<string, string>>;
|
|
@@ -37,6 +37,12 @@ export async function translate(sourceText, sourceLang, targetLang, projectConte
|
|
|
37
37
|
});
|
|
38
38
|
return translateWithSdk(prompt);
|
|
39
39
|
}
|
|
40
|
+
export async function detectLanguage(text, config) {
|
|
41
|
+
ensureToken(config);
|
|
42
|
+
const prompt = 'What language is this text written in? Return only the uppercase ISO 639-1 code.\n\n' + text;
|
|
43
|
+
const raw = await translateWithSdk(prompt);
|
|
44
|
+
return raw.trim().toUpperCase();
|
|
45
|
+
}
|
|
40
46
|
export async function translatePlural(one, other, sourceLang, targetLang, projectContext, stringContext, config) {
|
|
41
47
|
ensureToken(config);
|
|
42
48
|
const prompt = buildPluralSinglePrompt({
|
|
@@ -5,6 +5,7 @@ import type { TransduckConfig } from '../config.js';
|
|
|
5
5
|
export interface TranslationProvider {
|
|
6
6
|
translate(sourceText: string, sourceLang: string, targetLang: string, projectContext: string, stringContext: string | null, config: TransduckConfig, _clientOverride?: any): Promise<string>;
|
|
7
7
|
translatePlural(one: string, other: string, sourceLang: string, targetLang: string, projectContext: string, stringContext: string | null, config: TransduckConfig, _clientOverride?: any): Promise<Record<string, string>>;
|
|
8
|
+
detectLanguage(text: string, config: TransduckConfig, _clientOverride?: any): Promise<string>;
|
|
8
9
|
}
|
|
9
10
|
/**
|
|
10
11
|
* Return the provider module for the configured provider.
|
|
@@ -3,4 +3,5 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { TransduckConfig } from '../config.js';
|
|
5
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 detectLanguage(text: string, config: TransduckConfig, _clientOverride?: any): Promise<string>;
|
|
6
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>>;
|
|
@@ -20,6 +20,23 @@ export async function translate(sourceText, sourceLang, targetLang, projectConte
|
|
|
20
20
|
});
|
|
21
21
|
return response.choices[0].message.content.trim();
|
|
22
22
|
}
|
|
23
|
+
export async function detectLanguage(text, 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 response = await client.chat.completions.create({
|
|
31
|
+
model: config.backendModel,
|
|
32
|
+
messages: [
|
|
33
|
+
{ role: 'system', content: 'What language is this text written in? Return only the uppercase ISO 639-1 code.' },
|
|
34
|
+
{ role: 'user', content: text },
|
|
35
|
+
],
|
|
36
|
+
temperature: 0.0,
|
|
37
|
+
});
|
|
38
|
+
return response.choices[0].message.content.trim().toUpperCase();
|
|
39
|
+
}
|
|
23
40
|
export async function translatePlural(one, other, sourceLang, targetLang, projectContext, stringContext, config, _clientOverride) {
|
|
24
41
|
const apiKey = process.env[config.apiKeyEnv];
|
|
25
42
|
const client = _clientOverride ?? new OpenAI({
|
package/dist/result.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A string wrapper that carries translation metadata.
|
|
3
|
+
*
|
|
4
|
+
* Works in template literals, concatenation, and equality checks via
|
|
5
|
+
* toString() and valueOf() overrides, plus Symbol.toPrimitive.
|
|
6
|
+
*/
|
|
7
|
+
export declare class TranslationResult extends String {
|
|
8
|
+
readonly pending: boolean;
|
|
9
|
+
readonly sourceLang: string;
|
|
10
|
+
readonly lang: string;
|
|
11
|
+
constructor(text: string, opts: {
|
|
12
|
+
pending: boolean;
|
|
13
|
+
sourceLang: string;
|
|
14
|
+
lang: string;
|
|
15
|
+
});
|
|
16
|
+
toString(): string;
|
|
17
|
+
valueOf(): string;
|
|
18
|
+
[Symbol.toPrimitive](_hint: string): string;
|
|
19
|
+
}
|
package/dist/result.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A string wrapper that carries translation metadata.
|
|
3
|
+
*
|
|
4
|
+
* Works in template literals, concatenation, and equality checks via
|
|
5
|
+
* toString() and valueOf() overrides, plus Symbol.toPrimitive.
|
|
6
|
+
*/
|
|
7
|
+
export class TranslationResult extends String {
|
|
8
|
+
pending;
|
|
9
|
+
sourceLang;
|
|
10
|
+
lang;
|
|
11
|
+
constructor(text, opts) {
|
|
12
|
+
super(text);
|
|
13
|
+
this.pending = opts.pending;
|
|
14
|
+
this.sourceLang = opts.sourceLang;
|
|
15
|
+
this.lang = opts.lang;
|
|
16
|
+
}
|
|
17
|
+
toString() {
|
|
18
|
+
return super.toString();
|
|
19
|
+
}
|
|
20
|
+
valueOf() {
|
|
21
|
+
return super.valueOf();
|
|
22
|
+
}
|
|
23
|
+
[Symbol.toPrimitive](_hint) {
|
|
24
|
+
return super.valueOf();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { LookupParams, InsertParams, InsertPluralParams } from './storage.js';
|
|
2
|
+
export declare class SharedStore {
|
|
3
|
+
private pool;
|
|
4
|
+
private url;
|
|
5
|
+
constructor(url: string);
|
|
6
|
+
initialize(): Promise<void>;
|
|
7
|
+
private getPool;
|
|
8
|
+
lookup(params: LookupParams): Promise<string | null>;
|
|
9
|
+
insert(params: InsertParams): Promise<void>;
|
|
10
|
+
lookupPlural(params: LookupParams): Promise<Record<string, string>>;
|
|
11
|
+
insertPlural(params: InsertPluralParams): Promise<void>;
|
|
12
|
+
stats(): Promise<{
|
|
13
|
+
totalTranslations: number;
|
|
14
|
+
totalFailed: number;
|
|
15
|
+
byLanguage: Record<string, number>;
|
|
16
|
+
}>;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
}
|