gptrans 1.9.8 → 2.0.2

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.
@@ -0,0 +1,303 @@
1
+ ---
2
+ name: modelmix
3
+ description: Instructions for using the ModelMix Node.js library to interact with multiple AI LLM providers through a unified interface. Use when integrating AI models (OpenAI, Anthropic, Google, Groq, Perplexity, Grok, etc.), chaining models with fallback, getting structured JSON from LLMs, adding MCP tools, streaming responses, or managing multi-provider AI workflows in Node.js.
4
+ ---
5
+
6
+ # ModelMix Library Skill
7
+
8
+ ## Overview
9
+
10
+ ModelMix is a Node.js library that provides a unified fluent API to interact with multiple AI LLM providers. It handles automatic fallback between models, round-robin load balancing, structured JSON output, streaming, MCP tool integration, rate limiting, and token tracking.
11
+
12
+ Use this skill when:
13
+ - Integrating one or more AI models into a Node.js project
14
+ - Chaining models with automatic fallback
15
+ - Extracting structured JSON from LLMs
16
+ - Adding MCP tools or custom tools to models
17
+ - Working with templates and file-based prompts
18
+
19
+ Do NOT use this skill for:
20
+ - Python or non-Node.js projects
21
+ - Direct HTTP calls to LLM APIs (use ModelMix instead)
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ npm install modelmix
27
+ ```
28
+
29
+ ## Core Concepts
30
+
31
+ ### Import
32
+
33
+ ```javascript
34
+ import { ModelMix } from 'modelmix';
35
+ ```
36
+
37
+ ### Creating an Instance
38
+
39
+ ```javascript
40
+ // Static factory (preferred)
41
+ const model = ModelMix.new();
42
+
43
+ // With global options
44
+ const model = ModelMix.new({
45
+ options: { max_tokens: 4096, temperature: 0.7 },
46
+ config: {
47
+ system: "You are a helpful assistant.",
48
+ max_history: 5,
49
+ debug: 0, // 0=silent, 1=minimal, 2=summary, 3=full
50
+ roundRobin: false // false=fallback, true=rotate models
51
+ }
52
+ });
53
+ ```
54
+
55
+ ### Attaching Models (Fluent Chain)
56
+
57
+ Chain shorthand methods to attach providers. First model is primary; others are fallbacks:
58
+
59
+ ```javascript
60
+ const model = ModelMix.new()
61
+ .sonnet45() // primary
62
+ .gpt5mini() // fallback 1
63
+ .gemini3flash() // fallback 2
64
+ .addText("Hello!")
65
+ ```
66
+
67
+ If `sonnet45` fails, it automatically tries `gpt5mini`, then `gemini3flash`.
68
+
69
+ ## Available Model Shorthands
70
+
71
+ - **OpenAI**: `gpt52` `gpt51` `gpt5` `gpt5mini` `gpt5nano` `gpt41` `gpt41mini` `gpt41nano`
72
+ - **Anthropic**: `opus46` `opus45` `sonnet45` `sonnet4` `haiku45` `haiku35` (thinking variants: add `think` suffix)
73
+ - **Google**: `gemini3pro` `gemini3flash` `gemini25pro` `gemini25flash`
74
+ - **Grok**: `grok4` `grok41` (thinking variant available)
75
+ - **Perplexity**: `sonar` `sonarPro`
76
+ - **Groq**: `scout` `maverick`
77
+ - **Together**: `qwen3` `kimiK2`
78
+ - **Multi-provider**: `deepseekR1` `gptOss`
79
+ - **MiniMax**: `minimaxM21`
80
+ - **Fireworks**: `deepseekV32` `GLM47`
81
+
82
+ Each method is called as `mix.methodName()` and accepts optional `{ options, config }` to override per-model settings.
83
+
84
+ ## Common Tasks
85
+
86
+ ### Get a text response
87
+
88
+ ```javascript
89
+ const answer = await ModelMix.new()
90
+ .gpt5mini()
91
+ .addText("What is the capital of France?")
92
+ .message();
93
+ ```
94
+
95
+ ### Get structured JSON
96
+
97
+ ```javascript
98
+ const result = await ModelMix.new()
99
+ .gpt5mini()
100
+ .addText("Name and capital of 3 South American countries.")
101
+ .json(
102
+ { countries: [{ name: "", capital: "" }] }, // schema example
103
+ { countries: [{ name: "country name", capital: "in uppercase" }] }, // descriptions
104
+ { addNote: true } // options
105
+ );
106
+ // result.countries → [{ name: "Brazil", capital: "BRASILIA" }, ...]
107
+ ```
108
+
109
+ `json()` signature: `json(schemaExample, schemaDescription?, { addSchema, addExample, addNote }?)`
110
+
111
+ ### Stream a response
112
+
113
+ ```javascript
114
+ await ModelMix.new()
115
+ .gpt5mini()
116
+ .addText("Tell me a story.")
117
+ .stream(({ delta, message }) => {
118
+ process.stdout.write(delta);
119
+ });
120
+ ```
121
+
122
+ ### Get raw response (tokens, thinking, tool calls)
123
+
124
+ ```javascript
125
+ const raw = await ModelMix.new()
126
+ .sonnet45think()
127
+ .addText("Solve this step by step: 2+2*3")
128
+ .raw();
129
+ // raw.message, raw.think, raw.tokens, raw.toolCalls, raw.response
130
+ ```
131
+
132
+ ### Access full response after `message()` or `json()` with `lastRaw`
133
+
134
+ After calling `message()`, `json()`, `block()`, or `stream()`, use `lastRaw` to access the complete response (tokens, thinking, tool calls, etc.). It has the same structure as `raw()`.
135
+
136
+ ```javascript
137
+ const model = ModelMix.new().gpt5mini().addText("Hello!");
138
+ const text = await model.message();
139
+ console.log(model.lastRaw.tokens);
140
+ // { input: 122, output: 86, total: 541, cost: 0.000319 }
141
+ console.log(model.lastRaw.think); // reasoning content (if available)
142
+ console.log(model.lastRaw.response); // raw API response
143
+ ```
144
+
145
+ ### Add images
146
+
147
+ ```javascript
148
+ const model = ModelMix.new().sonnet45();
149
+ model.addImage('./photo.jpg'); // from file
150
+ model.addImageFromUrl('https://example.com/img.png'); // from URL
151
+ model.addText('Describe this image.');
152
+ const description = await model.message();
153
+ ```
154
+
155
+ ### Use templates with placeholders
156
+
157
+ ```javascript
158
+ const model = ModelMix.new().gpt5mini();
159
+ model.setSystemFromFile('./prompts/system.md');
160
+ model.addTextFromFile('./prompts/task.md');
161
+ model.replace({
162
+ '{role}': 'data analyst',
163
+ '{language}': 'Spanish'
164
+ });
165
+ model.replaceKeyFromFile('{code}', './src/utils.js');
166
+ console.log(await model.message());
167
+ ```
168
+
169
+ ### Round-robin load balancing
170
+
171
+ ```javascript
172
+ const pool = ModelMix.new({ config: { roundRobin: true } })
173
+ .gpt5mini()
174
+ .sonnet45()
175
+ .gemini3flash();
176
+
177
+ // Each call rotates to the next model
178
+ const r1 = await pool.new().addText("Request 1").message();
179
+ const r2 = await pool.new().addText("Request 2").message();
180
+ ```
181
+
182
+ ### MCP integration (external tools)
183
+
184
+ ```javascript
185
+ const model = ModelMix.new({ config: { max_history: 10 } }).gpt5nano();
186
+ model.setSystem('You are an assistant. Today is ' + new Date().toISOString());
187
+ await model.addMCP('@modelcontextprotocol/server-brave-search');
188
+ model.addText('Use Internet: What is the latest news about AI?');
189
+ console.log(await model.message());
190
+ ```
191
+
192
+ Requires `BRAVE_API_KEY` in `.env` for Brave Search MCP.
193
+
194
+ ### Custom local tools (addTool)
195
+
196
+ ```javascript
197
+ const model = ModelMix.new({ config: { max_history: 10 } }).gpt5mini();
198
+
199
+ model.addTool({
200
+ name: "get_weather",
201
+ description: "Get weather for a city",
202
+ inputSchema: {
203
+ type: "object",
204
+ properties: { city: { type: "string" } },
205
+ required: ["city"]
206
+ }
207
+ }, async ({ city }) => {
208
+ return `The weather in ${city} is sunny, 25C`;
209
+ });
210
+
211
+ model.addText("What's the weather in Tokyo?");
212
+ console.log(await model.message());
213
+ ```
214
+
215
+ ### Rate limiting (Bottleneck)
216
+
217
+ ```javascript
218
+ const model = ModelMix.new({
219
+ config: {
220
+ bottleneck: {
221
+ maxConcurrent: 4,
222
+ minTime: 1000
223
+ }
224
+ }
225
+ }).gpt5mini();
226
+ ```
227
+
228
+ ### Debug mode
229
+
230
+ ```javascript
231
+ const model = ModelMix.new({
232
+ config: { debug: 2 } // 0=silent, 1=minimal, 2=summary, 3=full
233
+ }).gpt5mini();
234
+ ```
235
+
236
+ For full debug output, also set the env: `DEBUG=ModelMix* node script.js`
237
+
238
+ ### Use free-tier models
239
+
240
+ ```javascript
241
+ // These use providers with free quotas (OpenRouter, Groq, Cerebras)
242
+ const model = ModelMix.new()
243
+ .gptOss()
244
+ .kimiK2()
245
+ .deepseekR1()
246
+ .hermes3()
247
+ .addText("What is the capital of France?");
248
+ console.log(await model.message());
249
+ ```
250
+
251
+ ### Conversation history
252
+
253
+ ```javascript
254
+ const chat = ModelMix.new({ config: { max_history: 10 } }).gpt5mini();
255
+ chat.addText("My name is Martin.");
256
+ await chat.message();
257
+ chat.addText("What's my name?");
258
+ const reply = await chat.message(); // "Martin"
259
+ ```
260
+
261
+ ## Agent Usage Rules
262
+
263
+ - Always check `package.json` for `modelmix` before running `npm install`.
264
+ - Use `ModelMix.new()` static factory to create instances (not `new ModelMix()`).
265
+ - Store API keys in `.env` and load with `dotenv/config` or `process.loadEnvFile()`. Never hardcode keys.
266
+ - Chain models for resilience: primary model first, fallbacks after.
267
+ - When using MCP tools or `addTool()`, set `max_history` to at least 3.
268
+ - Use `.json()` for structured output instead of parsing text manually.
269
+ - Use `.message()` for simple text, `.raw()` when you need tokens/thinking/toolCalls.
270
+ - For thinking models, append `think` to the method name (e.g. `sonnet45think()`).
271
+ - Template placeholders use `{key}` syntax in both system prompts and user messages.
272
+ - The library uses CommonJS internally (`require`) but supports ESM import via `{ ModelMix }`.
273
+ - Available provider Mix classes for custom setups: `MixOpenAI`, `MixAnthropic`, `MixGoogle`, `MixPerplexity`, `MixGroq`, `MixTogether`, `MixGrok`, `MixOpenRouter`, `MixOllama`, `MixLMStudio`, `MixCustom`, `MixCerebras`, `MixFireworks`, `MixMiniMax`.
274
+
275
+ ## API Quick Reference
276
+
277
+ | Method | Returns | Description |
278
+ | --- | --- | --- |
279
+ | `.addText(text)` | `this` | Add user message |
280
+ | `.addTextFromFile(path)` | `this` | Add user message from file |
281
+ | `.setSystem(text)` | `this` | Set system prompt |
282
+ | `.setSystemFromFile(path)` | `this` | Set system prompt from file |
283
+ | `.addImage(path)` | `this` | Add image from file |
284
+ | `.addImageFromUrl(url)` | `this` | Add image from URL or data URI |
285
+ | `.replace({})` | `this` | Set placeholder replacements |
286
+ | `.replaceKeyFromFile(key, path)` | `this` | Replace placeholder with file content |
287
+ | `.message()` | `Promise<string>` | Get text response |
288
+ | `.json(example, desc?, opts?)` | `Promise<object>` | Get structured JSON |
289
+ | `.raw()` | `Promise<{message, think, toolCalls, tokens, response}>` | Full response |
290
+ | `.lastRaw` | `object \| null` | Full response from last `message()`/`json()`/`block()`/`stream()` call |
291
+ | `.stream(callback)` | `Promise` | Stream response |
292
+ | `.block()` | `Promise<string>` | Extract code block from response |
293
+ | `.addMCP(package)` | `Promise` | Add MCP server tools |
294
+ | `.addTool(def, callback)` | `this` | Register custom local tool |
295
+ | `.addTools([{tool, callback}])` | `this` | Register multiple tools |
296
+ | `.removeTool(name)` | `this` | Remove a tool |
297
+ | `.listTools()` | `{local, mcp}` | List registered tools |
298
+ | `.new()` | `ModelMix` | Clone instance sharing models |
299
+ | `.attach(key, provider)` | `this` | Attach custom provider |
300
+
301
+ ## References
302
+
303
+ - [GitHub Repository](https://github.com/clasen/ModelMix)
package/README.md CHANGED
@@ -21,7 +21,10 @@ Whether you're building a multilingual website, a mobile app, or a localization
21
21
  ```bash
