fixed some missing translations and added a back button to login_settings.

This commit is contained in:
2026-04-07 11:14:20 +02:00
parent b27c0ebcac
commit fc94d812d2
6 changed files with 563 additions and 258 deletions
+80
View File
@@ -117,6 +117,14 @@ a {
.nav-form { .nav-form {
margin: 0; margin: 0;
display: flex;
flex-direction: row;
align-items: center;
gap: 0;
padding: 0;
background: transparent;
border: none;
color: #ecf4fb;
} }
.auth-shell { .auth-shell {
@@ -145,6 +153,68 @@ a {
width: min(980px, 100%); width: min(980px, 100%);
} }
.settings-topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
flex-wrap: wrap;
margin-bottom: 20px;
}
.back-link {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-radius: 999px;
background: rgba(31, 49, 64, 0.08);
border: 1px solid rgba(31, 49, 64, 0.12);
font-family: "Trebuchet MS", "Segoe UI", sans-serif;
font-weight: 700;
color: var(--navy);
transition: transform 0.2s ease, background 0.2s ease, border-color 0.2s ease;
}
.back-link:hover {
transform: translateY(-1px);
background: rgba(31, 49, 64, 0.12);
border-color: rgba(31, 49, 64, 0.18);
}
.back-link-arrow {
font-size: 18px;
line-height: 1;
}
.lang-switch,
#logoutButton {
margin-top: 0;
min-height: 40px;
padding: 8px 14px;
border-radius: 999px;
font-size: 14px;
line-height: 1.1;
}
.lang-switch {
min-width: 58px;
background: rgba(31, 49, 64, 0.08);
border: 1px solid rgba(31, 49, 64, 0.12);
color: var(--navy);
}
.lang-switch:hover {
color: var(--navy);
background: rgba(31, 49, 64, 0.12);
border-color: rgba(31, 49, 64, 0.18);
box-shadow: none;
}
#logoutButton {
color: #fff7eb;
}
.settings-grid { .settings-grid {
display: grid; display: grid;
gap: 24px; gap: 24px;
@@ -344,6 +414,16 @@ button:active {
padding: 10px 14px; padding: 10px 14px;
} }
.settings-topbar {
align-items: stretch;
}
.back-link,
.lang-switch {
width: 100%;
justify-content: center;
}
.auth-shell { .auth-shell {
padding: 22px 12px 28px; padding: 22px 12px 28px;
} }
+104 -139
View File
@@ -26,13 +26,7 @@ const boardDiv = document.getElementById("board");
const statusMessage = document.getElementById("statusMessage"); const statusMessage = document.getElementById("statusMessage");
const queenList = document.getElementById("queenList"); const queenList = document.getElementById("queenList");
const pieceCountBadge = document.getElementById("pieceCountBadge"); const pieceCountBadge = document.getElementById("pieceCountBadge");
const navDashboardLink = document.getElementById("navDashboardLink");
const langSwitch = document.getElementById("langSwitch"); const langSwitch = document.getElementById("langSwitch");
const accountLabel = document.getElementById("accountLabel");
const introKicker = document.getElementById("introKicker");
const introTitle = document.getElementById("introTitle");
const introText = document.getElementById("introText");
const infoButton = document.getElementById("infoButton");
const gameModeEyebrow = document.getElementById("gameModeEyebrow"); const gameModeEyebrow = document.getElementById("gameModeEyebrow");
const gameTitle = document.getElementById("gameTitle"); const gameTitle = document.getElementById("gameTitle");
const boardModeText = document.getElementById("boardModeText"); const boardModeText = document.getElementById("boardModeText");
@@ -42,9 +36,6 @@ const infoModalTitle = document.getElementById("infoModalTitle");
const infoModeRuleText = document.getElementById("infoModeRuleText"); const infoModeRuleText = document.getElementById("infoModeRuleText");
const infoModalProblemHeading = document.getElementById("infoModalProblemHeading"); const infoModalProblemHeading = document.getElementById("infoModalProblemHeading");
const infoModalProblemText = document.getElementById("infoModalProblemText"); const infoModalProblemText = document.getElementById("infoModalProblemText");
const stepsLabel = document.getElementById("stepsLabel");
const timeLabel = document.getElementById("timeLabel");
const statusLabel = document.getElementById("statusLabel");
const stepCounterValue = document.getElementById("stepCounterValue"); const stepCounterValue = document.getElementById("stepCounterValue");
const timerValue = document.getElementById("timerValue"); const timerValue = document.getElementById("timerValue");
const gameProgressState = document.getElementById("gameProgressState"); const gameProgressState = document.getElementById("gameProgressState");
@@ -57,20 +48,10 @@ const btnPrepareSolve = document.getElementById("btnPrepareSolve");
const btnInstantSolve = document.getElementById("btnInstantSolve"); const btnInstantSolve = document.getElementById("btnInstantSolve");
const btnNextStep = document.getElementById("btnNextStep"); const btnNextStep = document.getElementById("btnNextStep");
const btnPlayPause = document.getElementById("btnPlayPause"); const btnPlayPause = document.getElementById("btnPlayPause");
const btnReset = document.getElementById("btnReset");
const btnSave = document.getElementById("btnSave");
const btnLoad = document.getElementById("btnLoad");
const solverMessage = document.getElementById("solverMessage"); const solverMessage = document.getElementById("solverMessage");
const solverStepInfo = document.getElementById("solverStepInfo"); const solverStepInfo = document.getElementById("solverStepInfo");
const solverKicker = document.getElementById("solverKicker");
const solverTitle = document.getElementById("solverTitle");
const animationSolverTitle = document.getElementById("animationSolverTitle");
const animationSolverText = document.getElementById("animationSolverText");
const instantSolverTitle = document.getElementById("instantSolverTitle");
const instantSolverText = document.getElementById("instantSolverText");
const solverSpeedInput = document.getElementById("solverSpeedInput"); const solverSpeedInput = document.getElementById("solverSpeedInput");
const solverSpeedValue = document.getElementById("solverSpeedValue"); const solverSpeedValue = document.getElementById("solverSpeedValue");
const solverSpeedLabel = document.getElementById("solverSpeedLabel");
const settingsSidebar = document.getElementById("settingsSidebar"); const settingsSidebar = document.getElementById("settingsSidebar");
const settingsTrigger = document.getElementById("settingsTrigger"); const settingsTrigger = document.getElementById("settingsTrigger");
const settingsTriggerArrow = document.getElementById("settingsTriggerArrow"); const settingsTriggerArrow = document.getElementById("settingsTriggerArrow");
@@ -85,46 +66,14 @@ const boardSizeValue = document.getElementById("boardSizeValue");
const boardSizeValueMirror = document.getElementById("boardSizeValueMirror"); const boardSizeValueMirror = document.getElementById("boardSizeValueMirror");
const sidebarBoardSizeValue = document.getElementById("sidebarBoardSizeValue"); const sidebarBoardSizeValue = document.getElementById("sidebarBoardSizeValue");
const sidebarBoardSizeValueMirror = document.getElementById("sidebarBoardSizeValueMirror"); const sidebarBoardSizeValueMirror = document.getElementById("sidebarBoardSizeValueMirror");
const settingsSidebarTitle = document.getElementById("settingsSidebarTitle");
const settingsSidebarClose = document.getElementById("settingsSidebarClose");
const settingsSidebarText = document.getElementById("settingsSidebarText");
const boardSizeInputLabel = document.getElementById("boardSizeInputLabel");
const applyBoardSizeButton = document.getElementById("applyBoardSizeButton");
const settingsSidebarHint = document.getElementById("settingsSidebarHint");
const savePointsList = document.getElementById("savePointsList"); const savePointsList = document.getElementById("savePointsList");
const saveNameInput = document.getElementById("saveNameInput"); const saveNameInput = document.getElementById("saveNameInput");
const saveNoteInput = document.getElementById("saveNoteInput"); const saveNoteInput = document.getElementById("saveNoteInput");
const saveFavoriteInput = document.getElementById("saveFavoriteInput"); const saveFavoriteInput = document.getElementById("saveFavoriteInput");
const saveSidebarTitle = document.getElementById("saveSidebarTitle");
const saveSidebarClose = document.getElementById("saveSidebarClose");
const saveSidebarText = document.getElementById("saveSidebarText");
const saveNameLabel = document.getElementById("saveNameLabel");
const saveNoteLabel = document.getElementById("saveNoteLabel");
const saveFavoriteLabel = document.getElementById("saveFavoriteLabel");
const saveNowButton = document.getElementById("saveNowButton");
const savePointModeFilter = document.getElementById("savePointModeFilter"); const savePointModeFilter = document.getElementById("savePointModeFilter");
const savePointSizeFilter = document.getElementById("savePointSizeFilter"); const savePointSizeFilter = document.getElementById("savePointSizeFilter");
const savePointSortFilter = document.getElementById("savePointSortFilter"); const savePointSortFilter = document.getElementById("savePointSortFilter");
const savePointFavoritesOnly = document.getElementById("savePointFavoritesOnly"); const savePointFavoritesOnly = document.getElementById("savePointFavoritesOnly");
const savePointsSidebarTitle = document.getElementById("savePointsSidebarTitle");
const savePointsSidebarClose = document.getElementById("savePointsSidebarClose");
const savePointsSidebarText = document.getElementById("savePointsSidebarText");
const savePointModeFilterLabel = document.getElementById("savePointModeFilterLabel");
const savePointSizeFilterLabel = document.getElementById("savePointSizeFilterLabel");
const savePointSortFilterLabel = document.getElementById("savePointSortFilterLabel");
const savePointFavoritesOnlyLabel = document.getElementById("savePointFavoritesOnlyLabel");
const savePointsEmptyState = document.getElementById("savePointsEmptyState");
const historyKicker = document.getElementById("historyKicker");
const historyTitle = document.getElementById("historyTitle");
const historySubtitle = document.getElementById("historySubtitle");
const btnRefreshHistory = document.getElementById("btnRefreshHistory");
const historyHeadSave = document.getElementById("historyHeadSave");
const historyHeadMode = document.getElementById("historyHeadMode");
const historyHeadBoard = document.getElementById("historyHeadBoard");
const historyHeadSteps = document.getElementById("historyHeadSteps");
const historyHeadTime = document.getElementById("historyHeadTime");
const historyHeadStatus = document.getElementById("historyHeadStatus");
const historyHeadSaved = document.getElementById("historyHeadSaved");
const timeHistoryTableBody = document.getElementById("timeHistoryTableBody"); const timeHistoryTableBody = document.getElementById("timeHistoryTableBody");
const infoModal = document.getElementById("infoModal"); const infoModal = document.getElementById("infoModal");
const infoModalBackdrop = document.getElementById("infoModalBackdrop"); const infoModalBackdrop = document.getElementById("infoModalBackdrop");
@@ -152,7 +101,7 @@ const MODE_COPY = {
gameTitle: "Türme-Spielbrett", gameTitle: "Türme-Spielbrett",
boardText: "Platziere Türme so, dass keine zwei dieselbe Spalte teilen.", boardText: "Platziere Türme so, dass keine zwei dieselbe Spalte teilen.",
infoTitle: "Spielregeln und Acht-Türme-Problem", infoTitle: "Spielregeln und Acht-Türme-Problem",
infoRuleText: "Im Türme-Modus duerfen sich Türme nicht in derselben Spalte stehen. Diagonalen spielen hier keine Rolle. Mit einem Klick setzt oder entfernst du eine Figur.", infoRuleText: "Im Türme-Modus dürfen sich Türme nicht in derselben Spalte stehen. Diagonalen spielen hier keine Rolle. Mit einem Klick setzt oder entfernst du eine Figur.",
infoProblemHeading: "Was ist das Acht-Türme-Problem?", infoProblemHeading: "Was ist das Acht-Türme-Problem?",
infoProblemText: "Beim Acht-Türme-Problem suchst du eine Anordnung von acht Türmen auf einem 8x8-Brett, bei der sich keine zwei Türme angreifen. Es ist eine einfachere Variante, um systematische Suche und Konfliktprüfung sichtbar zu machen." infoProblemText: "Beim Acht-Türme-Problem suchst du eine Anordnung von acht Türmen auf einem 8x8-Brett, bei der sich keine zwei Türme angreifen. Es ist eine einfachere Variante, um systematische Suche und Konfliktprüfung sichtbar zu machen."
} }
@@ -183,14 +132,22 @@ const MODE_COPY = {
} }
}; };
const UI_TRANSLATIONS = { const translations = {
de: { de: {
pageTitle: "Dashboard",
navBrand: "8-Damen- und 8-Türme Solver",
navDashboard: "Dashboard", navDashboard: "Dashboard",
navLogout: "Logout",
accountLabel: "Account", accountLabel: "Account",
accountShortcutAria: "Konto und Login-Einstellungen öffnen",
introKicker: "Puzzle-Modus", introKicker: "Puzzle-Modus",
introTitle: "Wähl deinen Modus und löse das Brett auf deine Art.", introTitle: "Wähl deinen Modus und löse das Brett auf deine Art.",
introText: "Wechsle zwischen Damen und Türmen, nutze Animation oder Sofort-Lösung und öffne die Brett-Einstellungen jederzeit ueber den linken Seiten-Tab.", introText: "Wechsle zwischen Damen und Türmen, nutze Animation oder Sofort-Lösung und öffne die Brett-Einstellungen jederzeit ueber den linken Seiten-Tab.",
modeSwitchAria: "Spielmodus",
btnQueens: "Damen",
btnRooks: "Türme",
infoButton: "Regeln & Hilfe", infoButton: "Regeln & Hilfe",
gameStatsAria: "Spielstatus",
stepsLabel: "Schritte", stepsLabel: "Schritte",
timeLabel: "Zeit", timeLabel: "Zeit",
statusLabel: "Status", statusLabel: "Status",
@@ -256,6 +213,11 @@ const UI_TRANSLATIONS = {
historyHeadTime: "Zeit", historyHeadTime: "Zeit",
historyHeadStatus: "Status", historyHeadStatus: "Status",
historyHeadSaved: "Gespeichert", historyHeadSaved: "Gespeichert",
infoModalClose: "Schliessen",
infoModalIntro: "Setze pro Reihe genau eine Figur auf das Brett. Eine gültige Lösung hat keine Konflikte.",
infoModalModeHeading: "So funktioniert der aktuelle Modus",
infoModalTimeHeading: "Was zeigen Zeit und Schritte?",
infoModalTimeText: "Der Schrittzähler erfasst deine Züge auf dem Brett. Die Zeit läuft für das aktuelle Spiel weiter, auch wenn du die Seite neu lädst. Beim Speichern werden Zeit und Schritte zusammen mit dem Save-Point abgelegt.",
pieceListTitle: ({ modeLabel }) => `Positionen der ${modeLabel}`, pieceListTitle: ({ modeLabel }) => `Positionen der ${modeLabel}`,
historyEmpty: "Noch keine gespeicherten Zeiten vorhanden.", historyEmpty: "Noch keine gespeicherten Zeiten vorhanden.",
placedWord: "gesetzt", placedWord: "gesetzt",
@@ -312,16 +274,27 @@ const UI_TRANSLATIONS = {
pieceCount: ({ count }) => `${count} gesetzt`, pieceCount: ({ count }) => `${count} gesetzt`,
pieceEmpty: ({ modeLabel }) => `Noch keine ${modeLabel.toLowerCase()} gesetzt.`, pieceEmpty: ({ modeLabel }) => `Noch keine ${modeLabel.toLowerCase()} gesetzt.`,
saveDefaultName: ({ piece, size, count }) => `${piece} ${size}x${size} - ${count} gesetzt`, saveDefaultName: ({ piece, size, count }) => `${piece} ${size}x${size} - ${count} gesetzt`,
filterQueens: "Damen",
filterRooks: "Türme",
solverStepInfo: ({ currentStep, totalSteps }) => `Schritt ${currentStep} / ${totalSteps}`,
speedValue: ({ speed }) => `${speed} ms`, speedValue: ({ speed }) => `${speed} ms`,
languageSwitchAria: "Sprache wechseln" languageSwitchAria: "Sprache wechseln"
}, },
en: { en: {
pageTitle: "Dashboard",
navBrand: "8 Queens and 8 Rooks Solver",
navDashboard: "Dashboard", navDashboard: "Dashboard",
navLogout: "Logout",
accountLabel: "Account", accountLabel: "Account",
accountShortcutAria: "Open account and login settings",
introKicker: "Puzzle Mode", introKicker: "Puzzle Mode",
introTitle: "Choose your mode and solve the board your way.", introTitle: "Choose your mode and solve the board your way.",
introText: "Switch between queens and rooks, use animation or instant solve, and open board settings anytime from the left side tab.", introText: "Switch between queens and rooks, use animation or instant solve, and open board settings anytime from the left side tab.",
modeSwitchAria: "Game mode",
btnQueens: "Queens",
btnRooks: "Rooks",
infoButton: "Rules & Help", infoButton: "Rules & Help",
gameStatsAria: "Game status",
stepsLabel: "Steps", stepsLabel: "Steps",
timeLabel: "Time", timeLabel: "Time",
statusLabel: "Status", statusLabel: "Status",
@@ -387,6 +360,11 @@ const UI_TRANSLATIONS = {
historyHeadTime: "Time", historyHeadTime: "Time",
historyHeadStatus: "Status", historyHeadStatus: "Status",
historyHeadSaved: "Saved", historyHeadSaved: "Saved",
infoModalClose: "Close",
infoModalIntro: "Place exactly one piece in each row. A valid solution has no conflicts.",
infoModalModeHeading: "How the current mode works",
infoModalTimeHeading: "What do time and steps show?",
infoModalTimeText: "The step counter records your moves on the board. Time keeps running for the current game even if you reload the page. When saving, time and steps are stored together with the save point.",
pieceListTitle: ({ modeLabel }) => `${modeLabel} positions`, pieceListTitle: ({ modeLabel }) => `${modeLabel} positions`,
historyEmpty: "No saved times yet.", historyEmpty: "No saved times yet.",
placedWord: "placed", placedWord: "placed",
@@ -443,6 +421,9 @@ const UI_TRANSLATIONS = {
pieceCount: ({ count }) => `${count} placed`, pieceCount: ({ count }) => `${count} placed`,
pieceEmpty: ({ modeLabel }) => `No ${modeLabel.toLowerCase()} placed yet.`, pieceEmpty: ({ modeLabel }) => `No ${modeLabel.toLowerCase()} placed yet.`,
saveDefaultName: ({ piece, size, count }) => `${piece} ${size}x${size} - ${count} placed`, saveDefaultName: ({ piece, size, count }) => `${piece} ${size}x${size} - ${count} placed`,
filterQueens: "Queens",
filterRooks: "Rooks",
solverStepInfo: ({ currentStep, totalSteps }) => `Step ${currentStep} / ${totalSteps}`,
speedValue: ({ speed }) => `${speed} ms`, speedValue: ({ speed }) => `${speed} ms`,
languageSwitchAria: "Switch language" languageSwitchAria: "Switch language"
} }
@@ -486,7 +467,7 @@ function detectBrowserLanguage() {
} }
function t(key, params = {}) { function t(key, params = {}) {
const value = UI_TRANSLATIONS[currentLanguage][key]; const value = translations[currentLanguage][key];
return typeof value === "function" ? value(params) : value; return typeof value === "function" ? value(params) : value;
} }
@@ -494,6 +475,69 @@ function getDefaultSolverMessage() {
return t("defaultSolverMessage"); 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) { function getModeCopy(selectedMode = mode, language = currentLanguage) {
return MODE_COPY[language][selectedMode === "rooks" ? "rooks" : "queens"]; return MODE_COPY[language][selectedMode === "rooks" ? "rooks" : "queens"];
} }
@@ -644,85 +688,6 @@ function updateSolvedCelebration() {
solvedCelebration.hidden = false; solvedCelebration.hidden = false;
} }
function applyLanguageContent() {
document.documentElement.lang = currentLanguage;
navDashboardLink.textContent = t("navDashboard");
accountLabel.textContent = t("accountLabel");
introKicker.textContent = t("introKicker");
introTitle.textContent = t("introTitle");
introText.textContent = t("introText");
infoButton.textContent = t("infoButton");
infoButton.setAttribute("aria-label", t("infoButton"));
stepsLabel.textContent = t("stepsLabel");
timeLabel.textContent = t("timeLabel");
statusLabel.textContent = t("statusLabel");
solverKicker.textContent = t("solverKicker");
solverTitle.textContent = t("solverTitle");
animationSolverTitle.textContent = t("animationSolverTitle");
animationSolverText.textContent = t("animationSolverText");
instantSolverTitle.textContent = t("instantSolverTitle");
instantSolverText.textContent = t("instantSolverText");
solverSpeedLabel.textContent = t("solverSpeedLabel");
btnNextStep.textContent = t("btnNextStep");
btnInstantSolve.textContent = t("btnInstantSolve");
btnReset.textContent = t("btnReset");
settingsSidebarTitle.textContent = t("settingsSidebarTitle");
settingsSidebarClose.textContent = t("settingsSidebarClose");
settingsSidebarText.textContent = t("settingsSidebarText");
boardSizeInputLabel.textContent = t("boardSizeInputLabel");
applyBoardSizeButton.textContent = t("applyBoardSizeButton");
settingsSidebarHint.textContent = t("settingsSidebarHint");
btnSave.textContent = t("btnSave");
btnLoad.textContent = t("btnLoad");
saveSidebarTitle.textContent = t("saveSidebarTitle");
saveSidebarClose.textContent = t("saveSidebarClose");
saveSidebarText.textContent = t("saveSidebarText");
saveNameLabel.textContent = t("saveNameLabel");
saveNameInput.placeholder = t("saveNamePlaceholder");
saveNoteLabel.textContent = t("saveNoteLabel");
saveNoteInput.placeholder = t("saveNotePlaceholder");
saveFavoriteLabel.textContent = t("saveFavoriteLabel");
saveNowButton.textContent = t("saveNowButton");
savePointsSidebarTitle.textContent = t("savePointsSidebarTitle");
savePointsSidebarClose.textContent = t("savePointsSidebarClose");
savePointsSidebarText.textContent = t("savePointsSidebarText");
savePointModeFilterLabel.textContent = t("savePointModeFilterLabel");
savePointSizeFilterLabel.textContent = t("savePointSizeFilterLabel");
savePointSortFilterLabel.textContent = t("savePointSortFilterLabel");
savePointFavoritesOnlyLabel.textContent = t("savePointFavoritesOnlyLabel");
savePointsEmptyState.textContent = t("savePointsEmptyState");
historyKicker.textContent = t("historyKicker");
historyTitle.textContent = t("historyTitle");
historySubtitle.textContent = t("historySubtitle");
btnRefreshHistory.textContent = t("btnRefreshHistory");
historyHeadSave.textContent = t("historyHeadSave");
historyHeadMode.textContent = t("historyHeadMode");
historyHeadBoard.textContent = t("historyHeadBoard");
historyHeadSteps.textContent = t("historyHeadSteps");
historyHeadTime.textContent = t("historyHeadTime");
historyHeadStatus.textContent = t("historyHeadStatus");
historyHeadSaved.textContent = t("historyHeadSaved");
savePointModeFilter.options[0].textContent = t("filterAll");
savePointModeFilter.options[1].textContent = getModeCopy("queens").plural;
savePointModeFilter.options[2].textContent = getModeCopy("rooks").plural;
savePointSizeFilter.options[0].textContent = t("filterAll");
savePointSortFilter.options[0].textContent = t("filterNewest");
savePointSortFilter.options[1].textContent = t("filterOldest");
savePointSortFilter.options[2].textContent = t("filterUpdated");
savePointSortFilter.options[3].textContent = t("filterFavorites");
langSwitch.textContent = currentLanguage.toUpperCase();
langSwitch.setAttribute("aria-label", t("languageSwitchAria"));
langSwitch.title = currentLanguage === "de" ? "Switch to English" : "Zu Deutsch wechseln";
updateModeContent();
updateQueenList();
updateProgressDisplay();
updateSolverSpeed(solverSpeedMs);
updateSolverControls();
}
function updateSettingsTrigger(isOpen = settingsSidebar.classList.contains("open")) { function updateSettingsTrigger(isOpen = settingsSidebar.classList.contains("open")) {
settingsTrigger.setAttribute("aria-expanded", String(isOpen)); settingsTrigger.setAttribute("aria-expanded", String(isOpen));
settingsTriggerArrow.textContent = isOpen ? "←" : "→"; settingsTriggerArrow.textContent = isOpen ? "←" : "→";
@@ -814,7 +779,10 @@ function updateSolverControls() {
btnPlayPause.textContent = t("btnPlayPause"); btnPlayPause.textContent = t("btnPlayPause");
} }
solverStepInfo.textContent = `Schritt ${currentStep} / ${solverSteps.length}`; solverStepInfo.textContent = t("solverStepInfo", {
currentStep,
totalSteps: solverSteps.length
});
} }
function discardSolverTrace(message = getDefaultSolverMessage()) { function discardSolverTrace(message = getDefaultSolverMessage()) {
@@ -1682,11 +1650,10 @@ function restoreSessionFromStorage() {
} }
async function initializeDashboard() { async function initializeDashboard() {
currentLanguage = normalizeLanguage(getStoredLanguage() || detectBrowserLanguage());
pendingBoardSize = boardSize; pendingBoardSize = boardSize;
updateModeButtons(); updateModeButtons();
updateBoardSizeDisplays(boardSize); updateBoardSizeDisplays(boardSize);
applyLanguageContent(); setLanguage(getStoredLanguage() || detectBrowserLanguage());
updateSettingsTrigger(false); updateSettingsTrigger(false);
setSolverMessage(t("solverReady", { modeLabel: getModeLabel(mode) })); setSolverMessage(t("solverReady", { modeLabel: getModeLabel(mode) }));
@@ -1701,9 +1668,7 @@ async function initializeDashboard() {
if (langSwitch) { if (langSwitch) {
langSwitch.addEventListener("click", async () => { langSwitch.addEventListener("click", async () => {
currentLanguage = currentLanguage === "de" ? "en" : "de"; setLanguage(currentLanguage === "de" ? "en" : "de");
storeLanguage(currentLanguage);
applyLanguageContent();
resetSaveForm(); resetSaveForm();
await refreshTimeHistory(); await refreshTimeHistory();
+3 -3
View File
@@ -30,13 +30,13 @@ const translations = {
"hero.challengeValue": "Zeit und Schritte messen", "hero.challengeValue": "Zeit und Schritte messen",
"problem.eyebrow": "Warum dieses Problem spannend ist", "problem.eyebrow": "Warum dieses Problem spannend ist",
"problem.title": "Die Acht-Damen- und Acht-Türme-Probleme sind mehr als Schachrätsel.", "problem.title": "Die Acht-Damen- und Acht-Türme-Probleme sind mehr als Schachrätsel.",
"problem.text": "Beim Acht-Damen-Problem suchst du eine Anordnung von acht Damen auf einem Schachbrett, sodass sich keine zwei Damen schlagen. Beim Acht-Tuerme-Problem gilt dieselbe Idee für Türme, nur mit einfacheren Konfliktregeln. Beide Aufgaben zeigen, wie Mustererkennung, systematische Suche und Backtracking funktionieren.<br><br>Gerade beim Damenproblem wird schnell sichtbar, warum Strategie wichtig ist: Es gibt unzählige moegliche Platzierungen, aber nur sehr wenige wirklich gültige Loesungen. Genau das macht diese App spannend, weil man nicht nur das Ergebnis sieht, sondern auch den Weg dorthin.", "problem.text": "Beim Acht-Damen-Problem suchst du eine Anordnung von acht Damen auf einem Schachbrett, sodass sich keine zwei Damen schlagen. Beim Acht-Tuerme-Problem gilt dieselbe Idee für Türme, nur mit einfacheren Konfliktregeln. Beide Aufgaben zeigen, wie Mustererkennung, systematische Suche und Backtracking funktionieren.<br><br>Gerade beim Damenproblem wird schnell sichtbar, warum Strategie wichtig ist: Es gibt unzählige moegliche Platzierungen, aber nur sehr wenige wirklich gültige Lösungen. Genau das macht diese App spannend, weil man nicht nur das Ergebnis sieht, sondern auch den Weg dorthin.",
"features.eyebrow": "Was die Applikation bietet", "features.eyebrow": "Was die Applikation bietet",
"features.title": "Von der Idee bis zur gelösten Stellung.", "features.title": "Von der Idee bis zur gelösten Stellung.",
"features.card1Title": "Interaktives Brett", "features.card1Title": "Interaktives Brett",
"features.card1Text": "Setze Damen oder Türme direkt auf dem Brett und prüfe, ob deine Anordnung konfliktfrei ist.", "features.card1Text": "Setze Damen oder Türme direkt auf dem Brett und prüfe, ob deine Anordnung konfliktfrei ist.",
"features.card2Title": "Solver mit Animation", "features.card2Title": "Solver mit Animation",
"features.card2Text": "Beobachte den Loesungsweg Schritt fuer Schritt und verstehe, wie Backtracking arbeitet.", "features.card2Text": "Beobachte den Lösungsweg Schritt fuer Schritt und verstehe, wie Backtracking arbeitet.",
"features.card3Title": "Zeit und Schritte", "features.card3Title": "Zeit und Schritte",
"features.card3Text": "Miss deinen Fortschritt während des Spiels und vergleiche unterschiedliche Versuche.", "features.card3Text": "Miss deinen Fortschritt während des Spiels und vergleiche unterschiedliche Versuche.",
"features.card4Title": "Save-Points", "features.card4Title": "Save-Points",
@@ -49,7 +49,7 @@ const translations = {
"journey.step1Text": "Registriere dich und erhalte Zugang zu Dashboard, Save-Points und deinem Fortschritt.", "journey.step1Text": "Registriere dich und erhalte Zugang zu Dashboard, Save-Points und deinem Fortschritt.",
"journey.step2Title": "Spielmodus wählen", "journey.step2Title": "Spielmodus wählen",
"journey.step2Text": "Arbeite mit Damen oder Türmen und passe die Brettgrösse deinem Ziel an.", "journey.step2Text": "Arbeite mit Damen oder Türmen und passe die Brettgrösse deinem Ziel an.",
"journey.step3Title": "Loesen oder analysieren", "journey.step3Title": "Lösen oder analysieren",
"journey.step3Text": "Teste eigene Ideen oder lass dir die Lösung visuell vom Solver lösen lassen, dabei wird 1:1 gezeigt wie der Solver vorgeht.", "journey.step3Text": "Teste eigene Ideen oder lass dir die Lösung visuell vom Solver lösen lassen, dabei wird 1:1 gezeigt wie der Solver vorgeht.",
"cta.eyebrow": "Bereit für den ersten Versuch?", "cta.eyebrow": "Bereit für den ersten Versuch?",
"cta.title": "Mach aus einem bekannten Problem ein sichtbares Lernerlebnis.", "cta.title": "Mach aus einem bekannten Problem ein sichtbares Lernerlebnis.",
+224 -3
View File
@@ -1,3 +1,224 @@
const SUPPORTED_LANGUAGES = ["de", "en"];
const DEFAULT_LANGUAGE = "de";
const LANGUAGE_STORAGE_KEY = "preferredLanguage";
const langSwitch = document.getElementById("langSwitch");
const translations = {
de: {
pageTitle: "Login-Einstellungen",
navBrand: "8-Damen- und 8-Türme Solver",
navDashboard: "Dashboard",
navSettings: "Login-Einstellungen",
navLogout: "Logout",
backToDashboard: "Zurück zum Dashboard",
backToDashboardAria: "Zurück zum Dashboard",
languageSwitchAria: "Sprache wechseln",
settingsTitle: "Login-Einstellungen",
settingsSubtitle: "Verwalte die Authenticator-App-2FA und dein Passwort, ohne den Registrierungsablauf zu ändern.",
twoFactorTitle: "Authenticator-App-2FA",
twoFactorEnabledHint: "2FA ist für dieses Konto aktuell aktiviert.",
twoFactorEnabledAppHint: "Nutze Google Authenticator, Microsoft Authenticator, Authy oder eine andere TOTP-App, um deine 6-stelligen Login-Codes zu generieren.",
disableTwoFactorButton: "2FA deaktivieren",
disableTwoFactorLoading: "2FA wird deaktiviert...",
finishSetupTitle: "2FA-Einrichtung abschließen",
finishSetupHint: "Scanne diesen QR-Code mit deiner Authenticator-App und gib danach den aktuellen 6-stelligen Code zur Bestätigung ein.",
scanFallbackHint: "Wenn das Scannen nicht funktioniert, kannst du den manuellen Einrichtungsschlüssel unten verwenden.",
manualSetupKey: "Manueller Einrichtungsschlüssel",
setupUriLabel: "Einrichtungs-URI",
setupUriHint: "Diese URI wird für den QR-Setup verwendet und kann als Fallback für kompatible Apps kopiert werden.",
authenticatorCodeLabel: "Authenticator-Code",
confirmAndEnableButton: "Bestätigen und aktivieren",
confirmAndEnableLoading: "2FA wird aktiviert...",
cancelSetupTitle: "Einrichtung abbrechen",
cancelSetupHint: "Brich die aktuelle Authenticator-Einrichtung ab, wenn du mit einem neuen Schlüssel neu starten möchtest.",
cancelSetupButton: "Einrichtung abbrechen",
startSetupHint: "Schütze dein Konto mit 6-stelligen Codes aus einer Authenticator-App.",
startSetupHintFollowup: "Nach dem Start der Einrichtung erhältst du einen QR-Code zum Scannen und bestätigst dann einmal mit einem aktuellen Code.",
startSetupButton: "Einrichtung starten",
startSetupLoading: "Einrichtung wird gestartet...",
changePasswordTitle: "Passwort ändern",
currentPasswordLabel: "Aktuelles Passwort",
newPasswordLabel: "Neues Passwort",
confirmPasswordLabel: "Neues Passwort bestätigen",
updatePasswordButton: "Passwort aktualisieren",
updatePasswordLoading: "Passwort wird aktualisiert...",
qrLoadError: "QR-Vorschau konnte nicht geladen werden."
},
en: {
pageTitle: "Login Settings",
navBrand: "8-Damen- und 8-Türme Solver",
navDashboard: "Dashboard",
navSettings: "Login settings",
navLogout: "Logout",
backToDashboard: "Back to dashboard",
backToDashboardAria: "Back to dashboard",
languageSwitchAria: "Switch language",
settingsTitle: "Login settings",
settingsSubtitle: "Manage authenticator app 2FA and your password without changing the registration flow.",
twoFactorTitle: "Authenticator app 2FA",
twoFactorEnabledHint: "2FA is currently enabled for this account.",
twoFactorEnabledAppHint: "Use Google Authenticator, Microsoft Authenticator, Authy, or another TOTP app to generate your 6-digit login codes.",
disableTwoFactorButton: "Disable 2FA",
disableTwoFactorLoading: "Disabling 2FA...",
finishSetupTitle: "Finish 2FA setup",
finishSetupHint: "Scan this QR code with your authenticator app, then enter the current 6-digit code to confirm.",
scanFallbackHint: "If scanning does not work, you can still use the manual setup key below.",
manualSetupKey: "Manual setup key",
setupUriLabel: "Setup URI",
setupUriHint: "This URI is used to generate the QR setup and can be copied as a fallback for compatible apps.",
authenticatorCodeLabel: "Authenticator code",
confirmAndEnableButton: "Confirm and enable",
confirmAndEnableLoading: "Enabling 2FA...",
cancelSetupTitle: "Cancel setup",
cancelSetupHint: "Cancel the current authenticator setup if you want to restart with a new secret.",
cancelSetupButton: "Cancel setup",
startSetupHint: "Protect your account with 6-digit codes from an authenticator app.",
startSetupHintFollowup: "After starting setup, you will get a QR code to scan and then confirm once with a live code.",
startSetupButton: "Start setup",
startSetupLoading: "Starting setup...",
changePasswordTitle: "Change password",
currentPasswordLabel: "Current password",
newPasswordLabel: "New password",
confirmPasswordLabel: "Confirm new password",
updatePasswordButton: "Update password",
updatePasswordLoading: "Updating password...",
qrLoadError: "QR preview could not be loaded."
}
};
const serverMessageTranslations = {
de: {
"Authenticator app 2FA is already enabled.": "Authenticator-App-2FA ist bereits aktiviert.",
"Add the secret to your authenticator app, then confirm it with the current 6-digit code.": "Füge den Schlüssel zu deiner Authenticator-App hinzu und bestätige ihn dann mit dem aktuellen 6-stelligen Code.",
"Start the authenticator setup first.": "Starte zuerst die Authenticator-Einrichtung.",
"Invalid authenticator code. Please try the current code again.": "Ungültiger Authenticator-Code. Bitte versuche den aktuellen Code erneut.",
"Authenticator app 2FA enabled.": "Authenticator-App-2FA wurde aktiviert.",
"Authenticator app setup cancelled.": "Die Einrichtung der Authenticator-App wurde abgebrochen.",
"Authenticator app 2FA disabled.": "Authenticator-App-2FA wurde deaktiviert.",
"Your current password is incorrect.": "Dein aktuelles Passwort ist falsch.",
"The new passwords do not match.": "Die neuen Passwörter stimmen nicht überein.",
"Password updated successfully.": "Das Passwort wurde erfolgreich aktualisiert.",
"Unknown settings action.": "Unbekannte Einstellungsaktion."
},
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);
}
function t(key) {
return translations[currentLanguage]?.[key] ?? translations.en[key] ?? key;
}
function translateServerMessage(message) {
const originalMessage = String(message || "").trim();
if (!originalMessage || currentLanguage === "en") {
return originalMessage;
}
const exactMatch = serverMessageTranslations[currentLanguage]?.[originalMessage];
if (exactMatch) {
return exactMatch;
}
const minLengthMatch = originalMessage.match(/^Password must be at least (\d+) characters long\.$/);
if (minLengthMatch) {
return `Das Passwort muss mindestens ${minLengthMatch[1]} Zeichen lang sein.`;
}
const maxLengthMatch = originalMessage.match(/^Password must be at most (\d+) characters long\.$/);
if (maxLengthMatch) {
return `Das Passwort darf höchstens ${maxLengthMatch[1]} Zeichen lang sein.`;
}
return originalMessage;
}
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));
});
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;
}
});
document.querySelectorAll(".message").forEach((element) => {
if (!element.dataset.originalText) {
element.dataset.originalText = element.textContent.trim();
}
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";
}
}
function setLanguage(language) {
currentLanguage = normalizeLanguage(language);
applyTextTranslations();
storeLanguage(currentLanguage);
}
setLanguage(getStoredLanguage() || detectBrowserLanguage());
if (langSwitch) {
langSwitch.addEventListener("click", () => {
setLanguage(currentLanguage === "de" ? "en" : "de");
});
}
const magneticButtons = document.querySelectorAll(".magnetic"); const magneticButtons = document.querySelectorAll(".magnetic");
magneticButtons.forEach((button) => { magneticButtons.forEach((button) => {
@@ -37,8 +258,8 @@ document.querySelectorAll("form").forEach((form) => {
if (loadingLabel) { if (loadingLabel) {
submitter.dataset.loadingLabelApplied = "true"; submitter.dataset.loadingLabelApplied = "true";
submitter.dataset.originalLabel = submitter.innerText; submitter.dataset.originalLabel = submitter.textContent;
submitter.innerText = loadingLabel; submitter.textContent = loadingLabel;
submitter.style.opacity = "0.7"; submitter.style.opacity = "0.7";
} }
}); });
@@ -52,7 +273,7 @@ document.querySelectorAll(".qr-code[data-qr-value]").forEach((container) => {
} }
if (typeof window.QRCode !== "function") { if (typeof window.QRCode !== "function") {
container.textContent = "QR preview could not be loaded."; container.textContent = t("qrLoadError");
return; return;
} }
+87 -78
View File
@@ -4,7 +4,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard</title> <title data-i18n="pageTitle">Dashboard</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}">
</head> </head>
@@ -13,26 +13,27 @@
<nav class="dashboard-nav"> <nav class="dashboard-nav">
<a class="brand" href="{{ url_for('main.home') }}"> <a class="brand" href="{{ url_for('main.home') }}">
<span class="brand-mark">♛</span> <span class="brand-mark">♛</span>
<span>8-Damen- und 8-Türme Solver</span> <span data-i18n="navBrand">8-Damen- und 8-Türme Solver</span>
</a> </a>
<div class="nav-actions"> <div class="nav-actions">
<a class="nav-link" href="{{ url_for('main.dashboard') }}" id="navDashboardLink">Dashboard</a> <a class="nav-link" href="{{ url_for('main.dashboard') }}" id="navDashboardLink"
data-i18n="navDashboard">Dashboard</a>
<button class="nav-link-button lang-switch" id="langSwitch" type="button" <button class="nav-link-button lang-switch" id="langSwitch" type="button"
aria-label="Sprache wechseln">DE</button> aria-label="Sprache wechseln">DE</button>
<a class="account-shortcut" href="{{ url_for('login.login_settings') }}" <a class="account-shortcut" href="{{ url_for('login.login_settings') }}" data-i18n-aria-label="accountShortcutAria"
aria-label="Konto und Login-Einstellungen öffnen"> aria-label="Konto und Login-Einstellungen öffnen">
<span class="account-avatar" aria-hidden="true">{{ current_user.username[:1]|upper }}</span> <span class="account-avatar" aria-hidden="true">{{ current_user.username[:1]|upper }}</span>
<span class="account-copy"> <span class="account-copy">
<span class="account-label" id="accountLabel">Account</span> <span class="account-label" id="accountLabel" data-i18n="accountLabel">Account</span>
<span class="account-name">{{ current_user.username }}</span> <span class="account-name">{{ current_user.username }}</span>
</span> </span>
</a> </a>
<form action="{{ url_for('login.logout') }}" method="post" class="nav-logout-form"> <form action="{{ url_for('login.logout') }}" method="post" class="nav-logout-form">
<button class="nav-link-button" type="submit">Logout</button> <button class="nav-link-button" type="submit" data-i18n="navLogout">Logout</button>
</form> </form>
</div> </div>
</nav> </nav>
@@ -55,16 +56,22 @@
{% endwith %} {% endwith %}
<section class="dashboard-intro card"> <section class="dashboard-intro card">
<p class="section-kicker" id="introKicker">Puzzle-Modus</p> <p class="section-kicker" id="introKicker" data-i18n="introKicker">Puzzle-Modus</p>
<h1 id="introTitle">Wähl deinen Modus und löse das Brett auf deine Art.</h1> <h1 id="introTitle" data-i18n="introTitle">Wähl deinen Modus und löse das Brett auf deine Art.</h1>
<p class="dashboard-intro-text" id="introText"> <p class="dashboard-intro-text" id="introText" data-i18n="introText">
Wechsle zwischen Damen und Türmen, nutze Animation oder Sofort-Lösung und öffne die Wechsle zwischen Damen und Türmen, nutze Animation oder Sofort-Lösung und öffne die
Brett-Einstellungen jederzeit über den linken Seiten-Tab. Brett-Einstellungen jederzeit über den linken Seiten-Tab.
</p> </p>
<div class="mode-switch" aria-label="Spielmodus"> <div class="mode-switch" data-i18n-aria-label="modeSwitchAria" aria-label="Spielmodus">
<button class="btn active" onclick="setMode('queens')" id="btnQueens" type="button">♛ Damen</button> <button class="btn active" onclick="setMode('queens')" id="btnQueens" type="button">
<button class="btn secondary" onclick="setMode('rooks')" id="btnRooks" type="button">♜ Tuerme</button> <span aria-hidden="true">♛</span>
<span data-i18n="btnQueens">Damen</span>
</button>
<button class="btn secondary" onclick="setMode('rooks')" id="btnRooks" type="button">
<span aria-hidden="true">♜</span>
<span data-i18n="btnRooks">Türme</span>
</button>
</div> </div>
</section> </section>
@@ -79,6 +86,7 @@
</div> </div>
<button class="info-button" id="infoButton" onclick="toggleInfoModal(true)" type="button" <button class="info-button" id="infoButton" onclick="toggleInfoModal(true)" type="button"
data-i18n="infoButton" data-i18n-aria-label="infoButton"
aria-label="Spielregeln und Hilfe anzeigen">Regeln & Hilfe</button> aria-label="Spielregeln und Hilfe anzeigen">Regeln & Hilfe</button>
</div> </div>
@@ -87,17 +95,17 @@
<span id="boardSizeValue">8</span> x <span id="boardSizeValueMirror">8</span> <span id="boardSizeValue">8</span> x <span id="boardSizeValueMirror">8</span>
</p> </p>
<div class="game-stats" aria-label="Spielstatus"> <div class="game-stats" data-i18n-aria-label="gameStatsAria" aria-label="Spielstatus">
<article class="game-stat-card"> <article class="game-stat-card">
<p class="game-stat-label" id="stepsLabel">Schritte</p> <p class="game-stat-label" id="stepsLabel" data-i18n="stepsLabel">Schritte</p>
<p class="game-stat-value" id="stepCounterValue">0</p> <p class="game-stat-value" id="stepCounterValue">0</p>
</article> </article>
<article class="game-stat-card"> <article class="game-stat-card">
<p class="game-stat-label" id="timeLabel">Zeit</p> <p class="game-stat-label" id="timeLabel" data-i18n="timeLabel">Zeit</p>
<p class="game-stat-value" id="timerValue">00:00:00</p> <p class="game-stat-value" id="timerValue">00:00:00</p>
</article> </article>
<article class="game-stat-card"> <article class="game-stat-card">
<p class="game-stat-label" id="statusLabel">Status</p> <p class="game-stat-label" id="statusLabel" data-i18n="statusLabel">Status</p>
<p class="game-stat-value game-stat-status" id="gameProgressState">Läuft</p> <p class="game-stat-value game-stat-status" id="gameProgressState">Läuft</p>
</article> </article>
</div> </div>
@@ -112,20 +120,21 @@
<div class="board" id="board"></div> <div class="board" id="board"></div>
<div class="controls"> <div class="controls">
<button class="btn secondary" onclick="resetBoard()" id="btnReset" type="button">Brett <button class="btn secondary" onclick="resetBoard()" id="btnReset" type="button" data-i18n="btnReset">Brett
zuruecksetzen</button> zuruecksetzen</button>
<div class="controls-save-row"> <div class="controls-save-row">
<button class="btn" onclick="openSavePanel()" id="btnSave" type="button">Speichern</button> <button class="btn" onclick="openSavePanel()" id="btnSave" type="button"
data-i18n="btnSave">Speichern</button>
<button class="btn secondary" onclick="openSavePointsPanel()" id="btnLoad" <button class="btn secondary" onclick="openSavePointsPanel()" id="btnLoad"
type="button">Save-Points</button> type="button" data-i18n="btnLoad">Save-Points</button>
</div> </div>
</div> </div>
<div class="solver-panel"> <div class="solver-panel">
<div class="solver-panel-header"> <div class="solver-panel-header">
<div> <div>
<p class="section-kicker" id="solverKicker">Solver</p> <p class="section-kicker" id="solverKicker" data-i18n="solverKicker">Solver</p>
<h3 id="solverTitle">Lösungswerkzeuge</h3> <h3 id="solverTitle" data-i18n="solverTitle">Lösungswerkzeuge</h3>
<p class="solver-message" id="solverMessage">Bereite einen Ablauf vor und spiele ihn Schritt <p class="solver-message" id="solverMessage">Bereite einen Ablauf vor und spiele ihn Schritt
für Schritt ab.</p> für Schritt ab.</p>
</div> </div>
@@ -134,24 +143,24 @@
<div class="solver-groups"> <div class="solver-groups">
<section class="solver-group"> <section class="solver-group">
<h4 id="animationSolverTitle">Animations-Solver</h4> <h4 id="animationSolverTitle" data-i18n="animationSolverTitle">Animations-Solver</h4>
<p class="solver-group-text" id="animationSolverText"> <p class="solver-group-text" id="animationSolverText" data-i18n="animationSolverText">
Bereite den Lösungsweg vor, gehe ihn manüll Schritt für Schritt durch oder spiele die Bereite den Lösungsweg vor, gehe ihn manüll Schritt für Schritt durch oder spiele die
Animation automatisch ab. Animation automatisch ab.
</p> </p>
<div class="solver-controls"> <div class="solver-controls">
<button class="btn" onclick="prepareSolutionTrace()" id="btnPrepareSolve" <button class="btn" onclick="prepareSolutionTrace()" id="btnPrepareSolve"
type="button">Animation vorbereiten</button> type="button" data-i18n="btnPrepareSolve">Animation vorbereiten</button>
<button class="btn secondary" onclick="nextSolverStep()" id="btnNextStep" <button class="btn secondary" onclick="nextSolverStep()" id="btnNextStep"
type="button">Nächster Schritt</button> type="button" data-i18n="btnNextStep">Nächster Schritt</button>
<button class="btn secondary" onclick="toggleSolverPlayback()" id="btnPlayPause" <button class="btn secondary" onclick="toggleSolverPlayback()" id="btnPlayPause"
type="button">Animation abspielen</button> type="button" data-i18n="btnPlayPause">Animation abspielen</button>
</div> </div>
<div class="solver-speed"> <div class="solver-speed">
<label class="solver-speed-label" id="solverSpeedLabel" <label class="solver-speed-label" id="solverSpeedLabel"
for="solverSpeedInput">Animationsgeschwindigkeit</label> for="solverSpeedInput" data-i18n="solverSpeedLabel">Animationsgeschwindigkeit</label>
<input id="solverSpeedInput" type="range" min="80" max="900" step="20" value="320" <input id="solverSpeedInput" type="range" min="80" max="900" step="20" value="320"
oninput="updateSolverSpeed(this.value)"> oninput="updateSolverSpeed(this.value)">
<span class="solver-speed-value" id="solverSpeedValue">320 ms</span> <span class="solver-speed-value" id="solverSpeedValue">320 ms</span>
@@ -159,14 +168,14 @@
</section> </section>
<section class="solver-group instant-solver-group"> <section class="solver-group instant-solver-group">
<h4 id="instantSolverTitle">Sofort-Lösung</h4> <h4 id="instantSolverTitle" data-i18n="instantSolverTitle">Sofort-Lösung</h4>
<p class="solver-group-text" id="instantSolverText"> <p class="solver-group-text" id="instantSolverText" data-i18n="instantSolverText">
Lade direkt eine gültige Lösung, ohne Zwischenschritte oder Animation. Lade direkt eine gültige Lösung, ohne Zwischenschritte oder Animation.
</p> </p>
<div class="instant-solver-actions"> <div class="instant-solver-actions">
<button class="btn accent" onclick="instantSolve()" id="btnInstantSolve" <button class="btn accent" onclick="instantSolve()" id="btnInstantSolve"
type="button">Sofort lösen</button> type="button" data-i18n="btnInstantSolve">Sofort lösen</button>
</div> </div>
</section> </section>
</div> </div>
@@ -184,32 +193,32 @@
<div class="card"> <div class="card">
<div class="history-header"> <div class="history-header">
<div> <div>
<p class="section-kicker" id="historyKicker">Save-Points</p> <p class="section-kicker" id="historyKicker" data-i18n="historyKicker">Save-Points</p>
<h3 id="historyTitle">Gespeicherte Zeiten und Fortschritte</h3> <h3 id="historyTitle" data-i18n="historyTitle">Gespeicherte Zeiten und Fortschritte</h3>
<p class="history-subtitle" id="historySubtitle">Die Tabelle zeigt gespeicherte Save-Points mit <p class="history-subtitle" id="historySubtitle" data-i18n="historySubtitle">Die Tabelle zeigt gespeicherte Save-Points mit
Zeit, Schritten und Zeit, Schritten und
Status.</p> Status.</p>
</div> </div>
<button class="btn secondary" onclick="refreshTimeHistory()" id="btnRefreshHistory" <button class="btn secondary" onclick="refreshTimeHistory()" id="btnRefreshHistory"
type="button">Aktualisieren</button> type="button" data-i18n="btnRefreshHistory">Aktualisieren</button>
</div> </div>
<div class="history-table-wrapper"> <div class="history-table-wrapper">
<table class="history-table"> <table class="history-table">
<thead> <thead>
<tr> <tr>
<th id="historyHeadSave">Save-Point</th> <th id="historyHeadSave" data-i18n="historyHeadSave">Save-Point</th>
<th id="historyHeadMode">Modus</th> <th id="historyHeadMode" data-i18n="historyHeadMode">Modus</th>
<th id="historyHeadBoard">Brett</th> <th id="historyHeadBoard" data-i18n="historyHeadBoard">Brett</th>
<th id="historyHeadSteps">Schritte</th> <th id="historyHeadSteps" data-i18n="historyHeadSteps">Schritte</th>
<th id="historyHeadTime">Zeit</th> <th id="historyHeadTime" data-i18n="historyHeadTime">Zeit</th>
<th id="historyHeadStatus">Status</th> <th id="historyHeadStatus" data-i18n="historyHeadStatus">Status</th>
<th id="historyHeadSaved">Gespeichert</th> <th id="historyHeadSaved" data-i18n="historyHeadSaved">Gespeichert</th>
</tr> </tr>
</thead> </thead>
<tbody id="timeHistoryTableBody"> <tbody id="timeHistoryTableBody">
<tr> <tr>
<td colspan="7" class="history-empty">Noch keine gespeicherten Zeiten vorhanden.</td> <td colspan="7" class="history-empty" data-i18n="historyEmpty">Noch keine gespeicherten Zeiten vorhanden.</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -219,90 +228,90 @@
<aside class="sidebar settings-sidebar" id="settingsSidebar" aria-hidden="true"> <aside class="sidebar settings-sidebar" id="settingsSidebar" aria-hidden="true">
<div class="sidebar-header"> <div class="sidebar-header">
<h3 id="settingsSidebarTitle">Brett-Einstellungen</h3> <h3 id="settingsSidebarTitle" data-i18n="settingsSidebarTitle">Brett-Einstellungen</h3>
<button class="btn secondary sidebar-close" onclick="toggleSidebar(false)" id="settingsSidebarClose" <button class="btn secondary sidebar-close" onclick="toggleSidebar(false)" id="settingsSidebarClose"
type="button">Schliessen</button> type="button" data-i18n="settingsSidebarClose">Schliessen</button>
</div> </div>
<p id="settingsSidebarText">Passe die Brettgrösse zwischen 4 und 13 an.</p> <p id="settingsSidebarText" data-i18n="settingsSidebarText">Passe die Brettgrösse zwischen 4 und 13 an.</p>
<label class="sidebar-label" id="boardSizeInputLabel" for="boardSizeInput">Grösse</label> <label class="sidebar-label" id="boardSizeInputLabel" for="boardSizeInput" data-i18n="boardSizeInputLabel">Grösse</label>
<input id="boardSizeInput" type="range" min="4" max="13" value="8" oninput="previewBoardSize(this.value)"> <input id="boardSizeInput" type="range" min="4" max="13" value="8" oninput="previewBoardSize(this.value)">
<div class="sidebar-size-row"> <div class="sidebar-size-row">
<strong><span id="sidebarBoardSizeValue">8</span> x <span id="sidebarBoardSizeValueMirror">8</span></strong> <strong><span id="sidebarBoardSizeValue">8</span> x <span id="sidebarBoardSizeValueMirror">8</span></strong>
</div> </div>
<button class="btn" onclick="applyBoardSize()" id="applyBoardSizeButton" type="button">Grösse anwenden</button> <button class="btn" onclick="applyBoardSize()" id="applyBoardSizeButton" type="button" data-i18n="applyBoardSizeButton">Grösse anwenden</button>
<p class="sidebar-hint" id="settingsSidebarHint">Beim Ändern wird das Brett zur Sicherheit zurückgesetzt. Auf <p class="sidebar-hint" id="settingsSidebarHint" data-i18n="settingsSidebarHint">Beim Ändern wird das Brett zur Sicherheit zurückgesetzt. Auf
dem Handy ist 8x8 eingeplant, grössere Bretter können abgeschnitten wirken.</p> dem Handy ist 8x8 eingeplant, grössere Bretter können abgeschnitten wirken.</p>
</aside> </aside>
<aside class="sidebar save-sidebar" id="saveSidebar" aria-hidden="true"> <aside class="sidebar save-sidebar" id="saveSidebar" aria-hidden="true">
<div class="sidebar-header"> <div class="sidebar-header">
<h3 id="saveSidebarTitle">Speicherpunkt</h3> <h3 id="saveSidebarTitle" data-i18n="saveSidebarTitle">Speicherpunkt</h3>
<button class="btn secondary sidebar-close" onclick="toggleSavePanel(false)" id="saveSidebarClose" <button class="btn secondary sidebar-close" onclick="toggleSavePanel(false)" id="saveSidebarClose"
type="button">Schliessen</button> type="button" data-i18n="saveSidebarClose">Schliessen</button>
</div> </div>
<p id="saveSidebarText">Gib dem Spielstand einen klaren Namen und optional eine kurze Notiz.</p> <p id="saveSidebarText" data-i18n="saveSidebarText">Gib dem Spielstand einen klaren Namen und optional eine kurze Notiz.</p>
<label class="sidebar-label" id="saveNameLabel" for="saveNameInput">Name</label> <label class="sidebar-label" id="saveNameLabel" for="saveNameInput" data-i18n="saveNameLabel">Name</label>
<input class="sidebar-input" id="saveNameInput" type="text" maxlength="80" <input class="sidebar-input" id="saveNameInput" type="text" maxlength="80"
placeholder="z. B. Fast gelöste Dame 8x8"> placeholder="z. B. Fast gelöste Dame 8x8" data-i18n-placeholder="saveNamePlaceholder">
<label class="sidebar-label" id="saveNoteLabel" for="saveNoteInput">Notiz</label> <label class="sidebar-label" id="saveNoteLabel" for="saveNoteInput" data-i18n="saveNoteLabel">Notiz</label>
<textarea class="sidebar-input sidebar-textarea" id="saveNoteInput" rows="4" maxlength="240" <textarea class="sidebar-input sidebar-textarea" id="saveNoteInput" rows="4" maxlength="240"
placeholder="z. B. Gute Startposition für Türmetest"></textarea> placeholder="z. B. Gute Startposition für Türmetest" data-i18n-placeholder="saveNotePlaceholder"></textarea>
<label class="sidebar-checkbox"> <label class="sidebar-checkbox">
<input id="saveFavoriteInput" type="checkbox"> <input id="saveFavoriteInput" type="checkbox">
<span id="saveFavoriteLabel">Als Favorit markieren</span> <span id="saveFavoriteLabel" data-i18n="saveFavoriteLabel">Als Favorit markieren</span>
</label> </label>
<button class="btn" onclick="saveGame()" id="saveNowButton" type="button">Jetzt speichern</button> <button class="btn" onclick="saveGame()" id="saveNowButton" type="button" data-i18n="saveNowButton">Jetzt speichern</button>
</aside> </aside>
<aside class="sidebar save-points-sidebar" id="savePointsSidebar" aria-hidden="true"> <aside class="sidebar save-points-sidebar" id="savePointsSidebar" aria-hidden="true">
<div class="sidebar-header"> <div class="sidebar-header">
<h3 id="savePointsSidebarTitle">Save-Points</h3> <h3 id="savePointsSidebarTitle" data-i18n="savePointsSidebarTitle">Save-Points</h3>
<button class="btn secondary sidebar-close" onclick="toggleSavePointsPanel(false)" <button class="btn secondary sidebar-close" onclick="toggleSavePointsPanel(false)"
id="savePointsSidebarClose" type="button">Schliessen</button> id="savePointsSidebarClose" type="button" data-i18n="savePointsSidebarClose">Schliessen</button>
</div> </div>
<p id="savePointsSidebarText">Wähle einen Speicherstand aus der Liste.</p> <p id="savePointsSidebarText" data-i18n="savePointsSidebarText">Wähle einen Speicherstand aus der Liste.</p>
<div class="save-point-filters"> <div class="save-point-filters">
<label class="sidebar-label" id="savePointModeFilterLabel" for="savePointModeFilter">Modus</label> <label class="sidebar-label" id="savePointModeFilterLabel" for="savePointModeFilter" data-i18n="savePointModeFilterLabel">Modus</label>
<select class="sidebar-input" id="savePointModeFilter" onchange="refreshSavePoints()"> <select class="sidebar-input" id="savePointModeFilter" onchange="refreshSavePoints()">
<option value="all">Alle</option> <option value="all" data-i18n="filterAll">Alle</option>
<option value="queens">Damen</option> <option value="queens" data-i18n="filterQueens">Damen</option>
<option value="rooks">Türme</option> <option value="rooks" data-i18n="filterRooks">Türme</option>
</select> </select>
<label class="sidebar-label" id="savePointSizeFilterLabel" for="savePointSizeFilter">Brettgrösse</label> <label class="sidebar-label" id="savePointSizeFilterLabel" for="savePointSizeFilter" data-i18n="savePointSizeFilterLabel">Brettgrösse</label>
<select class="sidebar-input" id="savePointSizeFilter" onchange="refreshSavePoints()"> <select class="sidebar-input" id="savePointSizeFilter" onchange="refreshSavePoints()">
<option value="all">Alle</option> <option value="all" data-i18n="filterAll">Alle</option>
{% for board_size in range(4, 14) %} {% for board_size in range(4, 14) %}
<option value="{{ board_size }}">{{ board_size }} x {{ board_size }}</option> <option value="{{ board_size }}">{{ board_size }} x {{ board_size }}</option>
{% endfor %} {% endfor %}
</select> </select>
<label class="sidebar-label" id="savePointSortFilterLabel" for="savePointSortFilter">Sortierung</label> <label class="sidebar-label" id="savePointSortFilterLabel" for="savePointSortFilter" data-i18n="savePointSortFilterLabel">Sortierung</label>
<select class="sidebar-input" id="savePointSortFilter" onchange="refreshSavePoints()"> <select class="sidebar-input" id="savePointSortFilter" onchange="refreshSavePoints()">
<option value="newest">Neueste zuerst</option> <option value="newest" data-i18n="filterNewest">Neueste zuerst</option>
<option value="oldest">älteste zuerst</option> <option value="oldest" data-i18n="filterOldest">älteste zuerst</option>
<option value="updated">Zuletzt bearbeitet</option> <option value="updated" data-i18n="filterUpdated">Zuletzt bearbeitet</option>
<option value="favorites">Favoriten zuerst</option> <option value="favorites" data-i18n="filterFavorites">Favoriten zuerst</option>
</select> </select>
<label class="sidebar-checkbox"> <label class="sidebar-checkbox">
<input id="savePointFavoritesOnly" type="checkbox" onchange="refreshSavePoints()"> <input id="savePointFavoritesOnly" type="checkbox" onchange="refreshSavePoints()">
<span id="savePointFavoritesOnlyLabel">Nur Favoriten anzeigen</span> <span id="savePointFavoritesOnlyLabel" data-i18n="savePointFavoritesOnlyLabel">Nur Favoriten anzeigen</span>
</label> </label>
</div> </div>
<div class="save-points-list" id="savePointsList"> <div class="save-points-list" id="savePointsList">
<p class="sidebar-hint" id="savePointsEmptyState">Noch keine Speicherstände gefunden.</p> <p class="sidebar-hint" id="savePointsEmptyState" data-i18n="savePointsEmptyState">Noch keine Speicherstände gefunden.</p>
</div> </div>
</aside> </aside>
@@ -315,13 +324,13 @@
<div class="modal-header"> <div class="modal-header">
<h3 id="infoModalTitle">Spielregeln und Acht-Damen-Problem</h3> <h3 id="infoModalTitle">Spielregeln und Acht-Damen-Problem</h3>
<button class="btn secondary sidebar-close" onclick="toggleInfoModal(false)" <button class="btn secondary sidebar-close" onclick="toggleInfoModal(false)"
type="button">Schliessen</button> type="button" data-i18n="infoModalClose">Schliessen</button>
</div> </div>
<div class="modal-content"> <div class="modal-content">
<p>Setze pro Reihe genau eine Figur auf das Brett. Eine gültige Lösung hat keine Konflikte.</p> <p data-i18n="infoModalIntro">Setze pro Reihe genau eine Figur auf das Brett. Eine gültige Lösung hat keine Konflikte.</p>
<h4>So funktioniert der aktuelle Modus</h4> <h4 data-i18n="infoModalModeHeading">So funktioniert der aktuelle Modus</h4>
<p id="infoModeRuleText">Im Damen-Modus dürfen sich Damen weder in derselben Spalte noch diagonal <p id="infoModeRuleText">Im Damen-Modus dürfen sich Damen weder in derselben Spalte noch diagonal
angreifen. Mit einem Klick setzt oder entfernst du eine Figur.</p> angreifen. Mit einem Klick setzt oder entfernst du eine Figur.</p>
@@ -330,8 +339,8 @@
auf einem 8x8-Brett, bei der sich keine zwei Damen schlagen. Es ist ein bekanntes Beispiel für auf einem 8x8-Brett, bei der sich keine zwei Damen schlagen. Es ist ein bekanntes Beispiel für
kombinatorische Suche und Backtracking.</p> kombinatorische Suche und Backtracking.</p>
<h4>Was zeigen Zeit und Schritte?</h4> <h4 data-i18n="infoModalTimeHeading">Was zeigen Zeit und Schritte?</h4>
<p>Der Schrittzähler erfasst deine Züge auf dem Brett. Die Zeit läuft für das aktuelle Spiel weiter, <p data-i18n="infoModalTimeText">Der Schrittzähler erfasst deine Züge auf dem Brett. Die Zeit läuft für das aktuelle Spiel weiter,
auch wenn du die Seite neu lädst. Beim Speichern werden Zeit und Schritte zusammen mit dem auch wenn du die Seite neu lädst. Beim Speichern werden Zeit und Schritte zusammen mit dem
Save-Point abgelegt.</p> Save-Point abgelegt.</p>
</div> </div>
+62 -32
View File
@@ -4,7 +4,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login Settings</title> <title data-i18n="pageTitle">Login Settings</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/login.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/login.css') }}">
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" defer></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" defer></script>
</head> </head>
@@ -13,22 +13,33 @@
<nav class="auth-nav"> <nav class="auth-nav">
<a class="brand" href="{{ url_for('main.home') }}"> <a class="brand" href="{{ url_for('main.home') }}">
<span class="brand-mark">♛</span> <span class="brand-mark">♛</span>
<span>8-Damen- und 8-Tuerme Solver</span> <span data-i18n="navBrand">8-Damen- und 8-Türme Solver</span>
</a> </a>
<ul class="nav-links"> <ul class="nav-links">
<li><a href="{{ url_for('main.dashboard') }}">Dashboard</a></li> <li><a href="{{ url_for('main.dashboard') }}" data-i18n="navDashboard">Dashboard</a></li>
<li><a href="{{ url_for('login.login_settings') }}">Login settings</a></li> <li><a href="{{ url_for('login.login_settings') }}" data-i18n="navSettings">Login settings</a></li>
</ul> </ul>
<form action="{{ url_for('login.logout') }}" method="post" class="nav-form"> <form action="{{ url_for('login.logout') }}" method="post" class="nav-form">
<button type="submit" class="nav-button">Logout</button> <button type="submit" class="nav-button" id="logoutButton" data-i18n="navLogout">Logout</button>
</form> </form>
</nav> </nav>
<main class="auth-shell settings-shell"> <main class="auth-shell settings-shell">
<section class="auth-panel settings-panel"> <section class="auth-panel settings-panel">
<h1>Login settings</h1> <div class="settings-topbar">
<p class="muted">Manage authenticator app 2FA and your password without changing the registration flow.</p> <a class="back-link" href="{{ url_for('main.dashboard') }}" data-i18n-aria-label="backToDashboardAria"
aria-label="Back to dashboard">
<span class="back-link-arrow" aria-hidden="true">&larr;</span>
<span data-i18n="backToDashboard">Back to dashboard</span>
</a>
<button class="nav-button lang-switch" id="langSwitch" type="button" aria-label="Switch language">DE</button>
</div>
<h1 data-i18n="settingsTitle">Login settings</h1>
<p class="muted" data-i18n="settingsSubtitle">Manage authenticator app 2FA and your password without
changing the registration flow.</p>
{% with messages = get_flashed_messages(with_categories=true) %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% if messages %}
@@ -47,76 +58,95 @@
<form action="{{ url_for('login.login_settings') }}" method="post"> <form action="{{ url_for('login.login_settings') }}" method="post">
<input type="hidden" name="action" value="disable_two_factor"> <input type="hidden" name="action" value="disable_two_factor">
<h2>Authenticator app 2FA</h2> <h2 data-i18n="twoFactorTitle">Authenticator app 2FA</h2>
<p class="field-hint">2FA is currently enabled for this account.</p> <p class="field-hint" data-i18n="twoFactorEnabledHint">2FA is currently enabled for this account.</p>
<p class="field-hint">Use Google Authenticator, Microsoft Authenticator, Authy, or another TOTP app to generate your 6-digit login codes.</p> <p class="field-hint" data-i18n="twoFactorEnabledAppHint">Use Google Authenticator, Microsoft
Authenticator, Authy, or another TOTP app
to generate your 6-digit login codes.</p>
<button class="secondary-button" type="submit" data-loading-label="Disabling 2FA...">Disable 2FA</button> <button class="secondary-button" type="submit" data-loading-label="Disabling 2FA..."
data-i18n="disableTwoFactorButton" data-i18n-loading-label="disableTwoFactorLoading">Disable
2FA</button>
</form> </form>
{% elif pending_two_factor_secret %} {% elif pending_two_factor_secret %}
<form action="{{ url_for('login.login_settings') }}" method="post"> <form action="{{ url_for('login.login_settings') }}" method="post">
<input type="hidden" name="action" value="confirm_two_factor_setup"> <input type="hidden" name="action" value="confirm_two_factor_setup">
<h2>Finish 2FA setup</h2> <h2 data-i18n="finishSetupTitle">Finish 2FA setup</h2>
<p class="field-hint">Scan this QR code with your authenticator app, then enter the current 6-digit code to confirm.</p> <p class="field-hint" data-i18n="finishSetupHint">Scan this QR code with your authenticator app,
then enter the current 6-digit
code to confirm.</p>
<div class="qr-panel"> <div class="qr-panel">
<div class="qr-code" data-qr-value="{{ pending_two_factor_uri }}"></div> <div class="qr-code" data-qr-value="{{ pending_two_factor_uri }}"></div>
</div> </div>
<p class="field-hint">If scanning does not work, you can still use the manual setup key below.</p> <p class="field-hint" data-i18n="scanFallbackHint">If scanning does not work, you can still use the
manual setup key below.</p>
<label for="two_factor_secret">Manual setup key</label> <label for="two_factor_secret" data-i18n="manualSetupKey">Manual setup key</label>
<input id="two_factor_secret" type="text" value="{{ pending_two_factor_secret }}" readonly> <input id="two_factor_secret" type="text" value="{{ pending_two_factor_secret }}" readonly>
<label for="two_factor_uri">Setup URI</label> <label for="two_factor_uri" data-i18n="setupUriLabel">Setup URI</label>
<input id="two_factor_uri" type="text" value="{{ pending_two_factor_uri }}" readonly> <input id="two_factor_uri" type="text" value="{{ pending_two_factor_uri }}" readonly>
<p class="field-hint">This URI is used to generate the QR setup and can be copied as a fallback for compatible apps.</p> <p class="field-hint" data-i18n="setupUriHint">This URI is used to generate the QR setup and can be
copied as a fallback for
compatible apps.</p>
<label for="code">Authenticator code</label> <label for="code" data-i18n="authenticatorCodeLabel">Authenticator code</label>
<input id="code" type="text" name="code" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" required> <input id="code" type="text" name="code" inputmode="numeric" pattern="[0-9]{6}" maxlength="6"
required>
<div class="button-row"> <div class="button-row">
<button class="magnetic" type="submit" data-loading-label="Enabling 2FA...">Confirm and enable</button> <button class="magnetic" type="submit" data-loading-label="Enabling 2FA..."
data-i18n="confirmAndEnableButton" data-i18n-loading-label="confirmAndEnableLoading">Confirm and
enable</button>
</div> </div>
</form> </form>
<form action="{{ url_for('login.login_settings') }}" method="post"> <form action="{{ url_for('login.login_settings') }}" method="post">
<input type="hidden" name="action" value="cancel_two_factor_setup"> <input type="hidden" name="action" value="cancel_two_factor_setup">
<h2>Cancel setup</h2> <h2 data-i18n="cancelSetupTitle">Cancel setup</h2>
<p class="field-hint">Cancel the current authenticator setup if you want to restart with a new secret.</p> <p class="field-hint" data-i18n="cancelSetupHint">Cancel the current authenticator setup if you want
to restart with a new
secret.</p>
<button class="secondary-button" type="submit">Cancel setup</button> <button class="secondary-button" type="submit" data-i18n="cancelSetupButton">Cancel setup</button>
</form> </form>
{% else %} {% else %}
<form action="{{ url_for('login.login_settings') }}" method="post"> <form action="{{ url_for('login.login_settings') }}" method="post">
<input type="hidden" name="action" value="start_two_factor_setup"> <input type="hidden" name="action" value="start_two_factor_setup">
<h2>Authenticator app 2FA</h2> <h2 data-i18n="twoFactorTitle">Authenticator app 2FA</h2>
<p class="field-hint">Protect your account with 6-digit codes from an authenticator app.</p> <p class="field-hint" data-i18n="startSetupHint">Protect your account with 6-digit codes from an
<p class="field-hint">After starting setup, you will get a QR code to scan and then confirm once with a live code.</p> authenticator app.</p>
<p class="field-hint" data-i18n="startSetupHintFollowup">After starting setup, you will get a QR code to scan and then confirm once
with a live code.</p>
<button class="magnetic" type="submit" data-loading-label="Starting setup...">Start setup</button> <button class="magnetic" type="submit" data-loading-label="Starting setup..."
data-i18n="startSetupButton" data-i18n-loading-label="startSetupLoading">Start setup</button>
</form> </form>
{% endif %} {% endif %}
<form action="{{ url_for('login.login_settings') }}" method="post"> <form action="{{ url_for('login.login_settings') }}" method="post">
<input type="hidden" name="action" value="password"> <input type="hidden" name="action" value="password">
<h2>Change password</h2> <h2 data-i18n="changePasswordTitle">Change password</h2>
<label for="current_password">Current password</label> <label for="current_password" data-i18n="currentPasswordLabel">Current password</label>
<input id="current_password" type="password" name="current_password" minlength="8" maxlength="128" <input id="current_password" type="password" name="current_password" minlength="8" maxlength="128"
required> required>
<label for="new_password">New password</label> <label for="new_password" data-i18n="newPasswordLabel">New password</label>
<input id="new_password" type="password" name="new_password" minlength="8" maxlength="128" required> <input id="new_password" type="password" name="new_password" minlength="8" maxlength="128" required>
<label for="confirm_password">Confirm new password</label> <label for="confirm_password" data-i18n="confirmPasswordLabel">Confirm new password</label>
<input id="confirm_password" type="password" name="confirm_password" minlength="8" maxlength="128" <input id="confirm_password" type="password" name="confirm_password" minlength="8" maxlength="128"
required> required>
<button class="magnetic" type="submit" data-loading-label="Updating password...">Update password</button> <button class="magnetic" type="submit" data-loading-label="Updating password..."
data-i18n="updatePasswordButton" data-i18n-loading-label="updatePasswordLoading">Update
password</button>
</form> </form>
</div> </div>
</section> </section>