311 lines
11 KiB
JavaScript
311 lines
11 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. Titles become locked; only extra details stay editable.",
|
||
"nav.freezeModalTitle": "Freeze suggestions?",
|
||
"nav.freezeModalBody": "Once you leave Suggest, your games are locked: titles 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.",
|
||
|
||
"suggest.title": "Suggest games (up to 5)",
|
||
"suggest.new": "Add new suggestion",
|
||
"suggest.addButton": "Suggest a 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",
|
||
|
||
"vote.saved": "Saved vote",
|
||
"vote.missing": "Missing",
|
||
"vote.missingWarn": "You haven’t voted yet. Slide to set a score.",
|
||
|
||
"results.rank": "Rank",
|
||
"results.game": "Game",
|
||
"results.author": "Author",
|
||
"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",
|
||
|
||
"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. Titel bleiben gesperrt; nur Zusatzinfos bleiben bearbeitbar.",
|
||
"nav.freezeModalTitle": "Vorschläge einfrieren?",
|
||
"nav.freezeModalBody": "Sobald du die Vorschlagsphase verlässt, sind deine Spiele gesperrt: Titel 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.",
|
||
|
||
"suggest.title": "Schlage Spiele vor (bis zu 5)",
|
||
"suggest.new": "Neuen Vorschlag hinzufügen",
|
||
"suggest.addButton": "Spiel vorschlagen",
|
||
"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",
|
||
|
||
"vote.saved": "Stimme gespeichert",
|
||
"vote.missing": "Fehlt",
|
||
"vote.missingWarn": "Du hast hier noch nicht abgestimmt. Schiebe den Regler.",
|
||
|
||
"results.rank": "Rang",
|
||
"results.game": "Spiel",
|
||
"results.author": "Autor",
|
||
"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",
|
||
|
||
"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 };
|