feat(me): show actionable personal tasks with unavailable states
This commit is contained in:
1 parent
d9ea93a2e7
commit
a537c55793
9 files changed
+435
-12
No files matched your search
@@ -0,0 +1,108 @@
|
|||||||
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { createTranslator } from "next-intl";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const state = vi.hoisted(() => ({
|
||||||
|
data: {
|
||||||
|
support: 2 as number | null,
|
||||||
|
friendRequests: 3 as number | null,
|
||||||
|
registeredEvent: false as
|
||||||
|
| false
|
||||||
|
| null
|
||||||
|
| { id: number; title: string; startsAt: Date },
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/services/dashboard-tasks", () => ({
|
||||||
|
loadDashboardTasks: async () => state.data,
|
||||||
|
}));
|
||||||
|
vi.mock("@/components/link", () => ({
|
||||||
|
default: ({
|
||||||
|
href,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
href: string;
|
||||||
|
children: ReactNode;
|
||||||
|
}) => (
|
||||||
|
<a href={href} {...props}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
vi.mock("next-intl/server", () => ({
|
||||||
|
getTranslations: async () => {
|
||||||
|
const flat = JSON.parse(
|
||||||
|
readFileSync("docs/task-account-i18n.json", "utf8"),
|
||||||
|
).it;
|
||||||
|
const messages = Object.fromEntries(
|
||||||
|
Object.entries(flat).map(([key, value]) => [
|
||||||
|
key.replace("pages.myDashboard.tasks.", ""),
|
||||||
|
value,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
return createTranslator({ locale: "it", messages });
|
||||||
|
},
|
||||||
|
getFormatter: async () => ({
|
||||||
|
dateTime: (date: Date) =>
|
||||||
|
new Intl.DateTimeFormat("it", {
|
||||||
|
dateStyle: "medium",
|
||||||
|
timeStyle: "short",
|
||||||
|
timeZone: "UTC",
|
||||||
|
}).format(date),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import DashboardTasks from "./DashboardTasks";
|
||||||
|
|
||||||
|
describe("dashboard task presentation", () => {
|
||||||
|
it("renders actionable links from real task data", async () => {
|
||||||
|
state.data = {
|
||||||
|
support: 2,
|
||||||
|
friendRequests: 3,
|
||||||
|
registeredEvent: {
|
||||||
|
id: 7,
|
||||||
|
title:
|
||||||
|
"Un evento con un titolo molto lungo per verificare la visualizzazione sullo schermo del telefono",
|
||||||
|
startsAt: new Date("2026-09-12T15:00:00Z"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const html = renderToStaticMarkup(
|
||||||
|
await DashboardTasks({ userId: 42, rewardAvailable: true }),
|
||||||
|
);
|
||||||
|
expect(html).toContain('href="/help/tickets"');
|
||||||
|
expect(html).toContain('href="/friends"');
|
||||||
|
expect(html).toContain('href="#referral-reward"');
|
||||||
|
expect(html).toContain("-7");
|
||||||
|
if (process.env.ACCOUNT_VISUAL_FIXTURE) {
|
||||||
|
const css = readFileSync(
|
||||||
|
"src/app/(site)/me/dashboard.module.css",
|
||||||
|
"utf8",
|
||||||
|
).replaceAll(/\.([\w]+)(?=[\s.{:#>])/g, ".$1");
|
||||||
|
// Vitest CSS modules are scoped; reproduce those class names for the fixture.
|
||||||
|
const taskClass = html.match(/<ul class="([^"]+)"/)?.[1] ?? "tasks";
|
||||||
|
const output = join("storage", "account-fixtures");
|
||||||
|
mkdirSync(output, { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
join(output, "tasks.html"),
|
||||||
|
`<!doctype html><html lang="it"><meta name="viewport" content="width=device-width, initial-scale=1"><style>:root{--color-border:#ddd;--color-primary:#a34023;--color-text-muted:#777;--color-surface:#fff}*{box-sizing:border-box}body{font:16px Arial;background:#f7f5f2;margin:0;padding:20px}main{max-width:1000px;margin:auto}a{color:#943712;text-decoration:none}.p-5{padding:20px}.surface-card{border:1px solid #ddd;border-radius:16px;padding:12px}h3{margin:12px}${css.replaceAll(".tasks", "." + taskClass)}</style><main>${html}<div id="referral-reward">Premio inviti</div></main></html>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it("shows an honest empty state only after successful queries", async () => {
|
||||||
|
state.data = { support: 0, friendRequests: 0, registeredEvent: false };
|
||||||
|
expect(
|
||||||
|
renderToStaticMarkup(
|
||||||
|
await DashboardTasks({ userId: 42, rewardAvailable: false }),
|
||||||
|
),
|
||||||
|
).toContain("Nessuna azione");
|
||||||
|
state.data = { support: null, friendRequests: null, registeredEvent: null };
|
||||||
|
const html = renderToStaticMarkup(
|
||||||
|
await DashboardTasks({ userId: 42, rewardAvailable: null }),
|
||||||
|
);
|
||||||
|
expect(html).toContain('role="status"');
|
||||||
|
expect(html).not.toContain("Nessuna azione");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { getFormatter, getTranslations } from "next-intl/server";
|
||||||
|
import Link from "@/components/link";
|
||||||
|
import { SurfaceCard } from "@/components/surface-card";
|
||||||
|
import { slugify } from "@/lib/format";
|
||||||
|
import { loadDashboardTasks } from "@/lib/services/dashboard-tasks";
|
||||||
|
import styles from "./dashboard.module.css";
|
||||||
|
|
||||||
|
export default async function DashboardTasks({
|
||||||
|
userId,
|
||||||
|
rewardAvailable,
|
||||||
|
}: {
|
||||||
|
userId: number;
|
||||||
|
rewardAvailable: boolean | null;
|
||||||
|
}) {
|
||||||
|
const [data, t, format] = await Promise.all([
|
||||||
|
loadDashboardTasks(userId),
|
||||||
|
getTranslations("pages.myDashboard.tasks"),
|
||||||
|
getFormatter(),
|
||||||
|
]);
|
||||||
|
const items: { href: string; label: string }[] = [];
|
||||||
|
if (data.support)
|
||||||
|
items.push({
|
||||||
|
href: "/help/tickets",
|
||||||
|
label: t("support", { count: data.support }),
|
||||||
|
});
|
||||||
|
if (data.friendRequests)
|
||||||
|
items.push({
|
||||||
|
href: "/friends",
|
||||||
|
label: t("friends", { count: data.friendRequests }),
|
||||||
|
});
|
||||||
|
if (data.registeredEvent)
|
||||||
|
items.push({
|
||||||
|
href: `/events/${slugify(data.registeredEvent.title)}-${data.registeredEvent.id}`,
|
||||||
|
label: t("event", {
|
||||||
|
title: data.registeredEvent.title,
|
||||||
|
date: format.dateTime(data.registeredEvent.startsAt, {
|
||||||
|
dateStyle: "medium",
|
||||||
|
timeStyle: "short",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (rewardAvailable)
|
||||||
|
items.push({ href: "#referral-reward", label: t("reward") });
|
||||||
|
const unavailable =
|
||||||
|
data.support === null ||
|
||||||
|
data.friendRequests === null ||
|
||||||
|
data.registeredEvent === null ||
|
||||||
|
rewardAvailable === null;
|
||||||
|
return (
|
||||||
|
<SurfaceCard title={t("title")} bodyClassName="p-5">
|
||||||
|
{items.length > 0 ? (
|
||||||
|
<ul className={styles.tasks}>
|
||||||
|
{items.map((item) => (
|
||||||
|
<li key={item.href}>
|
||||||
|
<Link href={item.href}>
|
||||||
|
{item.label}
|
||||||
|
<span aria-hidden="true"> →</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : !unavailable ? (
|
||||||
|
<p className="muted">{t("empty")}</p>
|
||||||
|
) : null}
|
||||||
|
{unavailable && (
|
||||||
|
<p role="status" className="muted">
|
||||||
|
{t("unavailable")} <Link href="/me">{t("retry")}</Link>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</SurfaceCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -325,3 +325,20 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.tasks {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.tasks a {
|
||||||
|
display: block;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.tasks a:hover {
|
||||||
|
background: color-mix(in srgb, var(--color-primary) 8%, var(--color-surface));
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import { auth } from "@/lib/auth";
|
|||||||
import { avatarImageUrl, slugify } from "@/lib/format";
|
import { avatarImageUrl, slugify } from "@/lib/format";
|
||||||
import { loadUserDashboard } from "@/lib/services/user-dashboard";
|
import { loadUserDashboard } from "@/lib/services/user-dashboard";
|
||||||
import CopyReferralButton from "./CopyReferralButton";
|
import CopyReferralButton from "./CopyReferralButton";
|
||||||
|
import DashboardTasks from "./DashboardTasks";
|
||||||
import styles from "./dashboard.module.css";
|
import styles from "./dashboard.module.css";
|
||||||
|
|
||||||
export async function generateMetadata() {
|
export async function generateMetadata() {
|
||||||
@@ -96,9 +97,13 @@ export default async function MePage({
|
|||||||
: null;
|
: null;
|
||||||
const referralTotal = Math.max(
|
const referralTotal = Math.max(
|
||||||
0,
|
0,
|
||||||
Number(referralsRows[0]?.referralsTotal ?? 0),
|
Number(referralsRows?.[0]?.referralsTotal ?? 0),
|
||||||
);
|
);
|
||||||
const canClaim = referralTotal >= needed;
|
const canClaim =
|
||||||
|
referralsRows !== null &&
|
||||||
|
referralTotal >= needed &&
|
||||||
|
rewardAmount > 0 &&
|
||||||
|
rewardKind !== null;
|
||||||
const remaining = Math.max(0, needed - referralTotal);
|
const remaining = Math.max(0, needed - referralTotal);
|
||||||
const onlineFriends = friends.filter((friend) => friend.online === "1");
|
const onlineFriends = friends.filter((friend) => friend.online === "1");
|
||||||
const date = (value: number) =>
|
const date = (value: number) =>
|
||||||
@@ -195,6 +200,10 @@ export default async function MePage({
|
|||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
{alertRaw && <p className={styles.feedback}>{alertRaw}</p>}
|
{alertRaw && <p className={styles.feedback}>{alertRaw}</p>}
|
||||||
|
<DashboardTasks
|
||||||
|
userId={userId}
|
||||||
|
rewardAvailable={referralsRows === null ? null : canClaim}
|
||||||
|
/>
|
||||||
<section aria-label={t("wallet")} className={styles.wallet}>
|
<section aria-label={t("wallet")} className={styles.wallet}>
|
||||||
{wallet.map((currency) => (
|
{wallet.map((currency) => (
|
||||||
<SurfaceCard key={currency.kind} className={styles.currency}>
|
<SurfaceCard key={currency.kind} className={styles.currency}>
|
||||||
@@ -334,6 +343,7 @@ export default async function MePage({
|
|||||||
</div>
|
</div>
|
||||||
<aside className={styles.stack}>
|
<aside className={styles.stack}>
|
||||||
<SurfaceCard title={t("referrals")} bodyClassName="p-5">
|
<SurfaceCard title={t("referrals")} bodyClassName="p-5">
|
||||||
|
<div id="referral-reward" />
|
||||||
<p>{t("referralHint")}</p>
|
<p>{t("referralHint")}</p>
|
||||||
<div className={styles.progressLabel}>
|
<div className={styles.progressLabel}>
|
||||||
<strong>
|
<strong>
|
||||||
@@ -369,7 +379,11 @@ export default async function MePage({
|
|||||||
className={styles.referralInput}
|
className={styles.referralInput}
|
||||||
/>
|
/>
|
||||||
<CopyReferralButton value={referralLink} />
|
<CopyReferralButton value={referralLink} />
|
||||||
{canClaim ? (
|
{referralsRows === null ? (
|
||||||
|
<p role="status" className="muted">
|
||||||
|
{t("tasks.rewardUnavailable")}
|
||||||
|
</p>
|
||||||
|
) : canClaim ? (
|
||||||
<form action={claimReferral} className={styles.claim}>
|
<form action={claimReferral} className={styles.claim}>
|
||||||
<button type="submit" className="btn btn-primary">
|
<button type="submit" className="btn btn-primary">
|
||||||
{t("claim")}
|
{t("claim")}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const state = vi.hoisted(() => ({
|
||||||
|
fail: false,
|
||||||
|
queries: [] as { query: string; params: unknown[] }[],
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/db", async () => {
|
||||||
|
const schema = await import("@/db/schema");
|
||||||
|
const { drizzle } = await import("drizzle-orm/mysql-proxy");
|
||||||
|
return {
|
||||||
|
...schema,
|
||||||
|
db: drizzle(async (query, params) => {
|
||||||
|
state.queries.push({ query, params });
|
||||||
|
if (state.fail) throw new Error("offline");
|
||||||
|
return { rows: query.includes("count(*)") ? [[0]] : [] };
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
import { loadDashboardTasks } from "./dashboard-tasks";
|
||||||
|
|
||||||
|
describe("personal dashboard tasks", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
state.fail = false;
|
||||||
|
state.queries = [];
|
||||||
|
});
|
||||||
|
it("rejects invalid user ids before accessing the database", async () => {
|
||||||
|
await expect(loadDashboardTasks(0)).rejects.toThrow("Invalid user");
|
||||||
|
expect(state.queries).toHaveLength(0);
|
||||||
|
});
|
||||||
|
it("scopes every query to the signed-in user and only offers future published registrations", async () => {
|
||||||
|
expect(
|
||||||
|
await loadDashboardTasks(42, new Date("2026-09-11T12:00:00Z")),
|
||||||
|
).toEqual({ support: 0, friendRequests: 0, registeredEvent: false });
|
||||||
|
expect(state.queries).toHaveLength(3);
|
||||||
|
for (const { params } of state.queries) expect(params).toContain(42);
|
||||||
|
const support = state.queries.find(({ query }) =>
|
||||||
|
query.includes("website_help_center_tickets"),
|
||||||
|
);
|
||||||
|
expect(support?.query).toContain(
|
||||||
|
"`website_help_center_tickets`.`open` = ?",
|
||||||
|
);
|
||||||
|
expect(support?.query).toContain(
|
||||||
|
"order by `website_help_center_ticket_replies`.`id` desc limit 1) <> ?",
|
||||||
|
);
|
||||||
|
const event = state.queries.find(({ query }) =>
|
||||||
|
query.includes("website_events"),
|
||||||
|
);
|
||||||
|
expect(event?.params).toEqual([
|
||||||
|
42,
|
||||||
|
"published",
|
||||||
|
"2026-09-11 12:00:00.000",
|
||||||
|
1,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it("does not report empty tasks when the database is unavailable", async () => {
|
||||||
|
state.fail = true;
|
||||||
|
expect(await loadDashboardTasks(42)).toEqual({
|
||||||
|
support: null,
|
||||||
|
friendRequests: null,
|
||||||
|
registeredEvent: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import "server-only";
|
||||||
|
import { and, asc, count, eq, gt, sql } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
db,
|
||||||
|
MessengerFriendrequests,
|
||||||
|
WebsiteEvent,
|
||||||
|
WebsiteEventRegistration,
|
||||||
|
WebsiteHelpCenterTicketReplies,
|
||||||
|
WebsiteHelpCenterTickets,
|
||||||
|
} from "@/lib/db";
|
||||||
|
|
||||||
|
/** Caller supplies the authenticated session user, never a request parameter. */
|
||||||
|
export async function loadDashboardTasks(userId: number, now = new Date()) {
|
||||||
|
if (!Number.isSafeInteger(userId) || userId <= 0)
|
||||||
|
throw new Error("Invalid user");
|
||||||
|
const [supportRows, friendRows, eventRows] = await Promise.all([
|
||||||
|
db
|
||||||
|
.select({ value: count() })
|
||||||
|
.from(WebsiteHelpCenterTickets)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(WebsiteHelpCenterTickets.userId, userId),
|
||||||
|
eq(WebsiteHelpCenterTickets.open, true),
|
||||||
|
sql`(select ${WebsiteHelpCenterTicketReplies.userId} from ${WebsiteHelpCenterTicketReplies} where ${WebsiteHelpCenterTicketReplies.ticketId} = ${WebsiteHelpCenterTickets.id} order by ${WebsiteHelpCenterTicketReplies.id} desc limit 1) <> ${userId}`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.catch(() => null),
|
||||||
|
db
|
||||||
|
.select({ value: count() })
|
||||||
|
.from(MessengerFriendrequests)
|
||||||
|
.where(eq(MessengerFriendrequests.userToId, userId))
|
||||||
|
.catch(() => null),
|
||||||
|
db
|
||||||
|
.select({
|
||||||
|
id: WebsiteEvent.id,
|
||||||
|
title: WebsiteEvent.title,
|
||||||
|
startsAt: WebsiteEvent.startsAt,
|
||||||
|
})
|
||||||
|
.from(WebsiteEventRegistration)
|
||||||
|
.innerJoin(
|
||||||
|
WebsiteEvent,
|
||||||
|
eq(WebsiteEvent.id, WebsiteEventRegistration.eventId),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(WebsiteEventRegistration.userId, userId),
|
||||||
|
eq(WebsiteEvent.status, "published"),
|
||||||
|
gt(WebsiteEvent.startsAt, now),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.orderBy(asc(WebsiteEvent.startsAt), asc(WebsiteEvent.id))
|
||||||
|
.limit(1)
|
||||||
|
.catch(() => null),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
support: supportRows === null ? null : Number(supportRows[0]?.value ?? 0),
|
||||||
|
friendRequests:
|
||||||
|
friendRows === null ? null : Number(friendRows[0]?.value ?? 0),
|
||||||
|
registeredEvent: eventRows === null ? null : (eventRows[0] ?? false),
|
||||||
|
};
|
||||||
|
}
|
||||||
+32
-3
@@ -261,7 +261,17 @@
|
|||||||
"news": {
|
"news": {
|
||||||
"title": "News",
|
"title": "News",
|
||||||
"subtitle": "The latest announcements, events and updates from the hotel",
|
"subtitle": "The latest announcements, events and updates from the hotel",
|
||||||
"noArticles": "No articles yet."
|
"noArticles": "No articles yet.",
|
||||||
|
"searchLabel": "Search news",
|
||||||
|
"orderLabel": "Order",
|
||||||
|
"newest": "Newest first",
|
||||||
|
"oldest": "Oldest first",
|
||||||
|
"apply": "Apply",
|
||||||
|
"loadError": "News could not be loaded. Please try again.",
|
||||||
|
"pagination": "News pages",
|
||||||
|
"previous": "Previous",
|
||||||
|
"next": "Next",
|
||||||
|
"pageSummary": "Page {page} of {pages} · {total} articles"
|
||||||
},
|
},
|
||||||
"article": {
|
"article": {
|
||||||
"backToNews": "← News",
|
"backToNews": "← News",
|
||||||
@@ -1761,7 +1771,15 @@
|
|||||||
"colPriority": "Priority",
|
"colPriority": "Priority",
|
||||||
"colAssigned": "Assigned",
|
"colAssigned": "Assigned",
|
||||||
"colDate": "Date",
|
"colDate": "Date",
|
||||||
"meta": "by {user} · {count} msg · {category}"
|
"meta": "by {user} · {count} msg · {category}",
|
||||||
|
"awaitingStaff": "Waiting for staff",
|
||||||
|
"assignedMine": "Assigned to me",
|
||||||
|
"unassigned": "Unassigned",
|
||||||
|
"assignedAny": "Assigned",
|
||||||
|
"lastReplyAge": "Since last response",
|
||||||
|
"ageMinutes": "{count} min ago",
|
||||||
|
"ageHours": "{count} h ago",
|
||||||
|
"ageDays": "{count} d ago"
|
||||||
},
|
},
|
||||||
"ticketInbox": {
|
"ticketInbox": {
|
||||||
"hint": "Merged view of open CMS desk and help-center tickets. Detail pages and replies stay on each product’s own route.",
|
"hint": "Merged view of open CMS desk and help-center tickets. Detail pages and replies stay on each product’s own route.",
|
||||||
@@ -4520,7 +4538,18 @@
|
|||||||
"eventUpcoming": "Coming up",
|
"eventUpcoming": "Coming up",
|
||||||
"eventDetails": "View event",
|
"eventDetails": "View event",
|
||||||
"noUpcomingEvents": "No upcoming events have been announced yet.",
|
"noUpcomingEvents": "No upcoming events have been announced yet.",
|
||||||
"roomsUnavailable": "Rooms are temporarily unavailable. Open the community to try again."
|
"roomsUnavailable": "Rooms are temporarily unavailable. Open the community to try again.",
|
||||||
|
"tasks": {
|
||||||
|
"title": "To do",
|
||||||
|
"support": "{count, plural, one {# ticket with a support reply} other {# tickets with support replies}}",
|
||||||
|
"friends": "{count, plural, one {# friend request to review} other {# friend requests to review}}",
|
||||||
|
"event": "Your next registered event: {title} · {date}",
|
||||||
|
"reward": "Your referral reward is ready to claim",
|
||||||
|
"empty": "You're all caught up. No actions waiting.",
|
||||||
|
"unavailable": "Some actions could not be checked. Available actions are shown above.",
|
||||||
|
"retry": "Try again",
|
||||||
|
"rewardUnavailable": "Referral progress is temporarily unavailable. Please try again."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
|
|||||||
+32
-3
@@ -261,7 +261,17 @@
|
|||||||
"news": {
|
"news": {
|
||||||
"title": "Notizie",
|
"title": "Notizie",
|
||||||
"subtitle": "Gli ultimi annunci, eventi e aggiornamenti dall'hotel",
|
"subtitle": "Gli ultimi annunci, eventi e aggiornamenti dall'hotel",
|
||||||
"noArticles": "Ancora nessun articolo."
|
"noArticles": "Ancora nessun articolo.",
|
||||||
|
"searchLabel": "Cerca notizie",
|
||||||
|
"orderLabel": "Ordine",
|
||||||
|
"newest": "Prima le più recenti",
|
||||||
|
"oldest": "Prima le meno recenti",
|
||||||
|
"apply": "Applica",
|
||||||
|
"loadError": "Non è stato possibile caricare le notizie. Riprova.",
|
||||||
|
"pagination": "Pagine delle notizie",
|
||||||
|
"previous": "Precedente",
|
||||||
|
"next": "Successiva",
|
||||||
|
"pageSummary": "Pagina {page} di {pages} · {total} articoli"
|
||||||
},
|
},
|
||||||
"article": {
|
"article": {
|
||||||
"backToNews": "← Notizie",
|
"backToNews": "← Notizie",
|
||||||
@@ -1736,7 +1746,15 @@
|
|||||||
"meta": "di {user} · {count} msg · {category}",
|
"meta": "di {user} · {count} msg · {category}",
|
||||||
"queueHint": "Questa coda e il supporto CMS. I ticket del help center pubblici stanno nella tab Help center.",
|
"queueHint": "Questa coda e il supporto CMS. I ticket del help center pubblici stanno nella tab Help center.",
|
||||||
"queueOtherCta": "Apri help center",
|
"queueOtherCta": "Apri help center",
|
||||||
"queueCounts": "Aperti qui: {here} · Help center: {other}"
|
"queueCounts": "Aperti qui: {here} · Help center: {other}",
|
||||||
|
"awaitingStaff": "In attesa dello staff",
|
||||||
|
"assignedMine": "Assegnati a me",
|
||||||
|
"unassigned": "Non assegnati",
|
||||||
|
"assignedAny": "Assegnati",
|
||||||
|
"lastReplyAge": "Dall’ultima risposta",
|
||||||
|
"ageMinutes": "{count} min fa",
|
||||||
|
"ageHours": "{count} ore fa",
|
||||||
|
"ageDays": "{count} giorni fa"
|
||||||
},
|
},
|
||||||
"helpTickets": {
|
"helpTickets": {
|
||||||
"title": "Ticket help center",
|
"title": "Ticket help center",
|
||||||
@@ -4525,7 +4543,18 @@
|
|||||||
"eventUpcoming": "Prossimo evento",
|
"eventUpcoming": "Prossimo evento",
|
||||||
"eventDetails": "Scopri l’evento",
|
"eventDetails": "Scopri l’evento",
|
||||||
"noUpcomingEvents": "Non sono ancora stati annunciati nuovi eventi.",
|
"noUpcomingEvents": "Non sono ancora stati annunciati nuovi eventi.",
|
||||||
"roomsUnavailable": "Le stanze non sono al momento disponibili. Apri la community per riprovare."
|
"roomsUnavailable": "Le stanze non sono al momento disponibili. Apri la community per riprovare.",
|
||||||
|
"tasks": {
|
||||||
|
"title": "Da fare",
|
||||||
|
"support": "{count, plural, one {# ticket con risposta del supporto} other {# ticket con risposte del supporto}}",
|
||||||
|
"friends": "{count, plural, one {# richiesta di amicizia da controllare} other {# richieste di amicizia da controllare}}",
|
||||||
|
"event": "Il prossimo evento a cui sei iscritto: {title} · {date}",
|
||||||
|
"reward": "Il premio per gli inviti è pronto da ritirare",
|
||||||
|
"empty": "Tutto a posto. Nessuna azione in attesa.",
|
||||||
|
"unavailable": "Non è stato possibile verificare alcune azioni. Quelle disponibili sono mostrate sopra.",
|
||||||
|
"retry": "Riprova",
|
||||||
|
"rewardUnavailable": "Il progresso degli inviti non è al momento disponibile. Riprova."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
|
|||||||
+32
-3
@@ -249,7 +249,17 @@
|
|||||||
"news": {
|
"news": {
|
||||||
"title": "Nieuws",
|
"title": "Nieuws",
|
||||||
"subtitle": "De laatste aankondigingen, evenementen en updates van het hotel",
|
"subtitle": "De laatste aankondigingen, evenementen en updates van het hotel",
|
||||||
"noArticles": "Nog geen artikelen."
|
"noArticles": "Nog geen artikelen.",
|
||||||
|
"searchLabel": "Nieuws zoeken",
|
||||||
|
"orderLabel": "Volgorde",
|
||||||
|
"newest": "Nieuwste eerst",
|
||||||
|
"oldest": "Oudste eerst",
|
||||||
|
"apply": "Toepassen",
|
||||||
|
"loadError": "Het nieuws kon niet worden geladen. Probeer het opnieuw.",
|
||||||
|
"pagination": "Nieuwspagina's",
|
||||||
|
"previous": "Vorige",
|
||||||
|
"next": "Volgende",
|
||||||
|
"pageSummary": "Pagina {page} van {pages} · {total} artikelen"
|
||||||
},
|
},
|
||||||
"article": {
|
"article": {
|
||||||
"backToNews": "← Nieuws",
|
"backToNews": "← Nieuws",
|
||||||
@@ -3432,7 +3442,15 @@
|
|||||||
"meta": "door {user} · {count} berichten · {category}",
|
"meta": "door {user} · {count} berichten · {category}",
|
||||||
"queueHint": "Deze wachtrij is de CMS support desk. Speler-helpcenter tickets staan onder Helpcentrum.",
|
"queueHint": "Deze wachtrij is de CMS support desk. Speler-helpcenter tickets staan onder Helpcentrum.",
|
||||||
"queueOtherCta": "Open helpcentrum-tickets",
|
"queueOtherCta": "Open helpcentrum-tickets",
|
||||||
"queueCounts": "Open hier: {here} · Helpcentrum: {other}"
|
"queueCounts": "Open hier: {here} · Helpcentrum: {other}",
|
||||||
|
"awaitingStaff": "Wacht op medewerkers",
|
||||||
|
"assignedMine": "Aan mij toegewezen",
|
||||||
|
"unassigned": "Niet toegewezen",
|
||||||
|
"assignedAny": "Toegewezen",
|
||||||
|
"lastReplyAge": "Sinds laatste antwoord",
|
||||||
|
"ageMinutes": "{count} min geleden",
|
||||||
|
"ageHours": "{count} uur geleden",
|
||||||
|
"ageDays": "{count} dagen geleden"
|
||||||
},
|
},
|
||||||
"helpTickets": {
|
"helpTickets": {
|
||||||
"title": "Helpcenter-tickets",
|
"title": "Helpcenter-tickets",
|
||||||
@@ -3994,7 +4012,18 @@
|
|||||||
"eventUpcoming": "Binnenkort",
|
"eventUpcoming": "Binnenkort",
|
||||||
"eventsUnavailable": "De agenda is tijdelijk niet beschikbaar. Open alle evenementen om het opnieuw te proberen.",
|
"eventsUnavailable": "De agenda is tijdelijk niet beschikbaar. Open alle evenementen om het opnieuw te proberen.",
|
||||||
"messagesUnavailable": "Open je inbox om te kijken",
|
"messagesUnavailable": "Open je inbox om te kijken",
|
||||||
"eventOngoing": "Nu bezig"
|
"eventOngoing": "Nu bezig",
|
||||||
|
"tasks": {
|
||||||
|
"title": "Te doen",
|
||||||
|
"support": "{count, plural, one {# ticket met een antwoord van de klantenservice} other {# tickets met antwoorden van de klantenservice}}",
|
||||||
|
"friends": "{count, plural, one {# vriendschapsverzoek om te bekijken} other {# vriendschapsverzoeken om te bekijken}}",
|
||||||
|
"event": "Je volgende aangemelde evenement: {title} · {date}",
|
||||||
|
"reward": "Je beloning voor uitnodigingen staat klaar",
|
||||||
|
"empty": "Alles is bijgewerkt. Er zijn geen acties nodig.",
|
||||||
|
"unavailable": "Sommige acties konden niet worden gecontroleerd. Beschikbare acties staan hierboven.",
|
||||||
|
"retry": "Opnieuw proberen",
|
||||||
|
"rewardUnavailable": "De voortgang van uitnodigingen is tijdelijk niet beschikbaar. Probeer het opnieuw."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toolbar": {
|
"toolbar": {
|
||||||
|
|||||||
Reference in new issue
Block a user