glotto 3.0.0 → 3.1.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.
Files changed (67) hide show
  1. package/README.md +112 -47
  2. package/esm/cli.js +136 -32
  3. package/esm/deno.d.ts +5 -0
  4. package/esm/deno.js +18 -6
  5. package/esm/src/config.d.ts +4 -0
  6. package/esm/src/config.d.ts.map +1 -0
  7. package/esm/src/config.js +95 -0
  8. package/esm/src/contants.d.ts +4 -0
  9. package/esm/src/contants.d.ts.map +1 -1
  10. package/esm/src/contants.js +14 -5
  11. package/esm/src/diff.d.ts +4 -0
  12. package/esm/src/diff.d.ts.map +1 -0
  13. package/esm/src/diff.js +53 -0
  14. package/esm/src/file.d.ts +4 -3
  15. package/esm/src/file.d.ts.map +1 -1
  16. package/esm/src/file.js +23 -5
  17. package/esm/src/providers/anthropic.d.ts +2 -2
  18. package/esm/src/providers/anthropic.d.ts.map +1 -1
  19. package/esm/src/providers/anthropic.js +9 -1
  20. package/esm/src/providers/gemini.d.ts +2 -2
  21. package/esm/src/providers/gemini.d.ts.map +1 -1
  22. package/esm/src/providers/gemini.js +10 -2
  23. package/esm/src/providers/openai.d.ts +2 -2
  24. package/esm/src/providers/openai.d.ts.map +1 -1
  25. package/esm/src/providers/openai.js +9 -1
  26. package/esm/src/translator.d.ts +14 -7
  27. package/esm/src/translator.d.ts.map +1 -1
  28. package/esm/src/translator.js +161 -77
  29. package/esm/src/types.d.ts +31 -3
  30. package/esm/src/types.d.ts.map +1 -1
  31. package/esm/src/utilites.d.ts +3 -4
  32. package/esm/src/utilites.d.ts.map +1 -1
  33. package/esm/src/utilites.js +40 -16
  34. package/package.json +20 -6
  35. package/schema/glotto.schema.json +87 -0
  36. package/script/cli.js +134 -30
  37. package/script/deno.d.ts +5 -0
  38. package/script/deno.js +18 -6
  39. package/script/src/config.d.ts +4 -0
  40. package/script/src/config.d.ts.map +1 -0
  41. package/script/src/config.js +132 -0
  42. package/script/src/contants.d.ts +4 -0
  43. package/script/src/contants.d.ts.map +1 -1
  44. package/script/src/contants.js +15 -6
  45. package/script/src/diff.d.ts +4 -0
  46. package/script/src/diff.d.ts.map +1 -0
  47. package/script/src/diff.js +57 -0
  48. package/script/src/file.d.ts +4 -3
  49. package/script/src/file.d.ts.map +1 -1
  50. package/script/src/file.js +28 -10
  51. package/script/src/providers/anthropic.d.ts +2 -2
  52. package/script/src/providers/anthropic.d.ts.map +1 -1
  53. package/script/src/providers/anthropic.js +9 -1
  54. package/script/src/providers/gemini.d.ts +2 -2
  55. package/script/src/providers/gemini.d.ts.map +1 -1
  56. package/script/src/providers/gemini.js +10 -2
  57. package/script/src/providers/openai.d.ts +2 -2
  58. package/script/src/providers/openai.d.ts.map +1 -1
  59. package/script/src/providers/openai.js +9 -1
  60. package/script/src/translator.d.ts +14 -7
  61. package/script/src/translator.d.ts.map +1 -1
  62. package/script/src/translator.js +168 -83
  63. package/script/src/types.d.ts +31 -3
  64. package/script/src/types.d.ts.map +1 -1
  65. package/script/src/utilites.d.ts +3 -4
  66. package/script/src/utilites.d.ts.map +1 -1
  67. package/script/src/utilites.js +43 -20
package/README.md CHANGED
@@ -4,30 +4,6 @@ Glotto translates i18n JSON files (i18next, react-intl, vue-i18n, etc.) using AI
4
4
  extracts every string leaf with its path, sends them to the model as plain-text tagged batches, and reconstructs the JSON from the responses. The original
5
5
  structure, keys, arrays, variables and HTML tags are preserved by Glotto itself — the model only sees and produces text.
6
6
 
