world-2026-hub/assets/js/i18n.js
Lucas Kalil c246117545 feat(bracket): redesign with wallchart, radial orbit, and rounds pager
Center-out wallchart layout (computeWallchartLayout), radial orbit view with
circular flag tokens, and mobile rounds pager with button navigation (Steps 1–3).
Stadium-night art direction, SVG connectors, fit-to-chart zoom, escalating card
heat toward the Final, dual gold-real/blue-sim champion celebration paths.
Adds bracket view toggle with persistent wc2026_prefs.bracketView storage.
2026-07-03 22:09:49 -03:00

438 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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.ft': 'FT',
'bracket.viewLabel': 'Bracket view',
'bracket.viewRounds': 'Rounds',
'bracket.viewWallchart': 'Wallchart',
'bracket.viewRadial': 'Radial',
'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.ft': 'FIM',
'bracket.viewLabel': 'Visualização do mata-mata',
'bracket.viewRounds': 'Fases',
'bracket.viewWallchart': 'Chaveamento',
'bracket.viewRadial': 'Radial',
'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;
}