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,80 @@
|
||||
// tests/integration/platform/docs-i18n.test.ts
|
||||
import { rmSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import yaml from "js-yaml";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { buildTestApp, type TestApp } from "../test-server";
|
||||
|
||||
// The docs route resolves locale specs relative to apps/api/src (next to
|
||||
// openapi.yaml). Write a small German fixture spec there so serve-by-lang has
|
||||
// a real file to pick up, then clean it up. This avoids committing a full
|
||||
// generated locale spec into the repo just for the test.
|
||||
const apiSrc = join(dirname(fileURLToPath(import.meta.url)), "../../../apps/api/src");
|
||||
const deSpecPath = join(apiSrc, "openapi.de.yaml");
|
||||
|
||||
const DE_SPEC = {
|
||||
openapi: "3.1.0",
|
||||
info: { title: "SnapOtter API", version: "2.0.0", description: "Deutsche Beschreibung." },
|
||||
tags: [{ name: "Tools", description: "Werkzeuge." }],
|
||||
paths: {
|
||||
"/api/v1/tools/image/resize": {
|
||||
post: { tags: ["Tools"], summary: "Groesse aendern", description: "Bild skalieren." },
|
||||
},
|
||||
},
|
||||
"x-i18n": { locale: "de", entries: {} },
|
||||
};
|
||||
|
||||
describe("API docs i18n serving", () => {
|
||||
let testApp: TestApp;
|
||||
|
||||
beforeAll(async () => {
|
||||
writeFileSync(deSpecPath, yaml.dump(DE_SPEC, { lineWidth: -1 }), "utf8");
|
||||
testApp = await buildTestApp();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
rmSync(deSpecPath, { force: true });
|
||||
await testApp.cleanup();
|
||||
});
|
||||
|
||||
it("serves the German spec for ?lang=de", async () => {
|
||||
const res = await testApp.app.inject({ method: "GET", url: "/api/v1/openapi.yaml?lang=de" });
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.headers["content-type"]).toContain("text/yaml");
|
||||
expect(res.body).toContain("Groesse aendern");
|
||||
expect(res.body).toContain("locale: de");
|
||||
});
|
||||
|
||||
it("falls back to English for an unsupported lang", async () => {
|
||||
const res = await testApp.app.inject({ method: "GET", url: "/api/v1/openapi.yaml?lang=zz" });
|
||||
expect(res.statusCode).toBe(200);
|
||||
// English spec has the English summary, not the German one.
|
||||
expect(res.body).toContain("openapi: 3.1.0");
|
||||
expect(res.body).not.toContain("Groesse aendern");
|
||||
});
|
||||
|
||||
it("falls back to English for a supported locale with no spec file", async () => {
|
||||
// fr is supported but no openapi.fr.yaml exists.
|
||||
const res = await testApp.app.inject({ method: "GET", url: "/api/v1/openapi.yaml?lang=fr" });
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).not.toContain("Groesse aendern");
|
||||
});
|
||||
|
||||
it("keeps the default (no lang) response ASCII-only", async () => {
|
||||
const res = await testApp.app.inject({ method: "GET", url: "/api/v1/openapi.yaml" });
|
||||
const offending = [...res.body].find((ch) => ch.charCodeAt(0) > 0x7f);
|
||||
expect(offending).toBeUndefined();
|
||||
});
|
||||
|
||||
it("serves a localized llms.de.txt using translated tool strings", async () => {
|
||||
const res = await testApp.app.inject({ method: "GET", url: "/llms.de.txt" });
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.headers["content-type"]).toContain("text/plain");
|
||||
expect(res.body).toContain("## Tools");
|
||||
// Tag prose comes from the German fixture spec.
|
||||
expect(res.body).toContain("Werkzeuge.");
|
||||
// Tool lines come from shared i18n; the Resize tool id is present with a mode.
|
||||
expect(res.body).toContain("(resize, sync)");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user