// i18n.js — EN/PT-BR dictionaries + t(key). Every UI string in the app goes // through t(); static HTML uses data-i18n / data-i18n-aria attributes. // Language persists in wc2026_prefs.lang; changing it dispatches "langchange" // so modules can re-render dynamic content. import { getPrefs, setPref } from './storage.js'; // App version for footer display — bump this after any notable changes const APP_VERSION = 'v1.0.3'; const dicts = { en: { 'a11y.skip': 'Skip to content', 'a11y.mainNav': 'Main navigation', 'a11y.langSwitch': 'Switch language', 'nav.home': 'Home', 'nav.matches': 'Matches', 'nav.groups': 'Groups', 'nav.bracket': 'Knockout', 'nav.stadiums': 'Stadiums', 'nav.stats': 'Stats', 'hero.live': 'Live', 'hero.nextMatch': 'Next match', 'hero.nextMatches': 'Next matches', 'hero.inProgress': 'In progress', 'hero.countdownLabel': 'Time until kickoff', 'hero.vs': 'vs', 'countdown.days': 'days', 'countdown.hours': 'hours', 'countdown.minutes': 'min', 'countdown.seconds': 'sec', 'dash.title': 'Tournament overview', 'dash.total': 'Total matches', 'dash.completed': 'Completed', 'dash.upcoming': 'Upcoming', 'dash.teams': 'Teams', 'app.loading': 'Loading data…', 'app.error': 'Could not load tournament data.', 'app.errorHint': 'If you opened index.html directly from disk, serve the folder instead: python -m http.server', 'app.comingSoon': 'This section arrives in a later build step.', 'app.tbd': 'TBD', 'phase.group': 'Group', 'phase.r32': 'Round of 32', 'phase.r16': 'Round of 16', 'phase.qf': 'Quarterfinals', 'phase.sf': 'Semifinals', 'phase.third': 'Third Place', 'phase.final': 'Final', 'schedule.searchPlaceholder': 'Search team, city or stadium…', 'schedule.dateFilter': 'Filter by date', 'schedule.allGroups': 'All groups', 'schedule.allPhases': 'All phases', 'schedule.groupStage': 'Group stage', 'schedule.allTeams': 'All teams', 'schedule.allStadiums': 'All stadiums', 'schedule.sortAsc': 'Date ↑', 'schedule.sortDesc': 'Date ↓', 'schedule.match': 'match', 'schedule.matches': 'matches', 'schedule.noResults': 'No matches found — adjust the filters.', 'schedule.clear': 'Clear filters', 'status.finished': 'Full-time', 'status.pens': 'pens', 'standings.team': 'Team', 'standings.played': 'P', 'standings.won': 'W', 'standings.drawn': 'D', 'standings.lost': 'L', 'standings.gf': 'GF', 'standings.ga': 'GA', 'standings.gd': 'GD', 'standings.pts': 'Pts', 'standings.legendTop2': 'Advance to the Round of 32', 'standings.legendThird': 'In contention for best third place', 'standings.inProgress': 'In progress', 'standings.thirdTitle': 'Best third-placed teams', 'standings.thirdNote': '8 of 12 advance to the Round of 32 — ranked by points, then goal difference, then goals for.', 'standings.group': 'Group', 'standings.qualified': 'Qualified', 'standings.eliminated': 'Eliminated', 'stadiums.capacity': 'Capacity', 'stadiums.viewMatches': 'View matches', 'status.scheduled': 'Scheduled', 'status.pending': 'Awaiting result', 'modal.close': 'Close', 'modal.date': 'Date & time', 'modal.stadium': 'Stadium', 'modal.city': 'City', 'modal.stats': 'Match stats', 'modal.possession': 'Possession', 'modal.shots': 'Shots', 'modal.cards': 'Cards', 'modal.statsSoon': 'Detailed stats will appear here once available.', 'bracket.groupWinner': 'Group {g} Winner', 'bracket.groupRunnerUp': 'Group {g} Runner-up', 'bracket.bestThird': 'Best 3rd #{n}', 'bracket.champion': 'Champion', 'bracket.zoomIn': 'Zoom in', 'bracket.zoomOut': 'Zoom out', 'bracket.zoomReset': 'Reset zoom', 'sim.mode': 'Simulation', 'sim.reset': 'Reset picks', 'sim.hint': 'Simulation on — click a highlighted match to pick its winner. Real results are never changed.', 'sim.title': 'Simulate', 'sim.pickWinner': 'Pick the winner. Equal or empty score means penalties.', 'sim.save': 'Save pick', 'sim.clear': 'Remove pick', 'sim.chip': 'SIM', 'time.local': 'Local time', 'time.stadium': 'Stadium time', 'time.toggleAria': 'Toggle between local and stadium time', 'schedule.myMatches': 'My matches', 'schedule.occAria': 'Occurrence filter', 'schedule.occAll': 'All matches', 'schedule.occPlayed': 'Played', 'schedule.occUpcoming': 'Upcoming', 'fav.toggle': 'Favorite', 'challenge.title': 'Bracket challenge', 'challenge.correct': '{x} of {y} picks correct', 'share.button': 'Share prediction', 'share.copied': 'Link copied!', 'share.confirm': 'Apply the shared prediction? Your current picks will be replaced.', 'modal.addCalendar': 'Add to calendar', 'stats.heroTitle': 'Tournament in progress', 'stats.heroProgress': '{x} of {y} matches played', 'stats.tileGoals': 'Goals', 'stats.tileAvg': 'Goals / match', 'stats.tileBiggestMargin': 'Biggest margin', 'stats.tileCleanSheets': 'Clean sheets', 'stats.sectionsNav': 'Statistics sections', 'stats.navOverview': 'Overview', 'stats.navTeams': 'Teams', 'stats.navPlayers': 'Players', 'stats.navRecords': 'Records', 'stats.navComparator': 'Comparator', 'stats.navArchive': 'Archive', 'stats.overviewTitle': 'Overview', 'stats.played': 'Matches played', 'stats.decisive': 'Decisive', 'stats.draws': 'Draws', 'stats.goalsByPhase': 'Goals by stage', 'stats.goalsByRound': 'Goals by round', 'stats.matchday': 'Matchday', 'stats.stageGroup': 'Group stage', 'stats.verdictTitle': 'Final verdict', 'stats.runnerUp': 'Runner-up', 'stats.thirdPlace': 'Third place', 'stats.fourthPlace': 'Fourth place', 'stats.teamStatsTitle': 'Team statistics', 'stats.colGpg': 'G/M', 'stats.colCS': 'CS', 'tip.played': 'Matches played', 'tip.won': 'Wins', 'tip.drawn': 'Draws', 'tip.lost': 'Losses', 'tip.gf': 'Goals for (scored)', 'tip.ga': 'Goals against (conceded)', 'tip.gd': 'Goal difference (for − against)', 'tip.pts': 'Points', 'tip.gpg': 'Goals per match (average)', 'tip.cs': 'Clean sheets (no goals conceded)', 'tip.rank': 'Final ranking — deepest stage reached, then points', 'stats.rankCol': 'Rank', 'stats.bestAttack': 'Best attack', 'stats.bestDefense': 'Best defense', 'stats.mostCleanSheets': 'Most clean sheets', 'stats.mostWins': 'Most wins', 'stats.mostConceded': 'Most goals conceded', 'stats.bestGoalDiff': 'Best goal difference', 'stats.leaderPrev': 'Previous team', 'stats.leaderNext': 'Next team', 'stats.biggestWin': 'Biggest win', 'stats.winStreak': 'Longest win streak', 'stats.championPath': "Champion's path", 'stats.recordsTitle': 'Records', 'stats.highScoreMatch': 'Highest-scoring match', 'stats.formatDebutsTitle': 'Format debuts', 'stats.debutTeams': 'First 48-team World Cup', 'stats.debutMatches': 'Matches (up from 64)', 'stats.debutGroups': 'Groups', 'stats.debutR32': 'A new knockout round', 'stats.debutThird': 'Best third-placed teams advance', 'stats.debutChampion': 'First champion of the 48-team era', 'stats.comparatorTitle': 'Team comparator', 'stats.cmpTeamA': 'Team A', 'stats.cmpTeamB': 'Team B', 'stats.prevPage': 'Previous page', 'stats.nextPage': 'Next page', 'stats.seeAllMatches': 'See all matches', 'footer.note': `Fan-made static hub ${APP_VERSION}`, }, pt: { 'a11y.skip': 'Pular para o conteúdo', 'a11y.mainNav': 'Navegação principal', 'a11y.langSwitch': 'Trocar idioma', 'nav.home': 'Início', 'nav.matches': 'Partidas', 'nav.groups': 'Grupos', 'nav.bracket': 'Mata-mata', 'nav.stadiums': 'Estádios', 'nav.stats': 'Estatísticas', 'hero.live': 'Ao vivo', 'hero.nextMatch': 'Próxima partida', 'hero.nextMatches': 'Próximas partidas', 'hero.inProgress': 'Bola rolando!', 'hero.countdownLabel': 'Tempo até o início da partida', 'hero.vs': 'vs', 'countdown.days': 'dias', 'countdown.hours': 'horas', 'countdown.minutes': 'min', 'countdown.seconds': 'seg', 'dash.title': 'Visão geral do torneio', 'dash.total': 'Total de partidas', 'dash.completed': 'Encerradas', 'dash.upcoming': 'Próximas', 'dash.teams': 'Seleções', 'app.loading': 'Carregando dados…', 'app.error': 'Não foi possível carregar os dados do torneio.', 'app.errorHint': 'Se você abriu o index.html direto do disco, sirva a pasta: python -m http.server', 'app.comingSoon': 'Esta seção chega em uma próxima etapa.', 'app.tbd': 'A definir', 'phase.group': 'Grupo', 'phase.r32': '16 avos de final', 'phase.r16': 'Oitavas de final', 'phase.qf': 'Quartas de final', 'phase.sf': 'Semifinais', 'phase.third': 'Disputa de 3º lugar', 'phase.final': 'Final', 'schedule.searchPlaceholder': 'Buscar seleção, cidade ou estádio…', 'schedule.dateFilter': 'Filtrar por data', 'schedule.allGroups': 'Todos os grupos', 'schedule.allPhases': 'Todas as fases', 'schedule.groupStage': 'Fase de grupos', 'schedule.allTeams': 'Todas as seleções', 'schedule.allStadiums': 'Todos os estádios', 'schedule.sortAsc': 'Data ↑', 'schedule.sortDesc': 'Data ↓', 'schedule.match': 'partida', 'schedule.matches': 'partidas', 'schedule.noResults': 'Nenhuma partida encontrada — ajuste os filtros.', 'schedule.clear': 'Limpar filtros', 'status.finished': 'Encerrado', 'status.pens': 'pên.', 'standings.team': 'Seleção', 'standings.played': 'J', 'standings.won': 'V', 'standings.drawn': 'E', 'standings.lost': 'D', 'standings.gf': 'GP', 'standings.ga': 'GC', 'standings.gd': 'SG', 'standings.pts': 'Pts', 'standings.legendTop2': 'Avançam aos 16 avos de final', 'standings.legendThird': 'Na briga por melhor 3º lugar', 'standings.inProgress': 'Em andamento', 'standings.thirdTitle': 'Melhores terceiros colocados', 'standings.thirdNote': '8 de 12 avançam aos 16 avos de final — ordenados por pontos, depois saldo de gols, depois gols pró.', 'standings.group': 'Grupo', 'standings.qualified': 'Classificado', 'standings.eliminated': 'Eliminado', 'stadiums.capacity': 'Capacidade', 'stadiums.viewMatches': 'Ver partidas', 'status.scheduled': 'Agendada', 'status.pending': 'Pendente de resultado', 'modal.close': 'Fechar', 'modal.date': 'Data e hora', 'modal.stadium': 'Estádio', 'modal.city': 'Cidade', 'modal.stats': 'Estatísticas', 'modal.possession': 'Posse de bola', 'modal.shots': 'Finalizações', 'modal.cards': 'Cartões', 'modal.statsSoon': 'Estatísticas detalhadas aparecerão aqui quando disponíveis.', 'bracket.groupWinner': '1º do Grupo {g}', 'bracket.groupRunnerUp': '2º do Grupo {g}', 'bracket.bestThird': 'Melhor 3º #{n}', 'bracket.champion': 'Campeão', 'bracket.zoomIn': 'Aproximar', 'bracket.zoomOut': 'Afastar', 'bracket.zoomReset': 'Restaurar zoom', 'sim.mode': 'Simulação', 'sim.reset': 'Limpar palpites', 'sim.hint': 'Simulação ativa — clique numa partida destacada para escolher o vencedor. Resultados reais nunca mudam.', 'sim.title': 'Simular', 'sim.pickWinner': 'Escolha o vencedor. Placar igual ou vazio indica pênaltis.', 'sim.save': 'Salvar palpite', 'sim.clear': 'Remover palpite', 'sim.chip': 'SIM', 'time.local': 'Hora local', 'time.stadium': 'Hora do estádio', 'time.toggleAria': 'Alternar entre hora local e do estádio', 'schedule.myMatches': 'Minhas partidas', 'schedule.occAria': 'Filtro de ocorrência', 'schedule.occAll': 'Todos os jogos', 'schedule.occPlayed': 'Já ocorreram', 'schedule.occUpcoming': 'A ocorrer', 'fav.toggle': 'Favoritar', 'challenge.title': 'Bolão do mata-mata', 'challenge.correct': '{x} de {y} palpites certos', 'share.button': 'Compartilhar palpites', 'share.copied': 'Link copiado!', 'share.confirm': 'Aplicar os palpites compartilhados? Seus palpites atuais serão substituídos.', 'modal.addCalendar': 'Adicionar à agenda', 'stats.heroTitle': 'Copa em andamento', 'stats.heroProgress': '{x} de {y} jogos disputados', 'stats.tileGoals': 'Gols', 'stats.tileAvg': 'Gols por jogo', 'stats.tileBiggestMargin': 'Maior margem', 'stats.tileCleanSheets': 'Sem sofrer gols', 'stats.sectionsNav': 'Seções de estatísticas', 'stats.navOverview': 'Visão geral', 'stats.navTeams': 'Seleções', 'stats.navPlayers': 'Jogadores', 'stats.navRecords': 'Recordes', 'stats.navComparator': 'Comparador', 'stats.navArchive': 'Arquivo', 'stats.overviewTitle': 'Visão geral', 'stats.played': 'Jogos disputados', 'stats.decisive': 'Decididas', 'stats.draws': 'Empates', 'stats.goalsByPhase': 'Gols por fase', 'stats.goalsByRound': 'Gols por rodada', 'stats.matchday': 'Rodada', 'stats.stageGroup': 'Fase de grupos', 'stats.verdictTitle': 'Veredito final', 'stats.runnerUp': 'Vice-campeão', 'stats.thirdPlace': 'Terceiro lugar', 'stats.fourthPlace': 'Quarto lugar', 'stats.teamStatsTitle': 'Estatísticas por time', 'stats.colGpg': 'G/J', 'stats.colCS': 'CS', 'tip.played': 'Jogos disputados', 'tip.won': 'Vitórias', 'tip.drawn': 'Empates', 'tip.lost': 'Derrotas', 'tip.gf': 'Gols pró (marcados)', 'tip.ga': 'Gols contra (sofridos)', 'tip.gd': 'Saldo de gols (pró − contra)', 'tip.pts': 'Pontos', 'tip.gpg': 'Gols por jogo (média)', 'tip.cs': 'Clean sheets (sem sofrer gols)', 'tip.rank': 'Classificação final — fase alcançada, depois pontos', 'stats.rankCol': 'Posição', 'stats.bestAttack': 'Melhor ataque', 'stats.bestDefense': 'Melhor defesa', 'stats.mostCleanSheets': 'Mais clean sheets', 'stats.mostWins': 'Mais vitórias', 'stats.mostConceded': 'Mais gols sofridos', 'stats.bestGoalDiff': 'Melhor saldo de gols', 'stats.leaderPrev': 'Time anterior', 'stats.leaderNext': 'Próximo time', 'stats.biggestWin': 'Maior goleada', 'stats.winStreak': 'Maior sequência de vitórias', 'stats.championPath': 'Caminho do campeão', 'stats.recordsTitle': 'Recordes', 'stats.highScoreMatch': 'Jogo com mais gols', 'stats.formatDebutsTitle': 'Estreias do formato', 'stats.debutTeams': 'Primeira Copa com 48 seleções', 'stats.debutMatches': 'Partidas (eram 64)', 'stats.debutGroups': 'Grupos', 'stats.debutR32': 'Uma nova fase eliminatória', 'stats.debutThird': 'Melhores terceiros avançam', 'stats.debutChampion': 'Primeiro campeão da era dos 48', 'stats.comparatorTitle': 'Comparador de times', 'stats.cmpTeamA': 'Time A', 'stats.cmpTeamB': 'Time B', 'stats.prevPage': 'Página anterior', 'stats.nextPage': 'Próxima página', 'stats.seeAllMatches': 'Ver todas as partidas', 'footer.note': `Hub estático feito por fãs ${APP_VERSION}`, }, }; let lang = 'en'; export function initI18n() { const saved = getPrefs().lang; lang = saved ?? (navigator.language?.toLowerCase().startsWith('pt') ? 'pt' : 'en'); document.documentElement.lang = getLocale(); applyI18n(); } export function t(key) { return dicts[lang][key] ?? dicts.en[key] ?? key; } export function getLang() { return lang; } export function getLocale() { return lang === 'pt' ? 'pt-BR' : 'en-US'; } export function setLang(next) { if (next === lang || !dicts[next]) return; lang = next; setPref('lang', next); document.documentElement.lang = getLocale(); applyI18n(); document.dispatchEvent(new CustomEvent('langchange')); } export function applyI18n(root = document) { for (const el of root.querySelectorAll('[data-i18n]')) { el.textContent = t(el.dataset.i18n); } for (const el of root.querySelectorAll('[data-i18n-aria]')) { el.setAttribute('aria-label', t(el.dataset.i18nAria)); } } // Phase labels come from matches.json in English ("Group A", "Round of 32"…); // translate the known ones, pass anything else through untouched. const PHASE_KEYS = { 'Round of 32': 'phase.r32', 'Round of 16': 'phase.r16', Quarterfinals: 'phase.qf', Semifinals: 'phase.sf', 'Third Place': 'phase.third', Final: 'phase.final', }; export function translatePhase(phase) { if (phase.startsWith('Group ')) return `${t('phase.group')} ${phase.slice(6)}`; const key = PHASE_KEYS[phase]; return key ? t(key) : phase; }