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:
1 parent
1199176883
commit
e004dfedaf
47 files changed
+1740
-471
No files matched your search
+30
-27
@@ -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)} – {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>
|
||||
|
||||
Reference in new issue
Block a user