7
- ## Why this approach
8
-
9
- - **Provider-agnostic**: works with Gemini, OpenAI/OpenAI-compatible (Ollama, vLLM, TogetherAI, etc.) and Anthropic. Every provider implements the same
10
- `TextTranslator` contract: text in, text out.
11
- - **No JSON deadlock**: the model is never asked to emit valid JSON, so grammar-constrained sampling, broken responses and infinite loops on local models are
12
- eliminated.
13
- - **Structure-safe**: keys, nesting, arrays and non-string leaves (numbers, booleans, null, empty objects/arrays) are preserved by reconstruction, not by the
14
- model.
15
- - **Token efficient**: source size per request is bounded; failed entries are retried individually instead of re-sending the whole batch.
16
- - **Variable preservation**: the prompt instructs the model to keep `{{name}}`, `__VAR__`, `$t(...)`, `%s`, `%d`, HTML tags and markdown intact.
17
-
18
- ## How it works
19
-
20
- 1. **Extract** — recursively walk the JSON and collect every leaf as `{ id, path, value, translatable }`. Empty objects/arrays are kept as non-translatable
21
- leaves so the structure is rebuildable.
22
- 2. **Batch** — group translatable leaves into batches by source byte size (`--max-batch-size`, default 12 KB).
23
- 3. **Encode** — wrap each entry as `≪id≫value≪/id≫` and send a single prompt per batch.
24
- 4. **Translate** — call `provider.translate(prompt) → string`. No JSON mode is requested.
25
- 5. **Decode** — parse the response with the same tag regex, mapping `id → translation`.
26
- 6. **Reconstruct** — walk all leaves, place translated values (or original values for non-translatable leaves) back into a fresh object/array tree following the
27
- recorded path, then write the resulting JSON.
28
-
29
- If a batch response is missing some IDs, only the missing entries are retried (with exponential backoff) — the successful ones are kept.
30
-
31
7
  ## Installation
32
8
 
33
9
  ### [JSR (Deno)](https://jsr.io/@ibodev/glotto)
@@ -54,25 +30,43 @@ Or via npx:
54
30
  npx glotto
55
31
  ```
56
32
 
33
+ ### [pnpm](https://pnpm.io/)
34
+
35
+ ```bash
36
+ pnpm add --global glotto
37
+ ```
38
+
39
+ Or via pnpx:
40
+
41
+ ```bash
42
+ pnpx glotto
43
+ ```
44
+
57
45
  ## Usage
58
46
 
59
- ### Basic (default Gemini)
47
+ ### Basic (default OpenAI)
48
+
49
+ ```bash
50
+ glotto --key <openai-api-key> -i en.json -o tr.json -f English -t Turkish
51
+ ```
52
+
53
+ ### Gemini
60
54
 
61
55
  ```bash
62
- glotto --key <gemini-api-key> -i en.json -o ar.json -f English -t Arabic
56
+ glotto --key <gemini-api-key> -i en.json -o ar.json -f English -t Arabic -p gemini
63
57
  ```
64
58
 
65
- ### OpenAI / OpenAI-compatible (Ollama, vLLM, etc.)
59
+ ### OpenAI / OpenAI-compatible endpoints
66
60
 
67
61
  ```bash
68
62
  glotto --key <openai-api-key> -i en.json -o tr.json -f English -t Turkish -p openai
69
63
  ```
70
64
 
71
- Custom base URL (e.g. local Ollama):
65
+ Custom base URL (e.g. self-hosted OpenAI-compatible server):
72
66
 
73
67
  ```bash
74
- glotto --key any-string -i en.json -o tr.json -f English -t Turkish \
75
- -p openai -m qwen2.5:32b --url http://localhost:11434/v1
68
+ glotto --key <key> -i en.json -o tr.json -f English -t Turkish \
69
+ -p openai -m <model-name> --url <base-url>
76
70
  ```
77
71
 
78
72
  ### Anthropic (Claude)
@@ -84,7 +78,63 @@ glotto --key <anthropic-api-key> -i en.json -o tr.json -f English -t Turkish -p
84
78
  ### Override the model
85
79
 
86
80
  ```bash
