Files
GameList/wwwroot/js/i18n.js

465 lines
22 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.
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 havent 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 youre 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 to see answers for players and admins.",
"help.cat.gettingStarted": "Getting started",
"help.cat.suggest": "Suggest phase",
"help.cat.vote": "Vote phase",
"help.cat.results": "Results phase",
"help.cat.admin": "Admin tools",
"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.limit": "How many games can I add?",
"help.a.limit": "Up to 5 suggestions per player. A joker lets you add one extra during the vote phase.",
"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.howVote": "How do votes work?",
"help.a.howVote": "Move the 010 slider for each game. Your score and emoji update immediately.",
"help.q.finalize": "What does finalize do?",
"help.a.finalize": "Finalize locks your votes until you unfinalize. You must score every game first.",
"help.q.linkedVotes": "What are linked games?",
"help.a.linkedVotes": "Admins can link duplicates. Changing one linked slider updates all siblings; votes are shared.",
"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.openResults": "How do I open or close results?",
"help.a.openResults": "Admins toggle results on the Admin panel. Opening moves everyone to Results; closing sends players back to Vote and clears finalizations.",
"help.q.linkDuplicates": "How do I merge duplicates?",
"help.a.linkDuplicates": "In Vote, pick two games under Admin Link games. Linking clears votes for that group and unfinalizes affected players.",
"help.q.grantJoker": "When should I grant a joker?",
"help.a.grantJoker": "Grant a joker during Vote to let a player add one extra suggestion; their finalized state resets.",
"help.q.reset": "Reset vs factory reset?",
"help.a.reset": "Reset clears suggestions/votes and phases but keeps players. Factory reset wipes everything, including players, and reseeds the app.",
},
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 Antworten für Spieler und Admins zu sehen.",
"help.cat.gettingStarted": "Erste Schritte",
"help.cat.suggest": "Phase Vorschlagen",
"help.cat.vote": "Phase Bewerten",
"help.cat.results": "Phase Ergebnisse",
"help.cat.admin": "Admin-Werkzeuge",
"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.limit": "Wie viele Spiele darf ich hinzufügen?",
"help.a.limit": "Bis zu 5 Vorschläge pro Spieler. Ein Joker erlaubt während der Abstimmung einen zusätzlichen Vorschlag.",
"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.howVote": "Wie funktioniert die Abstimmung?",
"help.a.howVote": "Bewege den 010 Schieberegler für jedes Spiel. Deine Wertung und das Emoji aktualisieren sich sofort.",
"help.q.finalize": "Was bedeutet Abschließen?",
"help.a.finalize": "Abschließen sperrt deine Stimmen, bis du wieder öffnest. Zuerst müssen alle Spiele bewertet sein.",
"help.q.linkedVotes": "Was sind verknüpfte Spiele?",
"help.a.linkedVotes": "Admins können Duplikate verknüpfen. Ein Regler ändert dann die Stimmen aller verknüpften Spiele gemeinsam.",
"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.openResults": "Wie öffne oder schließe ich Ergebnisse?",
"help.a.openResults": "Admins schalten im Admin-Bereich die Ergebnisse frei. Öffnen bringt alle in die Ergebnisphase; Schließen schickt Spieler zurück zur Abstimmung und entfernt Finalisierungen.",
"help.q.linkDuplicates": "Wie verbinde ich Duplikate?",
"help.a.linkDuplicates": "In der Abstimmungsphase zwei Spiele unter Admin Spiele verknüpfen auswählen. Verknüpfen löscht die Stimmen dieser Gruppe und öffnet betroffene Spieler erneut.",
"help.q.grantJoker": "Wann sollte ich einen Joker vergeben?",
"help.a.grantJoker": "Vergib während der Abstimmung einen Joker, damit ein Spieler einen zusätzlichen Vorschlag machen kann; seine Finalisierung wird zurückgesetzt.",
"help.q.reset": "Reset vs. Werkseinstellung?",
"help.a.reset": "Reset löscht Vorschläge/Stimmen und Phasen, behält aber Spieler. Werkseinstellung entfernt alles inklusive Spieler und setzt die App neu auf.",
}
};
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 };