i18n: translate all public page bodies (EN + IT)

Internationalised the ~44 public pages with next-intl (the shell was
already translated). Each page now pulls its copy from a "pages.<slug>"
namespace via getTranslations (server) / useTranslations (client); the
EN + IT catalogs were authored by parallel agents and merged centrally,
with natural Italian (ICU plurals) and it backfilled from en for any
gap. request.ts gained getMessageFallback/onError so a missing key
degrades to the English value, never a raw key.

Behaviour unchanged (only display text moved to t() calls; queries,
actions, fields, ContentCard structure preserved). Verified on the prod
server: with NEXT_LOCALE=it, home/community/staff/news/shop/rankings all
render Italian copy, no raw-key leakage; English unchanged. The nav
language switcher toggles EN/IT live. tsc 0, vitest 49/49, next build 0.

Admin pages intentionally left in English (staff tooling).
This commit is contained in:
Simo committed 2026-06-28 20:40:06 +02:00
1 parent 1199176883
commit e004dfedaf
47 files changed
+1740 -471

No files matched your search

+30 -27
View File
@@ -1,3 +1,4 @@
import { getTranslations } from "next-intl/server";
import Link from "next/link";
import { ContentCard, EmptyState, OnlineBadge } from "@/components/public/ui";
import { prisma } from "@/lib/prisma";
@@ -6,15 +7,6 @@ import { siteSettings } from "@/lib/services/site-settings";
// Radio config + today's schedule are read live from the DB / settings table.
export const dynamic = "force-dynamic";
// Sub-pages linked from the radio home hub.
const RADIO_LINKS: { label: string; href: string; icon: string; text: string }[] = [
{ label: "Schedule", href: "/radio/schedule", icon: "🗓️", text: "Our weekly line-up of DJs and shows." },
{ label: "Shouts", href: "/radio/shouts", icon: "📣", text: "Shout-outs and requests from the community." },
{ label: "Requests", href: "/radio/requests", icon: "🎵", text: "Ask the DJ to spin your favourite track." },
{ label: "Apply as DJ", href: "/radio/apply", icon: "🎤", text: "Join the team and take to the airwaves." },
{ label: "Leaderboard", href: "/radio/leaderboard", icon: "🏆", text: "Top listeners ranked by points earned." },
];
// Canonical weekday order so today's slots come out chronologically and we can
// match the current day against whatever casing the `day` column stores.
const WEEKDAYS = [
@@ -36,6 +28,17 @@ function formatTime(d: Date | null | undefined): string {
}
export default async function RadioPage() {
const t = await getTranslations("pages.radio");
// Sub-pages linked from the radio home hub.
const RADIO_LINKS: { label: string; href: string; icon: string; text: string }[] = [
{ label: t("linkSchedule"), href: "/radio/schedule", icon: "🗓️", text: t("linkScheduleText") },
{ label: t("linkShouts"), href: "/radio/shouts", icon: "📣", text: t("linkShoutsText") },
{ label: t("linkRequests"), href: "/radio/requests", icon: "🎵", text: t("linkRequestsText") },
{ label: t("linkApply"), href: "/radio/apply", icon: "🎤", text: t("linkApplyText") },
{ label: t("linkLeaderboard"), href: "/radio/leaderboard", icon: "🏆", text: t("linkLeaderboardText") },
];
// ── Radio config from website_settings (graceful fallbacks) ──
const [streamUrl, currentDj, currentSong] = await Promise.all([
siteSettings.get("radio_stream_url", ""),
@@ -77,20 +80,20 @@ export default async function RadioPage() {
{/* ── Header: live stream + now playing ─────────────────────── */}
<ContentCard
icon="📻"
title="Radio"
subtitle="Tune in to the hotel radio — live DJ sets, requests and shoutouts."
title={t("title")}
subtitle={t("subtitle")}
action={<OnlineBadge online={isLive} />}
>
<div className="card-grid sm-2" style={{ alignItems: "stretch" }}>
<div className="card" style={{ display: "grid", gap: "0.35rem", alignContent: "center" }}>
<p className="muted" style={{ margin: 0, fontSize: "0.8rem", textTransform: "uppercase", letterSpacing: "0.05em" }}>
Now playing
{t("nowPlaying")}
</p>
<p style={{ margin: 0, fontSize: "1.1rem", fontWeight: 700 }}>
{currentSong ? currentSong : <span className="muted" style={{ fontWeight: 400 }}>Nothing playing right now.</span>}
{currentSong ? currentSong : <span className="muted" style={{ fontWeight: 400 }}>{t("nothingPlaying")}</span>}
</p>
<p className="muted" style={{ margin: 0 }}>
{currentDj ? `On air: ${currentDj}` : "No DJ on air."}
{currentDj ? t("onAir", { dj: currentDj }) : t("noDj")}
</p>
</div>
<div className="card" style={{ display: "flex", alignItems: "center" }}>
@@ -101,11 +104,11 @@ export default async function RadioPage() {
style={{ width: "100%" }}
preload="none"
>
Your browser does not support the audio element.
{t("audioUnsupported")}
</audio>
) : (
<p className="muted" style={{ margin: 0 }}>
The stream is currently offline.
{t("streamOffline")}
</p>
)}
</div>
@@ -115,8 +118,8 @@ export default async function RadioPage() {
{/* ── Sub-page links ──────────────────────────────────────── */}
<ContentCard
icon="🎚️"
title="Explore the radio"
subtitle="Schedule, shouts, requests and more"
title={t("exploreTitle")}
subtitle={t("exploreSubtitle")}
>
<div className="card-grid sm-2 lg-3">
{RADIO_LINKS.map((l) => (
@@ -140,24 +143,24 @@ export default async function RadioPage() {
{/* ── Today's schedule ────────────────────────────────────── */}
<ContentCard
icon="🗓️"
title={`Today's schedule${today ? ` · ${today}` : ""}`}
subtitle="What's on the airwaves today"
title={today ? t("todayTitleWithDay", { day: today }) : t("todayTitle")}
subtitle={t("todaySubtitle")}
action={
<Link className="btn btn-outline" href="/radio/schedule">
Full week →
{t("fullWeek")}
</Link>
}
padded={todaysSlots.length === 0}
>
{todaysSlots.length === 0 ? (
<EmptyState icon="📻">No shows scheduled for today.</EmptyState>
<EmptyState icon="📻">{t("noShowsToday")}</EmptyState>
) : (
<table>
<thead>
<tr>
<th>Time</th>
<th>Show</th>
<th>DJ</th>
<th>{t("colTime")}</th>
<th>{t("colShow")}</th>
<th>{t("colDj")}</th>
</tr>
</thead>
<tbody>
@@ -169,13 +172,13 @@ export default async function RadioPage() {
{formatTime(s.startTime)} &ndash; {formatTime(s.endTime)}
</td>
<td>
{s.showName || <span className="muted">Untitled show</span>}
{s.showName || <span className="muted">{t("untitledShow")}</span>}
</td>
<td>
{djName ? (
<Link href={`/u/${djName}`}>{djName}</Link>
) : (
<span className="muted">{`User #${s.userId}`}</span>
<span className="muted">{t("userNumber", { id: String(s.userId) })}</span>
)}
</td>
</tr>