393 lines
16 KiB
JavaScript
393 lines
16 KiB
JavaScript
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",
|
||
},
|
||
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",
|
||
}
|
||
};
|
||
|
||
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 };
|