transduck 0.4.3 → 0.5.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.js +20 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +57 -3
- package/dist/storage.js +58 -95
- package/package.json +3 -2
- package/src/backend.ts +23 -0
- package/src/cli.ts +68 -3
- package/src/storage.ts +81 -104
- package/tests/ait.test.ts +1 -1
- package/tests/backend.test.ts +1 -1
- package/tests/cli.test.ts +6 -6
- package/tests/config.test.ts +2 -2
- package/tests/handler.test.ts +4 -4
- package/tests/providers.test.ts +5 -5
- package/tests/storage.test.ts +1 -1
package/dist/backend.js
CHANGED
|
@@ -4,11 +4,31 @@
|
|
|
4
4
|
import { getProvider } from './providers/index.js';
|
|
5
5
|
// Re-export prompts for backward compat (tests import buildMessages from backend)
|
|
6
6
|
export { buildMessages, buildPluralMessages } from './providers/prompts.js';
|
|
7
|
+
function checkApiKey(config) {
|
|
8
|
+
if (config.provider === 'claude_code') {
|
|
9
|
+
const token = process.env[config.tokenEnv];
|
|
10
|
+
if (!token) {
|
|
11
|
+
throw new Error(`Missing API token: ${config.tokenEnv} environment variable is not set.\n` +
|
|
12
|
+
`Run: export ${config.tokenEnv}=your-token-here`);
|
|
13
|
+
}
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const apiKey = process.env[config.apiKeyEnv];
|
|
17
|
+
if (!apiKey) {
|
|
18
|
+
throw new Error(`Missing API key: ${config.apiKeyEnv} environment variable is not set.\n` +
|
|
19
|
+
`Run: export ${config.apiKeyEnv}=your-key-here\n` +
|
|
20
|
+
`Or add it to your .env file.`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
7
23
|
export async function translate(sourceText, sourceLang, targetLang, projectContext, stringContext, config, _clientOverride) {
|
|
24
|
+
if (!_clientOverride)
|
|
25
|
+
checkApiKey(config);
|
|
8
26
|
const provider = await getProvider(config);
|
|
9
27
|
return provider.translate(sourceText, sourceLang, targetLang, projectContext, stringContext, config, _clientOverride);
|
|
10
28
|
}
|
|
11
29
|
export async function translatePlural(one, other, sourceLang, targetLang, projectContext, stringContext, config, _clientOverride) {
|
|
30
|
+
if (!_clientOverride)
|
|
31
|
+
checkApiKey(config);
|
|
12
32
|
const provider = await getProvider(config);
|
|
13
33
|
return provider.translatePlural(one, other, sourceLang, targetLang, projectContext, stringContext, config, _clientOverride);
|
|
14
34
|
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -43,6 +43,13 @@ export declare function runScan(opts: ScanOptions): Promise<string>;
|
|
|
43
43
|
export interface StatsOptions {
|
|
44
44
|
configPath?: string;
|
|
45
45
|
}
|
|
46
|
+
export interface ClearOptions {
|
|
47
|
+
lang?: string;
|
|
48
|
+
failedOnly?: boolean;
|
|
49
|
+
yes?: boolean;
|
|
50
|
+
configPath?: string;
|
|
51
|
+
}
|
|
52
|
+
export declare function runClear(opts: ClearOptions): Promise<string>;
|
|
46
53
|
export declare function runStats(opts: StatsOptions): Promise<string>;
|
|
47
54
|
declare const program: Command;
|
|
48
55
|
export { program };
|
package/dist/cli.js
CHANGED
|
@@ -18,7 +18,7 @@ export async function runInit(opts) {
|
|
|
18
18
|
const config = {
|
|
19
19
|
project: { name: opts.name, context: opts.context },
|
|
20
20
|
languages: { source: opts.sourceLang.toUpperCase(), targets: opts.targetLangs.map(l => l.toUpperCase()) },
|
|
21
|
-
storage: { path: './translations.
|
|
21
|
+
storage: { path: './translations.db' },
|
|
22
22
|
};
|
|
23
23
|
if (providerChoice === 2) {
|
|
24
24
|
config.backend = {
|
|
@@ -46,7 +46,7 @@ export async function runInit(opts) {
|
|
|
46
46
|
}
|
|
47
47
|
const configPath = join(opts.dir, 'transduck.yaml');
|
|
48
48
|
writeFileSync(configPath, yamlStringify(config));
|
|
49
|
-
const dbPath = join(opts.dir, 'translations.
|
|
49
|
+
const dbPath = join(opts.dir, 'translations.db');
|
|
50
50
|
const store = new TranslationStore(dbPath);
|
|
51
51
|
await store.initialize();
|
|
52
52
|
store.close();
|
|
@@ -434,6 +434,45 @@ export async function runScan(opts) {
|
|
|
434
434
|
}
|
|
435
435
|
return lines.join('\n');
|
|
436
436
|
}
|
|
437
|
+
export async function runClear(opts) {
|
|
438
|
+
const cfg = loadConfig(opts.configPath);
|
|
439
|
+
const store = new TranslationStore(cfg.storagePath);
|
|
440
|
+
await store.initialize();
|
|
441
|
+
const targetLang = opts.lang?.toUpperCase();
|
|
442
|
+
const entryCount = store.count(targetLang, opts.failedOnly);
|
|
443
|
+
let desc;
|
|
444
|
+
if (targetLang && opts.failedOnly) {
|
|
445
|
+
desc = `${entryCount} failed translation(s) for ${targetLang}`;
|
|
446
|
+
}
|
|
447
|
+
else if (targetLang) {
|
|
448
|
+
desc = `${entryCount} translation(s) for ${targetLang}`;
|
|
449
|
+
}
|
|
450
|
+
else if (opts.failedOnly) {
|
|
451
|
+
desc = `${entryCount} failed translation(s) across all languages`;
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
desc = `${entryCount} translation(s) across all languages`;
|
|
455
|
+
}
|
|
456
|
+
if (entryCount === 0) {
|
|
457
|
+
store.close();
|
|
458
|
+
return 'Nothing to clear.';
|
|
459
|
+
}
|
|
460
|
+
if (!opts.yes) {
|
|
461
|
+
const readline = await import('readline');
|
|
462
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
463
|
+
const answer = await new Promise(resolve => {
|
|
464
|
+
rl.question(`This will delete ${desc}. Type "Yes" to confirm: `, resolve);
|
|
465
|
+
});
|
|
466
|
+
rl.close();
|
|
467
|
+
if (answer !== 'Yes') {
|
|
468
|
+
store.close();
|
|
469
|
+
return 'Cancelled.';
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
await store.clear(targetLang, opts.failedOnly);
|
|
473
|
+
store.close();
|
|
474
|
+
return `Deleted ${desc}.`;
|
|
475
|
+
}
|
|
437
476
|
export async function runStats(opts) {
|
|
438
477
|
const cfg = loadConfig(opts.configPath);
|
|
439
478
|
const store = new TranslationStore(cfg.storagePath);
|
|
@@ -454,7 +493,7 @@ export async function runStats(opts) {
|
|
|
454
493
|
}
|
|
455
494
|
// CLI entry point
|
|
456
495
|
const program = new Command();
|
|
457
|
-
program.name('transduck').description('AI-native translation tool').version('0.
|
|
496
|
+
program.name('transduck').description('AI-native translation tool').version('0.5.1');
|
|
458
497
|
program.command('init')
|
|
459
498
|
.description('Initialize a new transduck project')
|
|
460
499
|
.action(async () => {
|
|
@@ -539,6 +578,21 @@ program.command('warm')
|
|
|
539
578
|
});
|
|
540
579
|
console.log(output);
|
|
541
580
|
});
|
|
581
|
+
program.command('clear')
|
|
582
|
+
.description('Clear cached translations from the database')
|
|
583
|
+
.option('--lang <code>', 'Only clear translations for this language')
|
|
584
|
+
.option('--failed-only', 'Only clear failed translations')
|
|
585
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
586
|
+
.option('--config <path>', 'Path to transduck.yaml')
|
|
587
|
+
.action(async (opts) => {
|
|
588
|
+
const output = await runClear({
|
|
589
|
+
lang: opts.lang,
|
|
590
|
+
failedOnly: opts.failedOnly,
|
|
591
|
+
yes: opts.yes,
|
|
592
|
+
configPath: opts.config ?? '',
|
|
593
|
+
});
|
|
594
|
+
console.log(output);
|
|
595
|
+
});
|
|
542
596
|
program.command('stats')
|
|
543
597
|
.description('Show translation database statistics')
|
|
544
598
|
.option('--config <path>', 'Path to transduck.yaml')
|
package/dist/storage.js
CHANGED
|
@@ -1,24 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
|
-
// 1 GB default map size — plenty for translation caches
|
|
4
|
-
const DEFAULT_MAP_SIZE = 1024 * 1024 * 1024;
|
|
5
3
|
function contentHash(sourceText, projectContextHash, stringContextHash) {
|
|
6
4
|
return createHash('sha256')
|
|
7
5
|
.update(sourceText + '\x00' + projectContextHash + '\x00' + stringContextHash)
|
|
8
6
|
.digest('hex');
|
|
9
7
|
}
|
|
10
|
-
function makeKey(sourceLang, targetLang, chash, pluralCategory) {
|
|
11
|
-
return `${sourceLang}|${targetLang}|${chash}|${pluralCategory}`;
|
|
12
|
-
}
|
|
13
|
-
function parseKey(key) {
|
|
14
|
-
const parts = key.split('|');
|
|
15
|
-
return {
|
|
16
|
-
sourceLang: parts[0],
|
|
17
|
-
targetLang: parts[1],
|
|
18
|
-
contentHash: parts[2],
|
|
19
|
-
pluralCategory: parts[3] ?? '',
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
8
|
export class TranslationStore {
|
|
23
9
|
dbPath;
|
|
24
10
|
db = null;
|
|
@@ -26,12 +12,25 @@ export class TranslationStore {
|
|
|
26
12
|
this.dbPath = dbPath;
|
|
27
13
|
}
|
|
28
14
|
async initialize() {
|
|
29
|
-
this.db =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
15
|
+
this.db = new Database(this.dbPath);
|
|
16
|
+
this.db.pragma('journal_mode = WAL');
|
|
17
|
+
this.db.exec(`
|
|
18
|
+
CREATE TABLE IF NOT EXISTS translations (
|
|
19
|
+
source_lang TEXT NOT NULL,
|
|
20
|
+
target_lang TEXT NOT NULL,
|
|
21
|
+
content_hash TEXT NOT NULL,
|
|
22
|
+
plural_category TEXT NOT NULL DEFAULT '',
|
|
23
|
+
source_text TEXT NOT NULL,
|
|
24
|
+
translated_text TEXT NOT NULL,
|
|
25
|
+
model TEXT NOT NULL,
|
|
26
|
+
status TEXT NOT NULL,
|
|
27
|
+
created_at TEXT NOT NULL,
|
|
28
|
+
project_context_hash TEXT NOT NULL,
|
|
29
|
+
string_context_hash TEXT NOT NULL,
|
|
30
|
+
string_context TEXT NOT NULL DEFAULT '',
|
|
31
|
+
PRIMARY KEY (source_lang, target_lang, content_hash, plural_category)
|
|
32
|
+
)
|
|
33
|
+
`);
|
|
35
34
|
}
|
|
36
35
|
getDb() {
|
|
37
36
|
if (!this.db)
|
|
@@ -41,81 +40,45 @@ export class TranslationStore {
|
|
|
41
40
|
async lookup(params) {
|
|
42
41
|
const db = this.getDb();
|
|
43
42
|
const chash = contentHash(params.sourceText, params.projectContextHash, params.stringContextHash);
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
if (entry && entry.status === 'translated') {
|
|
47
|
-
return entry.translated_text;
|
|
48
|
-
}
|
|
49
|
-
return null;
|
|
43
|
+
const row = db.prepare('SELECT translated_text FROM translations WHERE source_lang = ? AND target_lang = ? AND content_hash = ? AND plural_category = ? AND status = ?').get(params.sourceLang, params.targetLang, chash, '', 'translated');
|
|
44
|
+
return row?.translated_text ?? null;
|
|
50
45
|
}
|
|
51
46
|
async insert(params) {
|
|
52
47
|
const db = this.getDb();
|
|
53
48
|
const chash = contentHash(params.sourceText, params.projectContextHash, params.stringContextHash);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (db.get(key) !== undefined)
|
|
57
|
-
return;
|
|
58
|
-
await db.put(key, {
|
|
59
|
-
source_text: params.sourceText,
|
|
60
|
-
translated_text: params.translatedText,
|
|
61
|
-
model: params.model,
|
|
62
|
-
status: params.status,
|
|
63
|
-
created_at: new Date().toISOString(),
|
|
64
|
-
project_context_hash: params.projectContextHash,
|
|
65
|
-
string_context_hash: params.stringContextHash,
|
|
66
|
-
string_context: params.stringContext,
|
|
67
|
-
});
|
|
49
|
+
db.prepare(`INSERT OR IGNORE INTO translations (source_lang, target_lang, content_hash, plural_category, source_text, translated_text, model, status, created_at, project_context_hash, string_context_hash, string_context)
|
|
50
|
+
VALUES (?, ?, ?, '', ?, ?, ?, ?, ?, ?, ?, ?)`).run(params.sourceLang, params.targetLang, chash, params.sourceText, params.translatedText, params.model, params.status, new Date().toISOString(), params.projectContextHash, params.stringContextHash, params.stringContext);
|
|
68
51
|
}
|
|
69
52
|
async lookupPlural(params) {
|
|
70
53
|
const db = this.getDb();
|
|
71
54
|
const chash = contentHash(params.sourceText, params.projectContextHash, params.stringContextHash);
|
|
72
|
-
const
|
|
55
|
+
const rows = db.prepare(`SELECT plural_category, translated_text FROM translations
|
|
56
|
+
WHERE source_lang = ? AND target_lang = ? AND content_hash = ? AND plural_category != '' AND status = ?`).all(params.sourceLang, params.targetLang, chash, 'translated');
|
|
73
57
|
const result = {};
|
|
74
|
-
for (const
|
|
75
|
-
|
|
76
|
-
if (!keyStr.startsWith(prefix))
|
|
77
|
-
break;
|
|
78
|
-
const parsed = parseKey(keyStr);
|
|
79
|
-
// Skip regular entries (empty plural_category)
|
|
80
|
-
if (!parsed.pluralCategory)
|
|
81
|
-
continue;
|
|
82
|
-
if (value.status === 'translated') {
|
|
83
|
-
result[parsed.pluralCategory] = value.translated_text;
|
|
84
|
-
}
|
|
58
|
+
for (const row of rows) {
|
|
59
|
+
result[row.plural_category] = row.translated_text;
|
|
85
60
|
}
|
|
86
61
|
return result;
|
|
87
62
|
}
|
|
88
63
|
async insertPlural(params) {
|
|
89
64
|
const db = this.getDb();
|
|
90
65
|
const chash = contentHash(params.sourceText, params.projectContextHash, params.stringContextHash);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (db.get(key) !== undefined)
|
|
94
|
-
return;
|
|
95
|
-
await db.put(key, {
|
|
96
|
-
source_text: params.sourceText,
|
|
97
|
-
translated_text: params.translatedText,
|
|
98
|
-
model: params.model,
|
|
99
|
-
status: params.status,
|
|
100
|
-
created_at: new Date().toISOString(),
|
|
101
|
-
project_context_hash: params.projectContextHash,
|
|
102
|
-
string_context_hash: params.stringContextHash,
|
|
103
|
-
string_context: params.stringContext,
|
|
104
|
-
});
|
|
66
|
+
db.prepare(`INSERT OR IGNORE INTO translations (source_lang, target_lang, content_hash, plural_category, source_text, translated_text, model, status, created_at, project_context_hash, string_context_hash, string_context)
|
|
67
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(params.sourceLang, params.targetLang, chash, params.pluralCategory, params.sourceText, params.translatedText, params.model, params.status, new Date().toISOString(), params.projectContextHash, params.stringContextHash, params.stringContext);
|
|
105
68
|
}
|
|
106
69
|
async stats() {
|
|
107
70
|
const db = this.getDb();
|
|
108
71
|
let totalTranslations = 0;
|
|
109
72
|
let totalFailed = 0;
|
|
110
73
|
const byLanguage = {};
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (
|
|
114
|
-
totalTranslations
|
|
115
|
-
byLanguage[
|
|
74
|
+
const rows = db.prepare('SELECT target_lang, status, COUNT(*) as count FROM translations GROUP BY target_lang, status').all();
|
|
75
|
+
for (const row of rows) {
|
|
76
|
+
if (row.status === 'translated') {
|
|
77
|
+
totalTranslations += row.count;
|
|
78
|
+
byLanguage[row.target_lang] = (byLanguage[row.target_lang] ?? 0) + row.count;
|
|
116
79
|
}
|
|
117
|
-
else if (
|
|
118
|
-
totalFailed
|
|
80
|
+
else if (row.status === 'failed') {
|
|
81
|
+
totalFailed += row.count;
|
|
119
82
|
}
|
|
120
83
|
}
|
|
121
84
|
return { totalTranslations, totalFailed, byLanguage };
|
|
@@ -125,36 +88,36 @@ export class TranslationStore {
|
|
|
125
88
|
*/
|
|
126
89
|
count(targetLang, failedOnly) {
|
|
127
90
|
const db = this.getDb();
|
|
128
|
-
let
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
91
|
+
let sql = 'SELECT COUNT(*) as count FROM translations WHERE 1=1';
|
|
92
|
+
const params = [];
|
|
93
|
+
if (targetLang) {
|
|
94
|
+
sql += ' AND target_lang = ?';
|
|
95
|
+
params.push(targetLang);
|
|
96
|
+
}
|
|
97
|
+
if (failedOnly) {
|
|
98
|
+
sql += ' AND status = ?';
|
|
99
|
+
params.push('failed');
|
|
136
100
|
}
|
|
137
|
-
|
|
101
|
+
const row = db.prepare(sql).get(...params);
|
|
102
|
+
return row.count;
|
|
138
103
|
}
|
|
139
104
|
/**
|
|
140
105
|
* Delete entries matching optional filters. Returns count deleted.
|
|
141
106
|
*/
|
|
142
107
|
async clear(targetLang, failedOnly) {
|
|
143
108
|
const db = this.getDb();
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
continue;
|
|
150
|
-
if (failedOnly && value.status !== 'failed')
|
|
151
|
-
continue;
|
|
152
|
-
toDelete.push(keyStr);
|
|
109
|
+
let sql = 'DELETE FROM translations WHERE 1=1';
|
|
110
|
+
const params = [];
|
|
111
|
+
if (targetLang) {
|
|
112
|
+
sql += ' AND target_lang = ?';
|
|
113
|
+
params.push(targetLang);
|
|
153
114
|
}
|
|
154
|
-
|
|
155
|
-
|
|
115
|
+
if (failedOnly) {
|
|
116
|
+
sql += ' AND status = ?';
|
|
117
|
+
params.push('failed');
|
|
156
118
|
}
|
|
157
|
-
|
|
119
|
+
const result = db.prepare(sql).run(...params);
|
|
120
|
+
return result.changes;
|
|
158
121
|
}
|
|
159
122
|
close() {
|
|
160
123
|
if (this.db) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "transduck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "AI-native translation tool using source text as keys",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -25,12 +25,13 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"commander": "^12.0.0",
|
|
28
|
-
"
|
|
28
|
+
"better-sqlite3": "^11.0.0",
|
|
29
29
|
"openai": "^4.0.0",
|
|
30
30
|
"yaml": "^2.0.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@testing-library/react": "^16.0.0",
|
|
34
|
+
"@types/better-sqlite3": "^7.0.0",
|
|
34
35
|
"@types/node": "^22.0.0",
|
|
35
36
|
"@types/react": "^19.0.0",
|
|
36
37
|
"@types/react-dom": "^19.0.0",
|
package/src/backend.ts
CHANGED
|
@@ -8,6 +8,27 @@ import { getProvider } from './providers/index.js';
|
|
|
8
8
|
// Re-export prompts for backward compat (tests import buildMessages from backend)
|
|
9
9
|
export { buildMessages, buildPluralMessages } from './providers/prompts.js';
|
|
10
10
|
|
|
11
|
+
function checkApiKey(config: TransduckConfig): void {
|
|
12
|
+
if (config.provider === 'claude_code') {
|
|
13
|
+
const token = process.env[config.tokenEnv];
|
|
14
|
+
if (!token) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`Missing API token: ${config.tokenEnv} environment variable is not set.\n` +
|
|
17
|
+
`Run: export ${config.tokenEnv}=your-token-here`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const apiKey = process.env[config.apiKeyEnv];
|
|
23
|
+
if (!apiKey) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Missing API key: ${config.apiKeyEnv} environment variable is not set.\n` +
|
|
26
|
+
`Run: export ${config.apiKeyEnv}=your-key-here\n` +
|
|
27
|
+
`Or add it to your .env file.`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
11
32
|
export async function translate(
|
|
12
33
|
sourceText: string,
|
|
13
34
|
sourceLang: string,
|
|
@@ -17,6 +38,7 @@ export async function translate(
|
|
|
17
38
|
config: TransduckConfig,
|
|
18
39
|
_clientOverride?: any,
|
|
19
40
|
): Promise<string> {
|
|
41
|
+
if (!_clientOverride) checkApiKey(config);
|
|
20
42
|
const provider = await getProvider(config);
|
|
21
43
|
return provider.translate(sourceText, sourceLang, targetLang, projectContext, stringContext, config, _clientOverride);
|
|
22
44
|
}
|
|
@@ -31,6 +53,7 @@ export async function translatePlural(
|
|
|
31
53
|
config: TransduckConfig,
|
|
32
54
|
_clientOverride?: any,
|
|
33
55
|
): Promise<Record<string, string>> {
|
|
56
|
+
if (!_clientOverride) checkApiKey(config);
|
|
34
57
|
const provider = await getProvider(config);
|
|
35
58
|
return provider.translatePlural(one, other, sourceLang, targetLang, projectContext, stringContext, config, _clientOverride);
|
|
36
59
|
}
|
package/src/cli.ts
CHANGED
|
@@ -31,7 +31,7 @@ export async function runInit(opts: InitOptions): Promise<string> {
|
|
|
31
31
|
const config: Record<string, any> = {
|
|
32
32
|
project: { name: opts.name, context: opts.context },
|
|
33
33
|
languages: { source: opts.sourceLang.toUpperCase(), targets: opts.targetLangs.map(l => l.toUpperCase()) },
|
|
34
|
-
storage: { path: './translations.
|
|
34
|
+
storage: { path: './translations.db' },
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
if (providerChoice === 2) {
|
|
@@ -60,7 +60,7 @@ export async function runInit(opts: InitOptions): Promise<string> {
|
|
|
60
60
|
const configPath = join(opts.dir, 'transduck.yaml');
|
|
61
61
|
writeFileSync(configPath, yamlStringify(config));
|
|
62
62
|
|
|
63
|
-
const dbPath = join(opts.dir, 'translations.
|
|
63
|
+
const dbPath = join(opts.dir, 'translations.db');
|
|
64
64
|
const store = new TranslationStore(dbPath);
|
|
65
65
|
await store.initialize();
|
|
66
66
|
store.close();
|
|
@@ -539,6 +539,55 @@ export interface StatsOptions {
|
|
|
539
539
|
configPath?: string;
|
|
540
540
|
}
|
|
541
541
|
|
|
542
|
+
export interface ClearOptions {
|
|
543
|
+
lang?: string;
|
|
544
|
+
failedOnly?: boolean;
|
|
545
|
+
yes?: boolean;
|
|
546
|
+
configPath?: string;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export async function runClear(opts: ClearOptions): Promise<string> {
|
|
550
|
+
const cfg = loadConfig(opts.configPath);
|
|
551
|
+
const store = new TranslationStore(cfg.storagePath);
|
|
552
|
+
await store.initialize();
|
|
553
|
+
|
|
554
|
+
const targetLang = opts.lang?.toUpperCase();
|
|
555
|
+
const entryCount = store.count(targetLang, opts.failedOnly);
|
|
556
|
+
|
|
557
|
+
let desc: string;
|
|
558
|
+
if (targetLang && opts.failedOnly) {
|
|
559
|
+
desc = `${entryCount} failed translation(s) for ${targetLang}`;
|
|
560
|
+
} else if (targetLang) {
|
|
561
|
+
desc = `${entryCount} translation(s) for ${targetLang}`;
|
|
562
|
+
} else if (opts.failedOnly) {
|
|
563
|
+
desc = `${entryCount} failed translation(s) across all languages`;
|
|
564
|
+
} else {
|
|
565
|
+
desc = `${entryCount} translation(s) across all languages`;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (entryCount === 0) {
|
|
569
|
+
store.close();
|
|
570
|
+
return 'Nothing to clear.';
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (!opts.yes) {
|
|
574
|
+
const readline = await import('readline');
|
|
575
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
576
|
+
const answer = await new Promise<string>(resolve => {
|
|
577
|
+
rl.question(`This will delete ${desc}. Type "Yes" to confirm: `, resolve);
|
|
578
|
+
});
|
|
579
|
+
rl.close();
|
|
580
|
+
if (answer !== 'Yes') {
|
|
581
|
+
store.close();
|
|
582
|
+
return 'Cancelled.';
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
await store.clear(targetLang, opts.failedOnly);
|
|
587
|
+
store.close();
|
|
588
|
+
return `Deleted ${desc}.`;
|
|
589
|
+
}
|
|
590
|
+
|
|
542
591
|
export async function runStats(opts: StatsOptions): Promise<string> {
|
|
543
592
|
const cfg = loadConfig(opts.configPath);
|
|
544
593
|
const store = new TranslationStore(cfg.storagePath);
|
|
@@ -562,7 +611,7 @@ export async function runStats(opts: StatsOptions): Promise<string> {
|
|
|
562
611
|
// CLI entry point
|
|
563
612
|
const program = new Command();
|
|
564
613
|
|
|
565
|
-
program.name('transduck').description('AI-native translation tool').version('0.
|
|
614
|
+
program.name('transduck').description('AI-native translation tool').version('0.5.1');
|
|
566
615
|
|
|
567
616
|
program.command('init')
|
|
568
617
|
.description('Initialize a new transduck project')
|
|
@@ -659,6 +708,22 @@ program.command('warm')
|
|
|
659
708
|
console.log(output);
|
|
660
709
|
});
|
|
661
710
|
|
|
711
|
+
program.command('clear')
|
|
712
|
+
.description('Clear cached translations from the database')
|
|
713
|
+
.option('--lang <code>', 'Only clear translations for this language')
|
|
714
|
+
.option('--failed-only', 'Only clear failed translations')
|
|
715
|
+
.option('-y, --yes', 'Skip confirmation prompt')
|
|
716
|
+
.option('--config <path>', 'Path to transduck.yaml')
|
|
717
|
+
.action(async (opts: { lang?: string; failedOnly?: boolean; yes?: boolean; config?: string }) => {
|
|
718
|
+
const output = await runClear({
|
|
719
|
+
lang: opts.lang,
|
|
720
|
+
failedOnly: opts.failedOnly,
|
|
721
|
+
yes: opts.yes,
|
|
722
|
+
configPath: opts.config ?? '',
|
|
723
|
+
});
|
|
724
|
+
console.log(output);
|
|
725
|
+
});
|
|
726
|
+
|
|
662
727
|
program.command('stats')
|
|
663
728
|
.description('Show translation database statistics')
|
|
664
729
|
.option('--config <path>', 'Path to transduck.yaml')
|
package/src/storage.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
3
|
|
|
4
|
-
// 1 GB default map size — plenty for translation caches
|
|
5
|
-
const DEFAULT_MAP_SIZE = 1024 * 1024 * 1024;
|
|
6
|
-
|
|
7
4
|
export interface LookupParams {
|
|
8
5
|
sourceText: string;
|
|
9
6
|
sourceLang: string;
|
|
@@ -27,17 +24,6 @@ export interface InsertPluralParams extends LookupParams {
|
|
|
27
24
|
status: string;
|
|
28
25
|
}
|
|
29
26
|
|
|
30
|
-
interface StoredEntry {
|
|
31
|
-
source_text: string;
|
|
32
|
-
translated_text: string;
|
|
33
|
-
model: string;
|
|
34
|
-
status: string;
|
|
35
|
-
created_at: string;
|
|
36
|
-
project_context_hash: string;
|
|
37
|
-
string_context_hash: string;
|
|
38
|
-
string_context: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
27
|
interface Stats {
|
|
42
28
|
totalTranslations: number;
|
|
43
29
|
totalFailed: number;
|
|
@@ -50,38 +36,37 @@ function contentHash(sourceText: string, projectContextHash: string, stringConte
|
|
|
50
36
|
.digest('hex');
|
|
51
37
|
}
|
|
52
38
|
|
|
53
|
-
function makeKey(sourceLang: string, targetLang: string, chash: string, pluralCategory: string): string {
|
|
54
|
-
return `${sourceLang}|${targetLang}|${chash}|${pluralCategory}`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function parseKey(key: string): { sourceLang: string; targetLang: string; contentHash: string; pluralCategory: string } {
|
|
58
|
-
const parts = key.split('|');
|
|
59
|
-
return {
|
|
60
|
-
sourceLang: parts[0],
|
|
61
|
-
targetLang: parts[1],
|
|
62
|
-
contentHash: parts[2],
|
|
63
|
-
pluralCategory: parts[3] ?? '',
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
39
|
export class TranslationStore {
|
|
68
40
|
private dbPath: string;
|
|
69
|
-
private db:
|
|
41
|
+
private db: Database.Database | null = null;
|
|
70
42
|
|
|
71
43
|
constructor(dbPath: string) {
|
|
72
44
|
this.dbPath = dbPath;
|
|
73
45
|
}
|
|
74
46
|
|
|
75
47
|
async initialize(): Promise<void> {
|
|
76
|
-
this.db =
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
48
|
+
this.db = new Database(this.dbPath);
|
|
49
|
+
this.db.pragma('journal_mode = WAL');
|
|
50
|
+
this.db.exec(`
|
|
51
|
+
CREATE TABLE IF NOT EXISTS translations (
|
|
52
|
+
source_lang TEXT NOT NULL,
|
|
53
|
+
target_lang TEXT NOT NULL,
|
|
54
|
+
content_hash TEXT NOT NULL,
|
|
55
|
+
plural_category TEXT NOT NULL DEFAULT '',
|
|
56
|
+
source_text TEXT NOT NULL,
|
|
57
|
+
translated_text TEXT NOT NULL,
|
|
58
|
+
model TEXT NOT NULL,
|
|
59
|
+
status TEXT NOT NULL,
|
|
60
|
+
created_at TEXT NOT NULL,
|
|
61
|
+
project_context_hash TEXT NOT NULL,
|
|
62
|
+
string_context_hash TEXT NOT NULL,
|
|
63
|
+
string_context TEXT NOT NULL DEFAULT '',
|
|
64
|
+
PRIMARY KEY (source_lang, target_lang, content_hash, plural_category)
|
|
65
|
+
)
|
|
66
|
+
`);
|
|
82
67
|
}
|
|
83
68
|
|
|
84
|
-
private getDb():
|
|
69
|
+
private getDb(): Database.Database {
|
|
85
70
|
if (!this.db) throw new Error('Store not initialized');
|
|
86
71
|
return this.db;
|
|
87
72
|
}
|
|
@@ -89,47 +74,36 @@ export class TranslationStore {
|
|
|
89
74
|
async lookup(params: LookupParams): Promise<string | null> {
|
|
90
75
|
const db = this.getDb();
|
|
91
76
|
const chash = contentHash(params.sourceText, params.projectContextHash, params.stringContextHash);
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
return null;
|
|
77
|
+
const row = db.prepare(
|
|
78
|
+
'SELECT translated_text FROM translations WHERE source_lang = ? AND target_lang = ? AND content_hash = ? AND plural_category = ? AND status = ?'
|
|
79
|
+
).get(params.sourceLang, params.targetLang, chash, '', 'translated') as { translated_text: string } | undefined;
|
|
80
|
+
return row?.translated_text ?? null;
|
|
98
81
|
}
|
|
99
82
|
|
|
100
83
|
async insert(params: InsertParams): Promise<void> {
|
|
101
84
|
const db = this.getDb();
|
|
102
85
|
const chash = contentHash(params.sourceText, params.projectContextHash, params.stringContextHash);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
project_context_hash: params.projectContextHash,
|
|
113
|
-
string_context_hash: params.stringContextHash,
|
|
114
|
-
string_context: params.stringContext,
|
|
115
|
-
});
|
|
86
|
+
db.prepare(
|
|
87
|
+
`INSERT OR IGNORE INTO translations (source_lang, target_lang, content_hash, plural_category, source_text, translated_text, model, status, created_at, project_context_hash, string_context_hash, string_context)
|
|
88
|
+
VALUES (?, ?, ?, '', ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
89
|
+
).run(
|
|
90
|
+
params.sourceLang, params.targetLang, chash,
|
|
91
|
+
params.sourceText, params.translatedText, params.model, params.status,
|
|
92
|
+
new Date().toISOString(),
|
|
93
|
+
params.projectContextHash, params.stringContextHash, params.stringContext,
|
|
94
|
+
);
|
|
116
95
|
}
|
|
117
96
|
|
|
118
97
|
async lookupPlural(params: LookupParams): Promise<Record<string, string>> {
|
|
119
98
|
const db = this.getDb();
|
|
120
99
|
const chash = contentHash(params.sourceText, params.projectContextHash, params.stringContextHash);
|
|
121
|
-
const
|
|
100
|
+
const rows = db.prepare(
|
|
101
|
+
`SELECT plural_category, translated_text FROM translations
|
|
102
|
+
WHERE source_lang = ? AND target_lang = ? AND content_hash = ? AND plural_category != '' AND status = ?`
|
|
103
|
+
).all(params.sourceLang, params.targetLang, chash, 'translated') as { plural_category: string; translated_text: string }[];
|
|
122
104
|
const result: Record<string, string> = {};
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const keyStr = key as string;
|
|
126
|
-
if (!keyStr.startsWith(prefix)) break;
|
|
127
|
-
const parsed = parseKey(keyStr);
|
|
128
|
-
// Skip regular entries (empty plural_category)
|
|
129
|
-
if (!parsed.pluralCategory) continue;
|
|
130
|
-
if (value.status === 'translated') {
|
|
131
|
-
result[parsed.pluralCategory] = value.translated_text;
|
|
132
|
-
}
|
|
105
|
+
for (const row of rows) {
|
|
106
|
+
result[row.plural_category] = row.translated_text;
|
|
133
107
|
}
|
|
134
108
|
return result;
|
|
135
109
|
}
|
|
@@ -137,19 +111,15 @@ export class TranslationStore {
|
|
|
137
111
|
async insertPlural(params: InsertPluralParams): Promise<void> {
|
|
138
112
|
const db = this.getDb();
|
|
139
113
|
const chash = contentHash(params.sourceText, params.projectContextHash, params.stringContextHash);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
project_context_hash: params.projectContextHash,
|
|
150
|
-
string_context_hash: params.stringContextHash,
|
|
151
|
-
string_context: params.stringContext,
|
|
152
|
-
});
|
|
114
|
+
db.prepare(
|
|
115
|
+
`INSERT OR IGNORE INTO translations (source_lang, target_lang, content_hash, plural_category, source_text, translated_text, model, status, created_at, project_context_hash, string_context_hash, string_context)
|
|
116
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
117
|
+
).run(
|
|
118
|
+
params.sourceLang, params.targetLang, chash, params.pluralCategory,
|
|
119
|
+
params.sourceText, params.translatedText, params.model, params.status,
|
|
120
|
+
new Date().toISOString(),
|
|
121
|
+
params.projectContextHash, params.stringContextHash, params.stringContext,
|
|
122
|
+
);
|
|
153
123
|
}
|
|
154
124
|
|
|
155
125
|
async stats(): Promise<Stats> {
|
|
@@ -158,13 +128,16 @@ export class TranslationStore {
|
|
|
158
128
|
let totalFailed = 0;
|
|
159
129
|
const byLanguage: Record<string, number> = {};
|
|
160
130
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
131
|
+
const rows = db.prepare(
|
|
132
|
+
'SELECT target_lang, status, COUNT(*) as count FROM translations GROUP BY target_lang, status'
|
|
133
|
+
).all() as { target_lang: string; status: string; count: number }[];
|
|
134
|
+
|
|
135
|
+
for (const row of rows) {
|
|
136
|
+
if (row.status === 'translated') {
|
|
137
|
+
totalTranslations += row.count;
|
|
138
|
+
byLanguage[row.target_lang] = (byLanguage[row.target_lang] ?? 0) + row.count;
|
|
139
|
+
} else if (row.status === 'failed') {
|
|
140
|
+
totalFailed += row.count;
|
|
168
141
|
}
|
|
169
142
|
}
|
|
170
143
|
|
|
@@ -176,14 +149,18 @@ export class TranslationStore {
|
|
|
176
149
|
*/
|
|
177
150
|
count(targetLang?: string, failedOnly?: boolean): number {
|
|
178
151
|
const db = this.getDb();
|
|
179
|
-
let
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
152
|
+
let sql = 'SELECT COUNT(*) as count FROM translations WHERE 1=1';
|
|
153
|
+
const params: unknown[] = [];
|
|
154
|
+
if (targetLang) {
|
|
155
|
+
sql += ' AND target_lang = ?';
|
|
156
|
+
params.push(targetLang);
|
|
157
|
+
}
|
|
158
|
+
if (failedOnly) {
|
|
159
|
+
sql += ' AND status = ?';
|
|
160
|
+
params.push('failed');
|
|
185
161
|
}
|
|
186
|
-
|
|
162
|
+
const row = db.prepare(sql).get(...params) as { count: number };
|
|
163
|
+
return row.count;
|
|
187
164
|
}
|
|
188
165
|
|
|
189
166
|
/**
|
|
@@ -191,18 +168,18 @@ export class TranslationStore {
|
|
|
191
168
|
*/
|
|
192
169
|
async clear(targetLang?: string, failedOnly?: boolean): Promise<number> {
|
|
193
170
|
const db = this.getDb();
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (failedOnly && value.status !== 'failed') continue;
|
|
200
|
-
toDelete.push(keyStr);
|
|
171
|
+
let sql = 'DELETE FROM translations WHERE 1=1';
|
|
172
|
+
const params: unknown[] = [];
|
|
173
|
+
if (targetLang) {
|
|
174
|
+
sql += ' AND target_lang = ?';
|
|
175
|
+
params.push(targetLang);
|
|
201
176
|
}
|
|
202
|
-
|
|
203
|
-
|
|
177
|
+
if (failedOnly) {
|
|
178
|
+
sql += ' AND status = ?';
|
|
179
|
+
params.push('failed');
|
|
204
180
|
}
|
|
205
|
-
|
|
181
|
+
const result = db.prepare(sql).run(...params);
|
|
182
|
+
return result.changes;
|
|
206
183
|
}
|
|
207
184
|
|
|
208
185
|
close(): void {
|
package/tests/ait.test.ts
CHANGED
|
@@ -15,7 +15,7 @@ function makeConfig(tmpDir: string): TransduckConfig {
|
|
|
15
15
|
projectContext: 'A test site',
|
|
16
16
|
sourceLang: 'EN',
|
|
17
17
|
targetLangs: ['DE', 'ES'],
|
|
18
|
-
storagePath: join(tmpDir, 'test.
|
|
18
|
+
storagePath: join(tmpDir, 'test.db'),
|
|
19
19
|
provider: 'openai',
|
|
20
20
|
apiKeyEnv: 'OPENAI_API_KEY',
|
|
21
21
|
tokenEnv: 'CLAUDE_CODE_OAUTH_TOKEN',
|
package/tests/backend.test.ts
CHANGED
|
@@ -8,7 +8,7 @@ function makeConfig(): TransduckConfig {
|
|
|
8
8
|
projectContext: 'A travel site about Mallorca',
|
|
9
9
|
sourceLang: 'EN',
|
|
10
10
|
targetLangs: ['DE'],
|
|
11
|
-
storagePath: '/tmp/test.
|
|
11
|
+
storagePath: '/tmp/test.db',
|
|
12
12
|
provider: 'openai',
|
|
13
13
|
apiKeyEnv: 'OPENAI_API_KEY',
|
|
14
14
|
tokenEnv: 'CLAUDE_CODE_OAUTH_TOKEN',
|
package/tests/cli.test.ts
CHANGED
|
@@ -27,7 +27,7 @@ languages:
|
|
|
27
27
|
targets:
|
|
28
28
|
- DE
|
|
29
29
|
storage:
|
|
30
|
-
path: ./translations.
|
|
30
|
+
path: ./translations.db
|
|
31
31
|
backend:
|
|
32
32
|
api_key_env: OPENAI_API_KEY
|
|
33
33
|
model: gpt-4.1-mini
|
|
@@ -55,7 +55,7 @@ describe('CLI functions', () => {
|
|
|
55
55
|
});
|
|
56
56
|
const { existsSync } = await import('fs');
|
|
57
57
|
expect(existsSync(join(tmpDir, 'transduck.yaml'))).toBe(true);
|
|
58
|
-
expect(existsSync(join(tmpDir, 'translations.
|
|
58
|
+
expect(existsSync(join(tmpDir, 'translations.db'))).toBe(true);
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
it('stats on empty db shows zero', async () => {
|
|
@@ -68,7 +68,7 @@ describe('CLI functions', () => {
|
|
|
68
68
|
|
|
69
69
|
it('translate returns cached with vars interpolated', async () => {
|
|
70
70
|
const configPath = writeConfig(tmpDir);
|
|
71
|
-
const dbPath = join(tmpDir, 'translations.
|
|
71
|
+
const dbPath = join(tmpDir, 'translations.db');
|
|
72
72
|
|
|
73
73
|
// Pre-populate the DB
|
|
74
74
|
const store = new TranslationStore(dbPath);
|
|
@@ -91,7 +91,7 @@ describe('CLI functions', () => {
|
|
|
91
91
|
|
|
92
92
|
it('translate-plural returns cached plural form', async () => {
|
|
93
93
|
const configPath = writeConfig(tmpDir);
|
|
94
|
-
const dbPath = join(tmpDir, 'translations.
|
|
94
|
+
const dbPath = join(tmpDir, 'translations.db');
|
|
95
95
|
|
|
96
96
|
// Pre-populate the DB with plural forms
|
|
97
97
|
const store = new TranslationStore(dbPath);
|
|
@@ -132,7 +132,7 @@ describe('CLI functions', () => {
|
|
|
132
132
|
|
|
133
133
|
it('warm handles plural entries', async () => {
|
|
134
134
|
const configPath = writeConfig(tmpDir);
|
|
135
|
-
const dbPath = join(tmpDir, 'translations.
|
|
135
|
+
const dbPath = join(tmpDir, 'translations.db');
|
|
136
136
|
|
|
137
137
|
// Pre-populate some plural forms so warm skips them
|
|
138
138
|
const store = new TranslationStore(dbPath);
|
|
@@ -202,7 +202,7 @@ describe('CLI functions', () => {
|
|
|
202
202
|
writeFileSync(join(srcDir, 'app.py'), 'ait("Hello")\n');
|
|
203
203
|
|
|
204
204
|
// Pre-populate the DB so warm skips
|
|
205
|
-
const dbPath = join(tmpDir, 'translations.
|
|
205
|
+
const dbPath = join(tmpDir, 'translations.db');
|
|
206
206
|
const store = new TranslationStore(dbPath);
|
|
207
207
|
await store.initialize();
|
|
208
208
|
await store.insert({
|
package/tests/config.test.ts
CHANGED
|
@@ -19,7 +19,7 @@ languages:
|
|
|
19
19
|
- DE
|
|
20
20
|
- ES
|
|
21
21
|
storage:
|
|
22
|
-
path: ./translations.
|
|
22
|
+
path: ./translations.db
|
|
23
23
|
backend:
|
|
24
24
|
api_key_env: OPENAI_API_KEY
|
|
25
25
|
model: gpt-4.1-mini
|
|
@@ -60,7 +60,7 @@ describe('loadConfig', () => {
|
|
|
60
60
|
const configPath = join(tmpDir, 'transduck.yaml');
|
|
61
61
|
writeFileSync(configPath, VALID_YAML);
|
|
62
62
|
const cfg = loadConfig(configPath);
|
|
63
|
-
expect(cfg.storagePath).toBe(join(tmpDir, 'translations.
|
|
63
|
+
expect(cfg.storagePath).toBe(join(tmpDir, 'translations.db'));
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
it('discovers config from TRANSDUCK_CONFIG env var', () => {
|
package/tests/handler.test.ts
CHANGED
|
@@ -21,7 +21,7 @@ languages:
|
|
|
21
21
|
targets:
|
|
22
22
|
- DE
|
|
23
23
|
storage:
|
|
24
|
-
path: ./translations.
|
|
24
|
+
path: ./translations.db
|
|
25
25
|
backend:
|
|
26
26
|
api_key_env: OPENAI_API_KEY
|
|
27
27
|
model: gpt-4.1-mini
|
|
@@ -48,7 +48,7 @@ describe('handleTranslationRequest', () => {
|
|
|
48
48
|
const { createHash } = await import('crypto');
|
|
49
49
|
const hash = (t: string) => createHash('sha256').update(t).digest('hex');
|
|
50
50
|
|
|
51
|
-
const store = new TranslationStore(join(tmpDir, 'translations.
|
|
51
|
+
const store = new TranslationStore(join(tmpDir, 'translations.db'));
|
|
52
52
|
await store.initialize();
|
|
53
53
|
await store.insert({
|
|
54
54
|
sourceText: 'Hello', sourceLang: 'EN', targetLang: 'DE',
|
|
@@ -83,7 +83,7 @@ describe('handleTranslationRequest', () => {
|
|
|
83
83
|
const { createHash } = await import('crypto');
|
|
84
84
|
const hash = (t: string) => createHash('sha256').update(t).digest('hex');
|
|
85
85
|
|
|
86
|
-
const store = new TranslationStore(join(tmpDir, 'translations.
|
|
86
|
+
const store = new TranslationStore(join(tmpDir, 'translations.db'));
|
|
87
87
|
await store.initialize();
|
|
88
88
|
await store.insert({
|
|
89
89
|
sourceText: 'Book', sourceLang: 'EN', targetLang: 'DE',
|
|
@@ -106,7 +106,7 @@ describe('handleTranslationRequest', () => {
|
|
|
106
106
|
const { createHash } = await import('crypto');
|
|
107
107
|
const hash = (t: string) => createHash('sha256').update(t).digest('hex');
|
|
108
108
|
|
|
109
|
-
const store = new TranslationStore(join(tmpDir, 'translations.
|
|
109
|
+
const store = new TranslationStore(join(tmpDir, 'translations.db'));
|
|
110
110
|
await store.initialize();
|
|
111
111
|
const sourceKey = '{count} item\x00{count} items';
|
|
112
112
|
await store.insertPlural({
|
package/tests/providers.test.ts
CHANGED
|
@@ -20,7 +20,7 @@ function makeConfig(overrides: Partial<TransduckConfig> = {}): TransduckConfig {
|
|
|
20
20
|
projectContext: 'A test site',
|
|
21
21
|
sourceLang: 'EN',
|
|
22
22
|
targetLangs: ['DE'],
|
|
23
|
-
storagePath: '/tmp/test.
|
|
23
|
+
storagePath: '/tmp/test.db',
|
|
24
24
|
provider: 'openai',
|
|
25
25
|
apiKeyEnv: 'OPENAI_API_KEY',
|
|
26
26
|
tokenEnv: 'CLAUDE_CODE_OAUTH_TOKEN',
|
|
@@ -207,7 +207,7 @@ languages:
|
|
|
207
207
|
source: EN
|
|
208
208
|
targets: [DE]
|
|
209
209
|
storage:
|
|
210
|
-
path: ./translations.
|
|
210
|
+
path: ./translations.db
|
|
211
211
|
backend:
|
|
212
212
|
api_key_env: OPENAI_API_KEY
|
|
213
213
|
model: gpt-4.1-mini
|
|
@@ -230,7 +230,7 @@ languages:
|
|
|
230
230
|
source: EN
|
|
231
231
|
targets: [DE]
|
|
232
232
|
storage:
|
|
233
|
-
path: ./translations.
|
|
233
|
+
path: ./translations.db
|
|
234
234
|
backend:
|
|
235
235
|
provider: claude_api
|
|
236
236
|
api_key_env: ANTHROPIC_API_KEY
|
|
@@ -255,7 +255,7 @@ languages:
|
|
|
255
255
|
source: EN
|
|
256
256
|
targets: [DE]
|
|
257
257
|
storage:
|
|
258
|
-
path: ./translations.
|
|
258
|
+
path: ./translations.db
|
|
259
259
|
backend:
|
|
260
260
|
provider: claude_code
|
|
261
261
|
token_env: CLAUDE_CODE_OAUTH_TOKEN
|
|
@@ -277,7 +277,7 @@ languages:
|
|
|
277
277
|
source: EN
|
|
278
278
|
targets: [DE]
|
|
279
279
|
storage:
|
|
280
|
-
path: ./translations.
|
|
280
|
+
path: ./translations.db
|
|
281
281
|
backend:
|
|
282
282
|
provider: claude_code
|
|
283
283
|
`);
|
package/tests/storage.test.ts
CHANGED
|
@@ -14,7 +14,7 @@ describe('TranslationStore', () => {
|
|
|
14
14
|
|
|
15
15
|
beforeEach(async () => {
|
|
16
16
|
const tmpDir = mkdtempSync(join(tmpdir(), 'transduck-test-'));
|
|
17
|
-
store = new TranslationStore(join(tmpDir, 'test.
|
|
17
|
+
store = new TranslationStore(join(tmpDir, 'test.db'));
|
|
18
18
|
await store.initialize();
|
|
19
19
|
});
|
|
20
20
|
|