From bcf18812eb5572ba1fc4f5b47f4c5b92b3c7074c Mon Sep 17 00:00:00 2001 From: ScriptSorcerer20 Date: Thu, 9 Apr 2026 10:34:09 +0200 Subject: [PATCH 1/3] Seperate language translation in diffrent file and sort files --- app/static/js/dashboard.js | 356 ++++++++++++++------------- app/static/js/index.js | 126 ++++------ app/static/js/language.js | 261 ++++++++++++++++++++ app/static/js/legal.js | 142 +++-------- app/static/js/login.js | 97 +++----- app/templates/dashboard.html | 5 +- app/templates/impressum.html | 3 +- app/templates/index.html | 3 +- app/templates/login.html | 8 +- app/templates/login_settings.html | 1 + app/templates/privacy.html | 3 +- app/templates/sign_up.html | 1 + app/templates/two_factor_verify.html | 1 + passenger_wsgi.py | 2 +- 14 files changed, 570 insertions(+), 439 deletions(-) create mode 100644 app/static/js/language.js diff --git a/app/static/js/dashboard.js b/app/static/js/dashboard.js index 60aaa41..3a7b2ef 100644 --- a/app/static/js/dashboard.js +++ b/app/static/js/dashboard.js @@ -1,9 +1,13 @@ +// Board configuration const DEFAULT_BOARD_SIZE = 8; const MIN_BOARD_SIZE = 4; const MAX_BOARD_SIZE = 13; const EMPTY_CELL = -1; const GAME_STORAGE_KEY = "eight-queens-dashboard-session-v1"; +const AppLanguage = window.AppLanguage; + +// Mutable game and solver state let mode = "queens"; let boardSize = DEFAULT_BOARD_SIZE; let pendingBoardSize = DEFAULT_BOARD_SIZE; @@ -22,6 +26,7 @@ let solverLoading = false; let solverRequestId = 0; let currentSolverHighlight = null; +// Cached DOM references const boardDiv = document.getElementById("board"); const statusMessage = document.getElementById("statusMessage"); const queenList = document.getElementById("queenList"); @@ -78,7 +83,9 @@ const timeHistoryTableBody = document.getElementById("timeHistoryTableBody"); const infoModal = document.getElementById("infoModal"); const infoModalBackdrop = document.getElementById("infoModalBackdrop"); -const SUPPORTED_LANGUAGES = ["de", "en"]; +// ----------------------------------------------------------------------------- +// Translation dictionaries and language-specific copy + let currentLanguage = "de"; const MODE_COPY = { @@ -437,42 +444,8 @@ const translations = { } }; -function createEmptyBoard(size) { - return Array(size).fill(EMPTY_CELL); -} - -function clampBoardSize(size) { - return Math.min(MAX_BOARD_SIZE, Math.max(MIN_BOARD_SIZE, Number(size))); -} - -function getColumnLabel(index) { - return String.fromCharCode(65 + index); -} - -function normalizeLanguage(language) { - const shortLanguage = String(language || "").trim().slice(0, 2).toLowerCase(); - return SUPPORTED_LANGUAGES.includes(shortLanguage) ? shortLanguage : "de"; -} - -function getStoredLanguage() { - try { - return window.localStorage.getItem("preferredLanguage"); - } catch (error) { - return null; - } -} - -function storeLanguage(language) { - try { - window.localStorage.setItem("preferredLanguage", language); - } catch (error) { - return; - } -} - -function detectBrowserLanguage() { - return normalizeLanguage(navigator.language || navigator.userLanguage || "de"); -} +// ----------------------------------------------------------------------------- +// Translation helpers function t(key, params = {}) { const value = translations[currentLanguage][key]; @@ -483,73 +456,86 @@ function getDefaultSolverMessage() { return t("defaultSolverMessage"); } -function applyTextTranslations() { - document.documentElement.lang = currentLanguage; - document.body.dataset.language = currentLanguage; - - document.querySelectorAll("[data-i18n]").forEach((element) => { - const key = element.dataset.i18n; - const translatedText = t(key); - - if (translatedText === undefined) { - return; - } - - if (element.tagName === "TITLE" || String(translatedText).includes(" { - const translatedText = t(element.dataset.i18nPlaceholder); - - if (translatedText === undefined) { - return; - } - - element.setAttribute("placeholder", translatedText); - }); - - document.querySelectorAll("[data-i18n-aria-label]").forEach((element) => { - const translatedText = t(element.dataset.i18nAriaLabel); - - if (translatedText === undefined) { - return; - } - - element.setAttribute("aria-label", translatedText); - }); -} - -function setLanguage(language) { - currentLanguage = normalizeLanguage(language); - applyTextTranslations(); - - langSwitch.textContent = currentLanguage.toUpperCase(); - langSwitch.setAttribute("aria-label", t("languageSwitchAria")); - langSwitch.title = currentLanguage === "de" ? "Switch to English" : "Zu Deutsch wechseln"; - - updateSettingsTrigger(); - updateModeContent(); - updateQueenList(); - updateProgressDisplay(); - updateSolverSpeed(solverSpeedMs); - updateSolverControls(); - - if (!solverLoading && !hasPreparedSolverTrace()) { - setSolverMessage(t("solverReady", { modeLabel: getModeLabel(mode) })); - } - - storeLanguage(currentLanguage); -} - function getModeCopy(selectedMode = mode, language = currentLanguage) { return MODE_COPY[language][selectedMode === "rooks" ? "rooks" : "queens"]; } +function getPieceLabel(selectedMode) { + return getModeCopy(selectedMode).singular; +} + +function getModeLabel(selectedMode) { + return getModeCopy(selectedMode).plural; +} + +function applyTextTranslations() { + AppLanguage.applyTranslations({ + translate: (key) => t(key), + allowHtml: (element, translatedText) => element.tagName === "TITLE" || String(translatedText).includes(" col !== EMPTY_CELL).length; } @@ -1051,6 +1095,16 @@ function openSavePanel() { toggleSavePanel(true); } +function closeAllPanels() { + toggleSidebar(false); + toggleSavePanel(false); + toggleSavePointsPanel(false); + toggleInfoModal(false); +} + +// ----------------------------------------------------------------------------- +// Save point and history helpers + function getBoardPreview(savedBoard, limit = 4) { const preview = []; @@ -1098,10 +1152,6 @@ function createSavePointActionButton(label, className, onClick) { return button; } -function getModeLabel(selectedMode) { - return getModeCopy(selectedMode).plural; -} - function renderSavePoints(savePoints) { savePointsList.innerHTML = ""; @@ -1211,6 +1261,8 @@ async function openSavePointsPanel() { toggleSavePointsPanel(true); } +// Server-loaded save points and locally restored sessions both flow through +// this helper so the dashboard only has one "hydrate state into UI" path. function applyLoadedGame(data) { discardSolverTrace(t("solverReady", { modeLabel: getModeLabel(data.mode || "queens") })); @@ -1235,6 +1287,9 @@ function applyLoadedGame(data) { persistCurrentSession(); } +// ----------------------------------------------------------------------------- +// Solver execution + function applySolverStep(step) { board = sanitizeBoard(step.board, boardSize); @@ -1320,6 +1375,8 @@ function startSolverPlayback(stepImmediately = true) { scheduleNextSolverPlaybackStep(); } +// requestId prevents older async responses from overwriting a newer solver run +// when the user changes size, mode, or solver action quickly. async function prepareSolutionTrace() { const endpoint = mode === "rooks" ? "/solve_rooks_trace" : "/solve_trace"; const requestId = ++solverRequestId; @@ -1484,55 +1541,8 @@ async function instantSolve() { } } -function resetBoard(options = {}) { - const { - statusMessageText = "", - statusColor = "", - solverMessageText = t("solverReady", { modeLabel: getModeLabel(mode) }) - } = options; - - discardSolverTrace(solverMessageText); - board = createEmptyBoard(boardSize); - currentSolverHighlight = null; - resetProgress(); - - if (statusMessageText) { - setStatus(statusMessageText, statusColor); - } else { - setStatus(); - } - - drawBoard(); -} - -function updateQueenList() { - queenList.innerHTML = ""; - let count = 1; - - for (let row = 0; row < boardSize; row++) { - if (board[row] === EMPTY_CELL) { - continue; - } - - const col = board[row]; - const position = `${getColumnLabel(col)}${boardSize - row}`; - const li = document.createElement("li"); - - li.textContent = `${getPieceLabel(mode)} ${count}: ${position}`; - queenList.appendChild(li); - count += 1; - } - - const placedPieces = count - 1; - pieceCountBadge.textContent = t("pieceCount", { count: placedPieces }); - - if (placedPieces === 0) { - const emptyItem = document.createElement("li"); - emptyItem.className = "empty"; - emptyItem.textContent = t("pieceEmpty", { modeLabel: getModeLabel(mode) }); - queenList.appendChild(emptyItem); - } -} +// ----------------------------------------------------------------------------- +// Save point persistence actions async function saveGame() { const response = await fetch("/save", { @@ -1623,6 +1633,9 @@ async function deleteSavePoint(savePointId) { setStatus(t("deleteSuccess"), "#2563eb"); } +// ----------------------------------------------------------------------------- +// Persistence and startup + function restoreSessionFromStorage() { try { const rawSession = window.localStorage.getItem(GAME_STORAGE_KEY); @@ -1657,11 +1670,12 @@ function restoreSessionFromStorage() { } } +// Boot order: translate UI, restore any local session, then load history data. async function initializeDashboard() { pendingBoardSize = boardSize; updateModeButtons(); updateBoardSizeDisplays(boardSize); - setLanguage(getStoredLanguage() || detectBrowserLanguage()); + languageController.init(); updateSettingsTrigger(false); setSolverMessage(t("solverReady", { modeLabel: getModeLabel(mode) })); @@ -1674,18 +1688,6 @@ async function initializeDashboard() { await refreshTimeHistory(); } -if (langSwitch) { - langSwitch.addEventListener("click", async () => { - setLanguage(currentLanguage === "de" ? "en" : "de"); - resetSaveForm(); - await refreshTimeHistory(); - - if (savePointsSidebar.classList.contains("open")) { - await refreshSavePoints(); - } - }); -} - window.addEventListener("keydown", (event) => { if (event.key === "Escape") { closeAllPanels(); diff --git a/app/static/js/index.js b/app/static/js/index.js index c2e7b1f..87b8dc1 100644 --- a/app/static/js/index.js +++ b/app/static/js/index.js @@ -1,8 +1,12 @@ +// settings for preview board const HERO_BOARD_SIZE = 8; const heroBoard = document.getElementById("heroBoard"); + +//translation for English and German +const AppLanguage = window.AppLanguage; const langSwitch = document.getElementById("langSwitch"); -const supportedLanguages = ["de", "en"]; const initialLanguage = window.LANDING_INITIAL_LANGUAGE || "de"; +let currentLanguage = initialLanguage; const translations = { de: { @@ -119,6 +123,43 @@ const translations = { } }; +function t(key) { + return translations[currentLanguage]?.[key] ?? null; +} + +function applyTranslations(language) { + currentLanguage = language; + + AppLanguage.applyTranslations({ + translate: (key) => t(key), + allowHtml: (element, translatedText) => element.tagName === "TITLE" || String(translatedText).includes(" { - const key = element.dataset.i18n; - const translatedText = dictionary[key]; - - if (!translatedText) { - return; - } - - if (element.tagName === "TITLE" || translatedText.includes(" { - const key = element.dataset.i18nAriaLabel; - const translatedText = dictionary[key]; - - if (!translatedText) { - return; - } - - element.setAttribute("aria-label", translatedText); - }); - - if (langSwitch) { - langSwitch.textContent = resolvedLanguage.toUpperCase(); - langSwitch.setAttribute("aria-label", resolvedLanguage === "de" ? "Switch language to English" : "Sprache auf Deutsch wechseln"); - langSwitch.title = resolvedLanguage === "de" ? "Switch to English" : "Zu Deutsch wechseln"; - } - - storeLanguage(resolvedLanguage); -} - -function getPreferredLanguage() { - return normalizeLanguage(getStoredLanguage() || detectBrowserLanguage() || initialLanguage); -} - function createHeroBoard() { if (!heroBoard) { return; @@ -241,16 +208,9 @@ function rotatePattern() { applyPattern(queenPatterns[currentPatternIndex]); } -if (langSwitch) { - langSwitch.addEventListener("click", () => { - const nextLanguage = document.documentElement.lang === "de" ? "en" : "de"; - setLanguage(nextLanguage); - }); -} - createHeroBoard(); applyPattern(queenPatterns[currentPatternIndex]); -setLanguage(getPreferredLanguage()); +languageController.init(); if (heroBoard) { window.setInterval(rotatePattern, 2200); diff --git a/app/static/js/language.js b/app/static/js/language.js new file mode 100644 index 0000000..843b0e7 --- /dev/null +++ b/app/static/js/language.js @@ -0,0 +1,261 @@ +// handels the language translation + +(function attachAppLanguage(window, document) { + const DEFAULT_LANGUAGE = "de"; + const DEFAULT_STORAGE_KEY = "preferredLanguage"; + const DEFAULT_SUPPORTED_LANGUAGES = ["de", "en"]; + + function resolveSupportedLanguages(supportedLanguages = DEFAULT_SUPPORTED_LANGUAGES) { + const normalizedLanguages = Array.isArray(supportedLanguages) + ? supportedLanguages + .map((language) => String(language || "").trim().slice(0, 2).toLowerCase()) + .filter(Boolean) + : []; + + return normalizedLanguages.length > 0 ? normalizedLanguages : DEFAULT_SUPPORTED_LANGUAGES.slice(); + } + + function normalizeLanguage( + language, + { + supportedLanguages = DEFAULT_SUPPORTED_LANGUAGES, + defaultLanguage = DEFAULT_LANGUAGE + } = {} + ) { + const normalizedSupportedLanguages = resolveSupportedLanguages(supportedLanguages); + const normalizedDefaultLanguage = normalizedSupportedLanguages.includes(defaultLanguage) + ? defaultLanguage + : normalizedSupportedLanguages[0] || DEFAULT_LANGUAGE; + const shortLanguage = String(language || "").trim().slice(0, 2).toLowerCase(); + + return normalizedSupportedLanguages.includes(shortLanguage) ? shortLanguage : normalizedDefaultLanguage; + } + + function getStoredLanguage({ storageKey = DEFAULT_STORAGE_KEY } = {}) { + try { + return window.localStorage.getItem(storageKey); + } catch (error) { + return null; + } + } + + function storeLanguage(language, { storageKey = DEFAULT_STORAGE_KEY } = {}) { + try { + window.localStorage.setItem(storageKey, language); + } catch (error) { + return; + } + } + + function detectBrowserLanguage(options = {}) { + return normalizeLanguage(navigator.language || navigator.userLanguage || options.defaultLanguage, options); + } + + function getPreferredLanguage( + { + supportedLanguages = DEFAULT_SUPPORTED_LANGUAGES, + defaultLanguage = DEFAULT_LANGUAGE, + storageKey = DEFAULT_STORAGE_KEY, + fallbackLanguage = defaultLanguage + } = {} + ) { + return normalizeLanguage( + getStoredLanguage({ storageKey }) || detectBrowserLanguage({ supportedLanguages, defaultLanguage }) || fallbackLanguage, + { + supportedLanguages, + defaultLanguage: fallbackLanguage + } + ); + } + + function getNextLanguage(language, supportedLanguages = DEFAULT_SUPPORTED_LANGUAGES) { + const normalizedSupportedLanguages = resolveSupportedLanguages(supportedLanguages); + const currentLanguage = normalizeLanguage(language, { + supportedLanguages: normalizedSupportedLanguages, + defaultLanguage: normalizedSupportedLanguages[0] + }); + const currentIndex = normalizedSupportedLanguages.indexOf(currentLanguage); + + if (currentIndex === -1 || normalizedSupportedLanguages.length < 2) { + return currentLanguage; + } + + return normalizedSupportedLanguages[(currentIndex + 1) % normalizedSupportedLanguages.length]; + } + + function setDocumentLanguage(language) { + if (document.documentElement) { + document.documentElement.lang = language; + } + + if (document.body) { + document.body.dataset.language = language; + } + } + + function applyTextTranslations({ + translate, + selector = "[data-i18n]", + allowHtml = () => false + } = {}) { + if (typeof translate !== "function") { + return; + } + + document.querySelectorAll(selector).forEach((element) => { + const key = element.dataset.i18n; + const translatedText = translate(key, element); + + if (translatedText === undefined || translatedText === null) { + return; + } + + if (allowHtml(element, translatedText, key)) { + element.innerHTML = String(translatedText); + return; + } + + element.textContent = String(translatedText); + }); + } + + function applyAttributeTranslations({ + translate, + mappings = [] + } = {}) { + if (typeof translate !== "function") { + return; + } + + mappings.forEach(({ selector, datasetKey, attribute }) => { + document.querySelectorAll(selector).forEach((element) => { + const key = element.dataset[datasetKey]; + const translatedText = translate(key, element, attribute); + + if (translatedText === undefined || translatedText === null) { + return; + } + + element.setAttribute(attribute, String(translatedText)); + }); + }); + } + + function applyTranslations({ + translate, + selector = "[data-i18n]", + allowHtml, + attributeMappings = [] + } = {}) { + applyTextTranslations({ translate, selector, allowHtml }); + applyAttributeTranslations({ translate, mappings: attributeMappings }); + } + + function updateLanguageSwitch( + button, + { + language, + label = String(language || "").toUpperCase(), + ariaLabel, + title + } = {} + ) { + if (!button) { + return; + } + + button.textContent = label; + + if (ariaLabel !== undefined) { + button.setAttribute("aria-label", ariaLabel); + } + + if (title !== undefined) { + button.title = title; + } + } + + function createController({ + button = document.getElementById("langSwitch"), + defaultLanguage = DEFAULT_LANGUAGE, + supportedLanguages = DEFAULT_SUPPORTED_LANGUAGES, + storageKey = DEFAULT_STORAGE_KEY, + onApply = () => { } + } = {}) { + const options = { + defaultLanguage, + supportedLanguages, + storageKey + }; + + let currentLanguage = normalizeLanguage(defaultLanguage, options); + + function apply(language, source = "apply") { + currentLanguage = normalizeLanguage(language, options); + setDocumentLanguage(currentLanguage); + + onApply(currentLanguage, { + source, + button, + applyTranslations, + updateLanguageSwitch: (switchOptions = {}) => updateLanguageSwitch(button, { + language: currentLanguage, + ...switchOptions + }), + getNextLanguage: () => getNextLanguage(currentLanguage, supportedLanguages), + normalizeLanguage: (value) => normalizeLanguage(value, options) + }); + + storeLanguage(currentLanguage, { storageKey }); + return currentLanguage; + } + + function toggle() { + return apply(getNextLanguage(currentLanguage, supportedLanguages), "toggle"); + } + + function init(initialLanguage = getPreferredLanguage({ + supportedLanguages, + defaultLanguage, + storageKey, + fallbackLanguage: defaultLanguage + })) { + if (button) { + button.addEventListener("click", toggle); + } + + return apply(initialLanguage, "init"); + } + + return { + apply, + getCurrentLanguage: () => currentLanguage, + getPreferredLanguage: (fallbackLanguage = defaultLanguage) => getPreferredLanguage({ + supportedLanguages, + defaultLanguage, + storageKey, + fallbackLanguage + }), + init, + toggle + }; + } + + window.AppLanguage = { + DEFAULT_LANGUAGE, + DEFAULT_STORAGE_KEY, + DEFAULT_SUPPORTED_LANGUAGES: DEFAULT_SUPPORTED_LANGUAGES.slice(), + applyAttributeTranslations, + applyTextTranslations, + applyTranslations, + createController, + detectBrowserLanguage, + getNextLanguage, + getPreferredLanguage, + getStoredLanguage, + normalizeLanguage, + setDocumentLanguage, + storeLanguage, + updateLanguageSwitch + }; +})(window, document); diff --git a/app/static/js/legal.js b/app/static/js/legal.js index d8869fd..93e1a20 100644 --- a/app/static/js/legal.js +++ b/app/static/js/legal.js @@ -1,9 +1,9 @@ -const SUPPORTED_LANGUAGES = ["de", "en"]; -const DEFAULT_LANGUAGE = "de"; -const LANGUAGE_STORAGE_KEY = "preferredLanguage"; +//translation for English and German +const AppLanguage = window.AppLanguage; const langSwitch = document.getElementById("langSwitch"); const legalPage = document.body.dataset.legalPage; +let currentLanguage = AppLanguage.DEFAULT_LANGUAGE; const translations = { de: { @@ -27,10 +27,10 @@ const translations = { contactEmailLabel: "E-Mail:", projectResponsibilityTitle: "Projektverantwortung", projectResponsibilityText: "Dieses Webprojekt wurde im Rahmen eines Schulprojekts zum Acht-Damen- und Acht-Türme-Problem erstellt. Inhaltlich verantwortlich ist die jeweils oben genannte verantwortliche Stelle.", - contentLiabilityTitle: "Haftung fuer Inhalte", - contentLiabilityText: "Die Inhalte dieser Website wurden mit Sorgfalt erstellt. Für die Richtigkeit, Vollstaendigkeit und Aktualitaet der Inhalte kann jedoch keine Gewaehr übernommen werden. Bei Bekanntwerden von Rechtsverletzungen werden entsprechende Inhalte umgehend überarbeitet oder entfernt.", - linksLiabilityTitle: "Haftung fuer Links", - linksLiabilityText: "Diese Website kann Links zu externen Angeboten enthalten. Fuer deren Inhalte sind ausschliesslich die jeweiligen Betreiber verantwortlich. Eine permanente inhaltliche Kontrolle verlinkter Seiten ist ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar.", + contentLiabilityTitle: "Haftung für Inhalte", + contentLiabilityText: "Die Inhalte dieser Website wurden mit Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte kann jedoch keine Gewähr übernommen werden. Bei Bekanntwerden von Rechtsverletzungen werden entsprechende Inhalte umgehend überarbeitet oder entfernt.", + linksLiabilityTitle: "Haftung für Links", + linksLiabilityText: "Diese Website kann Links zu externen Angeboten enthalten. Für deren Inhalte sind ausschliesslich die jeweiligen Betreiber verantwortlich. Eine permanente inhaltliche Kontrolle verlinkter Seiten ist ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar.", versionTitle: "Stand", versionText: "Stand dieser Vorlage: April 2026" }, @@ -52,7 +52,7 @@ const translations = { processingPurposeItem4: "Speichern und Laden von Spielständen", processingPurposeItem5: "technischer Betrieb, Fehleranalyse und Missbrauchsschutz", legalBasisTitle: "4. Rechtsgrundlage", - legalBasisText: "Soweit personenbezogene Daten verarbeitet werden, erfolgt dies in der Regel zur Bereitstellung der Anwendung und ihrer Funktionen sowie aufgrund des berechtigten Interesses an einem sicheren und funktionsfaehigen Webangebot. Falls eine ausdrückliche Einwilligung erforderlich ist, wird diese gesondert eingeholt.", + legalBasisText: "Soweit personenbezogene Daten verarbeitet werden, erfolgt dies in der Regel zur Bereitstellung der Anwendung und ihrer Funktionen sowie aufgrund des berechtigten Interesses an einem sicheren und funktionsfähigen Webangebot. Falls eine ausdrückliche Einwilligung erforderlich ist, wird diese gesondert eingeholt.", storageTitle: "5. Speicherung und Löschung", storageText: "Personenbezogene Daten werden nur so lange gespeichert, wie sie für den jeweiligen Zweck benötigt werden. Benutzerkonten und zugehörige Spielstände bleiben grundsätzlich gespeichert, bis sie gelöscht oder für das Projekt nicht mehr benötigt werden.", thirdPartyTitle: "6. Weitergabe an Dritte", @@ -61,11 +61,11 @@ const translations = { rightsText: "Betroffene Personen haben im Rahmen des anwendbaren Datenschutzrechts insbesondere das Recht auf:", rightsItem1: "Auskunft über die verarbeiteten personenbezogenen Daten", rightsItem2: "Berichtigung unrichtiger Daten", - rightsItem3: "Löschung oder Einschraenkung der Verarbeitung", + rightsItem3: "Löschung oder Einschränkung der Verarbeitung", rightsItem4: "Widerspruch gegen die Verarbeitung", rightsItem5: "Herausgabe der bereitgestellten Daten, soweit anwendbar", securityTitle: "8. Datensicherheit", - securityText: "Es werden technische und organisatorische Massnahmen getroffen, um gespeicherte Daten vor Verlust, Missbrauch oder unberechtigtem Zugriff zu schuetzen. Dazu gehoeren insbesondere passwortgeschuetzte Konten und die Möglichkeit, Zwei-Faktor-Authentifizierung zu aktivieren.", + securityText: "Es werden technische und organisatorische Massnahmen getroffen, um gespeicherte Daten vor Verlust, Missbrauch oder unberechtigtem Zugriff zu schützen. Dazu gehoeren insbesondere passwortgeschützte Konten und die Möglichkeit, Zwei-Faktor-Authentifizierung zu aktivieren.", versionTitle: "9. Stand", versionText: "Stand dieser Vorlage: April 2026" } @@ -136,107 +136,39 @@ const translations = { } }; -function normalizeLanguage(language) { - const shortLanguage = String(language || "").trim().slice(0, 2).toLowerCase(); - return SUPPORTED_LANGUAGES.includes(shortLanguage) ? shortLanguage : DEFAULT_LANGUAGE; -} - -function getStoredLanguage() { - try { - return window.localStorage.getItem(LANGUAGE_STORAGE_KEY); - } catch (error) { - return null; - } -} - -function storeLanguage(language) { - try { - window.localStorage.setItem(LANGUAGE_STORAGE_KEY, language); - } catch (error) { - return; - } -} - -function detectBrowserLanguage() { - return normalizeLanguage(navigator.language || navigator.userLanguage || DEFAULT_LANGUAGE); -} - -function t(language, key) { - const commonTranslation = translations[language]?.common?.[key]; +function t(key) { + const commonTranslation = translations[currentLanguage]?.common?.[key]; if (commonTranslation) { return commonTranslation; } - return translations[language]?.[legalPage]?.[key] ?? null; + return translations[currentLanguage]?.[legalPage]?.[key] ?? null; } -function translateFooter(language) { - const footerText = document.querySelector(".page-footer-text"); - const footerNav = document.querySelector(".page-footer-links"); - const privacyLink = document.querySelector('.page-footer-links a[href$="/privacy-policy"]'); - const imprintLink = document.querySelector('.page-footer-links a[href$="/impressum"]'); - - if (footerText) { - footerText.textContent = t(language, "footerText") || footerText.textContent; - } - - if (footerNav) { - footerNav.setAttribute("aria-label", t(language, "footerNavAria") || footerNav.getAttribute("aria-label") || ""); - } - - if (privacyLink) { - privacyLink.textContent = t(language, "footerPrivacy") || privacyLink.textContent; - } - - if (imprintLink) { - imprintLink.textContent = t(language, "footerImprint") || imprintLink.textContent; - } -} - -function applyTranslations(language) { - document.documentElement.lang = language; - document.body.dataset.language = language; - - document.querySelectorAll("[data-i18n]").forEach((element) => { - const translatedText = t(language, element.dataset.i18n); - - if (!translatedText) { - return; - } - - element.textContent = translatedText; - }); - - document.querySelectorAll("[data-i18n-aria-label]").forEach((element) => { - const translatedText = t(language, element.dataset.i18nAriaLabel); - - if (!translatedText) { - return; - } - - element.setAttribute("aria-label", translatedText); - }); - - translateFooter(language); - - if (langSwitch) { - langSwitch.textContent = language.toUpperCase(); - langSwitch.setAttribute("aria-label", t(language, "languageSwitchAria") || "Sprache wechseln"); - langSwitch.title = language === "de" ? "Switch to English" : "Zu Deutsch wechseln"; - } - - storeLanguage(language); -} - -function setLanguage(language) { - applyTranslations(normalizeLanguage(language)); -} - -setLanguage(getStoredLanguage() || detectBrowserLanguage()); - -if (langSwitch) { - langSwitch.addEventListener("click", () => { - setLanguage(document.documentElement.lang === "de" ? "en" : "de"); +function applyTranslations() { + AppLanguage.applyTranslations({ + translate: (key) => t(key), + attributeMappings: [ + { + selector: "[data-i18n-aria-label]", + datasetKey: "i18nAriaLabel", + attribute: "aria-label" + } + ] }); } + +const languageController = AppLanguage.createController({ + button: langSwitch, + onApply(language, helpers) { + currentLanguage = language; + applyTranslations(); + helpers.updateLanguageSwitch({ + ariaLabel: t("languageSwitchAria") || "Sprache wechseln", + title: language === "de" ? "Switch to English" : "Zu Deutsch wechseln" + }); + } +}); + +languageController.init(); diff --git a/app/static/js/login.js b/app/static/js/login.js index 30b38a5..b426cad 100644 --- a/app/static/js/login.js +++ b/app/static/js/login.js @@ -1,7 +1,6 @@ -const SUPPORTED_LANGUAGES = ["de", "en"]; -const DEFAULT_LANGUAGE = "de"; -const LANGUAGE_STORAGE_KEY = "preferredLanguage"; +//translation for English and German +const AppLanguage = window.AppLanguage; const langSwitch = document.getElementById("langSwitch"); const translations = { @@ -112,32 +111,7 @@ const serverMessageTranslations = { en: {} }; -let currentLanguage = DEFAULT_LANGUAGE; - -function normalizeLanguage(language) { - const shortLanguage = String(language || "").trim().slice(0, 2).toLowerCase(); - return SUPPORTED_LANGUAGES.includes(shortLanguage) ? shortLanguage : DEFAULT_LANGUAGE; -} - -function getStoredLanguage() { - try { - return window.localStorage.getItem(LANGUAGE_STORAGE_KEY); - } catch (error) { - return null; - } -} - -function storeLanguage(language) { - try { - window.localStorage.setItem(LANGUAGE_STORAGE_KEY, language); - } catch (error) { - return; - } -} - -function detectBrowserLanguage() { - return normalizeLanguage(navigator.language || navigator.userLanguage || DEFAULT_LANGUAGE); -} +let currentLanguage = AppLanguage.DEFAULT_LANGUAGE; function t(key) { return translations[currentLanguage]?.[key] ?? translations.en[key] ?? key; @@ -172,29 +146,25 @@ function translateServerMessage(message) { } function applyTextTranslations() { - document.documentElement.lang = currentLanguage; - - document.querySelectorAll("[data-i18n]").forEach((element) => { - const translatedText = t(element.dataset.i18n); - - if (element.tagName === "TITLE") { - element.textContent = translatedText; - return; - } - - element.textContent = translatedText; - }); - - document.querySelectorAll("[data-i18n-aria-label]").forEach((element) => { - element.setAttribute("aria-label", t(element.dataset.i18nAriaLabel)); + AppLanguage.applyTranslations({ + translate: (key) => t(key), + attributeMappings: [ + { + selector: "[data-i18n-aria-label]", + datasetKey: "i18nAriaLabel", + attribute: "aria-label" + }, + { + selector: "[data-i18n-loading-label]", + datasetKey: "i18nLoadingLabel", + attribute: "data-loading-label" + } + ] }); document.querySelectorAll("[data-i18n-loading-label]").forEach((element) => { - const translatedLoadingLabel = t(element.dataset.i18nLoadingLabel); - element.dataset.loadingLabel = translatedLoadingLabel; - if (element.dataset.loadingLabelApplied === "true") { - element.textContent = translatedLoadingLabel; + element.textContent = element.dataset.loadingLabel; } }); @@ -205,30 +175,27 @@ function applyTextTranslations() { element.textContent = translateServerMessage(element.dataset.originalText); }); +} - if (langSwitch) { - langSwitch.textContent = currentLanguage.toUpperCase(); - langSwitch.setAttribute("aria-label", t("languageSwitchAria")); - langSwitch.title = currentLanguage === "de" ? "Switch to English" : "Zu Deutsch wechseln"; +const languageController = AppLanguage.createController({ + button: langSwitch, + onApply(language, helpers) { + currentLanguage = language; + applyTextTranslations(); + helpers.updateLanguageSwitch({ + ariaLabel: t("languageSwitchAria"), + title: currentLanguage === "de" ? "Switch to English" : "Zu Deutsch wechseln" + }); } -} +}); -function setLanguage(language) { - currentLanguage = normalizeLanguage(language); - applyTextTranslations(); - storeLanguage(currentLanguage); -} +languageController.init(); -setLanguage(getStoredLanguage() || detectBrowserLanguage()); - -if (langSwitch) { - langSwitch.addEventListener("click", () => { - setLanguage(currentLanguage === "de" ? "en" : "de"); - }); -} +// ----------------------------------------------------------------------------------------------- const magneticButtons = document.querySelectorAll(".magnetic"); +// magnetic button effect for desktop user magneticButtons.forEach((button) => { button.addEventListener("mousemove", (event) => { const rect = button.getBoundingClientRect(); diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index 45379c1..1009092 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -364,7 +364,8 @@ - + + - \ No newline at end of file + diff --git a/app/templates/impressum.html b/app/templates/impressum.html index bfca517..04b486e 100644 --- a/app/templates/impressum.html +++ b/app/templates/impressum.html @@ -76,7 +76,8 @@ {% include "partials/footer.html" %} + - \ No newline at end of file + diff --git a/app/templates/index.html b/app/templates/index.html index 8222126..5f88a89 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -190,7 +190,8 @@ - + + diff --git a/app/templates/login.html b/app/templates/login.html index 1ef3298..453897b 100644 --- a/app/templates/login.html +++ b/app/templates/login.html @@ -13,7 +13,7 @@