gptrans 2.1.8 → 2.2.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 +11 -7
- package/demo/case_1.js +4 -2
- package/demo/case_2.js +4 -1
- package/demo/case_3.js +4 -1
- package/demo/case_4.js +2 -1
- package/demo/case_language_info.js +4 -0
- package/demo/case_parallelism.js +2 -0
- package/demo/case_references.js +6 -0
- package/demo/case_refine.js +3 -0
- package/demo/example-image-translation.js +1 -1
- package/index.js +64 -46
- package/package.json +5 -4
- package/pnpm-workspace.yaml +7 -7
- package/skills/gptrans/SKILL.md +14 -9
- package/test/gptrans.batch.test.js +31 -19
- package/test/gptrans.tAsync.test.js +64 -2
- package/test-image-translation.js +1 -1
package/README.md
CHANGED
|
@@ -45,7 +45,9 @@ Here's a simple example to get you started:
|
|
|
45
45
|
```javascript
|
|
46
46
|
import GPTrans from 'gptrans';
|
|
47
47
|
|
|
48
|
+
const dbPath = new URL('./db', import.meta.url).pathname;
|
|
48
49
|
const gptrans = new GPTrans({
|
|
50
|
+
path: dbPath,
|
|
49
51
|
from: 'en-US',
|
|
50
52
|
target: 'es-AR',
|
|
51
53
|
model: 'sonnet45'
|
|
@@ -74,6 +76,7 @@ When creating a new instance of GPTrans, you can customize:
|
|
|
74
76
|
|
|
75
77
|
| Option | Description | Default |
|
|
76
78
|
|--------|-------------|---------|
|
|
79
|
+
| `path` | Absolute directory path for the DeepBase translation cache | Required |
|
|
77
80
|
| `from` | Source language locale (BCP 47) | `en-US` |
|
|
78
81
|
| `target` | Target language locale (BCP 47) | `es` |
|
|
79
82
|
| `model` | Translation model key or array of models for fallback | `sonnet45` `gpt41` |
|
|
@@ -96,7 +99,7 @@ For simplified or universal language codes, you can omit the region specificatio
|
|
|
96
99
|
## 🔍 How It Works
|
|
97
100
|
|
|
98
101
|
1. **First-Time Translation Behavior:** On the first request, Gptrans will return the original text while processing the translation in the background. This ensures your application remains responsive without waiting for API calls.
|
|
99
|
-
2. **Translation Caching:** Once processed, translations are stored
|
|
102
|
+
2. **Translation Caching:** Once processed, translations are stored as `gptrans_<tag>.json` under the required absolute `path`. Subsequent requests for the same text will be served instantly from the cache.
|
|
100
103
|
3. **Smart Batch Processing:** Automatically groups translation requests to optimize API usage and provide better context.
|
|
101
104
|
4. **Dynamic Model Integration:** Easily plug in multiple AI translation providers with the ModelMix library.
|
|
102
105
|
5. **Customizable Prompts:** Load and modify translation prompts (see the `prompt/translate.md` file) to fine-tune the translation output.
|
|
@@ -132,6 +135,7 @@ The `instruction` option lets you guide the AI translator's style, tone, or beha
|
|
|
132
135
|
|
|
133
136
|
```javascript
|
|
134
137
|
const gptrans = new GPTrans({
|
|
138
|
+
path: dbPath,
|
|
135
139
|
from: 'en',
|
|
136
140
|
target: 'es-AR',
|
|
137
141
|
instruction: 'Use natural and colloquial tone'
|
|
@@ -168,7 +172,7 @@ Include translations from other languages as context to improve accuracy and con
|
|
|
168
172
|
|
|
169
173
|
```javascript
|
|
170
174
|
// Use English and Portuguese translations as reference
|
|
171
|
-
const gptrans = new GPTrans({ from: 'es', target: 'fr' });
|
|
175
|
+
const gptrans = new GPTrans({ path: dbPath, from: 'es', target: 'fr' });
|
|
172
176
|
await gptrans.preload({
|
|
173
177
|
references: ['en', 'pt']
|
|
174
178
|
});
|
|
@@ -182,7 +186,7 @@ Translate from an intermediate language instead of the original:
|
|
|
182
186
|
|
|
183
187
|
```javascript
|
|
184
188
|
// Original is Spanish, but translate FROM English TO Portuguese
|
|
185
|
-
const gptrans = new GPTrans({ from: 'es', target: 'pt' });
|
|
189
|
+
const gptrans = new GPTrans({ path: dbPath, from: 'es', target: 'pt' });
|
|
186
190
|
await gptrans.preload({
|
|
187
191
|
baseLanguage: 'en'
|
|
188
192
|
});
|
|
@@ -199,7 +203,7 @@ You can use both options together:
|
|
|
199
203
|
|
|
200
204
|
```javascript
|
|
201
205
|
// Translate from English to German, showing Spanish and Portuguese as reference
|
|
202
|
-
const gptrans = new GPTrans({ from: 'es', target: 'de' });
|
|
206
|
+
const gptrans = new GPTrans({ path: dbPath, from: 'es', target: 'de' });
|
|
203
207
|
await gptrans.preload({
|
|
204
208
|
baseLanguage: 'en',
|
|
205
209
|
references: ['es', 'pt']
|
|
@@ -215,7 +219,7 @@ await gptrans.preload({
|
|
|
215
219
|
// English: "The student is very good" (neutral)
|
|
216
220
|
|
|
217
221
|
// Solution: Translate to Portuguese using English as base
|
|
218
|
-
const ptTranslator = new GPTrans({ from: 'es', target: 'pt' });
|
|
222
|
+
const ptTranslator = new GPTrans({ path: dbPath, from: 'es', target: 'pt' });
|
|
219
223
|
await ptTranslator.preload({
|
|
220
224
|
baseLanguage: 'en', // Use neutral English version
|
|
221
225
|
references: ['es'] // Show original Spanish for context
|
|
@@ -236,6 +240,7 @@ GPTrans supports a fallback mechanism for translation models. Instead of providi
|
|
|
236
240
|
|
|
237
241
|
```javascript
|
|
238
242
|
const translator = new GPTrans({
|
|
243
|
+
path: dbPath,
|
|
239
244
|
model: ['claude46', 'gpt54'],
|
|
240
245
|
// ... other options
|
|
241
246
|
});
|
|
@@ -251,7 +256,7 @@ When using multiple models:
|
|
|
251
256
|
The `refine()` method lets you improve existing translations by running them through the AI again with a specific instruction. It processes translations in batches (same as the translation flow) and only updates entries that genuinely benefit from the refinement.
|
|
252
257
|
|
|
253
258
|
```javascript
|
|
254
|
-
const gptrans = new GPTrans({ from: 'en', target: 'es-AR' });
|
|
259
|
+
const gptrans = new GPTrans({ path: dbPath, from: 'en', target: 'es-AR' });
|
|
255
260
|
|
|
256
261
|
// After translations already exist...
|
|
257
262
|
// Refine with a single instruction
|
|
@@ -306,4 +311,3 @@ Contributions are welcome! Please open an issue or submit a pull request on GitH
|
|
|
306
311
|
GPTrans is released under the MIT License.
|
|
307
312
|
|
|
308
313
|
Happy translating! 🌍✨
|
|
309
|
-
|
package/demo/case_1.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import GPTrans from '../index.js';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const dbPath = new URL('../db', import.meta.url).pathname;
|
|
4
|
+
const gptrans = new GPTrans({ path: dbPath, model: 'sonnet45' });
|
|
4
5
|
|
|
5
6
|
console.log(gptrans.t('Hello, {name}!', { name: 'Anya' }));
|
|
6
7
|
|
|
@@ -14,6 +15,7 @@ console.log(gptrans.t('Card'));
|
|
|
14
15
|
|
|
15
16
|
// Case 2: Translate from Spanish Spain to Spanish Argentina
|
|
16
17
|
const es2ar = new GPTrans({
|
|
18
|
+
path: dbPath,
|
|
17
19
|
from: 'es-ES',
|
|
18
20
|
target: 'es-AR',
|
|
19
21
|
model: 'sonnet46'
|
|
@@ -25,10 +27,10 @@ console.log(es2ar.setContext().t('Tienes fuego?'));
|
|
|
25
27
|
|
|
26
28
|
// Case 3
|
|
27
29
|
const ar2es = new GPTrans({
|
|
30
|
+
path: dbPath,
|
|
28
31
|
from: 'es-AR',
|
|
29
32
|
target: 'es-ES',
|
|
30
33
|
model: 'gpt41'
|
|
31
34
|
});
|
|
32
35
|
|
|
33
36
|
console.log(ar2es.t('¿Tenés fuego?'));
|
|
34
|
-
|
package/demo/case_2.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import GPTrans from '../index.js';
|
|
2
2
|
|
|
3
|
+
const dbPath = new URL('../db', import.meta.url).pathname;
|
|
4
|
+
|
|
3
5
|
try {
|
|
4
6
|
const gptrans = new GPTrans({
|
|
7
|
+
path: dbPath,
|
|
5
8
|
target: 'it',
|
|
6
9
|
});
|
|
7
10
|
|
|
@@ -17,4 +20,4 @@ try {
|
|
|
17
20
|
console.log(gptrans.t('Card'));
|
|
18
21
|
} catch (e) {
|
|
19
22
|
console.error(e);
|
|
20
|
-
}
|
|
23
|
+
}
|
package/demo/case_3.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import GPTrans from '../index.js';
|
|
2
2
|
|
|
3
|
+
const dbPath = new URL('../db', import.meta.url).pathname;
|
|
4
|
+
|
|
3
5
|
try {
|
|
4
6
|
const gptrans = new GPTrans({
|
|
7
|
+
path: dbPath,
|
|
5
8
|
target: 'ar',
|
|
6
9
|
from: 'es',
|
|
7
10
|
});
|
|
@@ -9,4 +12,4 @@ try {
|
|
|
9
12
|
console.log(gptrans.t('Cargando...'));
|
|
10
13
|
} catch (e) {
|
|
11
14
|
console.error(e);
|
|
12
|
-
}
|
|
15
|
+
}
|
package/demo/case_4.js
CHANGED
|
@@ -5,9 +5,11 @@ import { promises as fs } from 'fs';
|
|
|
5
5
|
|
|
6
6
|
// Get current file directory
|
|
7
7
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const dbPath = new URL('../db', import.meta.url).pathname;
|
|
8
9
|
|
|
9
10
|
// Initialize translator
|
|
10
11
|
const model = new GPTrans({
|
|
12
|
+
path: dbPath,
|
|
11
13
|
model: ['sonnet46', 'gpt54'],
|
|
12
14
|
from: 'es', // Assuming the source file is in Spanish
|
|
13
15
|
target: 'en',
|
|
@@ -24,4 +26,3 @@ const content = await fs.readFile(filePath, 'utf-8');
|
|
|
24
26
|
const translatedContent = model.t(content);
|
|
25
27
|
console.log(translatedContent);
|
|
26
28
|
|
|
27
|
-
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import GPTrans from '../index.js';
|
|
2
2
|
|
|
3
|
+
const dbPath = new URL('../db', import.meta.url).pathname;
|
|
4
|
+
|
|
3
5
|
// Ejemplo: Cómo obtener la información completa del idioma
|
|
4
6
|
|
|
5
7
|
const gptrans = new GPTrans({
|
|
8
|
+
path: dbPath,
|
|
6
9
|
from: 'es',
|
|
7
10
|
target: 'en',
|
|
8
11
|
});
|
|
@@ -25,6 +28,7 @@ console.log(' Gentilicio:', gptrans.replaceFrom.FROM_DENONYM); // 'Spani
|
|
|
25
28
|
console.log('\n📌 Ejemplo con variantes regionales:');
|
|
26
29
|
|
|
27
30
|
const gptrans2 = new GPTrans({
|
|
31
|
+
path: dbPath,
|
|
28
32
|
from: 'en-GB',
|
|
29
33
|
target: 'pt-BR',
|
|
30
34
|
});
|
package/demo/case_parallelism.js
CHANGED
|
@@ -5,6 +5,7 @@ import { dirname, join } from 'path';
|
|
|
5
5
|
// Cargar .env desde la carpeta demo
|
|
6
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
7
|
const __dirname = dirname(__filename);
|
|
8
|
+
const dbPath = new URL('../db', import.meta.url).pathname;
|
|
8
9
|
try {
|
|
9
10
|
process.loadEnvFile(join(__dirname, '.env'));
|
|
10
11
|
} catch {
|
|
@@ -43,6 +44,7 @@ async function testParallelTranslations() {
|
|
|
43
44
|
texts.map(async (text, index) => {
|
|
44
45
|
// Todas las instancias comparten el MISMO NOMBRE
|
|
45
46
|
const gptrans = new GPTrans({
|
|
47
|
+
path: dbPath,
|
|
46
48
|
from: sourceLang,
|
|
47
49
|
target: targetLang,
|
|
48
50
|
model: 'sonnet45',
|
package/demo/case_references.js
CHANGED
|
@@ -5,6 +5,7 @@ import { dirname, join } from 'path';
|
|
|
5
5
|
// Load .env from demo folder
|
|
6
6
|
const __filename = fileURLToPath(import.meta.url);
|
|
7
7
|
const __dirname = dirname(__filename);
|
|
8
|
+
const dbPath = new URL('../db', import.meta.url).pathname;
|
|
8
9
|
try {
|
|
9
10
|
process.loadEnvFile(join(__dirname, '.env'));
|
|
10
11
|
} catch {
|
|
@@ -19,6 +20,7 @@ async function testReferences() {
|
|
|
19
20
|
|
|
20
21
|
// First, create translations in English and Portuguese
|
|
21
22
|
const enTranslator = new GPTrans({
|
|
23
|
+
path: dbPath,
|
|
22
24
|
from: 'es',
|
|
23
25
|
target: 'en',
|
|
24
26
|
model: 'sonnet45',
|
|
@@ -27,6 +29,7 @@ async function testReferences() {
|
|
|
27
29
|
});
|
|
28
30
|
|
|
29
31
|
const ptTranslator = new GPTrans({
|
|
32
|
+
path: dbPath,
|
|
30
33
|
from: 'es',
|
|
31
34
|
target: 'pt',
|
|
32
35
|
model: 'sonnet45',
|
|
@@ -63,6 +66,7 @@ async function testReferences() {
|
|
|
63
66
|
|
|
64
67
|
// Now translate to French using English as reference
|
|
65
68
|
const frTranslator = new GPTrans({
|
|
69
|
+
path: dbPath,
|
|
66
70
|
from: 'es',
|
|
67
71
|
target: 'fr',
|
|
68
72
|
model: 'sonnet45',
|
|
@@ -86,6 +90,7 @@ async function testReferences() {
|
|
|
86
90
|
|
|
87
91
|
// Translate from English to Italian (using English as base instead of Spanish)
|
|
88
92
|
const itTranslator = new GPTrans({
|
|
93
|
+
path: dbPath,
|
|
89
94
|
from: 'es',
|
|
90
95
|
target: 'it',
|
|
91
96
|
model: 'sonnet45',
|
|
@@ -110,6 +115,7 @@ async function testReferences() {
|
|
|
110
115
|
|
|
111
116
|
// Translate to German with multiple references
|
|
112
117
|
const deTranslator = new GPTrans({
|
|
118
|
+
path: dbPath,
|
|
113
119
|
from: 'es',
|
|
114
120
|
target: 'de',
|
|
115
121
|
model: 'sonnet45',
|
package/demo/case_refine.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import GPTrans from '../index.js';
|
|
2
2
|
|
|
3
|
+
const dbPath = new URL('../db', import.meta.url).pathname;
|
|
4
|
+
|
|
3
5
|
console.log('🚀 Testing GPTrans Refine\n');
|
|
4
6
|
console.log('='.repeat(70));
|
|
5
7
|
|
|
6
8
|
async function testRefine() {
|
|
7
9
|
// Step 1: Create initial translations
|
|
8
10
|
const gptrans = new GPTrans({
|
|
11
|
+
path: dbPath,
|
|
9
12
|
from: 'en-US',
|
|
10
13
|
target: 'es-AR',
|
|
11
14
|
model: 'sonnet45',
|
|
@@ -11,6 +11,7 @@ async function main() {
|
|
|
11
11
|
try {
|
|
12
12
|
// Initialize GPTrans with Spanish as target
|
|
13
13
|
const gptrans = new GPTrans({
|
|
14
|
+
path: path.join(__dirname, 'db'),
|
|
14
15
|
from: 'en-US',
|
|
15
16
|
target: 'es'
|
|
16
17
|
});
|
|
@@ -81,4 +82,3 @@ async function main() {
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
main();
|
|
84
|
-
|
package/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import DeepBase from 'deepbase';
|
|
2
|
+
import { JsonDriver } from 'deepbase-json';
|
|
2
3
|
import stringHash from 'string-hash';
|
|
3
4
|
import { ModelMix } from 'modelmix';
|
|
4
5
|
import { isoAssoc, isLanguageAvailable } from './isoAssoc.js';
|
|
5
6
|
import { GeminiGenerator } from 'genmix';
|
|
6
7
|
import fs from 'fs';
|
|
7
|
-
import
|
|
8
|
+
import pathModule from 'path';
|
|
8
9
|
|
|
9
10
|
class GPTrans {
|
|
10
11
|
static #mmixInstances = new Map();
|
|
@@ -62,7 +63,11 @@ class GPTrans {
|
|
|
62
63
|
return isLanguageAvailable(langCode);
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
constructor({ from = 'en-US', target = 'es', model = 'sonnet46', batchThreshold = 1500, debounceTimeout = 500, promptFile = null, name = '', context = '', instruction = '', freeze = false, debug = false } = {}) {
|
|
66
|
+
constructor({ from = 'en-US', target = 'es', model = 'sonnet46', batchThreshold = 1500, debounceTimeout = 500, promptFile = null, name = '', context = '', instruction = '', freeze = false, debug = false, path: dbPath } = {}) {
|
|
67
|
+
|
|
68
|
+
if (typeof dbPath !== 'string' || !pathModule.isAbsolute(dbPath)) {
|
|
69
|
+
throw new TypeError('GPTrans requires an absolute "path" option.');
|
|
70
|
+
}
|
|
66
71
|
|
|
67
72
|
target = this.normalizeBCP47(target);
|
|
68
73
|
from = this.normalizeBCP47(from);
|
|
@@ -73,12 +78,12 @@ class GPTrans {
|
|
|
73
78
|
/* optional .env missing or unreadable */
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
const path = new URL('../../db', import.meta.url).pathname;
|
|
77
81
|
const namePrefix = name ? '_' + name : '';
|
|
78
|
-
this.dbPath =
|
|
82
|
+
this.dbPath = dbPath;
|
|
79
83
|
this.instanceName = name;
|
|
80
|
-
this.dbTarget =
|
|
81
|
-
this.dbFrom =
|
|
84
|
+
this.dbTarget = this._createDatabase('gptrans' + namePrefix + '_' + target);
|
|
85
|
+
this.dbFrom = this._createDatabase('gptrans' + namePrefix + '_from_' + from);
|
|
86
|
+
this.pendingSourceWrites = Promise.resolve();
|
|
82
87
|
|
|
83
88
|
try {
|
|
84
89
|
this.replaceTarget = isoAssoc(target, 'TARGET_');
|
|
@@ -116,6 +121,17 @@ class GPTrans {
|
|
|
116
121
|
return iso.toLowerCase();
|
|
117
122
|
}
|
|
118
123
|
|
|
124
|
+
_createDatabase(name) {
|
|
125
|
+
return new DeepBase(new JsonDriver({ name, path: this.dbPath }));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_saveSourceText(context, contextHash, key, text) {
|
|
129
|
+
this.pendingSourceWrites = this.pendingSourceWrites.then(async () => {
|
|
130
|
+
await this.dbFrom.set(context, key, text);
|
|
131
|
+
await this.dbFrom.set('_context', contextHash, context);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
119
135
|
setContext(context = '') {
|
|
120
136
|
if (this.context !== context && this.pendingTranslations.size > 0) {
|
|
121
137
|
clearTimeout(this.debounceTimer);
|
|
@@ -143,9 +159,10 @@ class GPTrans {
|
|
|
143
159
|
// If this key was enqueued via t(), avoid duplicate work in a later batch.
|
|
144
160
|
this._dequeuePendingTranslation(key);
|
|
145
161
|
|
|
162
|
+
await this.pendingSourceWrites;
|
|
146
163
|
const translatedText = await this._translate(text, [[key, text]], {}, this.preloadBaseLanguage);
|
|
147
164
|
const immediateTranslation = translatedText.trim();
|
|
148
|
-
this.dbTarget.set(contextHash, key, immediateTranslation);
|
|
165
|
+
await this.dbTarget.set(contextHash, key, immediateTranslation);
|
|
149
166
|
|
|
150
167
|
return this._applyParams(immediateTranslation, params);
|
|
151
168
|
}
|
|
@@ -171,11 +188,10 @@ class GPTrans {
|
|
|
171
188
|
}
|
|
172
189
|
|
|
173
190
|
const contextHash = this._hash(this.context);
|
|
174
|
-
const translation = this.dbTarget.
|
|
191
|
+
const translation = this.dbTarget.getSync(contextHash, key);
|
|
175
192
|
|
|
176
|
-
if (!this.freeze && !this.dbFrom.
|
|
177
|
-
this.
|
|
178
|
-
this.dbFrom.set('_context', contextHash, this.context);
|
|
193
|
+
if (!this.freeze && !this.dbFrom.getSync(this.context, key)) {
|
|
194
|
+
this._saveSourceText(this.context, contextHash, key, text);
|
|
179
195
|
}
|
|
180
196
|
|
|
181
197
|
if (translation) {
|
|
@@ -233,16 +249,17 @@ class GPTrans {
|
|
|
233
249
|
async _processBatch(context) {
|
|
234
250
|
|
|
235
251
|
const batch = Array.from(this.pendingTranslations.entries());
|
|
252
|
+
const batchCharCount = this.pendingCharCount;
|
|
236
253
|
|
|
237
254
|
// Clear pending translations and character count before awaiting translation
|
|
238
255
|
this.pendingTranslations.clear();
|
|
256
|
+
this.pendingCharCount = 0;
|
|
257
|
+
await this.pendingSourceWrites;
|
|
239
258
|
|
|
240
|
-
this.modelConfig.options.max_tokens =
|
|
241
|
-
const minTime = Math.floor((60000 / (8000 /
|
|
259
|
+
this.modelConfig.options.max_tokens = batchCharCount + 1000;
|
|
260
|
+
const minTime = Math.floor((60000 / (8000 / batchCharCount)) * 1.4);
|
|
242
261
|
GPTrans.mmix(this.modelKey, this.modelMixOptions).limiter.updateSettings({ minTime });
|
|
243
262
|
|
|
244
|
-
this.pendingCharCount = 0;
|
|
245
|
-
|
|
246
263
|
// Load references for each text in the batch if preloadReferences is set
|
|
247
264
|
const batchReferences = {};
|
|
248
265
|
if (this.preloadReferences && this.preloadReferences.length > 0) {
|
|
@@ -280,7 +297,7 @@ class GPTrans {
|
|
|
280
297
|
const minLength = Math.min(translatedTexts.length, batch.length);
|
|
281
298
|
for (let i = 0; i < minLength; i++) {
|
|
282
299
|
if (translatedTexts[i] && translatedTexts[i].trim()) {
|
|
283
|
-
this.dbTarget.set(contextHash, batch[i][0], translatedTexts[i].trim());
|
|
300
|
+
await this.dbTarget.set(contextHash, batch[i][0], translatedTexts[i].trim());
|
|
284
301
|
}
|
|
285
302
|
}
|
|
286
303
|
return;
|
|
@@ -295,15 +312,15 @@ class GPTrans {
|
|
|
295
312
|
return;
|
|
296
313
|
}
|
|
297
314
|
|
|
298
|
-
|
|
315
|
+
for (const [index, [key]] of batch.entries()) {
|
|
299
316
|
if (!trimmed[index]) {
|
|
300
317
|
console.error(`❌ No translation found for ${key} at index ${index}`);
|
|
301
318
|
console.error(` Original text: ${batch[index][1]}`);
|
|
302
|
-
|
|
319
|
+
continue;
|
|
303
320
|
}
|
|
304
321
|
|
|
305
|
-
this.dbTarget.set(contextHash, key, trimmed[index]);
|
|
306
|
-
}
|
|
322
|
+
await this.dbTarget.set(contextHash, key, trimmed[index]);
|
|
323
|
+
}
|
|
307
324
|
|
|
308
325
|
} catch (e) {
|
|
309
326
|
console.error('❌ Error in _processBatch:', e.message);
|
|
@@ -411,12 +428,9 @@ class GPTrans {
|
|
|
411
428
|
|
|
412
429
|
for (const lang of referenceLangs) {
|
|
413
430
|
const namePrefix = this.instanceName ? '_' + this.instanceName : '';
|
|
414
|
-
const dbRef =
|
|
415
|
-
name: `gptrans${namePrefix}_${lang}`,
|
|
416
|
-
path: this.dbPath
|
|
417
|
-
});
|
|
431
|
+
const dbRef = this._createDatabase(`gptrans${namePrefix}_${lang}`);
|
|
418
432
|
|
|
419
|
-
const translation = dbRef.
|
|
433
|
+
const translation = dbRef.getSync(contextHash, key);
|
|
420
434
|
if (translation) {
|
|
421
435
|
references[lang] = translation;
|
|
422
436
|
}
|
|
@@ -427,6 +441,8 @@ class GPTrans {
|
|
|
427
441
|
|
|
428
442
|
async preload({ references = [], baseLanguage = null } = {}) {
|
|
429
443
|
|
|
444
|
+
await this.pendingSourceWrites;
|
|
445
|
+
|
|
430
446
|
if (!this.context && this.replaceFrom.FROM_ISO === this.replaceTarget.TARGET_ISO) {
|
|
431
447
|
return this;
|
|
432
448
|
}
|
|
@@ -454,7 +470,7 @@ class GPTrans {
|
|
|
454
470
|
// Track which keys need translation
|
|
455
471
|
const keysNeedingTranslation = [];
|
|
456
472
|
|
|
457
|
-
for (const [context, pairs] of this.dbFrom.
|
|
473
|
+
for (const [context, pairs] of Object.entries(this.dbFrom.getSync() ?? {})) {
|
|
458
474
|
// Skip the _context metadata
|
|
459
475
|
if (context === '_context') continue;
|
|
460
476
|
|
|
@@ -463,7 +479,7 @@ class GPTrans {
|
|
|
463
479
|
|
|
464
480
|
for (const [key, text] of Object.entries(pairs)) {
|
|
465
481
|
// Check if translation already exists
|
|
466
|
-
if (!this.dbTarget.
|
|
482
|
+
if (!this.dbTarget.getSync(contextHash, key)) {
|
|
467
483
|
keysNeedingTranslation.push({ context, contextHash, key });
|
|
468
484
|
// Only call get() if translation doesn't exist
|
|
469
485
|
this.get(key, text);
|
|
@@ -488,7 +504,7 @@ class GPTrans {
|
|
|
488
504
|
// Check if all needed translations are now complete
|
|
489
505
|
let allTranslated = true;
|
|
490
506
|
for (const { contextHash, key } of keysNeedingTranslation) {
|
|
491
|
-
if (!this.dbTarget.
|
|
507
|
+
if (!this.dbTarget.getSync(contextHash, key)) {
|
|
492
508
|
allTranslated = false;
|
|
493
509
|
break;
|
|
494
510
|
}
|
|
@@ -514,12 +530,14 @@ class GPTrans {
|
|
|
514
530
|
}
|
|
515
531
|
|
|
516
532
|
async purge() {
|
|
533
|
+
await this.pendingSourceWrites;
|
|
534
|
+
|
|
517
535
|
// Iterate through dbTarget and remove keys that don't exist in dbFrom
|
|
518
|
-
for (const [contextHash, pairs] of this.dbTarget.
|
|
536
|
+
for (const [contextHash, pairs] of Object.entries(this.dbTarget.getSync() ?? {})) {
|
|
519
537
|
for (const key of Object.keys(pairs)) {
|
|
520
538
|
|
|
521
|
-
const context = this.dbFrom.
|
|
522
|
-
if (!this.dbFrom.
|
|
539
|
+
const context = this.dbFrom.getSync('_context', contextHash);
|
|
540
|
+
if (!this.dbFrom.getSync(context, key)) {
|
|
523
541
|
console.log(contextHash, key);
|
|
524
542
|
await this.dbTarget.del(contextHash, key);
|
|
525
543
|
}
|
|
@@ -546,7 +564,7 @@ class GPTrans {
|
|
|
546
564
|
let currentBatch = [];
|
|
547
565
|
let currentCharCount = 0;
|
|
548
566
|
|
|
549
|
-
for (const [contextHash, pairs] of this.dbTarget.
|
|
567
|
+
for (const [contextHash, pairs] of Object.entries(this.dbTarget.getSync() ?? {})) {
|
|
550
568
|
for (const [key, translation] of Object.entries(pairs)) {
|
|
551
569
|
const entryCharCount = translation.length;
|
|
552
570
|
|
|
@@ -607,20 +625,20 @@ class GPTrans {
|
|
|
607
625
|
const minLength = Math.min(refinedTexts.length, entries.length);
|
|
608
626
|
for (let i = 0; i < minLength; i++) {
|
|
609
627
|
if (refinedTexts[i] && refinedTexts[i].trim()) {
|
|
610
|
-
this.dbTarget.set(entries[i].contextHash, entries[i].key, refinedTexts[i].trim());
|
|
628
|
+
await this.dbTarget.set(entries[i].contextHash, entries[i].key, refinedTexts[i].trim());
|
|
611
629
|
}
|
|
612
630
|
}
|
|
613
631
|
return;
|
|
614
632
|
}
|
|
615
633
|
|
|
616
|
-
entries.
|
|
634
|
+
for (const [index, entry] of entries.entries()) {
|
|
617
635
|
const refinedText = refinedTexts[index]?.trim();
|
|
618
636
|
if (!refinedText) {
|
|
619
637
|
console.error(`❌ No refined text for ${entry.key} at index ${index}`);
|
|
620
|
-
|
|
638
|
+
continue;
|
|
621
639
|
}
|
|
622
|
-
this.dbTarget.set(entry.contextHash, entry.key, refinedText);
|
|
623
|
-
}
|
|
640
|
+
await this.dbTarget.set(entry.contextHash, entry.key, refinedText);
|
|
641
|
+
}
|
|
624
642
|
|
|
625
643
|
} catch (e) {
|
|
626
644
|
console.error('❌ Error in _processRefineBatch:', e.message);
|
|
@@ -668,13 +686,13 @@ class GPTrans {
|
|
|
668
686
|
} = options;
|
|
669
687
|
|
|
670
688
|
// Parse image filename and extension
|
|
671
|
-
const parsedPath =
|
|
689
|
+
const parsedPath = pathModule.parse(imagePath);
|
|
672
690
|
const filename = parsedPath.base;
|
|
673
691
|
const targetLang = this.replaceTarget.TARGET_ISO || 'en';
|
|
674
692
|
|
|
675
693
|
// Check if image is already in a language folder
|
|
676
|
-
const dirName =
|
|
677
|
-
const parentDir =
|
|
694
|
+
const dirName = pathModule.basename(pathModule.dirname(imagePath));
|
|
695
|
+
const parentDir = pathModule.dirname(pathModule.dirname(imagePath));
|
|
678
696
|
|
|
679
697
|
// If the image is in a language folder (e.g., en/image.jpg)
|
|
680
698
|
// create the target at the same level (e.g., es/image.jpg)
|
|
@@ -683,12 +701,12 @@ class GPTrans {
|
|
|
683
701
|
|
|
684
702
|
if (this._isLanguageFolder(dirName)) {
|
|
685
703
|
// Image is in a language folder: create sibling folder
|
|
686
|
-
targetDir =
|
|
687
|
-
targetPath =
|
|
704
|
+
targetDir = pathModule.join(parentDir, targetLang);
|
|
705
|
+
targetPath = pathModule.join(targetDir, filename);
|
|
688
706
|
} else {
|
|
689
707
|
// Image is not in a language folder: create subfolder
|
|
690
|
-
targetDir =
|
|
691
|
-
targetPath =
|
|
708
|
+
targetDir = pathModule.join(pathModule.dirname(imagePath), targetLang);
|
|
709
|
+
targetPath = pathModule.join(targetDir, filename);
|
|
692
710
|
}
|
|
693
711
|
|
|
694
712
|
// Check if translated image already exists
|
|
@@ -736,7 +754,7 @@ class GPTrans {
|
|
|
736
754
|
}
|
|
737
755
|
|
|
738
756
|
// Save translated image - preserve original file format
|
|
739
|
-
const filename =
|
|
757
|
+
const filename = pathModule.basename(targetPath, pathModule.extname(targetPath));
|
|
740
758
|
const formatOptions = generator.getReferenceMetadata();
|
|
741
759
|
|
|
742
760
|
// Apply default quality settings for JPEG images
|
|
@@ -755,4 +773,4 @@ class GPTrans {
|
|
|
755
773
|
}
|
|
756
774
|
}
|
|
757
775
|
|
|
758
|
-
export default GPTrans;
|
|
776
|
+
export default GPTrans;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gptrans",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.2.0",
|
|
5
5
|
"description": "🚆 GPTrans - The smarter AI-powered way to translate.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"translate",
|
|
@@ -35,9 +35,10 @@
|
|
|
35
35
|
"node": ">=20.6.0"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"deepbase": "^
|
|
39
|
-
"
|
|
40
|
-
"
|
|
38
|
+
"deepbase": "^3.9.0",
|
|
39
|
+
"deepbase-json": "3.9.0",
|
|
40
|
+
"genmix": "^1.2.5",
|
|
41
|
+
"modelmix": "^4.6.5",
|
|
41
42
|
"string-hash": "^1.1.3"
|
|
42
43
|
},
|
|
43
44
|
"scripts": {
|
package/pnpm-workspace.yaml
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
allowBuilds:
|
|
2
2
|
sharp: true
|
|
3
|
-
overrides:
|
|
4
|
-
ip-address: 10.1.1
|
|
5
3
|
minimumReleaseAgeExclude:
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
- genmix@1.2.5
|
|
5
|
+
- deepbase-json@3.9.0
|
|
6
|
+
- deepbase@3.9.0
|
|
7
|
+
overrides:
|
|
8
|
+
sharp: "^0.35.0"
|
|
9
|
+
"@hono/node-server": "^2.0.5"
|
|
10
|
+
ip-address: ">=10.1.1"
|
package/skills/gptrans/SKILL.md
CHANGED
|
@@ -82,7 +82,9 @@ You can manually edit translation files to override specific entries.
|
|
|
82
82
|
```javascript
|
|
83
83
|
import GPTrans from 'gptrans';
|
|
84
84
|
|
|
85
|
+
const dbPath = new URL('./db', import.meta.url).pathname;
|
|
85
86
|
const gptrans = new GPTrans({
|
|
87
|
+
path: dbPath,
|
|
86
88
|
from: 'en-US',
|
|
87
89
|
target: 'es-AR',
|
|
88
90
|
model: 'sonnet45'
|
|
@@ -119,7 +121,7 @@ console.log(gptrans.setContext().t('Welcome back'));
|
|
|
119
121
|
### Translate text with parameter substitution
|
|
120
122
|
|
|
121
123
|
```javascript
|
|
122
|
-
const gptrans = new GPTrans({ from: 'en', target: 'es-AR' });
|
|
124
|
+
const gptrans = new GPTrans({ path: dbPath, from: 'en', target: 'es-AR' });
|
|
123
125
|
console.log(gptrans.t('Hello, {name}!', { name: 'Martin' }));
|
|
124
126
|
// First call: "Hello, Martin!" (original)
|
|
125
127
|
// After caching: "Hola, Martin!" (translated)
|
|
@@ -129,6 +131,7 @@ console.log(gptrans.t('Hello, {name}!', { name: 'Martin' }));
|
|
|
129
131
|
|
|
130
132
|
```javascript
|
|
131
133
|
const gptrans = new GPTrans({
|
|
134
|
+
path: dbPath,
|
|
132
135
|
from: 'en',
|
|
133
136
|
target: 'fr',
|
|
134
137
|
model: ['sonnet45', 'gpt41'] // falls back to gpt41 if sonnet45 fails
|
|
@@ -138,7 +141,7 @@ const gptrans = new GPTrans({
|
|
|
138
141
|
### Pre-translate all pending texts
|
|
139
142
|
|
|
140
143
|
```javascript
|
|
141
|
-
const gptrans = new GPTrans({ from: 'en', target: 'es' });
|
|
144
|
+
const gptrans = new GPTrans({ path: dbPath, from: 'en', target: 'es' });
|
|
142
145
|
|
|
143
146
|
// Register texts
|
|
144
147
|
gptrans.t('Welcome');
|
|
@@ -157,7 +160,7 @@ console.log(gptrans.t('Welcome')); // "Bienvenido"
|
|
|
157
160
|
Use existing translations in other languages as context for better accuracy:
|
|
158
161
|
|
|
159
162
|
```javascript
|
|
160
|
-
const gptrans = new GPTrans({ from: 'es', target: 'fr' });
|
|
163
|
+
const gptrans = new GPTrans({ path: dbPath, from: 'es', target: 'fr' });
|
|
161
164
|
await gptrans.preload({
|
|
162
165
|
references: ['en', 'pt'] // AI sees English and Portuguese as reference
|
|
163
166
|
});
|
|
@@ -168,7 +171,7 @@ await gptrans.preload({
|
|
|
168
171
|
Translate from an intermediate language instead of the original (useful for gender-neutral intermediaries):
|
|
169
172
|
|
|
170
173
|
```javascript
|
|
171
|
-
const gptrans = new GPTrans({ from: 'es', target: 'pt' });
|
|
174
|
+
const gptrans = new GPTrans({ path: dbPath, from: 'es', target: 'pt' });
|
|
172
175
|
await gptrans.preload({
|
|
173
176
|
baseLanguage: 'en', // translate FROM English instead of Spanish
|
|
174
177
|
references: ['es'] // show original Spanish for context
|
|
@@ -178,7 +181,7 @@ await gptrans.preload({
|
|
|
178
181
|
### Set context for gender-aware translations
|
|
179
182
|
|
|
180
183
|
```javascript
|
|
181
|
-
const gptrans = new GPTrans({ from: 'en', target: 'es-AR' });
|
|
184
|
+
const gptrans = new GPTrans({ path: dbPath, from: 'en', target: 'es-AR' });
|
|
182
185
|
|
|
183
186
|
// Context applies to next translation(s) in the batch
|
|
184
187
|
console.log(gptrans.setContext('The user is female').t('You are welcome'));
|
|
@@ -191,6 +194,7 @@ console.log(gptrans.setContext().t('Thank you'));
|
|
|
191
194
|
|
|
192
195
|
```javascript
|
|
193
196
|
const gptrans = new GPTrans({
|
|
197
|
+
path: dbPath,
|
|
194
198
|
from: 'en', target: 'es-AR',
|
|
195
199
|
instruction: 'Use a formal and professional tone'
|
|
196
200
|
});
|
|
@@ -203,7 +207,7 @@ console.log(gptrans.t('Welcome to our platform'));
|
|
|
203
207
|
Improve cached translations with specific instructions:
|
|
204
208
|
|
|
205
209
|
```javascript
|
|
206
|
-
const gptrans = new GPTrans({ from: 'en', target: 'es-AR' });
|
|
210
|
+
const gptrans = new GPTrans({ path: dbPath, from: 'en', target: 'es-AR' });
|
|
207
211
|
|
|
208
212
|
// Single instruction
|
|
209
213
|
await gptrans.refine('Use a more colloquial tone');
|
|
@@ -224,7 +228,7 @@ await gptrans.refine('More formal', { promptFile: './my-refine-prompt.md' });
|
|
|
224
228
|
Requires `GEMINI_API_KEY`. Auto-detects language folders for output path:
|
|
225
229
|
|
|
226
230
|
```javascript
|
|
227
|
-
const gptrans = new GPTrans({ from: 'en', target: 'es' });
|
|
231
|
+
const gptrans = new GPTrans({ path: dbPath, from: 'en', target: 'es' });
|
|
228
232
|
|
|
229
233
|
// en/banner.jpg → es/banner.jpg (auto sibling folder)
|
|
230
234
|
const translatedPath = await gptrans.img('en/banner.jpg');
|
|
@@ -240,7 +244,7 @@ const result = await gptrans.img('en/hero.jpg', {
|
|
|
240
244
|
### Freeze mode (prevent new translations)
|
|
241
245
|
|
|
242
246
|
```javascript
|
|
243
|
-
const gptrans = new GPTrans({ from: 'en', target: 'es', freeze: true });
|
|
247
|
+
const gptrans = new GPTrans({ path: dbPath, from: 'en', target: 'es', freeze: true });
|
|
244
248
|
|
|
245
249
|
// Returns original text, logs "[key] text" — nothing queued
|
|
246
250
|
console.log(gptrans.t('New text'));
|
|
@@ -262,6 +266,7 @@ await gptrans.purge();
|
|
|
262
266
|
```javascript
|
|
263
267
|
// Spain Spanish → Argentina Spanish
|
|
264
268
|
const es2ar = new GPTrans({
|
|
269
|
+
path: dbPath,
|
|
265
270
|
from: 'es-ES',
|
|
266
271
|
target: 'es-AR',
|
|
267
272
|
model: 'sonnet45'
|
|
@@ -301,7 +306,7 @@ if (GPTrans.isLanguageAvailable('pt-BR')) {
|
|
|
301
306
|
|
|
302
307
|
| Method | Returns | Description |
|
|
303
308
|
| --- | --- | --- |
|
|
304
|
-
| `new GPTrans(options)` | `GPTrans` | Create instance
|
|
309
|
+
| `new GPTrans(options)` | `GPTrans` | Create an instance. `options.path` must be an absolute directory path. |
|
|
305
310
|
| `.t(text, params?)` | `string` | Translate text (sync). Returns cached or original. |
|
|
306
311
|
| `.get(key, text)` | `string \| undefined` | Get translation by key, queue if missing. |
|
|
307
312
|
| `.setContext(context?)` | `this` | Set context for next batch (gender, tone, etc.). |
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import test from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
3
4
|
import GPTrans from '../index.js';
|
|
4
5
|
|
|
5
6
|
function createMemoryDb() {
|
|
6
7
|
const store = new Map();
|
|
7
8
|
return {
|
|
8
|
-
|
|
9
|
+
getSync(...keys) {
|
|
10
|
+
if (keys.length === 0) {
|
|
11
|
+
return Object.fromEntries(Array.from(store.entries()).map(([context, pairs]) => [
|
|
12
|
+
context,
|
|
13
|
+
Object.fromEntries(pairs.entries())
|
|
14
|
+
]));
|
|
15
|
+
}
|
|
16
|
+
const [context, key] = keys;
|
|
17
|
+
if (keys.length === 1) {
|
|
18
|
+
return Object.fromEntries(store.get(context)?.entries() ?? []);
|
|
19
|
+
}
|
|
9
20
|
return store.get(context)?.get(key);
|
|
10
21
|
},
|
|
11
22
|
set(context, key, value) {
|
|
@@ -35,6 +46,7 @@ function createTestInstance() {
|
|
|
35
46
|
target: 'en-US',
|
|
36
47
|
debounceTimeout: 10_000,
|
|
37
48
|
batchThreshold: 50_000,
|
|
49
|
+
path: tmpdir(),
|
|
38
50
|
name: `unit_${Date.now()}_${Math.random().toString(36).slice(2)}`
|
|
39
51
|
});
|
|
40
52
|
|
|
@@ -63,9 +75,9 @@ test('_processBatch splits correctly with \\n------\\n separators', async (t) =>
|
|
|
63
75
|
await gp._processBatch('');
|
|
64
76
|
|
|
65
77
|
const h = gp._hash('');
|
|
66
|
-
assert.equal(gp.dbTarget.
|
|
67
|
-
assert.equal(gp.dbTarget.
|
|
68
|
-
assert.equal(gp.dbTarget.
|
|
78
|
+
assert.equal(gp.dbTarget.getSync(h, 'k1'), 'Hello');
|
|
79
|
+
assert.equal(gp.dbTarget.getSync(h, 'k2'), 'Goodbye');
|
|
80
|
+
assert.equal(gp.dbTarget.getSync(h, 'k3'), 'Thank you');
|
|
69
81
|
|
|
70
82
|
GPTrans.mmix = originalMmix;
|
|
71
83
|
});
|
|
@@ -86,9 +98,9 @@ test('_processBatch falls back to split by divider without newlines', async (t)
|
|
|
86
98
|
await gp._processBatch('');
|
|
87
99
|
|
|
88
100
|
const h = gp._hash('');
|
|
89
|
-
assert.equal(gp.dbTarget.
|
|
90
|
-
assert.equal(gp.dbTarget.
|
|
91
|
-
assert.equal(gp.dbTarget.
|
|
101
|
+
assert.equal(gp.dbTarget.getSync(h, 'k1'), 'Hello');
|
|
102
|
+
assert.equal(gp.dbTarget.getSync(h, 'k2'), 'Goodbye');
|
|
103
|
+
assert.equal(gp.dbTarget.getSync(h, 'k3'), 'Thank you');
|
|
92
104
|
|
|
93
105
|
GPTrans.mmix = originalMmix;
|
|
94
106
|
});
|
|
@@ -149,10 +161,10 @@ test('_processBatch discards batch when all translations are identical (3+)', as
|
|
|
149
161
|
|
|
150
162
|
const h = gp._hash('');
|
|
151
163
|
// Nothing should be saved
|
|
152
|
-
assert.equal(gp.dbTarget.
|
|
153
|
-
assert.equal(gp.dbTarget.
|
|
154
|
-
assert.equal(gp.dbTarget.
|
|
155
|
-
assert.equal(gp.dbTarget.
|
|
164
|
+
assert.equal(gp.dbTarget.getSync(h, 'k1'), undefined);
|
|
165
|
+
assert.equal(gp.dbTarget.getSync(h, 'k2'), undefined);
|
|
166
|
+
assert.equal(gp.dbTarget.getSync(h, 'k3'), undefined);
|
|
167
|
+
assert.equal(gp.dbTarget.getSync(h, 'k4'), undefined);
|
|
156
168
|
|
|
157
169
|
assert.ok(errors.some(e => e.includes('translations are identical')));
|
|
158
170
|
|
|
@@ -174,8 +186,8 @@ test('_processBatch allows batch of 2 with identical translations (edge case)',
|
|
|
174
186
|
await gp._processBatch('');
|
|
175
187
|
|
|
176
188
|
const h = gp._hash('');
|
|
177
|
-
assert.equal(gp.dbTarget.
|
|
178
|
-
assert.equal(gp.dbTarget.
|
|
189
|
+
assert.equal(gp.dbTarget.getSync(h, 'k1'), 'Yes');
|
|
190
|
+
assert.equal(gp.dbTarget.getSync(h, 'k2'), 'Yes');
|
|
179
191
|
|
|
180
192
|
GPTrans.mmix = originalMmix;
|
|
181
193
|
});
|
|
@@ -196,9 +208,9 @@ test('_processBatch saves partial duplicates normally', async (t) => {
|
|
|
196
208
|
await gp._processBatch('');
|
|
197
209
|
|
|
198
210
|
const h = gp._hash('');
|
|
199
|
-
assert.equal(gp.dbTarget.
|
|
200
|
-
assert.equal(gp.dbTarget.
|
|
201
|
-
assert.equal(gp.dbTarget.
|
|
211
|
+
assert.equal(gp.dbTarget.getSync(h, 'k1'), 'Hello');
|
|
212
|
+
assert.equal(gp.dbTarget.getSync(h, 'k2'), 'Hello');
|
|
213
|
+
assert.equal(gp.dbTarget.getSync(h, 'k3'), 'Goodbye');
|
|
202
214
|
|
|
203
215
|
GPTrans.mmix = originalMmix;
|
|
204
216
|
});
|
|
@@ -227,9 +239,9 @@ test('_processBatch saves partial results on count mismatch', async (t) => {
|
|
|
227
239
|
console.error = origError;
|
|
228
240
|
|
|
229
241
|
const h = gp._hash('');
|
|
230
|
-
assert.equal(gp.dbTarget.
|
|
231
|
-
assert.equal(gp.dbTarget.
|
|
232
|
-
assert.equal(gp.dbTarget.
|
|
242
|
+
assert.equal(gp.dbTarget.getSync(h, 'k1'), 'Hello');
|
|
243
|
+
assert.equal(gp.dbTarget.getSync(h, 'k2'), 'Goodbye');
|
|
244
|
+
assert.equal(gp.dbTarget.getSync(h, 'k3'), undefined); // Not saved
|
|
233
245
|
|
|
234
246
|
assert.ok(errors.some(e => e.includes('Translation count mismatch')));
|
|
235
247
|
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import test from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
3
6
|
import GPTrans from '../index.js';
|
|
4
7
|
|
|
5
8
|
function createMemoryDb() {
|
|
6
9
|
const store = new Map();
|
|
7
10
|
return {
|
|
8
|
-
|
|
11
|
+
getSync(...keys) {
|
|
12
|
+
if (keys.length === 0) {
|
|
13
|
+
return Object.fromEntries(Array.from(store.entries()).map(([context, pairs]) => [
|
|
14
|
+
context,
|
|
15
|
+
Object.fromEntries(pairs.entries())
|
|
16
|
+
]));
|
|
17
|
+
}
|
|
18
|
+
const [context, key] = keys;
|
|
19
|
+
if (keys.length === 1) {
|
|
20
|
+
return Object.fromEntries(store.get(context)?.entries() ?? []);
|
|
21
|
+
}
|
|
9
22
|
return store.get(context)?.get(key);
|
|
10
23
|
},
|
|
11
24
|
set(context, key, value) {
|
|
@@ -39,6 +52,7 @@ function createTestInstance() {
|
|
|
39
52
|
target: 'es',
|
|
40
53
|
debounceTimeout: 10_000,
|
|
41
54
|
batchThreshold: 5000,
|
|
55
|
+
path: tmpdir(),
|
|
42
56
|
name: `unit_${Date.now()}_${Math.random().toString(36).slice(2)}`
|
|
43
57
|
});
|
|
44
58
|
|
|
@@ -131,7 +145,7 @@ test('preload translates missing keys from dbFrom into dbTarget', async () => {
|
|
|
131
145
|
await gp.preload();
|
|
132
146
|
|
|
133
147
|
assert.equal(translateCalls, 1);
|
|
134
|
-
assert.equal(gp.dbTarget.
|
|
148
|
+
assert.equal(gp.dbTarget.getSync(contextHash, key), 'Comprar ahora');
|
|
135
149
|
assert.deepEqual(gp.preloadReferences, []);
|
|
136
150
|
assert.equal(gp.preloadBaseLanguage, null);
|
|
137
151
|
} finally {
|
|
@@ -141,3 +155,51 @@ test('preload translates missing keys from dbFrom into dbTarget', async () => {
|
|
|
141
155
|
}
|
|
142
156
|
}
|
|
143
157
|
});
|
|
158
|
+
|
|
159
|
+
test('constructor requires an absolute database path', () => {
|
|
160
|
+
assert.throws(() => new GPTrans(), /requires an absolute "path" option/);
|
|
161
|
+
assert.throws(() => new GPTrans({ path: './db' }), /requires an absolute "path" option/);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('DeepBase 3.9 cache is available to synchronous translations', async () => {
|
|
165
|
+
const directory = await mkdtemp(path.join(tmpdir(), 'gptrans-deepbase-'));
|
|
166
|
+
const originalMmix = GPTrans.mmix;
|
|
167
|
+
const gp = new GPTrans({
|
|
168
|
+
from: 'en',
|
|
169
|
+
target: 'es',
|
|
170
|
+
freeze: true,
|
|
171
|
+
debounceTimeout: 1,
|
|
172
|
+
path: directory,
|
|
173
|
+
name: 'sync-cache'
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const text = 'Hello';
|
|
178
|
+
const key = gp._textToKey(text);
|
|
179
|
+
const contextHash = gp._hash('');
|
|
180
|
+
await gp.dbTarget.set(contextHash, key, 'Hola');
|
|
181
|
+
|
|
182
|
+
assert.equal(gp.t(text), 'Hola');
|
|
183
|
+
assert.equal(gp.dbTarget.getSync(contextHash, key), 'Hola');
|
|
184
|
+
|
|
185
|
+
gp.setFreeze(false);
|
|
186
|
+
const sourceText = 'New source text';
|
|
187
|
+
const sourceKey = gp._textToKey(sourceText);
|
|
188
|
+
gp._translate = async () => 'Nuevo texto fuente';
|
|
189
|
+
GPTrans.mmix = () => ({ limiter: { updateSettings() {} } });
|
|
190
|
+
|
|
191
|
+
gp.t(sourceText);
|
|
192
|
+
await gp.preload();
|
|
193
|
+
|
|
194
|
+
assert.equal(gp.dbFrom.getSync('', sourceKey), sourceText);
|
|
195
|
+
assert.equal(gp.dbTarget.getSync(contextHash, sourceKey), 'Nuevo texto fuente');
|
|
196
|
+
} finally {
|
|
197
|
+
GPTrans.mmix = originalMmix;
|
|
198
|
+
if (gp.debounceTimer) {
|
|
199
|
+
clearTimeout(gp.debounceTimer);
|
|
200
|
+
}
|
|
201
|
+
await gp.dbTarget.dispose({ clearMemory: true, releaseInstance: true });
|
|
202
|
+
await gp.dbFrom.dispose({ clearMemory: true, releaseInstance: true });
|
|
203
|
+
await rm(directory, { recursive: true });
|
|
204
|
+
}
|
|
205
|
+
});
|
|
@@ -12,6 +12,7 @@ async function testImageTranslation() {
|
|
|
12
12
|
|
|
13
13
|
// Initialize GPTrans with Portuguese as target language
|
|
14
14
|
const gptrans = new GPTrans({
|
|
15
|
+
path: path.join(__dirname, 'db'),
|
|
15
16
|
from: 'en-US',
|
|
16
17
|
target: 'pt-BR',
|
|
17
18
|
model: 'sonnet45'
|
|
@@ -67,4 +68,3 @@ async function testImageTranslation() {
|
|
|
67
68
|
|
|
68
69
|
// Run the test
|
|
69
70
|
testImageTranslation();
|
|
70
|
-
|