rosetta-i18n 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/dist/esm/index.js +160 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/types/config.d.ts +42 -0
- package/dist/types/index.d.ts +28 -0
- package/dist/types/prompt.d.ts +17 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ian Hunter
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Rosetta i18n
|
|
2
|
+
|
|
3
|
+
Self-hosted AI translation engine. One text, every language — translate key-value content across locales through any OpenAI-compatible LLM, with brand voice and glossary enforcement.
|
|
4
|
+
|
|
5
|
+
Named after the Rosetta Stone: one text, three scripts, every language readable.
|
|
6
|
+
|
|
7
|
+
No vendor lock-in and no per-seat translation SaaS — bring your own LLM endpoint and API key.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add rosetta-i18n
|
|
13
|
+
# or
|
|
14
|
+
npm install rosetta-i18n
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { Rosetta } from "rosetta-i18n";
|
|
21
|
+
|
|
22
|
+
const rosetta = new Rosetta({
|
|
23
|
+
apiKey: process.env.OPENROUTER_API_KEY,
|
|
24
|
+
model: "anthropic/claude-sonnet-4.5",
|
|
25
|
+
brandVoice: {
|
|
26
|
+
variations: {
|
|
27
|
+
"*": "Confident, precise, editorial. Short sentences, active voice.",
|
|
28
|
+
es: "Tono editorial de gastronomía, accesible. Tercera persona.",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
glossary: {
|
|
32
|
+
es: { "award-winning": "premiado" },
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const translated = await rosetta.translate(
|
|
37
|
+
{ hero: "Every awarded restaurant in the world" },
|
|
38
|
+
{ source: "en", target: "es" },
|
|
39
|
+
);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Single strings
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const text = await rosetta.translateText("Every awarded restaurant", {
|
|
46
|
+
source: "en",
|
|
47
|
+
target: "ja",
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## How it works
|
|
52
|
+
|
|
53
|
+
Every request is sent to an OpenAI-compatible `/chat/completions` endpoint
|
|
54
|
+
(OpenRouter by default). The system prompt layers three signals, in this
|
|
55
|
+
precedence:
|
|
56
|
+
|
|
57
|
+
1. **Glossary** — exact term match, overrides model judgment
|
|
58
|
+
2. **Rules** — locale-specific conventions embedded in the brand voice
|
|
59
|
+
3. **Brand voice** — sets overall tone per locale
|
|
60
|
+
|
|
61
|
+
The user prompt carries the payload plus optional broad context and per-key
|
|
62
|
+
disambiguation hints.
|
|
63
|
+
|
|
64
|
+
## API
|
|
65
|
+
|
|
66
|
+
### `new Rosetta(config)`
|
|
67
|
+
|
|
68
|
+
| Option | Type | Default | Description |
|
|
69
|
+
| --- | --- | --- | --- |
|
|
70
|
+
| `apiKey` | `string` | — | **Required.** Key for the LLM endpoint. |
|
|
71
|
+
| `model` | `string` | — | **Required.** Model id, e.g. `anthropic/claude-sonnet-4.5`. |
|
|
72
|
+
| `brandVoice` | `BrandVoice` | — | **Required.** Tone + conventions per locale. `*` is the fallback. |
|
|
73
|
+
| `baseURL` | `string` | `https://openrouter.ai/api/v1` | Any OpenAI-compatible endpoint. |
|
|
74
|
+
| `glossary` | `Glossary` | `{}` | Exact term mappings per locale. |
|
|
75
|
+
| `temperature` | `number` | `0.3` | Sampling temperature. |
|
|
76
|
+
| `batchSize` | `number` | `25` | Keys per LLM request. |
|
|
77
|
+
| `concurrency` | `number` | `4` | Parallel batch requests in flight. |
|
|
78
|
+
| `retries` | `number` | `2` | Retries per batch on failure. |
|
|
79
|
+
|
|
80
|
+
### `rosetta.translate(data, options)`
|
|
81
|
+
|
|
82
|
+
Translates a key-value payload. Keys are preserved; only values are translated.
|
|
83
|
+
Batches run concurrently and failed batches are logged and skipped, so the
|
|
84
|
+
result carries only successfully translated keys — the caller can fall back to
|
|
85
|
+
the source locale for the rest.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
const out = await rosetta.translate(
|
|
89
|
+
{ hero: "…", subhead: "…" },
|
|
90
|
+
{
|
|
91
|
+
source: "en",
|
|
92
|
+
target: "pt-BR",
|
|
93
|
+
context: "restaurant listing page",
|
|
94
|
+
hints: { hero: ["home", "top banner"] },
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### `rosetta.translateText(text, options)`
|
|
100
|
+
|
|
101
|
+
Translates a single string. Throws on a non-OK response (unlike `translate`,
|
|
102
|
+
which degrades gracefully at the batch level).
|
|
103
|
+
|
|
104
|
+
## Releasing
|
|
105
|
+
|
|
106
|
+
Releases publish automatically from GitHub Actions via npm
|
|
107
|
+
[trusted publishing](https://docs.npmjs.com/trusted-publishers) (OIDC) — no npm
|
|
108
|
+
token required.
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pnpm bump patch # bump package.json, commit, tag, push
|
|
112
|
+
pnpm release # create the GitHub Release -> triggers npm-publish
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- `pnpm bump` accepts `patch`, `minor`, or `major` (default `patch`).
|
|
116
|
+
- `pnpm release` creates the GitHub Release for the latest tag with generated
|
|
117
|
+
release notes, which triggers `.github/workflows/npm-publish.yml`.
|
|
118
|
+
|
|
119
|
+
One-time setup (first publish and Trusted Publisher) is documented in
|
|
120
|
+
[`.github/workflows/npm-publish.yml`](.github/workflows/npm-publish.yml).
|
|
121
|
+
|
|
122
|
+
## Development
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
pnpm install
|
|
126
|
+
pnpm typecheck
|
|
127
|
+
pnpm lint
|
|
128
|
+
pnpm test
|
|
129
|
+
pnpm build
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Source lives in `src/`. `rollup` produces `dist/esm`, `tsc` emits declarations
|
|
133
|
+
to `dist/types`.
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT © Ian Hunter
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
function buildSystemPrompt(options) {
|
|
2
|
+
const { brandVoice, glossary, source, target } = options;
|
|
3
|
+
const voice = brandVoice.variations[target] ?? brandVoice.variations["*"] ?? "";
|
|
4
|
+
const parts = [voice];
|
|
5
|
+
if (glossary) {
|
|
6
|
+
const terms = glossary[target];
|
|
7
|
+
if (terms && Object.keys(terms).length > 0) {
|
|
8
|
+
parts.push(
|
|
9
|
+
`Glossary (use these exact renderings):
|
|
10
|
+
${Object.entries(terms).map(([src, tgt]) => `- "${src}" -> ${tgt}`).join("\n")}`
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
parts.push(
|
|
15
|
+
`Translate from ${source} to ${target}. Return ONLY the translated text \u2014 no explanations, no quotes around it.`
|
|
16
|
+
);
|
|
17
|
+
return parts.filter(Boolean).join("\n\n");
|
|
18
|
+
}
|
|
19
|
+
function buildDataPrompt(data, options) {
|
|
20
|
+
const lines = [
|
|
21
|
+
`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.`
|
|
22
|
+
];
|
|
23
|
+
if (options.context) {
|
|
24
|
+
lines.push(`Context: ${options.context}`);
|
|
25
|
+
}
|
|
26
|
+
if (options.hints) {
|
|
27
|
+
const hints = Object.entries(options.hints).map(([key, breadcrumb]) => `- ${key}: ${breadcrumb.join(" > ")}`).join("\n");
|
|
28
|
+
if (hints) {
|
|
29
|
+
lines.push(`Key disambiguation:
|
|
30
|
+
${hints}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
lines.push(JSON.stringify(data, null, 1));
|
|
34
|
+
return lines.join("\n\n");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class Rosetta {
|
|
38
|
+
config;
|
|
39
|
+
constructor(config) {
|
|
40
|
+
if (!config.apiKey) throw new Error("Rosetta: apiKey is required");
|
|
41
|
+
if (!config.model) throw new Error("Rosetta: model is required");
|
|
42
|
+
this.config = config;
|
|
43
|
+
}
|
|
44
|
+
/** Translate a single text string. */
|
|
45
|
+
async translateText(text, options) {
|
|
46
|
+
const system = buildSystemPrompt({
|
|
47
|
+
brandVoice: this.config.brandVoice,
|
|
48
|
+
glossary: this.config.glossary,
|
|
49
|
+
source: options.source,
|
|
50
|
+
target: options.target
|
|
51
|
+
});
|
|
52
|
+
const res = await this.chat([
|
|
53
|
+
{ role: "system", content: system },
|
|
54
|
+
{
|
|
55
|
+
role: "user",
|
|
56
|
+
content: options.context ? `${text}
|
|
57
|
+
|
|
58
|
+
Context: ${options.context}` : text
|
|
59
|
+
}
|
|
60
|
+
]);
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
throw new Error(`Rosetta: ${res.status} ${await res.text()}`);
|
|
63
|
+
}
|
|
64
|
+
const json = await res.json();
|
|
65
|
+
return json.choices[0].message.content.trim();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Translate a key-value payload, batched with concurrency. Failed batches
|
|
69
|
+
* are logged and skipped — the returned object carries only successfully
|
|
70
|
+
* translated keys, letting the caller fall back to the source locale for
|
|
71
|
+
* the rest.
|
|
72
|
+
*/
|
|
73
|
+
async translate(data, options) {
|
|
74
|
+
const batchSize = this.config.batchSize ?? 25;
|
|
75
|
+
const concurrency = this.config.concurrency ?? 4;
|
|
76
|
+
const keys = Object.keys(data);
|
|
77
|
+
const output = {};
|
|
78
|
+
for (let i = 0; i < keys.length; i += batchSize * concurrency) {
|
|
79
|
+
const chunk = keys.slice(i, i + batchSize * concurrency);
|
|
80
|
+
const batches = Array.from(
|
|
81
|
+
{ length: Math.ceil(chunk.length / batchSize) },
|
|
82
|
+
(_, b) => {
|
|
83
|
+
const batchKeys = chunk.slice(b * batchSize, (b + 1) * batchSize);
|
|
84
|
+
const batch = Object.fromEntries(batchKeys.map((k) => [k, data[k]]));
|
|
85
|
+
return { batch, batchKeys };
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
const results = await Promise.allSettled(
|
|
89
|
+
batches.map(
|
|
90
|
+
({ batch, batchKeys }) => this.translateBatch(batch, options, batchKeys)
|
|
91
|
+
)
|
|
92
|
+
);
|
|
93
|
+
for (let b = 0; b < results.length; b++) {
|
|
94
|
+
const result = results[b];
|
|
95
|
+
if (result.status !== "fulfilled") {
|
|
96
|
+
console.error(
|
|
97
|
+
`[rosetta] batch failed (${batches[b].batchKeys.length} keys):`,
|
|
98
|
+
result.reason?.message?.slice(0, 120)
|
|
99
|
+
);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
Object.assign(output, result.value);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return output;
|
|
106
|
+
}
|
|
107
|
+
async translateBatch(batch, options, batchKeys) {
|
|
108
|
+
const retries = this.config.retries ?? 2;
|
|
109
|
+
const system = buildSystemPrompt({
|
|
110
|
+
brandVoice: this.config.brandVoice,
|
|
111
|
+
glossary: this.config.glossary,
|
|
112
|
+
source: options.source,
|
|
113
|
+
target: options.target
|
|
114
|
+
});
|
|
115
|
+
const user = buildDataPrompt(batch, {
|
|
116
|
+
...options,
|
|
117
|
+
hints: options.hints ? Object.fromEntries(
|
|
118
|
+
batchKeys.map((key) => [key, options.hints?.[key] ?? []])
|
|
119
|
+
) : void 0
|
|
120
|
+
});
|
|
121
|
+
let lastError;
|
|
122
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
123
|
+
try {
|
|
124
|
+
const res = await this.chat([
|
|
125
|
+
{ role: "system", content: system },
|
|
126
|
+
{ role: "user", content: user }
|
|
127
|
+
]);
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
throw new Error(`Rosetta: ${res.status} ${await res.text()}`);
|
|
130
|
+
}
|
|
131
|
+
const json = await res.json();
|
|
132
|
+
return JSON.parse(json.choices[0].message.content);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
135
|
+
if (attempt < retries) {
|
|
136
|
+
await new Promise((ok) => setTimeout(ok, 2e3 * (attempt + 1)));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
throw lastError ?? new Error("Rosetta: batch failed");
|
|
141
|
+
}
|
|
142
|
+
async chat(messages) {
|
|
143
|
+
const baseURL = this.config.baseURL ?? "https://openrouter.ai/api/v1";
|
|
144
|
+
return fetch(`${baseURL}/chat/completions`, {
|
|
145
|
+
method: "POST",
|
|
146
|
+
headers: {
|
|
147
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
148
|
+
"Content-Type": "application/json"
|
|
149
|
+
},
|
|
150
|
+
body: JSON.stringify({
|
|
151
|
+
model: this.config.model,
|
|
152
|
+
temperature: this.config.temperature ?? 0.3,
|
|
153
|
+
messages
|
|
154
|
+
})
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export { Rosetta };
|
|
160
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../src/prompt.ts","../../src/index.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"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface BrandVoice {
|
|
2
|
+
/**
|
|
3
|
+
* Free-form briefing: tone, formality, audience, conventions. One text per
|
|
4
|
+
* target locale; `*` is the fallback for locales without their own text.
|
|
5
|
+
*/
|
|
6
|
+
variations: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
export type Glossary = Record<string, Record<string, string>>;
|
|
9
|
+
export interface RosettaConfig {
|
|
10
|
+
/** API key for the OpenAI-compatible endpoint. */
|
|
11
|
+
apiKey: string;
|
|
12
|
+
/** Model id (e.g. `anthropic/claude-sonnet-4.5` via OpenRouter). */
|
|
13
|
+
model: string;
|
|
14
|
+
/** OpenAI-compatible chat completions endpoint. */
|
|
15
|
+
baseURL?: string;
|
|
16
|
+
/** Brand voice: tone + conventions per locale. */
|
|
17
|
+
brandVoice: BrandVoice;
|
|
18
|
+
/** Exact term mappings per locale — highest precedence. */
|
|
19
|
+
glossary?: Glossary;
|
|
20
|
+
/** Sampling temperature (default 0.3 for translation consistency). */
|
|
21
|
+
temperature?: number;
|
|
22
|
+
/** Keys per LLM request when translating large payloads. */
|
|
23
|
+
batchSize?: number;
|
|
24
|
+
/** Parallel batch requests in flight. */
|
|
25
|
+
concurrency?: number;
|
|
26
|
+
/** Retries per batch on failure. */
|
|
27
|
+
retries?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface TranslateDataOptions {
|
|
30
|
+
source: string;
|
|
31
|
+
target: string;
|
|
32
|
+
/** Broad context: product surface, audience, purpose. */
|
|
33
|
+
context?: string;
|
|
34
|
+
/** Per-key hints that disambiguate short or overloaded text. */
|
|
35
|
+
hints?: Record<string, string[]>;
|
|
36
|
+
}
|
|
37
|
+
export interface TranslateTextOptions {
|
|
38
|
+
source: string;
|
|
39
|
+
target: string;
|
|
40
|
+
/** Broad context for this translation. */
|
|
41
|
+
context?: string;
|
|
42
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { RosettaConfig, TranslateDataOptions, TranslateTextOptions } from "./config";
|
|
2
|
+
export type { RosettaConfig, TranslateDataOptions, TranslateTextOptions, } from "./config";
|
|
3
|
+
/**
|
|
4
|
+
* Rosetta — self-hosted AI translation engine.
|
|
5
|
+
*
|
|
6
|
+
* Translates key-value content across locales through any OpenAI-compatible
|
|
7
|
+
* LLM, with brand voice, glossary, and per-locale rules applied on every
|
|
8
|
+
* call. Works with OpenRouter, direct Anthropic/OpenAI, or any
|
|
9
|
+
* OpenAI-compatible endpoint.
|
|
10
|
+
*
|
|
11
|
+
* Non-throwing at the batch level: failed batches are logged and skipped, so
|
|
12
|
+
* partial translations degrade gracefully to the source locale.
|
|
13
|
+
*/
|
|
14
|
+
export declare class Rosetta {
|
|
15
|
+
private config;
|
|
16
|
+
constructor(config: RosettaConfig);
|
|
17
|
+
/** Translate a single text string. */
|
|
18
|
+
translateText(text: string, options: TranslateTextOptions): Promise<string>;
|
|
19
|
+
/**
|
|
20
|
+
* Translate a key-value payload, batched with concurrency. Failed batches
|
|
21
|
+
* are logged and skipped — the returned object carries only successfully
|
|
22
|
+
* translated keys, letting the caller fall back to the source locale for
|
|
23
|
+
* the rest.
|
|
24
|
+
*/
|
|
25
|
+
translate(data: Record<string, unknown>, options: TranslateDataOptions): Promise<Record<string, unknown>>;
|
|
26
|
+
private translateBatch;
|
|
27
|
+
private chat;
|
|
28
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { BrandVoice, Glossary, TranslateDataOptions } from "./config";
|
|
2
|
+
/**
|
|
3
|
+
* Builds the system prompt for a translation request: brand voice, glossary
|
|
4
|
+
* enforcement, locale conventions, and output-shape rules.
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildSystemPrompt(options: {
|
|
7
|
+
brandVoice: BrandVoice;
|
|
8
|
+
glossary?: Glossary;
|
|
9
|
+
source: string;
|
|
10
|
+
target: string;
|
|
11
|
+
}): string;
|
|
12
|
+
/**
|
|
13
|
+
* Builds the user prompt for a key-value payload translation.
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildDataPrompt(data: Record<string, unknown>, options: TranslateDataOptions & {
|
|
16
|
+
hints?: Record<string, string[]>;
|
|
17
|
+
}): string;
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rosetta-i18n",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"author": "Ian Hunter <ian@01.studio>",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "Self-hosted AI translation engine — translate key-value content across locales through any OpenAI-compatible LLM, with brand voice and glossary enforcement.",
|
|
7
|
+
"homepage": "https://github.com/ian/rosetta#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/ian/rosetta.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/ian/rosetta/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"translation",
|
|
17
|
+
"i18n",
|
|
18
|
+
"localization",
|
|
19
|
+
"llm",
|
|
20
|
+
"ai",
|
|
21
|
+
"openai",
|
|
22
|
+
"openrouter",
|
|
23
|
+
"rosetta-i18n"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"main": "./dist/esm/index.js",
|
|
33
|
+
"types": "./dist/types/index.d.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/types/index.d.ts",
|
|
37
|
+
"default": "./dist/esm/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./config": {
|
|
40
|
+
"types": "./dist/types/config.d.ts",
|
|
41
|
+
"default": "./dist/esm/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./package.json": "./package.json"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"clean": "rm -rf ./dist",
|
|
47
|
+
"build": "rollup -c && tsc -p tsconfig.build.json",
|
|
48
|
+
"build:watch": "rollup -c -w",
|
|
49
|
+
"typecheck": "tsc --noEmit",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"test:watch": "vitest",
|
|
52
|
+
"bump": "./scripts/bump",
|
|
53
|
+
"release": "./scripts/release",
|
|
54
|
+
"format": "biome format",
|
|
55
|
+
"format:fix": "biome format --write",
|
|
56
|
+
"lint": "biome check",
|
|
57
|
+
"lint:fix": "biome check --write"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@biomejs/biome": "2.5.12",
|
|
61
|
+
"@types/node": "22.15.3",
|
|
62
|
+
"rollup": "^4.63.1",
|
|
63
|
+
"rollup-plugin-esbuild": "^6.1.1",
|
|
64
|
+
"typescript": "5.9.2",
|
|
65
|
+
"vitest": "^3.2.7"
|
|
66
|
+
},
|
|
67
|
+
"engines": {
|
|
68
|
+
"node": ">=18"
|
|
69
|
+
},
|
|
70
|
+
"packageManager": "pnpm@9.7.1"
|
|
71
|
+
}
|