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.
117 lines
4.0 KiB
JavaScript
117 lines
4.0 KiB
JavaScript
// scripts/i18n/translate.mjs
|
|
import { mkdirSync } from "node:fs";
|
|
import { ADAPTERS, resolveSurfaces } from "./adapters/registry.mjs";
|
|
import { collectPending, runTranslation } from "./core.mjs";
|
|
import { batchResultTranslator, readDone, writePending } from "./lib/batch.mjs";
|
|
import { localeCodes } from "./lib/shared-i18n.mjs";
|
|
|
|
function parseArgs(argv) {
|
|
const args = {
|
|
surface: "all",
|
|
locale: "all",
|
|
engine: "claude-code",
|
|
mode: "export",
|
|
batchDir: ".i18n-batches",
|
|
help: false,
|
|
};
|
|
for (const a of argv) {
|
|
if (a === "--help" || a === "-h") args.help = true;
|
|
else if (a === "--export") args.mode = "export";
|
|
else if (a === "--import") args.mode = "import";
|
|
else if (a === "--dry-run") args.mode = "dry-run";
|
|
else if (a.startsWith("--engine=")) args.engine = a.slice("--engine=".length);
|
|
else if (a.startsWith("--surface=")) args.surface = a.slice("--surface=".length);
|
|
else if (a.startsWith("--locale=")) args.locale = a.slice("--locale=".length);
|
|
else if (a.startsWith("--batch-dir=")) args.batchDir = a.slice("--batch-dir=".length);
|
|
}
|
|
return args;
|
|
}
|
|
|
|
function resolveLocales(spec) {
|
|
const all = localeCodes().filter((c) => c !== "en");
|
|
if (spec === "all") return all;
|
|
return spec
|
|
.split(",")
|
|
.map((s) => s.trim())
|
|
.filter((c) => all.includes(c));
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
if (args.help) {
|
|
console.log(
|
|
[
|
|
"Usage: tsx scripts/i18n/translate.mjs --export|--import|--dry-run [options]",
|
|
" --surface=all|<name,..> default all",
|
|
" --locale=all|<code,..> default all (minus en)",
|
|
" --engine=claude-code|api default claude-code (no key). api needs ANTHROPIC_API_KEY.",
|
|
" --batch-dir=<path> default .i18n-batches",
|
|
"",
|
|
"Claude Code flow: --export writes pending masked strings; a Claude Code session",
|
|
"translates each <surface>.<locale>.pending.json into <surface>.<locale>.done.json",
|
|
"(keeping every mask token); --import restores, validates, and writes via adapters.",
|
|
`Surfaces: ${Object.keys(ADAPTERS).join(", ") || "(none registered yet)"}`,
|
|
].join("\n"),
|
|
);
|
|
return;
|
|
}
|
|
|
|
const surfaces = resolveSurfaces(args.surface);
|
|
const locales = resolveLocales(args.locale);
|
|
if (surfaces.length === 0) {
|
|
console.log("No adapters registered yet. Nothing to do.");
|
|
return;
|
|
}
|
|
mkdirSync(args.batchDir, { recursive: true });
|
|
|
|
for (const surface of surfaces) {
|
|
const mod = await ADAPTERS[surface]();
|
|
const adapter = mod.adapter;
|
|
|
|
if (args.mode === "export") {
|
|
const units = await adapter.extract();
|
|
for (const locale of locales) {
|
|
const stored = await adapter.load(locale);
|
|
const { pending } = collectPending(units, stored);
|
|
if (pending.length === 0) continue;
|
|
const path = writePending(args.batchDir, surface, locale, pending);
|
|
console.log(`export ${surface} ${locale}: ${pending.length} -> ${path}`);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (args.mode === "import") {
|
|
for (const locale of locales) {
|
|
let doneMap;
|
|
try {
|
|
doneMap = readDone(args.batchDir, surface, locale);
|
|
} catch {
|
|
continue; // no done file for this locale yet
|
|
}
|
|
const summary = await runTranslation({
|
|
adapter,
|
|
locales: [locale],
|
|
translate: batchResultTranslator(doneMap),
|
|
log: (m) => process.stdout.write(`${m}\n`),
|
|
});
|
|
console.log(`import ${surface} ${locale}:`, JSON.stringify(summary[locale]));
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// dry-run: stub translator, no batch files, proves wiring
|
|
const summary = await runTranslation({
|
|
adapter,
|
|
locales,
|
|
translate: async (units, locale) =>
|
|
new Map(units.map((u) => [u.id, `${locale}:${u.sourceText}`])),
|
|
});
|
|
console.log(`dry-run ${surface}:`, JSON.stringify(summary, null, 2));
|
|
}
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|