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 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
+ }