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 CHANGED
@@ -3,5 +3,6 @@
3
3
  */
4
4
  import type { TransduckConfig } from './config.js';
5
5
  export { buildMessages, buildPluralMessages } from './providers/prompts.js';
6
+ export declare function detectLanguage(text: string, config: TransduckConfig, _clientOverride?: any): Promise<string>;
6
7
  export declare function translate(sourceText: string, sourceLang: string, targetLang: string, projectContext: string, stringContext: string | null, config: TransduckConfig, _clientOverride?: any): Promise<string>;
7
8
  export declare function translatePlural(one: string, other: string, sourceLang: string, targetLang: string, projectContext: string, stringContext: string | null, config: TransduckConfig, _clientOverride?: any): Promise<Record<string, string>>;
package/dist/backend.js CHANGED
@@ -20,6 +20,12 @@ function checkApiKey(config) {
20
20
  `Or add it to your .env file.`);
21
21
  }
22
22
  }
23
+ export async function detectLanguage(text, config, _clientOverride) {
24
+ if (!_clientOverride)
25
+ checkApiKey(config);
26
+ const provider = await getProvider(config);
27
+ return provider.detectLanguage(text, config, _clientOverride);
28
+ }
23
29
  export async function translate(sourceText, sourceLang, targetLang, projectContext, stringContext, config, _clientOverride) {
24
30
  if (!_clientOverride)
25
31
  checkApiKey(config);
package/dist/cli.d.ts CHANGED
@@ -7,6 +7,7 @@ export interface InitOptions {
7
7
  sourceLang: string;
8
8
  targetLangs: string[];
9
9
  provider?: number;
10
+ sharedUrl?: string;
10
11
  }
11
12
  export declare function runInit(opts: InitOptions): Promise<string>;
12
13
  export interface TranslateOptions {
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import { Command } from 'commander';
6
6
  import { stringify as yamlStringify } from 'yaml';
7
7
  import { loadConfig } from './config.js';
8
8
  import { TranslationStore } from './storage.js';
9
+ import { SharedStore } from './shared-store.js';
9
10
  import { translate as backendTranslate, translatePlural as backendTranslatePlural } from './backend.js';
10
11
  import { validateTranslation, extractPlaceholders } from './validation.js';
11
12
  import { getPluralCategory, interpolateVars } from './plural.js';
@@ -15,10 +16,14 @@ function hash(text) {
15
16
  }
16
17
  export async function runInit(opts) {
17
18
  const providerChoice = opts.provider ?? 1;
19
+ const storage = { path: './translations.db' };
20
+ if (opts.sharedUrl) {
21
+ storage.shared_url = opts.sharedUrl;
22
+ }
18
23
  const config = {
19
24
  project: { name: opts.name, context: opts.context },
20
25
  languages: { source: opts.sourceLang.toUpperCase(), targets: opts.targetLangs.map(l => l.toUpperCase()) },
21
- storage: { path: './translations.db' },
26
+ storage,
22
27
  };
23
28
  if (providerChoice === 2) {
24
29
  config.backend = {
@@ -194,6 +199,17 @@ export async function runWarm(opts) {
194
199
  const targetLangs = opts.langs.map(l => l.toUpperCase());
195
200
  const store = new TranslationStore(cfg.storagePath);
196
201
  await store.initialize();
202
+ let shared = null;
203
+ if (cfg.sharedUrl) {
204
+ try {
205
+ shared = new SharedStore(cfg.sharedUrl);
206
+ await shared.initialize();
207
+ }
208
+ catch (err) {
209
+ console.warn(`[transduck] Could not connect to shared store: ${err.message}`);
210
+ shared = null;
211
+ }
212
+ }
197
213
  const content = readFileSync(opts.filePath, 'utf-8');
198
214
  let entries;
199
215
  if (opts.filePath.endsWith('.json')) {
@@ -237,12 +253,15 @@ export async function runWarm(opts) {
237
253
  break;
238
254
  }
239
255
  }
240
- await store.insertPlural({
256
+ const pluralParams = {
241
257
  sourceText: sourceKey, sourceLang: cfg.sourceLang, targetLang: lang,
242
258
  projectContextHash, stringContextHash, stringContext: entry.context ?? '',
243
259
  pluralCategory: cat, translatedText: translatedText,
244
260
  model: cfg.backendModel, status: allPresent ? 'translated' : 'failed',
245
- });
261
+ };
262
+ await store.insertPlural(pluralParams);
263
+ if (shared)
264
+ await shared.insertPlural(pluralParams);
246
265
  anyStored = true;
247
266
  }
248
267
  if (anyStored)
@@ -270,19 +289,25 @@ export async function runWarm(opts) {
270
289
  try {
271
290
  const result = await backendTranslate(entry.text, cfg.sourceLang, lang, cfg.projectContext, entry.context ?? null, cfg);
272
291
  if (validateTranslation(entry.text, result)) {
273
- await store.insert({
292
+ const insertParams = {
274
293
  sourceText: entry.text, sourceLang: cfg.sourceLang, targetLang: lang,
275
294
  projectContextHash, stringContextHash, stringContext: entry.context ?? '',
276
295
  translatedText: result, model: cfg.backendModel, status: 'translated',
277
- });
296
+ };
297
+ await store.insert(insertParams);
298
+ if (shared)
299
+ await shared.insert(insertParams);
278
300
  translated++;
279
301
  }
280
302
  else {
281
- await store.insert({
303
+ const insertParams = {
282
304
  sourceText: entry.text, sourceLang: cfg.sourceLang, targetLang: lang,
283
305
  projectContextHash, stringContextHash, stringContext: entry.context ?? '',
284
306
  translatedText: result, model: cfg.backendModel, status: 'failed',
285
- });
307
+ };
308
+ await store.insert(insertParams);
309
+ if (shared)
310
+ await shared.insert(insertParams);
286
311
  failed++;
287
312
  }
288
313
  }
@@ -293,6 +318,8 @@ export async function runWarm(opts) {
293
318
  }
294
319
  }
295
320
  store.close();
321
+ if (shared)
322
+ await shared.close();
296
323
  return `Translated: ${translated} | Skipped: ${skipped} | Failed: ${failed}`;
297
324
  }
298
325
  export async function runScan(opts) {
@@ -479,21 +506,44 @@ export async function runStats(opts) {
479
506
  await store.initialize();
480
507
  const st = await store.stats();
481
508
  const lines = [
482
- `Total translations: ${st.totalTranslations}`,
483
- `Failed translations: ${st.totalFailed}`,
509
+ 'Local store:',
510
+ ` Total translations: ${st.totalTranslations}`,
511
+ ` Failed translations: ${st.totalFailed}`,
484
512
  ];
485
513
  if (Object.keys(st.byLanguage).length > 0) {
486
- lines.push('By language:');
514
+ lines.push(' By language:');
487
515
  for (const [lang, count] of Object.entries(st.byLanguage).sort()) {
488
- lines.push(` ${lang}: ${count}`);
516
+ lines.push(` ${lang}: ${count}`);
489
517
  }
490
518
  }
491
519
  store.close();
520
+ if (cfg.sharedUrl) {
521
+ try {
522
+ const shared = new SharedStore(cfg.sharedUrl);
523
+ await shared.initialize();
524
+ const sharedSt = await shared.stats();
525
+ lines.push('');
526
+ lines.push('Shared store:');
527
+ lines.push(` Total translations: ${sharedSt.totalTranslations}`);
528
+ lines.push(` Failed translations: ${sharedSt.totalFailed}`);
529
+ if (Object.keys(sharedSt.byLanguage).length > 0) {
530
+ lines.push(' By language:');
531
+ for (const [lang, count] of Object.entries(sharedSt.byLanguage).sort()) {
532
+ lines.push(` ${lang}: ${count}`);
533
+ }
534
+ }
535
+ await shared.close();
536
+ }
537
+ catch (err) {
538
+ lines.push('');
539
+ lines.push(`Shared store: error connecting (${err.message})`);
540
+ }
541
+ }
492
542
  return lines.join('\n');
493
543
  }
494
544
  // CLI entry point
495
545
  const program = new Command();
496
- program.name('transduck').description('AI-native translation tool').version('0.5.3');
546
+ program.name('transduck').description('AI-native translation tool').version('0.6.0');
497
547
  program.command('init')
498
548
  .description('Initialize a new transduck project')
499
549
  .action(async () => {
@@ -505,6 +555,7 @@ program.command('init')
505
555
  const context = await ask('Project context: ');
506
556
  const sourceLang = await ask('Source language (e.g. EN): ');
507
557
  const targetsRaw = await ask('Target languages (comma-separated): ');
558
+ const sharedUrl = await ask('Shared Postgres URL (optional, press Enter to skip): ');
508
559
  console.log('\nTranslation provider:');
509
560
  console.log(' 1. OpenAI (requires API key)');
510
561
  console.log(' 2. Claude API (requires API key)');
@@ -516,6 +567,7 @@ program.command('init')
516
567
  dir, name, context, sourceLang,
517
568
  targetLangs: targetsRaw.split(',').map(s => s.trim()),
518
569
  provider: providerChoice,
570
+ sharedUrl: sharedUrl.trim() || undefined,
519
571
  });
520
572
  console.log(output);
521
573
  const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
package/dist/config.d.ts CHANGED
@@ -10,6 +10,7 @@ export interface TransduckConfig {
10
10
  backendModel: string;
11
11
  backendTimeout: number;
12
12
  backendMaxRetries: number;
13
+ sharedUrl: string | null;
13
14
  readOnly: boolean;
14
15
  }
15
16
  export declare function loadConfig(path?: string): TransduckConfig;
package/dist/config.js CHANGED
@@ -36,6 +36,7 @@ export function loadConfig(path) {
36
36
  const backendModel = backend.model ?? 'gpt-4.1-mini';
37
37
  const backendTimeout = backend.timeout_seconds ?? 10;
38
38
  const backendMaxRetries = backend.max_retries ?? 2;
39
+ const sharedUrl = process.env.TRANSDUCK_SHARED_URL || raw.storage?.shared_url || null;
39
40
  let readOnly = raw.runtime?.read_only ?? false;
40
41
  if (provider === 'claude_code') {
41
42
  readOnly = true;
@@ -46,6 +47,7 @@ export function loadConfig(path) {
46
47
  sourceLang: String(raw.languages.source).toUpperCase(),
47
48
  targetLangs: raw.languages.targets.map((l) => String(l).toUpperCase()),
48
49
  storagePath,
50
+ sharedUrl,
49
51
  provider,
50
52
  apiKeyEnv,
51
53
  tokenEnv,
package/dist/handler.d.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  interface TranslationRequestString {
2
2
  text: string;
3
3
  context?: string;
4
+ sourceLang?: string;
4
5
  }
5
6
  interface TranslationRequestPlural {
6
7
  one: string;
7
8
  other: string;
8
9
  context?: string | null;
10
+ sourceLang?: string;
9
11
  }
10
12
  export interface TranslationRequest {
11
13
  language: string;
package/dist/handler.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import { createHash } from 'crypto';
2
2
  import { loadConfig } from './config.js';
3
3
  import { TranslationStore } from './storage.js';
4
+ import { SharedStore } from './shared-store.js';
4
5
  import { translate as backendTranslate, translatePlural as backendTranslatePlural } from './backend.js';
5
6
  import { validateTranslation } from './validation.js';
6
7
  function hash(text) {
7
8
  return createHash('sha256').update(text).digest('hex');
8
9
  }
9
10
  let _store = null;
11
+ let _sharedStore = null;
10
12
  async function getStore(configPath) {
11
13
  if (!_store) {
12
14
  const cfg = loadConfig(configPath);
@@ -15,51 +17,76 @@ async function getStore(configPath) {
15
17
  }
16
18
  return _store;
17
19
  }
20
+ async function getSharedStore(configPath) {
21
+ if (_sharedStore)
22
+ return _sharedStore;
23
+ const cfg = loadConfig(configPath);
24
+ if (!cfg.sharedUrl)
25
+ return null;
26
+ try {
27
+ _sharedStore = new SharedStore(cfg.sharedUrl);
28
+ await _sharedStore.initialize();
29
+ return _sharedStore;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
18
35
  /** @internal Reset the singleton store — for testing only. */
19
36
  export function _resetHandlerStore() {
20
37
  if (_store) {
21
38
  _store.close();
22
39
  }
23
40
  _store = null;
41
+ _sharedStore = null;
24
42
  }
25
43
  export async function handleTranslationRequest(body, configPath) {
26
44
  const cfg = loadConfig(configPath);
27
45
  const store = await getStore(configPath);
46
+ const shared = await getSharedStore(configPath);
28
47
  const targetLang = body.language.toUpperCase();
29
48
  const projectContextHash = hash(cfg.projectContext);
30
49
  const translations = {};
31
50
  const plurals = {};
32
51
  // Translate regular strings
33
52
  for (const item of body.strings ?? []) {
53
+ const sourceLang = item.sourceLang?.toUpperCase() ?? cfg.sourceLang;
34
54
  const stringContextHash = hash(item.context ?? '');
35
55
  const key = `${item.text}||${item.context ?? ''}`;
36
- // Cache lookup
37
- const cached = await store.lookup({
38
- sourceText: item.text,
39
- sourceLang: cfg.sourceLang,
40
- targetLang,
41
- projectContextHash,
42
- stringContextHash,
43
- });
56
+ const lookupParams = {
57
+ sourceText: item.text, sourceLang, targetLang,
58
+ projectContextHash, stringContextHash,
59
+ };
60
+ // Tier 1: local cache
61
+ const cached = await store.lookup(lookupParams);
44
62
  if (cached !== null) {
45
63
  translations[key] = cached;
46
64
  continue;
47
65
  }
66
+ // Tier 2: shared store
67
+ if (shared) {
68
+ const sharedCached = await shared.lookup(lookupParams);
69
+ if (sharedCached !== null) {
70
+ // Propagate to local
71
+ await store.insert({
72
+ ...lookupParams, stringContext: item.context ?? '',
73
+ translatedText: sharedCached, model: cfg.backendModel, status: 'translated',
74
+ });
75
+ translations[key] = sharedCached;
76
+ continue;
77
+ }
78
+ }
48
79
  // Backend call
49
80
  try {
50
- const translated = await backendTranslate(item.text, cfg.sourceLang, targetLang, cfg.projectContext, item.context ?? null, cfg);
81
+ const translated = await backendTranslate(item.text, sourceLang, targetLang, cfg.projectContext, item.context ?? null, cfg);
51
82
  if (validateTranslation(item.text, translated)) {
52
- await store.insert({
53
- sourceText: item.text,
54
- sourceLang: cfg.sourceLang,
55
- targetLang,
56
- projectContextHash,
57
- stringContextHash,
58
- stringContext: item.context ?? '',
59
- translatedText: translated,
60
- model: cfg.backendModel,
61
- status: 'translated',
62
- });
83
+ const insertParams = {
84
+ ...lookupParams, stringContext: item.context ?? '',
85
+ translatedText: translated, model: cfg.backendModel, status: 'translated',
86
+ };
87
+ await store.insert(insertParams);
88
+ if (shared)
89
+ await shared.insert(insertParams);
63
90
  translations[key] = translated;
64
91
  }
65
92
  else {
@@ -72,37 +99,48 @@ export async function handleTranslationRequest(body, configPath) {
72
99
  }
73
100
  // Translate plurals
74
101
  for (const item of body.plurals ?? []) {
102
+ const sourceLang = item.sourceLang?.toUpperCase() ?? cfg.sourceLang;
75
103
  const stringContextHash = hash(item.context ?? '');
76
104
  const sourceKey = item.one + '\x00' + item.other;
77
105
  const responseKey = `${sourceKey}||${item.context ?? ''}`;
78
- // Cache lookup
79
- const cachedForms = await store.lookupPlural({
80
- sourceText: sourceKey,
81
- sourceLang: cfg.sourceLang,
82
- targetLang,
83
- projectContextHash,
84
- stringContextHash,
85
- });
106
+ const lookupParams = {
107
+ sourceText: sourceKey, sourceLang, targetLang,
108
+ projectContextHash, stringContextHash,
109
+ };
110
+ // Tier 1: local cache
111
+ const cachedForms = await store.lookupPlural(lookupParams);
86
112
  if (Object.keys(cachedForms).length > 0) {
87
113
  plurals[responseKey] = cachedForms;
88
114
  continue;
89
115
  }
116
+ // Tier 2: shared store
117
+ if (shared) {
118
+ const sharedForms = await shared.lookupPlural(lookupParams);
119
+ if (Object.keys(sharedForms).length > 0) {
120
+ // Propagate to local
121
+ for (const [cat, text] of Object.entries(sharedForms)) {
122
+ await store.insertPlural({
123
+ ...lookupParams, stringContext: item.context ?? '',
124
+ pluralCategory: cat, translatedText: text,
125
+ model: cfg.backendModel, status: 'translated',
126
+ });
127
+ }
128
+ plurals[responseKey] = sharedForms;
129
+ continue;
130
+ }
131
+ }
90
132
  // Backend call
91
133
  try {
92
- const forms = await backendTranslatePlural(item.one, item.other, cfg.sourceLang, targetLang, cfg.projectContext, item.context ?? null, cfg);
134
+ const forms = await backendTranslatePlural(item.one, item.other, sourceLang, targetLang, cfg.projectContext, item.context ?? null, cfg);
93
135
  for (const [cat, translatedText] of Object.entries(forms)) {
94
- await store.insertPlural({
95
- sourceText: sourceKey,
96
- sourceLang: cfg.sourceLang,
97
- targetLang,
98
- projectContextHash,
99
- stringContextHash,
100
- stringContext: item.context ?? '',
101
- pluralCategory: cat,
102
- translatedText: translatedText,
103
- model: cfg.backendModel,
104
- status: 'translated',
105
- });
136
+ const insertParams = {
137
+ ...lookupParams, stringContext: item.context ?? '',
138
+ pluralCategory: cat, translatedText: translatedText,
139
+ model: cfg.backendModel, status: 'translated',
140
+ };
141
+ await store.insertPlural(insertParams);
142
+ if (shared)
143
+ await shared.insertPlural(insertParams);
106
144
  }
107
145
  plurals[responseKey] = forms;
108
146
  }
package/dist/index.d.ts CHANGED
@@ -1,12 +1,27 @@
1
1
  import { type TransduckConfig } from './config.js';
2
2
  import { TranslationStore } from './storage.js';
3
+ import { SharedStore } from './shared-store.js';
4
+ import { TranslationResult } from './result.js';
3
5
  export declare function initialize(config?: TransduckConfig): Promise<void>;
4
6
  export declare function setLanguage(lang: string): void;
5
7
  export declare function _resetState(): void;
6
8
  export declare function _getStore(): TranslationStore | null;
7
- export declare function ait(sourceText: string, context?: string, vars?: Record<string, string | number>): Promise<string>;
9
+ export declare function _getSharedStore(): SharedStore | null;
10
+ export declare function _setSharedStore(shared: SharedStore | null): void;
11
+ export interface AitOptions {
12
+ context?: string;
13
+ vars?: Record<string, string | number>;
14
+ sourceLang?: string;
15
+ background?: boolean;
16
+ }
17
+ export declare function ait(sourceText: string, contextOrOpts?: string | AitOptions, vars?: Record<string, string | number>): Promise<TranslationResult>;
8
18
  export { createTransDuckHandler } from './handler.js';
19
+ export { TranslationResult } from './result.js';
20
+ export { SharedStore } from './shared-store.js';
21
+ export declare function detectLanguage(text: string): Promise<string>;
9
22
  export declare function aitPlural(one: string, other: string, count: number, opts?: {
10
23
  context?: string;
11
24
  vars?: Record<string, string | number>;
12
- }): Promise<string>;
25
+ sourceLang?: string;
26
+ background?: boolean;
27
+ }): Promise<TranslationResult>;