German language option + a full wording-clarity pass
A tiny typed i18n layer (no library): en.ts is the single source of UI copy and the clarity rewrite lives there — shorter sentences, plain words, the dense Methodology/Scoreboard copy untangled. de.ts mirrors its shape exactly, enforced by the compiler, with placeholder parity covered by tests. Every page now reads from the dictionary via useT(); dates flip locale through useFormat() (en-GB/de-DE); raw server values (status, outcome, stage) map through total records. The header gains an EN/DE toggle (persisted, defaults from the browser language); <html lang> stays in sync. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,34 +4,36 @@ import { ArrowLeft } from 'lucide-react';
|
||||
import { Card, CardBody, CardHeader } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { teamFlag } from '@/lib/teams';
|
||||
import { kickoffDay, kickoffTime } from '@/lib/format';
|
||||
import { fmt, useFormat, useT } from '@/lib/i18n';
|
||||
import type { Fixture, TeamProfile } from '@/lib/types';
|
||||
|
||||
const pct = (x: number | null) => (x == null ? '—' : `${(x * 100).toFixed(1)}%`);
|
||||
|
||||
function FixtureRow({ f, team }: { f: Fixture; team: string }) {
|
||||
const { t, kickoffDay, kickoffTime } = useFormat();
|
||||
const isHome = f.home.team === team;
|
||||
const opp = isHome ? f.away : f.home;
|
||||
const done = f.status === 'finished' || f.status === 'live';
|
||||
const us = isHome ? f.homeScore : f.awayScore;
|
||||
const them = isHome ? f.awayScore : f.homeScore;
|
||||
const res = done && us != null && them != null ? (us > them ? 'W' : us < them ? 'L' : 'D') : null;
|
||||
const res = done && us != null && them != null ? (us > them ? 'win' : us < them ? 'loss' : 'draw') : null;
|
||||
return (
|
||||
<Link to="/match/$num" params={{ num: String(f.num) }} className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm hover:bg-elevated">
|
||||
<span className="w-10 text-xs text-faint">{f.group ? `Grp ${f.group}` : f.round.replace('Round of', 'R')}</span>
|
||||
<span className="text-faint">{isHome ? 'vs' : '@'}</span>
|
||||
<span className="w-10 text-xs text-faint">{f.group ? fmt(t.team.groupShort, { group: f.group }) : f.stage !== 'group' ? t.team.stageShort[f.stage] : f.round}</span>
|
||||
<span className="text-faint">{isHome ? t.common.vs : t.team.awayIndicator}</span>
|
||||
<span aria-hidden>{opp.team ? teamFlag(opp.team) : '⚽'}</span>
|
||||
<span className="truncate text-ink">{opp.label}</span>
|
||||
<span className="ml-auto tnum text-faint">
|
||||
{done && us != null ? <span className="text-ink">{us}–{them}</span> : `${kickoffDay(f.kickoff)} ${kickoffTime(f.kickoff)}`}
|
||||
</span>
|
||||
{res && <span className={`grid h-5 w-5 place-items-center rounded text-[11px] font-bold ${res === 'W' ? 'bg-win/20 text-win' : res === 'L' ? 'bg-loss/20 text-loss' : 'bg-draw/20 text-draw'}`}>{res}</span>}
|
||||
{res && <span className={`grid h-5 w-5 place-items-center rounded text-[11px] font-bold ${res === 'win' ? 'bg-win/20 text-win' : res === 'loss' ? 'bg-loss/20 text-loss' : 'bg-draw/20 text-draw'}`}>{t.team.resultShort[res]}</span>}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function TeamProfilePage() {
|
||||
const { name } = useParams({ strict: false }) as { name: string };
|
||||
const t = useT();
|
||||
const [profile, setProfile] = useState<TeamProfile | null>(null);
|
||||
const [failed, setFailed] = useState(false);
|
||||
|
||||
@@ -46,14 +48,14 @@ export function TeamProfilePage() {
|
||||
return () => { alive = false; };
|
||||
}, [name]);
|
||||
|
||||
if (failed) return <div className="py-16 text-center text-muted">Unknown team. <Link to="/groups" className="text-accent">All teams</Link></div>;
|
||||
if (failed) return <div className="py-16 text-center text-muted">{t.team.unknown} <Link to="/groups" className="text-accent">{t.team.allTeams}</Link></div>;
|
||||
if (!profile) return <div className="h-80 animate-pulse rounded-xl border border-line bg-panel" />;
|
||||
|
||||
const s = profile.standing;
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<Link to="/teams" className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-ink">
|
||||
<ArrowLeft size={15} /> All teams
|
||||
<ArrowLeft size={15} /> {t.team.allTeams}
|
||||
</Link>
|
||||
|
||||
<Card>
|
||||
@@ -62,16 +64,16 @@ export function TeamProfilePage() {
|
||||
<div>
|
||||
<h1 className="font-display text-2xl font-extrabold text-ink">{profile.team}</h1>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2 text-sm">
|
||||
{profile.group && <Badge tone="neutral">Group {profile.group}</Badge>}
|
||||
<Badge tone="muted">Elo {profile.elo}</Badge>
|
||||
<Badge tone="gold">Title {pct(profile.championOdds)}</Badge>
|
||||
<Badge tone="accent">Advance {pct(profile.qualifyOdds)}</Badge>
|
||||
{profile.group && <Badge tone="neutral">{fmt(t.common.group, { group: profile.group })}</Badge>}
|
||||
<Badge tone="muted">{fmt(t.team.eloBadge, { rating: profile.elo })}</Badge>
|
||||
<Badge tone="gold">{fmt(t.team.titleOddsBadge, { pct: pct(profile.championOdds) })}</Badge>
|
||||
<Badge tone="accent">{fmt(t.team.advanceOddsBadge, { pct: pct(profile.qualifyOdds) })}</Badge>
|
||||
</div>
|
||||
</div>
|
||||
{s && (
|
||||
<div className="ml-auto text-right text-sm">
|
||||
<div className="text-faint text-xs">Group standing</div>
|
||||
<div className="tnum text-ink">#{s.rank} · {s.points} pts · {s.won}-{s.drawn}-{s.lost} · GD {s.gd > 0 ? `+${s.gd}` : s.gd}</div>
|
||||
<div className="text-faint text-xs">{t.team.groupStanding}</div>
|
||||
<div className="tnum text-ink">{fmt(t.team.standingLine, { rank: s.rank, points: s.points, won: s.won, drawn: s.drawn, lost: s.lost, gd: s.gd > 0 ? `+${s.gd}` : s.gd })}</div>
|
||||
</div>
|
||||
)}
|
||||
</CardBody>
|
||||
@@ -79,14 +81,14 @@ export function TeamProfilePage() {
|
||||
|
||||
<div className="grid gap-5 md:grid-cols-[1.3fr_1fr]">
|
||||
<Card>
|
||||
<CardHeader><span className="font-display font-bold text-ink">Fixtures</span></CardHeader>
|
||||
<CardHeader><span className="font-display font-bold text-ink">{t.team.fixtures}</span></CardHeader>
|
||||
<CardBody className="space-y-0.5">
|
||||
{profile.fixtures.map((f) => <FixtureRow key={f.num} f={f} team={profile.team} />)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader><span className="font-display font-bold text-ink">All-time top scorers</span></CardHeader>
|
||||
<CardHeader><span className="font-display font-bold text-ink">{t.team.allTimeTopScorers}</span></CardHeader>
|
||||
<CardBody>
|
||||
{profile.keyPlayers.length ? (
|
||||
<ol className="space-y-1.5">
|
||||
@@ -95,11 +97,11 @@ export function TeamProfilePage() {
|
||||
<span className="w-4 text-right text-xs font-bold text-faint">{i + 1}</span>
|
||||
<span className="truncate text-ink-soft">{p.name}</span>
|
||||
<span className="tnum ml-auto font-semibold text-ink">{p.goals}</span>
|
||||
{p.pens ? <span className="text-[11px] text-faint">{p.pens}p</span> : null}
|
||||
{p.pens ? <span className="text-[11px] text-faint">{fmt(t.common.pensShort, { n: p.pens })}</span> : null}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
) : <p className="text-sm text-faint">No data.</p>}
|
||||
) : <p className="text-sm text-faint">{t.common.noData}</p>}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user