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,76 @@
|
||||
// tests/unit/api/openapi-i18n.test.ts
|
||||
import { mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
generateLocaleLlmsTxt,
|
||||
resolveSpecFile,
|
||||
} from "../../../apps/api/src/routes/openapi-i18n.js";
|
||||
|
||||
describe("resolveSpecFile", () => {
|
||||
let dir: string;
|
||||
const setup = () => {
|
||||
dir = mkdtempSync(join(tmpdir(), "openapi-i18n-"));
|
||||
writeFileSync(join(dir, "openapi.yaml"), "openapi: 3.1.0\n", "utf8");
|
||||
writeFileSync(join(dir, "openapi.de.yaml"), "openapi: 3.1.0\nx-i18n:\n locale: de\n", "utf8");
|
||||
return dir;
|
||||
};
|
||||
|
||||
it("returns the locale file when it exists and the code is supported", () => {
|
||||
const d = setup();
|
||||
expect(resolveSpecFile(d, "de")).toBe(join(d, "openapi.de.yaml"));
|
||||
});
|
||||
|
||||
it("falls back to English for an unsupported code", () => {
|
||||
const d = setup();
|
||||
expect(resolveSpecFile(d, "zz")).toBe(join(d, "openapi.yaml"));
|
||||
});
|
||||
|
||||
it("falls back to English when the supported locale spec is absent", () => {
|
||||
const d = setup();
|
||||
// fr is a supported locale but no openapi.fr.yaml was written.
|
||||
expect(resolveSpecFile(d, "fr")).toBe(join(d, "openapi.yaml"));
|
||||
});
|
||||
|
||||
it("returns English for en and for an empty/undefined lang", () => {
|
||||
const d = setup();
|
||||
expect(resolveSpecFile(d, "en")).toBe(join(d, "openapi.yaml"));
|
||||
expect(resolveSpecFile(d, "")).toBe(join(d, "openapi.yaml"));
|
||||
expect(resolveSpecFile(d, undefined)).toBe(join(d, "openapi.yaml"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("generateLocaleLlmsTxt", () => {
|
||||
const spec = {
|
||||
info: { title: "SnapOtter API", version: "2.0.0" },
|
||||
tags: [{ name: "Tools", description: "Werkzeuge zur Dateiverarbeitung." }],
|
||||
paths: {
|
||||
"/api/v1/tools/image/resize": { post: { tags: ["Tools"], summary: "Groesse aendern" } },
|
||||
},
|
||||
};
|
||||
const sections = [{ id: "image", name: "Image" }];
|
||||
const toolsBySection = {
|
||||
image: [{ id: "resize", executionHint: "fast" as const }],
|
||||
};
|
||||
// Translated tool strings for de, as loadToolStrings(locale) returns them.
|
||||
const toolStrings = { resize: { name: "Groesse aendern", description: "Nach Pixeln." } };
|
||||
|
||||
it("uses translated tool names and descriptions from shared i18n", () => {
|
||||
const out = generateLocaleLlmsTxt({ spec, sections, toolsBySection, toolStrings });
|
||||
expect(out).toContain("# SnapOtter API");
|
||||
expect(out).toContain("Werkzeuge zur Dateiverarbeitung.");
|
||||
expect(out).toContain("Groesse aendern - Nach Pixeln. (resize, sync)");
|
||||
});
|
||||
|
||||
it("falls back to the tool id label when a translation is missing", () => {
|
||||
const out = generateLocaleLlmsTxt({
|
||||
spec,
|
||||
sections,
|
||||
toolsBySection,
|
||||
toolStrings: {}, // no translation available
|
||||
});
|
||||
// Still lists the tool by id with the sync mode, never crashes.
|
||||
expect(out).toContain("(resize, sync)");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user