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/README.md +3 -3
- package/dist/cli.js +3 -3
- package/dist/react/index.d.ts +2 -1
- package/dist/react/index.js +1 -1
- package/dist/react/provider.d.ts +27 -3
- package/dist/react/provider.js +104 -20
- package/dist/storage.d.ts +10 -5
- package/dist/storage.js +137 -153
- package/package.json +18 -10
- package/src/cli.ts +3 -3
- package/src/react/index.ts +2 -1
- package/src/react/provider.tsx +142 -18
- package/src/storage.ts +146 -167
- 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/react-provider.test.tsx +363 -2
- package/tests/storage.test.ts +27 -59
package/dist/storage.js
CHANGED
|
@@ -1,178 +1,162 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
148
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
104
|
+
const db = this.getDb();
|
|
105
|
+
let totalTranslations = 0;
|
|
106
|
+
let totalFailed = 0;
|
|
163
107
|
const byLanguage = {};
|
|
164
|
-
for (const
|
|
165
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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.
|
|
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
|
-
"
|
|
45
|
-
"react-dom": ">=18.0.0",
|
|
44
|
+
"@anthropic-ai/claude-agent-sdk": ">=0.1.0",
|
|
46
45
|
"@anthropic-ai/sdk": ">=0.30.0",
|
|
47
|
-
"
|
|
46
|
+
"react": ">=18.0.0",
|
|
47
|
+
"react-dom": ">=18.0.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependenciesMeta": {
|
|
50
|
-
"react": {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
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.
|
|
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.
|
|
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.
|
|
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')
|
package/src/react/index.ts
CHANGED
|
@@ -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';
|