87
- glotto --key <openai-api-key> -i en.json -o tr.json -f English -t Turkish -p openai -m gpt-4.1
81
+ glotto --key <key> -i en.json -o tr.json -f English -t Turkish -p openai -m <model-name>
82
+ ```
83
+
84
+ ### Multi-target — translate to several languages in one run
85
+
86
+ Pass comma-separated values to `-t/--to` and `-o/--output`. The two lists must have the same length; entries pair up by index.
87
+
88
+ ```bash
89
+ glotto --key <key> -i en.json -f English \
90
+ -t "Turkish,French,German" \
91
+ -o "tr.json,fr.json,de.json"
92
+ ```
93
+
94
+ ### Incremental — translate only missing keys
95
+
96
+ When `--incremental` is set and the target file already exists, Glotto compares it against the source and only sends keys whose target value is missing or
97
+ empty. The existing target structure is preserved; only the missing values are filled in.
98
+
99
+ ```bash
100
+ glotto --key <key> -i en.json -o tr.json -f English -t Turkish --incremental
101
+ ```
102
+
103
+ If the target file does not exist, Glotto falls back to a full translation.
104
+
105
+ ### Stats — see token usage
106
+
107
+ `--stats` prints input/output token totals, call counts, batch sizes and per-target breakdown after the run finishes. Off by default.
108
+
109
+ ```bash
110
+ glotto --key <key> -i en.json -o tr.json -f English -t Turkish --stats
111
+ ```
112
+
113
+ ### Config file (`glotto.config.json`)
114
+
115
+ You can keep flags in a config file so you don't have to retype them. Glotto looks for `glotto.config.json` in the current working directory by default; pass
116
+ `--config <path>` to use a custom location. CLI flags always override config values.
117
+
118
+ Reference the JSON Schema for editor autocompletion and validation:
119
+
120
+ ```json
121
+ {
122
+ "$schema": "https://raw.githubusercontent.com/ibodev1/glotto/main/schema/glotto.schema.json",
123
+ "provider": "openai",
124
+ "model": "gpt-4.1-mini",
125
+ "input": "locales/en.json",
126
+ "from": "English",
127
+ "to": ["Turkish", "French", "German"],
128
+ "output": ["locales/tr.json", "locales/fr.json", "locales/de.json"],
129
+ "incremental": true,
130
+ "stats": true
131
+ }
132
+ ```
133
+
134
+ With this config you only need to provide the API key on the CLI:
135
+
136
+ ```bash
137
+ glotto --key <key>
88
138
  ```
89
139
 
90
140
  ### Disable rate-limit delay and request timeout
@@ -107,37 +157,48 @@ glotto --key <key> -i en.json -o tr.json -f English -t Turkish --max-batch-size
107
157
  | Flag | Description |
108
158
  | ------------------ | ------------------------------------------------------------------------------------ |
109
159
  | `--key` | API key for the chosen provider (required) |
110
- | `-p`, `--provider` | `gemini` \| `openai` \| `anthropic` (default `gemini`) |
111
- | `-m`, `--model` | Model name (defaults: `gemini-2.5-flash`, `gpt-4.1-mini`, `claude-3-5-haiku-latest`) |
160
+ | `-p`, `--provider` | `gemini` \| `openai` \| `anthropic` (default `openai`) |
161
+ | `-m`, `--model` | Model name (defaults: `gpt-4.1-mini`, `gemini-2.5-flash`, `claude-3-5-haiku-latest`) |
112
162
  | `-i`, `--input` | Source JSON file (required) |
113
- | `-o`, `--output` | Target JSON file (required) |
163
+ | `-o`, `--output` | Target JSON file path. Comma-separated for multi-target (required) |
114
164
  | `-f`, `--from` | Source language |
115
- | `-t`, `--to` | Target language |
165
+ | `-t`, `--to` | Target language. Comma-separated for multi-target |
116
166
  | `--url` | Custom base URL for OpenAI/Anthropic |
167
+ | `--config` | Path to a config file (default: `./glotto.config.json` if present) |
168
+ | `--stats` | Print AI usage stats (tokens, calls, bytes) at the end |
169
+ | `--incremental` | Translate only missing/empty keys when the target already exists |
117
170
  | `--no-limit` | Skip the inter-batch rate-limit delay |
118
171
  | `--no-timeout` | Disable request timeout |
119
172
  | `--max-batch-size` | Max source bytes per batch in KB (default 12) |
120
173
  | `-h`, `--help` | Help |
121
174
  | `-v`, `--version` | Version |
122
175
 
123
- ## Recommended models
124
-
125
- For high-quality multilingual output via OpenAI-compatible endpoints (Ollama, vLLM):
126
-
127
- - `aya-expanse:32b` Cohere multilingual specialist, strong Turkish/Arabic/EU languages
128
- - `qwen2.5:32b` / `qwen2.5:14b` strong general multilingual + instruction-following
129
- - `mistral-small3:24b` lighter, ~13 GB at q4
130
- - `gemma3:27b-it` Google instruction-tuned
131
-
132
- Pure translation-only models (e.g. translategemma) are not ideal here: the prompt asks the model to preserve tags and follow per-entry instructions, which
133
- requires instruction-following rather than a sentence-translation fine-tune.
176
+ ## Config file fields
177
+
178
+ `glotto.config.json` accepts the keys below. CLI flags override config values.
179
+
180
+ | Key | Type | Notes |
181
+ | -------------- | ----------------------------------- | --------------------------------------- |
182
+ | `key` | string | API key |
183
+ | `provider` | `openai` \| `gemini` \| `anthropic` | |
184
+ | `model` | string | |
185
+ | `input` | string | Source JSON path |
186
+ | `from` | string | Source language |
187
+ | `to` | string \| string[] | Single target or array for multi-target |
188
+ | `output` | string \| string[] | Must match `to` length when array |
189
+ | `url` | string | Custom base URL |
190
+ | `noLimit` | boolean | |
191
+ | `noTimeout` | boolean | |
192
+ | `maxBatchSize` | integer (KB) | |
193
+ | `stats` | boolean | |
194
+ | `incremental` | boolean | |
134
195
 
135
196
  ## Development
136
197
 
137
198
  Run from source:
138
199
 
139
200
  ```bash
