3d3267d803
Coming-up cards and the post-match comparison now use the model's most
likely score ('2-0') instead of decimal goal expectations ('2.3-0.4');
the pre-match model card keeps just the honesty note since the bar's
chip already carries the predicted scoreline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
484 lines
20 KiB
TypeScript
484 lines
20 KiB
TypeScript
// The single source of truth for UI copy — the clarity pass lives here.
|
||
// Keys are mirrored exactly by de.ts (the compiler enforces it via Dict).
|
||
export const en = {
|
||
nav: {
|
||
live: "Live",
|
||
groups: "Groups",
|
||
bracket: "Bracket",
|
||
predict: "Predict",
|
||
vsMarket: "vs Market",
|
||
model: "Model",
|
||
story: "Story",
|
||
teams: "Teams",
|
||
data: "Data",
|
||
compare: "Compare",
|
||
more: "More",
|
||
},
|
||
common: {
|
||
liveNow: "Live",
|
||
offline: "Offline",
|
||
connecting: "Connecting…",
|
||
connected: "Connected",
|
||
liveVia: "Live · {source}",
|
||
offlineShowingSchedule: "Offline — showing the schedule",
|
||
offlineBanner: "Offline — showing the last data from {time}. Reconnecting…",
|
||
switchToLightMode: "Switch to light mode",
|
||
switchToDarkMode: "Switch to dark mode",
|
||
draw: "Draw",
|
||
fullTime: "Full time",
|
||
fullTimeShort: "FT",
|
||
inMinutes: "in {n}m",
|
||
todayAt: "today {time}",
|
||
group: "Group {group}",
|
||
matchNumberShort: "M{num}",
|
||
vs: "vs",
|
||
mostLikelyScore: "Most likely scoreline",
|
||
teamWinPct: "{team} win {pct}",
|
||
drawPct: "Draw {pct}",
|
||
never: "never",
|
||
justNow: "just now",
|
||
minAgo: "{n} min ago",
|
||
hoursAgo: "{n} h ago",
|
||
back: "Back",
|
||
noData: "No data",
|
||
pensShort: "{n}p",
|
||
},
|
||
stage: {
|
||
group: "Group stage",
|
||
r32: "Round of 32",
|
||
r16: "Round of 16",
|
||
qf: "Quarter-final",
|
||
sf: "Semi-final",
|
||
third: "Third place",
|
||
final: "Final",
|
||
},
|
||
status: {
|
||
scheduled: "Scheduled",
|
||
live: "Live",
|
||
finished: "Finished",
|
||
},
|
||
live: {
|
||
minute: "{n}'",
|
||
title: "Live",
|
||
loadingSubtitle: "Loading the tournament…",
|
||
subtitle: "{season} · 48 teams · 104 matches",
|
||
liveNowHeading: "Live now",
|
||
todayHeading: "Today's matches",
|
||
nextUp: "Next up",
|
||
nextUpDay: "Next up · {day}",
|
||
comingUp: "Coming up",
|
||
myTeam: "My team",
|
||
latestResults: "Latest results",
|
||
digestTitle: "The day in brief",
|
||
digestYesterday: "Yesterday",
|
||
digestMovers: "Title-odds movers",
|
||
empty: "The schedule is ready. Matches will appear here once the tournament kicks off.",
|
||
},
|
||
match: {
|
||
modelPrefix: "Model:",
|
||
loadError: "Couldn't load that match.",
|
||
backToLive: "Back to Live",
|
||
liveWinProbability: "Live win probability",
|
||
inPlayHelp: "Updates with the score and the clock. The goals still to come are estimated from each team's pre-match expectation.",
|
||
statsTitle: "Match stats",
|
||
stats: {
|
||
possessionPct: "Possession",
|
||
totalShots: "Shots",
|
||
shotsOnTarget: "On target",
|
||
wonCorners: "Corners",
|
||
foulsCommitted: "Fouls",
|
||
saves: "Saves",
|
||
},
|
||
timeline: "Timeline",
|
||
momentum: "How the win probability swung",
|
||
oddsMovement: "Market movement vs the model",
|
||
oddsMovementNote: "Solid lines: the bookmaker's odds with the margin removed, drifting toward kickoff. Dashed lines: our model's probabilities. The market is a benchmark — the model never sees it.",
|
||
preMatchModel: "Pre-match model",
|
||
modelExpectation: "Model expectation",
|
||
modelHonestyNote: "Model probabilities — not betting advice.",
|
||
recentForm: "Recent form",
|
||
headToHead: "Head to head",
|
||
h2h: {
|
||
teamWins: "{team} wins",
|
||
drawsTotal: "draws · {games} matches in total",
|
||
recentMeetings: "Recent meetings",
|
||
},
|
||
lineups: "Lineups",
|
||
keyPlayers: "Key players (all-time top scorers)",
|
||
xgRace: "xG race",
|
||
shotMap: "Shot map",
|
||
momentumTitle: "Attack momentum",
|
||
weatherLine: "{temp}°C · {precip}% rain · {wind} km/h wind",
|
||
altitude: "{m} m altitude",
|
||
attendanceLine: "{stadium} · {n} spectators · referee {ref}",
|
||
potm: "Player of the match",
|
||
xgAttribution: "xG and ratings are FotMob estimates.",
|
||
tabs: {
|
||
summary: "Summary",
|
||
timeline: "Timeline",
|
||
stats: "Stats",
|
||
lineups: "Lineups",
|
||
},
|
||
keyMoments: "Key moments",
|
||
viewTimeline: "View full timeline",
|
||
kickOff: "Kick-off",
|
||
halfTime: "Half-time",
|
||
goal: "Goal",
|
||
ownGoal: "Own goal",
|
||
penalty: "Penalty",
|
||
assist: "Assist · {name}",
|
||
noGoalsYet: "No goals yet.",
|
||
subbedOff: "Subbed off",
|
||
predictedLineups: "Predicted lineups",
|
||
predictedLineupsNote: "FotMob's projected starting XIs — the official lineups usually arrive about an hour before kickoff.",
|
||
predictedVsActual: "Model's pre-match prediction: {ph}–{pa} · final score {ah}–{aa}",
|
||
chips: {
|
||
att: "Att",
|
||
ref: "Ref",
|
||
poss: "Poss",
|
||
shots: "Shots",
|
||
},
|
||
},
|
||
compare: {
|
||
title: "Compare",
|
||
subtitle: "Any two of the 48, side by side: the model's matchup and the full historical record.",
|
||
modelMatchup: "Model matchup (neutral ground)",
|
||
neutralNote: "Hypothetical, on neutral ground — the model's current view of these two squads.",
|
||
samePick: "Pick two different teams.",
|
||
neverMet: "These two have never met in 150 years of internationals.",
|
||
goals: "Goals: {a} – {b}",
|
||
},
|
||
search: {
|
||
title: "Search",
|
||
placeholder: "Team or match…",
|
||
hint: "Type at least two letters — teams and matches.",
|
||
noResults: "Nothing found.",
|
||
},
|
||
notFound: {
|
||
message: "Off the pitch — this page doesn't exist.",
|
||
backToLive: "Back to Live",
|
||
},
|
||
footer: {
|
||
worldCup: "World Cup 2026",
|
||
allData: "All the data",
|
||
howModelWorks: "How the model works",
|
||
disclaimer: "Model odds — not betting advice",
|
||
},
|
||
glossary: {
|
||
rps: {
|
||
term: "RPS",
|
||
definition: "Ranked Probability Score — how far the forecast probabilities were from the actual result. Home win, draw and away win count as ordered outcomes: missing by one step costs less than missing by two. 0 is perfect; lower is better.",
|
||
},
|
||
brier: {
|
||
term: "Brier",
|
||
definition: "The average squared error of the forecast probabilities across the three outcomes (home win, draw, away win). 0 is perfect; lower is better.",
|
||
},
|
||
ece: {
|
||
term: "ECE",
|
||
definition: "Expected Calibration Error — the average gap between predicted probability and how often it really happened. Example: outcomes given 70% should occur about 70% of the time. Below 0.05 is excellent.",
|
||
},
|
||
elo: {
|
||
term: "Elo",
|
||
definition: "A team strength rating that updates after every match. Beat a stronger team and you gain more points. A 100-point gap means roughly a 64% expected score for the stronger side.",
|
||
},
|
||
xg: {
|
||
term: "xG",
|
||
definition: "Expected goals — for each shot, the chance it becomes a goal, based on thousands of similar past shots. Total xG measures the quality of chances, not luck.",
|
||
},
|
||
deVig: {
|
||
term: "de-vig",
|
||
definition: "Bookmaker odds include a profit margin, so their implied probabilities add up to more than 100%. De-vigging removes that margin and rescales them to exactly 100% — the bookmaker's honest opinion.",
|
||
},
|
||
ensemble: {
|
||
term: "ensemble",
|
||
definition: "A blend of two independent goal models: one built on Elo ratings, one on each team's attack and defence. The blend weight was tuned on validation data, where the combination beat both single models.",
|
||
},
|
||
},
|
||
groups: {
|
||
title: "Groups",
|
||
subtitle: "All 12 groups. The top 2 in each group advance, plus the 8 best third-placed teams.",
|
||
allTeams: "All 48 teams",
|
||
legendTop2: "Top 2 — advance",
|
||
legendThird: "3rd — still in the race for best third",
|
||
scenarios: "Final-round scenarios",
|
||
scThrough: "Top 2 secured",
|
||
scWin: "Top 2 with a win",
|
||
scAlive: "Still possible ({n}/{total} outcomes)",
|
||
scOut: "Can't reach the top 2",
|
||
scNote: "Scenarios count win/draw/loss outcomes of the remaining group matches; tight cases can still hinge on goal difference. Third place runs through the best-thirds race across all groups.",
|
||
thirdsRace: "Best-thirds race",
|
||
thirdsRaceNote: "All third-placed teams ranked by points, goal difference and goals — the top 8 advance to the Round of 32. Played · GD · Points.",
|
||
tableHeaderPldPts: "Pld · Pts",
|
||
colTeam: "Team",
|
||
colPlayed: "P",
|
||
colGoalDiff: "GD",
|
||
colPoints: "Pts",
|
||
colForm: "Form",
|
||
formLetter: {
|
||
W: "Win",
|
||
D: "Draw",
|
||
L: "Loss",
|
||
},
|
||
},
|
||
bracket: {
|
||
title: "Bracket",
|
||
subtitle: "The knockout rounds — from the Round of 32 to the Final.",
|
||
thirdPlacePlayoff: "Third-place play-off",
|
||
advanceTooltip: "Model: each team's chance to advance",
|
||
whatIf: "What if?",
|
||
whatIfHint: "Tap a team to send them through the bracket. Real results stay fixed. Percentages are the model's view of each pairing.",
|
||
reset: "Reset picks",
|
||
champion: "Your champion: {team}",
|
||
pathChance: "Model chance of exactly this run: {pct}",
|
||
projected: "Projected champion: {team}",
|
||
},
|
||
teams: {
|
||
title: "Teams",
|
||
subtitle: "All 48 teams, ranked by the model's title odds. Tap a team to open its profile.",
|
||
advanceShort: "advance {pct}",
|
||
goldenBoot: "Golden Boot race",
|
||
goldenBootNote: "Goals in this tournament, from the live event feed. Own goals and shootout kicks don't count.",
|
||
},
|
||
outcome: {
|
||
home: "Home win",
|
||
draw: "Draw",
|
||
away: "Away win",
|
||
},
|
||
predict: {
|
||
title: "Predict",
|
||
loadingSubtitle: "Running the simulation…",
|
||
subtitle: "Elo + Dixon-Coles, simulated {iterations} times (Monte Carlo) · ratings as of {date}",
|
||
methodologyLink: "How it works & how accurate it is",
|
||
trophy: {
|
||
title: "Who lifts the trophy?",
|
||
},
|
||
titleRace: {
|
||
title: "Title race over time",
|
||
empty: "This chart fills in as results come in. After every finished match, the teams are re-rated and the simulation runs again.",
|
||
},
|
||
fullOddsHeading: "Full odds — group, knockout & title",
|
||
nextMatchesHeading: "Match predictions",
|
||
filters: {
|
||
next3Days: "Next 3 days",
|
||
allUpcoming: "All upcoming",
|
||
allStages: "All",
|
||
groupStage: "Group stage",
|
||
knockout: "Knockout",
|
||
},
|
||
oddsTable: {
|
||
team: "Team",
|
||
winGroup: "Win group",
|
||
qualify: "Advance",
|
||
reachR16: "R16",
|
||
reachQF: "QF",
|
||
reachSF: "SF",
|
||
reachFinal: "Final",
|
||
champion: "Champion",
|
||
},
|
||
},
|
||
scoreboard: {
|
||
title: "Model vs Market",
|
||
subtitle: "Before each match we lock in our forecast next to the bookmaker's odds (margin removed). After full time, both are scored — the lower number was closer to what happened.",
|
||
headToHead: {
|
||
title: "Running head-to-head",
|
||
modelLabel: "our model · RPS",
|
||
marketLabel: "bookmaker · RPS",
|
||
closerTimes: "closer {n}×",
|
||
matchesScored: {
|
||
one: "{n} match scored",
|
||
other: "{n} matches scored",
|
||
},
|
||
tie: "level",
|
||
modelAhead: "model ahead",
|
||
marketAhead: "market ahead",
|
||
empty: "The head-to-head starts when the first match ends. Each forecast is frozen before kickoff and scored at full time.",
|
||
},
|
||
rules: {
|
||
text: "Honest rules: both forecasts are frozen before kickoff. The bookmaker's margin is removed ({devig}). Both are scored with the {rps}. Late snapshots don't count. Beating the market over a few matches is mostly noise — watch the gap, not the lead.",
|
||
rpsTerm: "Ranked Probability Score",
|
||
},
|
||
row: {
|
||
modelLabel: "Model",
|
||
lateNote: "snapshot late — not counted in the totals",
|
||
noMarketLine: "no bookmaker odds captured before kickoff",
|
||
result: "result: {outcome}",
|
||
modelRps: "model RPS {rps}",
|
||
marketRps: "market RPS {rps}",
|
||
modelCloser: "model closer",
|
||
marketCloser: "market closer",
|
||
},
|
||
emptyRows: "No frozen forecasts yet — the first snapshot is taken {minutes} minutes before kickoff.",
|
||
upsets: {
|
||
title: "Biggest surprises",
|
||
chance: "{pct} likely",
|
||
},
|
||
},
|
||
methodology: {
|
||
title: "Methodology",
|
||
subtitle: "How the model works — and an honest measure of how good it really is.",
|
||
stepLabel: "STEP {n}",
|
||
steps: {
|
||
elo: {
|
||
title: "Elo ratings",
|
||
body: "Every nation gets a strength rating (Elo), built from 150 years of international results — about 49,000 matches. The rating updates after every game, and important matches count more. A second, faster-reacting rating tracks current form (recent results move it three times as hard), and the two are blended.",
|
||
},
|
||
goalModels: {
|
||
title: "Two goal models",
|
||
body: "We estimate each match's expected goals in two ways: from the Elo ratings, and from a separate attack/defence model (Dixon-Coles, fit on the last 15 years, recent games weighted more). The attack/defence model carries most of the blend and is refit every night during the tournament — current form counts for more than distant history.",
|
||
},
|
||
scorelines: {
|
||
title: "Scorelines (Dixon-Coles)",
|
||
body: "A scoreline model turns those goal estimates into a probability for every possible score — and from that, win, draw and loss. It uses a bivariate Poisson distribution with the Dixon-Coles fix for low-scoring games.",
|
||
},
|
||
monteCarlo: {
|
||
title: "Monte Carlo",
|
||
body: "We simulate the whole 48-team tournament 20,000 times, playing out every remaining match. That gives each team's odds of winning the title or reaching each round — refreshed after every result.",
|
||
},
|
||
},
|
||
backtest: {
|
||
heading: "How good is it? (backtest on unseen matches)",
|
||
intro: "We test the model on matches it has never seen. Parameters were fit on matches before {trainEnd}. The blend settings were tuned on {valFrom}–{valTo}. The numbers below come from {tested} untouched matches ({testFrom}–{testTo}) — each predicted using only data available before kickoff (walk-forward).",
|
||
validationFallback: "a validation window",
|
||
stats: {
|
||
outcomeAccuracy: "outcome accuracy",
|
||
rps: "ranked probability score",
|
||
worldCupAccuracy: "World Cup accuracy ({n})",
|
||
},
|
||
metricsLine: "Metrics: {rps} · {brier} · {ece} — the live model is an {ensemble}.",
|
||
rpsChartHeading: "Ranked Probability Score (RPS) — lower is better. Our model vs. simpler baselines.",
|
||
thisModel: "This model",
|
||
baseRates: "Base rates",
|
||
coinFlip: "Coin flip",
|
||
bestBadge: "best",
|
||
},
|
||
calibration: {
|
||
heading: "Is it calibrated?",
|
||
xAxis: "predicted probability",
|
||
yAxis: "actually happened",
|
||
pointTooltip: "predicted {predicted}% → happened {observed}% ({count})",
|
||
caption: "The points hug the diagonal. That means the model is honest: when it says 30%, the outcome happens about 30% of the time. The calibration error (ECE) is {ece} — lower is better, and under 0.05 is excellent.",
|
||
},
|
||
diary: {
|
||
title: "Model diary",
|
||
note: "Every finished match re-rates the teams and re-runs the simulation. This log shows each update and the biggest title-odds move it caused.",
|
||
},
|
||
limits: {
|
||
heading: "Honest limits",
|
||
notAdvice: "These are model probabilities, not betting advice. The model does not use bookmaker odds, so we make no claim to beat the market. Sharp betting markets are still the best forecasters there are.",
|
||
sparseData: "Detailed data for international football (expected goals, match events) is sparse. Some teams get richer tactical breakdowns than others. Coverage improves as live data comes in during the tournament.",
|
||
variance: "Football is high-variance. A 60% favourite still loses a lot. Calibrated means our 60% really happens about 60% of the time — not that the favourite always wins.",
|
||
openData: "Everything is transparent and can be rebuilt from public data. That openness is the point.",
|
||
},
|
||
},
|
||
data: {
|
||
title: "The data",
|
||
subtitle: "Everything this site runs on — with its size, its source and its last update. No black boxes.",
|
||
lake: {
|
||
heading: "Event-data lake",
|
||
openDataBadge: "{provider} open data",
|
||
parsed: "event data parsed",
|
||
events: "on-pitch events",
|
||
matches: "matches",
|
||
players: "players",
|
||
summary: "Every pass, shot and duel from {seasons} competition seasons — eight World Cups among them. We turn it into the team style fingerprints ({covered} of {total} nations covered) and the score-state analysis on the methodology page.",
|
||
},
|
||
archive: {
|
||
heading: "Historical archive",
|
||
internationalResults: "International results (1872 → now)",
|
||
h2hPairings: "Head-to-head records precomputed",
|
||
ratingsThrough: "Ratings updated through",
|
||
squadValues: "Squad market values ({provider})",
|
||
fingerprints: "Team style fingerprints (event data)",
|
||
teamsCount: {
|
||
one: "{n} team",
|
||
other: "{n} teams",
|
||
},
|
||
},
|
||
live: {
|
||
heading: "Live collection (this tournament)",
|
||
oddsLines: "Bookmaker lines captured",
|
||
oddsFixtures: "Fixtures with market odds",
|
||
frozenForecasts: "Forecasts locked before kickoff",
|
||
enrichedMatches: "Matches enriched (form, head-to-head, lineups)",
|
||
cachedResponses: "API responses cached",
|
||
ingestRuns: "Data ingestion runs logged",
|
||
},
|
||
sources: {
|
||
heading: "Source health",
|
||
healthy: "healthy",
|
||
paused: "paused (too many failures)",
|
||
recentFailures: {
|
||
one: "{n} recent failure",
|
||
other: "{n} recent failures",
|
||
},
|
||
lastOk: "last OK {time}",
|
||
empty: "No live sources polled yet.",
|
||
footer: {
|
||
label: "Sources:",
|
||
roleFixtures: "fixtures",
|
||
roleLiveScores: "live scores, form, lineups, bookmaker lines",
|
||
roleFallback: "fallback",
|
||
roleResultsArchive: "results archive",
|
||
roleOpenData: "open data",
|
||
roleSquadValues: "squad values",
|
||
note: "All data is fetched on our servers, cached, and rate-limited. If a source blocks us or goes down, you see slightly older data — never a broken page.",
|
||
},
|
||
},
|
||
},
|
||
story: {
|
||
title: "Story",
|
||
subtitle: "A data-driven look back.",
|
||
loadingSubtitle: "Loading the data story…",
|
||
datasetMissing: "The story dataset isn't available. Run {command} to generate it.",
|
||
heroTitle: "The greatest final ever, in data",
|
||
intro: "Argentina and France scored six goals between them across 120 minutes — two from Messi, one from Di María, and a Mbappé hat-trick — before it went to penalties. Here's what the event data says about how it happened.",
|
||
byTheNumbers: "By the numbers",
|
||
stats: {
|
||
expectedGoals: "Expected goals",
|
||
shots: "Shots",
|
||
passes: "Passes",
|
||
passAccuracy: "Pass accuracy %",
|
||
},
|
||
shotMapTitle: "Every shot, sized by xG",
|
||
shotMapCaption: "Argentina ({xg} xG) created the bigger chances. France's comeback was built on a burst of high-quality shots once Mbappé caught fire. Each dot is one shot — the bigger the dot, the better the chance. Filled dots are goals.",
|
||
xgRaceTitle: "The xG race",
|
||
xgRaceCaption: "For 80 minutes, Argentina built a clear lead in expected goals. Then France scored twice in 97 seconds, and their line jumped sharply upward. Steep steps mean big chances.",
|
||
passNetworkTitle: "Passing shape (starting XI)",
|
||
passNetworkCaption: "Each circle is a player, placed at his average passing position. Lines show the completed passes between two players, counted up to the first substitution. Bigger circles mean a player saw more of the ball.",
|
||
},
|
||
team: {
|
||
unknown: "Unknown team.",
|
||
allTeams: "All teams",
|
||
follow: "Follow this team",
|
||
unfollow: "Unfollow this team",
|
||
notifyOn: "Get goal alerts for this team",
|
||
notifyOff: "Stop goal alerts",
|
||
notifyDenied: "Notifications are blocked in your browser settings.",
|
||
eloBadge: "Elo {rating}",
|
||
titleOddsBadge: "Title {pct}",
|
||
advanceOddsBadge: "Advance {pct}",
|
||
groupStanding: "Group standing",
|
||
standingLine: "#{rank} · {points} pts · {won}-{drawn}-{lost} · GD {gd}",
|
||
fixtures: "Fixtures",
|
||
eloHistory: "Elo rating over time",
|
||
allTimeTopScorers: "All-time top scorers",
|
||
groupShort: "Grp {group}",
|
||
awayIndicator: "@",
|
||
resultShort: {
|
||
win: "W",
|
||
draw: "D",
|
||
loss: "L",
|
||
},
|
||
stageShort: {
|
||
r32: "R32",
|
||
r16: "R16",
|
||
qf: "QF",
|
||
sf: "SF",
|
||
third: "3rd",
|
||
final: "F",
|
||
},
|
||
},
|
||
};
|
||
|
||
export type Dict = typeof en;
|