locale second stage

This commit is contained in:
2026-07-01 11:21:29 -05:00
parent dca3a66206
commit 09020b7aef
12 changed files with 237 additions and 191 deletions

View File

@@ -1,53 +1,54 @@
"use client";
import { GROUP_IDS, GroupId, Match, Team } from "@/lib/types";
import { GROUP_IDS, GroupId, Match, Team, teamName } from "@/lib/types";
import { computeGroupTables } from "@/lib/standings";
import { Dictionary } from "@/lib/i18n";
import Flag from "./Flag";
function teamById(teams: Team[], id: string | null) {
return id ? teams.find((t) => t.id === id) : undefined;
}
// Kürzel der lokalen Browser-Zeitzone, z.B. "MEZ"/"GMT+1" einmal ermittelt.
const TZ_LABEL = (() => {
function tzLabel(locale: string): string {
try {
const parts = new Intl.DateTimeFormat("de-DE", { timeZoneName: "short" })
const parts = new Intl.DateTimeFormat(locale === "en" ? "en-US" : "de-DE", { timeZoneName: "short" })
.formatToParts(new Date());
return parts.find((p) => p.type === "timeZoneName")?.value ?? "";
} catch {
return "";
}
})();
}
function fmtDate(iso: string): string {
function fmtDate(iso: string, locale: string): string {
const d = new Date(iso);
const s = d.toLocaleString("de-DE", {
const s = d.toLocaleString(locale === "en" ? "en-US" : "de-DE", {
weekday: "short", day: "2-digit", month: "2-digit",
hour: "2-digit", minute: "2-digit",
});
return TZ_LABEL ? `${s} ${TZ_LABEL}` : s;
const tz = tzLabel(locale);
return tz ? `${s} ${tz}` : s;
}
function statusText(m: Match): { text: string; live: boolean } {
function statusText(m: Match, dict: Dictionary): { text: string; live: boolean } {
switch (m.status) {
case "LIVE":
case "IN_PLAY":
return { text: m.minute != null ? `${m.minute}'` : "läuft", live: true };
return { text: m.minute != null ? `${m.minute}'` : dict.status.running, live: true };
case "PAUSED":
return { text: "Halbzeit", live: true };
return { text: dict.status.halftime, live: true };
case "FINISHED":
return { text: "Beendet", live: false };
return { text: dict.status.finished, live: false };
default:
return { text: fmtDate(m.utcDate), live: false };
return { text: "", live: false };
}
}
// Mitte: Ergebnis (wenn vorhanden) oder ein schlichtes "" bei ungespielten
// Spielen. Datum/Uhrzeit steht bereits links im Status und wird hier NICHT
// wiederholt.
function ScoreCell({ m }: { m: Match }) {
function ScoreCell({ m, dict }: { m: Match; dict: Dictionary }) {
const hasScore = m.homeScore != null && m.awayScore != null;
const st = statusText(m);
const st = statusText(m, dict);
if (hasScore) {
return (
<span className={`fx-score ${st.live ? "score-live" : ""}`}>
@@ -59,8 +60,8 @@ function ScoreCell({ m }: { m: Match }) {
}
function GroupStandings({
group, teams, matches,
}: { group: GroupId; teams: Team[]; matches: Match[] }) {
group, teams, matches, dict,
}: { group: GroupId; teams: Team[]; matches: Match[]; dict: Dictionary }) {
// Live-Tabelle: laufende Spiele werden mit Zwischenstand eingerechnet.
const table = computeGroupTables(teams, matches, true).find((t) => t.group === group);
if (!table) return null;
@@ -80,15 +81,15 @@ function GroupStandings({
return (
<div className="fx-standings">
<div className="fx-standings-title">
Tabelle Gruppe {group}
{hasLive && <span className="fx-standings-live"> LIVE</span>}
{dict.fixtures.standingsTitle(group)}
{hasLive && <span className="fx-standings-live">{dict.label.liveIndicator}</span>}
</div>
<table className="standings">
<thead>
<tr>
<th className="team">Mannschaft</th>
<th>Sp</th><th>S</th><th>U</th><th>N</th>
<th>Tore</th><th>±</th><th>Pkt</th>
<th className="team">{dict.table.team}</th>
<th>{dict.table.matches}</th><th>{dict.table.won}</th><th>{dict.table.drawn}</th><th>{dict.table.lost}</th>
<th>{dict.table.goals}</th><th>{dict.table.goalDiff}</th><th>{dict.table.points}</th>
</tr>
</thead>
<tbody>
@@ -101,8 +102,8 @@ function GroupStandings({
<td className="team">
<span className={`rankdot ${cls}`}>{r.rank}</span>
<Flag team={team} size={20} />
<span className="team-name">{team?.localisedName ?? team?.name ?? r.teamId}</span>
{isLive && <span className="row-live-dot" title="läuft gerade" />}
<span className="team-name">{teamName(team, "de")}</span>
{isLive && <span className="row-live-dot" title={dict.status.running} />}
</td>
<td>{r.played}</td>
<td>{r.won}</td>
@@ -121,11 +122,13 @@ function GroupStandings({
}
export default function Fixtures({
group, teams, matches, onSelectGroup,
group, teams, matches, onSelectGroup, dict, locale,
}: {
group: GroupId; teams: Team[]; matches: Match[];
onSelectGroup: (g: GroupId) => void;
dict: Dictionary; locale: string;
}) {
const intlLocale = locale === "en" ? "en-US" : "de-DE";
const list = matches
.filter((m) => m.group === group)
.sort((a, b) => +new Date(a.utcDate) - +new Date(b.utcDate));
@@ -145,36 +148,37 @@ export default function Fixtures({
</div>
{list.length === 0 ? (
<div className="notice">Für Gruppe {group} liegen noch keine Spiele im Feed vor.</div>
<div className="notice">{dict.fixtures.noMatches(group)}</div>
) : (
<div className="fixtures">
{list.map((m) => {
const home = teamById(teams, m.homeTeamId);
const away = teamById(teams, m.awayTeamId);
const st = statusText(m);
const st = statusText(m, dict);
const displayText = st.text || fmtDate(m.utcDate, locale);
const homeWin = m.homeScore != null && m.awayScore != null && m.homeScore > m.awayScore;
const awayWin = m.homeScore != null && m.awayScore != null && m.awayScore > m.homeScore;
return (
<div className={`fx ${st.live ? "fx-live" : ""}`} key={m.id}>
<div className="fx-status">
{st.live && <span className="dot live" />}
<span>{st.text}</span>
<span>{displayText}</span>
</div>
<div className="fx-teams">
<div className={`fx-team home ${homeWin ? "win" : ""}`}>
<Flag team={home} size={22} />
<span className="fx-name">{home?.localisedName ?? home?.name ?? "—"}</span>
<span className="fx-name">{teamName(home, locale)}</span>
</div>
<ScoreCell m={m} />
<ScoreCell m={m} dict={dict} />
<div className={`fx-team away ${awayWin ? "win" : ""}`}>
<Flag team={away} size={22} />
<span className="fx-name">{away?.localisedName ?? away?.name ?? "—"}</span>
<span className="fx-name">{teamName(away, locale)}</span>
</div>
</div>
<div className="fx-meta">
{m.venue && <span className="fx-venue">📍 {m.venue}</span>}
{m.attendance != null && (
<span className="fx-att">👥 {m.attendance.toLocaleString("de-DE")}</span>
<span className="fx-att">👥 {m.attendance.toLocaleString(intlLocale)}</span>
)}
</div>
</div>
@@ -184,7 +188,7 @@ export default function Fixtures({
)}
{list.length > 0 && (
<GroupStandings group={group} teams={teams} matches={matches} />
<GroupStandings group={group} teams={teams} matches={matches} dict={dict} />
)}
</div>
);