const translations = { en: { "lang.label": "Language", "lang.en": "English", "lang.de": "Deutsch", "auth.loginTab": "Log in", "auth.registerTab": "Register", "auth.username": "Username", "auth.password": "Password", "auth.displayName": "Display name (shows to group)", "auth.adminKey": "Admin key (optional)", "auth.loginSubmit": "Log in", "auth.registerSubmit": "Create account", "auth.loginHeading": "Log in", "auth.registerHeading": "Create account", "auth.switchToRegister": "Need an account? Register", "auth.switchToLogin": "Have an account? Log in", "auth.logout": "Logout", "auth.welcome": "Welcome, {name}!", "auth.defaultName": "Player", "auth.loading": "Loading…", "auth.needCredentials": "Username and password required", "auth.invalidCredentials": "Invalid username or password", "phase.suggest": "Suggest", "phase.reveal": "Reveal", "phase.vote": "Vote", "phase.results": "Results", "phase.loading": "Loading…", "counts.format": "Players: {players} • Suggestions: {suggestions} • Votes: {votes}", "nav.prev": "Back", "nav.next": "Next", "nav.waitingForResults": "Waiting…", "nav.freezeTitle": "Ready to reveal?", "nav.freezeHint": "Moving forward will freeze your suggestions. Game names become locked; only extra details stay editable.", "nav.freezeModalTitle": "Freeze suggestions?", "nav.freezeModalBody": "Once you leave Suggest, your games are locked: game names cannot be changed or deleted. Only optional details (description, links, players, artwork) remain editable. Continue?", "nav.voteHint": "Cast votes for every game to unlock results.", "nav.voteFinalized": "✅ You finalized your votes. Sit back and relax while the other players finalize their votes.", "suggest.title": "Suggest games (up to 5)", "suggest.new": "Add new suggestion", "suggest.addButton": "Suggest a game", "suggest.jokerAddButton": "🃏 Joker: add another game", "suggest.hint": "Only you can see your suggestions until voting starts.", "form.gameName": "Game name *", "form.genre": "Genre", "form.description": "Description", "form.players": "Players", "form.min": "Min", "form.max": "Max", "form.screenshot": "Screenshot URL", "form.youtube": "YouTube URL", "form.gameUrl": "Game website URL", "form.submit": "Submit", "form.placeholder.description": "Short description", "form.placeholder.gameName": "Game name *", "form.placeholder.genre": "Genre", "form.placeholder.screenshot": "Screenshot URL", "form.placeholder.youtube": "YouTube URL", "form.placeholder.gameUrl": "Game website URL", "section.mySuggestions": "Your suggestions", "section.allSuggestions": "All suggestions", "section.allSuggestions.count": "All {count} suggestions", "section.vote": "Vote 0-10", "section.vote.count": "Vote for all {count} games", "section.results": "Results", "card.edit": "Edit", "card.delete": "Delete", "card.players": "Players: {min}–{max}", "card.site": "Site ↗", "card.youtube": "YouTube ↗", "card.openScreenshot": "Open screenshot", "card.linked": "Votes linked", "card.linkedWith": "Linked with: {names}", "vote.saved": "Saved vote", "vote.missing": "Missing", "vote.missingWarn": "You haven’t voted yet.", "vote.missingFinalWarn": "You didn't vote for this game.", "vote.missingFooter": "At least one game is missing a score. Check before finalizing.", "vote.finalize": "Finalize votes", "vote.unfinalize": "Edit votes", "vote.finalHint": "Finalize when you’re done. You can unfinalize to change scores.", "vote.waitAdmin": "Waiting for admin to unlock results.", "results.rank": "Rank", "results.game": "Game", "results.author": "Author", "results.average": "Ø", "results.votesList": "All votes", "results.myVote": "Your vote", "results.links": "Links", "results.link.site": "Site ↗", "results.link.youtube": "YouTube ↗", "admin.title": "Admin", "admin.tools": "Admin tools", "admin.resultsOpenToggle": "Allow results phase", "admin.resultsLocked": "Results locked by admin", "admin.resultsUpdated": "Results availability updated", "admin.reset": "Reset (keep players)", "admin.factoryReset": "Factory reset", "admin.resetDone": "Reset complete", "admin.factoryResetDone": "Factory reset complete", "admin.readyForResults": "Ready for results", "admin.waitingForPlayers": "Waiting for players: {names}", "admin.playerName": "Name", "admin.playerUsername": "Username", "admin.playerStatus": "Status", "admin.playerGames": "Games", "admin.playerJoker": "Joker", "admin.playerDelete": "Delete", "admin.grantJokerChip": "Grant", "admin.statusSuggesting": "Suggesting", "admin.statusVoting": "Voting", "admin.statusFinished": "Finished", "admin.deleteTitle": "Delete account?", "admin.deleteBody": "Delete player \"{name}\" and all their games and votes? This cannot be undone.", "admin.deleteConfirm": "Delete", "admin.deleteDone": "Player deleted", "admin.jokerGranted": "Joker granted", "admin.linkTitle": "Link games", "admin.linkSource": "Game to link", "admin.linkTarget": "Link to (parent)", "admin.linkAction": "Link & clear votes", "admin.linkSourcePlaceholder": "Select source", "admin.linkTargetPlaceholder": "Select target", "admin.linkValidation": "Choose two different games to link.", "admin.linkDone": "Games linked. Votes cleared.", "admin.unlinkTitle": "Remove links?", "admin.unlinkBody": "Remove all links involving \"{name}\"? This clears votes and unfinalizes voters in this group: {peers}.", "admin.unlinkConfirm": "Remove links", "admin.unlinkDone": "Links removed. Votes cleared.", "admin.unlinkUnknownPeers": "linked games", "toast.unexpected": "Unexpected error", "toast.registered": "Registered", "toast.loggedIn": "Logged in", "toast.suggestionAdded": "Suggestion added", "toast.suggestionDeleted": "Suggestion deleted", "toast.savedChanges": "Saved changes", "toast.nameRequired": "Name required", "toast.displayNameRequired": "Display name is required", "toast.invalidImageUrl": "Screenshot URL must be http(s) and end with an image file.", "modal.editTitle": "Edit game", "modal.addTitle": "Suggest a game", "modal.confirmDeleteTitle": "Are you sure?", "modal.confirmDelete": "Confirm delete", "modal.save": "Save changes", "modal.cancel": "Cancel", "modal.close": "Close", "lightbox.close": "Close", "help.label": "Help", "help.title": "FAQ & tips", "help.intro": "Expand a section for common player questions and edge cases.", "help.cat.gettingStarted": "Getting started", "help.cat.suggest": "Suggest phase", "help.cat.vote": "Vote phase", "help.cat.results": "Results phase", "help.q.join": "How do I join?", "help.a.join": "Register with a username, password, and display name. Then log in with the same credentials.", "help.q.adminKey": "What is the admin key?", "help.a.adminKey": "If your host shares the admin key, enter it during registration to unlock admin controls.", "help.q.displayName": "Why do I need a display name?", "help.a.displayName": "Suggestions and votes show who submitted them. You need a display name before you can add games or scores.", "help.q.limit": "How many games can I add?", "help.a.limit": "Up to 5 suggestions per player in Suggest. Each joker granted in Vote lets you add one more; every joker is consumed when you use it.", "help.q.editNames": "Can I edit or delete suggestions later?", "help.a.editNames": "Game names lock after leaving Suggest. Optional fields stay editable; admins can edit or delete any time.", "help.q.mediaRules": "Any rules for links and images?", "help.a.mediaRules": "Use http(s) links. Screenshots must end with an image file (png, jpg, gif, webp, avif). Invalid links are rejected.", "help.q.cantAdd": "Why was my suggestion rejected?", "help.a.cantAdd": "You can only add games in Suggest or with a joker in Vote. Provide a display name, stay within the 5 + joker limit, use http(s) links to reachable media, and keep player counts between 1 and 32 with min \u2264 max.", "help.q.howVote": "How do votes work?", "help.a.howVote": "Move the 0–10 slider for each game. Scores save instantly; you can adjust until you finalize.", "help.q.finalize": "What does finalize do?", "help.a.finalize": "Finalize locks your votes while you're in Vote. You must score every game first. Toggle it off in the same phase to make changes.", "help.q.voteBlocked": "Why can't I vote?", "help.a.voteBlocked": "Voting only works in the Vote phase when you're logged in and have a display name. If you skipped the display name, re-register; admins cannot add one for you.", "help.q.jokerAfterFreeze": "I finalized but thought of a great game. Can I still add it?", "help.a.jokerAfterFreeze": "Ask an admin for a joker during Vote. It gives you one extra slot, consumes the joker, and unfinalizes everyone so the new game can be scored.", "help.q.linkedVotes": "What are linked games?", "help.a.linkedVotes": "Admins can link duplicates during Vote. Linked games share scores. Linking or unlinking clears votes for that group and reopens affected players to rescore.", "help.q.newGameAfterFinal": "A new game appeared after I finalized. What now?", "help.a.newGameAfterFinal": "New games (from jokers) or link changes remove finalization. Check your list and score every game again before finalizing.", "help.q.scoreRange": "What scores are allowed?", "help.a.scoreRange": "Scores must be whole numbers from 0 to 10. Anything outside that range is rejected.", "help.q.resultsLocked": "Why can't I see results?", "help.a.resultsLocked": "Results stay hidden until an admin opens them. You move to Results automatically when unlocked.", "help.q.resultsContent": "What do results show?", "help.a.resultsContent": "A leaderboard with averages, vote counts, your own score, and any links or media for each game.", "help.q.editInResults": "Can I edit games or votes in Results?", "help.a.editInResults": "No. Suggestions and votes are locked in Results. Changes require moving back to Vote (for example if an admin closes results or resets).", }, de: { "lang.label": "Sprache", "lang.en": "Englisch", "lang.de": "Deutsch", "auth.loginTab": "Anmelden", "auth.registerTab": "Registrieren", "auth.username": "Benutzername", "auth.password": "Passwort", "auth.displayName": "Anzeigename (für die Gruppe sichtbar)", "auth.adminKey": "Admin-Schlüssel (optional)", "auth.loginSubmit": "Anmelden", "auth.registerSubmit": "Konto erstellen", "auth.loginHeading": "Anmelden", "auth.registerHeading": "Konto erstellen", "auth.switchToRegister": "Noch kein Konto? Registrieren", "auth.switchToLogin": "Schon ein Konto? Anmelden", "auth.logout": "Abmelden", "auth.welcome": "Willkommen, {name}!", "auth.defaultName": "Spieler", "auth.loading": "Lädt…", "auth.needCredentials": "Benutzername und Passwort erforderlich", "auth.invalidCredentials": "Ungültiger Benutzername oder Passwort", "phase.suggest": "Vorschlagen", "phase.reveal": "Enthüllen", "phase.vote": "Bewerten", "phase.results": "Ergebnisse", "phase.loading": "Lädt…", "counts.format": "Spieler: {players} • Vorschläge: {suggestions} • Stimmen: {votes}", "nav.prev": "Zurück", "nav.next": "Weiter", "nav.waitingForResults": "Warten…", "nav.freezeTitle": "Bereit zum Aufdecken?", "nav.freezeHint": "Beim Weitergehen werden deine Vorschläge eingefroren. Spielnamen werden gesperrt; nur Zusatzinfos bleiben bearbeitbar.", "nav.freezeModalTitle": "Vorschläge einfrieren?", "nav.freezeModalBody": "Sobald du die Vorschlagsphase verlässt, sind deine Spiele gesperrt: Die Namen von deinen Spielen können nicht mehr geändert oder gelöscht werden. Nur optionale Angaben (Beschreibung, Links, Spielerzahlen, Bilder) bleiben bearbeitbar. Fortfahren?", "nav.voteHint": "Bewerte alle Spiele, um die Ergebnisse freizuschalten.", "nav.voteFinalized": "✅ Du hast deine Abstimmung abgeschlossen. Lehn dich zurück, bis die anderen fertig sind.", "suggest.title": "Schlage Spiele vor (bis zu 5)", "suggest.new": "Neuen Vorschlag hinzufügen", "suggest.addButton": "Spiel vorschlagen", "suggest.jokerAddButton": "🃏 Joker: Weiteres Spiel hinzufügen", "suggest.hint": "Nur du siehst deine Vorschläge bis zum Start der Abstimmung.", "form.gameName": "Spielname *", "form.genre": "Genre", "form.description": "Beschreibung", "form.players": "Spieler", "form.min": "Min", "form.max": "Max", "form.screenshot": "Screenshot-URL", "form.youtube": "YouTube-URL", "form.gameUrl": "Spiel-Webseite", "form.submit": "Absenden", "form.placeholder.description": "Kurze Beschreibung", "form.placeholder.gameName": "Spielname *", "form.placeholder.genre": "Genre", "form.placeholder.screenshot": "Screenshot-URL", "form.placeholder.youtube": "YouTube-URL", "form.placeholder.gameUrl": "Spiel-Webseite", "section.mySuggestions": "Deine Vorschläge", "section.allSuggestions": "Alle Vorschläge", "section.allSuggestions.count": "Alle {count} Vorschläge", "section.vote": "Bewerten 0-10", "section.vote.count": "Bewerte alle {count} Spiele", "section.results": "Ergebnisse", "card.edit": "Bearbeiten", "card.delete": "Löschen", "card.players": "Spieler: {min}–{max}", "card.site": "Webseite ↗", "card.youtube": "YouTube ↗", "card.openScreenshot": "Screenshot öffnen", "card.linked": "Verknüpfte Stimmen", "card.linkedWith": "Verknüpft mit: {names}", "vote.saved": "Stimme gespeichert", "vote.missing": "Fehlt", "vote.missingWarn": "Du hast hier noch nicht abgestimmt.", "vote.missingFinalWarn": "Du hast für dieses Spiel nicht abgestimmt.", "vote.missingFooter": "Für mindestens einen Spiel fehlt noch eine Wertung. Prüfe vor dem Abschließen.", "vote.finalize": "Abstimmung abschließen", "vote.unfinalize": "Abstimmung bearbeiten", "vote.finalHint": "Schließe ab, wenn du fertig bist. Zum Ändern wieder öffnen.", "vote.waitAdmin": "Warten, bis der Admin die Ergebnisse freigibt.", "results.rank": "Rang", "results.game": "Spiel", "results.author": "Autor", "results.average": "Ø", "results.votesList": "Alle Stimmen", "results.myVote": "Deine Stimme", "results.links": "Links", "results.link.site": "Webseite ↗", "results.link.youtube": "YouTube ↗", "admin.title": "Admin", "admin.tools": "Admin-Werkzeuge", "admin.resultsOpenToggle": "Ergebnisse freigeben", "admin.resultsLocked": "Ergebnisse vom Admin gesperrt", "admin.resultsUpdated": "Ergebnisfreigabe aktualisiert", "admin.reset": "Zurücksetzen (Spieler behalten)", "admin.factoryReset": "Werkseinstellung", "admin.resetDone": "Zurücksetzen abgeschlossen", "admin.factoryResetDone": "Werkseinstellung abgeschlossen", "admin.readyForResults": "Bereit für Ergebnisse", "admin.waitingForPlayers": "Warten auf: {names}", "admin.playerName": "Name", "admin.playerUsername": "Benutzername", "admin.playerStatus": "Status", "admin.playerGames": "Spiele", "admin.playerJoker": "Joker", "admin.playerDelete": "Löschen", "admin.grantJokerChip": "Joker", "admin.statusSuggesting": "Vorschlagen", "admin.statusVoting": "Bewerten", "admin.statusFinished": "Fertig", "admin.deleteTitle": "Konto löschen?", "admin.deleteBody": "Spieler \"{name}\" samt Spielen und Stimmen löschen? Dies kann nicht rückgängig gemacht werden.", "admin.deleteConfirm": "Löschen", "admin.deleteDone": "Spieler gelöscht", "admin.jokerGranted": "Joker vergeben", "admin.linkTitle": "Spiele verknüpfen", "admin.linkSource": "Spiel verknüpfen", "admin.linkTarget": "Verknüpfen mit", "admin.linkAction": "Verknüpfen & Stimmen löschen", "admin.linkSourcePlaceholder": "Quelle wählen", "admin.linkTargetPlaceholder": "Ziel wählen", "admin.linkValidation": "Wähle zwei verschiedene Spiele aus.", "admin.linkDone": "Spiele verknüpft. Stimmen gelöscht.", "admin.unlinkTitle": "Links entfernen?", "admin.unlinkBody": "Alle Links zu \"{name}\" entfernen? Dadurch werden Stimmen gelöscht und Finalisierungen aufgehoben für: {peers}.", "admin.unlinkConfirm": "Links entfernen", "admin.unlinkDone": "Links entfernt. Stimmen gelöscht.", "admin.unlinkUnknownPeers": "verknüpfte Spiele", "toast.unexpected": "Unerwarteter Fehler", "toast.registered": "Registriert", "toast.loggedIn": "Angemeldet", "toast.suggestionAdded": "Vorschlag hinzugefügt", "toast.suggestionDeleted": "Vorschlag gelöscht", "toast.savedChanges": "Änderungen gespeichert", "toast.nameRequired": "Name erforderlich", "toast.displayNameRequired": "Anzeigename ist erforderlich", "toast.invalidImageUrl": "Screenshot-URL muss mit http(s) beginnen und auf eine Bilddatei enden.", "modal.editTitle": "Spiel bearbeiten", "modal.addTitle": "Spiel vorschlagen", "modal.confirmDeleteTitle": "Bist du sicher?", "modal.confirmDelete": "Löschen bestätigen", "modal.save": "Änderungen speichern", "modal.cancel": "Abbrechen", "modal.close": "Schließen", "lightbox.close": "Schließen", "help.label": "Hilfe", "help.title": "FAQ & Tipps", "help.intro": "Klappe einen Abschnitt auf, um häufige Spielerfragen und Sonderfälle zu sehen.", "help.cat.gettingStarted": "Erste Schritte", "help.cat.suggest": "Phase Vorschlagen", "help.cat.vote": "Phase Bewerten", "help.cat.results": "Phase Ergebnisse", "help.q.join": "Wie kann ich mitmachen?", "help.a.join": "Registriere dich mit Benutzername, Passwort und Anzeigenamen. Danach mit denselben Daten anmelden.", "help.q.adminKey": "Was ist der Admin-Schlüssel?", "help.a.adminKey": "Wenn der Host dir den Admin-Schlüssel gibt, trage ihn bei der Registrierung ein, um Admin-Rechte zu erhalten.", "help.q.displayName": "Warum brauche ich einen Anzeigenamen?", "help.a.displayName": "Vorschläge und Stimmen zeigen, von wem sie stammen. Ohne Anzeigenamen kannst du keine Spiele oder Wertungen hinzufügen.", "help.q.limit": "Wie viele Spiele darf ich hinzufügen?", "help.a.limit": "Bis zu 5 Vorschläge pro Spieler in Vorschlagen. Jeder Joker in der Abstimmungsphase ermöglicht einen weiteren; der Joker wird beim Nutzen verbraucht.", "help.q.editNames": "Kann ich Vorschläge später ändern oder löschen?", "help.a.editNames": "Spieltitel sind nach Verlassen der Vorschlagsphase gesperrt. Optionale Felder bleiben bearbeitbar; Admins können jederzeit ändern oder löschen.", "help.q.mediaRules": "Gibt es Regeln für Links und Bilder?", "help.a.mediaRules": "Nutze http(s)-Links. Screenshots müssen auf eine Bilddatei enden (png, jpg, gif, webp, avif). Ungültige Links werden abgelehnt.", "help.q.cantAdd": "Warum wurde mein Vorschlag abgelehnt?", "help.a.cantAdd": "Du kannst nur in Vorschlagen oder mit einem Joker in Abstimmen hinzufügen. Gib einen Anzeigenamen an, bleib innerhalb des 5-Plus-Joker-Limits, nutze erreichbare http(s)-Links und halte Spielerzahlen zwischen 1 und 32 mit Min \u2264 Max.", "help.q.howVote": "Wie funktioniert die Abstimmung?", "help.a.howVote": "Bewege den 0–10 Schieberegler für jedes Spiel. Stimmen speichern sofort; du kannst ändern, bis du abschließt.", "help.q.finalize": "Was bedeutet Abschließen?", "help.a.finalize": "Abschließen sperrt deine Stimmen in der Phase Abstimmen. Alle Spiele müssen bewertet sein. Schalte es in derselben Phase wieder aus, um Änderungen zu machen.", "help.q.voteBlocked": "Warum kann ich nicht abstimmen?", "help.a.voteBlocked": "Abstimmen geht nur in der Phase Abstimmen, wenn du eingeloggt bist und einen Anzeigenamen hast. Wenn du keinen angegeben hast, registriere dich neu; Admins können ihn nicht nachtragen.", "help.q.jokerAfterFreeze": "Ich habe abgeschlossen, aber mir fällt ein tolles Spiel ein. Kann ich es noch hinzufügen?", "help.a.jokerAfterFreeze": "Bitte in der Abstimmungsphase einen Admin um einen Joker. Er gibt dir einen zusätzlichen Slot, verbraucht den Joker und öffnet alle Finalisierungen, damit das neue Spiel bewertet werden kann.", "help.q.linkedVotes": "Was sind verknüpfte Spiele?", "help.a.linkedVotes": "Admins können in der Abstimmungsphase Duplikate verknüpfen. Verknüpfte Spiele teilen Stimmen. Verknüpfen oder Trennen löscht die Stimmen der Gruppe und öffnet betroffene Spieler erneut.", "help.q.newGameAfterFinal": "Nach dem Abschließen ist ein neues Spiel aufgetaucht. Was nun?", "help.a.newGameAfterFinal": "Neue Spiele (durch Joker) oder Link-Änderungen heben Finalisierungen auf. Prüfe deine Liste und bewerte alles erneut, bevor du wieder abschließt.", "help.q.scoreRange": "Welche Wertungen sind erlaubt?", "help.a.scoreRange": "Nur ganze Zahlen von 0 bis 10 sind gültig. Alles andere wird abgelehnt.", "help.q.resultsLocked": "Warum sehe ich keine Ergebnisse?", "help.a.resultsLocked": "Ergebnisse bleiben verborgen, bis ein Admin sie freigibt. Danach wechselst du automatisch zur Ergebnisphase.", "help.q.resultsContent": "Was zeigen die Ergebnisse?", "help.a.resultsContent": "Eine Rangliste mit Durchschnitt, Stimmenanzahl, deiner eigenen Stimme sowie Links und Medien pro Spiel.", "help.q.editInResults": "Kann ich in den Ergebnissen noch etwas ändern?", "help.a.editInResults": "Nein. Vorschläge und Stimmen sind in den Ergebnissen gesperrt. Änderungen erfordern eine Rückkehr zur Abstimmungsphase (z. B. wenn ein Admin die Ergebnisse schließt oder zurücksetzt).", } }; const storageKey = "app_lang"; const defaultLang = "en"; let currentLang = defaultLang; const listeners = []; function interpolate(template, params = {}) { return template.replace(/\{(\w+)\}/g, (_, key) => (params[key] ?? `{${key}}`)); } function t(key, params) { const fallback = translations[defaultLang][key] ?? key; const phrase = translations[currentLang]?.[key] ?? fallback; return interpolate(phrase, params); } function detectLanguage() { const stored = localStorage.getItem(storageKey); if (stored && translations[stored]) return stored; const nav = navigator.language?.slice(0, 2); if (nav && translations[nav]) return nav; return defaultLang; } function applyTranslations(root = document) { root.querySelectorAll("[data-i18n]").forEach((el) => { const key = el.dataset.i18n; const attrs = (el.dataset.i18nAttr || "") .split(",") .map((a) => a.trim()) .filter(Boolean); const text = t(key); if (attrs.length === 0) { el.textContent = text; } else { attrs.forEach((attr) => el.setAttribute(attr, text)); } }); } function notify() { listeners.forEach((fn) => fn(currentLang)); } function setLanguage(lang) { if (!translations[lang]) lang = defaultLang; currentLang = lang; localStorage.setItem(storageKey, lang); document.documentElement.lang = lang; applyTranslations(); notify(); } function getLanguage() { return currentLang; } function initI18n() { currentLang = detectLanguage(); document.documentElement.lang = currentLang; applyTranslations(); notify(); return currentLang; } function onLanguageChange(fn) { listeners.push(fn); } export { t, setLanguage, getLanguage, initI18n, applyTranslations, onLanguageChange, translations };