22
22
  npm install gptrans
23
23
  ```
24
-
24
+ > **AI Skill**: You can also add GPTrans as a skill for AI agentic development:
25
+ > ```bash
26
+ > npx skills add https://github.com/clasen/GPTrans --skill gptrans
27
+ > ```
25
28
  ### 🌐 Environment Setup
26
29
 
27
30
  GPTrans uses dotenv for environment configuration. Create a `.env` file in your project root and add your API keys:
@@ -214,6 +217,26 @@ When using multiple models:
214
217
  - If the primary model fails (due to API errors, rate limits, etc.), GPTrans automatically falls back to the next model
215
218
  - This ensures higher availability and resilience of your translation service
216
219
 
220
+ ## ✏️ Refining Translations
221
+
222
+ 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.
223
+
224
+ ```javascript
225
+ const gptrans = new GPTrans({ from: 'en', target: 'es-AR' });
226
+
227
+ // After translations already exist...
228
+ // Refine with a single instruction
229
+ await gptrans.refine('Use a more natural and colloquial tone');
230
+
231
+ // Or pass multiple instructions at once (single API pass, saves tokens)
232
+ await gptrans.refine([
233
+ 'Shorten texts where possible without losing meaning',
234
+ 'Use a more colloquial tone'
235
+ ]);
236
+ ```
237
+
238
+ > **Note:** The refine method uses the **target translation** as input (not the original source text). If the AI determines a translation is already good, it keeps it unchanged. Passing an array of instructions is preferred over multiple `refine()` calls since it processes everything in a single API pass.
239
+
217
240
  # 🖼️ Image Translation
218
241
 
219
242
  Intelligent image translation using Google's Gemini AI.
@@ -0,0 +1,61 @@
1
+ import GPTrans from '../index.js';
2
+
3
+ console.log('🚀 Testing GPTrans Refine\n');
4
+ console.log('='.repeat(70));
5
+
6
+ async function testRefine() {
7
+ // Step 1: Create initial translations
8
+ const gptrans = new GPTrans({
9
+ from: 'en-US',
10
+ target: 'es-AR',
11
+ model: 'sonnet45',
12
+ name: 'refine_test'
13
+ });
14
+
15
+ const texts = [
16
+ 'You have exceeded the maximum number of attempts',
17
+ 'Your session has expired, please log in again',
18
+ 'The operation was completed successfully',
19
+ 'An unexpected error occurred, please try again later',
20
+ 'Are you sure you want to delete this item?'
21
+ ];
22
+
23
+ console.log('\n📝 Step 1: Creating initial translations...\n');
24
+ texts.forEach(text => {
25
+ console.log(` EN: ${text}`);
26
+ console.log(` ES: ${gptrans.t(text)}\n`);
27
+ });
28
+
29
+ await gptrans.preload();
30
+
31
+ console.log('='.repeat(70));
32
+ console.log('\n📝 Step 2: Translations before refine:\n');
33
+ texts.forEach(text => {
34
+ console.log(` EN: ${text}`);
35
+ console.log(` ES: ${gptrans.t(text)}\n`);
36
+ });
37
+
38
+ // Step 2: Refine with multiple instructions in a single pass
39
+ console.log('='.repeat(70));
40
+ console.log('\n🔄 Step 3: Refining with multiple instructions (single API pass)...\n');
41
+
42
+ await gptrans.refine([
43
+ 'Use "vos" instead of "tú" for all second-person references',
44
+ 'Use a more friendly and colloquial tone, less robotic',
45
+ 'Shorten messages where possible without losing clarity'
46
+ ]);
47
+
48
+ console.log('📝 Translations after refine:\n');
49
+ texts.forEach(text => {
50
+ console.log(` EN: ${text}`);
51
+ console.log(` ES: ${gptrans.t(text)}\n`);
52
+ });
53
+
54
+ console.log('='.repeat(70));
55
+ console.log('\n✅ Refine test completed!\n');
56
+ }
57
+
58
+ testRefine().catch(error => {
59
+ console.error('\n❌ Error:', error.message);
60
+ console.error(error.stack);
61
+ });
package/index.js CHANGED
@@ -11,11 +11,11 @@ class GPTrans {
11
11
  static #mmixInstances = new Map();
12
12
  static #translationLocks = new Map();
13
13
 
14
- static mmix(models = 'sonnet45', { debug = false } = {}) {
14
+ static mmix(models = 'sonnet45', { debug = 0 } = {}) {
15
15
  const key = Array.isArray(models) ? models.join(',') : models;
16
16
 
17
17
  if (!this.#mmixInstances.has(key)) {
18
- const mmix = new ModelMix({
18
+ let instance = ModelMix.new({
19
19
  config: {
20
20
  max_history: 1,
21
21
  debug,
@@ -25,9 +25,6 @@ class GPTrans {
25
25
  }
26
26
  }
27
27
  });
28
-
29
- // Chain models dynamically
30
- let instance = mmix;
31
28
  const modelArray = Array.isArray(models) ? models : [models];
32
29
 
33
30
  for (const model of modelArray) {
@@ -218,15 +215,15 @@ class GPTrans {
218
215
  const textsToTranslate = batch.map(([_, text]) => text).join(`\n${this.divider}\n`);
219
216
  try {
220
217
  const translations = await this._translate(textsToTranslate, batch, batchReferences, this.preloadBaseLanguage);
221
-
218
+
222
219
  // Try different split strategies to be more robust
223
220
  let translatedTexts = translations.split(`\n${this.divider}\n`);
224
-
221
+
225
222
  // If split doesn't match batch size, try alternative separators
226
223
  if (translatedTexts.length !== batch.length) {
227
224
  // Try without newlines around divider
228
225
  translatedTexts = translations.split(this.divider);
229
-
226
+
230
227
  // If still doesn't match, try with just newline
231
228
  if (translatedTexts.length !== batch.length) {
232
229
  translatedTexts = translations.split(/\n{2,}/); // Split by multiple newlines
@@ -234,7 +231,7 @@ class GPTrans {
234
231
  }
235
232
 
236
233
  const contextHash = this._hash(context);
237
-
234
+
238
235
  // Validate we have the right number of translations
239
236
  if (translatedTexts.length !== batch.length) {
240
237
  console.error(`❌ Translation count mismatch:`);
@@ -242,7 +239,7 @@ class GPTrans {
242
239
  console.error(` Received: ${translatedTexts.length} translations`);
243
240
  console.error(` Batch keys: ${batch.map(([key]) => key).join(', ')}`);
244
241
  console.error(`\n📝 Full response from model:\n${translations}\n`);
245
-
242
+
246
243
  // Try to save what we can
247
244
  const minLength = Math.min(translatedTexts.length, batch.length);
248
245
  for (let i = 0; i < minLength; i++) {
@@ -276,18 +273,14 @@ class GPTrans {
276
273
  try {
277
274
  const model = GPTrans.mmix(this.modelKey, this.modelMixOptions);
278
275
 
279
- model.setSystem("You are an expert translator specialized in literary translation between FROM_LANG and TARGET_DENONYM TARGET_LANG.");
276
+ model.setSystem("You are an expert translator specialized in literary translation between {FROM_LANG} and {TARGET_DENONYM} {TARGET_LANG}.");
280
277
 
281
- // Read and process prompt file
282
- let promptContent = fs.readFileSync(this.promptFile, 'utf-8');
283
-
284
- // Format references if available
278
+ // Build references section (includes header when references exist, empty otherwise)
285
279
  let referencesText = '';
286
280
  if (Object.keys(batchReferences).length > 0 && batch.length > 0) {
287
- // Group all references by language first
288
281
  const refsByLang = {};
289
-
290
- batch.forEach(([key], index) => {
282
+
283
+ batch.forEach(([key]) => {
291
284
  if (batchReferences[key]) {
292
285
  Object.entries(batchReferences[key]).forEach(([lang, translation]) => {
293
286
  if (!refsByLang[lang]) {
@@ -297,8 +290,7 @@ class GPTrans {
297
290
  });
298
291
  }
299
292
  });
300
-
301
- // Format: one language header, then all its translations with bullets
293
+
302
294
  const refBlocks = Object.entries(refsByLang).map(([lang, translations]) => {
303
295
  try {
304
296
  const langInfo = isoAssoc(lang);
@@ -311,17 +303,8 @@ class GPTrans {
311
303
  return `${header}\n${content}`;
312
304
  }
313
305
  });
314
-
315
- referencesText = refBlocks.join(`\n\n`);
316
- }
317
306
 
318
- // Remove reference section if no references
319
- if (!referencesText) {
320
- // Remove the entire "Reference Translations" section
321
- promptContent = promptContent.replace(
322
- /## Reference Translations \(for context\)[\s\S]*?(?=\n#|$)/,
323
- ''
324
- );
307
+ referencesText = `## Reference Translations (for context)\nThese are existing translations in other languages that may help you provide a more accurate translation. Use them as reference but do not simply copy them:\n\n${refBlocks.join('\n\n')}`;
325
308
  }
