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.
102 lines
4.3 KiB
TypeScript
102 lines
4.3 KiB
TypeScript
// 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" });
|
|
});
|
|
});
|