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

+17 -15
View File
@@ -1,4 +1,5 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { notFound } from "next/navigation";
import { ContentCard, EmptyState, OnlineBadge, StatBlock } from "@/components/public/ui";
import { auth } from "@/lib/auth";
@@ -39,6 +40,7 @@ export default async function ProfilePage({
}: {
params: Promise<{ username: string }>;
}) {
const t = await getTranslations("pages.profile");
const { username } = await params;
const [user, imagerBase, session] = await Promise.all([
@@ -115,22 +117,22 @@ export default async function ProfilePage({
<div style={{ minWidth: 0, flex: 1 }}>
<h1 style={{ margin: "0 0 0.25rem" }}>{user.username}</h1>
<p style={{ margin: "0 0 0.6rem" }}>
{user.motto || <span className="muted">No motto</span>}
{user.motto || <span className="muted">{t("noMotto")}</span>}
</p>
<OnlineBadge online={user.online === "1"} />
</div>
</div>
<div className="stat-grid" style={{ padding: "0 1.1rem 1.1rem" }}>
<StatBlock icon="🏅" value={user.rank} label="Rank" />
<StatBlock icon="💰" value={user.credits.toLocaleString()} label="Credits" />
<StatBlock icon="📅" value={registered} label="Registered" />
<StatBlock icon="🏅" value={user.rank} label={t("statRank")} />
<StatBlock icon="💰" value={user.credits.toLocaleString()} label={t("statCredits")} />
<StatBlock icon="📅" value={registered} label={t("statRegistered")} />
</div>
</ContentCard>
{/* ── Badges ───────────────────────────────────────────── */}
<ContentCard icon="🏅" title="Badges" padded={badges.length === 0}>
<ContentCard icon="🏅" title={t("badgesTitle")} padded={badges.length === 0}>
{badges.length === 0 ? (
<EmptyState icon="🏅">No badges yet.</EmptyState>
<EmptyState icon="🏅">{t("badgesEmpty")}</EmptyState>
) : (
<div
style={{
@@ -157,9 +159,9 @@ export default async function ProfilePage({
</ContentCard>
{/* ── Photos ───────────────────────────────────────────── */}
<ContentCard icon="📸" title="Photos" padded={photos.length === 0}>
<ContentCard icon="📸" title={t("photosTitle")} padded={photos.length === 0}>
{photos.length === 0 ? (
<EmptyState icon="📸">No photos yet.</EmptyState>
<EmptyState icon="📸">{t("photosEmpty")}</EmptyState>
) : (
<div className="card-grid sm-2 lg-3" style={{ padding: "1rem" }}>
{photos.map((p) => (
@@ -180,7 +182,7 @@ export default async function ProfilePage({
</ContentCard>
{/* ── Guestbook ────────────────────────────────────────── */}
<ContentCard icon="📝" title="Guestbook" subtitle={`Messages on ${user.username}'s wall`}>
<ContentCard icon="📝" title={t("guestbookTitle")} subtitle={t("guestbookSubtitle", { username: user.username })}>
{isLoggedIn ? (
<form action={postGuestbook} className="card" style={{ marginBottom: "1rem" }}>
{/* Profile owner id drives the write target; the author is taken from
@@ -188,7 +190,7 @@ export default async function ProfilePage({
<input type="hidden" name="profileId" value={String(user.id)} />
<input type="hidden" name="username" value={user.username} />
<label htmlFor="gb-message" className="muted">
Leave a message
{t("leaveMessage")}
</label>
<textarea
id="gb-message"
@@ -196,19 +198,19 @@ export default async function ProfilePage({
required
maxLength={255}
rows={3}
placeholder={`Write something on ${user.username}'s guestbook…`}
placeholder={t("guestbookPlaceholder", { username: user.username })}
style={{ width: "100%", margin: "0.4rem 0 0.75rem", resize: "vertical" }}
/>
<button type="submit" className="btn btn-primary">
Post
{t("post")}
</button>
</form>
) : (
<p className="muted">Log in to leave a guestbook message.</p>
<p className="muted">{t("loginToPost")}</p>
)}
{guestbook.length === 0 ? (
<EmptyState icon="📝">No guestbook entries yet.</EmptyState>
<EmptyState icon="📝">{t("guestbookEmpty")}</EmptyState>
) : (
<div className="grid" style={{ gap: "0.75rem" }}>
{guestbook.map((g) => {
@@ -234,7 +236,7 @@ export default async function ProfilePage({
) : null}
<div style={{ flex: 1 }}>
<p style={{ margin: "0 0 0.25rem" }}>
<strong>{author?.username ?? `User #${g.userId}`}</strong>{" "}
<strong>{author?.username ?? t("unknownUser", { id: g.userId })}</strong>{" "}
<span className="muted">{formatDate(g.createdAt)}</span>
</p>
<p style={{ margin: 0 }}>{g.message}</p>