transduck 0.4.2 → 0.5.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/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.lmdb' },
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.lmdb');
49
+ const dbPath = join(opts.dir, 'translations.db');
50
50
  const store = new TranslationStore(dbPath);
51
51
  await store.initialize();
52
52
  store.close();
@@ -454,7 +454,7 @@ export async function runStats(opts) {
454
454
  }
455
455
  // CLI entry point
456
456
  const program = new Command();
457
- program.name('transduck').description('AI-native translation tool').version('0.4.2');
457
+ program.name('transduck').description('AI-native translation tool').version('0.5.0');
458
458
  program.command('init')
459
459
  .description('Initialize a new transduck project')
460
460
  .action(async () => {
package/dist/storage.js CHANGED
@@ -1,24 +1,10 @@
1
- import { open } from 'lmdb';
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,11 +12,25 @@ export class TranslationStore {
26
12
  this.dbPath = dbPath;
27
13
  }
28
14
  async initialize() {
29
- this.db = open({
30
- path: this.dbPath,
31
- mapSize: DEFAULT_MAP_SIZE,
32
- encoding: 'json',
33
- });
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
+ `);
34
34
  }
35
35
  getDb() {
36
36
  if (!this.db)
@@ -40,81 +40,45 @@ export class TranslationStore {
40
40
  async lookup(params) {
41
41
  const db = this.getDb();
42
42
  const chash = contentHash(params.sourceText, params.projectContextHash, params.stringContextHash);
43
- const key = makeKey(params.sourceLang, params.targetLang, chash, '');
44
- const entry = db.get(key);
45
- if (entry && entry.status === 'translated') {
46
- return entry.translated_text;
47
- }
48
- 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;
49
45
  }
50
46
  async insert(params) {
51
47
  const db = this.getDb();
52
48
  const chash = contentHash(params.sourceText, params.projectContextHash, params.stringContextHash);
53
- const key = makeKey(params.sourceLang, params.targetLang, chash, '');
54
- // ON CONFLICT DO NOTHING only insert if key doesn't exist
55
- if (db.get(key) !== undefined)
56
- return;
57
- await db.put(key, {
58
- source_text: params.sourceText,
59
- translated_text: params.translatedText,
60
- model: params.model,
61
- status: params.status,
62
- created_at: new Date().toISOString(),
63
- project_context_hash: params.projectContextHash,
64
- string_context_hash: params.stringContextHash,
65
- string_context: params.stringContext,
66
- });
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);
67
51
  }
68
52
  async lookupPlural(params) {
69
53
  const db = this.getDb();
70
54
  const chash = contentHash(params.sourceText, params.projectContextHash, params.stringContextHash);
71
- const prefix = makeKey(params.sourceLang, params.targetLang, chash, '');
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');
72
57
  const result = {};
73
- for (const { key, value } of db.getRange({ start: prefix })) {
74
- const keyStr = key;
75
- if (!keyStr.startsWith(prefix))
76
- break;
77
- const parsed = parseKey(keyStr);
78
- // Skip regular entries (empty plural_category)
79
- if (!parsed.pluralCategory)
80
- continue;
81
- if (value.status === 'translated') {
82
- result[parsed.pluralCategory] = value.translated_text;
83
- }
58
+ for (const row of rows) {
59
+ result[row.plural_category] = row.translated_text;
84
60
  }
85
61
  return result;
86
62
  }
87
63
  async insertPlural(params) {
88
64
  const db = this.getDb();
89
65
  const chash = contentHash(params.sourceText, params.projectContextHash, params.stringContextHash);
90
- const key = makeKey(params.sourceLang, params.targetLang, chash, params.pluralCategory);
91
- // ON CONFLICT DO NOTHING
92
- if (db.get(key) !== undefined)
93
- return;
94
- await db.put(key, {
95
- source_text: params.sourceText,
96
- translated_text: params.translatedText,
97
- model: params.model,
98
- status: params.status,
99
- created_at: new Date().toISOString(),
100
- project_context_hash: params.projectContextHash,
101
- string_context_hash: params.stringContextHash,
102
- string_context: params.stringContext,
103
- });
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);
104
68
  }
105
69
  async stats() {
106
70
  const db = this.getDb();
107
71
  let totalTranslations = 0;
108
72
  let totalFailed = 0;
109
73
  const byLanguage = {};
110
- for (const { key, value } of db.getRange({})) {
111
- const parsed = parseKey(key);
112
- if (value.status === 'translated') {
113
- totalTranslations++;
114
- byLanguage[parsed.targetLang] = (byLanguage[parsed.targetLang] ?? 0) + 1;
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;
115
79
  }
116
- else if (value.status === 'failed') {
117
- totalFailed++;
80
+ else if (row.status === 'failed') {
81
+ totalFailed += row.count;
118
82
  }
119
83
  }
120
84
  return { totalTranslations, totalFailed, byLanguage };
@@ -124,36 +88,36 @@ export class TranslationStore {
124
88
  */
125
89
  count(targetLang, failedOnly) {
126
90
  const db = this.getDb();
127
- let n = 0;
128
- for (const { key, value } of db.getRange({})) {
129
- const parsed = parseKey(key);
130
- if (targetLang && parsed.targetLang !== targetLang)
131
- continue;
132
- if (failedOnly && value.status !== 'failed')
133
- continue;
134
- n++;
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');
135
100
  }
136
- return n;
101
+ const row = db.prepare(sql).get(...params);
102
+ return row.count;
137
103
  }
138
104
  /**
139
105
  * Delete entries matching optional filters. Returns count deleted.
140
106
  */
141
107
  async clear(targetLang, failedOnly) {
142
108
  const db = this.getDb();
143
- const toDelete = [];
144
- for (const { key, value } of db.getRange({})) {
145
- const keyStr = key;
146
- const parsed = parseKey(keyStr);
147
- if (targetLang && parsed.targetLang !== targetLang)
148
- continue;
149
- if (failedOnly && value.status !== 'failed')
150
- continue;
151
- 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);
152
114
  }
153
- for (const k of toDelete) {
154
- await db.remove(k);
115
+ if (failedOnly) {
116
+ sql += ' AND status = ?';
117
+ params.push('failed');
155
118
  }
156
- return toDelete.length;
119
+ const result = db.prepare(sql).run(...params);
120
+ return result.changes;
157
121
  }
158
122
  close() {
159
123
  if (this.db) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "transduck",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
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
- "lmdb": "^3.5.2",
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/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.lmdb' },
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.lmdb');
63
+ const dbPath = join(opts.dir, 'translations.db');
64
64
  const store = new TranslationStore(dbPath);
65
65
  await store.initialize();
66
66
  store.close();
@@ -562,7 +562,7 @@ export async function runStats(opts: StatsOptions): Promise<string> {
562
562
  // CLI entry point
563
563
  const program = new Command();
564
564
 
565
- program.name('transduck').description('AI-native translation tool').version('0.4.2');
565
+ program.name('transduck').description('AI-native translation tool').version('0.5.0');
566
566
 
567
567
  program.command('init')
568
568
  .description('Initialize a new transduck project')
package/src/storage.ts CHANGED
@@ -1,9 +1,6 @@
1
- import { open, type Database, type RootDatabase } from 'lmdb';
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,37 +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: RootDatabase<StoredEntry, string> | null = null;
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 = open<StoredEntry, string>({
77
- path: this.dbPath,
78
- mapSize: DEFAULT_MAP_SIZE,
79
- encoding: 'json',
80
- });
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
+ `);
81
67
  }
82
68
 
83
- private getDb(): RootDatabase<StoredEntry, string> {
69
+ private getDb(): Database.Database {
84
70
  if (!this.db) throw new Error('Store not initialized');
85
71
  return this.db;
86
72
  }
@@ -88,47 +74,36 @@ export class TranslationStore {
88
74
  async lookup(params: LookupParams): Promise<string | null> {
89
75
  const db = this.getDb();
90
76
  const chash = contentHash(params.sourceText, params.projectContextHash, params.stringContextHash);
91
- const key = makeKey(params.sourceLang, params.targetLang, chash, '');
92
- const entry = db.get(key);
93
- if (entry && entry.status === 'translated') {
94
- return entry.translated_text;
95
- }
96
- 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;
97
81
  }
98
82
 
99
83
  async insert(params: InsertParams): Promise<void> {
100
84
  const db = this.getDb();
101
85
  const chash = contentHash(params.sourceText, params.projectContextHash, params.stringContextHash);
102
- const key = makeKey(params.sourceLang, params.targetLang, chash, '');
103
- // ON CONFLICT DO NOTHING only insert if key doesn't exist
104
- if (db.get(key) !== undefined) return;
105
- await db.put(key, {
106
- source_text: params.sourceText,
107
- translated_text: params.translatedText,
108
- model: params.model,
109
- status: params.status,
110
- created_at: new Date().toISOString(),
111
- project_context_hash: params.projectContextHash,
112
- string_context_hash: params.stringContextHash,
113
- string_context: params.stringContext,
114
- });
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
+ );
115
95
  }
116
96
 
117
97
  async lookupPlural(params: LookupParams): Promise<Record<string, string>> {
118
98
  const db = this.getDb();
119
99
  const chash = contentHash(params.sourceText, params.projectContextHash, params.stringContextHash);
120
- const prefix = makeKey(params.sourceLang, params.targetLang, chash, '');
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 }[];
121
104
  const result: Record<string, string> = {};
122
-
123
- for (const { key, value } of db.getRange({ start: prefix })) {
124
- const keyStr = key as string;
125
- if (!keyStr.startsWith(prefix)) break;
126
- const parsed = parseKey(keyStr);
127
- // Skip regular entries (empty plural_category)
128
- if (!parsed.pluralCategory) continue;
129
- if (value.status === 'translated') {
130
- result[parsed.pluralCategory] = value.translated_text;
131
- }
105
+ for (const row of rows) {
106
+ result[row.plural_category] = row.translated_text;
132
107
  }
133
108
  return result;
134
109
  }
@@ -136,19 +111,15 @@ export class TranslationStore {
136
111
  async insertPlural(params: InsertPluralParams): Promise<void> {
137
112
  const db = this.getDb();
138
113
  const chash = contentHash(params.sourceText, params.projectContextHash, params.stringContextHash);
139
- const key = makeKey(params.sourceLang, params.targetLang, chash, params.pluralCategory);
140
- // ON CONFLICT DO NOTHING
141
- if (db.get(key) !== undefined) return;
142
- await db.put(key, {
143
- source_text: params.sourceText,
144
- translated_text: params.translatedText,
145
- model: params.model,
146
- status: params.status,
147
- created_at: new Date().toISOString(),
148
- project_context_hash: params.projectContextHash,
149
- string_context_hash: params.stringContextHash,
150
- string_context: params.stringContext,
151
- });
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
+ );
152
123
  }
153
124
 
154
125
  async stats(): Promise<Stats> {
@@ -157,13 +128,16 @@ export class TranslationStore {
157
128
  let totalFailed = 0;
158
129
  const byLanguage: Record<string, number> = {};
159
130
 
160
- for (const { key, value } of db.getRange({})) {
161
- const parsed = parseKey(key as string);
162
- if (value.status === 'translated') {
163
- totalTranslations++;
164
- byLanguage[parsed.targetLang] = (byLanguage[parsed.targetLang] ?? 0) + 1;
165
- } else if (value.status === 'failed') {
166
- totalFailed++;
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;
167
141
  }
168
142
  }
169
143
 
@@ -175,14 +149,18 @@ export class TranslationStore {
175
149
  */
176
150
  count(targetLang?: string, failedOnly?: boolean): number {
177
151
  const db = this.getDb();
178
- let n = 0;
179
- for (const { key, value } of db.getRange({})) {
180
- const parsed = parseKey(key as string);
181
- if (targetLang && parsed.targetLang !== targetLang) continue;
182
- if (failedOnly && value.status !== 'failed') continue;
183
- n++;
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');
184
161
  }
185
- return n;
162
+ const row = db.prepare(sql).get(...params) as { count: number };
163
+ return row.count;
186
164
  }
187
165
 
188
166
  /**
@@ -190,18 +168,18 @@ export class TranslationStore {
190
168
  */
191
169
  async clear(targetLang?: string, failedOnly?: boolean): Promise<number> {
192
170
  const db = this.getDb();
193
- const toDelete: string[] = [];
194
- for (const { key, value } of db.getRange({})) {
195
- const keyStr = key as string;
196
- const parsed = parseKey(keyStr);
197
- if (targetLang && parsed.targetLang !== targetLang) continue;
198
- if (failedOnly && value.status !== 'failed') continue;
199
- 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);
200
176
  }
201
- for (const k of toDelete) {
202
- await db.remove(k);
177
+ if (failedOnly) {
178
+ sql += ' AND status = ?';
179
+ params.push('failed');
203
180
  }
204
- return toDelete.length;
181
+ const result = db.prepare(sql).run(...params);
182
+ return result.changes;
205
183
  }
206
184
 
207
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.lmdb'),
18
+ storagePath: join(tmpDir, 'test.db'),
19
19
  provider: 'openai',
20
20
  apiKeyEnv: 'OPENAI_API_KEY',
21
21
  tokenEnv: 'CLAUDE_CODE_OAUTH_TOKEN',
@@ -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.lmdb',
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.lmdb
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.lmdb'))).toBe(true);
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.lmdb');
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.lmdb');
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.lmdb');
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.lmdb');
205
+ const dbPath = join(tmpDir, 'translations.db');
206
206
  const store = new TranslationStore(dbPath);
207
207
  await store.initialize();
208
208
  await store.insert({
@@ -19,7 +19,7 @@ languages:
19
19
  - DE
20
20
  - ES
21
21
  storage:
22
- path: ./translations.lmdb
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.lmdb'));
63
+ expect(cfg.storagePath).toBe(join(tmpDir, 'translations.db'));
64
64
  });
65
65
 
66
66
  it('discovers config from TRANSDUCK_CONFIG env var', () => {
@@ -21,7 +21,7 @@ languages:
21
21
  targets:
22
22
  - DE
23
23
  storage:
24
- path: ./translations.lmdb
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.lmdb'));
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.lmdb'));
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.lmdb'));
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({
@@ -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.lmdb',
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.lmdb
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.lmdb
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.lmdb
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.lmdb
280
+ path: ./translations.db
281
281
  backend:
282
282
  provider: claude_code
283
283
  `);
@@ -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.lmdb'));
17
+ store = new TranslationStore(join(tmpDir, 'test.db'));
18
18
  await store.initialize();
19
19
  });
20
20