mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Shared Claude Code translation pipeline (scripts/i18n, no API key) plus Astro/VitePress/Scalar i18n wiring. Landing and API reference translated into all 20 languages; docs i18n wiring + English source anchors. The translated docs markdown (apps/docs/<locale>/**, 3,620 files) follows in a companion PR because it exceeds GitHub's per-PR CI file limit.
62 lines
2.1 KiB
JavaScript
62 lines
2.1 KiB
JavaScript
// scripts/i18n/lib/claude.mjs
|
|
import { buildSystemPrompt } from "./glossary.mjs";
|
|
import { mask, restore } from "./mask.mjs";
|
|
|
|
const API_URL = "https://api.anthropic.com/v1/messages";
|
|
const MODEL = process.env.I18N_MODEL || "claude-sonnet-5"; // set I18N_MODEL=claude-opus-4-8 for max quality
|
|
const MAX_RETRIES = 5;
|
|
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
|
/**
|
|
* Default network sender: one Messages API call, returns the assistant text.
|
|
* @param {{ system: string, text: string }} args
|
|
* @returns {Promise<string>}
|
|
*/
|
|
async function defaultSend({ system, text }) {
|
|
const key = process.env.ANTHROPIC_API_KEY;
|
|
if (!key) throw new Error("ANTHROPIC_API_KEY is not set");
|
|
for (let attempt = 0; ; attempt++) {
|
|
const res = await fetch(API_URL, {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"x-api-key": key,
|
|
"anthropic-version": "2023-06-01",
|
|
},
|
|
body: JSON.stringify({
|
|
model: MODEL,
|
|
max_tokens: 8192,
|
|
system,
|
|
messages: [{ role: "user", content: text }],
|
|
}),
|
|
});
|
|
if (res.status === 429 || res.status >= 500) {
|
|
if (attempt >= MAX_RETRIES) throw new Error(`Messages API failed: ${res.status}`);
|
|
await sleep(1000 * 2 ** attempt);
|
|
continue;
|
|
}
|
|
if (!res.ok) throw new Error(`Messages API failed: ${res.status} ${await res.text()}`);
|
|
const json = await res.json();
|
|
return (json.content || []).map((c) => c.text || "").join("");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build a translate function: (units, locale) => Promise<Map<id, translatedText>>.
|
|
* `send` is injectable for tests. Each unit is masked before sending and restored after.
|
|
* @param {{ send?: (args: { system: string, text: string }) => Promise<string> }} [opts]
|
|
*/
|
|
export function makeTranslator({ send = defaultSend } = {}) {
|
|
return async function translate(units, locale) {
|
|
const system = buildSystemPrompt(locale);
|
|
const out = new Map();
|
|
for (const unit of units) {
|
|
const { masked, tokens } = mask(unit.sourceText);
|
|
const raw = await send({ system, text: masked });
|
|
out.set(unit.id, restore(raw, tokens));
|
|
}
|
|
return out;
|
|
};
|
|
}
|