Seperate language translation in diffrent file and sort files
This commit is contained in:
+179
-177
@@ -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("<br")) {
|
||||
element.innerHTML = translatedText;
|
||||
return;
|
||||
}
|
||||
|
||||
element.textContent = translatedText;
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-i18n-placeholder]").forEach((element) => {
|
||||
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("<br"),
|
||||
attributeMappings: [
|
||||
{
|
||||
selector: "[data-i18n-placeholder]",
|
||||
datasetKey: "i18nPlaceholder",
|
||||
attribute: "placeholder"
|
||||
},
|
||||
{
|
||||
selector: "[data-i18n-aria-label]",
|
||||
datasetKey: "i18nAriaLabel",
|
||||
attribute: "aria-label"
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// A language change also refreshes dynamic dashboard content,
|
||||
// not just static text nodes in the template.
|
||||
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"
|
||||
});
|
||||
|
||||
updateSettingsTrigger();
|
||||
updateModeContent();
|
||||
updateQueenList();
|
||||
updateProgressDisplay();
|
||||
updateSolverSpeed(solverSpeedMs);
|
||||
updateSolverControls();
|
||||
|
||||
if (!solverLoading && !hasPreparedSolverTrace()) {
|
||||
setSolverMessage(t("solverReady", { modeLabel: getModeLabel(mode) }));
|
||||
}
|
||||
|
||||
if (helpers.source === "toggle") {
|
||||
resetSaveForm();
|
||||
void refreshTimeHistory();
|
||||
|
||||
if (savePointsSidebar.classList.contains("open")) {
|
||||
void refreshSavePoints();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Core data and formatting helpers
|
||||
|
||||
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 sanitizeBoard(candidateBoard, size) {
|
||||
if (!Array.isArray(candidateBoard) || candidateBoard.length !== size) {
|
||||
return createEmptyBoard(size);
|
||||
@@ -585,6 +571,7 @@ function getElapsedMilliseconds() {
|
||||
return elapsedBeforeTimerStartMs + Math.max(0, Date.now() - timerStartedAtMs);
|
||||
}
|
||||
|
||||
// Keep a minimal local snapshot so a refresh can restore the current run.
|
||||
function persistCurrentSession() {
|
||||
try {
|
||||
window.localStorage.setItem(
|
||||
@@ -603,6 +590,9 @@ function persistCurrentSession() {
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Timer and UI synchronization
|
||||
|
||||
function updateProgressDisplay() {
|
||||
stepCounterValue.textContent = String(stepCount);
|
||||
timerValue.textContent = formatDuration(Math.floor(getElapsedMilliseconds() / 1000));
|
||||
@@ -661,6 +651,8 @@ function ensureFreshRunAfterSolvedBoard() {
|
||||
resetProgress();
|
||||
}
|
||||
|
||||
// These helpers keep the static labels and the dynamic dashboard widgets aligned
|
||||
// with the currently selected mode, language, and board size.
|
||||
function updateModeButtons() {
|
||||
btnQueens.classList.toggle("active", mode === "queens");
|
||||
btnRooks.classList.toggle("active", mode === "rooks");
|
||||
@@ -719,6 +711,9 @@ function setSolverMessage(message = getDefaultSolverMessage()) {
|
||||
solverMessage.textContent = message;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Rendering and solver state helpers
|
||||
|
||||
function updateSolverSpeed(rawValue) {
|
||||
solverSpeedMs = Number(rawValue);
|
||||
solverSpeedInput.value = String(solverSpeedMs);
|
||||
@@ -730,6 +725,8 @@ function updateSolverSpeed(rawValue) {
|
||||
}
|
||||
}
|
||||
|
||||
// These render helpers are reused by manual play, solver playback,
|
||||
// save-point loading, and session restore.
|
||||
function updateBoardLayout() {
|
||||
let cellSize = Math.max(32, Math.min(60, Math.floor(480 / boardSize)));
|
||||
let labelSize = cellSize;
|
||||
@@ -746,6 +743,35 @@ function updateBoardLayout() {
|
||||
boardDiv.style.setProperty("--label-size", `${labelSize}px`);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function isSolverPlaying() {
|
||||
return solverTimerId !== null;
|
||||
}
|
||||
@@ -816,6 +842,9 @@ function rewindSolverTrace() {
|
||||
updateSolverControls();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Board rules, rendering, and player interaction
|
||||
|
||||
function setMode(newMode) {
|
||||
if (mode === newMode) {
|
||||
return;
|
||||
@@ -831,13 +860,6 @@ function setMode(newMode) {
|
||||
});
|
||||
}
|
||||
|
||||
function closeAllPanels() {
|
||||
toggleSidebar(false);
|
||||
toggleSavePanel(false);
|
||||
toggleSavePointsPanel(false);
|
||||
toggleInfoModal(false);
|
||||
}
|
||||
|
||||
function getConflicts(selectedBoard = board, selectedMode = mode) {
|
||||
const conflicts = [];
|
||||
|
||||
@@ -883,6 +905,8 @@ function finishSolvedRun() {
|
||||
persistCurrentSession();
|
||||
}
|
||||
|
||||
// The board is always rendered from state so manual moves, restored sessions,
|
||||
// and solver playback all share the same visual path.
|
||||
function drawBoard() {
|
||||
boardDiv.innerHTML = "";
|
||||
boardDiv.className = "board-grid";
|
||||
@@ -962,6 +986,30 @@ function placeQueen(row, col) {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Panels and board settings
|
||||
|
||||
function previewBoardSize(size) {
|
||||
pendingBoardSize = clampBoardSize(size);
|
||||
sidebarBoardSizeValue.textContent = pendingBoardSize;
|
||||
@@ -1028,10 +1076,6 @@ function toggleInfoModal(forceOpen) {
|
||||
infoModal.setAttribute("aria-hidden", String(!shouldOpen));
|
||||
}
|
||||
|
||||
function getPieceLabel(selectedMode) {
|
||||
return getModeCopy(selectedMode).singular;
|
||||
}
|
||||
|
||||
function getPlacedPieceCount() {
|
||||
return board.filter((col) => 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();
|
||||
|
||||
+43
-83
@@ -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("<br"),
|
||||
attributeMappings: [
|
||||
{
|
||||
selector: "[data-i18n-aria-label]",
|
||||
datasetKey: "i18nAriaLabel",
|
||||
attribute: "aria-label"
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
const languageController = AppLanguage.createController({
|
||||
button: langSwitch,
|
||||
defaultLanguage: initialLanguage,
|
||||
onApply(language, helpers) {
|
||||
applyTranslations(language);
|
||||
helpers.updateLanguageSwitch({
|
||||
ariaLabel: language === "de" ? "Switch language to English" : "Sprache auf Deutsch wechseln",
|
||||
title: language === "de" ? "Switch to English" : "Zu Deutsch wechseln"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
languageController.init();
|
||||
|
||||
// ----------------------------------------------------------------------------------------
|
||||
|
||||
// simple pattern for preview board
|
||||
const queenPatterns = [
|
||||
[0, 4, 7, 5, 2, 6, 1, 3],
|
||||
[1, 3, 5, 7, 2, 0, 6, 4],
|
||||
@@ -127,80 +168,6 @@ const queenPatterns = [
|
||||
|
||||
let currentPatternIndex = 0;
|
||||
|
||||
function normalizeLanguage(language) {
|
||||
const shortLanguage = String(language || "").trim().slice(0, 2).toLowerCase();
|
||||
return supportedLanguages.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() {
|
||||
const browserLanguage = navigator.language || navigator.userLanguage || initialLanguage;
|
||||
return normalizeLanguage(browserLanguage);
|
||||
}
|
||||
|
||||
function setLanguage(language) {
|
||||
const resolvedLanguage = normalizeLanguage(language);
|
||||
const dictionary = translations[resolvedLanguage];
|
||||
const i18nElements = document.querySelectorAll("[data-i18n]");
|
||||
|
||||
document.documentElement.lang = resolvedLanguage;
|
||||
document.body.dataset.language = resolvedLanguage;
|
||||
|
||||
i18nElements.forEach((element) => {
|
||||
const key = element.dataset.i18n;
|
||||
const translatedText = dictionary[key];
|
||||
|
||||
if (!translatedText) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.tagName === "TITLE" || translatedText.includes("<br")) {
|
||||
element.innerHTML = translatedText;
|
||||
return;
|
||||
}
|
||||
|
||||
element.textContent = translatedText;
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-i18n-aria-label]").forEach((element) => {
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
+37
-105
@@ -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();
|
||||
|
||||
+32
-65
@@ -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();
|
||||
|
||||
@@ -364,7 +364,8 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/language.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/dashboard.js') }}" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -76,6 +76,7 @@
|
||||
</main>
|
||||
|
||||
{% include "partials/footer.html" %}
|
||||
<script src="{{ url_for('static', filename='js/language.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/legal.js') }}" defer></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -190,7 +190,8 @@
|
||||
<script>
|
||||
window.LANDING_INITIAL_LANGUAGE = "{{ initial_language }}";
|
||||
</script>
|
||||
<script src="{{ url_for('static', filename='js/index.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='js/language.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/index.js') }}" defer></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<nav class="auth-nav">
|
||||
<a class="brand" href="{{ url_for('main.home') }}">
|
||||
<span class="brand-mark">♛</span>
|
||||
<span>8-Damen- und 8-Tuerme Solver</span>
|
||||
<span class="nav-brand">8-Damen- und 8-Türme Solver</span>
|
||||
</a>
|
||||
|
||||
<ul class="nav-links">
|
||||
@@ -25,7 +25,8 @@
|
||||
<main class="auth-shell">
|
||||
<section class="auth-panel">
|
||||
<h1>Login to your account</h1>
|
||||
<p class="muted">If two-factor authentication is enabled, enter the current 6-digit code from your authenticator app after the password check.</p>
|
||||
<p class="muted">If two-factor authentication is enabled, enter the current 6-digit code from your
|
||||
authenticator app after the password check.</p>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
@@ -53,6 +54,7 @@
|
||||
|
||||
{% include "partials/footer.html" %}
|
||||
|
||||
<script src="{{ url_for('static', filename='js/language.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/login.js') }}" defer></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -155,6 +155,7 @@
|
||||
|
||||
{% include "partials/footer.html" %}
|
||||
|
||||
<script src="{{ url_for('static', filename='js/language.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/login.js') }}" defer></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@
|
||||
</main>
|
||||
|
||||
{% include "partials/footer.html" %}
|
||||
<script src="{{ url_for('static', filename='js/language.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/legal.js') }}" defer></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
|
||||
{% include "partials/footer.html" %}
|
||||
|
||||
<script src="{{ url_for('static', filename='js/language.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/login.js') }}" defer></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
|
||||
{% include "partials/footer.html" %}
|
||||
|
||||
<script src="{{ url_for('static', filename='js/language.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/login.js') }}" defer></script>
|
||||
</body>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user