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 Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { ContentCard, EmptyState } from "@/components/public/ui";
|
||||
@@ -25,10 +26,10 @@ export async function generateMetadata({
|
||||
|
||||
// The reaction set offered by the voting UI. Must stay in sync with
|
||||
// ALLOWED_REACTIONS in src/actions/article-reactions.ts.
|
||||
const REACTIONS: { key: string; label: string }[] = [
|
||||
{ key: "like", label: "👍 Like" },
|
||||
{ key: "love", label: "❤️ Love" },
|
||||
{ key: "wow", label: "😮 Wow" },
|
||||
const REACTIONS: { key: string; emoji: string }[] = [
|
||||
{ key: "like", emoji: "👍" },
|
||||
{ key: "love", emoji: "❤️" },
|
||||
{ key: "wow", emoji: "😮" },
|
||||
];
|
||||
|
||||
function formatDate(d: Date | null | undefined): string {
|
||||
@@ -41,6 +42,7 @@ export default async function ArticlePage({
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const t = await getTranslations("pages.article");
|
||||
|
||||
let article: Awaited<ReturnType<typeof prisma.websiteArticles.findUnique>> = null;
|
||||
try {
|
||||
@@ -109,7 +111,7 @@ export default async function ArticlePage({
|
||||
subtitle={article.createdAt ? article.createdAt.toISOString().slice(0, 10) : undefined}
|
||||
action={
|
||||
<Link className="btn btn-outline" href="/news">
|
||||
← News
|
||||
{t("backToNews")}
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
@@ -125,12 +127,13 @@ export default async function ArticlePage({
|
||||
</ContentCard>
|
||||
|
||||
{/* ── Reactions ─────────────────────────────────────────── */}
|
||||
<ContentCard icon="💬" title="Reactions" subtitle="Tell us what you think">
|
||||
<ContentCard icon="💬" title={t("reactionsTitle")} subtitle={t("reactionsSubtitle")}>
|
||||
{loggedIn ? (
|
||||
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
|
||||
{REACTIONS.map((r) => {
|
||||
const active = myReaction === r.key;
|
||||
const count = reactionCounts.get(r.key) ?? 0;
|
||||
const label = `${r.emoji} ${t(`reaction.${r.key}`)}`;
|
||||
return (
|
||||
<form key={r.key} action={toggleReaction} style={{ margin: 0 }}>
|
||||
<input type="hidden" name="articleId" value={String(articleId)} />
|
||||
@@ -139,9 +142,9 @@ export default async function ArticlePage({
|
||||
type="submit"
|
||||
aria-pressed={active}
|
||||
className={`btn ${active ? "btn-primary" : "btn-outline"}`}
|
||||
title={active ? "Click to remove your reaction" : `React with ${r.key}`}
|
||||
title={active ? t("removeReaction") : t("reactWith", { reaction: r.key })}
|
||||
>
|
||||
<span>{r.label}</span>
|
||||
<span>{label}</span>
|
||||
<span
|
||||
className="muted"
|
||||
style={{ fontSize: "0.85rem", color: "inherit", opacity: 0.85 }}
|
||||
@@ -155,14 +158,14 @@ export default async function ArticlePage({
|
||||
</div>
|
||||
) : reactionCounts.size === 0 ? (
|
||||
<EmptyState icon="💬">
|
||||
No reactions yet. <Link href="/login">Log in</Link> to react.
|
||||
{t("noReactions")} <Link href="/login">{t("logIn")}</Link> {t("toReact")}
|
||||
</EmptyState>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
|
||||
{REACTIONS.filter((r) => (reactionCounts.get(r.key) ?? 0) > 0).map((r) => (
|
||||
<span key={r.key} className="currency">
|
||||
<span style={{ fontWeight: 800 }}>{r.label}</span>
|
||||
<span style={{ fontWeight: 800 }}>{`${r.emoji} ${t(`reaction.${r.key}`)}`}</span>
|
||||
<span className="muted" style={{ fontSize: "0.85rem" }}>
|
||||
{reactionCounts.get(r.key) ?? 0}
|
||||
</span>
|
||||
@@ -170,7 +173,7 @@ export default async function ArticlePage({
|
||||
))}
|
||||
</div>
|
||||
<p className="muted" style={{ marginTop: "0.6rem" }}>
|
||||
<Link href="/login">Log in</Link> to react.
|
||||
<Link href="/login">{t("logIn")}</Link> {t("toReact")}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
@@ -179,11 +182,11 @@ export default async function ArticlePage({
|
||||
{/* ── Comments ──────────────────────────────────────────── */}
|
||||
<ContentCard
|
||||
icon="🗨️"
|
||||
title="Comments"
|
||||
subtitle={comments.length > 0 ? `${comments.length} so far` : "Join the conversation"}
|
||||
title={t("commentsTitle")}
|
||||
subtitle={comments.length > 0 ? t("commentsCount", { count: comments.length }) : t("commentsSubtitle")}
|
||||
>
|
||||
{comments.length === 0 ? (
|
||||
<EmptyState icon="🗨️">No comments yet. Be the first to comment.</EmptyState>
|
||||
<EmptyState icon="🗨️">{t("noComments")}</EmptyState>
|
||||
) : (
|
||||
<div className="grid" style={{ marginBottom: "1.5rem" }}>
|
||||
{comments.map((c) => {
|
||||
@@ -194,7 +197,7 @@ export default async function ArticlePage({
|
||||
{name ? (
|
||||
<Link href={`/u/${name}`}>{name}</Link>
|
||||
) : (
|
||||
<span>User #{c.userId}</span>
|
||||
<span>{t("userNumber", { id: c.userId })}</span>
|
||||
)}
|
||||
{c.createdAt ? <span> · {formatDate(c.createdAt)}</span> : null}
|
||||
</p>
|
||||
@@ -212,7 +215,7 @@ export default async function ArticlePage({
|
||||
htmlFor="comment"
|
||||
style={{ display: "block", fontWeight: 700, marginBottom: "0.5rem" }}
|
||||
>
|
||||
Add a comment
|
||||
{t("addComment")}
|
||||
</label>
|
||||
<textarea
|
||||
id="comment"
|
||||
@@ -220,18 +223,18 @@ export default async function ArticlePage({
|
||||
required
|
||||
maxLength={255}
|
||||
rows={3}
|
||||
placeholder="Write your comment…"
|
||||
placeholder={t("commentPlaceholder")}
|
||||
style={{ width: "100%", resize: "vertical" }}
|
||||
/>
|
||||
<div style={{ marginTop: "0.75rem" }}>
|
||||
<button type="submit" className="btn btn-primary">
|
||||
Post comment
|
||||
{t("postComment")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<p className="muted">
|
||||
<Link href="/login">Log in</Link> to leave a comment.
|
||||
<Link href="/login">{t("logIn")}</Link> {t("toComment")}
|
||||
</p>
|
||||
)}
|
||||
</ContentCard>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import Link from "next/link";
|
||||
import { ContentCard, EmptyState } from "@/components/public/ui";
|
||||
import { excerpt } from "@/lib/format";
|
||||
@@ -12,6 +13,8 @@ function formatDate(d: Date | null): string {
|
||||
}
|
||||
|
||||
export default async function NewsPage() {
|
||||
const t = await getTranslations("pages.news");
|
||||
|
||||
let articles: Awaited<ReturnType<typeof prisma.websiteArticles.findMany>> = [];
|
||||
try {
|
||||
articles = await prisma.websiteArticles.findMany({
|
||||
@@ -27,13 +30,13 @@ export default async function NewsPage() {
|
||||
<main style={{ display: "grid", gap: "1.5rem" }}>
|
||||
<ContentCard
|
||||
icon="📰"
|
||||
title="News"
|
||||
subtitle="The latest announcements, events and updates from the hotel"
|
||||
title={t("title")}
|
||||
subtitle={t("subtitle")}
|
||||
/>
|
||||
|
||||
<ContentCard padded={articles.length === 0}>
|
||||
{articles.length === 0 ? (
|
||||
<EmptyState icon="📰">No articles yet.</EmptyState>
|
||||
<EmptyState icon="📰">{t("noArticles")}</EmptyState>
|
||||
) : (
|
||||
<div className="card-grid sm-2 lg-3" style={{ padding: "1rem" }}>
|
||||
{articles.map((a) => (
|
||||
|
||||
Reference in new issue
Block a user