transduck 0.2.5 → 0.4.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/storage.js CHANGED
@@ -1,178 +1,162 @@
1
- import { DuckDBInstance, DuckDBResultReader } from '@duckdb/node-api';
2
- const SCHEMA_V2 = `
3
- CREATE TABLE IF NOT EXISTS translations (
4
- source_text TEXT NOT NULL,
5
- source_lang TEXT NOT NULL,
6
- target_lang TEXT NOT NULL,
7
- project_context_hash TEXT NOT NULL,
8
- string_context_hash TEXT NOT NULL,
9
- plural_category TEXT NOT NULL DEFAULT '',
10
- translated_text TEXT NOT NULL,
11
- model TEXT NOT NULL,
12
- status TEXT NOT NULL DEFAULT 'translated',
13
- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
14
- PRIMARY KEY(source_text, source_lang, target_lang, project_context_hash, string_context_hash, plural_category)
15
- );
16
- `;
17
- const MIGRATION_V1_TO_V2 = `
18
- CREATE TABLE translations_v2 (
19
- source_text TEXT NOT NULL,
20
- source_lang TEXT NOT NULL,
21
- target_lang TEXT NOT NULL,
22
- project_context_hash TEXT NOT NULL,
23
- string_context_hash TEXT NOT NULL,
24
- plural_category TEXT NOT NULL DEFAULT '',
25
- translated_text TEXT NOT NULL,
26
- model TEXT NOT NULL,
27
- status TEXT NOT NULL DEFAULT 'translated',
28
- created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
29
- PRIMARY KEY(source_text, source_lang, target_lang, project_context_hash, string_context_hash, plural_category)
30
- );
31
- INSERT INTO translations_v2
32
- SELECT source_text, source_lang, target_lang, project_context_hash,
33
- string_context_hash, '' as plural_category, translated_text,
34
- model, status, created_at
35
- FROM translations;
36
- DROP TABLE translations;
37
- ALTER TABLE translations_v2 RENAME TO translations;
38
- `;
39
- const CHECK_PLURAL_COLUMN = `
40
- SELECT column_name FROM information_schema.columns
41
- WHERE table_name = 'translations' AND column_name = 'plural_category'
42
- `;
43
- const LOOKUP_SQL = `
44
- SELECT translated_text FROM translations
45
- WHERE source_text = $1 AND source_lang = $2 AND target_lang = $3
46
- AND project_context_hash = $4 AND string_context_hash = $5
47
- AND plural_category = '' AND status = 'translated'
48
- `;
49
- const INSERT_SQL = `
50
- INSERT INTO translations
51
- (source_text, source_lang, target_lang, project_context_hash,
52
- string_context_hash, plural_category, translated_text, model, status)
53
- VALUES ($1, $2, $3, $4, $5, '', $6, $7, $8)
54
- ON CONFLICT DO NOTHING
55
- `;
56
- const LOOKUP_PLURAL_SQL = `
57
- SELECT plural_category, translated_text FROM translations
58
- WHERE source_text = $1 AND source_lang = $2 AND target_lang = $3
59
- AND project_context_hash = $4 AND string_context_hash = $5
60
- AND plural_category != '' AND status = 'translated'
61
- `;
62
- const INSERT_PLURAL_SQL = `
63
- INSERT INTO translations
64
- (source_text, source_lang, target_lang, project_context_hash,
65
- string_context_hash, plural_category, translated_text, model, status)
66
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
67
- ON CONFLICT DO NOTHING
68
- `;
1
+ import { open } from 'lmdb';
2
+ import { createHash } from 'crypto';
3
+ // 1 GB default map size — plenty for translation caches
4
+ const DEFAULT_MAP_SIZE = 1024 * 1024 * 1024;
5
+ function contentHash(sourceText, projectContextHash, stringContextHash) {
6
+ return createHash('sha256')
7
+ .update(sourceText + '\x00' + projectContextHash + '\x00' + stringContextHash)
8
+ .digest('hex');
9
+ }
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
+ }
69
22
  export class TranslationStore {
70
23
  dbPath;
71
- instance = null;
72
- conn = null;
24
+ db = null;
73
25
  constructor(dbPath) {
74
26
  this.dbPath = dbPath;
75
27
  }
76
- async getConn() {
77
- if (!this.conn)
78
- throw new Error('Store not initialized');
79
- return this.conn;
80
- }
81
- convertRow(row) {
82
- const converted = {};
83
- for (const [key, value] of Object.entries(row)) {
84
- if (typeof value === 'bigint') {
85
- converted[key] = Number(value);
86
- }
87
- else {
88
- converted[key] = value;
89
- }
90
- }
91
- return converted;
92
- }
93
- async query(sql, params = []) {
94
- const conn = await this.getConn();
95
- let result;
96
- if (params.length === 0) {
97
- result = await conn.run(sql);
98
- }
99
- else {
100
- const stmt = await conn.prepare(sql);
101
- for (let i = 0; i < params.length; i++) {
102
- stmt.bindVarchar(i + 1, params[i]);
103
- }
104
- result = await stmt.run();
105
- }
106
- const reader = new DuckDBResultReader(result);
107
- await reader.readAll();
108
- return [...reader.getRowObjects()].map(row => this.convertRow(row));
109
- }
110
28
  async initialize() {
111
- this.instance = await DuckDBInstance.create(this.dbPath);
112
- this.conn = await this.instance.connect();
113
- // Check if table exists
114
- const tables = await this.query("SELECT table_name FROM information_schema.tables WHERE table_name = 'translations'");
115
- if (tables.length === 0) {
116
- // Fresh database — create v2 schema
117
- await this.conn.run(SCHEMA_V2);
118
- return;
119
- }
120
- // Table exists — check if it has plural_category (v2)
121
- const hasPlural = await this.query(CHECK_PLURAL_COLUMN);
122
- if (hasPlural.length === 0) {
123
- // Migrate from v1 to v2
124
- await this.conn.run(MIGRATION_V1_TO_V2);
125
- }
29
+ this.db = open({
30
+ path: this.dbPath,
31
+ mapSize: DEFAULT_MAP_SIZE,
32
+ // Use msgpack (default) for efficient storage
33
+ });
34
+ }
35
+ getDb() {
36
+ if (!this.db)
37
+ throw new Error('Store not initialized');
38
+ return this.db;
126
39
  }
127
40
  async lookup(params) {
128
- const rows = await this.query(LOOKUP_SQL, [
129
- params.sourceText, params.sourceLang, params.targetLang,
130
- params.projectContextHash, params.stringContextHash,
131
- ]);
132
- return rows.length > 0 ? rows[0].translated_text : null;
41
+ const db = this.getDb();
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;
133
49
  }
134
50
  async insert(params) {
135
- await this.query(INSERT_SQL, [
136
- params.sourceText, params.sourceLang, params.targetLang,
137
- params.projectContextHash, params.stringContextHash,
138
- params.translatedText, params.model, params.status,
139
- ]);
51
+ const db = this.getDb();
52
+ 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
+ });
140
66
  }