140
- deno task cli --key <key> -i en.json -o tr.json -f English -t Turkish -p gemini
201
+ deno task cli --key <key> -i en.json -o tr.json -f English -t Turkish
141
202
  ```
142
203
 
143
204
  Build a single-file binary:
@@ -152,6 +213,10 @@ Build npm package:
152
213
  deno task build:npm
153
214
  ```
154
215
 
216
+ ## Star us
217
+
218
+ If Glotto saves you time, consider giving it a ⭐ on [GitHub](https://github.com/ibodev1/glotto) — it really helps the project reach more people.
219
+
155
220
  ## License
156
221
 
157
222
  MIT © 2026
package/esm/cli.js CHANGED
@@ -3,9 +3,11 @@ import "./_dnt.polyfills.js";
3
3
  import * as dntShim from "./_dnt.shims.js";
4
4
  import { Spinner } from './deps/jsr.io/@std/cli/1.0.29/unstable_spinner.js';
5
5
  import { parseArgs } from './deps/jsr.io/@std/cli/1.0.29/mod.js';
6
- import { getImportJson, resolvePath, writeOutput } from './src/file.js';
6
+ import { getImportJson, readJsonIfExists, resolvePath, writeOutput } from './src/file.js';
7
7
  import { extractLeaves, groupIntoBatches, reconstruct, runBatches } from './src/translator.js';
8
- import { DEFAULT_MAX_BATCH_BYTES, DEFAULT_PROVIDER, HELP_TEXT } from './src/contants.js';
8
+ import { findMissingLeaves, mergeTranslations } from './src/diff.js';
9
+ import { applyConfig, loadConfig } from './src/config.js';
10
+ import { DEFAULT_MAX_BATCH_BYTES, DEFAULT_PROVIDER, GITHUB_REPO_URL, HELP_TEXT } from './src/contants.js';
9
11
  import { formatBytes, validateArgs } from './src/utilites.js';
10
12
  import { logger } from './src/logger.js';
11
13
  import denoJson from './deno.js';
@@ -13,7 +15,7 @@ import Gemini from './src/providers/gemini.js';
13
15
  import OpenAIModel from './src/providers/openai.js';
14
16
  import AnthropicModel from './src/providers/anthropic.js';
15
17
  const spinner = new Spinner({ message: 'AI Thinks...', color: 'cyan' });
16
- const createTranslator = (args, options) => {
18
+ function createTranslator(args, options) {
17
19
  switch (args.provider) {
18
20
  case 'gemini':
19
21
  return new Gemini(args.key, args.model, options);
@@ -24,12 +26,99 @@ const createTranslator = (args, options) => {
24
26
  default:
25
27
  throw new Error(`Unknown provider: ${args.provider}`);
26
28
  }
27
- };
29
+ }
30
+ function printBanner() {
31
+ logger.box(`Glotto AI Translator v${denoJson.version}\n${GITHUB_REPO_URL}`);
32
+ }
33
+ function printStats(args, perTarget) {
34
+ const totalInput = perTarget.reduce((sum, t) => sum + t.usage.inputTokens, 0);
35
+ const totalOutput = perTarget.reduce((sum, t) => sum + t.usage.outputTokens, 0);
36
+ const totalCalls = perTarget.reduce((sum, t) => sum + t.calls, 0);
37
+ const totalBytes = perTarget.reduce((sum, t) => sum + t.bytes, 0);
38
+ const totalTranslated = perTarget.reduce((sum, t) => sum + t.translatedCount, 0);
39
+ const lines = [];
40
+ lines.push(`Provider: ${args.provider}${args.model ? ` (${args.model})` : ''}`);
41
+ lines.push(`Targets: ${perTarget.length}`);
42
+ lines.push('');
43
+ for (const t of perTarget) {
44
+ const mode = t.incrementalApplied ? ' [incremental]' : '';
45
+ lines.push(`→ ${t.to} (${t.output})${mode}: ${t.translatedCount}/${t.fullCount} entries, ${t.batchCount} batch(es), ${formatBytes(t.bytes)}, ${t.calls} call(s)`);
46
+ lines.push(` tokens — in: ${t.usage.inputTokens}, out: ${t.usage.outputTokens}`);
47
+ }
48
+ lines.push('');
49
+ lines.push(`Total: ${totalTranslated} entries translated, ${totalCalls} call(s), ${formatBytes(totalBytes)}`);
50
+ lines.push(`Total tokens — in: ${totalInput}, out: ${totalOutput}, sum: ${totalInput + totalOutput}`);
51
+ logger.box(lines.join('\n'));
52
+ }
53
+ async function translateForTarget(validatedArgs, fileContent, allLeaves, to, outputRel, translator, translateOptions, targetIndex, totalTargets) {
54
+ const outputPath = resolvePath(outputRel);
55
+ const targetLabel = `[Target ${targetIndex + 1}/${totalTargets}: ${to}]`;
56
+ logger.info(`${targetLabel} → ${outputRel}`);
57
+ const translatableLeaves = allLeaves.filter((leaf) => leaf.translatable);
58
+ let leavesToTranslate = translatableLeaves;
59
+ let existingTarget = null;
60
+ let incrementalApplied = false;
61
+ if (validatedArgs.incremental) {
62
+ existingTarget = await readJsonIfExists(outputPath);
63
+ if (existingTarget !== null) {
64
+ leavesToTranslate = findMissingLeaves(allLeaves, existingTarget);
65
+ incrementalApplied = true;
66
+ logger.info(`${targetLabel} Incremental: ${leavesToTranslate.length}/${translatableLeaves.length} entries missing in existing target`);
67
+ }
68
+ else {
69
+ logger.info(`${targetLabel} Incremental: target file not found, doing full translation`);
70
+ }
71
+ }
72
+ if (leavesToTranslate.length === 0) {
73
+ logger.info(`${targetLabel} Nothing to translate, writing existing/source content`);
74
+ const content = existingTarget !== null ? existingTarget : fileContent;
75
+ await writeOutput(outputPath, JSON.stringify(content, null, 2));
76
+ return {
77
+ to,
78
+ output: outputRel,
79
+ fullCount: translatableLeaves.length,
80
+ translatedCount: 0,
81
+ batchCount: 0,
82
+ bytes: 0,
83
+ calls: 0,
84
+ usage: { inputTokens: 0, outputTokens: 0 },
85
+ incrementalApplied,
86
+ };
87
+ }
88
+ const batches = groupIntoBatches(leavesToTranslate, validatedArgs.maxBatchBytes);
89
+ const totalBytes = batches.reduce((sum, b) => sum + b.byteSize, 0);
90
+ logger.info(`${targetLabel} ${leavesToTranslate.length} translatable entries, ${formatBytes(totalBytes)}, split into ${batches.length} batch(es)`);
91
+ for (const batch of batches) {
92
+ logger.info(` Batch ${batch.index + 1}: ${batch.leaves.length} entries, ${formatBytes(batch.byteSize)}`);
93
+ }
94
+ spinner.start();
95
+ const result = await runBatches(batches, translator, validatedArgs.from, to, translateOptions);
96
+ spinner.stop();
97
+ let finalContent;
98
+ if (incrementalApplied && existingTarget !== null) {
99
+ finalContent = mergeTranslations(existingTarget, allLeaves, result.translations);
100
+ }
101
+ else {
102
+ finalContent = reconstruct(allLeaves, result.translations);
103
+ }
104
+ await writeOutput(outputPath, JSON.stringify(finalContent, null, 2));
105
+ return {
106
+ to,
107
+ output: outputRel,
108
+ fullCount: translatableLeaves.length,
109
+ translatedCount: result.translations.size,
110
+ batchCount: batches.length,
111
+ bytes: totalBytes,
112
+ calls: result.calls,
113
+ usage: result.usage,
114
+ incrementalApplied,
115
+ };
116
+ }
28
117
  async function main() {
29
118
  try {
30
119
  const args = parseArgs(dntShim.Deno.args, {
31
- string: ['key', 'provider', 'model', 'input', 'output', 'from', 'to', 'url', 'max-batch-size'],
32
- boolean: ['help', 'version', 'no-limit', 'no-timeout'],
120
+ string: ['key', 'provider', 'model', 'input', 'output', 'from', 'to', 'url', 'max-batch-size', 'config'],
121
+ boolean: ['help', 'version', 'no-limit', 'no-timeout', 'stats', 'incremental'],
33
122
  alias: {
34
123
  provider: 'p',
35
124
  model: 'm',
@@ -41,7 +130,6 @@ async function main() {
41
130
  help: 'h',
42
131
  version: 'v',
43
132
  },
44
- default: { provider: DEFAULT_PROVIDER },
45
133
  });
46
134
  const help = args.help || dntShim.Deno.args.length === 0;
47
135
  const version = args.version;
@@ -50,38 +138,53 @@ async function main() {
50
138
  dntShim.Deno.exit(0);
51
139
  }
52
140
  if (help) {
141
+ printBanner();
53
142
  logger.box(HELP_TEXT);
54
143
  dntShim.Deno.exit(0);
55
144
  }
56
- const validatedArgs = validateArgs(args);
145
+ printBanner();
146
+ const config = await loadConfig(args.config);
147
+ const merged = applyConfig(args, config, dntShim.Deno.args);
148
+ if (!merged.provider) {
149
+ merged.provider = DEFAULT_PROVIDER;
150
+ }
151
+ const validatedArgs = validateArgs(merged);
57
152
  const fileContent = await getImportJson(validatedArgs.input);
58
153
  const allLeaves = extractLeaves(fileContent);
59
154
  const translatableLeaves = allLeaves.filter((leaf) => leaf.translatable);
60
- const batches = groupIntoBatches(allLeaves, validatedArgs.maxBatchBytes);
61
- const totalBytes = batches.reduce((sum, b) => sum + b.byteSize, 0);
62
155
  logger.info('Provider: ', validatedArgs.provider);
63
156
  logger.info('Input: ', validatedArgs.input);
64
- logger.info('Output: ', validatedArgs.output);
157
+ logger.info('Targets: ', validatedArgs.to.map((to, i) => `${to} → ${validatedArgs.output[i]}`).join(', '));
65
158
  logger.info('From: ', validatedArgs.from);
66
- logger.info('To: ', validatedArgs.to);
67
- if (validatedArgs.model)
159
+ if (validatedArgs.model) {
68
160
  logger.info('Model: ', validatedArgs.model);
69
- if (validatedArgs.url)
161
+ }
162
+ if (validatedArgs.url) {
70
163
  logger.info('URL: ', validatedArgs.url);
71
- if (validatedArgs.noLimit)
164
+ }
165
+ if (validatedArgs.noLimit) {
72
166
  logger.info('Rate limit protection: disabled (--no-limit)');
73
- if (validatedArgs.noTimeout)
167
+ }
168
+ if (validatedArgs.noTimeout) {
74
169
  logger.info('Request timeout: disabled (--no-timeout)');
170
+ }
171
+ if (validatedArgs.incremental) {
172
+ logger.info('Incremental mode: enabled (--incremental)');
173
+ }
174
+ if (validatedArgs.stats) {
175
+ logger.info('Stats: enabled (--stats)');
176
+ }
75
177
  if (validatedArgs.maxBatchBytes !== DEFAULT_MAX_BATCH_BYTES) {
76
178
  logger.info(`Max batch size: ${formatBytes(validatedArgs.maxBatchBytes)}`);
77
179
  }
78
- logger.info(`Total: ${translatableLeaves.length} translatable entries (of ${allLeaves.length} leaves), ${formatBytes(totalBytes)}, split into ${batches.length} batch(es)`);
79
- for (const batch of batches) {
80
- logger.info(` Batch ${batch.index + 1}: ${batch.leaves.length} entries, ${formatBytes(batch.byteSize)}`);
81
- }
82
- if (batches.length === 0) {
83
- logger.warn('No translatable entries found, copying input to output');
84
- await writeOutput(resolvePath(validatedArgs.output), JSON.stringify(fileContent, null, 2));
180
+ logger.info(`Source: ${translatableLeaves.length} translatable entries (of ${allLeaves.length} leaves)`);
181
+ if (translatableLeaves.length === 0 && !validatedArgs.incremental) {
182
+ logger.warn('No translatable entries found, copying input to output(s)');
183
+ for (const outputRel of validatedArgs.output) {
184
+ await writeOutput(resolvePath(outputRel), JSON.stringify(fileContent, null, 2));
185
+ }
186
+ logger.success('Translation completed');
187
+ logger.info(`★ Glotto faydalı olduysa GitHub'da yıldızlamayı unutma: ${GITHUB_REPO_URL}`);
85
188
  dntShim.Deno.exit(0);
86
189
  }
87
190
  const translateOptions = {
@@ -89,22 +192,23 @@ async function main() {
89
192
  noTimeout: validatedArgs.noTimeout,
90
193
  };
91
194
  const translator = createTranslator(validatedArgs, translateOptions);
92
- spinner.start();
93
- const translations = await runBatches(batches, translator, validatedArgs.from, validatedArgs.to, translateOptions);
94
- const result = reconstruct(allLeaves, translations);
95
- const outputPath = resolvePath(validatedArgs.output);
96
- await writeOutput(outputPath, JSON.stringify(result, null, 2));
195
+ const perTarget = [];
196
+ for (let i = 0; i < validatedArgs.to.length; i++) {
197
+ const stat = await translateForTarget(validatedArgs, fileContent, allLeaves, validatedArgs.to[i], validatedArgs.output[i], translator, translateOptions, i, validatedArgs.to.length);
198
+ perTarget.push(stat);
199
+ }
97
200
  spinner.stop();
98
201
  logger.success('Translation completed');
202
+ if (validatedArgs.stats) {
203
+ printStats(validatedArgs, perTarget);
204
+ }
205
+ logger.info(`★ Glotto faydalı olduysa GitHub'da yıldızlamayı unutma: ${GITHUB_REPO_URL}`);
206
+ dntShim.Deno.exit(0);
99
207
  }
100
208
  catch (error) {
101
209
  spinner.stop();
102
210
  logger.error(error);
103
211
  dntShim.Deno.exit(1);
104
212
  }
105
- finally {
106
- spinner.stop();
107
- dntShim.Deno.exit(0);
108
- }
109
213
  }
110
214
  main();
package/esm/deno.d.ts CHANGED
@@ -11,17 +11,22 @@ declare namespace _default {
11
11
  publish: string;
12
12
  "publish:npm": string;
13
13
  check: string;
14
+ test: string;
14
15
  };
15
16
  namespace fmt {
16
17
  let semiColons: boolean;
17
18
  let singleQuote: boolean;
18
19
  let lineWidth: number;
19
20
  }
21
+ namespace publish {
22
+ let exclude: string[];
23
+ }
20
24
  let imports: {
21
25
  "@anthropic-ai/sdk": string;
22
26
  "@deno/dnt": string;
23
27
  "@google/genai": string;
24
28
  "@openai/openai": string;
29
+ "@std/assert": string;
25
30
  "@std/cli": string;
26
31
  "@std/path": string;
27
32
  consola: string;
package/esm/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "@ibodev/glotto",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "exports": "./cli.ts",
5
5
  "lock": false,
6
6
  "nodeModulesDir": "auto",
@@ -10,21 +10,33 @@ export default {
10
10
  "build:npm": "deno run -A scripts/build_npm.ts",
11
11
  "publish": "deno publish",
12
12
  "publish:npm": "cd ./npm && npm publish",
13
- "check": "deno check **/*.ts"
13
+ "check": "deno check **/*.ts",
14
+ "test": "deno test -A --no-check tests/"
14
15
  },
15
16
  "fmt": {
16
17
  "semiColons": true,
17
18
  "singleQuote": true,
18
19
  "lineWidth": 160
19
20
  },
21
+ "publish": {
22
+ "exclude": [
23
+ "tests/",
24
+ "scripts/",
25
+ "npm/",
26
+ "assets/",
27
+ "glotto.exe",
28
+ "CLAUDE.md"
29
+ ]
30
+ },
20
31
  "imports": {
21
- "@anthropic-ai/sdk": "npm:@anthropic-ai/sdk@^0.95.0",
32
+ "@anthropic-ai/sdk": "npm:@anthropic-ai/sdk@^0.95.1",
22
33
  "@deno/dnt": "jsr:@deno/dnt@^0.42.3",
23
- "@google/genai": "npm:@google/genai@^1.52.0",
24
- "@openai/openai": "npm:openai@^6.36.0",
34
+ "@google/genai": "npm:@google/genai@^2.0.0",
35
+ "@openai/openai": "npm:openai@^6.37.0",
36
+ "@std/assert": "jsr:@std/assert@^1.0.19",
25
37
  "@std/cli": "jsr:@std/cli@^1.0.29",
26
38
  "@std/path": "jsr:@std/path@^1.1.4",
27
39
  "consola": "npm:consola@^3.4.2"
28
40
  },
29
- "allowScripts": ["npm:@google/genai@1.52.0", "npm:protobufjs@7.5.6"]
41
+ "allowScripts": ["npm:@google/genai@1.52.0", "npm:@google/genai@2.0.0", "npm:protobufjs@7.5.6"]
30
42
  };
@@ -0,0 +1,4 @@
1
+ import type { GlottoConfig, TranslateArgs } from './types.js';
2
+ export declare function loadConfig(explicitPath?: string): Promise<GlottoConfig>;
3
+ export declare function applyConfig(cli: TranslateArgs, config: GlottoConfig, rawArgs: string[]): TranslateArgs;
4
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/src/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAuB9D,wBAAsB,UAAU,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAc7E;AAkBD,wBAAgB,WAAW,CAAC,GAAG,EAAE,aAAa,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,aAAa,CA0CtG"}
@@ -0,0 +1,95 @@
1
+ import * as dntShim from "../_dnt.shims.js";
2
+ import { join } from '../deps/jsr.io/@std/path/1.1.4/mod.js';
3
+ import { CONFIG_FILE_NAME } from './contants.js';
4
+ async function readConfigFile(path) {
5
+ let raw;
6
+ try {
7
+ raw = await dntShim.Deno.readTextFile(path);
8
+ }
9
+ catch (error) {
10
+ if (error instanceof dntShim.Deno.errors.NotFound) {
11
+ return null;
12
+ }
13
+ throw error;
14
+ }
15
+ if (raw.trim() === '') {
16
+ return {};
17
+ }
18
+ const parsed = JSON.parse(raw);
19
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
20
+ throw new Error(`Config file must contain a JSON object: ${path}`);
21
+ }
22
+ return parsed;
23
+ }
24
+ export async function loadConfig(explicitPath) {
25
+ if (explicitPath) {
26
+ const config = await readConfigFile(explicitPath);
27
+ if (config === null) {
28
+ throw new Error(`Config file not found: ${explicitPath}`);
29
+ }
30
+ return config;
31
+ }
32
+ const defaultPath = join(dntShim.Deno.cwd(), CONFIG_FILE_NAME);
33
+ const config = await readConfigFile(defaultPath);
34
+ if (config === null) {
35
+ return {};
36
+ }
37
+ return config;
38
+ }
39
+ function joinList(value) {
40
+ if (value === undefined) {
41
+ return undefined;
42
+ }
43
+ if (Array.isArray(value)) {
44
+ return value.join(',');
45
+ }
46
+ return value;
47
+ }
48
+ function rawArgsHas(rawArgs, flag) {
49
+ const long = `--${flag}`;
50
+ const longEq = `--${flag}=`;
51
+ return rawArgs.some((a) => a === long || a.startsWith(longEq));
52
+ }
53
+ export function applyConfig(cli, config, rawArgs) {
54
+ const merged = { ...cli };
55
+ if (merged.key === undefined && config.key !== undefined) {
56
+ merged.key = config.key;
57
+ }
58
+ if (merged.provider === undefined && config.provider !== undefined) {
59
+ merged.provider = config.provider;
60
+ }
61
+ if (merged.model === undefined && config.model !== undefined) {
62
+ merged.model = config.model;
63
+ }
64
+ if (merged.input === undefined && config.input !== undefined) {
65
+ merged.input = config.input;
66
+ }
67
+ if (merged.output === undefined && config.output !== undefined) {
68
+ merged.output = joinList(config.output);
69
+ }
70
+ if (merged.from === undefined && config.from !== undefined) {
71
+ merged.from = config.from;
72
+ }
73
+ if (merged.to === undefined && config.to !== undefined) {
74
+ merged.to = joinList(config.to);
75
+ }
76
+ if (merged.url === undefined && config.url !== undefined) {
77
+ merged.url = config.url;
78
+ }
79
+ if (merged['max-batch-size'] === undefined && config.maxBatchSize !== undefined) {
80
+ merged['max-batch-size'] = String(config.maxBatchSize);
81
+ }
82
+ if (!rawArgsHas(rawArgs, 'no-limit') && config.noLimit !== undefined) {
83
+ merged['no-limit'] = config.noLimit;
84
+ }
85
+ if (!rawArgsHas(rawArgs, 'no-timeout') && config.noTimeout !== undefined) {
86
+ merged['no-timeout'] = config.noTimeout;
87
+ }
88
+ if (!rawArgsHas(rawArgs, 'stats') && config.stats !== undefined) {
89
+ merged.stats = config.stats;
90
+ }
91
+ if (!rawArgsHas(rawArgs, 'incremental') && config.incremental !== undefined) {
92
+ merged.incremental = config.incremental;
93
+ }
94
+ return merged;
95
+ }
@@ -5,5 +5,9 @@ export declare const DEFAULT_MAX_BATCH_BYTES: 12000;
5
5
  export declare const MAX_RETRIES: 3;
6
6
  export declare const BASE_RETRY_DELAY_MS: 2000;
7
7
  export declare const INTER_BATCH_DELAY_MS: 1500;
8
+ export declare const INTER_LEAF_DELAY_MS: 200;
9
+ export declare const PER_LEAF_FALLBACK_RATIO: 0.5;
10
+ export declare const GITHUB_REPO_URL: "https://github.com/ibodev1/glotto";
11
+ export declare const CONFIG_FILE_NAME: "glotto.config.json";
8
12
  export declare const HELP_TEXT: string;
9
13
  //# sourceMappingURL=contants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contants.d.ts","sourceRoot":"","sources":["../../src/src/contants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,eAAO,MAAM,gBAAgB,EAAE,QAAmB,CAAC;AAEnD,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAInD,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAG,KAAe,CAAC;AAEvD,eAAO,MAAM,WAAW,EAAG,CAAU,CAAC;AAEtC,eAAO,MAAM,mBAAmB,EAAG,IAAc,CAAC;AAElD,eAAO,MAAM,oBAAoB,EAAG,IAAc,CAAC;AAEnD,eAAO,MAAM,SAAS,QAgCrB,CAAC"}
1
+ {"version":3,"file":"contants.d.ts","sourceRoot":"","sources":["../../src/src/contants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,eAAO,MAAM,gBAAgB,EAAE,QAAmB,CAAC;AAEnD,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAInD,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAG,KAAe,CAAC;AAEvD,eAAO,MAAM,WAAW,EAAG,CAAU,CAAC;AAEtC,eAAO,MAAM,mBAAmB,EAAG,IAAc,CAAC;AAElD,eAAO,MAAM,oBAAoB,EAAG,IAAc,CAAC;AAEnD,eAAO,MAAM,mBAAmB,EAAG,GAAY,CAAC;AAEhD,eAAO,MAAM,uBAAuB,EAAG,GAAY,CAAC;AAEpD,eAAO,MAAM,eAAe,EAAG,mCAA4C,CAAC;AAE5E,eAAO,MAAM,gBAAgB,EAAG,oBAA6B,CAAC;AAE9D,eAAO,MAAM,SAAS,QAqCrB,CAAC"}