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
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user