transduck 0.5.3 → 0.6.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 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
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import 'dotenv/config';
2
3
  import { Command } from 'commander';
3
4
  export interface InitOptions {
4
5
  dir: string;
@@ -7,6 +8,7 @@ export interface InitOptions {
7
8
  sourceLang: string;
8
9
  targetLangs: string[];
9
10
  provider?: number;
11
+ sharedUrl?: string;
10
12
  }
11
13
  export declare function runInit(opts: InitOptions): Promise<string>;
12
14
  export interface TranslateOptions {
package/dist/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import 'dotenv/config';
2
3
  import { createHash } from 'crypto';
3
4
  import { readFileSync, writeFileSync, mkdirSync } from 'fs';
4
5
  import { join, dirname } from 'path';
@@ -6,6 +7,7 @@ import { Command } from 'commander';
6
7
  import { stringify as yamlStringify } from 'yaml';
7
8
  import { loadConfig } from './config.js';
8
9
  import { TranslationStore } from './storage.js';
10
+ import { SharedStore } from './shared-store.js';
9
11
  import { translate as backendTranslate, translatePlural as backendTranslatePlural } from './backend.js';
10
12
  import { validateTranslation, extractPlaceholders } from './validation.js';
11
13
  import { getPluralCategory, interpolateVars } from './plural.js';
@@ -15,10 +17,14 @@ function hash(text) {
15
17
  }
16
18
  export async function runInit(opts) {
17
19
  const providerChoice = opts.provider ?? 1;
20
+ const storage = { path: './translations.db' };
21
+ if (opts.sharedUrl) {
22
+ storage.shared_url = opts.sharedUrl;
23
+ }
18
24
  const config = {
19
25
  project: { name: opts.name, context: opts.context },
20
26
  languages: { source: opts.sourceLang.toUpperCase(), targets: opts.targetLangs.map(l => l.toUpperCase()) },
21
- storage: { path: './translations.db' },
27
+ storage,
22
28
  };
23
29
  if (providerChoice === 2) {
24
30
  config.backend = {
@@ -194,6 +200,17 @@ export async function runWarm(opts) {
194
200
  const targetLangs = opts.langs.map(l => l.toUpperCase());
195
201
  const store = new TranslationStore(cfg.storagePath);
196
202
  await store.initialize();
203
+ let shared = null;
204
+ if (cfg.sharedUrl) {
205
+ try {
206
+ shared = new SharedStore(cfg.sharedUrl);
207
+ await shared.initialize();
208
+ }
209
+ catch (err) {
210
+ console.warn(`[transduck] Could not connect to shared store: ${err.message}`);
211
+ shared = null;
212
+ }
213
+ }
197
214
  const content = readFileSync(opts.filePath, 'utf-8');
198
215
  let entries;
199
216
  if (opts.filePath.endsWith('.json')) {
@@ -237,12 +254,15 @@ export async function runWarm(opts) {
237
254
  break;
238
255
  }
239
256
  }
240
- await store.insertPlural({
257
+ const pluralParams = {
241
258
  sourceText: sourceKey, sourceLang: cfg.sourceLang, targetLang: lang,
242
259
  projectContextHash, stringContextHash, stringContext: entry.context ?? '',
243
260
  pluralCategory: cat, translatedText: translatedText,
244
261
  model: cfg.backendModel, status: allPresent ? 'translated' : 'failed',
245
- });
262
+ };
263
+ await store.insertPlural(pluralParams);
264
+ if (shared)
265
+ await shared.insertPlural(pluralParams);
246
266
  anyStored = true;
247
267
  }
248
268
  if (anyStored)
@@ -270,19 +290,25 @@ export async function runWarm(opts) {
270
290
  try {
271
291
  const result = await backendTranslate(entry.text, cfg.sourceLang, lang, cfg.projectContext, entry.context ?? null, cfg);
272
292
  if (validateTranslation(entry.text, result)) {
273
- await store.insert({
293
+ const insertParams = {
274
294
  sourceText: entry.text, sourceLang: cfg.sourceLang, targetLang: lang,
275
295
  projectContextHash, stringContextHash, stringContext: entry.context ?? '',
276
296
  translatedText: result, model: cfg.backendModel, status: 'translated',
277
- });
297
+ };
298
+ await store.insert(insertParams);
299
+ if (shared)
300
+ await shared.insert(insertParams);
278
301
  translated++;
279
302
  }
280
303
  else {
281
- await store.insert({
304
+ const insertParams = {
282
305
  sourceText: entry.text, sourceLang: cfg.sourceLang, targetLang: lang,
283
306
  projectContextHash, stringContextHash, stringContext: entry.context ?? '',
284
307
  translatedText: result, model: cfg.backendModel, status: 'failed',
285
- });
308
+ };
309
+ await store.insert(insertParams);
310
+ if (shared)
311
+ await shared.insert(insertParams);
286
312
  failed++;
287
313
  }
288
314
  }
@@ -293,6 +319,8 @@ export async function runWarm(opts) {
293
319
  }
294
320
  }
295
321
  store.close();
322
+ if (shared)
323
+ await shared.close();
296
324
  return `Translated: ${translated} | Skipped: ${skipped} | Failed: ${failed}`;
297
325
  }
298
326
  export async function runScan(opts) {
@@ -479,21 +507,44 @@ export async function runStats(opts) {
479
507
  await store.initialize();
480
508
  const st = await store.stats();
481
509
  const lines = [
482
- `Total translations: ${st.totalTranslations}`,
483
- `Failed translations: ${st.totalFailed}`,
510
+ 'Local store:',
511
+ ` Total translations: ${st.totalTranslations}`,
512
+ ` Failed translations: ${st.totalFailed}`,
484
513
  ];
485
514
  if (Object.keys(st.byLanguage).length > 0) {
486
- lines.push('By language:');
515
+ lines.push(' By language:');
487
516
  for (const [lang, count] of Object.entries(st.byLanguage).sort()) {
488
- lines.push(` ${lang}: ${count}`);
517
+ lines.push(` ${lang}: ${count}`);
489
518
  }
490
519
  }
491
520
  store.close();
521
+ if (cfg.sharedUrl) {
522
+ try {
523
+ const shared = new SharedStore(cfg.sharedUrl);
524
+ await shared.initialize();
525
+ const sharedSt = await shared.stats();
526
+ lines.push('');
527
+ lines.push('Shared store:');
528
+ lines.push(` Total translations: ${sharedSt.totalTranslations}`);
529
+ lines.push(` Failed translations: ${sharedSt.totalFailed}`);
530
+ if (Object.keys(sharedSt.byLanguage).length > 0) {
531
+ lines.push(' By language:');
532
+ for (const [lang, count] of Object.entries(sharedSt.byLanguage).sort()) {
533
+ lines.push(` ${lang}: ${count}`);
534
+ }
535
+ }
536
+ await shared.close();
537
+ }
538
+ catch (err) {
539
+ lines.push('');
540
+ lines.push(`Shared store: error connecting (${err.message})`);
541
+ }
542
+ }
492
543
  return lines.join('\n');
493
544
  }
494
545
  // CLI entry point
495
546
  const program = new Command();
496
- program.name('transduck').description('AI-native translation tool').version('0.5.3');
547
+ program.name('transduck').description('AI-native translation tool').version('0.6.1');
497
548
  program.command('init')
498
549
  .description('Initialize a new transduck project')
499
550
  .action(async () => {
@@ -505,6 +556,7 @@ program.command('init')
505
556
  const context = await ask('Project context: ');
506
557
  const sourceLang = await ask('Source language (e.g. EN): ');
507
558
  const targetsRaw = await ask('Target languages (comma-separated): ');
559
+ const sharedUrl = await ask('Shared Postgres URL (optional, press Enter to skip): ');
508
560
  console.log('\nTranslation provider:');
509
561
  console.log(' 1. OpenAI (requires API key)');
510
562
  console.log(' 2. Claude API (requires API key)');
@@ -516,6 +568,7 @@ program.command('init')
516
568
  dir, name, context, sourceLang,
517
569
  targetLangs: targetsRaw.split(',').map(s => s.trim()),
518
570
  provider: providerChoice,
571
+ sharedUrl: sharedUrl.trim() || undefined,
519
572
  });
520
573
  console.log(output);
521
574
  const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
package/dist/config.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import 'dotenv/config';
1
2
  export interface TransduckConfig {
2
3
  projectName: string;
3
4
  projectContext: string;
@@ -10,6 +11,7 @@ export interface TransduckConfig {
10
11
  backendModel: string;
11
12
  backendTimeout: number;
12
13
  backendMaxRetries: number;
14
+ sharedUrl: string | null;
13
15
  readOnly: boolean;
14
16
  }
15
17
  export declare function loadConfig(path?: string): TransduckConfig;
package/dist/config.js CHANGED
@@ -1,3 +1,4 @@
1
+ import 'dotenv/config';
1
2
  import { readFileSync, existsSync } from 'fs';
2
3
  import { resolve, dirname, join } from 'path';
3
4
  import { parse as parseYaml } from 'yaml';
@@ -36,6 +37,7 @@ export function loadConfig(path) {
36
37
  const backendModel = backend.model ?? 'gpt-4.1-mini';
37
38
  const backendTimeout = backend.timeout_seconds ?? 10;
38
39
  const backendMaxRetries = backend.max_retries ?? 2;
40
+ const sharedUrl = process.env.TRANSDUCK_SHARED_URL || raw.storage?.shared_url || null;
39
41
  let readOnly = raw.runtime?.read_only ?? false;
40
42
  if (provider === 'claude_code') {
41
43
  readOnly = true;
@@ -46,6 +48,7 @@ export function loadConfig(path) {
46
48
  sourceLang: String(raw.languages.source).toUpperCase(),
47
49
  targetLangs: raw.languages.targets.map((l) => String(l).toUpperCase()),
48
50
  storagePath,
51
+ sharedUrl,
49
52
  provider,
50
53
  apiKeyEnv,
51
54
  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>;