mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
feat(i18n): 21-language pipeline, landing/docs/API wiring, landing+API translations
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.
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
// tests/unit/scripts/i18n/core.test.ts
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { collectPending, runTranslation } from "../../../../scripts/i18n/core.mjs";
|
||||
import { hash } from "../../../../scripts/i18n/lib/hash.mjs";
|
||||
import { makeFakeAdapter } from "./fake-adapter";
|
||||
|
||||
const echo = (units: any[], locale: string) =>
|
||||
Promise.resolve(new Map(units.map((u) => [u.id, `${locale}:${u.sourceText}`])));
|
||||
|
||||
describe("runTranslation", () => {
|
||||
it("translates all units on a cold run", async () => {
|
||||
const adapter = makeFakeAdapter([{ id: "a", sourceText: "hi" }]);
|
||||
const summary = await runTranslation({ adapter, locales: ["de"], translate: echo });
|
||||
expect(summary.de.translated).toBe(1);
|
||||
expect((adapter._store.get("de") as Map<string, any>).get("a").text).toBe("de:hi");
|
||||
});
|
||||
|
||||
it("skips unchanged units on a second run (hash-gating)", async () => {
|
||||
const adapter = makeFakeAdapter([{ id: "a", sourceText: "hi" }]);
|
||||
const translate = vi.fn(echo);
|
||||
await runTranslation({ adapter, locales: ["de"], translate });
|
||||
translate.mockClear();
|
||||
const summary = await runTranslation({ adapter, locales: ["de"], translate });
|
||||
expect(translate).not.toHaveBeenCalled();
|
||||
expect(summary.de.skipped).toBe(1);
|
||||
});
|
||||
|
||||
it("re-translates a machine unit when its source changes", async () => {
|
||||
const units = [{ id: "a", sourceText: "hi" }];
|
||||
const adapter = makeFakeAdapter(units);
|
||||
await runTranslation({ adapter, locales: ["de"], translate: echo });
|
||||
units[0].sourceText = "hello";
|
||||
const summary = await runTranslation({ adapter, locales: ["de"], translate: echo });
|
||||
expect(summary.de.translated).toBe(1);
|
||||
expect((adapter._store.get("de") as Map<string, any>).get("a").text).toBe("de:hello");
|
||||
});
|
||||
|
||||
it("marks a human-edited unit stale instead of overwriting when source changes", async () => {
|
||||
const units = [{ id: "a", sourceText: "hi" }];
|
||||
const adapter = makeFakeAdapter(units);
|
||||
await runTranslation({ adapter, locales: ["de"], translate: echo });
|
||||
// Simulate a human refining the translation in the store.
|
||||
const entry = (adapter._store.get("de") as Map<string, any>).get("a");
|
||||
entry.text = "menschlich";
|
||||
// Source then changes upstream.
|
||||
units[0].sourceText = "hello";
|
||||
const summary = await runTranslation({ adapter, locales: ["de"], translate: echo });
|
||||
expect(summary.de.stale).toBe(1);
|
||||
expect(summary.de.translated).toBe(0);
|
||||
expect((adapter._store.get("de") as Map<string, any>).get("a").text).toBe("menschlich");
|
||||
});
|
||||
|
||||
it("records a failed unit and keeps prior text when validation fails", async () => {
|
||||
const adapter = makeFakeAdapter([{ id: "a", sourceText: "keep `code` here" }]);
|
||||
// Translator drops the code span -> validator rejects.
|
||||
const bad = (units: any[], locale: string) =>
|
||||
Promise.resolve(new Map(units.map((u) => [u.id, `${locale} no code`])));
|
||||
const summary = await runTranslation({ adapter, locales: ["de"], translate: bad });
|
||||
expect(summary.de.failed).toBe(1);
|
||||
expect(summary.de.translated).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectPending", () => {
|
||||
it("marks a new unit pending", () => {
|
||||
const units = [{ id: "a", sourceText: "hi" }];
|
||||
const { pending, merged, stats } = collectPending(units, new Map());
|
||||
expect(pending.map((p) => p.unit.id)).toEqual(["a"]);
|
||||
expect(stats).toMatchObject({ skipped: 0, stale: 0 });
|
||||
expect(merged.has("a")).toBe(false);
|
||||
});
|
||||
|
||||
it("skips a unit whose stored hash matches", () => {
|
||||
const units = [{ id: "a", sourceText: "hi" }];
|
||||
const stored = new Map([
|
||||
["a", { text: "de", sourceHash: hash("hi"), provenance: "machine", outputHash: hash("de") }],
|
||||
]);
|
||||
const { pending, stats } = collectPending(units, stored);
|
||||
expect(pending).toEqual([]);
|
||||
expect(stats.skipped).toBe(1);
|
||||
});
|
||||
|
||||
it("marks a human-edited unit stale when source changed, not pending", () => {
|
||||
const units = [{ id: "a", sourceText: "hello" }];
|
||||
const stored = new Map([
|
||||
[
|
||||
"a",
|
||||
{
|
||||
text: "menschlich",
|
||||
sourceHash: hash("hi"),
|
||||
provenance: "machine",
|
||||
outputHash: hash("de"),
|
||||
},
|
||||
],
|
||||
]);
|
||||
const { pending, merged, stats } = collectPending(units, stored);
|
||||
expect(pending).toEqual([]);
|
||||
expect(stats.stale).toBe(1);
|
||||
expect(merged.get("a")).toMatchObject({ provenance: "human", stale: true, text: "menschlich" });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user