i18n bis in die Terminal- und Panel-Fenster

Uebersetzt waren bisher nur die Vue-Fenster; Terminal, Panel und Popup hatten
ihre Beschriftungen hart auf Deutsch im Markup und in den Views -- ein
englisches UI blieb zur Haelfte deutsch.

Die Tabellen ziehen nach messages.ts um, dazu ein Vue-freier t() und ein
applyI18n(), das statisches Markup ueber data-i18n-Marker beschriftet.
i18n.ts bleibt der vue-i18n-Wrapper fuer die Vue-Fenster. So kommt kein Vue
in die Terminal- und Panel-Bundles (Terminal 657,02 -> 657,06 kB).
Neue Namensraeume: panel, popup, commands, archiveForm, search, wiki.
toLocaleTimeString folgt der Sprache statt fest 'de'.

Zwei Funde nebenbei:
- window.localStorage wird ueber try/catch gelesen. Gesperrter Storage
  (privater Modus) wirft schon beim Lesen, und unter Node 22 ueberschattet
  ein undefiniertes globales localStorage das der Testumgebung.
- Die Frontend-Tests liefen unbemerkt gegen happy-doms en-US.
  test-setup.ts nagelt die Sprache fest.

49 Tests (39 bestehende, 10 neue fuer Tabellen-Paritaet, t() und applyI18n).
This commit is contained in:
marcusH
2026-07-20 22:36:19 +02:00
parent 92f34cb64d
commit 1fa9ffcf1b
17 changed files with 746 additions and 386 deletions
+123
View File
@@ -0,0 +1,123 @@
// Übersetzungstabellen und der Vue-freie t()/applyI18n()-Helfer.
import { afterEach, describe, expect, it } from "vitest";
import { applyI18n, messages, storedLocale, t } from "./messages";
/// Alle Punkt-Pfade eines Nachrichtenbaums, für den Vollständigkeitsabgleich.
function keys(obj: object, prefix = ""): string[] {
return Object.entries(obj).flatMap(([k, v]) =>
typeof v === "object" && v !== null
? keys(v, `${prefix}${k}.`)
: [`${prefix}${k}`],
);
}
afterEach(() => {
try {
window.localStorage.removeItem("lang");
} catch {
/* Storage in der Testumgebung nicht verfügbar */
}
document.body.textContent = "";
});
describe("Nachrichtentabellen", () => {
it("führen dieselben Schlüssel in beiden Sprachen", () => {
expect(keys(messages.en).sort()).toEqual(keys(messages.de).sort());
});
it("lassen keinen Text unübersetzt stehen", () => {
// Gleiche Texte sind fast immer eine vergessene Übersetzung. Erlaubt sind
// nur Eigennamen und Zeichen ohne Sprache.
const erlaubt = new Set([
"Pool",
"Pools",
"Name",
"Theme",
"Wiki",
"MD",
"Autostart",
"Input",
"Output",
"Session",
"Cache ↑",
"Cache ↓",
"+ oAuth",
"+ apiKey",
"Terminal",
"Terminal — {name}",
".desktop-Starter",
".ai-control/ (Config + Icon)",
"Archiv-Berechtigung in .claude/settings.json",
"Todo-Hook in .claude/settings.json",
"OFFENE-PUNKTE.md bei jedem Sessionstart einspielen",
]);
const gleich = keys(messages.de).filter((k) => {
const de = pick(messages.de, k);
return de === pick(messages.en, k) && !erlaubt.has(de);
});
expect(gleich).toEqual([]);
});
});
function pick(obj: object, key: string): string {
return key
.split(".")
.reduce<never>((o, part) => (o as Record<string, never>)[part], obj as never);
}
describe("t()", () => {
it("löst Punkt-Pfade auf", () => {
expect(t("panel.tabDraft")).toBe("Dokument");
});
it("setzt Platzhalter ein", () => {
expect(t("search.noHits", { scope: "„x“" })).toBe("Keine Treffer für „x“");
});
it("lässt unbekannte Platzhalter stehen", () => {
expect(t("wiki.emptyTag", { falsch: "x" })).toBe("Keine Dokumente mit #{tag}.");
});
it("gibt bei fehlendem Schlüssel den Pfad zurück", () => {
expect(t("panel.gibtEsNicht")).toBe("panel.gibtEsNicht");
expect(t("nichts.da.tief")).toBe("nichts.da.tief");
});
it("folgt der gespeicherten Sprache", () => {
window.localStorage.setItem("lang", "en");
expect(storedLocale()).toBe("en");
expect(t("panel.tabDraft")).toBe("Document");
});
it("fällt bei unbekannter Sprache auf die Browsersprache zurück", () => {
window.localStorage.setItem("lang", "kli");
expect(storedLocale()).toBe("de"); // test-setup.ts pinnt navigator.language
});
});
describe("applyI18n()", () => {
it("beschriftet Text und Attribute", () => {
document.body.innerHTML = `
<button data-i18n="panel.tabWiki" data-i18n-title="panel.tabWikiTitle">alt</button>
<button data-i18n-aria="panel.close"></button>
<input data-i18n-placeholder="search.placeholder" />`;
applyI18n();
const btn = document.querySelector("button")!;
expect(btn.textContent).toBe("Wiki");
expect(btn.title).toBe("Archiv-Wiki");
expect(document.querySelectorAll("button")[1].getAttribute("aria-label")).toBe(
"Schließen",
);
expect(document.querySelector("input")!.placeholder).toBe(
"Archiv durchsuchen — #tag filtert",
);
expect(document.documentElement.lang).toBe("de");
});
it("lässt Markup ohne Marker unangetastet", () => {
document.body.innerHTML = `<span>unberührt</span>`;
applyI18n();
expect(document.querySelector("span")!.textContent).toBe("unberührt");
});
});