feat(housekeeping): add localized route states
This commit is contained in:
1 parent
2ed00bb949
commit
19fb8be7fb
12 files changed
+468
-30
No files matched your search
@@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "@/components/link";
|
||||
import { HousekeepingPageState } from "@/features/housekeeping/foundation/page/housekeeping-page-state";
|
||||
|
||||
interface HousekeepingErrorProps {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export default function HousekeepingError({
|
||||
error,
|
||||
reset,
|
||||
}: HousekeepingErrorProps) {
|
||||
const translate = useTranslations("pages.housekeeping");
|
||||
|
||||
return (
|
||||
<main className="mx-auto flex min-h-[60vh] max-w-xl items-center px-4 py-12">
|
||||
<div className="w-full">
|
||||
<HousekeepingPageState
|
||||
state="error"
|
||||
headingLevel="h1"
|
||||
title={translate("states.error.title")}
|
||||
description={translate("states.error.description")}
|
||||
retryAction={
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={reset}
|
||||
className="rounded-md bg-[var(--admin-accent)] px-3 py-2 text-sm font-medium text-[var(--admin-accent-foreground)]"
|
||||
>
|
||||
{translate("states.retry")}
|
||||
</button>
|
||||
<Link
|
||||
href="/ase-next"
|
||||
className="rounded-md border border-[var(--admin-border)] px-3 py-2 text-sm font-medium text-[var(--admin-text)] hover:bg-[var(--admin-canvas)]"
|
||||
>
|
||||
{translate("states.backToHousekeeping")}
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{error.digest ? (
|
||||
<p className="mt-3 text-xs text-[var(--admin-text-muted)]">
|
||||
{translate("states.supportReference", {
|
||||
reference: error.digest,
|
||||
})}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,32 +1,28 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import Link from "@/components/link";
|
||||
import { HousekeepingPageState } from "@/features/housekeeping/foundation/page/housekeeping-page-state";
|
||||
|
||||
export default async function HousekeepingForbidden() {
|
||||
const translate = await getTranslations("pages.housekeeping");
|
||||
|
||||
return (
|
||||
<main className="mx-auto flex min-h-[60vh] max-w-xl items-center px-4 py-12">
|
||||
<section
|
||||
role="alert"
|
||||
aria-labelledby="housekeeping-forbidden-title"
|
||||
className="w-full rounded-lg border border-[var(--admin-error)] bg-[var(--admin-surface)] p-6"
|
||||
>
|
||||
<h1
|
||||
id="housekeeping-forbidden-title"
|
||||
className="text-xl font-semibold text-[var(--admin-text)]"
|
||||
>
|
||||
{translate("states.forbidden.title")}
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-[var(--admin-text-muted)]">
|
||||
{translate("states.forbidden.description")}
|
||||
</p>
|
||||
<Link
|
||||
href="/"
|
||||
className="mt-5 inline-flex rounded-md border border-[var(--admin-border)] px-3 py-2 text-sm font-medium text-[var(--admin-text)] hover:bg-[var(--admin-canvas)]"
|
||||
>
|
||||
{translate("preview.backToSite")}
|
||||
</Link>
|
||||
</section>
|
||||
<div className="w-full">
|
||||
<HousekeepingPageState
|
||||
state="forbidden"
|
||||
headingLevel="h1"
|
||||
title={translate("states.forbidden.title")}
|
||||
description={translate("states.forbidden.description")}
|
||||
retryAction={
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex rounded-md border border-[var(--admin-border)] px-3 py-2 text-sm font-medium text-[var(--admin-text)] hover:bg-[var(--admin-canvas)]"
|
||||
>
|
||||
{translate("states.backToSite")}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { HousekeepingPageState } from "@/features/housekeeping/foundation/page/housekeeping-page-state";
|
||||
|
||||
export default async function HousekeepingLoading() {
|
||||
const translate = await getTranslations("pages.housekeeping");
|
||||
|
||||
return (
|
||||
<main className="mx-auto flex min-h-[60vh] max-w-xl items-center px-4 py-12">
|
||||
<div className="w-full">
|
||||
<HousekeepingPageState
|
||||
state="loading"
|
||||
headingLevel="h1"
|
||||
title={translate("states.loading.title")}
|
||||
description={translate("states.loading.description")}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import Link from "@/components/link";
|
||||
import { env } from "@/env";
|
||||
import { HousekeepingPageState } from "@/features/housekeeping/foundation/page/housekeeping-page-state";
|
||||
import { isHousekeepingPreviewEnabled } from "@/features/housekeeping/foundation/preview-gate";
|
||||
|
||||
export default async function HousekeepingNotFound() {
|
||||
const translate = await getTranslations("pages.housekeeping");
|
||||
const previewEnabled = isHousekeepingPreviewEnabled({
|
||||
nodeEnv: env.NODE_ENV,
|
||||
flag: env.HOUSEKEEPING_NEXT_PREVIEW_ENABLED,
|
||||
});
|
||||
const returnHref = previewEnabled ? "/ase-next" : "/";
|
||||
const returnLabel = previewEnabled
|
||||
? translate("states.backToHousekeeping")
|
||||
: translate("states.backToSite");
|
||||
|
||||
return (
|
||||
<main className="mx-auto flex min-h-[60vh] max-w-xl items-center px-4 py-12">
|
||||
<div className="w-full">
|
||||
<HousekeepingPageState
|
||||
state="not-found"
|
||||
headingLevel="h1"
|
||||
title={translate("states.notFound.title")}
|
||||
description={translate("states.notFound.description")}
|
||||
retryAction={
|
||||
<Link
|
||||
href={returnHref}
|
||||
className="inline-flex rounded-md border border-[var(--admin-border)] px-3 py-2 text-sm font-medium text-[var(--admin-text)] hover:bg-[var(--admin-canvas)]"
|
||||
>
|
||||
{returnLabel}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { isValidElement, type ReactElement, type ReactNode } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const boundaryMocks = vi.hoisted(() => {
|
||||
const messages: Record<string, string> = {
|
||||
"states.loading.title": "HK::loading-title",
|
||||
"states.loading.description": "HK::loading-description",
|
||||
"states.notFound.title": "HK::not-found-title",
|
||||
"states.notFound.description": "HK::not-found-description",
|
||||
"states.error.title": "HK::error-title",
|
||||
"states.error.description": "HK::error-description",
|
||||
"states.retry": "HK::retry",
|
||||
"states.backToSite": "HK::back-to-site",
|
||||
"states.backToHousekeeping": "HK::back-to-housekeeping",
|
||||
};
|
||||
|
||||
return {
|
||||
env: {
|
||||
NODE_ENV: "test" as "development" | "test" | "production",
|
||||
HOUSEKEEPING_NEXT_PREVIEW_ENABLED: true,
|
||||
},
|
||||
translate: vi.fn(
|
||||
(key: string, values?: Record<string, string | number>) => {
|
||||
if (key === "states.supportReference") {
|
||||
return `HK::support-reference:${values?.reference}`;
|
||||
}
|
||||
|
||||
const message = messages[key];
|
||||
if (message === undefined) {
|
||||
throw new Error(`Unexpected translation: ${key}`);
|
||||
}
|
||||
|
||||
return message;
|
||||
},
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/env", () => ({ env: boundaryMocks.env }));
|
||||
vi.mock("next-intl", () => ({
|
||||
useTranslations: () => boundaryMocks.translate,
|
||||
}));
|
||||
vi.mock("next-intl/server", () => ({
|
||||
getTranslations: vi.fn(async () => boundaryMocks.translate),
|
||||
}));
|
||||
|
||||
import HousekeepingRouteLoading from "@/app/ase-next/[domain]/[[...segments]]/loading";
|
||||
import HousekeepingError from "@/app/ase-next/error";
|
||||
import HousekeepingLoading from "@/app/ase-next/loading";
|
||||
import HousekeepingNotFound from "@/app/ase-next/not-found";
|
||||
|
||||
function findElementByType(
|
||||
node: ReactNode,
|
||||
type: string,
|
||||
): ReactElement<Record<string, unknown>> | undefined {
|
||||
if (!isValidElement(node)) return undefined;
|
||||
if (node.type === type) {
|
||||
return node as ReactElement<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
for (const value of Object.values(node.props as Record<string, unknown>)) {
|
||||
const candidates = Array.isArray(value) ? value : [value];
|
||||
for (const candidate of candidates) {
|
||||
const match = findElementByType(candidate as ReactNode, type);
|
||||
if (match) return match;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
describe("/ase-next App Router boundaries", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
boundaryMocks.env.NODE_ENV = "test";
|
||||
boundaryMocks.env.HOUSEKEEPING_NEXT_PREVIEW_ENABLED = true;
|
||||
});
|
||||
|
||||
it.each([
|
||||
["root", () => HousekeepingLoading()],
|
||||
["route", () => HousekeepingRouteLoading()],
|
||||
] as const)(
|
||||
"renders the localized %s loading boundary",
|
||||
async (_scope, view) => {
|
||||
const html = renderToStaticMarkup(await view());
|
||||
|
||||
expect(html).toContain("HK::loading-title");
|
||||
expect(html).toContain("HK::loading-description");
|
||||
expect(html).toContain('aria-live="polite"');
|
||||
},
|
||||
);
|
||||
|
||||
it("offers a safe housekeeping return for a missing preview route", async () => {
|
||||
const html = renderToStaticMarkup(await HousekeepingNotFound());
|
||||
|
||||
expect(html).toContain("HK::not-found-title");
|
||||
expect(html).toContain("HK::not-found-description");
|
||||
expect(html).toContain('href="/ase-next"');
|
||||
expect(html).toContain("HK::back-to-housekeeping");
|
||||
});
|
||||
|
||||
it("returns to the site when the preview itself is unavailable", async () => {
|
||||
boundaryMocks.env.NODE_ENV = "production";
|
||||
boundaryMocks.env.HOUSEKEEPING_NEXT_PREVIEW_ENABLED = false;
|
||||
|
||||
const html = renderToStaticMarkup(await HousekeepingNotFound());
|
||||
|
||||
expect(html).toContain('href="/"');
|
||||
expect(html).toContain("HK::back-to-site");
|
||||
expect(html).not.toContain('href="/ase-next"');
|
||||
});
|
||||
|
||||
it("renders only a safe digest and wires the retry action", () => {
|
||||
const reset = vi.fn();
|
||||
const error = Object.assign(new Error("database password leaked"), {
|
||||
digest: "digest-123",
|
||||
});
|
||||
const view = HousekeepingError({ error, reset });
|
||||
const html = renderToStaticMarkup(view);
|
||||
|
||||
expect(html).toContain("HK::error-title");
|
||||
expect(html).toContain("HK::error-description");
|
||||
expect(html).toContain("HK::support-reference:digest-123");
|
||||
expect(html).not.toContain("database password leaked");
|
||||
const button = findElementByType(view, "button");
|
||||
expect(button).toBeDefined();
|
||||
expect(button?.props.onClick).toBe(reset);
|
||||
(button?.props.onClick as (() => void) | undefined)?.();
|
||||
expect(reset).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import en from "@/messages/en.json";
|
||||
import itMessages from "@/messages/it.json";
|
||||
import nlMessages from "@/messages/nl.json";
|
||||
import { HOUSEKEEPING_MANIFESTS } from "../manifests";
|
||||
|
||||
const requiredKeys = [
|
||||
@@ -21,6 +22,18 @@ const requiredKeys = [
|
||||
"pages.housekeeping.states.error.description",
|
||||
"pages.housekeeping.states.forbidden.title",
|
||||
"pages.housekeeping.states.forbidden.description",
|
||||
"pages.housekeeping.states.conflict.title",
|
||||
"pages.housekeeping.states.conflict.description",
|
||||
"pages.housekeeping.states.dependency.title",
|
||||
"pages.housekeeping.states.dependency.description",
|
||||
"pages.housekeeping.states.notFound.title",
|
||||
"pages.housekeeping.states.notFound.description",
|
||||
"pages.housekeeping.states.success.title",
|
||||
"pages.housekeeping.states.success.description",
|
||||
"pages.housekeeping.states.retry",
|
||||
"pages.housekeeping.states.backToSite",
|
||||
"pages.housekeeping.states.backToHousekeeping",
|
||||
"pages.housekeeping.states.supportReference",
|
||||
];
|
||||
|
||||
const expectedDomainMessages = [
|
||||
@@ -68,6 +81,7 @@ describe("housekeeping localization contract", () => {
|
||||
it.each([
|
||||
["English", en],
|
||||
["Italian", itMessages],
|
||||
["Dutch", nlMessages],
|
||||
])("provides the planned housekeeping subtree in %s", (_locale, messages) => {
|
||||
const housekeeping = resolveMessage(messages, "pages.housekeeping");
|
||||
|
||||
@@ -115,4 +129,12 @@ describe("housekeeping localization contract", () => {
|
||||
),
|
||||
).toBe("Stiamo preparando gli strumenti di housekeeping disponibili.");
|
||||
});
|
||||
it("uses idiomatic Dutch copy for access and missing states", () => {
|
||||
expect(
|
||||
resolveMessage(nlMessages, "pages.housekeeping.states.forbidden.title"),
|
||||
).toBe("Toegang geweigerd");
|
||||
expect(
|
||||
resolveMessage(nlMessages, "pages.housekeeping.states.notFound.title"),
|
||||
).toBe("Pagina niet gevonden");
|
||||
});
|
||||
});
|
||||
@@ -10,7 +10,12 @@ describe("HousekeepingPageState", () => {
|
||||
["loading", "status"],
|
||||
["empty", "status"],
|
||||
["partial", "status"],
|
||||
["conflict", "alert"],
|
||||
["dependency", "alert"],
|
||||
["forbidden", "alert"],
|
||||
["error", "alert"],
|
||||
["not-found", "status"],
|
||||
["success", "status"],
|
||||
] as const)("renders %s with the %s role", (state, role) => {
|
||||
const pageState =
|
||||
state === "partial" ? (
|
||||
@@ -46,6 +51,20 @@ describe("HousekeepingPageState", () => {
|
||||
expect(loading).toContain('aria-live="polite"');
|
||||
});
|
||||
|
||||
it("supports a page-level heading for router boundaries", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<HousekeepingPageState
|
||||
state="not-found"
|
||||
headingLevel="h1"
|
||||
title="Missing page"
|
||||
description="The route is unavailable"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(html).toContain("<h1");
|
||||
expect(html).toContain(">Missing page</h1>");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["English", en.pages.housekeeping.states.partial],
|
||||
["Italian", itMessages.pages.housekeeping.states.partial],
|
||||
|
||||
@@ -3,35 +3,65 @@ import type { ReactNode } from "react";
|
||||
interface HousekeepingPageStateBaseProps {
|
||||
title: string;
|
||||
description: string;
|
||||
headingLevel?: "h1" | "h2";
|
||||
retryAction?: ReactNode;
|
||||
}
|
||||
|
||||
type HousekeepingPageStateProps = HousekeepingPageStateBaseProps &
|
||||
(
|
||||
| { state: "partial"; partialLabel: string }
|
||||
| { state: "loading" | "empty" | "error"; partialLabel?: never }
|
||||
| {
|
||||
state:
|
||||
| "loading"
|
||||
| "empty"
|
||||
| "conflict"
|
||||
| "dependency"
|
||||
| "forbidden"
|
||||
| "error"
|
||||
| "not-found"
|
||||
| "success";
|
||||
partialLabel?: never;
|
||||
}
|
||||
);
|
||||
|
||||
const ALERT_STATES = new Set(["conflict", "dependency", "forbidden", "error"]);
|
||||
|
||||
export function HousekeepingPageState({
|
||||
state,
|
||||
partialLabel,
|
||||
title,
|
||||
description,
|
||||
headingLevel = "h2",
|
||||
retryAction,
|
||||
}: HousekeepingPageStateProps) {
|
||||
const isError = state === "error";
|
||||
const isAlert = ALERT_STATES.has(state);
|
||||
const isPartial = state === "partial";
|
||||
const isWarning = isPartial || state === "conflict" || state === "dependency";
|
||||
const isError = state === "error" || state === "forbidden";
|
||||
const isSuccess = state === "success";
|
||||
const Heading = headingLevel;
|
||||
|
||||
return (
|
||||
<section
|
||||
role={isError ? "alert" : "status"}
|
||||
role={isAlert ? "alert" : "status"}
|
||||
data-state-tone={
|
||||
isError
|
||||
? "error"
|
||||
: isWarning
|
||||
? "warning"
|
||||
: isSuccess
|
||||
? "success"
|
||||
: "neutral"
|
||||
}
|
||||
aria-live={state === "loading" ? "polite" : undefined}
|
||||
className={`rounded-lg border p-4 ${
|
||||
isError
|
||||
? "border-[var(--admin-error)] bg-[var(--admin-surface)]"
|
||||
: isPartial
|
||||
? "border-[var(--admin-error-border)] bg-[var(--admin-error-subtle)]"
|
||||
: isWarning
|
||||
? "border-[var(--admin-warning-border)] bg-[var(--admin-warning-subtle)]"
|
||||
: "border-[var(--admin-border)] bg-[var(--admin-surface)]"
|
||||
: isSuccess
|
||||
? "border-[var(--admin-success)] bg-[var(--admin-success-subtle)]"
|
||||
: "border-[var(--admin-border)] bg-[var(--admin-surface)]"
|
||||
}`}
|
||||
>
|
||||
{isPartial ? (
|
||||
@@ -42,7 +72,9 @@ export function HousekeepingPageState({
|
||||
{partialLabel}
|
||||
</p>
|
||||
) : null}
|
||||
<h2 className="font-medium text-[var(--admin-text)]">{title}</h2>
|
||||
<Heading className="font-medium text-[var(--admin-text)]">
|
||||
{title}
|
||||
</Heading>
|
||||
<p className="mt-1 text-sm text-[var(--admin-text-muted)]">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
@@ -28,6 +28,7 @@ const routeMocks = vi.hoisted(() => {
|
||||
"domains.economy.description": "Localized Economy description",
|
||||
"states.forbidden.title": "HK::access-denied",
|
||||
"states.forbidden.description": "Localized insufficient access",
|
||||
"states.backToSite": "HK::back-to-site",
|
||||
"states.empty.title": "Localized empty title",
|
||||
"states.empty.description": "Localized empty description",
|
||||
};
|
||||
@@ -196,9 +197,13 @@ import AdminNextPage from "@/app/ase-next/page";
|
||||
|
||||
const routeFiles = [
|
||||
"src/app/ase-next/layout.tsx",
|
||||
"src/app/ase-next/error.tsx",
|
||||
"src/app/ase-next/forbidden.tsx",
|
||||
"src/app/ase-next/loading.tsx",
|
||||
"src/app/ase-next/not-found.tsx",
|
||||
"src/app/ase-next/page.tsx",
|
||||
"src/app/ase-next/[domain]/layout.tsx",
|
||||
"src/app/ase-next/[domain]/[[...segments]]/loading.tsx",
|
||||
"src/app/ase-next/[domain]/[[...segments]]/page.tsx",
|
||||
] as const;
|
||||
|
||||
|
||||
+21
-1
@@ -3322,7 +3322,27 @@
|
||||
"forbidden": {
|
||||
"title": "Access denied",
|
||||
"description": "Your account does not have permission to use this housekeeping area."
|
||||
}
|
||||
},
|
||||
"conflict": {
|
||||
"title": "Update conflict",
|
||||
"description": "This information changed while you were working. Reload it before trying again."
|
||||
},
|
||||
"dependency": {
|
||||
"title": "Service unavailable",
|
||||
"description": "A required housekeeping service is unavailable. Try again later."
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Page not found",
|
||||
"description": "This housekeeping route does not exist or is no longer available."
|
||||
},
|
||||
"success": {
|
||||
"title": "Changes saved",
|
||||
"description": "The housekeeping operation completed successfully."
|
||||
},
|
||||
"retry": "Try again",
|
||||
"backToSite": "Back to site",
|
||||
"backToHousekeeping": "Back to housekeeping",
|
||||
"supportReference": "Support reference: {reference}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+21
-1
@@ -3321,7 +3321,27 @@
|
||||
"forbidden": {
|
||||
"title": "Accesso negato",
|
||||
"description": "Il tuo account non dispone dei permessi necessari per usare questa area di housekeeping."
|
||||
}
|
||||
},
|
||||
"conflict": {
|
||||
"title": "Conflitto di aggiornamento",
|
||||
"description": "Queste informazioni sono cambiate mentre lavoravi. Ricaricale prima di riprovare."
|
||||
},
|
||||
"dependency": {
|
||||
"title": "Servizio non disponibile",
|
||||
"description": "Un servizio necessario per housekeeping non è disponibile. Riprova più tardi."
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Pagina non trovata",
|
||||
"description": "Questa pagina housekeeping non esiste o non è più disponibile."
|
||||
},
|
||||
"success": {
|
||||
"title": "Modifiche salvate",
|
||||
"description": "L’operazione housekeeping è stata completata correttamente."
|
||||
},
|
||||
"retry": "Riprova",
|
||||
"backToSite": "Torna al sito",
|
||||
"backToHousekeeping": "Torna a housekeeping",
|
||||
"supportReference": "Riferimento assistenza: {reference}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3168,6 +3168,87 @@
|
||||
"teamHint": "Online staff & werklast"
|
||||
}
|
||||
},
|
||||
"housekeeping": {
|
||||
"preview": {
|
||||
"badge": "Voorbeeld van de basis",
|
||||
"commandDisabled": "Zoeken en opdrachten worden in een later deelproject ingeschakeld.",
|
||||
"backToSite": "Terug naar de website"
|
||||
},
|
||||
"navigation": {
|
||||
"skipToContent": "Naar inhoud",
|
||||
"primary": "Housekeeping-domeinen",
|
||||
"contextual": "Domeinnavigatie"
|
||||
},
|
||||
"domains": {
|
||||
"operations": {
|
||||
"title": "Operaties",
|
||||
"description": "Operationeel overzicht en dagelijkse wachtrijen"
|
||||
},
|
||||
"people": {
|
||||
"title": "Gebruikers",
|
||||
"description": "Gebruikers, moderatie en ondersteuning"
|
||||
},
|
||||
"content": {
|
||||
"title": "Inhoud",
|
||||
"description": "Redactionele inhoud en betrokkenheid van de community"
|
||||
},
|
||||
"economy": {
|
||||
"title": "Economie",
|
||||
"description": "Catalogus- en winkelbeheer"
|
||||
},
|
||||
"hotel": {
|
||||
"title": "Hotel",
|
||||
"description": "Kamers, radio en hulpmiddelen voor assets"
|
||||
},
|
||||
"system": {
|
||||
"title": "Systeem",
|
||||
"description": "Configuratie, observatie en toegang"
|
||||
}
|
||||
},
|
||||
"states": {
|
||||
"loading": {
|
||||
"title": "Housekeeping laden",
|
||||
"description": "De beschikbare housekeeping-tools worden voorbereid."
|
||||
},
|
||||
"empty": {
|
||||
"title": "Niets beschikbaar",
|
||||
"description": "Er is geen housekeeping-inhoud beschikbaar voor dit domein."
|
||||
},
|
||||
"partial": {
|
||||
"label": "Gedeeltelijke gegevens",
|
||||
"title": "Sommige informatie is niet beschikbaar",
|
||||
"description": "Controleer de beschikbare informatie en probeer het later opnieuw."
|
||||
},
|
||||
"error": {
|
||||
"title": "Housekeeping kan niet worden geladen",
|
||||
"description": "Probeer het later opnieuw of neem contact op met een beheerder."
|
||||
},
|
||||
"forbidden": {
|
||||
"title": "Toegang geweigerd",
|
||||
"description": "Je account heeft geen toestemming om dit housekeeping-onderdeel te gebruiken."
|
||||
},
|
||||
"conflict": {
|
||||
"title": "Wijzigingsconflict",
|
||||
"description": "Deze informatie is tijdens je werk gewijzigd. Laad de gegevens opnieuw voordat je het nogmaals probeert."
|
||||
},
|
||||
"dependency": {
|
||||
"title": "Dienst niet beschikbaar",
|
||||
"description": "Een vereiste housekeeping-dienst is niet beschikbaar. Probeer het later opnieuw."
|
||||
},
|
||||
"notFound": {
|
||||
"title": "Pagina niet gevonden",
|
||||
"description": "Deze housekeeping-pagina bestaat niet of is niet meer beschikbaar."
|
||||
},
|
||||
"success": {
|
||||
"title": "Wijzigingen opgeslagen",
|
||||
"description": "De housekeeping-bewerking is voltooid."
|
||||
},
|
||||
"retry": "Opnieuw proberen",
|
||||
"backToSite": "Terug naar de website",
|
||||
"backToHousekeeping": "Terug naar housekeeping",
|
||||
"supportReference": "Referentie voor ondersteuning: {reference}"
|
||||
}
|
||||
},
|
||||
"studio": {
|
||||
"title": "Studio",
|
||||
"sourceFurnidata": "{source} furnidata",
|
||||
|
||||
Reference in new issue
Block a user