fix(i18n): one German word per thing across Settings

Anbieter instead of Provider, Standard instead of Default, Gesundheitsscore
for every spelling of the score, and Insights where the area was called
Auswertungen. The Coach's rate-limit notice no longer shows English in the
German interface. A guard reads the Settings namespaces of the German
catalog and fails on the retired spellings.
This commit is contained in:
Marc-André Bombeck committed 2026-10-21 19:48:29 +02:00
1 parent ec240bc07a
commit 131b48cc49
4 files changed
+79 -24

No files matched your search

@@ -0,0 +1,55 @@
/**
* The German Settings glossary: one word per thing.
*
* Settings used "Provider" beside "Anbieter", "Default" beside "Standard",
* and the score as "Health Score", "Health-Score", "Gesundheits-Score" and
* "Gesundheitsscore", sometimes on one screen. The settled words are
* Anbieter, Standard and Gesundheitsscore; the Insights area is "Insights"
* (the name already dominant in the catalog). This reads the German catalog
* under the Settings namespaces and fails on the retired spellings.
*
* Product names are not glossary words: "ChatGPT", "OpenAI" and the like are
* left alone, and so is "Provider" inside a code-shaped token.
*/
import { describe, expect, it } from "vitest";
import de from "../../messages/de.json";
const NAMESPACES = ["settings", "thresholds"] as const;
const RETIRED: ReadonlyArray<{ pattern: RegExp; use: string }> = [
{ pattern: /\bProvider\b/, use: "Anbieter" },
{ pattern: /\bDefaults?\b/, use: "Standard" },
{ pattern: /\bHealth[- ]?Score\b/i, use: "Gesundheitsscore" },
{ pattern: /\bGesundheits-Score\b/, use: "Gesundheitsscore" },
];
function walk(node: unknown, path: string, out: Array<[string, string]>) {
if (typeof node === "string") out.push([path, node]);
else if (node && typeof node === "object") {
for (const [key, value] of Object.entries(node)) {
walk(value, path ? `${path}.${key}` : key, out);
}
}
}
describe("German Settings glossary", () => {
const strings: Array<[string, string]> = [];
for (const ns of NAMESPACES) {
walk((de as Record<string, unknown>)[ns], ns, strings);
}
it("reads the catalog (an empty read is a broken walker)", () => {
expect(strings.length).toBeGreaterThan(500);
});
it("uses the settled words", () => {
const offenders: string[] = [];
for (const [key, value] of strings) {
for (const { pattern, use } of RETIRED) {
if (pattern.test(value)) offenders.push(`${key}: "${value}" → ${use}`);
}
}
expect(offenders).toEqual([]);
});
});
@@ -218,7 +218,7 @@ describe("<AiSection> — dropdown-driven UX (B2)", () => {
// both ship without the "KI"/"AI" prefix per the project-voice rule
// ("Aktiver KI-Provider" → "Aktiver Provider"). The picker below it is
// labelled for what it does: it chooses the form, not the order.
expect(html).toContain("Aktiver Provider");
expect(html).toContain("Aktiver Anbieter");
expect(html).toContain("Anbieter einrichten");
});