From fc050bd3f8dec2eee7c41441a390d6a5c9249a3b Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 31 Aug 2026 21:07:17 +0200 Subject: [PATCH] feat(housekeeping): localize content route headings --- .../domains/content/pages/brand.tsx | 6 +- .../content/pages/content-page-frame.tsx | 1 + .../content/pages/content-route-copy.test.tsx | 91 +++++++++++++++++++ .../domains/content/pages/editorial.tsx | 6 +- .../domains/content/pages/engagement.tsx | 6 +- .../domains/content/pages/help.tsx | 6 +- .../domains/content/pages/localization.tsx | 6 +- .../domains/content/pages/media.tsx | 6 +- .../foundation/localization-contract.test.ts | 66 ++++++++++++++ .../foundation/page/route-copy.test.ts | 55 +++++++++++ .../foundation/page/route-copy.ts | 21 +++++ src/messages/en.json | 16 ++-- src/messages/it.json | 16 ++-- src/messages/nl.json | 16 ++-- 14 files changed, 288 insertions(+), 30 deletions(-) create mode 100644 src/features/housekeeping/domains/content/pages/content-route-copy.test.tsx create mode 100644 src/features/housekeeping/foundation/page/route-copy.test.ts create mode 100644 src/features/housekeeping/foundation/page/route-copy.ts diff --git a/src/features/housekeeping/domains/content/pages/brand.tsx b/src/features/housekeeping/domains/content/pages/brand.tsx index 1e0b2cd4..562b7c57 100644 --- a/src/features/housekeeping/domains/content/pages/brand.tsx +++ b/src/features/housekeeping/domains/content/pages/brand.tsx @@ -1,4 +1,5 @@ import { PERMS } from "@/lib/permission-slugs"; +import { resolveHousekeepingRouteTitle } from "../../../foundation/page/route-copy"; import type { HousekeepingPageInput } from "../../../route-handlers"; import { contentQuery } from "../queries/content-queries"; import { ContentCommandForm } from "./content-command-form"; @@ -9,6 +10,7 @@ import { } from "./content-page-frame"; export function ContentBrandPage({ + title, context, result, routeId, @@ -118,7 +120,7 @@ export function ContentBrandPage({ ) : null; return ( ; readonly routeId: import("../routes").ContentRouteId | null; diff --git a/src/features/housekeeping/domains/content/pages/content-route-copy.test.tsx b/src/features/housekeeping/domains/content/pages/content-route-copy.test.tsx new file mode 100644 index 00000000..afc73a94 --- /dev/null +++ b/src/features/housekeeping/domains/content/pages/content-route-copy.test.tsx @@ -0,0 +1,91 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { expect, it, vi } from "vitest"; +import type { HousekeepingCapabilityContext } from "../../../foundation/contracts"; +import { ok } from "../../../foundation/contracts"; +import { contentQuery } from "../queries/content-queries"; +import { renderContentBrandPage } from "./brand"; +import { renderContentEditorialPage } from "./editorial"; +import { renderContentEngagementPage } from "./engagement"; +import { renderContentHelpPage } from "./help"; +import { renderContentLocalizationPage } from "./localization"; +import { renderContentMediaPage } from "./media"; + +vi.mock("../queries/content-queries", () => ({ + contentQuery: { run: vi.fn() }, +})); + +const context: HousekeepingCapabilityContext = { + actor: { id: 42, username: "operator", rank: 7 }, + isSuperAdmin: false, + has: () => true, + hasAny: () => true, + hasAll: () => true, +}; + +const cases = [ + [ + "content.editorial.article-create", + "/ase-next/content/editorial/articles/new", + "editorial", + "New article", + renderContentEditorialPage, + ], + [ + "content.media.ad-create", + "/ase-next/content/media/ads/new", + "media", + "New advertisement", + renderContentMediaPage, + ], + [ + "content.engagement.event-create", + "/ase-next/content/engagement/events/create", + "engagement", + "New event", + renderContentEngagementPage, + ], + [ + "content.help.question-create", + "/ase-next/content/help/questions/new", + "help", + "New help article", + renderContentHelpPage, + ], + [ + "content.brand.theme-builder", + "/ase-next/content/brand/theme-builder", + "brand", + "Theme builder", + renderContentBrandPage, + ], + [ + "content.localization.cms", + "/ase-next/content/localization/cms", + "localization", + "CMS translations", + renderContentLocalizationPage, + ], +] as const; + +it.each(cases)( + "renders localized title for %s", + async (routeId, canonicalHref, kind, title, renderPage) => { + vi.mocked(contentQuery.run).mockResolvedValue( + ok({ kind, items: [], total: 0, partialDependencies: [] }, routeId), + ); + + const page = await renderPage({ + context, + match: { + routeId, + domain: "content", + params: {}, + canonicalHref, + }, + translate: (key) => (key === `routes.${routeId}` ? title : key), + }); + const html = renderToStaticMarkup(page); + + expect(html).toContain(`

${title}

`); + }, +); diff --git a/src/features/housekeeping/domains/content/pages/editorial.tsx b/src/features/housekeeping/domains/content/pages/editorial.tsx index fb749f32..6b3ac8a5 100644 --- a/src/features/housekeeping/domains/content/pages/editorial.tsx +++ b/src/features/housekeeping/domains/content/pages/editorial.tsx @@ -1,4 +1,5 @@ import { PERMS } from "@/lib/permission-slugs"; +import { resolveHousekeepingRouteTitle } from "../../../foundation/page/route-copy"; import type { HousekeepingPageInput } from "../../../route-handlers"; import { contentQuery } from "../queries/content-queries"; import { ContentCommandForm } from "./content-command-form"; @@ -9,6 +10,7 @@ import { } from "./content-page-frame"; export function ContentEditorialPage({ + title, context, result, routeId, @@ -17,7 +19,7 @@ export function ContentEditorialPage({ const canPages = context.has(PERMS.PAGES_EDIT); return ( { "Stiamo preparando gli strumenti di housekeeping disponibili.", ); }); + it("keeps route-specific Content copy in the primary operator locales", () => { + expect(en.pages.housekeeping.routes.content.editorial).toEqual({ + "article-create": "New article", + "article-detail": "Edit article", + articles: "Articles", + tags: "Tags", + "writeable-boxes": "Writable boxes", + }); + expect(nl.pages.housekeeping.routes.content.editorial).toEqual({ + "article-create": "Nieuw artikel", + "article-detail": "Artikel bewerken", + articles: "Artikelen", + tags: "Tags", + "writeable-boxes": "Bewerkbare tekstblokken", + }); + expect(itMessages.pages.housekeeping.routes.content.editorial).toEqual({ + "article-create": "Nuovo articolo", + "article-detail": "Modifica articolo", + articles: "Articoli", + tags: "Tag", + "writeable-boxes": "Riquadri modificabili", + }); + }); + it("uses concrete Content workflow names across the primary operator locales", () => { + const expectations = [ + [ + en, + "New advertisement", + "New event", + "New help article", + "Theme builder", + "CMS translations", + ], + [ + nl, + "Nieuwe advertentie", + "Nieuw evenement", + "Nieuw helpartikel", + "Thema-bouwer", + "CMS-vertalingen", + ], + [ + itMessages, + "Nuova pubblicitĂ ", + "Nuovo evento", + "Nuovo articolo di assistenza", + "Editor tema", + "Traduzioni CMS", + ], + ] as const; + for (const [ + messages, + media, + engagement, + help, + brand, + localization, + ] of expectations) { + const routes = messages.pages.housekeeping.routes.content; + expect(routes.media["ad-create"]).toBe(media); + expect(routes.engagement["event-create"]).toBe(engagement); + expect(routes.help["question-create"]).toBe(help); + expect(routes.brand["theme-builder"]).toBe(brand); + expect(routes.localization.cms).toBe(localization); + } + }); }); diff --git a/src/features/housekeeping/foundation/page/route-copy.test.ts b/src/features/housekeeping/foundation/page/route-copy.test.ts new file mode 100644 index 00000000..d8c66d1a --- /dev/null +++ b/src/features/housekeeping/foundation/page/route-copy.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import type { HousekeepingRouteRenderInput } from "../routing/route-handler"; +import { + resolveHousekeepingMessage, + resolveHousekeepingRouteTitle, +} from "./route-copy"; + +function input( + translate?: (key: string) => string, +): HousekeepingRouteRenderInput { + return { + context: { + actor: { id: 42, username: "operator", rank: 7 }, + isSuperAdmin: false, + has: () => true, + hasAny: () => true, + hasAll: () => true, + }, + match: { + routeId: "content.editorial.article-create", + domain: "content", + params: {}, + canonicalHref: "/ase-next/content/editorial/articles/new", + }, + ...(translate ? { translate } : {}), + }; +} + +describe("Housekeeping route copy", () => { + it("resolves the localized title from the concrete route ID", () => { + expect( + resolveHousekeepingRouteTitle( + input((key) => + key === "routes.content.editorial.article-create" + ? "New article" + : key, + ), + "Editorial content", + ), + ).toBe("New article"); + }); + + it("uses the explicit fallback when translation is absent or unresolved", () => { + expect(resolveHousekeepingRouteTitle(input(), "Editorial content")).toBe( + "Editorial content", + ); + expect( + resolveHousekeepingMessage( + input((key) => key), + "routes.content.editorial.article-create", + "Editorial content", + ), + ).toBe("Editorial content"); + }); +}); diff --git a/src/features/housekeeping/foundation/page/route-copy.ts b/src/features/housekeeping/foundation/page/route-copy.ts new file mode 100644 index 00000000..6fa75c1a --- /dev/null +++ b/src/features/housekeeping/foundation/page/route-copy.ts @@ -0,0 +1,21 @@ +import type { HousekeepingRouteRenderInput } from "../routing/route-handler"; + +export function resolveHousekeepingMessage( + input: Pick, + key: string, + fallback: string, +): string { + const value = input.translate?.(key); + return value && value !== key ? value : fallback; +} + +export function resolveHousekeepingRouteTitle( + input: Pick, + fallback: string, +): string { + return resolveHousekeepingMessage( + input, + `routes.${input.match.routeId}`, + fallback, + ); +} diff --git a/src/messages/en.json b/src/messages/en.json index 198e32fc..36ab47ef 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -3315,18 +3315,18 @@ "content": { "brand": { "favicon": "Favicon", - "theme-builder": "Theme Builder", + "theme-builder": "Theme builder", "theme": "Theme" }, "editorial": { - "article-create": "Editorial article Create", - "article-detail": "Editorial article Detail", + "article-create": "New article", + "article-detail": "Edit article", "articles": "Articles", "tags": "Tags", - "writeable-boxes": "Editorial writeable Boxes" + "writeable-boxes": "Writable boxes" }, "engagement": { - "event-create": "Engagement event Create", + "event-create": "New event", "event-detail": "Engagement event Detail", "event-types": "Engagement event Types", "events": "Events", @@ -3337,18 +3337,18 @@ }, "help": { "email-templates": "Help center", - "question-create": "Help center", + "question-create": "New help article", "question-detail": "Help center", "questions": "Help center" }, "localization": { "client": "Translations", - "cms": "Translations", + "cms": "CMS translations", "emulator": "Translations", "overview": "Translations" }, "media": { - "ad-create": "Media", + "ad-create": "New advertisement", "ad-detail": "Media", "ads": "Advertisements", "banners": "Banners", diff --git a/src/messages/it.json b/src/messages/it.json index 9f9dff8b..3025da9b 100644 --- a/src/messages/it.json +++ b/src/messages/it.json @@ -3315,18 +3315,18 @@ "content": { "brand": { "favicon": "Favicon", - "theme-builder": "Costruttore temi", + "theme-builder": "Editor tema", "theme": "Tema" }, "editorial": { - "article-create": "Editorial article Create", - "article-detail": "Editorial article Detail", + "article-create": "Nuovo articolo", + "article-detail": "Modifica articolo", "articles": "Articoli", "tags": "Tag", - "writeable-boxes": "Editorial writeable Boxes" + "writeable-boxes": "Riquadri modificabili" }, "engagement": { - "event-create": "Engagement event Create", + "event-create": "Nuovo evento", "event-detail": "Engagement event Detail", "event-types": "Engagement event Types", "events": "Eventi", @@ -3337,18 +3337,18 @@ }, "help": { "email-templates": "Centro assistenza", - "question-create": "Centro assistenza", + "question-create": "Nuovo articolo di assistenza", "question-detail": "Centro assistenza", "questions": "Centro assistenza" }, "localization": { "client": "Traduzioni", - "cms": "Traduzioni", + "cms": "Traduzioni CMS", "emulator": "Traduzioni", "overview": "Traduzioni" }, "media": { - "ad-create": "Media", + "ad-create": "Nuova pubblicitĂ ", "ad-detail": "Media", "ads": "PubblicitĂ ", "banners": "Banner", diff --git a/src/messages/nl.json b/src/messages/nl.json index 2f28bf12..04883aef 100644 --- a/src/messages/nl.json +++ b/src/messages/nl.json @@ -3220,18 +3220,18 @@ "content": { "brand": { "favicon": "Favicon", - "theme-builder": "Themabouwer", + "theme-builder": "Thema-bouwer", "theme": "Thema" }, "editorial": { - "article-create": "Editorial article Create", - "article-detail": "Editorial article Detail", + "article-create": "Nieuw artikel", + "article-detail": "Artikel bewerken", "articles": "Artikelen", "tags": "Tags", - "writeable-boxes": "Editorial writeable Boxes" + "writeable-boxes": "Bewerkbare tekstblokken" }, "engagement": { - "event-create": "Engagement event Create", + "event-create": "Nieuw evenement", "event-detail": "Engagement event Detail", "event-types": "Engagement event Types", "events": "Events", @@ -3242,18 +3242,18 @@ }, "help": { "email-templates": "Helpcentrum", - "question-create": "Helpcentrum", + "question-create": "Nieuw helpartikel", "question-detail": "Helpcentrum", "questions": "Helpcentrum" }, "localization": { "client": "Vertalingen", - "cms": "Vertalingen", + "cms": "CMS-vertalingen", "emulator": "Vertalingen", "overview": "Vertalingen" }, "media": { - "ad-create": "Media", + "ad-create": "Nieuwe advertentie", "ad-detail": "Media", "ads": "Advertenties", "banners": "Banners",