diff --git a/messages/de.json b/messages/de.json index 6de2f0c33..40ecad1f3 100644 --- a/messages/de.json +++ b/messages/de.json @@ -980,7 +980,7 @@ }, "notifications": { "title": "Benachrichtigungen", - "description": "Telegram, ntfy, Web Push." + "description": "Zustellung pro Kanal — Telegram, ntfy, Web Push — und die Live-Übersicht der Kanalzuverlässigkeit." }, "dashboard": { "title": "Dashboard", diff --git a/messages/en.json b/messages/en.json index 4e4f09ab7..270de4ec3 100644 --- a/messages/en.json +++ b/messages/en.json @@ -980,7 +980,7 @@ }, "notifications": { "title": "Notifications", - "description": "Telegram, ntfy, Web Push." + "description": "Per-channel delivery — Telegram, ntfy, Web Push — plus the live channel-reliability board." }, "dashboard": { "title": "Dashboard", diff --git a/src/components/settings/__tests__/sections-i18n-parity.test.ts b/src/components/settings/__tests__/sections-i18n-parity.test.ts new file mode 100644 index 000000000..e1824614b --- /dev/null +++ b/src/components/settings/__tests__/sections-i18n-parity.test.ts @@ -0,0 +1,83 @@ +/** + * v1.4.16 phase B6 — settings i18n parity. + * + * Every slug declared in `SETTINGS_SECTION_SLUGS` MUST have a + * `settings.sections..title` AND a `settings.sections..description` + * resolved in both English and German. The audit doc + * (`docs/audit/v1416-settings-audit.md`) calls this out as the canonical + * shape — the sidebar, the page `

`, and the page subtitle all read + * from these two keys, so a missing key paints a raw key in production. + */ + +import { describe, expect, it } from "vitest"; +import en from "../../../../messages/en.json" with { type: "json" }; +import de from "../../../../messages/de.json" with { type: "json" }; +import { SETTINGS_SECTION_SLUGS } from "../section-slugs"; + +type Bag = Record; + +function lookup(bag: Bag, path: string[]): string | null { + let cursor: unknown = bag; + for (const segment of path) { + if (typeof cursor !== "object" || cursor === null) return null; + cursor = (cursor as Bag)[segment]; + } + return typeof cursor === "string" ? cursor : null; +} + +describe("settings sections — i18n parity (B6)", () => { + for (const slug of SETTINGS_SECTION_SLUGS) { + it(`every locale resolves settings.sections.${slug}.title`, () => { + const enValue = lookup(en as Bag, ["settings", "sections", slug, "title"]); + const deValue = lookup(de as Bag, ["settings", "sections", slug, "title"]); + expect(enValue, `EN missing title for ${slug}`).toBeTypeOf("string"); + expect(enValue?.length ?? 0).toBeGreaterThan(0); + expect(deValue, `DE missing title for ${slug}`).toBeTypeOf("string"); + expect(deValue?.length ?? 0).toBeGreaterThan(0); + }); + + it(`every locale resolves settings.sections.${slug}.description`, () => { + const enValue = lookup(en as Bag, [ + "settings", + "sections", + slug, + "description", + ]); + const deValue = lookup(de as Bag, [ + "settings", + "sections", + slug, + "description", + ]); + expect(enValue, `EN missing description for ${slug}`).toBeTypeOf("string"); + expect(enValue?.length ?? 0).toBeGreaterThan(0); + expect(deValue, `DE missing description for ${slug}`).toBeTypeOf("string"); + expect(deValue?.length ?? 0).toBeGreaterThan(0); + }); + } + + it("EN and DE description for every slug are different (catches accidental copy-paste)", () => { + for (const slug of SETTINGS_SECTION_SLUGS) { + const enValue = lookup(en as Bag, [ + "settings", + "sections", + slug, + "description", + ]); + const deValue = lookup(de as Bag, [ + "settings", + "sections", + slug, + "description", + ]); + // It's legitimate for some titles to coincide between locales when + // the proper noun is the same (e.g. "Dashboard", "API & Tokens"). + // Descriptions, however, MUST be translated — a copy-paste of the + // EN value into the DE bag is a bug. + expect( + deValue, + `DE description for ${slug} is identical to EN — needs translation`, + ).not.toBe(enValue); + } + }); +}); diff --git a/src/components/settings/__tests__/sections.test.tsx b/src/components/settings/__tests__/sections.test.tsx index 90e5cec30..4b1c82820 100644 --- a/src/components/settings/__tests__/sections.test.tsx +++ b/src/components/settings/__tests__/sections.test.tsx @@ -86,6 +86,12 @@ import { ApiSection } from "../api-section"; import { DashboardSection } from "../dashboard-section"; import { IntegrationsSection } from "../integrations-section"; import { NotificationsSection } from "../notifications-section"; +// v1.4.16 phase B6: route-level wrapper. The historic +// `` was renamed to `` and +// its filename changed from `thresholds-settings-section.tsx` to +// `thresholds-section.tsx` so the component name + filename match the +// slug like every other settings section. +import { ThresholdsSection } from "../thresholds-section"; function render(node: React.ReactElement, locale: "en" | "de" = "en") { return renderToStaticMarkup( @@ -180,4 +186,27 @@ describe("settings sections — SSR smoke", () => { expect(html).toContain("Jetzt prüfen"); expect(html).toContain("Quellen & Dokumentation"); }); + + it(" renders heading and embeds the editor (B6 rename)", () => { + // v1.4.16 phase B6 contract: the route wrapper is named + // `` (was ``), lives + // at `thresholds-section.tsx`, and embeds the inner editor under + // `` (was ``). The + // mock above stubs the editor as `data-testid="thresholds-editor"`, + // so this test asserts: + // - the wrapper renders without throwing + // - the localised page title surfaces + // - the editor mount-point is present + // - no raw i18n key leaks + const html = render(); + expect(html).toContain("Personal targets"); + expect(html).toContain('data-testid="thresholds-editor"'); + expect(html).not.toContain("settings.sections.thresholds."); + }); + + it(" resolves the German title (B6 rename)", () => { + const html = render(, "de"); + expect(html).toContain("Persönliche Zielwerte"); + expect(html).toContain('data-testid="thresholds-editor"'); + }); });