From 2d5f03048a5e5a83a4465b357d3a5ac9bb30ab83 Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Tue, 25 Aug 2026 19:39:08 +0200 Subject: [PATCH] feat: add housekeeping domain registry --- .../housekeeping/domains/content/manifest.ts | 22 +++ .../housekeeping/domains/economy/manifest.ts | 15 ++ .../housekeeping/domains/hotel/manifest.ts | 19 +++ .../domains/operations/manifest.ts | 15 ++ .../housekeeping/domains/people/manifest.ts | 26 +++ .../housekeeping/domains/system/manifest.ts | 23 +++ .../foundation/localization-contract.test.ts | 57 +++++++ .../foundation/navigation.test.ts | 120 ++++++++++++++ .../housekeeping/foundation/navigation.ts | 39 +++++ .../housekeeping/foundation/registry.test.ts | 153 ++++++++++++++++++ .../housekeeping/foundation/registry.ts | 75 +++++++++ src/features/housekeeping/manifests.ts | 16 ++ src/messages/en.json | 57 +++++++ src/messages/it.json | 57 +++++++ 14 files changed, 694 insertions(+) create mode 100644 src/features/housekeeping/domains/content/manifest.ts create mode 100644 src/features/housekeeping/domains/economy/manifest.ts create mode 100644 src/features/housekeeping/domains/hotel/manifest.ts create mode 100644 src/features/housekeeping/domains/operations/manifest.ts create mode 100644 src/features/housekeeping/domains/people/manifest.ts create mode 100644 src/features/housekeeping/domains/system/manifest.ts create mode 100644 src/features/housekeeping/foundation/localization-contract.test.ts create mode 100644 src/features/housekeeping/foundation/navigation.test.ts create mode 100644 src/features/housekeeping/foundation/navigation.ts create mode 100644 src/features/housekeeping/foundation/registry.test.ts create mode 100644 src/features/housekeeping/foundation/registry.ts create mode 100644 src/features/housekeeping/manifests.ts diff --git a/src/features/housekeeping/domains/content/manifest.ts b/src/features/housekeeping/domains/content/manifest.ts new file mode 100644 index 00000000..729dd646 --- /dev/null +++ b/src/features/housekeeping/domains/content/manifest.ts @@ -0,0 +1,22 @@ +import { PERMS } from "@/lib/permission-slugs"; +import { + anyCapability, + type HousekeepingDomainManifest, +} from "../../foundation/contracts"; + +export const contentManifest = { + id: "content", + labelKey: "pages.housekeeping.domains.content.title", + descriptionKey: "pages.housekeeping.domains.content.description", + iconId: "file-text", + previewHref: "/admin-next/content", + capability: anyCapability( + PERMS.NEWS_VIEW, + PERMS.PAGES_VIEW, + PERMS.BANNERS_VIEW, + PERMS.EVENTS_VIEW, + PERMS.POLLS_VIEW, + PERMS.PREFIXES_VIEW, + ), + routes: [], +} satisfies HousekeepingDomainManifest; diff --git a/src/features/housekeeping/domains/economy/manifest.ts b/src/features/housekeeping/domains/economy/manifest.ts new file mode 100644 index 00000000..728a3f1c --- /dev/null +++ b/src/features/housekeeping/domains/economy/manifest.ts @@ -0,0 +1,15 @@ +import { PERMS } from "@/lib/permission-slugs"; +import { + anyCapability, + type HousekeepingDomainManifest, +} from "../../foundation/contracts"; + +export const economyManifest = { + id: "economy", + labelKey: "pages.housekeeping.domains.economy.title", + descriptionKey: "pages.housekeeping.domains.economy.description", + iconId: "gem", + previewHref: "/admin-next/economy", + capability: anyCapability(PERMS.CATALOG_VIEW, PERMS.SHOP_VIEW), + routes: [], +} satisfies HousekeepingDomainManifest; diff --git a/src/features/housekeeping/domains/hotel/manifest.ts b/src/features/housekeeping/domains/hotel/manifest.ts new file mode 100644 index 00000000..3e55782d --- /dev/null +++ b/src/features/housekeeping/domains/hotel/manifest.ts @@ -0,0 +1,19 @@ +import { PERMS } from "@/lib/permission-slugs"; +import { + anyCapability, + type HousekeepingDomainManifest, +} from "../../foundation/contracts"; + +export const hotelManifest = { + id: "hotel", + labelKey: "pages.housekeeping.domains.hotel.title", + descriptionKey: "pages.housekeeping.domains.hotel.description", + iconId: "hotel", + previewHref: "/admin-next/hotel", + capability: anyCapability( + PERMS.ROOMS_VIEW, + PERMS.RADIO_VIEW, + PERMS.ASSETS_IMPORT, + ), + routes: [], +} satisfies HousekeepingDomainManifest; diff --git a/src/features/housekeeping/domains/operations/manifest.ts b/src/features/housekeeping/domains/operations/manifest.ts new file mode 100644 index 00000000..d5b170a6 --- /dev/null +++ b/src/features/housekeeping/domains/operations/manifest.ts @@ -0,0 +1,15 @@ +import { PERMS } from "@/lib/permission-slugs"; +import { + anyCapability, + type HousekeepingDomainManifest, +} from "../../foundation/contracts"; + +export const operationsManifest = { + id: "operations", + labelKey: "pages.housekeeping.domains.operations.title", + descriptionKey: "pages.housekeeping.domains.operations.description", + iconId: "inbox", + previewHref: "/admin-next/operations", + capability: anyCapability(PERMS.ADMIN_DASHBOARD), + routes: [], +} satisfies HousekeepingDomainManifest; diff --git a/src/features/housekeeping/domains/people/manifest.ts b/src/features/housekeeping/domains/people/manifest.ts new file mode 100644 index 00000000..5b914e58 --- /dev/null +++ b/src/features/housekeeping/domains/people/manifest.ts @@ -0,0 +1,26 @@ +import { PERMS } from "@/lib/permission-slugs"; +import { + anyCapability, + type HousekeepingDomainManifest, +} from "../../foundation/contracts"; + +export const peopleManifest = { + id: "people", + labelKey: "pages.housekeeping.domains.people.title", + descriptionKey: "pages.housekeeping.domains.people.description", + iconId: "users", + previewHref: "/admin-next/people", + capability: anyCapability( + PERMS.USERS_VIEW, + PERMS.MODERATION_VIEW, + PERMS.TICKETS_VIEW, + PERMS.BANS_VIEW, + PERMS.MOD_DASHBOARD, + PERMS.MOD_CFH_VIEW, + PERMS.MOD_TEAM_VIEW, + PERMS.MOD_TICKETS_VIEW, + PERMS.MOD_USERS_VIEW, + PERMS.MOD_BANS_VIEW, + ), + routes: [], +} satisfies HousekeepingDomainManifest; diff --git a/src/features/housekeeping/domains/system/manifest.ts b/src/features/housekeeping/domains/system/manifest.ts new file mode 100644 index 00000000..91d4402e --- /dev/null +++ b/src/features/housekeeping/domains/system/manifest.ts @@ -0,0 +1,23 @@ +import { PERMS } from "@/lib/permission-slugs"; +import { + anyCapability, + type HousekeepingDomainManifest, +} from "../../foundation/contracts"; + +export const systemManifest = { + id: "system", + labelKey: "pages.housekeeping.domains.system.title", + descriptionKey: "pages.housekeeping.domains.system.description", + iconId: "settings", + previewHref: "/admin-next/system", + capability: anyCapability( + PERMS.SETTINGS_VIEW, + PERMS.LOGS_VIEW, + PERMS.ANALYTICS_VIEW, + PERMS.DEVOPS_VIEW, + PERMS.NOTIFICATIONS_VIEW, + PERMS.PERMISSIONS_MANAGE, + PERMS.RCON_EXECUTE, + ), + routes: [], +} satisfies HousekeepingDomainManifest; diff --git a/src/features/housekeeping/foundation/localization-contract.test.ts b/src/features/housekeeping/foundation/localization-contract.test.ts new file mode 100644 index 00000000..76aadf47 --- /dev/null +++ b/src/features/housekeeping/foundation/localization-contract.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from "vitest"; +import en from "@/messages/en.json"; +import itMessages from "@/messages/it.json"; +import { HOUSEKEEPING_MANIFESTS } from "../manifests"; + +const requiredKeys = [ + "pages.housekeeping.preview.badge", + "pages.housekeeping.preview.commandDisabled", + "pages.housekeeping.preview.backToSite", + "pages.housekeeping.navigation.skipToContent", + "pages.housekeeping.navigation.primary", + "pages.housekeeping.navigation.contextual", + "pages.housekeeping.states.loading.title", + "pages.housekeeping.states.loading.description", + "pages.housekeeping.states.empty.title", + "pages.housekeeping.states.empty.description", + "pages.housekeeping.states.partial.label", + "pages.housekeeping.states.partial.title", + "pages.housekeeping.states.partial.description", + "pages.housekeeping.states.error.title", + "pages.housekeeping.states.error.description", +]; + +function resolveMessage(messages: unknown, key: string): unknown { + return key.split(".").reduce((value, segment) => { + if (value === null || typeof value !== "object") return undefined; + + return (value as Record)[segment]; + }, messages); +} + +describe("housekeeping localization contract", () => { + it.each([ + ["English", en], + ["Italian", itMessages], + ])("provides the planned housekeeping subtree in %s", (_locale, messages) => { + const housekeeping = resolveMessage(messages, "pages.housekeeping"); + + expect(housekeeping).toEqual(expect.any(Object)); + expect(Object.keys(housekeeping as object)).toEqual([ + "preview", + "navigation", + "domains", + "states", + ]); + + for (const key of [ + ...HOUSEKEEPING_MANIFESTS.flatMap((manifest) => [ + manifest.labelKey, + manifest.descriptionKey, + ]), + ...requiredKeys, + ]) { + expect(resolveMessage(messages, key), key).toEqual(expect.any(String)); + } + }); +}); diff --git a/src/features/housekeeping/foundation/navigation.test.ts b/src/features/housekeeping/foundation/navigation.test.ts new file mode 100644 index 00000000..1e62343b --- /dev/null +++ b/src/features/housekeeping/foundation/navigation.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, it, vi } from "vitest"; +import { PERMS } from "@/lib/permission-slugs"; +import { anyCapability, type HousekeepingCapabilityContext } from "./contracts"; +import { buildHousekeepingNavigation } from "./navigation"; +import { createHousekeepingRegistry } from "./registry"; + +const context = ( + granted: readonly string[], +): HousekeepingCapabilityContext => ({ + actor: { id: 42, username: "moderator", rank: 3 }, + isSuperAdmin: false, + has: (slug) => granted.includes(slug), + hasAny: (...slugs) => slugs.some((slug) => granted.includes(slug)), + hasAll: (...slugs) => slugs.every((slug) => granted.includes(slug)), +}); + +describe("housekeeping navigation", () => { + it("shows People to a moderator with a mod view capability while hiding Economy", () => { + const registry = createHousekeepingRegistry([ + { + id: "people", + labelKey: "pages.housekeeping.domains.people.title", + descriptionKey: "pages.housekeeping.domains.people.description", + iconId: "users", + previewHref: "/admin-next/people", + capability: anyCapability(PERMS.MOD_CFH_VIEW), + routes: [], + }, + { + id: "economy", + labelKey: "pages.housekeeping.domains.economy.title", + descriptionKey: "pages.housekeeping.domains.economy.description", + iconId: "gem", + previewHref: "/admin-next/economy", + capability: anyCapability(PERMS.CATALOG_VIEW), + routes: [], + }, + ]); + + const navigation = buildHousekeepingNavigation( + registry, + context([PERMS.MOD_CFH_VIEW]), + (key) => key, + ); + + expect(navigation).toEqual([ + { + id: "people", + href: "/admin-next/people", + iconId: "users", + label: "pages.housekeeping.domains.people.title", + description: "pages.housekeeping.domains.people.description", + items: [], + }, + ]); + }); + + it("filters unauthorized domains and routes before translation", () => { + const registry = createHousekeepingRegistry([ + { + id: "people", + labelKey: "people.title", + descriptionKey: "people.description", + iconId: "users", + previewHref: "/admin-next/people", + capability: anyCapability(PERMS.USERS_VIEW), + routes: [ + { + id: "users", + labelKey: "people.users", + href: "/admin-next/people/users", + capability: anyCapability(PERMS.USERS_VIEW), + }, + { + id: "bans", + labelKey: "people.bans", + href: "/admin-next/people/bans", + capability: anyCapability(PERMS.BANS_VIEW), + }, + ], + }, + { + id: "system", + labelKey: "system.title", + descriptionKey: "system.description", + iconId: "settings", + previewHref: "/admin-next/system", + capability: anyCapability(PERMS.SETTINGS_VIEW), + routes: [], + }, + ]); + const translate = vi.fn((key: string) => `translated:${key}`); + + const navigation = buildHousekeepingNavigation( + registry, + context([PERMS.USERS_VIEW]), + translate, + ); + + expect(navigation).toEqual([ + { + id: "people", + href: "/admin-next/people", + iconId: "users", + label: "translated:people.title", + description: "translated:people.description", + items: [ + { + id: "users", + href: "/admin-next/people/users", + label: "translated:people.users", + }, + ], + }, + ]); + expect(translate).not.toHaveBeenCalledWith("people.bans"); + expect(translate).not.toHaveBeenCalledWith("system.title"); + expect(translate).not.toHaveBeenCalledWith("system.description"); + }); +}); diff --git a/src/features/housekeeping/foundation/navigation.ts b/src/features/housekeeping/foundation/navigation.ts new file mode 100644 index 00000000..5af2037f --- /dev/null +++ b/src/features/housekeeping/foundation/navigation.ts @@ -0,0 +1,39 @@ +import type { HousekeepingDomainId } from "../migration/types"; +import { satisfiesCapability } from "./capability-context"; +import type { + HousekeepingCapabilityContext, + HousekeepingDomainManifest, +} from "./contracts"; +import type { HousekeepingRegistry } from "./registry"; + +export interface HousekeepingNavigationDomain { + id: HousekeepingDomainId; + href: string; + iconId: HousekeepingDomainManifest["iconId"]; + label: string; + description: string; + items: readonly { id: string; href: string; label: string }[]; +} + +export function buildHousekeepingNavigation( + registry: HousekeepingRegistry, + context: HousekeepingCapabilityContext, + translate: (key: string) => string, +): readonly HousekeepingNavigationDomain[] { + return registry.domains + .filter((domain) => satisfiesCapability(context, domain.capability)) + .map((domain) => ({ + id: domain.id, + href: domain.previewHref, + iconId: domain.iconId, + label: translate(domain.labelKey), + description: translate(domain.descriptionKey), + items: domain.routes + .filter((route) => satisfiesCapability(context, route.capability)) + .map((route) => ({ + id: route.id, + href: route.href, + label: translate(route.labelKey), + })), + })); +} diff --git a/src/features/housekeeping/foundation/registry.test.ts b/src/features/housekeeping/foundation/registry.test.ts new file mode 100644 index 00000000..e5d57cbc --- /dev/null +++ b/src/features/housekeeping/foundation/registry.test.ts @@ -0,0 +1,153 @@ +import { describe, expect, it } from "vitest"; +import { PERMS } from "@/lib/permission-slugs"; +import { HOUSEKEEPING_MANIFESTS } from "../manifests"; +import { + HOUSEKEEPING_DOMAIN_IDS, + type HousekeepingDomainId, +} from "../migration/types"; +import { anyCapability, type HousekeepingDomainManifest } from "./contracts"; +import { createHousekeepingRegistry } from "./registry"; + +const manifest = ( + id: HousekeepingDomainId, + capabilitySlug = PERMS.ADMIN_DASHBOARD, +): HousekeepingDomainManifest => ({ + id, + labelKey: `pages.housekeeping.domains.${id}.title`, + descriptionKey: `pages.housekeeping.domains.${id}.description`, + iconId: "settings", + previewHref: `/admin-next/${id}`, + capability: anyCapability(capabilitySlug), + routes: [], +}); + +describe("housekeeping registry", () => { + it("registers the six approved domains in their locked order", () => { + const registry = createHousekeepingRegistry(HOUSEKEEPING_MANIFESTS); + + expect(registry.domains.map((domain) => domain.id)).toEqual( + HOUSEKEEPING_DOMAIN_IDS, + ); + expect(Object.isFrozen(registry.domains)).toBe(true); + }); + + it("rejects duplicate and unknown domain identifiers", () => { + expect(() => + createHousekeepingRegistry([ + manifest("operations"), + manifest("operations"), + ]), + ).toThrow("duplicate domain id"); + expect(() => + createHousekeepingRegistry([ + { ...manifest("operations"), id: "future" as HousekeepingDomainId }, + ]), + ).toThrow("unknown domain id"); + }); + + it("rejects invalid preview and empty translation keys", () => { + expect(() => + createHousekeepingRegistry([ + { ...manifest("people"), previewHref: "/admin-next/operations" }, + ]), + ).toThrow("invalid preview href"); + expect(() => + createHousekeepingRegistry([{ ...manifest("people"), labelKey: " " }]), + ).toThrow("empty label key"); + expect(() => + createHousekeepingRegistry([ + { ...manifest("people"), descriptionKey: "\t" }, + ]), + ).toThrow("empty description key"); + }); + + it("rejects duplicate route identities and hrefs", () => { + const base = manifest("people"); + + expect(() => + createHousekeepingRegistry([ + { + ...base, + routes: [ + { + id: "users", + labelKey: "pages.housekeeping.domains.people.title", + href: "/admin-next/people/users", + capability: anyCapability(PERMS.USERS_VIEW), + }, + { + id: "users", + labelKey: "pages.housekeeping.domains.people.title", + href: "/admin-next/people/staff", + capability: anyCapability(PERMS.USERS_VIEW), + }, + ], + }, + ]), + ).toThrow("duplicate route id"); + expect(() => + createHousekeepingRegistry([ + { + ...base, + routes: [ + { + id: "users", + labelKey: "pages.housekeeping.domains.people.title", + href: "/admin-next/people/users", + capability: anyCapability(PERMS.USERS_VIEW), + }, + { + id: "staff", + labelKey: "pages.housekeeping.domains.people.title", + href: "/admin-next/people/users", + capability: anyCapability(PERMS.USERS_VIEW), + }, + ], + }, + ]), + ).toThrow("duplicate route href"); + }); + + it("rejects empty route fields and unknown capability slugs", () => { + const route = { + id: "users", + labelKey: "pages.housekeeping.domains.people.title", + href: "/admin-next/people/users", + capability: anyCapability(PERMS.USERS_VIEW), + }; + + expect(() => + createHousekeepingRegistry([ + { ...manifest("people"), routes: [{ ...route, id: " " }] }, + ]), + ).toThrow("empty route id"); + expect(() => + createHousekeepingRegistry([ + { ...manifest("people"), routes: [{ ...route, href: " " }] }, + ]), + ).toThrow("empty route href"); + expect(() => + createHousekeepingRegistry([ + { ...manifest("people"), routes: [{ ...route, labelKey: " " }] }, + ]), + ).toThrow("empty route label key"); + expect(() => + createHousekeepingRegistry([ + { + ...manifest("system"), + capability: anyCapability("admin.ghost.view"), + }, + ]), + ).toThrow("unknown capability slug: admin.ghost.view"); + expect(() => + createHousekeepingRegistry([ + { + ...manifest("people"), + routes: [ + { ...route, capability: anyCapability("admin.ghost.route") }, + ], + }, + ]), + ).toThrow("unknown capability slug: admin.ghost.route"); + }); +}); diff --git a/src/features/housekeeping/foundation/registry.ts b/src/features/housekeeping/foundation/registry.ts new file mode 100644 index 00000000..dc6c58ae --- /dev/null +++ b/src/features/housekeeping/foundation/registry.ts @@ -0,0 +1,75 @@ +import { PERMS } from "@/lib/permission-slugs"; +import { HOUSEKEEPING_DOMAIN_IDS } from "../migration/types"; +import type { + CapabilityRequirement, + HousekeepingDomainManifest, +} from "./contracts"; + +const approvedDomainIds = new Set(HOUSEKEEPING_DOMAIN_IDS); +const knownCapabilitySlugs = new Set(Object.values(PERMS)); + +export interface HousekeepingRegistry { + domains: readonly HousekeepingDomainManifest[]; +} + +export function createHousekeepingRegistry( + manifests: readonly HousekeepingDomainManifest[], +): HousekeepingRegistry { + const domainIds = new Set(); + const routeIds = new Set(); + const routeHrefs = new Set(); + + for (const manifest of manifests) { + if (!approvedDomainIds.has(manifest.id)) { + throw new Error(`unknown domain id: ${manifest.id}`); + } + if (domainIds.has(manifest.id)) { + throw new Error(`duplicate domain id: ${manifest.id}`); + } + domainIds.add(manifest.id); + + if (manifest.previewHref !== `/admin-next/${manifest.id}`) { + throw new Error(`invalid preview href: ${manifest.previewHref}`); + } + validateNonEmpty(manifest.labelKey, "label key"); + validateNonEmpty(manifest.descriptionKey, "description key"); + validateCapability(manifest.capability); + + for (const route of manifest.routes) { + validateNonEmpty(route.id, "route id"); + if (routeIds.has(route.id)) { + throw new Error(`duplicate route id: ${route.id}`); + } + routeIds.add(route.id); + + validateNonEmpty(route.href, "route href"); + if (routeHrefs.has(route.href)) { + throw new Error(`duplicate route href: ${route.href}`); + } + routeHrefs.add(route.href); + + validateNonEmpty(route.labelKey, "route label key"); + validateCapability(route.capability); + } + } + + return { domains: Object.freeze([...manifests]) }; +} + +function validateNonEmpty(value: string, name: string): void { + if (!value.trim()) { + throw new Error(`empty ${name}`); + } +} + +function validateCapability(requirement: CapabilityRequirement): void { + if (requirement.slugs.length === 0) { + throw new Error("empty capability requirement"); + } + + for (const slug of requirement.slugs) { + if (!knownCapabilitySlugs.has(slug)) { + throw new Error(`unknown capability slug: ${slug}`); + } + } +} diff --git a/src/features/housekeeping/manifests.ts b/src/features/housekeeping/manifests.ts new file mode 100644 index 00000000..b8e32485 --- /dev/null +++ b/src/features/housekeeping/manifests.ts @@ -0,0 +1,16 @@ +import { contentManifest } from "./domains/content/manifest"; +import { economyManifest } from "./domains/economy/manifest"; +import { hotelManifest } from "./domains/hotel/manifest"; +import { operationsManifest } from "./domains/operations/manifest"; +import { peopleManifest } from "./domains/people/manifest"; +import { systemManifest } from "./domains/system/manifest"; +import type { HousekeepingDomainManifest } from "./foundation/contracts"; + +export const HOUSEKEEPING_MANIFESTS = [ + operationsManifest, + peopleManifest, + contentManifest, + economyManifest, + hotelManifest, + systemManifest, +] as const satisfies readonly HousekeepingDomainManifest[]; diff --git a/src/messages/en.json b/src/messages/en.json index 825ca795..fcb8fbb2 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -3255,6 +3255,63 @@ "failed": "Failed" } } + }, + "housekeeping": { + "preview": { + "badge": "Foundation preview", + "commandDisabled": "Search and commands are enabled in a later subproject.", + "backToSite": "Back to site" + }, + "navigation": { + "skipToContent": "Skip to content", + "primary": "Housekeeping domains", + "contextual": "Domain navigation" + }, + "domains": { + "operations": { + "title": "Operations", + "description": "Operational overview and daily queues" + }, + "people": { + "title": "People", + "description": "Users, moderation, and support" + }, + "content": { + "title": "Content", + "description": "Editorial content and community engagement" + }, + "economy": { + "title": "Economy", + "description": "Catalog and shop operations" + }, + "hotel": { + "title": "Hotel", + "description": "Rooms, radio, and asset tools" + }, + "system": { + "title": "System", + "description": "Configuration, observability, and access" + } + }, + "states": { + "loading": { + "title": "Loading housekeeping", + "description": "Preparing the available housekeeping tools." + }, + "empty": { + "title": "Nothing available", + "description": "No housekeeping content is available for this domain." + }, + "partial": { + "label": "Partial data", + "title": "Some information is unavailable", + "description": "Review the available information and try again later." + }, + "error": { + "title": "Unable to load housekeeping", + "description": "Try again later or contact an administrator." + } + } } } } diff --git a/src/messages/it.json b/src/messages/it.json index a53cbb4c..758f9b1b 100644 --- a/src/messages/it.json +++ b/src/messages/it.json @@ -3260,6 +3260,63 @@ "failed": "Failed" } } + }, + "housekeeping": { + "preview": { + "badge": "Anteprima della base", + "commandDisabled": "Ricerca e comandi saranno abilitati in un sottoprogetto successivo.", + "backToSite": "Torna al sito" + }, + "navigation": { + "skipToContent": "Vai al contenuto", + "primary": "Domini housekeeping", + "contextual": "Navigazione del dominio" + }, + "domains": { + "operations": { + "title": "Operazioni", + "description": "Panoramica operativa e code giornaliere" + }, + "people": { + "title": "Persone", + "description": "Utenti, moderazione e supporto" + }, + "content": { + "title": "Contenuti", + "description": "Contenuti editoriali e coinvolgimento della community" + }, + "economy": { + "title": "Economia", + "description": "Operazioni di catalogo e shop" + }, + "hotel": { + "title": "Hotel", + "description": "Stanze, radio e strumenti per gli asset" + }, + "system": { + "title": "Sistema", + "description": "Configurazione, osservabilità e accesso" + } + }, + "states": { + "loading": { + "title": "Caricamento housekeeping", + "description": "Preparazione degli strumenti housekeeping disponibili." + }, + "empty": { + "title": "Nessun contenuto disponibile", + "description": "Non ci sono contenuti housekeeping disponibili per questo dominio." + }, + "partial": { + "label": "Dati parziali", + "title": "Alcune informazioni non sono disponibili", + "description": "Controlla le informazioni disponibili e riprova più tardi." + }, + "error": { + "title": "Impossibile caricare housekeeping", + "description": "Riprova più tardi o contatta un amministratore." + } + } } } }