rosetta-i18n 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -0
- package/dist/esm/cli.js +378 -0
- package/dist/esm/cli.js.map +1 -0
- package/dist/esm/next.js +208 -0
- package/dist/esm/next.js.map +1 -0
- package/dist/types/cli.d.ts +47 -0
- package/dist/types/next.d.ts +30 -0
- package/package.json +17 -2
package/README.md
CHANGED
|
@@ -101,6 +101,151 @@ const out = await rosetta.translate(
|
|
|
101
101
|
Translates a single string. Throws on a non-OK response (unlike `translate`,
|
|
102
102
|
which degrades gracefully at the batch level).
|
|
103
103
|
|
|
104
|
+
## CLI
|
|
105
|
+
|
|
106
|
+
Translate a JSON catalog from the command line — run it in CI or as a release
|
|
107
|
+
step. It reads a source catalog, translates it, and writes one file per target
|
|
108
|
+
locale.
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
OPENROUTER_API_KEY=sk-... npx rosetta-i18n translate-catalog messages/en.json \
|
|
112
|
+
--target es --target pt-BR \
|
|
113
|
+
--merge \
|
|
114
|
+
--context "Next.js UI catalog"
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Writes `messages/es.json` and `messages/pt-BR.json` (flattened keys preserved).
|
|
118
|
+
|
|
119
|
+
| Flag | Description |
|
|
120
|
+
| --- | --- |
|
|
121
|
+
| `--target <locale>` | Target locale (repeatable, required) |
|
|
122
|
+
| `--source <locale>` | Source locale (default `en`) |
|
|
123
|
+
| `--out <dir>` | Output directory (default: the input file's directory) |
|
|
124
|
+
| `--merge` | Only translate keys missing from an existing `<target>.json` |
|
|
125
|
+
| `--context <text>` | Broad context passed to the model |
|
|
126
|
+
| `--brand-voice <text>` | Brand voice briefing (default `ROSETTA_BRAND_VOICE`) |
|
|
127
|
+
| `--glossary <file>` | JSON file of per-locale exact term mappings |
|
|
128
|
+
| `--model <id>` / `--base-url <url>` | Override the endpoint/model |
|
|
129
|
+
| `--concurrency` / `--batch-size` / `--retries` | Tune batching |
|
|
130
|
+
| `--dry-run` | Print the plan without calling the model |
|
|
131
|
+
|
|
132
|
+
`--merge` is the CI-friendly mode: add keys to `en.json`, rerun, and only the new
|
|
133
|
+
keys hit the model — existing translations are preserved for diffing and review.
|
|
134
|
+
|
|
135
|
+
## React & Next.js
|
|
136
|
+
|
|
137
|
+
Rosetta calls an LLM with your API key, so it runs **server-side only**. Never
|
|
138
|
+
import it in a Client Component (`"use client"`) — bundle the key out with
|
|
139
|
+
[`server-only`](https://www.npmjs.com/package/server-only) and expose
|
|
140
|
+
translations through server code.
|
|
141
|
+
|
|
142
|
+
### Server helpers (`rosetta-i18n/next`)
|
|
143
|
+
|
|
144
|
+
The `next` entrypoint (optional `next` peer) wires env config, a memoized
|
|
145
|
+
instance, and Next's data cache:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
// lib/rosetta.ts
|
|
149
|
+
import "server-only";
|
|
150
|
+
import { cachedTranslate, getRosetta } from "rosetta-i18n/next";
|
|
151
|
+
|
|
152
|
+
// Memoized instance built from env (see table below).
|
|
153
|
+
export const rosetta = getRosetta();
|
|
154
|
+
|
|
155
|
+
// Cached translation — keyed on source/target/payload.
|
|
156
|
+
export async function translateCopy(
|
|
157
|
+
data: Record<string, string>,
|
|
158
|
+
target: string,
|
|
159
|
+
) {
|
|
160
|
+
return cachedTranslate(data, { source: "en", target }, {
|
|
161
|
+
revalidate: 60 * 60 * 24,
|
|
162
|
+
tags: ["i18n"],
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
| Env var | Required | Default |
|
|
168
|
+
| --- | --- | --- |
|
|
169
|
+
| `OPENROUTER_API_KEY` | yes | — |
|
|
170
|
+
| `ROSETTA_MODEL` | no | `anthropic/claude-sonnet-4.5` |
|
|
171
|
+
| `ROSETTA_BASE_URL` | no | `https://openrouter.ai/api/v1` |
|
|
172
|
+
| `ROSETTA_BRAND_VOICE` | no | — (the `*` brand voice) |
|
|
173
|
+
|
|
174
|
+
`createRosetta(overrides)` is also exported if you'd rather pass config
|
|
175
|
+
explicitly.
|
|
176
|
+
|
|
177
|
+
### Recommended: pre-translate message catalogs with the CLI
|
|
178
|
+
|
|
179
|
+
For `next-intl` / `react-i18next`, translate the locale JSON once and ship it —
|
|
180
|
+
no LLM call in the request path. Use the [CLI](#cli) above, then load the files:
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
// i18n/request.ts (next-intl)
|
|
184
|
+
import { getRequestConfig } from "next-intl/server";
|
|
185
|
+
|
|
186
|
+
export default getRequestConfig(async ({ locale }) => ({
|
|
187
|
+
messages: (await import(`../messages/${locale}.json`)).default,
|
|
188
|
+
}));
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Server Component (RSC)
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
// app/[locale]/hero.tsx
|
|
195
|
+
import { getRosetta } from "rosetta-i18n/next";
|
|
196
|
+
|
|
197
|
+
export async function Hero({ locale }: { locale: string }) {
|
|
198
|
+
const copy = await getRosetta().translate(
|
|
199
|
+
{ hero: "Every awarded restaurant in the world" },
|
|
200
|
+
{ source: "en", target: locale, context: "home hero" },
|
|
201
|
+
);
|
|
202
|
+
return <h1>{String(copy.hero)}</h1>;
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Prefer `cachedTranslate` from `rosetta-i18n/next` (above) or React's `cache` so
|
|
207
|
+
repeated renders reuse the result instead of re-calling the model.
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
### Route Handler
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
// app/api/translate/route.ts
|
|
214
|
+
import { NextResponse } from "next/server";
|
|
215
|
+
import { rosetta } from "@/lib/rosetta";
|
|
216
|
+
|
|
217
|
+
export async function POST(req: Request) {
|
|
218
|
+
const { data, target, context } = await req.json();
|
|
219
|
+
const translated = await rosetta.translate(data, {
|
|
220
|
+
source: "en",
|
|
221
|
+
target,
|
|
222
|
+
context,
|
|
223
|
+
});
|
|
224
|
+
return NextResponse.json(translated);
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Server Action
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
// app/actions.ts
|
|
232
|
+
"use server";
|
|
233
|
+
import { rosetta } from "@/lib/rosetta";
|
|
234
|
+
|
|
235
|
+
export async function translateBlurb(blurb: string, target: string) {
|
|
236
|
+
return rosetta.translateText(blurb, { source: "en", target });
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Client Components
|
|
241
|
+
|
|
242
|
+
Client Components consume already-translated strings via props, context, or the
|
|
243
|
+
catalog — they never import Rosetta. To trigger a translation from the browser,
|
|
244
|
+
call the route handler or Server Action above.
|
|
245
|
+
|
|
246
|
+
> **Edge runtime:** Rosetta only uses `fetch`, so it works on the Edge runtime as
|
|
247
|
+
> long as your endpoint does. Node runtime is recommended for large catalogs.
|
|
248
|
+
|
|
104
249
|
## Releasing
|
|
105
250
|
|
|
106
251
|
Releases publish automatically from GitHub Actions via npm
|
package/dist/esm/cli.js
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { writeFile, readFile } from 'node:fs/promises';
|
|
3
|
+
import { resolve, dirname, join } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
function buildSystemPrompt(options) {
|
|
7
|
+
const { brandVoice, glossary, source, target } = options;
|
|
8
|
+
const voice = brandVoice.variations[target] ?? brandVoice.variations["*"] ?? "";
|
|
9
|
+
const parts = [voice];
|
|
10
|
+
if (glossary) {
|
|
11
|
+
const terms = glossary[target];
|
|
12
|
+
if (terms && Object.keys(terms).length > 0) {
|
|
13
|
+
parts.push(
|
|
14
|
+
`Glossary (use these exact renderings):
|
|
15
|
+
${Object.entries(terms).map(([src, tgt]) => `- "${src}" -> ${tgt}`).join("\n")}`
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
parts.push(
|
|
20
|
+
`Translate from ${source} to ${target}. Return ONLY the translated text \u2014 no explanations, no quotes around it.`
|
|
21
|
+
);
|
|
22
|
+
return parts.filter(Boolean).join("\n\n");
|
|
23
|
+
}
|
|
24
|
+
function buildDataPrompt(data, options) {
|
|
25
|
+
const lines = [
|
|
26
|
+
`Translate the values of this JSON object from ${options.source} to ${options.target}. Keep every key exactly as provided \u2014 return a JSON object with identical keys and translated values. Preserve {placeholders} and ICU plural syntax (=1 {...} other {...}) intact, translating only the inner text. Output ONLY the JSON.`
|
|
27
|
+
];
|
|
28
|
+
if (options.context) {
|
|
29
|
+
lines.push(`Context: ${options.context}`);
|
|
30
|
+
}
|
|
31
|
+
if (options.hints) {
|
|
32
|
+
const hints = Object.entries(options.hints).map(([key, breadcrumb]) => `- ${key}: ${breadcrumb.join(" > ")}`).join("\n");
|
|
33
|
+
if (hints) {
|
|
34
|
+
lines.push(`Key disambiguation:
|
|
35
|
+
${hints}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
lines.push(JSON.stringify(data, null, 1));
|
|
39
|
+
return lines.join("\n\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class Rosetta {
|
|
43
|
+
config;
|
|
44
|
+
constructor(config) {
|
|
45
|
+
if (!config.apiKey) throw new Error("Rosetta: apiKey is required");
|
|
46
|
+
if (!config.model) throw new Error("Rosetta: model is required");
|
|
47
|
+
this.config = config;
|
|
48
|
+
}
|
|
49
|
+
/** Translate a single text string. */
|
|
50
|
+
async translateText(text, options) {
|
|
51
|
+
const system = buildSystemPrompt({
|
|
52
|
+
brandVoice: this.config.brandVoice,
|
|
53
|
+
glossary: this.config.glossary,
|
|
54
|
+
source: options.source,
|
|
55
|
+
target: options.target
|
|
56
|
+
});
|
|
57
|
+
const res = await this.chat([
|
|
58
|
+
{ role: "system", content: system },
|
|
59
|
+
{
|
|
60
|
+
role: "user",
|
|
61
|
+
content: options.context ? `${text}
|
|
62
|
+
|
|
63
|
+
Context: ${options.context}` : text
|
|
64
|
+
}
|
|
65
|
+
]);
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
throw new Error(`Rosetta: ${res.status} ${await res.text()}`);
|
|
68
|
+
}
|
|
69
|
+
const json = await res.json();
|
|
70
|
+
return json.choices[0].message.content.trim();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Translate a key-value payload, batched with concurrency. Failed batches
|
|
74
|
+
* are logged and skipped — the returned object carries only successfully
|
|
75
|
+
* translated keys, letting the caller fall back to the source locale for
|
|
76
|
+
* the rest.
|
|
77
|
+
*/
|
|
78
|
+
async translate(data, options) {
|
|
79
|
+
const batchSize = this.config.batchSize ?? 25;
|
|
80
|
+
const concurrency = this.config.concurrency ?? 4;
|
|
81
|
+
const keys = Object.keys(data);
|
|
82
|
+
const output = {};
|
|
83
|
+
for (let i = 0; i < keys.length; i += batchSize * concurrency) {
|
|
84
|
+
const chunk = keys.slice(i, i + batchSize * concurrency);
|
|
85
|
+
const batches = Array.from(
|
|
86
|
+
{ length: Math.ceil(chunk.length / batchSize) },
|
|
87
|
+
(_, b) => {
|
|
88
|
+
const batchKeys = chunk.slice(b * batchSize, (b + 1) * batchSize);
|
|
89
|
+
const batch = Object.fromEntries(batchKeys.map((k) => [k, data[k]]));
|
|
90
|
+
return { batch, batchKeys };
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
const results = await Promise.allSettled(
|
|
94
|
+
batches.map(
|
|
95
|
+
({ batch, batchKeys }) => this.translateBatch(batch, options, batchKeys)
|
|
96
|
+
)
|
|
97
|
+
);
|
|
98
|
+
for (let b = 0; b < results.length; b++) {
|
|
99
|
+
const result = results[b];
|
|
100
|
+
if (result.status !== "fulfilled") {
|
|
101
|
+
console.error(
|
|
102
|
+
`[rosetta] batch failed (${batches[b].batchKeys.length} keys):`,
|
|
103
|
+
result.reason?.message?.slice(0, 120)
|
|
104
|
+
);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
Object.assign(output, result.value);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return output;
|
|
111
|
+
}
|
|
112
|
+
async translateBatch(batch, options, batchKeys) {
|
|
113
|
+
const retries = this.config.retries ?? 2;
|
|
114
|
+
const system = buildSystemPrompt({
|
|
115
|
+
brandVoice: this.config.brandVoice,
|
|
116
|
+
glossary: this.config.glossary,
|
|
117
|
+
source: options.source,
|
|
118
|
+
target: options.target
|
|
119
|
+
});
|
|
120
|
+
const user = buildDataPrompt(batch, {
|
|
121
|
+
...options,
|
|
122
|
+
hints: options.hints ? Object.fromEntries(
|
|
123
|
+
batchKeys.map((key) => [key, options.hints?.[key] ?? []])
|
|
124
|
+
) : void 0
|
|
125
|
+
});
|
|
126
|
+
let lastError;
|
|
127
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
128
|
+
try {
|
|
129
|
+
const res = await this.chat([
|
|
130
|
+
{ role: "system", content: system },
|
|
131
|
+
{ role: "user", content: user }
|
|
132
|
+
]);
|
|
133
|
+
if (!res.ok) {
|
|
134
|
+
throw new Error(`Rosetta: ${res.status} ${await res.text()}`);
|
|
135
|
+
}
|
|
136
|
+
const json = await res.json();
|
|
137
|
+
return JSON.parse(json.choices[0].message.content);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
140
|
+
if (attempt < retries) {
|
|
141
|
+
await new Promise((ok) => setTimeout(ok, 2e3 * (attempt + 1)));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
throw lastError ?? new Error("Rosetta: batch failed");
|
|
146
|
+
}
|
|
147
|
+
async chat(messages) {
|
|
148
|
+
const baseURL = this.config.baseURL ?? "https://openrouter.ai/api/v1";
|
|
149
|
+
return fetch(`${baseURL}/chat/completions`, {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers: {
|
|
152
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
153
|
+
"Content-Type": "application/json"
|
|
154
|
+
},
|
|
155
|
+
body: JSON.stringify({
|
|
156
|
+
model: this.config.model,
|
|
157
|
+
temperature: this.config.temperature ?? 0.3,
|
|
158
|
+
messages
|
|
159
|
+
})
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const HELP = /* @__PURE__ */ Symbol("help");
|
|
165
|
+
const COMMAND = "translate-catalog";
|
|
166
|
+
function takeValue(args, index, flag) {
|
|
167
|
+
const value = args[index];
|
|
168
|
+
if (value === void 0) throw new Error(`Missing value for ${flag}`);
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
function toInt(value, flag) {
|
|
172
|
+
const parsed = Number.parseInt(value, 10);
|
|
173
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
174
|
+
throw new Error(`${flag} expects a non-negative integer, got "${value}"`);
|
|
175
|
+
}
|
|
176
|
+
return parsed;
|
|
177
|
+
}
|
|
178
|
+
function parseArgs(argv) {
|
|
179
|
+
const args = [...argv];
|
|
180
|
+
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
181
|
+
return HELP;
|
|
182
|
+
}
|
|
183
|
+
const command = args.shift();
|
|
184
|
+
if (command !== COMMAND) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`Unknown command "${command}". Try \`rosetta-i18n --help\`.`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
let input;
|
|
190
|
+
const targets = [];
|
|
191
|
+
const options = { source: "en" };
|
|
192
|
+
for (let i = 0; i < args.length; i++) {
|
|
193
|
+
const arg = args[i];
|
|
194
|
+
switch (arg) {
|
|
195
|
+
case "--target":
|
|
196
|
+
targets.push(takeValue(args, ++i, arg));
|
|
197
|
+
break;
|
|
198
|
+
case "--source":
|
|
199
|
+
options.source = takeValue(args, ++i, arg);
|
|
200
|
+
break;
|
|
201
|
+
case "--out":
|
|
202
|
+
options.outDir = resolve(takeValue(args, ++i, arg));
|
|
203
|
+
break;
|
|
204
|
+
case "--context":
|
|
205
|
+
options.context = takeValue(args, ++i, arg);
|
|
206
|
+
break;
|
|
207
|
+
case "--brand-voice":
|
|
208
|
+
options.brandVoice = takeValue(args, ++i, arg);
|
|
209
|
+
break;
|
|
210
|
+
case "--glossary":
|
|
211
|
+
options.glossaryPath = resolve(takeValue(args, ++i, arg));
|
|
212
|
+
break;
|
|
213
|
+
case "--model":
|
|
214
|
+
options.model = takeValue(args, ++i, arg);
|
|
215
|
+
break;
|
|
216
|
+
case "--base-url":
|
|
217
|
+
options.baseURL = takeValue(args, ++i, arg);
|
|
218
|
+
break;
|
|
219
|
+
case "--concurrency":
|
|
220
|
+
options.concurrency = toInt(takeValue(args, ++i, arg), arg);
|
|
221
|
+
break;
|
|
222
|
+
case "--batch-size":
|
|
223
|
+
options.batchSize = toInt(takeValue(args, ++i, arg), arg);
|
|
224
|
+
break;
|
|
225
|
+
case "--retries":
|
|
226
|
+
options.retries = toInt(takeValue(args, ++i, arg), arg);
|
|
227
|
+
break;
|
|
228
|
+
case "--dry-run":
|
|
229
|
+
options.dryRun = true;
|
|
230
|
+
break;
|
|
231
|
+
case "--merge":
|
|
232
|
+
options.merge = true;
|
|
233
|
+
break;
|
|
234
|
+
default:
|
|
235
|
+
if (arg.startsWith("-")) throw new Error(`Unknown flag "${arg}".`);
|
|
236
|
+
if (input) throw new Error(`Unexpected argument "${arg}".`);
|
|
237
|
+
input = resolve(arg);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (!input) throw new Error("Missing input catalog path.");
|
|
241
|
+
if (targets.length === 0) {
|
|
242
|
+
throw new Error("At least one --target <locale> is required.");
|
|
243
|
+
}
|
|
244
|
+
options.input = input;
|
|
245
|
+
options.targets = targets;
|
|
246
|
+
options.outDir = options.outDir ?? dirname(input);
|
|
247
|
+
return options;
|
|
248
|
+
}
|
|
249
|
+
async function readJson(path) {
|
|
250
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
251
|
+
}
|
|
252
|
+
async function readJsonIfExists(path) {
|
|
253
|
+
try {
|
|
254
|
+
return await readJson(path);
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (error.code === "ENOENT") return {};
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function missingKeys(input, existing) {
|
|
261
|
+
return Object.fromEntries(
|
|
262
|
+
Object.entries(input).filter(([key]) => !(key in existing))
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
async function runCatalog(options, client) {
|
|
266
|
+
const input = await readJson(options.input);
|
|
267
|
+
const keys = Object.keys(input).length;
|
|
268
|
+
if (options.dryRun) {
|
|
269
|
+
console.log(
|
|
270
|
+
`[rosetta] dry run \u2014 ${keys} keys ${options.source} \u2192 ${options.targets.join(", ")}`
|
|
271
|
+
);
|
|
272
|
+
console.log(`[rosetta] would write to ${options.outDir}/`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (!client) throw new Error("A translate client is required.");
|
|
276
|
+
for (const target of options.targets) {
|
|
277
|
+
const outputPath = join(options.outDir, `${target}.json`);
|
|
278
|
+
const existing = options.merge ? await readJsonIfExists(outputPath) : {};
|
|
279
|
+
const payload = options.merge ? missingKeys(input, existing) : input;
|
|
280
|
+
const payloadKeys = Object.keys(payload);
|
|
281
|
+
if (payloadKeys.length === 0) {
|
|
282
|
+
console.log(`[rosetta] ${target} \u2014 up to date, skipping`);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
const translated = await client.translate(payload, {
|
|
286
|
+
source: options.source,
|
|
287
|
+
target,
|
|
288
|
+
context: options.context
|
|
289
|
+
});
|
|
290
|
+
const result = options.merge ? { ...existing, ...translated } : translated;
|
|
291
|
+
const written = Object.keys(translated).length;
|
|
292
|
+
await writeFile(outputPath, `${JSON.stringify(result, null, 2)}
|
|
293
|
+
`);
|
|
294
|
+
console.log(
|
|
295
|
+
`[rosetta] ${target} \u2192 ${outputPath} (${written}/${payloadKeys.length} keys)`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function buildClient(options) {
|
|
300
|
+
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
301
|
+
if (!apiKey) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
"OPENROUTER_API_KEY is not set (pass it in the environment, or use --dry-run)."
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
let glossary;
|
|
307
|
+
if (options.glossaryPath) {
|
|
308
|
+
glossary = JSON.parse(
|
|
309
|
+
await readFile(options.glossaryPath, "utf8")
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
return new Rosetta({
|
|
313
|
+
apiKey,
|
|
314
|
+
model: options.model ?? process.env.ROSETTA_MODEL ?? "anthropic/claude-sonnet-4.5",
|
|
315
|
+
baseURL: options.baseURL ?? process.env.ROSETTA_BASE_URL,
|
|
316
|
+
brandVoice: {
|
|
317
|
+
variations: {
|
|
318
|
+
"*": options.brandVoice ?? process.env.ROSETTA_BRAND_VOICE ?? ""
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
glossary,
|
|
322
|
+
batchSize: options.batchSize,
|
|
323
|
+
concurrency: options.concurrency,
|
|
324
|
+
retries: options.retries
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
const HELP_TEXT = `rosetta-i18n \u2014 translate a JSON catalog with an LLM
|
|
328
|
+
|
|
329
|
+
Usage:
|
|
330
|
+
rosetta-i18n translate-catalog <input.json> --target <locale> [--target <locale>...] [options]
|
|
331
|
+
|
|
332
|
+
Options:
|
|
333
|
+
--target <locale> Target locale (repeatable, required)
|
|
334
|
+
--source <locale> Source locale (default: en)
|
|
335
|
+
--out <dir> Output directory (default: the input file's directory)
|
|
336
|
+
--merge Only translate keys missing from existing <target>.json
|
|
337
|
+
--context <text> Broad context passed to the model
|
|
338
|
+
--brand-voice <text> Brand voice briefing (default: ROSETTA_BRAND_VOICE)
|
|
339
|
+
--glossary <file> JSON file of per-locale exact term mappings
|
|
340
|
+
--model <id> Model id (default: ROSETTA_MODEL or claude-sonnet-4.5)
|
|
341
|
+
--base-url <url> OpenAI-compatible endpoint (default: ROSETTA_BASE_URL)
|
|
342
|
+
--concurrency <n> Parallel batches in flight
|
|
343
|
+
--batch-size <n> Keys per request
|
|
344
|
+
--retries <n> Retries per batch
|
|
345
|
+
--dry-run Print the plan without calling the model
|
|
346
|
+
-h, --help Show this help
|
|
347
|
+
|
|
348
|
+
Example:
|
|
349
|
+
OPENROUTER_API_KEY=sk-... rosetta-i18n translate-catalog messages/en.json \\
|
|
350
|
+
--target es --target pt-BR --merge --context "Next.js UI catalog"
|
|
351
|
+
`;
|
|
352
|
+
function printHelp() {
|
|
353
|
+
console.log(HELP_TEXT);
|
|
354
|
+
}
|
|
355
|
+
async function main() {
|
|
356
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
357
|
+
if (parsed === HELP) {
|
|
358
|
+
printHelp();
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (parsed.dryRun) {
|
|
362
|
+
await runCatalog(parsed);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const client = await buildClient(parsed);
|
|
366
|
+
await runCatalog(parsed, client);
|
|
367
|
+
}
|
|
368
|
+
const isMain = process.argv[1] !== void 0 && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
369
|
+
if (isMain) {
|
|
370
|
+
main().catch((error) => {
|
|
371
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
372
|
+
console.error(`[rosetta] ${message}`);
|
|
373
|
+
process.exit(1);
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export { HELP, buildClient, missingKeys, parseArgs, printHelp, runCatalog };
|
|
378
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sources":["../../src/prompt.ts","../../src/index.ts","../../src/cli.ts"],"sourcesContent":["import type { BrandVoice, Glossary, TranslateDataOptions } from \"./config\";\n\n/**\n * Builds the system prompt for a translation request: brand voice, glossary\n * enforcement, locale conventions, and output-shape rules.\n */\nexport function buildSystemPrompt(options: {\n\tbrandVoice: BrandVoice;\n\tglossary?: Glossary;\n\tsource: string;\n\ttarget: string;\n}): string {\n\tconst { brandVoice, glossary, source, target } = options;\n\n\tconst voice =\n\t\tbrandVoice.variations[target] ?? brandVoice.variations[\"*\"] ?? \"\";\n\n\tconst parts = [voice];\n\n\tif (glossary) {\n\t\tconst terms = glossary[target];\n\t\tif (terms && Object.keys(terms).length > 0) {\n\t\t\tparts.push(\n\t\t\t\t`Glossary (use these exact renderings):\\n${Object.entries(terms)\n\t\t\t\t\t.map(([src, tgt]) => `- \"${src}\" -> ${tgt}`)\n\t\t\t\t\t.join(\"\\n\")}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tparts.push(\n\t\t`Translate from ${source} to ${target}. Return ONLY the translated text — no explanations, no quotes around it.`,\n\t);\n\n\treturn parts.filter(Boolean).join(\"\\n\\n\");\n}\n\n/**\n * Builds the user prompt for a key-value payload translation.\n */\nexport function buildDataPrompt(\n\tdata: Record<string, unknown>,\n\toptions: TranslateDataOptions & { hints?: Record<string, string[]> },\n): string {\n\tconst lines = [\n\t\t`Translate the values of this JSON object from ${options.source} to ${options.target}. Keep every key exactly as provided — return a JSON object with identical keys and translated values. Preserve {placeholders} and ICU plural syntax (=1 {...} other {...}) intact, translating only the inner text. Output ONLY the JSON.`,\n\t];\n\n\tif (options.context) {\n\t\tlines.push(`Context: ${options.context}`);\n\t}\n\n\tif (options.hints) {\n\t\tconst hints = Object.entries(options.hints)\n\t\t\t.map(([key, breadcrumb]) => `- ${key}: ${breadcrumb.join(\" > \")}`)\n\t\t\t.join(\"\\n\");\n\t\tif (hints) {\n\t\t\tlines.push(`Key disambiguation:\\n${hints}`);\n\t\t}\n\t}\n\n\tlines.push(JSON.stringify(data, null, 1));\n\n\treturn lines.join(\"\\n\\n\");\n}\n","import type {\n\tRosettaConfig,\n\tTranslateDataOptions,\n\tTranslateTextOptions,\n} from \"./config\";\nimport { buildDataPrompt, buildSystemPrompt } from \"./prompt\";\n\nexport type {\n\tRosettaConfig,\n\tTranslateDataOptions,\n\tTranslateTextOptions,\n} from \"./config\";\n\ninterface ChatMessage {\n\trole: \"system\" | \"user\" | \"assistant\";\n\tcontent: string;\n}\n\n/**\n * Rosetta — self-hosted AI translation engine.\n *\n * Translates key-value content across locales through any OpenAI-compatible\n * LLM, with brand voice, glossary, and per-locale rules applied on every\n * call. Works with OpenRouter, direct Anthropic/OpenAI, or any\n * OpenAI-compatible endpoint.\n *\n * Non-throwing at the batch level: failed batches are logged and skipped, so\n * partial translations degrade gracefully to the source locale.\n */\nexport class Rosetta {\n\tprivate config: RosettaConfig;\n\n\tconstructor(config: RosettaConfig) {\n\t\tif (!config.apiKey) throw new Error(\"Rosetta: apiKey is required\");\n\t\tif (!config.model) throw new Error(\"Rosetta: model is required\");\n\t\tthis.config = config;\n\t}\n\n\t/** Translate a single text string. */\n\tasync translateText(\n\t\ttext: string,\n\t\toptions: TranslateTextOptions,\n\t): Promise<string> {\n\t\tconst system = buildSystemPrompt({\n\t\t\tbrandVoice: this.config.brandVoice,\n\t\t\tglossary: this.config.glossary,\n\t\t\tsource: options.source,\n\t\t\ttarget: options.target,\n\t\t});\n\n\t\tconst res = await this.chat([\n\t\t\t{ role: \"system\", content: system },\n\t\t\t{\n\t\t\t\trole: \"user\",\n\t\t\t\tcontent: options.context\n\t\t\t\t\t? `${text}\\n\\nContext: ${options.context}`\n\t\t\t\t\t: text,\n\t\t\t},\n\t\t]);\n\n\t\tif (!res.ok) {\n\t\t\tthrow new Error(`Rosetta: ${res.status} ${await res.text()}`);\n\t\t}\n\t\tconst json = (await res.json()) as {\n\t\t\tchoices: Array<{ message: { content: string } }>;\n\t\t};\n\t\treturn json.choices[0].message.content.trim();\n\t}\n\n\t/**\n\t * Translate a key-value payload, batched with concurrency. Failed batches\n\t * are logged and skipped — the returned object carries only successfully\n\t * translated keys, letting the caller fall back to the source locale for\n\t * the rest.\n\t */\n\tasync translate(\n\t\tdata: Record<string, unknown>,\n\t\toptions: TranslateDataOptions,\n\t): Promise<Record<string, unknown>> {\n\t\tconst batchSize = this.config.batchSize ?? 25;\n\t\tconst concurrency = this.config.concurrency ?? 4;\n\t\tconst keys = Object.keys(data);\n\t\tconst output: Record<string, unknown> = {};\n\n\t\tfor (let i = 0; i < keys.length; i += batchSize * concurrency) {\n\t\t\tconst chunk = keys.slice(i, i + batchSize * concurrency);\n\t\t\tconst batches = Array.from(\n\t\t\t\t{ length: Math.ceil(chunk.length / batchSize) },\n\t\t\t\t(_, b) => {\n\t\t\t\t\tconst batchKeys = chunk.slice(b * batchSize, (b + 1) * batchSize);\n\t\t\t\t\tconst batch = Object.fromEntries(batchKeys.map((k) => [k, data[k]]));\n\t\t\t\t\treturn { batch, batchKeys };\n\t\t\t\t},\n\t\t\t);\n\n\t\t\tconst results = await Promise.allSettled(\n\t\t\t\tbatches.map(({ batch, batchKeys }) =>\n\t\t\t\t\tthis.translateBatch(batch, options, batchKeys),\n\t\t\t\t),\n\t\t\t);\n\n\t\t\tfor (let b = 0; b < results.length; b++) {\n\t\t\t\tconst result = results[b];\n\t\t\t\tif (result.status !== \"fulfilled\") {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`[rosetta] batch failed (${batches[b].batchKeys.length} keys):`,\n\t\t\t\t\t\tresult.reason?.message?.slice(0, 120),\n\t\t\t\t\t);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tObject.assign(output, result.value);\n\t\t\t}\n\t\t}\n\n\t\treturn output;\n\t}\n\n\tprivate async translateBatch(\n\t\tbatch: Record<string, unknown>,\n\t\toptions: TranslateDataOptions,\n\t\tbatchKeys: string[],\n\t): Promise<Record<string, unknown>> {\n\t\tconst retries = this.config.retries ?? 2;\n\t\tconst system = buildSystemPrompt({\n\t\t\tbrandVoice: this.config.brandVoice,\n\t\t\tglossary: this.config.glossary,\n\t\t\tsource: options.source,\n\t\t\ttarget: options.target,\n\t\t});\n\n\t\tconst user = buildDataPrompt(batch, {\n\t\t\t...options,\n\t\t\thints: options.hints\n\t\t\t\t? Object.fromEntries(\n\t\t\t\t\t\tbatchKeys.map((key) => [key, options.hints?.[key] ?? []]),\n\t\t\t\t\t)\n\t\t\t\t: undefined,\n\t\t});\n\n\t\tlet lastError: Error | undefined;\n\t\tfor (let attempt = 0; attempt <= retries; attempt++) {\n\t\t\ttry {\n\t\t\t\tconst res = await this.chat([\n\t\t\t\t\t{ role: \"system\", content: system },\n\t\t\t\t\t{ role: \"user\", content: user },\n\t\t\t\t]);\n\t\t\t\tif (!res.ok) {\n\t\t\t\t\tthrow new Error(`Rosetta: ${res.status} ${await res.text()}`);\n\t\t\t\t}\n\t\t\t\tconst json = (await res.json()) as {\n\t\t\t\t\tchoices: Array<{ message: { content: string } }>;\n\t\t\t\t};\n\t\t\t\treturn JSON.parse(json.choices[0].message.content);\n\t\t\t} catch (error) {\n\t\t\t\tlastError = error instanceof Error ? error : new Error(String(error));\n\t\t\t\tif (attempt < retries) {\n\t\t\t\t\tawait new Promise((ok) => setTimeout(ok, 2000 * (attempt + 1)));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tthrow lastError ?? new Error(\"Rosetta: batch failed\");\n\t}\n\n\tprivate async chat(messages: ChatMessage[]): Promise<Response> {\n\t\tconst baseURL = this.config.baseURL ?? \"https://openrouter.ai/api/v1\";\n\t\treturn fetch(`${baseURL}/chat/completions`, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bearer ${this.config.apiKey}`,\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tmodel: this.config.model,\n\t\t\t\ttemperature: this.config.temperature ?? 0.3,\n\t\t\t\tmessages,\n\t\t\t}),\n\t\t});\n\t}\n}\n","import { readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport type { Glossary } from \"./config\";\nimport { Rosetta } from \"./index\";\n\nexport const HELP = Symbol(\"help\");\n\nexport interface CatalogOptions {\n\t/** Path to the source catalog JSON. */\n\tinput: string;\n\t/** Target locales to write. */\n\ttargets: string[];\n\t/** Source locale. */\n\tsource: string;\n\t/** Directory to write `<target>.json` into. Defaults to the input's dir. */\n\toutDir: string;\n\tcontext?: string;\n\tbrandVoice?: string;\n\tglossaryPath?: string;\n\tmodel?: string;\n\tbaseURL?: string;\n\tconcurrency?: number;\n\tbatchSize?: number;\n\tretries?: number;\n\t/** Print the plan without calling the model or writing files. */\n\tdryRun?: boolean;\n\t/** Only translate keys missing from an existing target file. */\n\tmerge?: boolean;\n}\n\n/** Minimal surface the catalog runner needs — the real `Rosetta` or a fake. */\nexport interface TranslateClient {\n\ttranslate(\n\t\tdata: Record<string, unknown>,\n\t\toptions: {\n\t\t\tsource: string;\n\t\t\ttarget: string;\n\t\t\tcontext?: string;\n\t\t},\n\t): Promise<Record<string, unknown>>;\n}\n\nconst COMMAND = \"translate-catalog\";\n\nfunction takeValue(args: string[], index: number, flag: string): string {\n\tconst value = args[index];\n\tif (value === undefined) throw new Error(`Missing value for ${flag}`);\n\treturn value;\n}\n\nfunction toInt(value: string, flag: string): number {\n\tconst parsed = Number.parseInt(value, 10);\n\tif (!Number.isFinite(parsed) || parsed < 0) {\n\t\tthrow new Error(`${flag} expects a non-negative integer, got \"${value}\"`);\n\t}\n\treturn parsed;\n}\n\n/**\n * Parse CLI arguments for `translate-catalog`.\n * Returns `HELP` for `--help`, throws a `UsageError` message otherwise.\n */\nexport function parseArgs(argv: string[]): CatalogOptions | typeof HELP {\n\tconst args = [...argv];\n\tif (args.length === 0 || args.includes(\"--help\") || args.includes(\"-h\")) {\n\t\treturn HELP;\n\t}\n\n\tconst command = args.shift();\n\tif (command !== COMMAND) {\n\t\tthrow new Error(\n\t\t\t`Unknown command \"${command}\". Try \\`rosetta-i18n --help\\`.`,\n\t\t);\n\t}\n\n\tlet input: string | undefined;\n\tconst targets: string[] = [];\n\tconst options: Partial<CatalogOptions> = { source: \"en\" };\n\n\tfor (let i = 0; i < args.length; i++) {\n\t\tconst arg = args[i];\n\t\tswitch (arg) {\n\t\t\tcase \"--target\":\n\t\t\t\ttargets.push(takeValue(args, ++i, arg));\n\t\t\t\tbreak;\n\t\t\tcase \"--source\":\n\t\t\t\toptions.source = takeValue(args, ++i, arg);\n\t\t\t\tbreak;\n\t\t\tcase \"--out\":\n\t\t\t\toptions.outDir = resolve(takeValue(args, ++i, arg));\n\t\t\t\tbreak;\n\t\t\tcase \"--context\":\n\t\t\t\toptions.context = takeValue(args, ++i, arg);\n\t\t\t\tbreak;\n\t\t\tcase \"--brand-voice\":\n\t\t\t\toptions.brandVoice = takeValue(args, ++i, arg);\n\t\t\t\tbreak;\n\t\t\tcase \"--glossary\":\n\t\t\t\toptions.glossaryPath = resolve(takeValue(args, ++i, arg));\n\t\t\t\tbreak;\n\t\t\tcase \"--model\":\n\t\t\t\toptions.model = takeValue(args, ++i, arg);\n\t\t\t\tbreak;\n\t\t\tcase \"--base-url\":\n\t\t\t\toptions.baseURL = takeValue(args, ++i, arg);\n\t\t\t\tbreak;\n\t\t\tcase \"--concurrency\":\n\t\t\t\toptions.concurrency = toInt(takeValue(args, ++i, arg), arg);\n\t\t\t\tbreak;\n\t\t\tcase \"--batch-size\":\n\t\t\t\toptions.batchSize = toInt(takeValue(args, ++i, arg), arg);\n\t\t\t\tbreak;\n\t\t\tcase \"--retries\":\n\t\t\t\toptions.retries = toInt(takeValue(args, ++i, arg), arg);\n\t\t\t\tbreak;\n\t\t\tcase \"--dry-run\":\n\t\t\t\toptions.dryRun = true;\n\t\t\t\tbreak;\n\t\t\tcase \"--merge\":\n\t\t\t\toptions.merge = true;\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tif (arg.startsWith(\"-\")) throw new Error(`Unknown flag \"${arg}\".`);\n\t\t\t\tif (input) throw new Error(`Unexpected argument \"${arg}\".`);\n\t\t\t\tinput = resolve(arg);\n\t\t}\n\t}\n\n\tif (!input) throw new Error(\"Missing input catalog path.\");\n\tif (targets.length === 0) {\n\t\tthrow new Error(\"At least one --target <locale> is required.\");\n\t}\n\n\toptions.input = input;\n\toptions.targets = targets;\n\toptions.outDir = options.outDir ?? dirname(input);\n\treturn options as CatalogOptions;\n}\n\nasync function readJson(path: string): Promise<Record<string, unknown>> {\n\treturn JSON.parse(await readFile(path, \"utf8\")) as Record<string, unknown>;\n}\n\nasync function readJsonIfExists(\n\tpath: string,\n): Promise<Record<string, unknown>> {\n\ttry {\n\t\treturn await readJson(path);\n\t} catch (error) {\n\t\tif ((error as NodeJS.ErrnoException).code === \"ENOENT\") return {};\n\t\tthrow error;\n\t}\n}\n\n/** Keys present in `input` but missing from `existing` (top-level). */\nexport function missingKeys(\n\tinput: Record<string, unknown>,\n\texisting: Record<string, unknown>,\n): Record<string, unknown> {\n\treturn Object.fromEntries(\n\t\tObject.entries(input).filter(([key]) => !(key in existing)),\n\t);\n}\n\n/**\n * Translate a catalog into each target locale and write `<outDir>/<target>.json`.\n * The `client` is only required when not running `--dry-run`.\n */\nexport async function runCatalog(\n\toptions: CatalogOptions,\n\tclient?: TranslateClient,\n): Promise<void> {\n\tconst input = await readJson(options.input);\n\tconst keys = Object.keys(input).length;\n\n\tif (options.dryRun) {\n\t\tconsole.log(\n\t\t\t`[rosetta] dry run — ${keys} keys ${options.source} → ${options.targets.join(\", \")}`,\n\t\t);\n\t\tconsole.log(`[rosetta] would write to ${options.outDir}/`);\n\t\treturn;\n\t}\n\n\tif (!client) throw new Error(\"A translate client is required.\");\n\n\tfor (const target of options.targets) {\n\t\tconst outputPath = join(options.outDir, `${target}.json`);\n\t\tconst existing = options.merge ? await readJsonIfExists(outputPath) : {};\n\n\t\tconst payload = options.merge ? missingKeys(input, existing) : input;\n\t\tconst payloadKeys = Object.keys(payload);\n\n\t\tif (payloadKeys.length === 0) {\n\t\t\tconsole.log(`[rosetta] ${target} — up to date, skipping`);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst translated = await client.translate(payload, {\n\t\t\tsource: options.source,\n\t\t\ttarget,\n\t\t\tcontext: options.context,\n\t\t});\n\n\t\tconst result = options.merge ? { ...existing, ...translated } : translated;\n\t\tconst written = Object.keys(translated).length;\n\t\tawait writeFile(outputPath, `${JSON.stringify(result, null, 2)}\\n`);\n\n\t\tconsole.log(\n\t\t\t`[rosetta] ${target} → ${outputPath} (${written}/${payloadKeys.length} keys)`,\n\t\t);\n\t}\n}\n\n/** Build a real Rosetta client from CLI options + env. */\nexport async function buildClient(options: CatalogOptions): Promise<Rosetta> {\n\tconst apiKey = process.env.OPENROUTER_API_KEY;\n\tif (!apiKey) {\n\t\tthrow new Error(\n\t\t\t\"OPENROUTER_API_KEY is not set (pass it in the environment, or use --dry-run).\",\n\t\t);\n\t}\n\n\tlet glossary: Glossary | undefined;\n\tif (options.glossaryPath) {\n\t\tglossary = JSON.parse(\n\t\t\tawait readFile(options.glossaryPath, \"utf8\"),\n\t\t) as Glossary;\n\t}\n\n\treturn new Rosetta({\n\t\tapiKey,\n\t\tmodel:\n\t\t\toptions.model ??\n\t\t\tprocess.env.ROSETTA_MODEL ??\n\t\t\t\"anthropic/claude-sonnet-4.5\",\n\t\tbaseURL: options.baseURL ?? process.env.ROSETTA_BASE_URL,\n\t\tbrandVoice: {\n\t\t\tvariations: {\n\t\t\t\t\"*\": options.brandVoice ?? process.env.ROSETTA_BRAND_VOICE ?? \"\",\n\t\t\t},\n\t\t},\n\t\tglossary,\n\t\tbatchSize: options.batchSize,\n\t\tconcurrency: options.concurrency,\n\t\tretries: options.retries,\n\t});\n}\n\nconst HELP_TEXT = `rosetta-i18n — translate a JSON catalog with an LLM\n\nUsage:\n rosetta-i18n translate-catalog <input.json> --target <locale> [--target <locale>...] [options]\n\nOptions:\n --target <locale> Target locale (repeatable, required)\n --source <locale> Source locale (default: en)\n --out <dir> Output directory (default: the input file's directory)\n --merge Only translate keys missing from existing <target>.json\n --context <text> Broad context passed to the model\n --brand-voice <text> Brand voice briefing (default: ROSETTA_BRAND_VOICE)\n --glossary <file> JSON file of per-locale exact term mappings\n --model <id> Model id (default: ROSETTA_MODEL or claude-sonnet-4.5)\n --base-url <url> OpenAI-compatible endpoint (default: ROSETTA_BASE_URL)\n --concurrency <n> Parallel batches in flight\n --batch-size <n> Keys per request\n --retries <n> Retries per batch\n --dry-run Print the plan without calling the model\n -h, --help Show this help\n\nExample:\n OPENROUTER_API_KEY=sk-... rosetta-i18n translate-catalog messages/en.json \\\\\n --target es --target pt-BR --merge --context \"Next.js UI catalog\"\n`;\n\nexport function printHelp(): void {\n\tconsole.log(HELP_TEXT);\n}\n\nasync function main(): Promise<void> {\n\tconst parsed = parseArgs(process.argv.slice(2));\n\tif (parsed === HELP) {\n\t\tprintHelp();\n\t\treturn;\n\t}\n\n\tif (parsed.dryRun) {\n\t\tawait runCatalog(parsed);\n\t\treturn;\n\t}\n\n\tconst client = await buildClient(parsed);\n\tawait runCatalog(parsed, client);\n}\n\nconst isMain =\n\tprocess.argv[1] !== undefined &&\n\timport.meta.url === pathToFileURL(process.argv[1]).href;\n\nif (isMain) {\n\tmain().catch((error: unknown) => {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconsole.error(`[rosetta] ${message}`);\n\t\tprocess.exit(1);\n\t});\n}\n"],"names":[],"mappings":";;;;;AAMO,SAAS,kBAAkB,OAAA,EAKvB;AACV,EAAA,MAAM,EAAE,UAAA,EAAY,QAAA,EAAU,MAAA,EAAQ,QAAO,GAAI,OAAA;AAEjD,EAAA,MAAM,KAAA,GACL,WAAW,UAAA,CAAW,MAAM,KAAK,UAAA,CAAW,UAAA,CAAW,GAAG,CAAA,IAAK,EAAA;AAEhE,EAAA,MAAM,KAAA,GAAQ,CAAC,KAAK,CAAA;AAEpB,EAAA,IAAI,QAAA,EAAU;AACb,IAAA,MAAM,KAAA,GAAQ,SAAS,MAAM,CAAA;AAC7B,IAAA,IAAI,SAAS,MAAA,CAAO,IAAA,CAAK,KAAK,CAAA,CAAE,SAAS,CAAA,EAAG;AAC3C,MAAA,KAAA,CAAM,IAAA;AAAA,QACL,CAAA;AAAA,EAA2C,OAAO,OAAA,CAAQ,KAAK,EAC7D,GAAA,CAAI,CAAC,CAAC,GAAA,EAAK,GAAG,CAAA,KAAM,CAAA,GAAA,EAAM,GAAG,CAAA,KAAA,EAAQ,GAAG,EAAE,CAAA,CAC1C,IAAA,CAAK,IAAI,CAAC,CAAA;AAAA,OACb;AAAA,IACD;AAAA,EACD;AAEA,EAAA,KAAA,CAAM,IAAA;AAAA,IACL,CAAA,eAAA,EAAkB,MAAM,CAAA,IAAA,EAAO,MAAM,CAAA,8EAAA;AAAA,GACtC;AAEA,EAAA,OAAO,KAAA,CAAM,MAAA,CAAO,OAAO,CAAA,CAAE,KAAK,MAAM,CAAA;AACzC;AAKO,SAAS,eAAA,CACf,MACA,OAAA,EACS;AACT,EAAA,MAAM,KAAA,GAAQ;AAAA,IACb,CAAA,8CAAA,EAAiD,OAAA,CAAQ,MAAM,CAAA,IAAA,EAAO,QAAQ,MAAM,CAAA,+OAAA;AAAA,GACrF;AAEA,EAAA,IAAI,QAAQ,OAAA,EAAS;AACpB,IAAA,KAAA,CAAM,IAAA,CAAK,CAAA,SAAA,EAAY,OAAA,CAAQ,OAAO,CAAA,CAAE,CAAA;AAAA,EACzC;AAEA,EAAA,IAAI,QAAQ,KAAA,EAAO;AAClB,IAAA,MAAM,KAAA,GAAQ,OAAO,OAAA,CAAQ,OAAA,CAAQ,KAAK,CAAA,CACxC,GAAA,CAAI,CAAC,CAAC,GAAA,EAAK,UAAU,MAAM,CAAA,EAAA,EAAK,GAAG,KAAK,UAAA,CAAW,IAAA,CAAK,KAAK,CAAC,CAAA,CAAE,CAAA,CAChE,IAAA,CAAK,IAAI,CAAA;AACX,IAAA,IAAI,KAAA,EAAO;AACV,MAAA,KAAA,CAAM,IAAA,CAAK,CAAA;AAAA,EAAwB,KAAK,CAAA,CAAE,CAAA;AAAA,IAC3C;AAAA,EACD;AAEA,EAAA,KAAA,CAAM,KAAK,IAAA,CAAK,SAAA,CAAU,IAAA,EAAM,IAAA,EAAM,CAAC,CAAC,CAAA;AAExC,EAAA,OAAO,KAAA,CAAM,KAAK,MAAM,CAAA;AACzB;;ACnCO,MAAM,OAAA,CAAQ;AAAA,EACZ,MAAA;AAAA,EAER,YAAY,MAAA,EAAuB;AAClC,IAAA,IAAI,CAAC,MAAA,CAAO,MAAA,EAAQ,MAAM,IAAI,MAAM,6BAA6B,CAAA;AACjE,IAAA,IAAI,CAAC,MAAA,CAAO,KAAA,EAAO,MAAM,IAAI,MAAM,4BAA4B,CAAA;AAC/D,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AAAA,EACf;AAAA;AAAA,EAGA,MAAM,aAAA,CACL,IAAA,EACA,OAAA,EACkB;AAClB,IAAA,MAAM,SAAS,iBAAA,CAAkB;AAAA,MAChC,UAAA,EAAY,KAAK,MAAA,CAAO,UAAA;AAAA,MACxB,QAAA,EAAU,KAAK,MAAA,CAAO,QAAA;AAAA,MACtB,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,QAAQ,OAAA,CAAQ;AAAA,KAChB,CAAA;AAED,IAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,IAAA,CAAK;AAAA,MAC3B,EAAE,IAAA,EAAM,QAAA,EAAU,OAAA,EAAS,MAAA,EAAO;AAAA,MAClC;AAAA,QACC,IAAA,EAAM,MAAA;AAAA,QACN,OAAA,EAAS,OAAA,CAAQ,OAAA,GACd,CAAA,EAAG,IAAI;;AAAA,SAAA,EAAgB,OAAA,CAAQ,OAAO,CAAA,CAAA,GACtC;AAAA;AACJ,KACA,CAAA;AAED,IAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACZ,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,SAAA,EAAY,GAAA,CAAI,MAAM,IAAI,MAAM,GAAA,CAAI,IAAA,EAAM,CAAA,CAAE,CAAA;AAAA,IAC7D;AACA,IAAA,MAAM,IAAA,GAAQ,MAAM,GAAA,CAAI,IAAA,EAAK;AAG7B,IAAA,OAAO,KAAK,OAAA,CAAQ,CAAC,CAAA,CAAE,OAAA,CAAQ,QAAQ,IAAA,EAAK;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,SAAA,CACL,IAAA,EACA,OAAA,EACmC;AACnC,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,MAAA,CAAO,SAAA,IAAa,EAAA;AAC3C,IAAA,MAAM,WAAA,GAAc,IAAA,CAAK,MAAA,CAAO,WAAA,IAAe,CAAA;AAC/C,IAAA,MAAM,IAAA,GAAO,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA;AAC7B,IAAA,MAAM,SAAkC,EAAC;AAEzC,IAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,KAAK,MAAA,EAAQ,CAAA,IAAK,YAAY,WAAA,EAAa;AAC9D,MAAA,MAAM,QAAQ,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,CAAA,GAAI,YAAY,WAAW,CAAA;AACvD,MAAA,MAAM,UAAU,KAAA,CAAM,IAAA;AAAA,QACrB,EAAE,MAAA,EAAQ,IAAA,CAAK,KAAK,KAAA,CAAM,MAAA,GAAS,SAAS,CAAA,EAAE;AAAA,QAC9C,CAAC,GAAG,CAAA,KAAM;AACT,UAAA,MAAM,YAAY,KAAA,CAAM,KAAA,CAAM,IAAI,SAAA,EAAA,CAAY,CAAA,GAAI,KAAK,SAAS,CAAA;AAChE,UAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,WAAA,CAAY,SAAA,CAAU,GAAA,CAAI,CAAC,CAAA,KAAM,CAAC,CAAA,EAAG,IAAA,CAAK,CAAC,CAAC,CAAC,CAAC,CAAA;AACnE,UAAA,OAAO,EAAE,OAAO,SAAA,EAAU;AAAA,QAC3B;AAAA,OACD;AAEA,MAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,UAAA;AAAA,QAC7B,OAAA,CAAQ,GAAA;AAAA,UAAI,CAAC,EAAE,KAAA,EAAO,SAAA,OACrB,IAAA,CAAK,cAAA,CAAe,KAAA,EAAO,OAAA,EAAS,SAAS;AAAA;AAC9C,OACD;AAEA,MAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,OAAA,CAAQ,QAAQ,CAAA,EAAA,EAAK;AACxC,QAAA,MAAM,MAAA,GAAS,QAAQ,CAAC,CAAA;AACxB,QAAA,IAAI,MAAA,CAAO,WAAW,WAAA,EAAa;AAClC,UAAA,OAAA,CAAQ,KAAA;AAAA,YACP,CAAA,wBAAA,EAA2B,OAAA,CAAQ,CAAC,CAAA,CAAE,UAAU,MAAM,CAAA,OAAA,CAAA;AAAA,YACtD,MAAA,CAAO,MAAA,EAAQ,OAAA,EAAS,KAAA,CAAM,GAAG,GAAG;AAAA,WACrC;AACA,UAAA;AAAA,QACD;AACA,QAAA,MAAA,CAAO,MAAA,CAAO,MAAA,EAAQ,MAAA,CAAO,KAAK,CAAA;AAAA,MACnC;AAAA,IACD;AAEA,IAAA,OAAO,MAAA;AAAA,EACR;AAAA,EAEA,MAAc,cAAA,CACb,KAAA,EACA,OAAA,EACA,SAAA,EACmC;AACnC,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,MAAA,CAAO,OAAA,IAAW,CAAA;AACvC,IAAA,MAAM,SAAS,iBAAA,CAAkB;AAAA,MAChC,UAAA,EAAY,KAAK,MAAA,CAAO,UAAA;AAAA,MACxB,QAAA,EAAU,KAAK,MAAA,CAAO,QAAA;AAAA,MACtB,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,QAAQ,OAAA,CAAQ;AAAA,KAChB,CAAA;AAED,IAAA,MAAM,IAAA,GAAO,gBAAgB,KAAA,EAAO;AAAA,MACnC,GAAG,OAAA;AAAA,MACH,KAAA,EAAO,OAAA,CAAQ,KAAA,GACZ,MAAA,CAAO,WAAA;AAAA,QACP,SAAA,CAAU,GAAA,CAAI,CAAC,GAAA,KAAQ,CAAC,GAAA,EAAK,OAAA,CAAQ,KAAA,GAAQ,GAAG,CAAA,IAAK,EAAE,CAAC;AAAA,OACzD,GACC;AAAA,KACH,CAAA;AAED,IAAA,IAAI,SAAA;AACJ,IAAA,KAAA,IAAS,OAAA,GAAU,CAAA,EAAG,OAAA,IAAW,OAAA,EAAS,OAAA,EAAA,EAAW;AACpD,MAAA,IAAI;AACH,QAAA,MAAM,GAAA,GAAM,MAAM,IAAA,CAAK,IAAA,CAAK;AAAA,UAC3B,EAAE,IAAA,EAAM,QAAA,EAAU,OAAA,EAAS,MAAA,EAAO;AAAA,UAClC,EAAE,IAAA,EAAM,MAAA,EAAQ,OAAA,EAAS,IAAA;AAAK,SAC9B,CAAA;AACD,QAAA,IAAI,CAAC,IAAI,EAAA,EAAI;AACZ,UAAA,MAAM,IAAI,KAAA,CAAM,CAAA,SAAA,EAAY,GAAA,CAAI,MAAM,IAAI,MAAM,GAAA,CAAI,IAAA,EAAM,CAAA,CAAE,CAAA;AAAA,QAC7D;AACA,QAAA,MAAM,IAAA,GAAQ,MAAM,GAAA,CAAI,IAAA,EAAK;AAG7B,QAAA,OAAO,KAAK,KAAA,CAAM,IAAA,CAAK,QAAQ,CAAC,CAAA,CAAE,QAAQ,OAAO,CAAA;AAAA,MAClD,SAAS,KAAA,EAAO;AACf,QAAA,SAAA,GAAY,iBAAiB,KAAA,GAAQ,KAAA,GAAQ,IAAI,KAAA,CAAM,MAAA,CAAO,KAAK,CAAC,CAAA;AACpE,QAAA,IAAI,UAAU,OAAA,EAAS;AACtB,UAAA,MAAM,IAAI,QAAQ,CAAC,EAAA,KAAO,WAAW,EAAA,EAAI,GAAA,IAAQ,OAAA,GAAU,CAAA,CAAE,CAAC,CAAA;AAAA,QAC/D;AAAA,MACD;AAAA,IACD;AACA,IAAA,MAAM,SAAA,IAAa,IAAI,KAAA,CAAM,uBAAuB,CAAA;AAAA,EACrD;AAAA,EAEA,MAAc,KAAK,QAAA,EAA4C;AAC9D,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,MAAA,CAAO,OAAA,IAAW,8BAAA;AACvC,IAAA,OAAO,KAAA,CAAM,CAAA,EAAG,OAAO,CAAA,iBAAA,CAAA,EAAqB;AAAA,MAC3C,MAAA,EAAQ,MAAA;AAAA,MACR,OAAA,EAAS;AAAA,QACR,aAAA,EAAe,CAAA,OAAA,EAAU,IAAA,CAAK,MAAA,CAAO,MAAM,CAAA,CAAA;AAAA,QAC3C,cAAA,EAAgB;AAAA,OACjB;AAAA,MACA,IAAA,EAAM,KAAK,SAAA,CAAU;AAAA,QACpB,KAAA,EAAO,KAAK,MAAA,CAAO,KAAA;AAAA,QACnB,WAAA,EAAa,IAAA,CAAK,MAAA,CAAO,WAAA,IAAe,GAAA;AAAA,QACxC;AAAA,OACA;AAAA,KACD,CAAA;AAAA,EACF;AACD;;AC5KO,MAAM,IAAA,0BAAc,MAAM;AAqCjC,MAAM,OAAA,GAAU,mBAAA;AAEhB,SAAS,SAAA,CAAU,IAAA,EAAgB,KAAA,EAAe,IAAA,EAAsB;AACvE,EAAA,MAAM,KAAA,GAAQ,KAAK,KAAK,CAAA;AACxB,EAAA,IAAI,UAAU,MAAA,EAAW,MAAM,IAAI,KAAA,CAAM,CAAA,kBAAA,EAAqB,IAAI,CAAA,CAAE,CAAA;AACpE,EAAA,OAAO,KAAA;AACR;AAEA,SAAS,KAAA,CAAM,OAAe,IAAA,EAAsB;AACnD,EAAA,MAAM,MAAA,GAAS,MAAA,CAAO,QAAA,CAAS,KAAA,EAAO,EAAE,CAAA;AACxC,EAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,MAAM,CAAA,IAAK,SAAS,CAAA,EAAG;AAC3C,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,EAAG,IAAI,CAAA,sCAAA,EAAyC,KAAK,CAAA,CAAA,CAAG,CAAA;AAAA,EACzE;AACA,EAAA,OAAO,MAAA;AACR;AAMO,SAAS,UAAU,IAAA,EAA8C;AACvE,EAAA,MAAM,IAAA,GAAO,CAAC,GAAG,IAAI,CAAA;AACrB,EAAA,IAAI,IAAA,CAAK,MAAA,KAAW,CAAA,IAAK,IAAA,CAAK,QAAA,CAAS,QAAQ,CAAA,IAAK,IAAA,CAAK,QAAA,CAAS,IAAI,CAAA,EAAG;AACxE,IAAA,OAAO,IAAA;AAAA,EACR;AAEA,EAAA,MAAM,OAAA,GAAU,KAAK,KAAA,EAAM;AAC3B,EAAA,IAAI,YAAY,OAAA,EAAS;AACxB,IAAA,MAAM,IAAI,KAAA;AAAA,MACT,oBAAoB,OAAO,CAAA,+BAAA;AAAA,KAC5B;AAAA,EACD;AAEA,EAAA,IAAI,KAAA;AACJ,EAAA,MAAM,UAAoB,EAAC;AAC3B,EAAA,MAAM,OAAA,GAAmC,EAAE,MAAA,EAAQ,IAAA,EAAK;AAExD,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,IAAA,CAAK,QAAQ,CAAA,EAAA,EAAK;AACrC,IAAA,MAAM,GAAA,GAAM,KAAK,CAAC,CAAA;AAClB,IAAA,QAAQ,GAAA;AAAK,MACZ,KAAK,UAAA;AACJ,QAAA,OAAA,CAAQ,KAAK,SAAA,CAAU,IAAA,EAAM,EAAE,CAAA,EAAG,GAAG,CAAC,CAAA;AACtC,QAAA;AAAA,MACD,KAAK,UAAA;AACJ,QAAA,OAAA,CAAQ,MAAA,GAAS,SAAA,CAAU,IAAA,EAAM,EAAE,GAAG,GAAG,CAAA;AACzC,QAAA;AAAA,MACD,KAAK,OAAA;AACJ,QAAA,OAAA,CAAQ,SAAS,OAAA,CAAQ,SAAA,CAAU,MAAM,EAAE,CAAA,EAAG,GAAG,CAAC,CAAA;AAClD,QAAA;AAAA,MACD,KAAK,WAAA;AACJ,QAAA,OAAA,CAAQ,OAAA,GAAU,SAAA,CAAU,IAAA,EAAM,EAAE,GAAG,GAAG,CAAA;AAC1C,QAAA;AAAA,MACD,KAAK,eAAA;AACJ,QAAA,OAAA,CAAQ,UAAA,GAAa,SAAA,CAAU,IAAA,EAAM,EAAE,GAAG,GAAG,CAAA;AAC7C,QAAA;AAAA,MACD,KAAK,YAAA;AACJ,QAAA,OAAA,CAAQ,eAAe,OAAA,CAAQ,SAAA,CAAU,MAAM,EAAE,CAAA,EAAG,GAAG,CAAC,CAAA;AACxD,QAAA;AAAA,MACD,KAAK,SAAA;AACJ,QAAA,OAAA,CAAQ,KAAA,GAAQ,SAAA,CAAU,IAAA,EAAM,EAAE,GAAG,GAAG,CAAA;AACxC,QAAA;AAAA,MACD,KAAK,YAAA;AACJ,QAAA,OAAA,CAAQ,OAAA,GAAU,SAAA,CAAU,IAAA,EAAM,EAAE,GAAG,GAAG,CAAA;AAC1C,QAAA;AAAA,MACD,KAAK,eAAA;AACJ,QAAA,OAAA,CAAQ,WAAA,GAAc,MAAM,SAAA,CAAU,IAAA,EAAM,EAAE,CAAA,EAAG,GAAG,GAAG,GAAG,CAAA;AAC1D,QAAA;AAAA,MACD,KAAK,cAAA;AACJ,QAAA,OAAA,CAAQ,SAAA,GAAY,MAAM,SAAA,CAAU,IAAA,EAAM,EAAE,CAAA,EAAG,GAAG,GAAG,GAAG,CAAA;AACxD,QAAA;AAAA,MACD,KAAK,WAAA;AACJ,QAAA,OAAA,CAAQ,OAAA,GAAU,MAAM,SAAA,CAAU,IAAA,EAAM,EAAE,CAAA,EAAG,GAAG,GAAG,GAAG,CAAA;AACtD,QAAA;AAAA,MACD,KAAK,WAAA;AACJ,QAAA,OAAA,CAAQ,MAAA,GAAS,IAAA;AACjB,QAAA;AAAA,MACD,KAAK,SAAA;AACJ,QAAA,OAAA,CAAQ,KAAA,GAAQ,IAAA;AAChB,QAAA;AAAA,MACD;AACC,QAAA,IAAI,GAAA,CAAI,WAAW,GAAG,CAAA,QAAS,IAAI,KAAA,CAAM,CAAA,cAAA,EAAiB,GAAG,CAAA,EAAA,CAAI,CAAA;AACjE,QAAA,IAAI,OAAO,MAAM,IAAI,KAAA,CAAM,CAAA,qBAAA,EAAwB,GAAG,CAAA,EAAA,CAAI,CAAA;AAC1D,QAAA,KAAA,GAAQ,QAAQ,GAAG,CAAA;AAAA;AACrB,EACD;AAEA,EAAA,IAAI,CAAC,KAAA,EAAO,MAAM,IAAI,MAAM,6BAA6B,CAAA;AACzD,EAAA,IAAI,OAAA,CAAQ,WAAW,CAAA,EAAG;AACzB,IAAA,MAAM,IAAI,MAAM,6CAA6C,CAAA;AAAA,EAC9D;AAEA,EAAA,OAAA,CAAQ,KAAA,GAAQ,KAAA;AAChB,EAAA,OAAA,CAAQ,OAAA,GAAU,OAAA;AAClB,EAAA,OAAA,CAAQ,MAAA,GAAS,OAAA,CAAQ,MAAA,IAAU,OAAA,CAAQ,KAAK,CAAA;AAChD,EAAA,OAAO,OAAA;AACR;AAEA,eAAe,SAAS,IAAA,EAAgD;AACvE,EAAA,OAAO,KAAK,KAAA,CAAM,MAAM,QAAA,CAAS,IAAA,EAAM,MAAM,CAAC,CAAA;AAC/C;AAEA,eAAe,iBACd,IAAA,EACmC;AACnC,EAAA,IAAI;AACH,IAAA,OAAO,MAAM,SAAS,IAAI,CAAA;AAAA,EAC3B,SAAS,KAAA,EAAO;AACf,IAAA,IAAK,KAAA,CAAgC,IAAA,KAAS,QAAA,EAAU,OAAO,EAAC;AAChE,IAAA,MAAM,KAAA;AAAA,EACP;AACD;AAGO,SAAS,WAAA,CACf,OACA,QAAA,EAC0B;AAC1B,EAAA,OAAO,MAAA,CAAO,WAAA;AAAA,IACb,MAAA,CAAO,OAAA,CAAQ,KAAK,CAAA,CAAE,MAAA,CAAO,CAAC,CAAC,GAAG,CAAA,KAAM,EAAE,GAAA,IAAO,QAAA,CAAS;AAAA,GAC3D;AACD;AAMA,eAAsB,UAAA,CACrB,SACA,MAAA,EACgB;AAChB,EAAA,MAAM,KAAA,GAAQ,MAAM,QAAA,CAAS,OAAA,CAAQ,KAAK,CAAA;AAC1C,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,IAAA,CAAK,KAAK,CAAA,CAAE,MAAA;AAEhC,EAAA,IAAI,QAAQ,MAAA,EAAQ;AACnB,IAAA,OAAA,CAAQ,GAAA;AAAA,MACP,CAAA,yBAAA,EAAuB,IAAI,CAAA,MAAA,EAAS,OAAA,CAAQ,MAAM,WAAM,OAAA,CAAQ,OAAA,CAAQ,IAAA,CAAK,IAAI,CAAC,CAAA;AAAA,KACnF;AACA,IAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,yBAAA,EAA4B,OAAA,CAAQ,MAAM,CAAA,CAAA,CAAG,CAAA;AACzD,IAAA;AAAA,EACD;AAEA,EAAA,IAAI,CAAC,MAAA,EAAQ,MAAM,IAAI,MAAM,iCAAiC,CAAA;AAE9D,EAAA,KAAA,MAAW,MAAA,IAAU,QAAQ,OAAA,EAAS;AACrC,IAAA,MAAM,aAAa,IAAA,CAAK,OAAA,CAAQ,MAAA,EAAQ,CAAA,EAAG,MAAM,CAAA,KAAA,CAAO,CAAA;AACxD,IAAA,MAAM,WAAW,OAAA,CAAQ,KAAA,GAAQ,MAAM,gBAAA,CAAiB,UAAU,IAAI,EAAC;AAEvE,IAAA,MAAM,UAAU,OAAA,CAAQ,KAAA,GAAQ,WAAA,CAAY,KAAA,EAAO,QAAQ,CAAA,GAAI,KAAA;AAC/D,IAAA,MAAM,WAAA,GAAc,MAAA,CAAO,IAAA,CAAK,OAAO,CAAA;AAEvC,IAAA,IAAI,WAAA,CAAY,WAAW,CAAA,EAAG;AAC7B,MAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,UAAA,EAAa,MAAM,CAAA,4BAAA,CAAyB,CAAA;AACxD,MAAA;AAAA,IACD;AAEA,IAAA,MAAM,UAAA,GAAa,MAAM,MAAA,CAAO,SAAA,CAAU,OAAA,EAAS;AAAA,MAClD,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,MAAA;AAAA,MACA,SAAS,OAAA,CAAQ;AAAA,KACjB,CAAA;AAED,IAAA,MAAM,MAAA,GAAS,QAAQ,KAAA,GAAQ,EAAE,GAAG,QAAA,EAAU,GAAG,YAAW,GAAI,UAAA;AAChE,IAAA,MAAM,OAAA,GAAU,MAAA,CAAO,IAAA,CAAK,UAAU,CAAA,CAAE,MAAA;AACxC,IAAA,MAAM,SAAA,CAAU,YAAY,CAAA,EAAG,IAAA,CAAK,UAAU,MAAA,EAAQ,IAAA,EAAM,CAAC,CAAC;AAAA,CAAI,CAAA;AAElE,IAAA,OAAA,CAAQ,GAAA;AAAA,MACP,CAAA,UAAA,EAAa,MAAM,CAAA,QAAA,EAAM,UAAU,KAAK,OAAO,CAAA,CAAA,EAAI,YAAY,MAAM,CAAA,MAAA;AAAA,KACtE;AAAA,EACD;AACD;AAGA,eAAsB,YAAY,OAAA,EAA2C;AAC5E,EAAA,MAAM,MAAA,GAAS,QAAQ,GAAA,CAAI,kBAAA;AAC3B,EAAA,IAAI,CAAC,MAAA,EAAQ;AACZ,IAAA,MAAM,IAAI,KAAA;AAAA,MACT;AAAA,KACD;AAAA,EACD;AAEA,EAAA,IAAI,QAAA;AACJ,EAAA,IAAI,QAAQ,YAAA,EAAc;AACzB,IAAA,QAAA,GAAW,IAAA,CAAK,KAAA;AAAA,MACf,MAAM,QAAA,CAAS,OAAA,CAAQ,YAAA,EAAc,MAAM;AAAA,KAC5C;AAAA,EACD;AAEA,EAAA,OAAO,IAAI,OAAA,CAAQ;AAAA,IAClB,MAAA;AAAA,IACA,KAAA,EACC,OAAA,CAAQ,KAAA,IACR,OAAA,CAAQ,IAAI,aAAA,IACZ,6BAAA;AAAA,IACD,OAAA,EAAS,OAAA,CAAQ,OAAA,IAAW,OAAA,CAAQ,GAAA,CAAI,gBAAA;AAAA,IACxC,UAAA,EAAY;AAAA,MACX,UAAA,EAAY;AAAA,QACX,GAAA,EAAK,OAAA,CAAQ,UAAA,IAAc,OAAA,CAAQ,IAAI,mBAAA,IAAuB;AAAA;AAC/D,KACD;AAAA,IACA,QAAA;AAAA,IACA,WAAW,OAAA,CAAQ,SAAA;AAAA,IACnB,aAAa,OAAA,CAAQ,WAAA;AAAA,IACrB,SAAS,OAAA,CAAQ;AAAA,GACjB,CAAA;AACF;AAEA,MAAM,SAAA,GAAY,CAAA;;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA,CAAA;AA0BX,SAAS,SAAA,GAAkB;AACjC,EAAA,OAAA,CAAQ,IAAI,SAAS,CAAA;AACtB;AAEA,eAAe,IAAA,GAAsB;AACpC,EAAA,MAAM,SAAS,SAAA,CAAU,OAAA,CAAQ,IAAA,CAAK,KAAA,CAAM,CAAC,CAAC,CAAA;AAC9C,EAAA,IAAI,WAAW,IAAA,EAAM;AACpB,IAAA,SAAA,EAAU;AACV,IAAA;AAAA,EACD;AAEA,EAAA,IAAI,OAAO,MAAA,EAAQ;AAClB,IAAA,MAAM,WAAW,MAAM,CAAA;AACvB,IAAA;AAAA,EACD;AAEA,EAAA,MAAM,MAAA,GAAS,MAAM,WAAA,CAAY,MAAM,CAAA;AACvC,EAAA,MAAM,UAAA,CAAW,QAAQ,MAAM,CAAA;AAChC;AAEA,MAAM,MAAA,GACL,OAAA,CAAQ,IAAA,CAAK,CAAC,CAAA,KAAM,MAAA,IACpB,MAAA,CAAA,IAAA,CAAY,GAAA,KAAQ,aAAA,CAAc,OAAA,CAAQ,IAAA,CAAK,CAAC,CAAC,CAAA,CAAE,IAAA;AAEpD,IAAI,MAAA,EAAQ;AACX,EAAA,IAAA,EAAK,CAAE,KAAA,CAAM,CAAC,KAAA,KAAmB;AAChC,IAAA,MAAM,UAAU,KAAA,YAAiB,KAAA,GAAQ,KAAA,CAAM,OAAA,GAAU,OAAO,KAAK,CAAA;AACrE,IAAA,OAAA,CAAQ,KAAA,CAAM,CAAA,UAAA,EAAa,OAAO,CAAA,CAAE,CAAA;AACpC,IAAA,OAAA,CAAQ,KAAK,CAAC,CAAA;AAAA,EACf,CAAC,CAAA;AACF;;"}
|
package/dist/esm/next.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { unstable_cache } from 'next/cache';
|
|
2
|
+
|
|
3
|
+
function buildSystemPrompt(options) {
|
|
4
|
+
const { brandVoice, glossary, source, target } = options;
|
|
5
|
+
const voice = brandVoice.variations[target] ?? brandVoice.variations["*"] ?? "";
|
|
6
|
+
const parts = [voice];
|
|
7
|
+
if (glossary) {
|
|
8
|
+
const terms = glossary[target];
|
|
9
|
+
if (terms && Object.keys(terms).length > 0) {
|
|
10
|
+
parts.push(
|
|
11
|
+
`Glossary (use these exact renderings):
|
|
12
|
+
${Object.entries(terms).map(([src, tgt]) => `- "${src}" -> ${tgt}`).join("\n")}`
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
parts.push(
|
|
17
|
+
`Translate from ${source} to ${target}. Return ONLY the translated text \u2014 no explanations, no quotes around it.`
|
|
18
|
+
);
|
|
19
|
+
return parts.filter(Boolean).join("\n\n");
|
|
20
|
+
}
|
|
21
|
+
function buildDataPrompt(data, options) {
|
|
22
|
+
const lines = [
|
|
23
|
+
`Translate the values of this JSON object from ${options.source} to ${options.target}. Keep every key exactly as provided \u2014 return a JSON object with identical keys and translated values. Preserve {placeholders} and ICU plural syntax (=1 {...} other {...}) intact, translating only the inner text. Output ONLY the JSON.`
|
|
24
|
+
];
|
|
25
|
+
if (options.context) {
|
|
26
|
+
lines.push(`Context: ${options.context}`);
|
|
27
|
+
}
|
|
28
|
+
if (options.hints) {
|
|
29
|
+
const hints = Object.entries(options.hints).map(([key, breadcrumb]) => `- ${key}: ${breadcrumb.join(" > ")}`).join("\n");
|
|
30
|
+
if (hints) {
|
|
31
|
+
lines.push(`Key disambiguation:
|
|
32
|
+
${hints}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
lines.push(JSON.stringify(data, null, 1));
|
|
36
|
+
return lines.join("\n\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class Rosetta {
|
|
40
|
+
config;
|
|
41
|
+
constructor(config) {
|
|
42
|
+
if (!config.apiKey) throw new Error("Rosetta: apiKey is required");
|
|
43
|
+
if (!config.model) throw new Error("Rosetta: model is required");
|
|
44
|
+
this.config = config;
|
|
45
|
+
}
|
|
46
|
+
/** Translate a single text string. */
|
|
47
|
+
async translateText(text, options) {
|
|
48
|
+
const system = buildSystemPrompt({
|
|
49
|
+
brandVoice: this.config.brandVoice,
|
|
50
|
+
glossary: this.config.glossary,
|
|
51
|
+
source: options.source,
|
|
52
|
+
target: options.target
|
|
53
|
+
});
|
|
54
|
+
const res = await this.chat([
|
|
55
|
+
{ role: "system", content: system },
|
|
56
|
+
{
|
|
57
|
+
role: "user",
|
|
58
|
+
content: options.context ? `${text}
|
|
59
|
+
|
|
60
|
+
Context: ${options.context}` : text
|
|
61
|
+
}
|
|
62
|
+
]);
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
throw new Error(`Rosetta: ${res.status} ${await res.text()}`);
|
|
65
|
+
}
|
|
66
|
+
const json = await res.json();
|
|
67
|
+
return json.choices[0].message.content.trim();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Translate a key-value payload, batched with concurrency. Failed batches
|
|
71
|
+
* are logged and skipped — the returned object carries only successfully
|
|
72
|
+
* translated keys, letting the caller fall back to the source locale for
|
|
73
|
+
* the rest.
|
|
74
|
+
*/
|
|
75
|
+
async translate(data, options) {
|
|
76
|
+
const batchSize = this.config.batchSize ?? 25;
|
|
77
|
+
const concurrency = this.config.concurrency ?? 4;
|
|
78
|
+
const keys = Object.keys(data);
|
|
79
|
+
const output = {};
|
|
80
|
+
for (let i = 0; i < keys.length; i += batchSize * concurrency) {
|
|
81
|
+
const chunk = keys.slice(i, i + batchSize * concurrency);
|
|
82
|
+
const batches = Array.from(
|
|
83
|
+
{ length: Math.ceil(chunk.length / batchSize) },
|
|
84
|
+
(_, b) => {
|
|
85
|
+
const batchKeys = chunk.slice(b * batchSize, (b + 1) * batchSize);
|
|
86
|
+
const batch = Object.fromEntries(batchKeys.map((k) => [k, data[k]]));
|
|
87
|
+
return { batch, batchKeys };
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
const results = await Promise.allSettled(
|
|
91
|
+
batches.map(
|
|
92
|
+
({ batch, batchKeys }) => this.translateBatch(batch, options, batchKeys)
|
|
93
|
+
)
|
|
94
|
+
);
|
|
95
|
+
for (let b = 0; b < results.length; b++) {
|
|
96
|
+
const result = results[b];
|
|
97
|
+
if (result.status !== "fulfilled") {
|
|
98
|
+
console.error(
|
|
99
|
+
`[rosetta] batch failed (${batches[b].batchKeys.length} keys):`,
|
|
100
|
+
result.reason?.message?.slice(0, 120)
|
|
101
|
+
);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
Object.assign(output, result.value);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return output;
|
|
108
|
+
}
|
|
109
|
+
async translateBatch(batch, options, batchKeys) {
|
|
110
|
+
const retries = this.config.retries ?? 2;
|
|
111
|
+
const system = buildSystemPrompt({
|
|
112
|
+
brandVoice: this.config.brandVoice,
|
|
113
|
+
glossary: this.config.glossary,
|
|
114
|
+
source: options.source,
|
|
115
|
+
target: options.target
|
|
116
|
+
});
|
|
117
|
+
const user = buildDataPrompt(batch, {
|
|
118
|
+
...options,
|
|
119
|
+
hints: options.hints ? Object.fromEntries(
|
|
120
|
+
batchKeys.map((key) => [key, options.hints?.[key] ?? []])
|
|
121
|
+
) : void 0
|
|
122
|
+
});
|
|
123
|
+
let lastError;
|
|
124
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
125
|
+
try {
|
|
126
|
+
const res = await this.chat([
|
|
127
|
+
{ role: "system", content: system },
|
|
128
|
+
{ role: "user", content: user }
|
|
129
|
+
]);
|
|
130
|
+
if (!res.ok) {
|
|
131
|
+
throw new Error(`Rosetta: ${res.status} ${await res.text()}`);
|
|
132
|
+
}
|
|
133
|
+
const json = await res.json();
|
|
134
|
+
return JSON.parse(json.choices[0].message.content);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
137
|
+
if (attempt < retries) {
|
|
138
|
+
await new Promise((ok) => setTimeout(ok, 2e3 * (attempt + 1)));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
throw lastError ?? new Error("Rosetta: batch failed");
|
|
143
|
+
}
|
|
144
|
+
async chat(messages) {
|
|
145
|
+
const baseURL = this.config.baseURL ?? "https://openrouter.ai/api/v1";
|
|
146
|
+
return fetch(`${baseURL}/chat/completions`, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: {
|
|
149
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
150
|
+
"Content-Type": "application/json"
|
|
151
|
+
},
|
|
152
|
+
body: JSON.stringify({
|
|
153
|
+
model: this.config.model,
|
|
154
|
+
temperature: this.config.temperature ?? 0.3,
|
|
155
|
+
messages
|
|
156
|
+
})
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function createRosetta(overrides = {}) {
|
|
162
|
+
const apiKey = overrides.apiKey ?? process.env.OPENROUTER_API_KEY;
|
|
163
|
+
if (!apiKey) {
|
|
164
|
+
throw new Error("rosetta-i18n: set OPENROUTER_API_KEY or pass apiKey.");
|
|
165
|
+
}
|
|
166
|
+
return new Rosetta({
|
|
167
|
+
apiKey,
|
|
168
|
+
model: overrides.model ?? process.env.ROSETTA_MODEL ?? "anthropic/claude-sonnet-4.5",
|
|
169
|
+
baseURL: overrides.baseURL ?? process.env.ROSETTA_BASE_URL,
|
|
170
|
+
brandVoice: overrides.brandVoice ?? {
|
|
171
|
+
variations: {
|
|
172
|
+
"*": process.env.ROSETTA_BRAND_VOICE ?? ""
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
glossary: overrides.glossary,
|
|
176
|
+
temperature: overrides.temperature,
|
|
177
|
+
batchSize: overrides.batchSize,
|
|
178
|
+
concurrency: overrides.concurrency,
|
|
179
|
+
retries: overrides.retries
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
let instance;
|
|
183
|
+
function getRosetta() {
|
|
184
|
+
if (!instance) {
|
|
185
|
+
instance = createRosetta();
|
|
186
|
+
}
|
|
187
|
+
return instance;
|
|
188
|
+
}
|
|
189
|
+
function cachedTranslate(data, options, cache = {}) {
|
|
190
|
+
const run = unstable_cache(
|
|
191
|
+
() => getRosetta().translate(data, options),
|
|
192
|
+
[
|
|
193
|
+
"rosetta",
|
|
194
|
+
options.source,
|
|
195
|
+
options.target,
|
|
196
|
+
options.context ?? "",
|
|
197
|
+
JSON.stringify(data)
|
|
198
|
+
],
|
|
199
|
+
{
|
|
200
|
+
revalidate: cache.revalidate ?? 60 * 60 * 24,
|
|
201
|
+
tags: cache.tags
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
return run();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export { cachedTranslate, createRosetta, getRosetta };
|
|
208
|
+
//# sourceMappingURL=next.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"next.js","sources":["../../src/prompt.ts","../../src/index.ts","../../src/next.ts"],"sourcesContent":["import type { BrandVoice, Glossary, TranslateDataOptions } from \"./config\";\n\n/**\n * Builds the system prompt for a translation request: brand voice, glossary\n * enforcement, locale conventions, and output-shape rules.\n */\nexport function buildSystemPrompt(options: {\n\tbrandVoice: BrandVoice;\n\tglossary?: Glossary;\n\tsource: string;\n\ttarget: string;\n}): string {\n\tconst { brandVoice, glossary, source, target } = options;\n\n\tconst voice =\n\t\tbrandVoice.variations[target] ?? brandVoice.variations[\"*\"] ?? \"\";\n\n\tconst parts = [voice];\n\n\tif (glossary) {\n\t\tconst terms = glossary[target];\n\t\tif (terms && Object.keys(terms).length > 0) {\n\t\t\tparts.push(\n\t\t\t\t`Glossary (use these exact renderings):\\n${Object.entries(terms)\n\t\t\t\t\t.map(([src, tgt]) => `- \"${src}\" -> ${tgt}`)\n\t\t\t\t\t.join(\"\\n\")}`,\n\t\t\t);\n\t\t}\n\t}\n\n\tparts.push(\n\t\t`Translate from ${source} to ${target}. Return ONLY the translated text — no explanations, no quotes around it.`,\n\t);\n\n\treturn parts.filter(Boolean).join(\"\\n\\n\");\n}\n\n/**\n * Builds the user prompt for a key-value payload translation.\n */\nexport function buildDataPrompt(\n\tdata: Record<string, unknown>,\n\toptions: TranslateDataOptions & { hints?: Record<string, string[]> },\n): string {\n\tconst lines = [\n\t\t`Translate the values of this JSON object from ${options.source} to ${options.target}. Keep every key exactly as provided — return a JSON object with identical keys and translated values. Preserve {placeholders} and ICU plural syntax (=1 {...} other {...}) intact, translating only the inner text. Output ONLY the JSON.`,\n\t];\n\n\tif (options.context) {\n\t\tlines.push(`Context: ${options.context}`);\n\t}\n\n\tif (options.hints) {\n\t\tconst hints = Object.entries(options.hints)\n\t\t\t.map(([key, breadcrumb]) => `- ${key}: ${breadcrumb.join(\" > \")}`)\n\t\t\t.join(\"\\n\");\n\t\tif (hints) {\n\t\t\tlines.push(`Key disambiguation:\\n${hints}`);\n\t\t}\n\t}\n\n\tlines.push(JSON.stringify(data, null, 1));\n\n\treturn lines.join(\"\\n\\n\");\n}\n","import type {\n\tRosettaConfig,\n\tTranslateDataOptions,\n\tTranslateTextOptions,\n} from \"./config\";\nimport { buildDataPrompt, buildSystemPrompt } from \"./prompt\";\n\nexport type {\n\tRosettaConfig,\n\tTranslateDataOptions,\n\tTranslateTextOptions,\n} from \"./config\";\n\ninterface ChatMessage {\n\trole: \"system\" | \"user\" | \"assistant\";\n\tcontent: string;\n}\n\n/**\n * Rosetta — self-hosted AI translation engine.\n *\n * Translates key-value content across locales through any OpenAI-compatible\n * LLM, with brand voice, glossary, and per-locale rules applied on every\n * call. Works with OpenRouter, direct Anthropic/OpenAI, or any\n * OpenAI-compatible endpoint.\n *\n * Non-throwing at the batch level: failed batches are logged and skipped, so\n * partial translations degrade gracefully to the source locale.\n */\nexport class Rosetta {\n\tprivate config: RosettaConfig;\n\n\tconstructor(config: RosettaConfig) {\n\t\tif (!config.apiKey) throw new Error(\"Rosetta: apiKey is required\");\n\t\tif (!config.model) throw new Error(\"Rosetta: model is required\");\n\t\tthis.config = config;\n\t}\n\n\t/** Translate a single text string. */\n\tasync translateText(\n\t\ttext: string,\n\t\toptions: TranslateTextOptions,\n\t): Promise<string> {\n\t\tconst system = buildSystemPrompt({\n\t\t\tbrandVoice: this.config.brandVoice,\n\t\t\tglossary: this.config.glossary,\n\t\t\tsource: options.source,\n\t\t\ttarget: options.target,\n\t\t});\n\n\t\tconst res = await this.chat([\n\t\t\t{ role: \"system\", content: system },\n\t\t\t{\n\t\t\t\trole: \"user\",\n\t\t\t\tcontent: options.context\n\t\t\t\t\t? `${text}\\n\\nContext: ${options.context}`\n\t\t\t\t\t: text,\n\t\t\t},\n\t\t]);\n\n\t\tif (!res.ok) {\n\t\t\tthrow new Error(`Rosetta: ${res.status} ${await res.text()}`);\n\t\t}\n\t\tconst json = (await res.json()) as {\n\t\t\tchoices: Array<{ message: { content: string } }>;\n\t\t};\n\t\treturn json.choices[0].message.content.trim();\n\t}\n\n\t/**\n\t * Translate a key-value payload, batched with concurrency. Failed batches\n\t * are logged and skipped — the returned object carries only successfully\n\t * translated keys, letting the caller fall back to the source locale for\n\t * the rest.\n\t */\n\tasync translate(\n\t\tdata: Record<string, unknown>,\n\t\toptions: TranslateDataOptions,\n\t): Promise<Record<string, unknown>> {\n\t\tconst batchSize = this.config.batchSize ?? 25;\n\t\tconst concurrency = this.config.concurrency ?? 4;\n\t\tconst keys = Object.keys(data);\n\t\tconst output: Record<string, unknown> = {};\n\n\t\tfor (let i = 0; i < keys.length; i += batchSize * concurrency) {\n\t\t\tconst chunk = keys.slice(i, i + batchSize * concurrency);\n\t\t\tconst batches = Array.from(\n\t\t\t\t{ length: Math.ceil(chunk.length / batchSize) },\n\t\t\t\t(_, b) => {\n\t\t\t\t\tconst batchKeys = chunk.slice(b * batchSize, (b + 1) * batchSize);\n\t\t\t\t\tconst batch = Object.fromEntries(batchKeys.map((k) => [k, data[k]]));\n\t\t\t\t\treturn { batch, batchKeys };\n\t\t\t\t},\n\t\t\t);\n\n\t\t\tconst results = await Promise.allSettled(\n\t\t\t\tbatches.map(({ batch, batchKeys }) =>\n\t\t\t\t\tthis.translateBatch(batch, options, batchKeys),\n\t\t\t\t),\n\t\t\t);\n\n\t\t\tfor (let b = 0; b < results.length; b++) {\n\t\t\t\tconst result = results[b];\n\t\t\t\tif (result.status !== \"fulfilled\") {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`[rosetta] batch failed (${batches[b].batchKeys.length} keys):`,\n\t\t\t\t\t\tresult.reason?.message?.slice(0, 120),\n\t\t\t\t\t);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tObject.assign(output, result.value);\n\t\t\t}\n\t\t}\n\n\t\treturn output;\n\t}\n\n\tprivate async translateBatch(\n\t\tbatch: Record<string, unknown>,\n\t\toptions: TranslateDataOptions,\n\t\tbatchKeys: string[],\n\t): Promise<Record<string, unknown>> {\n\t\tconst retries = this.config.retries ?? 2;\n\t\tconst system = buildSystemPrompt({\n\t\t\tbrandVoice: this.config.brandVoice,\n\t\t\tglossary: this.config.glossary,\n\t\t\tsource: options.source,\n\t\t\ttarget: options.target,\n\t\t});\n\n\t\tconst user = buildDataPrompt(batch, {\n\t\t\t...options,\n\t\t\thints: options.hints\n\t\t\t\t? Object.fromEntries(\n\t\t\t\t\t\tbatchKeys.map((key) => [key, options.hints?.[key] ?? []]),\n\t\t\t\t\t)\n\t\t\t\t: undefined,\n\t\t});\n\n\t\tlet lastError: Error | undefined;\n\t\tfor (let attempt = 0; attempt <= retries; attempt++) {\n\t\t\ttry {\n\t\t\t\tconst res = await this.chat([\n\t\t\t\t\t{ role: \"system\", content: system },\n\t\t\t\t\t{ role: \"user\", content: user },\n\t\t\t\t]);\n\t\t\t\tif (!res.ok) {\n\t\t\t\t\tthrow new Error(`Rosetta: ${res.status} ${await res.text()}`);\n\t\t\t\t}\n\t\t\t\tconst json = (await res.json()) as {\n\t\t\t\t\tchoices: Array<{ message: { content: string } }>;\n\t\t\t\t};\n\t\t\t\treturn JSON.parse(json.choices[0].message.content);\n\t\t\t} catch (error) {\n\t\t\t\tlastError = error instanceof Error ? error : new Error(String(error));\n\t\t\t\tif (attempt < retries) {\n\t\t\t\t\tawait new Promise((ok) => setTimeout(ok, 2000 * (attempt + 1)));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tthrow lastError ?? new Error(\"Rosetta: batch failed\");\n\t}\n\n\tprivate async chat(messages: ChatMessage[]): Promise<Response> {\n\t\tconst baseURL = this.config.baseURL ?? \"https://openrouter.ai/api/v1\";\n\t\treturn fetch(`${baseURL}/chat/completions`, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\tAuthorization: `Bearer ${this.config.apiKey}`,\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tmodel: this.config.model,\n\t\t\t\ttemperature: this.config.temperature ?? 0.3,\n\t\t\t\tmessages,\n\t\t\t}),\n\t\t});\n\t}\n}\n","import { unstable_cache } from \"next/cache\";\nimport type { RosettaConfig, TranslateDataOptions } from \"./config\";\nimport { Rosetta } from \"./index\";\n\nexport type { RosettaConfig, TranslateDataOptions } from \"./config\";\n\n/**\n * Build a server-side Rosetta instance. Config comes from `overrides` first,\n * then the environment:\n *\n * - `OPENROUTER_API_KEY` — required\n * - `ROSETTA_MODEL` — default `anthropic/claude-sonnet-4.5`\n * - `ROSETTA_BASE_URL` — any OpenAI-compatible endpoint\n * - `ROSETTA_BRAND_VOICE` — default `*` brand voice briefing\n *\n * Import this only from server code — add `import \"server-only\"` at the top of\n * the module that calls it.\n */\nexport function createRosetta(overrides: Partial<RosettaConfig> = {}): Rosetta {\n\tconst apiKey = overrides.apiKey ?? process.env.OPENROUTER_API_KEY;\n\tif (!apiKey) {\n\t\tthrow new Error(\"rosetta-i18n: set OPENROUTER_API_KEY or pass apiKey.\");\n\t}\n\n\treturn new Rosetta({\n\t\tapiKey,\n\t\tmodel:\n\t\t\toverrides.model ??\n\t\t\tprocess.env.ROSETTA_MODEL ??\n\t\t\t\"anthropic/claude-sonnet-4.5\",\n\t\tbaseURL: overrides.baseURL ?? process.env.ROSETTA_BASE_URL,\n\t\tbrandVoice: overrides.brandVoice ?? {\n\t\t\tvariations: {\n\t\t\t\t\"*\": process.env.ROSETTA_BRAND_VOICE ?? \"\",\n\t\t\t},\n\t\t},\n\t\tglossary: overrides.glossary,\n\t\ttemperature: overrides.temperature,\n\t\tbatchSize: overrides.batchSize,\n\t\tconcurrency: overrides.concurrency,\n\t\tretries: overrides.retries,\n\t});\n}\n\nlet instance: Rosetta | undefined;\n\n/** Memoized `createRosetta()` for the current server process. */\nexport function getRosetta(): Rosetta {\n\tif (!instance) {\n\t\tinstance = createRosetta();\n\t}\n\treturn instance;\n}\n\nexport interface CacheOptions {\n\t/** Seconds to cache for, or `false` to cache indefinitely. */\n\trevalidate?: number | false;\n\t/** Cache tags for on-demand revalidation. */\n\ttags?: string[];\n}\n\n/**\n * Translate a payload behind Next's data cache, keyed on the source/target and\n * payload. Repeated renders reuse the cached result instead of re-calling the\n * model.\n */\nexport function cachedTranslate(\n\tdata: Record<string, unknown>,\n\toptions: TranslateDataOptions,\n\tcache: CacheOptions = {},\n): Promise<Record<string, unknown>> {\n\tconst run = unstable_cache(\n\t\t() => getRosetta().translate(data, options),\n\t\t[\n\t\t\t\"rosetta\",\n\t\t\toptions.source,\n\t\t\toptions.target,\n\t\t\toptions.context ?? \"\",\n\t\t\tJSON.stringify(data),\n\t\t],\n\t\t{\n\t\t\trevalidate: cache.revalidate ?? 60 * 60 * 24,\n\t\t\ttags: cache.tags,\n\t\t},\n\t);\n\treturn run();\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Rosetta } from "./index";
|
|
2
|
+
export declare const HELP: unique symbol;
|
|
3
|
+
export interface CatalogOptions {
|
|
4
|
+
/** Path to the source catalog JSON. */
|
|
5
|
+
input: string;
|
|
6
|
+
/** Target locales to write. */
|
|
7
|
+
targets: string[];
|
|
8
|
+
/** Source locale. */
|
|
9
|
+
source: string;
|
|
10
|
+
/** Directory to write `<target>.json` into. Defaults to the input's dir. */
|
|
11
|
+
outDir: string;
|
|
12
|
+
context?: string;
|
|
13
|
+
brandVoice?: string;
|
|
14
|
+
glossaryPath?: string;
|
|
15
|
+
model?: string;
|
|
16
|
+
baseURL?: string;
|
|
17
|
+
concurrency?: number;
|
|
18
|
+
batchSize?: number;
|
|
19
|
+
retries?: number;
|
|
20
|
+
/** Print the plan without calling the model or writing files. */
|
|
21
|
+
dryRun?: boolean;
|
|
22
|
+
/** Only translate keys missing from an existing target file. */
|
|
23
|
+
merge?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** Minimal surface the catalog runner needs — the real `Rosetta` or a fake. */
|
|
26
|
+
export interface TranslateClient {
|
|
27
|
+
translate(data: Record<string, unknown>, options: {
|
|
28
|
+
source: string;
|
|
29
|
+
target: string;
|
|
30
|
+
context?: string;
|
|
31
|
+
}): Promise<Record<string, unknown>>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parse CLI arguments for `translate-catalog`.
|
|
35
|
+
* Returns `HELP` for `--help`, throws a `UsageError` message otherwise.
|
|
36
|
+
*/
|
|
37
|
+
export declare function parseArgs(argv: string[]): CatalogOptions | typeof HELP;
|
|
38
|
+
/** Keys present in `input` but missing from `existing` (top-level). */
|
|
39
|
+
export declare function missingKeys(input: Record<string, unknown>, existing: Record<string, unknown>): Record<string, unknown>;
|
|
40
|
+
/**
|
|
41
|
+
* Translate a catalog into each target locale and write `<outDir>/<target>.json`.
|
|
42
|
+
* The `client` is only required when not running `--dry-run`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function runCatalog(options: CatalogOptions, client?: TranslateClient): Promise<void>;
|
|
45
|
+
/** Build a real Rosetta client from CLI options + env. */
|
|
46
|
+
export declare function buildClient(options: CatalogOptions): Promise<Rosetta>;
|
|
47
|
+
export declare function printHelp(): void;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { RosettaConfig, TranslateDataOptions } from "./config";
|
|
2
|
+
import { Rosetta } from "./index";
|
|
3
|
+
export type { RosettaConfig, TranslateDataOptions } from "./config";
|
|
4
|
+
/**
|
|
5
|
+
* Build a server-side Rosetta instance. Config comes from `overrides` first,
|
|
6
|
+
* then the environment:
|
|
7
|
+
*
|
|
8
|
+
* - `OPENROUTER_API_KEY` — required
|
|
9
|
+
* - `ROSETTA_MODEL` — default `anthropic/claude-sonnet-4.5`
|
|
10
|
+
* - `ROSETTA_BASE_URL` — any OpenAI-compatible endpoint
|
|
11
|
+
* - `ROSETTA_BRAND_VOICE` — default `*` brand voice briefing
|
|
12
|
+
*
|
|
13
|
+
* Import this only from server code — add `import "server-only"` at the top of
|
|
14
|
+
* the module that calls it.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createRosetta(overrides?: Partial<RosettaConfig>): Rosetta;
|
|
17
|
+
/** Memoized `createRosetta()` for the current server process. */
|
|
18
|
+
export declare function getRosetta(): Rosetta;
|
|
19
|
+
export interface CacheOptions {
|
|
20
|
+
/** Seconds to cache for, or `false` to cache indefinitely. */
|
|
21
|
+
revalidate?: number | false;
|
|
22
|
+
/** Cache tags for on-demand revalidation. */
|
|
23
|
+
tags?: string[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Translate a payload behind Next's data cache, keyed on the source/target and
|
|
27
|
+
* payload. Repeated renders reuse the cached result instead of re-calling the
|
|
28
|
+
* model.
|
|
29
|
+
*/
|
|
30
|
+
export declare function cachedTranslate(data: Record<string, unknown>, options: TranslateDataOptions, cache?: CacheOptions): Promise<Record<string, unknown>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rosetta-i18n",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"author": "Ian Hunter <ian@01.studio>",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Self-hosted AI translation engine — translate key-value content across locales through any OpenAI-compatible LLM, with brand voice and glossary enforcement.",
|
|
@@ -31,11 +31,18 @@
|
|
|
31
31
|
],
|
|
32
32
|
"main": "./dist/esm/index.js",
|
|
33
33
|
"types": "./dist/types/index.d.ts",
|
|
34
|
+
"bin": {
|
|
35
|
+
"rosetta-i18n": "./dist/esm/cli.js"
|
|
36
|
+
},
|
|
34
37
|
"exports": {
|
|
35
38
|
".": {
|
|
36
39
|
"types": "./dist/types/index.d.ts",
|
|
37
40
|
"default": "./dist/esm/index.js"
|
|
38
41
|
},
|
|
42
|
+
"./next": {
|
|
43
|
+
"types": "./dist/types/next.d.ts",
|
|
44
|
+
"default": "./dist/esm/next.js"
|
|
45
|
+
},
|
|
39
46
|
"./config": {
|
|
40
47
|
"types": "./dist/types/config.d.ts",
|
|
41
48
|
"default": "./dist/esm/index.js"
|
|
@@ -44,7 +51,7 @@
|
|
|
44
51
|
},
|
|
45
52
|
"scripts": {
|
|
46
53
|
"clean": "rm -rf ./dist",
|
|
47
|
-
"build": "rollup -c && tsc -p tsconfig.build.json",
|
|
54
|
+
"build": "rollup -c && tsc -p tsconfig.build.json && chmod +x dist/esm/cli.js",
|
|
48
55
|
"build:watch": "rollup -c -w",
|
|
49
56
|
"typecheck": "tsc --noEmit",
|
|
50
57
|
"test": "vitest run",
|
|
@@ -56,6 +63,14 @@
|
|
|
56
63
|
"lint": "biome check",
|
|
57
64
|
"lint:fix": "biome check --write"
|
|
58
65
|
},
|
|
66
|
+
"peerDependencies": {
|
|
67
|
+
"next": ">=14.0.0"
|
|
68
|
+
},
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"next": {
|
|
71
|
+
"optional": true
|
|
72
|
+
}
|
|
73
|
+
},
|
|
59
74
|
"devDependencies": {
|
|
60
75
|
"@biomejs/biome": "2.5.12",
|
|
61
76
|
"@types/node": "22.15.3",
|