glotto 2.9.0 → 3.0.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 +86 -55
- package/esm/cli.js +47 -39
- package/esm/deno.d.ts +0 -1
- package/esm/deno.js +1 -2
- package/esm/src/contants.d.ts +2 -2
- package/esm/src/contants.d.ts.map +1 -1
- package/esm/src/contants.js +22 -12
- package/esm/src/file.d.ts +2 -7
- package/esm/src/file.d.ts.map +1 -1
- package/esm/src/file.js +1 -108
- package/esm/src/providers/anthropic.d.ts +6 -11
- package/esm/src/providers/anthropic.d.ts.map +1 -1
- package/esm/src/providers/anthropic.js +14 -108
- package/esm/src/providers/gemini.d.ts +6 -11
- package/esm/src/providers/gemini.d.ts.map +1 -1
- package/esm/src/providers/gemini.js +13 -114
- package/esm/src/providers/openai.d.ts +6 -11
- package/esm/src/providers/openai.d.ts.map +1 -1
- package/esm/src/providers/openai.js +10 -109
- package/esm/src/translator.d.ts +8 -0
- package/esm/src/translator.d.ts.map +1 -0
- package/esm/src/translator.js +200 -0
- package/esm/src/types.d.ts +28 -11
- package/esm/src/types.d.ts.map +1 -1
- package/esm/src/utilites.d.ts +0 -6
- package/esm/src/utilites.d.ts.map +1 -1
- package/esm/src/utilites.js +18 -132
- package/package.json +1 -1
- package/script/cli.js +45 -37
- package/script/deno.d.ts +0 -1
- package/script/deno.js +1 -2
- package/script/src/contants.d.ts +2 -2
- package/script/src/contants.d.ts.map +1 -1
- package/script/src/contants.js +23 -13
- package/script/src/file.d.ts +2 -7
- package/script/src/file.d.ts.map +1 -1
- package/script/src/file.js +2 -114
- package/script/src/providers/anthropic.d.ts +6 -11
- package/script/src/providers/anthropic.d.ts.map +1 -1
- package/script/src/providers/anthropic.js +13 -107
- package/script/src/providers/gemini.d.ts +6 -11
- package/script/src/providers/gemini.d.ts.map +1 -1
- package/script/src/providers/gemini.js +12 -113
- package/script/src/providers/openai.d.ts +6 -11
- package/script/src/providers/openai.d.ts.map +1 -1
- package/script/src/providers/openai.js +9 -108
- package/script/src/translator.d.ts +8 -0
- package/script/src/translator.d.ts.map +1 -0
- package/script/src/translator.js +209 -0
- package/script/src/types.d.ts +28 -11
- package/script/src/types.d.ts.map +1 -1
- package/script/src/utilites.d.ts +0 -6
- package/script/src/utilites.d.ts.map +1 -1
- package/script/src/utilites.js +19 -136
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_common16.d.ts +0 -23
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_common16.d.ts.map +0 -1
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_common16.js +0 -51
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_common32.d.ts +0 -35
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_common32.d.ts.map +0 -1
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_common32.js +0 -192
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_common64.d.ts +0 -35
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_common64.d.ts.map +0 -1
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_common64.js +0 -113
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_common_detach.d.ts +0 -4
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_common_detach.d.ts.map +0 -1
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_common_detach.js +0 -13
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_types.d.ts +0 -9
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_types.d.ts.map +0 -1
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_types.js +0 -2
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_validate_binary_like.d.ts +0 -2
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_validate_binary_like.d.ts.map +0 -1
- package/esm/deps/jsr.io/@std/encoding/1.0.10/_validate_binary_like.js +0 -26
- package/esm/deps/jsr.io/@std/encoding/1.0.10/ascii85.d.ts +0 -61
- package/esm/deps/jsr.io/@std/encoding/1.0.10/ascii85.d.ts.map +0 -1
- package/esm/deps/jsr.io/@std/encoding/1.0.10/ascii85.js +0 -152
- package/esm/deps/jsr.io/@std/encoding/1.0.10/base32.d.ts +0 -40
- package/esm/deps/jsr.io/@std/encoding/1.0.10/base32.d.ts.map +0 -1
- package/esm/deps/jsr.io/@std/encoding/1.0.10/base32.js +0 -87
- package/esm/deps/jsr.io/@std/encoding/1.0.10/base58.d.ts +0 -40
- package/esm/deps/jsr.io/@std/encoding/1.0.10/base58.d.ts.map +0 -1
- package/esm/deps/jsr.io/@std/encoding/1.0.10/base58.js +0 -131
- package/esm/deps/jsr.io/@std/encoding/1.0.10/base64.d.ts +0 -40
- package/esm/deps/jsr.io/@std/encoding/1.0.10/base64.d.ts.map +0 -1
- package/esm/deps/jsr.io/@std/encoding/1.0.10/base64.js +0 -82
- package/esm/deps/jsr.io/@std/encoding/1.0.10/base64url.d.ts +0 -40
- package/esm/deps/jsr.io/@std/encoding/1.0.10/base64url.d.ts.map +0 -1
- package/esm/deps/jsr.io/@std/encoding/1.0.10/base64url.js +0 -72
- package/esm/deps/jsr.io/@std/encoding/1.0.10/hex.d.ts +0 -39
- package/esm/deps/jsr.io/@std/encoding/1.0.10/hex.d.ts.map +0 -1
- package/esm/deps/jsr.io/@std/encoding/1.0.10/hex.js +0 -87
- package/esm/deps/jsr.io/@std/encoding/1.0.10/mod.d.ts +0 -98
- package/esm/deps/jsr.io/@std/encoding/1.0.10/mod.d.ts.map +0 -1
- package/esm/deps/jsr.io/@std/encoding/1.0.10/mod.js +0 -99
- package/esm/deps/jsr.io/@std/encoding/1.0.10/varint.d.ts +0 -120
- package/esm/deps/jsr.io/@std/encoding/1.0.10/varint.d.ts.map +0 -1
- package/esm/deps/jsr.io/@std/encoding/1.0.10/varint.js +0 -205
- package/script/deps/jsr.io/@std/encoding/1.0.10/_common16.d.ts +0 -23
- package/script/deps/jsr.io/@std/encoding/1.0.10/_common16.d.ts.map +0 -1
- package/script/deps/jsr.io/@std/encoding/1.0.10/_common16.js +0 -57
- package/script/deps/jsr.io/@std/encoding/1.0.10/_common32.d.ts +0 -35
- package/script/deps/jsr.io/@std/encoding/1.0.10/_common32.d.ts.map +0 -1
- package/script/deps/jsr.io/@std/encoding/1.0.10/_common32.js +0 -198
- package/script/deps/jsr.io/@std/encoding/1.0.10/_common64.d.ts +0 -35
- package/script/deps/jsr.io/@std/encoding/1.0.10/_common64.d.ts.map +0 -1
- package/script/deps/jsr.io/@std/encoding/1.0.10/_common64.js +0 -119
- package/script/deps/jsr.io/@std/encoding/1.0.10/_common_detach.d.ts +0 -4
- package/script/deps/jsr.io/@std/encoding/1.0.10/_common_detach.d.ts.map +0 -1
- package/script/deps/jsr.io/@std/encoding/1.0.10/_common_detach.js +0 -16
- package/script/deps/jsr.io/@std/encoding/1.0.10/_types.d.ts +0 -9
- package/script/deps/jsr.io/@std/encoding/1.0.10/_types.d.ts.map +0 -1
- package/script/deps/jsr.io/@std/encoding/1.0.10/_types.js +0 -3
- package/script/deps/jsr.io/@std/encoding/1.0.10/_validate_binary_like.d.ts +0 -2
- package/script/deps/jsr.io/@std/encoding/1.0.10/_validate_binary_like.d.ts.map +0 -1
- package/script/deps/jsr.io/@std/encoding/1.0.10/_validate_binary_like.js +0 -29
- package/script/deps/jsr.io/@std/encoding/1.0.10/ascii85.d.ts +0 -61
- package/script/deps/jsr.io/@std/encoding/1.0.10/ascii85.d.ts.map +0 -1
- package/script/deps/jsr.io/@std/encoding/1.0.10/ascii85.js +0 -156
- package/script/deps/jsr.io/@std/encoding/1.0.10/base32.d.ts +0 -40
- package/script/deps/jsr.io/@std/encoding/1.0.10/base32.d.ts.map +0 -1
- package/script/deps/jsr.io/@std/encoding/1.0.10/base32.js +0 -91
- package/script/deps/jsr.io/@std/encoding/1.0.10/base58.d.ts +0 -40
- package/script/deps/jsr.io/@std/encoding/1.0.10/base58.d.ts.map +0 -1
- package/script/deps/jsr.io/@std/encoding/1.0.10/base58.js +0 -135
- package/script/deps/jsr.io/@std/encoding/1.0.10/base64.d.ts +0 -40
- package/script/deps/jsr.io/@std/encoding/1.0.10/base64.d.ts.map +0 -1
- package/script/deps/jsr.io/@std/encoding/1.0.10/base64.js +0 -86
- package/script/deps/jsr.io/@std/encoding/1.0.10/base64url.d.ts +0 -40
- package/script/deps/jsr.io/@std/encoding/1.0.10/base64url.d.ts.map +0 -1
- package/script/deps/jsr.io/@std/encoding/1.0.10/base64url.js +0 -76
- package/script/deps/jsr.io/@std/encoding/1.0.10/hex.d.ts +0 -39
- package/script/deps/jsr.io/@std/encoding/1.0.10/hex.d.ts.map +0 -1
- package/script/deps/jsr.io/@std/encoding/1.0.10/hex.js +0 -91
- package/script/deps/jsr.io/@std/encoding/1.0.10/mod.d.ts +0 -98
- package/script/deps/jsr.io/@std/encoding/1.0.10/mod.d.ts.map +0 -1
- package/script/deps/jsr.io/@std/encoding/1.0.10/mod.js +0 -115
- package/script/deps/jsr.io/@std/encoding/1.0.10/varint.d.ts +0 -120
- package/script/deps/jsr.io/@std/encoding/1.0.10/varint.d.ts.map +0 -1
- package/script/deps/jsr.io/@std/encoding/1.0.10/varint.js +0 -211
package/README.md
CHANGED
|
@@ -1,32 +1,42 @@
|
|
|
1
1
|
# Glotto AI Translator
|
|
2
2
|
|
|
3
|
-
Glotto
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
-
|
|
3
|
+
Glotto translates i18n JSON files (i18next, react-intl, vue-i18n, etc.) using AI providers without forcing the model to produce JSON. It walks the input JSON,
|
|
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
|
+
structure, keys, arrays, variables and HTML tags are preserved by Glotto itself — the model only sees and produces text.
|
|
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.
|
|
12
30
|
|
|
13
31
|
## Installation
|
|
14
32
|
|
|
15
|
-
Glotto can be installed and used through either JSR (Deno) or npm. Choose the method that best suits your development environment.
|
|
16
|
-
|
|
17
33
|
### [JSR (Deno)](https://jsr.io/@ibodev/glotto)
|
|
18
34
|
|
|
19
|
-
#### Global Installation
|
|
20
|
-
|
|
21
|
-
Install Glotto globally to use it as a CLI tool from anywhere:
|
|
22
|
-
|
|
23
35
|
```bash
|
|
24
36
|
deno install --global --name glotto -A jsr:@ibodev/glotto
|
|
25
37
|
```
|
|
26
38
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
Alternatively, run Glotto directly without installation:
|
|
39
|
+
Or run without installing:
|
|
30
40
|
|
|
31
41
|
```bash
|
|
32
42
|
deno run -A jsr:@ibodev/glotto
|
|
@@ -34,19 +44,11 @@ deno run -A jsr:@ibodev/glotto
|
|
|
34
44
|
|
|
35
45
|
### [npm (Node)](https://www.npmjs.com/package/glotto)
|
|
36
46
|
|
|
37
|
-
#### Global Installation
|
|
38
|
-
|
|
39
|
-
Install Glotto globally via npm to use it as a CLI tool:
|
|
40
|
-
|
|
41
47
|
```bash
|
|
42
48
|
npm install --global glotto
|
|
43
49
|
```
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
#### Direct Execution
|
|
48
|
-
|
|
49
|
-
Run Glotto directly using npx without installation:
|
|
51
|
+
Or via npx:
|
|
50
52
|
|
|
51
53
|
```bash
|
|
52
54
|
npx glotto
|
|
@@ -54,73 +56,102 @@ npx glotto
|
|
|
54
56
|
|
|
55
57
|
## Usage
|
|
56
58
|
|
|
57
|
-
### Basic
|
|
59
|
+
### Basic (default Gemini)
|
|
58
60
|
|
|
59
61
|
```bash
|
|
60
62
|
glotto --key <gemini-api-key> -i en.json -o ar.json -f English -t Arabic
|
|
61
63
|
```
|
|
62
64
|
|
|
63
|
-
###
|
|
65
|
+
### OpenAI / OpenAI-compatible (Ollama, vLLM, etc.)
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
glotto --key <openai-api-key> -i en.json -o tr.json -f English -t Turkish -p openai
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Custom base URL (e.g. local Ollama):
|
|
64
72
|
|
|
65
73
|
```bash
|
|
66
|
-
glotto --key
|
|
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
|
|
67
76
|
```
|
|
68
77
|
|
|
69
|
-
|
|
78
|
+
### Anthropic (Claude)
|
|
70
79
|
|
|
71
80
|
```bash
|
|
72
|
-
glotto --key <
|
|
81
|
+
glotto --key <anthropic-api-key> -i en.json -o tr.json -f English -t Turkish -p anthropic
|
|
73
82
|
```
|
|
74
83
|
|
|
75
|
-
###
|
|
84
|
+
### Override the model
|
|
76
85
|
|
|
77
86
|
```bash
|
|
78
|
-
glotto --key <
|
|
87
|
+
glotto --key <openai-api-key> -i en.json -o tr.json -f English -t Turkish -p openai -m gpt-4.1
|
|
79
88
|
```
|
|
80
89
|
|
|
81
|
-
|
|
90
|
+
### Disable rate-limit delay and request timeout
|
|
82
91
|
|
|
83
92
|
```bash
|
|
84
|
-
glotto --key <
|
|
93
|
+
glotto --key <key> -i en.json -o tr.json -f English -t Turkish --no-limit --no-timeout
|
|
85
94
|
```
|
|
86
95
|
|
|
87
|
-
|
|
96
|
+
### Tune batch size
|
|
97
|
+
|
|
98
|
+
`--max-batch-size` is the maximum source bytes per batch, in KB. Smaller batches mean more requests but lower per-request token cost; larger batches mean fewer
|
|
99
|
+
requests.
|
|
88
100
|
|
|
89
101
|
```bash
|
|
90
|
-
glotto --key <
|
|
102
|
+
glotto --key <key> -i en.json -o tr.json -f English -t Turkish --max-batch-size 8
|
|
91
103
|
```
|
|
92
104
|
|
|
93
|
-
|
|
105
|
+
## Parameters
|
|
106
|
+
|
|
107
|
+
| Flag | Description |
|
|
108
|
+
| ------------------ | ------------------------------------------------------------------------------------ |
|
|
109
|
+
| `--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`) |
|
|
112
|
+
| `-i`, `--input` | Source JSON file (required) |
|
|
113
|
+
| `-o`, `--output` | Target JSON file (required) |
|
|
114
|
+
| `-f`, `--from` | Source language |
|
|
115
|
+
| `-t`, `--to` | Target language |
|
|
116
|
+
| `--url` | Custom base URL for OpenAI/Anthropic |
|
|
117
|
+
| `--no-limit` | Skip the inter-batch rate-limit delay |
|
|
118
|
+
| `--no-timeout` | Disable request timeout |
|
|
119
|
+
| `--max-batch-size` | Max source bytes per batch in KB (default 12) |
|
|
120
|
+
| `-h`, `--help` | Help |
|
|
121
|
+
| `-v`, `--version` | Version |
|
|
122
|
+
|
|
123
|
+
## Recommended models
|
|
94
124
|
|
|
95
|
-
-
|
|
96
|
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
|
|
102
|
-
-
|
|
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.
|
|
103
134
|
|
|
104
135
|
## Development
|
|
105
136
|
|
|
106
|
-
|
|
137
|
+
Run from source:
|
|
107
138
|
|
|
108
139
|
```bash
|
|
109
|
-
deno task cli --key <
|
|
140
|
+
deno task cli --key <key> -i en.json -o tr.json -f English -t Turkish -p gemini
|
|
110
141
|
```
|
|
111
142
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
To create a production build:
|
|
143
|
+
Build a single-file binary:
|
|
115
144
|
|
|
116
145
|
```bash
|
|
117
146
|
deno task build
|
|
118
147
|
```
|
|
119
148
|
|
|
120
|
-
|
|
149
|
+
Build npm package:
|
|
121
150
|
|
|
122
|
-
|
|
151
|
+
```bash
|
|
152
|
+
deno task build:npm
|
|
153
|
+
```
|
|
123
154
|
|
|
124
155
|
## License
|
|
125
156
|
|
|
126
|
-
MIT
|
|
157
|
+
MIT © 2026
|
package/esm/cli.js
CHANGED
|
@@ -3,8 +3,9 @@ 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,
|
|
7
|
-
import {
|
|
6
|
+
import { getImportJson, resolvePath, writeOutput } from './src/file.js';
|
|
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
9
|
import { formatBytes, validateArgs } from './src/utilites.js';
|
|
9
10
|
import { logger } from './src/logger.js';
|
|
10
11
|
import denoJson from './deno.js';
|
|
@@ -12,11 +13,23 @@ import Gemini from './src/providers/gemini.js';
|
|
|
12
13
|
import OpenAIModel from './src/providers/openai.js';
|
|
13
14
|
import AnthropicModel from './src/providers/anthropic.js';
|
|
14
15
|
const spinner = new Spinner({ message: 'AI Thinks...', color: 'cyan' });
|
|
16
|
+
const createTranslator = (args, options) => {
|
|
17
|
+
switch (args.provider) {
|
|
18
|
+
case 'gemini':
|
|
19
|
+
return new Gemini(args.key, args.model, options);
|
|
20
|
+
case 'openai':
|
|
21
|
+
return new OpenAIModel(args.key, args.url, args.model, options);
|
|
22
|
+
case 'anthropic':
|
|
23
|
+
return new AnthropicModel(args.key, args.url, args.model, options);
|
|
24
|
+
default:
|
|
25
|
+
throw new Error(`Unknown provider: ${args.provider}`);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
15
28
|
async function main() {
|
|
16
29
|
try {
|
|
17
30
|
const args = parseArgs(dntShim.Deno.args, {
|
|
18
|
-
string: ['key', 'provider', 'model', 'input', 'output', 'from', 'to', 'url'],
|
|
19
|
-
boolean: ['help', 'version'],
|
|
31
|
+
string: ['key', 'provider', 'model', 'input', 'output', 'from', 'to', 'url', 'max-batch-size'],
|
|
32
|
+
boolean: ['help', 'version', 'no-limit', 'no-timeout'],
|
|
20
33
|
alias: {
|
|
21
34
|
provider: 'p',
|
|
22
35
|
model: 'm',
|
|
@@ -33,8 +46,7 @@ async function main() {
|
|
|
33
46
|
const help = args.help || dntShim.Deno.args.length === 0;
|
|
34
47
|
const version = args.version;
|
|
35
48
|
if (version) {
|
|
36
|
-
|
|
37
|
-
logger.info('Glotto version: ' + VERSION);
|
|
49
|
+
logger.info('Glotto version: ' + denoJson.version);
|
|
38
50
|
dntShim.Deno.exit(0);
|
|
39
51
|
}
|
|
40
52
|
if (help) {
|
|
@@ -43,49 +55,45 @@ async function main() {
|
|
|
43
55
|
}
|
|
44
56
|
const validatedArgs = validateArgs(args);
|
|
45
57
|
const fileContent = await getImportJson(validatedArgs.input);
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
58
|
+
const allLeaves = extractLeaves(fileContent);
|
|
59
|
+
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);
|
|
50
62
|
logger.info('Provider: ', validatedArgs.provider);
|
|
51
63
|
logger.info('Input: ', validatedArgs.input);
|
|
52
64
|
logger.info('Output: ', validatedArgs.output);
|
|
53
65
|
logger.info('From: ', validatedArgs.from);
|
|
54
66
|
logger.info('To: ', validatedArgs.to);
|
|
55
|
-
if (validatedArgs.model)
|
|
67
|
+
if (validatedArgs.model)
|
|
56
68
|
logger.info('Model: ', validatedArgs.model);
|
|
57
|
-
|
|
58
|
-
if (validatedArgs.url) {
|
|
69
|
+
if (validatedArgs.url)
|
|
59
70
|
logger.info('URL: ', validatedArgs.url);
|
|
71
|
+
if (validatedArgs.noLimit)
|
|
72
|
+
logger.info('Rate limit protection: disabled (--no-limit)');
|
|
73
|
+
if (validatedArgs.noTimeout)
|
|
74
|
+
logger.info('Request timeout: disabled (--no-timeout)');
|
|
75
|
+
if (validatedArgs.maxBatchBytes !== DEFAULT_MAX_BATCH_BYTES) {
|
|
76
|
+
logger.info(`Max batch size: ${formatBytes(validatedArgs.maxBatchBytes)}`);
|
|
60
77
|
}
|
|
61
|
-
logger.info(`Total: ${
|
|
62
|
-
for (const
|
|
63
|
-
logger.info(`
|
|
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)}`);
|
|
64
81
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const gemini = new Gemini(validatedArgs.key, chunks, validatedArgs.from, validatedArgs.to, validatedArgs.model);
|
|
70
|
-
result = await gemini.translate();
|
|
71
|
-
break;
|
|
72
|
-
}
|
|
73
|
-
case 'openai': {
|
|
74
|
-
const openai = new OpenAIModel(validatedArgs.key, chunks, validatedArgs.from, validatedArgs.to, validatedArgs.url, validatedArgs.model);
|
|
75
|
-
result = await openai.translate();
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
case 'anthropic': {
|
|
79
|
-
const anthropic = new AnthropicModel(validatedArgs.key, chunks, validatedArgs.from, validatedArgs.to, validatedArgs.url, validatedArgs.model);
|
|
80
|
-
result = await anthropic.translate();
|
|
81
|
-
break;
|
|
82
|
-
}
|
|
83
|
-
default: {
|
|
84
|
-
logger.warn('Provider not found');
|
|
85
|
-
break;
|
|
86
|
-
}
|
|
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));
|
|
85
|
+
dntShim.Deno.exit(0);
|
|
87
86
|
}
|
|
88
|
-
|
|
87
|
+
const translateOptions = {
|
|
88
|
+
noLimit: validatedArgs.noLimit,
|
|
89
|
+
noTimeout: validatedArgs.noTimeout,
|
|
90
|
+
};
|
|
91
|
+
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));
|
|
89
97
|
spinner.stop();
|
|
90
98
|
logger.success('Translation completed');
|
|
91
99
|
}
|
package/esm/deno.d.ts
CHANGED
package/esm/deno.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export default {
|
|
2
2
|
"name": "@ibodev/glotto",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"exports": "./cli.ts",
|
|
5
5
|
"lock": false,
|
|
6
6
|
"nodeModulesDir": "auto",
|
|
@@ -23,7 +23,6 @@ export default {
|
|
|
23
23
|
"@google/genai": "npm:@google/genai@^1.52.0",
|
|
24
24
|
"@openai/openai": "npm:openai@^6.36.0",
|
|
25
25
|
"@std/cli": "jsr:@std/cli@^1.0.29",
|
|
26
|
-
"@std/encoding": "jsr:@std/encoding@^1.0.10",
|
|
27
26
|
"@std/path": "jsr:@std/path@^1.1.4",
|
|
28
27
|
"consola": "npm:consola@^3.4.2"
|
|
29
28
|
},
|
package/esm/src/contants.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { Provider } from './types.js';
|
|
2
2
|
export declare const DEFAULT_PROVIDER: Provider;
|
|
3
3
|
export declare const DEFAULT_MODELS: Record<Provider, string>;
|
|
4
|
-
export declare const
|
|
4
|
+
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
|
-
export declare const
|
|
7
|
+
export declare const INTER_BATCH_DELAY_MS: 1500;
|
|
8
8
|
export declare const HELP_TEXT: string;
|
|
9
9
|
//# 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,SAAS,QAgCrB,CAAC"}
|
package/esm/src/contants.js
CHANGED
|
@@ -4,30 +4,40 @@ export const DEFAULT_MODELS = {
|
|
|
4
4
|
openai: 'gpt-4.1-mini',
|
|
5
5
|
anthropic: 'claude-3-5-haiku-latest',
|
|
6
6
|
};
|
|
7
|
-
export const
|
|
7
|
+
export const DEFAULT_MAX_BATCH_BYTES = 12_000;
|
|
8
8
|
export const MAX_RETRIES = 3;
|
|
9
9
|
export const BASE_RETRY_DELAY_MS = 2_000;
|
|
10
|
-
export const
|
|
10
|
+
export const INTER_BATCH_DELAY_MS = 1_500;
|
|
11
11
|
export const HELP_TEXT = `
|
|
12
12
|
Glotto AI Translator
|
|
13
13
|
-------------------
|
|
14
14
|
A tool for translating i18n JSON files using AI services.
|
|
15
15
|
|
|
16
|
+
Glotto walks the input JSON, extracts every string leaf with its path, sends them to the
|
|
17
|
+
chosen provider as plain-text batches (using ≪id≫value≪/id≫ tagged entries), and
|
|
18
|
+
reconstructs the JSON from the responses. JSON structure, keys, variables and HTML tags
|
|
19
|
+
are preserved by the tool itself — the model only sees and produces text.
|
|
20
|
+
|
|
16
21
|
Options:
|
|
17
|
-
--key
|
|
18
|
-
-p, --provider
|
|
19
|
-
-m, --model
|
|
20
|
-
-i, --input
|
|
21
|
-
-o, --output
|
|
22
|
-
-f, --from
|
|
23
|
-
-t, --to
|
|
24
|
-
--url
|
|
25
|
-
-
|
|
26
|
-
-
|
|
22
|
+
--key API key for the AI service (required)
|
|
23
|
+
-p, --provider AI translation provider to use (default: ${DEFAULT_PROVIDER})
|
|
24
|
+
-m, --model Model name for the selected provider (optional)
|
|
25
|
+
-i, --input Path to source JSON file (required)
|
|
26
|
+
-o, --output Path to target JSON file (required)
|
|
27
|
+
-f, --from Source language (required)
|
|
28
|
+
-t, --to Target language (required)
|
|
29
|
+
--url Custom base URL for OpenAI/Anthropic (optional)
|
|
30
|
+
--no-limit Disable rate limit delay between batches
|
|
31
|
+
--no-timeout Disable request timeout (wait indefinitely for AI response)
|
|
32
|
+
--max-batch-size Maximum source size per batch, in KB (default: ${DEFAULT_MAX_BATCH_BYTES / 1024} KB)
|
|
33
|
+
-h, --help Display this help message
|
|
34
|
+
-v, --version Display version
|
|
27
35
|
|
|
28
36
|
Examples:
|
|
29
37
|
glotto --key {{key}} --input=en.json --output=tr.json --from=english --to=turkish
|
|
30
38
|
glotto --key {{key}} -i en.json -o tr.json -f english -t turkish -p gemini
|
|
31
39
|
glotto --key {{key}} -i en.json -o tr.json -f english -t turkish -p openai
|
|
32
40
|
glotto --key {{key}} -i en.json -o tr.json -f english -t turkish -p anthropic
|
|
41
|
+
glotto --key {{key}} -i en.json -o tr.json -f english -t turkish --no-limit --no-timeout
|
|
42
|
+
glotto --key {{key}} -i en.json -o tr.json -f english -t turkish --max-batch-size 8
|
|
33
43
|
`;
|
package/esm/src/file.d.ts
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { JsonValue } from './types.js';
|
|
2
2
|
export declare const resolvePath: (...paths: string[]) => string;
|
|
3
|
-
export declare const getImportJson: <T =
|
|
3
|
+
export declare const getImportJson: <T = JsonValue>(input: string) => Promise<T>;
|
|
4
4
|
export declare const writeOutput: (outputPath: string, content: string) => Promise<void>;
|
|
5
|
-
export declare const existsFile: (path: string) => Promise<boolean>;
|
|
6
|
-
export declare const ensureDirectoryExists: (directory: string) => Promise<void>;
|
|
7
|
-
export declare const splitJson: (data: JsonObject, maxChunkBytes?: number) => ChunkInfo[];
|
|
8
|
-
export declare const mergeInputs: (inputs: JsonObject[]) => JsonObject;
|
|
9
|
-
export declare const writeTemp: (targetLanguage: string, tempJsonFileName: string, content: string) => Promise<void>;
|
|
10
5
|
//# sourceMappingURL=file.d.ts.map
|
package/esm/src/file.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/src/file.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,
|
|
1
|
+
{"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/src/file.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,eAAO,MAAM,WAAW,GAAI,GAAG,OAAO,MAAM,EAAE,KAAG,MAA4C,CAAC;AAE9F,eAAO,MAAM,aAAa,GAAU,CAAC,GAAG,SAAS,EAAE,OAAO,MAAM,KAAG,OAAO,CAAC,CAAC,CAO3E,CAAC;AAEF,eAAO,MAAM,WAAW,GAAU,YAAY,MAAM,EAAE,SAAS,MAAM,KAAG,OAAO,CAAC,IAAI,CAEnF,CAAC"}
|
package/esm/src/file.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import * as dntShim from "../_dnt.shims.js";
|
|
2
2
|
import { join } from '../deps/jsr.io/@std/path/1.1.4/mod.js';
|
|
3
|
-
|
|
4
|
-
export const resolvePath = (...paths) => {
|
|
5
|
-
return join(dntShim.Deno.cwd(), ...paths);
|
|
6
|
-
};
|
|
3
|
+
export const resolvePath = (...paths) => join(dntShim.Deno.cwd(), ...paths);
|
|
7
4
|
export const getImportJson = async (input) => {
|
|
8
5
|
const filePath = resolvePath(input);
|
|
9
6
|
const fileContent = await dntShim.Deno.readTextFile(filePath);
|
|
@@ -15,107 +12,3 @@ export const getImportJson = async (input) => {
|
|
|
15
12
|
export const writeOutput = async (outputPath, content) => {
|
|
16
13
|
await dntShim.Deno.writeTextFile(outputPath, content, { create: true });
|
|
17
14
|
};
|
|
18
|
-
export const existsFile = async (path) => {
|
|
19
|
-
try {
|
|
20
|
-
await dntShim.Deno.stat(path);
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
catch (error) {
|
|
24
|
-
if (error instanceof dntShim.Deno.errors.NotFound) {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
throw error;
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
export const ensureDirectoryExists = async (directory) => {
|
|
31
|
-
const directoryExists = await existsFile(directory);
|
|
32
|
-
if (!directoryExists) {
|
|
33
|
-
await dntShim.Deno.mkdir(directory, { recursive: true });
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
const getByteSize = (str) => {
|
|
37
|
-
return new TextEncoder().encode(str).byteLength;
|
|
38
|
-
};
|
|
39
|
-
const buildChunkWithSize = (data, keys) => {
|
|
40
|
-
const chunk = Object.create(null);
|
|
41
|
-
for (const key of keys) {
|
|
42
|
-
chunk[key] = data[key];
|
|
43
|
-
}
|
|
44
|
-
const json = JSON.stringify(chunk);
|
|
45
|
-
return { json, size: getByteSize(json) };
|
|
46
|
-
};
|
|
47
|
-
export const splitJson = (data, maxChunkBytes = 30_000) => {
|
|
48
|
-
const encoder = new TextEncoder();
|
|
49
|
-
const dataKeys = Object.keys(data);
|
|
50
|
-
const totalKeys = dataKeys.length;
|
|
51
|
-
const fullJson = JSON.stringify(data);
|
|
52
|
-
const totalBytes = getByteSize(fullJson);
|
|
53
|
-
if (totalBytes <= maxChunkBytes) {
|
|
54
|
-
return [
|
|
55
|
-
{
|
|
56
|
-
data: encoder.encode(fullJson),
|
|
57
|
-
keyCount: totalKeys,
|
|
58
|
-
byteSize: totalBytes,
|
|
59
|
-
index: 0,
|
|
60
|
-
},
|
|
61
|
-
];
|
|
62
|
-
}
|
|
63
|
-
const chunks = [];
|
|
64
|
-
let currentKeys = [];
|
|
65
|
-
let currentSize = 2;
|
|
66
|
-
for (let i = 0; i < totalKeys; i++) {
|
|
67
|
-
const key = dataKeys[i];
|
|
68
|
-
const keyValueJson = JSON.stringify({ [key]: data[key] });
|
|
69
|
-
const entrySize = getByteSize(keyValueJson) - 2;
|
|
70
|
-
const separatorSize = currentKeys.length > 0 ? 1 : 0;
|
|
71
|
-
const projectedSize = currentSize + entrySize + separatorSize;
|
|
72
|
-
if (projectedSize > maxChunkBytes && currentKeys.length > 0) {
|
|
73
|
-
const { json, size } = buildChunkWithSize(data, currentKeys);
|
|
74
|
-
chunks.push({
|
|
75
|
-
data: encoder.encode(json),
|
|
76
|
-
keyCount: currentKeys.length,
|
|
77
|
-
byteSize: size,
|
|
78
|
-
index: chunks.length,
|
|
79
|
-
});
|
|
80
|
-
currentKeys = [key];
|
|
81
|
-
currentSize = 2 + entrySize;
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
currentKeys.push(key);
|
|
85
|
-
currentSize = projectedSize;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
if (currentKeys.length > 0) {
|
|
89
|
-
const { json, size } = buildChunkWithSize(data, currentKeys);
|
|
90
|
-
chunks.push({
|
|
91
|
-
data: encoder.encode(json),
|
|
92
|
-
keyCount: currentKeys.length,
|
|
93
|
-
byteSize: size,
|
|
94
|
-
index: chunks.length,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
return chunks;
|
|
98
|
-
};
|
|
99
|
-
export const mergeInputs = (inputs) => {
|
|
100
|
-
return Object.assign({}, ...inputs);
|
|
101
|
-
};
|
|
102
|
-
const getOsTempDir = () => {
|
|
103
|
-
if (dntShim.Deno.build.os === 'windows') {
|
|
104
|
-
return dntShim.Deno.env.get('TEMP') ?? dntShim.Deno.env.get('TMP') ?? 'C:\\Windows\\Temp';
|
|
105
|
-
}
|
|
106
|
-
return dntShim.Deno.env.get('TMPDIR') ?? '/tmp';
|
|
107
|
-
};
|
|
108
|
-
const resolveTempDir = async (targetLanguage) => {
|
|
109
|
-
const tempDir = getOsTempDir();
|
|
110
|
-
const glottoTempDir = join(tempDir, 'glotto');
|
|
111
|
-
await ensureDirectoryExists(glottoTempDir);
|
|
112
|
-
const languageTempDir = join(glottoTempDir, targetLanguage);
|
|
113
|
-
await ensureDirectoryExists(languageTempDir);
|
|
114
|
-
return languageTempDir;
|
|
115
|
-
};
|
|
116
|
-
export const writeTemp = async (targetLanguage, tempJsonFileName, content) => {
|
|
117
|
-
const tempDir = await resolveTempDir(targetLanguage);
|
|
118
|
-
const tempJsonFilePath = join(tempDir, tempJsonFileName);
|
|
119
|
-
await dntShim.Deno.writeTextFile(tempJsonFilePath, content, { create: true });
|
|
120
|
-
logger.info(`Temp saved: ${tempJsonFilePath}`);
|
|
121
|
-
};
|
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
client: Anthropic;
|
|
8
|
-
model: string;
|
|
9
|
-
constructor(key: string, chunks: ChunkInfo[], from: string, to: string, baseUrl?: string, modelName?: string);
|
|
10
|
-
private translateChunk;
|
|
11
|
-
translate(): Promise<string>;
|
|
1
|
+
import type { TextTranslator, TranslateOptions } from '../types.js';
|
|
2
|
+
declare class AnthropicModel implements TextTranslator {
|
|
3
|
+
private client;
|
|
4
|
+
private model;
|
|
5
|
+
constructor(key: string, baseUrl?: string, modelName?: string, options?: TranslateOptions);
|
|
6
|
+
translate(prompt: string): Promise<string>;
|
|
12
7
|
}
|
|
13
8
|
export default AnthropicModel;
|
|
14
9
|
//# sourceMappingURL=anthropic.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anthropic.d.ts","sourceRoot":"","sources":["../../../src/src/providers/anthropic.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"anthropic.d.ts","sourceRoot":"","sources":["../../../src/src/providers/anthropic.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGpE,cAAM,cAAe,YAAW,cAAc;IAC5C,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,KAAK,CAAS;gBAGpB,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,MAAM,EAChB,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,GAAE,gBAAuD;IAU5D,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAYjD;AAED,eAAe,cAAc,CAAC"}
|