141
67
  async lookupPlural(params) {
142
- const rows = await this.query(LOOKUP_PLURAL_SQL, [
143
- params.sourceText, params.sourceLang, params.targetLang,
144
- params.projectContextHash, params.stringContextHash,
145
- ]);
68
+ const db = this.getDb();
69
+ const chash = contentHash(params.sourceText, params.projectContextHash, params.stringContextHash);
70
+ const prefix = makeKey(params.sourceLang, params.targetLang, chash, '');
146
71
  const result = {};
147
- for (const row of rows) {
148
- result[row.plural_category] = row.translated_text;
72
+ for (const { key, value } of db.getRange({ start: prefix })) {
73
+ const keyStr = key;
74
+ if (!keyStr.startsWith(prefix))
75
+ break;
76
+ const parsed = parseKey(keyStr);
77
+ // Skip regular entries (empty plural_category)
78
+ if (!parsed.pluralCategory)
79
+ continue;
80
+ if (value.status === 'translated') {
81
+ result[parsed.pluralCategory] = value.translated_text;
82
+ }
149
83
  }
150
84
  return result;
151
85
  }
152
86
  async insertPlural(params) {
153
- await this.query(INSERT_PLURAL_SQL, [
154
- params.sourceText, params.sourceLang, params.targetLang,
155
- params.projectContextHash, params.stringContextHash,
156
- params.pluralCategory, params.translatedText, params.model, params.status,
157
- ]);
87
+ const db = this.getDb();
88
+ const chash = contentHash(params.sourceText, params.projectContextHash, params.stringContextHash);
89
+ const key = makeKey(params.sourceLang, params.targetLang, chash, params.pluralCategory);
90
+ // ON CONFLICT DO NOTHING
91
+ if (db.get(key) !== undefined)
92
+ return;
93
+ await db.put(key, {
94
+ source_text: params.sourceText,
95
+ translated_text: params.translatedText,
96
+ model: params.model,
97
+ status: params.status,
98
+ created_at: new Date().toISOString(),
99
+ project_context_hash: params.projectContextHash,
100
+ string_context_hash: params.stringContextHash,
101
+ });
158
102
  }
159
103
  async stats() {
160
- const totalRows = await this.query("SELECT count(*) as c FROM translations WHERE status = 'translated'");
161
- const failedRows = await this.query("SELECT count(*) as c FROM translations WHERE status = 'failed'");
162
- const langRows = await this.query("SELECT target_lang, count(*) as c FROM translations WHERE status = 'translated' GROUP BY target_lang");
104
+ const db = this.getDb();
105
+ let totalTranslations = 0;
106
+ let totalFailed = 0;
163
107
  const byLanguage = {};
164
- for (const row of langRows) {
165
- byLanguage[row.target_lang] = Number(row.c);
108
+ for (const { key, value } of db.getRange({})) {
109
+ const parsed = parseKey(key);
110
+ if (value.status === 'translated') {
111
+ totalTranslations++;
112
+ byLanguage[parsed.targetLang] = (byLanguage[parsed.targetLang] ?? 0) + 1;
113
+ }
114
+ else if (value.status === 'failed') {
115
+ totalFailed++;
116
+ }
117
+ }
118
+ return { totalTranslations, totalFailed, byLanguage };
119
+ }
120
+ /**
121
+ * Count entries matching optional filters.
122
+ */
123
+ count(targetLang, failedOnly) {
124
+ const db = this.getDb();
125
+ let n = 0;
126
+ for (const { key, value } of db.getRange({})) {
127
+ const parsed = parseKey(key);
128
+ if (targetLang && parsed.targetLang !== targetLang)
129
+ continue;
130
+ if (failedOnly && value.status !== 'failed')
131
+ continue;
132
+ n++;
166
133
  }
167
- return {
168
- totalTranslations: Number(totalRows[0].c),
169
- totalFailed: Number(failedRows[0].c),
170
- byLanguage,
171
- };
134
+ return n;
135
+ }
136
+ /**
137
+ * Delete entries matching optional filters. Returns count deleted.
138
+ */
139
+ async clear(targetLang, failedOnly) {
140
+ const db = this.getDb();
141
+ const toDelete = [];
142
+ for (const { key, value } of db.getRange({})) {
143
+ const keyStr = key;
144
+ const parsed = parseKey(keyStr);
145
+ if (targetLang && parsed.targetLang !== targetLang)
146
+ continue;
147
+ if (failedOnly && value.status !== 'failed')
148
+ continue;
149
+ toDelete.push(keyStr);
150
+ }
151
+ for (const k of toDelete) {
152
+ await db.remove(k);
153
+ }
154
+ return toDelete.length;
172
155
  }
173
156
  close() {
174
- // @duckdb/node-api handles cleanup via GC
175
- this.conn = null;
176
- this.instance = null;
157
+ if (this.db) {
158
+ this.db.close();
159
+ this.db = null;
160
+ }
177
161
  }
178
162
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "transduck",
3
- "version": "0.2.5",
3
+ "version": "0.4.0",
4
4
  "description": "AI-native translation tool using source text as keys",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -24,16 +24,16 @@
24
24
  "test:watch": "vitest"
25
25
  },
26
26
  "dependencies": {
27
- "@duckdb/node-api": "^1.5.0-r.1",
28
27
  "commander": "^12.0.0",
28
+ "lmdb": "^3.5.2",
29
29
  "openai": "^4.0.0",
30
30
  "yaml": "^2.0.0"
31
31
  },
32
32
  "devDependencies": {
33
+ "@testing-library/react": "^16.0.0",
33
34
  "@types/node": "^22.0.0",
34
35
  "@types/react": "^19.0.0",
35
36
  "@types/react-dom": "^19.0.0",
36
- "@testing-library/react": "^16.0.0",
37
37
  "jsdom": "^25.0.0",
38
38
  "react": "^19.0.0",
39
39
  "react-dom": "^19.0.0",
@@ -41,15 +41,23 @@
41
41
  "vitest": "^2.0.0"
42
42
  },
43
43
  "peerDependencies": {
44
- "react": ">=18.0.0",
45
- "react-dom": ">=18.0.0",
44
+ "@anthropic-ai/claude-agent-sdk": ">=0.1.0",
46
45
  "@anthropic-ai/sdk": ">=0.30.0",
47
- "@anthropic-ai/claude-agent-sdk": ">=0.1.0"
46
+ "react": ">=18.0.0",
47
+ "react-dom": ">=18.0.0"
48
48
  },
49
49
  "peerDependenciesMeta": {
50
- "react": { "optional": true },
51
- "react-dom": { "optional": true },
52
- "@anthropic-ai/sdk": { "optional": true },
53
- "@anthropic-ai/claude-agent-sdk": { "optional": true }
50
+ "react": {
51
+ "optional": true
52
+ },
53
+ "react-dom": {
54
+ "optional": true
55
+ },
56
+ "@anthropic-ai/sdk": {
57
+ "optional": true
58
+ },
59
+ "@anthropic-ai/claude-agent-sdk": {
60
+ "optional": true
61
+ }
54
62
  }
55
63
  }
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.duckdb' },
34
+ storage: { path: './translations.lmdb' },
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.duckdb');
63
+ const dbPath = join(opts.dir, 'translations.lmdb');
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.2.5');
565
+ program.name('transduck').description('AI-native translation tool').version('0.4.0');
566
566
 
567
567
  program.command('init')
568
568
  .description('Initialize a new transduck project')
@@ -1,3 +1,4 @@
1
1
  'use client';
2
2
 
3
- export { TransDuckProvider, t, ait, tPlural, aitPlural, useTransDuck, _resetReactState } from './provider.js';
3
+ export { TransDuckProvider, t, ait, tPlural, aitPlural, useTransDuck, _resetReactState, _getReactState } from './provider.js';
4
+ export type { UseTransDuckReturn } from './provider.js';