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 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 in `db/gptrans_<tag>.json`. Subsequent requests for the same text will be served instantly from the cache.
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 gptrans = new GPTrans({ model: 'sonnet45' });
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
  });
@@ -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',
@@ -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',
@@ -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 path from 'path';
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 = path;
82
+ this.dbPath = dbPath;
79
83
  this.instanceName = name;
80
- this.dbTarget = new DeepBase({ name: 'gptrans' + namePrefix + '_' + target, path });
81
- this.dbFrom = new DeepBase({ name: 'gptrans' + namePrefix + '_from_' + from, path });
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.get(contextHash, key);
191
+ const translation = this.dbTarget.getSync(contextHash, key);
175
192
 
176
- if (!this.freeze && !this.dbFrom.get(this.context, key)) {
177
- this.dbFrom.set(this.context, key, text);
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 = this.pendingCharCount + 1000;
241
- const minTime = Math.floor((60000 / (8000 / this.pendingCharCount)) * 1.4);
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
- batch.forEach(([key], index) => {
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
- return;
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 = new DeepBase({
415
- name: `gptrans${namePrefix}_${lang}`,
416
- path: this.dbPath
417
- });
431
+ const dbRef = this._createDatabase(`gptrans${namePrefix}_${lang}`);
418
432
 
419
- const translation = dbRef.get(contextHash, key);
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.entries()) {
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.get(contextHash, key)) {
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.get(contextHash, key)) {
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.entries()) {
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.get('_context', contextHash);
522
- if (!this.dbFrom.get(context, key)) {
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.entries()) {
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.forEach((entry, index) => {
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
- return;
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 = path.parse(imagePath);
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 = path.basename(path.dirname(imagePath));
677
- const parentDir = path.dirname(path.dirname(imagePath));
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 = path.join(parentDir, targetLang);
687
- targetPath = path.join(targetDir, filename);
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 = path.join(path.dirname(imagePath), targetLang);
691
- targetPath = path.join(targetDir, filename);
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 = path.basename(targetPath, path.extname(targetPath));
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.1.8",
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": "^1.5.6",
39
- "genmix": "^1.2.4",
40
- "modelmix": "^4.5.6",
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": {
@@ -1,10 +1,10 @@
1
1
  allowBuilds:
2
2
  sharp: true
3
- overrides:
4
- ip-address: 10.1.1
5
3
  minimumReleaseAgeExclude:
6
- - fast-uri@3.1.1 || 3.1.2
7
- - hono@4.12.16 || 4.12.18 || 4.12.21 || 4.12.25
8
- - ip-address@10.1.1
9
- - ws@8.20.1 || 8.21.0
10
- - qs@6.15.2
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"
@@ -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 with `from`, `target`, `model`, etc. |
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
- get(context, key) {
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.get(h, 'k1'), 'Hello');
67
- assert.equal(gp.dbTarget.get(h, 'k2'), 'Goodbye');
68
- assert.equal(gp.dbTarget.get(h, 'k3'), 'Thank you');
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.get(h, 'k1'), 'Hello');
90
- assert.equal(gp.dbTarget.get(h, 'k2'), 'Goodbye');
91
- assert.equal(gp.dbTarget.get(h, 'k3'), 'Thank you');
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.get(h, 'k1'), undefined);
153
- assert.equal(gp.dbTarget.get(h, 'k2'), undefined);
154
- assert.equal(gp.dbTarget.get(h, 'k3'), undefined);
155
- assert.equal(gp.dbTarget.get(h, 'k4'), undefined);
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.get(h, 'k1'), 'Yes');
178
- assert.equal(gp.dbTarget.get(h, 'k2'), 'Yes');
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.get(h, 'k1'), 'Hello');
200
- assert.equal(gp.dbTarget.get(h, 'k2'), 'Hello');
201
- assert.equal(gp.dbTarget.get(h, 'k3'), 'Goodbye');
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.get(h, 'k1'), 'Hello');
231
- assert.equal(gp.dbTarget.get(h, 'k2'), 'Goodbye');
232
- assert.equal(gp.dbTarget.get(h, 'k3'), undefined); // Not saved
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
- get(context, key) {
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.get(contextHash, key), 'Comprar ahora');
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
-