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.
- package/README.md +112 -47
- package/esm/cli.js +136 -32
- package/esm/deno.d.ts +5 -0
- package/esm/deno.js +18 -6
- package/esm/src/config.d.ts +4 -0
- package/esm/src/config.d.ts.map +1 -0
- package/esm/src/config.js +95 -0
- package/esm/src/contants.d.ts +4 -0
- package/esm/src/contants.d.ts.map +1 -1
- package/esm/src/contants.js +14 -5
- package/esm/src/diff.d.ts +4 -0
- package/esm/src/diff.d.ts.map +1 -0
- package/esm/src/diff.js +53 -0
- package/esm/src/file.d.ts +4 -3
- package/esm/src/file.d.ts.map +1 -1
- package/esm/src/file.js +23 -5
- package/esm/src/providers/anthropic.d.ts +2 -2
- package/esm/src/providers/anthropic.d.ts.map +1 -1
- package/esm/src/providers/anthropic.js +9 -1
- package/esm/src/providers/gemini.d.ts +2 -2
- package/esm/src/providers/gemini.d.ts.map +1 -1
- package/esm/src/providers/gemini.js +10 -2
- package/esm/src/providers/openai.d.ts +2 -2
- package/esm/src/providers/openai.d.ts.map +1 -1
- package/esm/src/providers/openai.js +9 -1
- package/esm/src/translator.d.ts +14 -7
- package/esm/src/translator.d.ts.map +1 -1
- package/esm/src/translator.js +161 -77
- package/esm/src/types.d.ts +31 -3
- package/esm/src/types.d.ts.map +1 -1
- package/esm/src/utilites.d.ts +3 -4
- package/esm/src/utilites.d.ts.map +1 -1
- package/esm/src/utilites.js +40 -16
- package/package.json +20 -6
- package/schema/glotto.schema.json +87 -0
- package/script/cli.js +134 -30
- package/script/deno.d.ts +5 -0
- package/script/deno.js +18 -6
- package/script/src/config.d.ts +4 -0
- package/script/src/config.d.ts.map +1 -0
- package/script/src/config.js +132 -0
- package/script/src/contants.d.ts +4 -0
- package/script/src/contants.d.ts.map +1 -1
- package/script/src/contants.js +15 -6
- package/script/src/diff.d.ts +4 -0
- package/script/src/diff.d.ts.map +1 -0
- package/script/src/diff.js +57 -0
- package/script/src/file.d.ts +4 -3
- package/script/src/file.d.ts.map +1 -1
- package/script/src/file.js +28 -10
- package/script/src/providers/anthropic.d.ts +2 -2
- package/script/src/providers/anthropic.d.ts.map +1 -1
- package/script/src/providers/anthropic.js +9 -1
- package/script/src/providers/gemini.d.ts +2 -2
- package/script/src/providers/gemini.d.ts.map +1 -1
- package/script/src/providers/gemini.js +10 -2
- package/script/src/providers/openai.d.ts +2 -2
- package/script/src/providers/openai.d.ts.map +1 -1
- package/script/src/providers/openai.js +9 -1
- package/script/src/translator.d.ts +14 -7
- package/script/src/translator.d.ts.map +1 -1
- package/script/src/translator.js +168 -83
- package/script/src/types.d.ts +31 -3
- package/script/src/types.d.ts.map +1 -1
- package/script/src/utilites.d.ts +3 -4
- package/script/src/utilites.d.ts.map +1 -1
- 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
|
|
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
|
|
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.
|
|
65
|
+
Custom base URL (e.g. self-hosted OpenAI-compatible server):
|
|
72
66
|
|
|
73
67
|
```bash
|
|
74
|
-
glotto --key
|
|
75
|
-
-p openai -m
|
|
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 <
|
|
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 `
|
|
111
|
-
| `-m`, `--model` | Model name (defaults: `
|
|
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
|
-
##
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
157
|
+
logger.info('Targets: ', validatedArgs.to.map((to, i) => `${to} → ${validatedArgs.output[i]}`).join(', '));
|
|
65
158
|
logger.info('From: ', validatedArgs.from);
|
|
66
|
-
|
|
67
|
-
if (validatedArgs.model)
|
|
159
|
+
if (validatedArgs.model) {
|
|
68
160
|
logger.info('Model: ', validatedArgs.model);
|
|
69
|
-
|
|
161
|
+
}
|
|
162
|
+
if (validatedArgs.url) {
|
|
70
163
|
logger.info('URL: ', validatedArgs.url);
|
|
71
|
-
|
|
164
|
+
}
|
|
165
|
+
if (validatedArgs.noLimit) {
|
|
72
166
|
logger.info('Rate limit protection: disabled (--no-limit)');
|
|
73
|
-
|
|
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(`
|
|
79
|
-
|
|
80
|
-
logger.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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.
|
|
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.
|
|
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@^
|
|
24
|
-
"@openai/openai": "npm:openai@^6.
|
|
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
|
+
}
|
package/esm/src/contants.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|