326
309
 
327
310
  // Determine which FROM_ values to use
@@ -334,30 +317,23 @@ class GPTrans {
334
317
  }
335
318
  }
336
319
 
337
- // Apply replacements to prompt
338
- promptContent = promptContent
339
- .replace(/INPUT/g, text)
340
- .replace(/CONTEXT/g, this.context)
341
- .replace(/REFERENCES/g, referencesText);
342
-
343
- // Apply language-specific replacements
344
- Object.entries(this.replaceTarget).forEach(([key, value]) => {
345
- promptContent = promptContent.replace(new RegExp(key, 'g'), value);
320
+ // Use ModelMix templates: addTextFromFile + replace + block
321
+ model.addTextFromFile(this.promptFile);
322
+ model.replace({
323
+ '{INPUT}': text,
324
+ '{CONTEXT}': this.context,
325
+ '{REFERENCES}': referencesText,
326
+ '{TARGET_ISO}': this.replaceTarget.TARGET_ISO,
327
+ '{TARGET_LANG}': this.replaceTarget.TARGET_LANG,
328
+ '{TARGET_COUNTRY}': this.replaceTarget.TARGET_COUNTRY,
329
+ '{TARGET_DENONYM}': this.replaceTarget.TARGET_DENONYM,
330
+ '{FROM_ISO}': fromReplace.FROM_ISO,
331
+ '{FROM_LANG}': fromReplace.FROM_LANG,
332
+ '{FROM_COUNTRY}': fromReplace.FROM_COUNTRY,
333
+ '{FROM_DENONYM}': fromReplace.FROM_DENONYM,
346
334
  });
347
- Object.entries(fromReplace).forEach(([key, value]) => {
348
- promptContent = promptContent.replace(new RegExp(key, 'g'), value);
349
- });
350
-
351
- model.addText(promptContent);
352
-
353
- const response = await model.message();
354
335
 
355
- // Extract content from code block if present
356
- const codeBlockRegex = /```(?:\w*\n)?([\s\S]*?)```/;
357
- const match = response.match(codeBlockRegex);
358
- const translatedText = match ? match[1].trim() : response.trim();
359
-
360
- return translatedText;
336
+ return await model.block({ addSystemExtra: false });
361
337
 
362
338
  } finally {
363
339
  // Always release the lock
@@ -431,14 +407,14 @@ class GPTrans {
431
407
 
432
408
  // Track which keys need translation
433
409
  const keysNeedingTranslation = [];
434
-
410
+
435
411
  for (const [context, pairs] of this.dbFrom.entries()) {
436
412
  // Skip the _context metadata
437
413
  if (context === '_context') continue;
438
-
414
+
439
415
  this.setContext(context);
440
416
  const contextHash = this._hash(context);
441
-
417
+
442
418
  for (const [key, text] of Object.entries(pairs)) {
443
419
  // Check if translation already exists
444
420
  if (!this.dbTarget.get(contextHash, key)) {
@@ -462,7 +438,7 @@ class GPTrans {
462
438
  const checkInterval = setInterval(() => {
463
439
  // Check if there are still pending translations or batch being processed
464
440
  const hasPending = this.pendingTranslations.size > 0 || this.isProcessingBatch;
465
-
441
+
466
442
  // Check if all needed translations are now complete
467
443
  let allTranslated = true;
468
444
  for (const { contextHash, key } of keysNeedingTranslation) {
@@ -471,7 +447,7 @@ class GPTrans {
471
447
  break;
472
448
  }
473
449
  }
474
-
450
+
475
451
  if (allTranslated && !hasPending) {
476
452
  clearInterval(checkInterval);
477
453
  resolve();
@@ -507,6 +483,140 @@ class GPTrans {
507
483
  return this;
508
484
  }
509
485
 
486
+ async refine(instruction, { promptFile = null } = {}) {
487
+ // Accept string or array of instructions
488
+ const instructions = Array.isArray(instruction) ? instruction : [instruction];
489
+ const merged = instructions.filter(i => i && i.trim()).join('\n- ');
490
+
491
+ if (!merged) {
492
+ throw new Error('Refinement instruction is required');
493
+ }
494
+
495
+ const finalInstruction = instructions.length > 1 ? `- ${merged}` : merged;
496
+ const refinePromptFile = promptFile ?? new URL('./prompt/refine.md', import.meta.url).pathname;
497
+
498
+ // Collect all existing translations grouped by contextHash
499
+ const allBatches = [];
500
+ let currentBatch = [];
501
+ let currentCharCount = 0;
502
+
503
+ for (const [contextHash, pairs] of this.dbTarget.entries()) {
504
+ for (const [key, translation] of Object.entries(pairs)) {
505
+ const entryCharCount = translation.length;
506
+
507
+ // If adding this entry exceeds threshold, flush current batch
508
+ if (currentBatch.length > 0 && currentCharCount + entryCharCount >= this.batchThreshold) {
509
+ allBatches.push({ entries: currentBatch, contextHash });
510
+ currentBatch = [];
511
+ currentCharCount = 0;
512
+ }
513
+
514
+ currentBatch.push({ key, translation, contextHash });
515
+ currentCharCount += entryCharCount;
516
+ }
517
+ }
518
+
519
+ // Push remaining entries
520
+ if (currentBatch.length > 0) {
521
+ allBatches.push({ entries: currentBatch, contextHash: currentBatch[0].contextHash });
522
+ }
523
+
524
+ if (allBatches.length === 0) {
525
+ return this;
526
+ }
527
+
528
+ // Process each batch sequentially (respecting rate limits via locks)
529
+ for (const batch of allBatches) {
530
+ await this._processRefineBatch(batch.entries, finalInstruction, refinePromptFile);
531
+ }
532
+
533
+ return this;
534
+ }
535
+
536
+ async _processRefineBatch(entries, instruction, refinePromptFile) {
537
+ const charCount = entries.reduce((sum, e) => sum + e.translation.length, 0);
538
+ this.modelConfig.options.max_tokens = charCount + 1000;
539
+ const minTime = Math.floor((60000 / (8000 / charCount)) * 1.4);
540
+ GPTrans.mmix(this.modelKey, this.modelMixOptions).limiter.updateSettings({ minTime });
541
+
542
+ const textsToRefine = entries.map(e => e.translation).join(`\n${this.divider}\n`);
543
+
544
+ try {
545
+ const refined = await this._refineTranslate(textsToRefine, instruction, refinePromptFile);
546
+
547
+ // Split with same strategy as _processBatch
548
+ let refinedTexts = refined.split(`\n${this.divider}\n`);
549
+
550
+ if (refinedTexts.length !== entries.length) {
551
+ refinedTexts = refined.split(this.divider);
552
+
553
+ if (refinedTexts.length !== entries.length) {
554
+ refinedTexts = refined.split(/\n{2,}/);
555
+ }
556
+ }
557
+
558
+ if (refinedTexts.length !== entries.length) {
559
+ console.error(`❌ Refine count mismatch:`);
560
+ console.error(` Expected: ${entries.length} translations`);
561
+ console.error(` Received: ${refinedTexts.length} translations`);
562
+ console.error(`\n📝 Full response from model:\n${refined}\n`);
563
+
564
+ // Save what we can
565
+ const minLength = Math.min(refinedTexts.length, entries.length);
566
+ for (let i = 0; i < minLength; i++) {
567
+ if (refinedTexts[i] && refinedTexts[i].trim()) {
568
+ this.dbTarget.set(entries[i].contextHash, entries[i].key, refinedTexts[i].trim());
569
+ }
570
+ }
571
+ return;
572
+ }
573
+
574
+ entries.forEach((entry, index) => {
575
+ const refinedText = refinedTexts[index]?.trim();
576
+ if (!refinedText) {
577
+ console.error(`❌ No refined text for ${entry.key} at index ${index}`);
578
+ return;
579
+ }
580
+ this.dbTarget.set(entry.contextHash, entry.key, refinedText);
581
+ });
582
+
583
+ } catch (e) {
584
+ console.error('❌ Error in _processRefineBatch:', e.message);
585
+ console.error(e.stack);
586
+ }
587
+ }
588
+
589
+ async _refineTranslate(text, instruction, refinePromptFile) {
590
+ const releaseLock = await GPTrans.#acquireTranslationLock(this.modelKey);
591
+
592
+ try {
593
+ const model = GPTrans.mmix(this.modelKey, this.modelMixOptions);
594
+
595
+ model.setSystem("You are an expert translator and editor specialized in refining {TARGET_DENONYM} {TARGET_LANG} translations.");
596
+
597
+ // Use ModelMix templates: addTextFromFile + replace + block
598
+ model.addTextFromFile(refinePromptFile);
599
+ model.replace({
600
+ '{INPUT}': text,
601
+ '{INSTRUCTION}': instruction,
602
+ '{CONTEXT}': this.context,
603
+ '{TARGET_ISO}': this.replaceTarget.TARGET_ISO,
604
+ '{TARGET_LANG}': this.replaceTarget.TARGET_LANG,
605
+ '{TARGET_COUNTRY}': this.replaceTarget.TARGET_COUNTRY,
606
+ '{TARGET_DENONYM}': this.replaceTarget.TARGET_DENONYM,
607
+ '{FROM_ISO}': this.replaceFrom.FROM_ISO,
608
+ '{FROM_LANG}': this.replaceFrom.FROM_LANG,
609
+ '{FROM_COUNTRY}': this.replaceFrom.FROM_COUNTRY,
610
+ '{FROM_DENONYM}': this.replaceFrom.FROM_DENONYM,
611
+ });
612
+
613
+ return await model.block({ addSystemExtra: false });
614
+
615
+ } finally {
616
+ releaseLock();
617
+ }
618
+ }
619
+
510
620
  async img(imagePath, options = {}) {
511
621
  const {
512
622
  quality = '1K',
@@ -586,7 +696,7 @@ class GPTrans {
586
696
  // Save translated image - preserve original file format
587
697
  const filename = path.basename(targetPath, path.extname(targetPath));
588
698
  const formatOptions = generator.getReferenceMetadata();
589
-
699
+
590
700
  // Apply default quality settings for JPEG images
591
701
  if (formatOptions && (formatOptions.format === 'jpeg' || formatOptions.format === 'jpg')) {
592
702
  // Apply custom quality if specified, otherwise use defaults
@@ -595,7 +705,7 @@ class GPTrans {
595
705
  formatOptions.chromaSubsampling = '4:4:4'; // Better color quality
596
706
  formatOptions.optimiseCoding = true; // Lossless size reduction
597
707
  }
598
-
708
+
599
709
  await generator.save({ directory: targetDir, filename, formatOptions });
600
710
  } else {
601
711
  throw new Error('No translated image was generated');
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gptrans",
3
3
  "type": "module",
4
- "version": "1.9.8",
4
+ "version": "2.0.2",
5
5
  "description": "🚆 GPTrans - The smarter AI-powered way to translate.",
6
6
  "keywords": [
7
7
  "translate",
@@ -15,6 +15,9 @@
15
15
  "translation",
16
16
  "translator",
17
17
  "magic",
18
+ "agentic",
19
+ "skill",
20
+ "image",
18
21
  "clasen"
19
22
  ],
20
23
  "repository": {
@@ -37,7 +40,7 @@
37
40
  "dotenv": "^16.4.7",
38
41
  "form-data": "^4.0.4",
39
42
  "genmix": "^1.0.4",
40
- "modelmix": "^4.2.8",
43
+ "modelmix": "^4.3.6",
41
44
  "string-hash": "^1.1.3"
42
45
  }
43
46
  }
@@ -0,0 +1,26 @@
1
+ # Goal
2
+ Refine existing translations in {TARGET_ISO} ({TARGET_DENONYM} {TARGET_LANG}) based on the following instruction.
3
+
4
+ ## Refinement Instruction
5
+ {INSTRUCTION}
6
+
7
+ ## Current Translations to Evaluate
8
+ ```
9
+ {INPUT}
10
+ ```
11
+
12
+ # Return Format
13
+ - Provide the refined translations within a code block using ```.
14
+ - Return each translation separated by `------` (same divider as the input).
15
+ - If a translation is already good and does not need improvement, return it exactly as-is.
16
+ - Do not add, remove, or reorder translations. The output must have the exact same number of entries as the input.
17
+
18
+ # Warnings
19
+ - **Preserve meaning:** The refined translation must preserve the original meaning. Only improve style, naturalness, or clarity as instructed.
20
+ - **Minimal changes:** Only modify translations that genuinely benefit from the refinement instruction. If the current translation is already good, keep it unchanged.
21
+ - **Proper names:** Do not translate proper names (people, places, brands, etc.) unless they have an officially recognized translation in the target language.
22
+ - **Variables:** Do not translate content between curly braces. These are system variables and must remain exactly the same.
23
+ - **Consistency:** Maintain consistent terminology across all translations in the batch.
24
+
25
+ # Context
26
+ {CONTEXT}
@@ -1,28 +1,25 @@
1
1
  # Goal
2
- Translation from FROM_ISO to TARGET_ISO (TARGET_DENONYM TARGET_LANG) with cultural adaptations.
2
+ Translation from {FROM_ISO} to {TARGET_ISO} ({TARGET_DENONYM} {TARGET_LANG}) with cultural adaptations.
3
3
 
4
4
  ## Text to translate
5
5
  ```
6
- INPUT
6
+ {INPUT}
7
7
  ```
8
8
 
9
- ## Reference Translations (for context)
10
- These are existing translations in other languages that may help you provide a more accurate translation. Use them as reference but do not simply copy them:
11
-
12
- REFERENCES
9
+ {REFERENCES}
13
10
 
14
11
  # Return Format
15
12
  - Provide the final translation within a code block using ```.
16
13
  - Do not include alternative translations, only provide the best translation.
17
14
 
18
15
  # Warnings
19
- - **Context:** I will provide you with a text in FROM_DENONYM FROM_LANG. The goal is to translate it to TARGET_ISO (TARGET_DENONYM TARGET_LANG) while maintaining the essence, style, intention, and tone of the original.
16
+ - **Context:** I will provide you with a text in {FROM_DENONYM} {FROM_LANG}. The goal is to translate it to {TARGET_ISO} ({TARGET_DENONYM} {TARGET_LANG}) while maintaining the essence, style, intention, and tone of the original.
20
17
  - **Proper names:** Do not translate proper names (people, places, brands, etc.) unless they have an officially recognized translation in the target language.
21
- - **Cultural references:** Adapt or explain references that are not familiar in TARGET_DENONYM culture, whenever necessary.
18
+ - **Cultural references:** Adapt or explain references that are not familiar in {TARGET_DENONYM} culture, whenever necessary.
22
19
  - **Wordplay and humor:** When it's impossible to directly translate wordplay, find a resource that recreates the playful effect.
23
20
  - **Idioms:** Do not introduce new idioms or expressions that are not present in the original text.
24
- - **Variables:** Do not translate content between curly braces {variable}. These are system variables and must remain exactly the same.
21
+ - **Variables:** Do not translate content between curly braces. These are system variables and must remain exactly the same.
25
22
 
26
23
 
27
24
  # Context
28
- CONTEXT
25
+ {CONTEXT}
@@ -0,0 +1,306 @@
1
+ ---
2
+ name: gptrans
3
+ description: Instructions for using the GPTrans Node.js library for AI-powered translations with smart batching, caching, context-aware translations, refinement, and image translation. Use when adding i18n/localization to a Node.js project, translating text or images between languages, or managing multilingual content.
4
+ version: 1.0.0
5
+ tags: [nodejs, translation, i18n, localization, ai, gpt, multilingual]
6
+ ---
7
+
8
+ # GPTrans Library Skill
9
+
10
+ ## Table of Contents
11
+
12
+ - [Overview](#overview)
13
+ - [Installation](#installation)
14
+ - [Core Concepts](#core-concepts)
15
+ - [Basic Usage](#basic-usage)
16
+ - [Common Tasks](#common-tasks)
17
+ - [Agent Usage Rules](#agent-usage-rules)
18
+ - [API Quick Reference](#api-quick-reference)
19
+ - [References](#references)
20
+
21
+ ## Overview
22
+
23
+ GPTrans is a Node.js library that provides AI-powered translations with smart batching, caching, and context awareness. It uses LLMs (via ModelMix) to produce high-quality, culturally-adapted translations while minimizing API calls through intelligent debouncing and batch processing.
24
+
25
+ Use this skill when:
26
+ - Adding internationalization (i18n) or localization to a Node.js application
27
+ - Translating text content between languages using AI
28
+ - Translating images between languages (via Gemini)
29
+ - Managing multilingual content with caching and manual correction support
30
+ - Building tools that need context-aware translations (gender, tone, domain)
31
+ - Pre-translating or bulk-translating content with `preload()`
32
+ - Refining existing translations with specific style instructions
33
+
34
+ Do NOT use this skill for:
35
+ - Python or non-Node.js projects
36
+ - Simple key-value i18n (use `i18next` or similar instead)
37
+ - Projects that don't need AI-quality translations
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ npm install gptrans
43
+ ```
44
+
45
+ ### Environment Setup
46
+
47
+ GPTrans requires API keys for the underlying LLM providers. Create a `.env` file:
48
+
49
+ ```env
50
+ OPENAI_API_KEY=your_openai_api_key
51
+ ANTHROPIC_API_KEY=your_anthropic_api_key
52
+ GEMINI_API_KEY=your_gemini_api_key # only for image translation
53
+ ```
54
+
55
+ ### Dependencies
56
+
57
+ GPTrans uses [ModelMix](https://github.com/clasen/ModelMix) internally for LLM calls. It is installed automatically as a dependency.
58
+
59
+ ## Core Concepts
60
+
61
+ ### How It Works
62
+
63
+ 1. **First call to `t()`**: Returns the original text immediately. The translation is queued in the background.
64
+ 2. **Batching**: Queued translations are grouped by character count (`batchThreshold`) and debounce timer (`debounceTimeout`) to optimize API usage and provide better context to the LLM.
65
+ 3. **Caching**: Completed translations are stored in `db/gptrans_<locale>.json`. Subsequent calls return instantly from cache.
66
+ 4. **Second call to `t()`**: Returns the cached translation.
67
+
68
+ ### Language Tags
69
+
70
+ GPTrans uses BCP 47 language tags: `en-US`, `es-AR`, `pt-BR`, `fr`, `de`, etc. Region can be omitted for universal variants (e.g., `es` for generic Spanish).
71
+
72
+ ### Translation Cache Files
73
+
74
+ Translations are stored as JSON in `db/` directory:
75
+ - `db/gptrans_<target>.json` — cached translations for target locale
76
+ - `db/gptrans_from_<source>.json` — source text registry
77
+
78
+ You can manually edit translation files to override specific entries.
79
+
80
+ ## Basic Usage
81
+
82
+ ```javascript
83
+ import GPTrans from 'gptrans';
84
+
85
+ const gptrans = new GPTrans({
86
+ from: 'en-US',
87
+ target: 'es-AR',
88
+ model: 'sonnet45'
89
+ });
90
+
91
+ // Translate text — returns original on first call, cached translation after
92
+ console.log(gptrans.t('Hello, {name}!', { name: 'John' }));
93
+
94
+ // Context-aware translation (e.g., gender)
95
+ console.log(gptrans.setContext('Message is for a woman').t('You are very good'));
96
+
97
+ // Context auto-clears — pass empty to reset
98
+ console.log(gptrans.setContext().t('Welcome back'));
99
+ ```
100
+
101
+ ### Constructor Options
102
+
103
+ | Option | Type | Default | Description |
104
+ | --- | --- | --- | --- |
105
+ | `from` | `string` | `'en-US'` | Source language (BCP 47) |
106
+ | `target` | `string` | `'es'` | Target language (BCP 47) |
107
+ | `model` | `string \| string[]` | `'sonnet45'` | Model key or array for fallback chain |
108
+ | `batchThreshold` | `number` | `1500` | Max characters before triggering batch |
109
+ | `debounceTimeout` | `number` | `500` | Milliseconds to wait before processing |
110
+ | `freeze` | `boolean` | `false` | Prevent new translations from being queued |
111
+ | `name` | `string` | `''` | Instance name (isolates cache files) |
112
+ | `context` | `string` | `''` | Default context for translations |
113
+ | `promptFile` | `string` | `null` | Custom prompt file path (overrides built-in) |
114
+ | `debug` | `boolean` | `false` | Enable debug output |
115
+
116
+ ## Common Tasks
117
+
118
+ ### Translate text with parameter substitution
119
+
120
+ ```javascript
121
+ const gptrans = new GPTrans({ from: 'en', target: 'es-AR' });
122
+ console.log(gptrans.t('Hello, {name}!', { name: 'Martin' }));
123
+ // First call: "Hello, Martin!" (original)
124
+ // After caching: "Hola, Martin!" (translated)
125
+ ```
126
+
127
+ ### Use model fallback for resilience
128
+
129
+ ```javascript
130
+ const gptrans = new GPTrans({
131
+ from: 'en',
132
+ target: 'fr',
133
+ model: ['sonnet45', 'gpt41'] // falls back to gpt41 if sonnet45 fails
134
+ });
135
+ ```
136
+
137
+ ### Pre-translate all pending texts
138
+
139
+ ```javascript
140
+ const gptrans = new GPTrans({ from: 'en', target: 'es' });
141
+
142
+ // Register texts
143
+ gptrans.t('Welcome');
144
+ gptrans.t('Sign in');
145
+ gptrans.t('Sign out');
146
+
147
+ // Wait for all translations to complete
148
+ await gptrans.preload();
149
+
150
+ // Now all calls return cached translations
151
+ console.log(gptrans.t('Welcome')); // "Bienvenido"
152
+ ```
153
+
154
+ ### Pre-translate with reference languages
155
+
156
+ Use existing translations in other languages as context for better accuracy:
157
+
158
+ ```javascript
159
+ const gptrans = new GPTrans({ from: 'es', target: 'fr' });
160
+ await gptrans.preload({
161
+ references: ['en', 'pt'] // AI sees English and Portuguese as reference
162
+ });
163
+ ```
164
+
165
+ ### Pre-translate using an alternate base language
166
+
167
+ Translate from an intermediate language instead of the original (useful for gender-neutral intermediaries):
168
+
169
+ ```javascript
170
+ const gptrans = new GPTrans({ from: 'es', target: 'pt' });
171
+ await gptrans.preload({
172
+ baseLanguage: 'en', // translate FROM English instead of Spanish
173
+ references: ['es'] // show original Spanish for context
174
+ });
175
+ ```
176
+
177
+ ### Set context for gender-aware translations
178
+
179
+ ```javascript
180
+ const gptrans = new GPTrans({ from: 'en', target: 'es-AR' });
181
+
182
+ // Context applies to next translation(s) in the batch
183
+ console.log(gptrans.setContext('The user is female').t('You are welcome'));
184
+
185
+ // Reset context
186
+ console.log(gptrans.setContext().t('Thank you'));
187
+ ```
188
+
189
+ ### Refine existing translations
190
+
191
+ Improve cached translations with specific instructions:
192
+
193
+ ```javascript
194
+ const gptrans = new GPTrans({ from: 'en', target: 'es-AR' });
195
+
196
+ // Single instruction
197
+ await gptrans.refine('Use a more colloquial tone');
198
+
199
+ // Multiple instructions (single API pass — preferred)
200
+ await gptrans.refine([
201
+ 'Use "vos" instead of "tu"',
202
+ 'Shorten texts where possible without losing meaning',
203
+ 'Use a more friendly and natural tone'
204
+ ]);
205
+
206
+ // Custom refine prompt
207
+ await gptrans.refine('More formal', { promptFile: './my-refine-prompt.md' });
208
+ ```
209
+
210
+ ### Translate an image
211
+
212
+ Requires `GEMINI_API_KEY`. Auto-detects language folders for output path:
213
+
214
+ ```javascript
215
+ const gptrans = new GPTrans({ from: 'en', target: 'es' });
216
+
217
+ // en/banner.jpg → es/banner.jpg (auto sibling folder)
218
+ const translatedPath = await gptrans.img('en/banner.jpg');
219
+
220
+ // With options
221
+ const result = await gptrans.img('en/hero.jpg', {
222
+ quality: '1K',
223
+ jpegQuality: 92,
224
+ prompt: 'Translate all visible text to Spanish. Keep layout and style.'
225
+ });
226
+ ```
227
+
228
+ ### Freeze mode (prevent new translations)
229
+
230
+ ```javascript
231
+ const gptrans = new GPTrans({ from: 'en', target: 'es', freeze: true });
232
+
233
+ // Returns original text, logs "[key] text" — nothing queued
234
+ console.log(gptrans.t('New text'));
235
+
236
+ // Toggle at runtime
237
+ gptrans.setFreeze(false);
238
+ ```
239
+
240
+ ### Purge orphaned translations
241
+
242
+ Remove cached translations whose source text no longer exists:
243
+
244
+ ```javascript
245
+ await gptrans.purge();
246
+ ```
247
+
248
+ ### Translate between regional variants
249
+
250
+ ```javascript
251
+ // Spain Spanish → Argentina Spanish
252
+ const es2ar = new GPTrans({
253
+ from: 'es-ES',
254
+ target: 'es-AR',
255
+ model: 'sonnet45'
256
+ });
257
+
258
+ console.log(es2ar.t('Eres muy bueno'));
259
+ ```
260
+
261
+ ### Check language availability
262
+
263
+ ```javascript
264
+ import GPTrans from 'gptrans';
265
+
266
+ if (GPTrans.isLanguageAvailable('pt-BR')) {
267
+ // Language is supported
268
+ }
269
+ ```
270
+
271
+ ## Agent Usage Rules
272
+
273
+ - Always check `package.json` for `gptrans` before running `npm install`.
274
+ - Store API keys (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`) in `.env`. Never hardcode keys.
275
+ - Use BCP 47 language tags for `from` and `target` (e.g., `en-US`, `es-AR`, `pt-BR`, or just `es`).
276
+ - The `t()` method is synchronous and non-blocking. It returns the original text on first call and the cached translation on subsequent calls. Do NOT `await` it.
277
+ - To ensure translations are complete before using them, call `await gptrans.preload()` after registering texts with `t()`.
278
+ - Use `setContext()` for gender-aware or domain-specific translations. Context is captured per-batch and auto-resets when changed.
279
+ - Prefer passing an array of instructions to `refine()` over multiple calls — it processes everything in a single API pass.
280
+ - Use model arrays (`model: ['sonnet45', 'gpt41']`) for production resilience with automatic fallback.
281
+ - Translation caches live in `db/gptrans_<locale>.json`. These files can be manually edited to override specific translations.
282
+ - The `name` constructor option isolates cache files (`db/gptrans_<name>_<locale>.json`), useful for multiple independent translation contexts in the same project.
283
+ - When translating images, ensure `GEMINI_API_KEY` is set. The `img()` method auto-creates sibling language folders.
284
+ - Do NOT use `freeze: true` during initial translation — it prevents all new translations from being queued.
285
+ - Variables in curly braces (`{name}`, `{count}`) in source text are preserved through translation. Parameter substitution happens at `t()` call time.
286
+
287
+ ## API Quick Reference
288
+
289
+ | Method | Returns | Description |
290
+ | --- | --- | --- |
291
+ | `new GPTrans(options)` | `GPTrans` | Create instance with `from`, `target`, `model`, etc. |
292
+ | `.t(text, params?)` | `string` | Translate text (sync). Returns cached or original. |
293
+ | `.get(key, text)` | `string \| undefined` | Get translation by key, queue if missing. |
294
+ | `.setContext(context?)` | `this` | Set context for next batch (gender, tone, etc.). |
295
+ | `.setFreeze(freeze?)` | `this` | Enable/disable freeze mode at runtime. |
296
+ | `await .preload(options?)` | `this` | Pre-translate all pending texts. Options: `{ references, baseLanguage }`. |
297
+ | `await .refine(instruction, options?)` | `this` | Refine existing translations. Accepts string or array. |
298
+ | `await .img(path, options?)` | `string` | Translate image text. Returns output path. |
299
+ | `await .purge()` | `this` | Remove orphaned translations from cache. |
300
+ | `GPTrans.isLanguageAvailable(code)` | `boolean` | Check if a language code is supported. |
301
+
302
+ ## References
303
+
304
+ - [GitHub Repository](https://github.com/clasen/GPTrans)
305
+ - [npm Package](https://www.npmjs.com/package/gptrans)
306
+ - [ModelMix (underlying LLM library)](https://github.com/clasen/ModelMix)