fixed some missing translations and added a back button to login_settings.
This commit is contained in:
@@ -117,6 +117,14 @@ a {
|
||||
|
||||
.nav-form {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #ecf4fb;
|
||||
}
|
||||
|
||||
.auth-shell {
|
||||
@@ -145,6 +153,68 @@ a {
|
||||
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 {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
@@ -344,6 +414,16 @@ button:active {
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.settings-topbar {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.back-link,
|
||||
.lang-switch {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.auth-shell {
|
||||
padding: 22px 12px 28px;
|
||||
}
|
||||
@@ -355,4 +435,4 @@ button:active {
|
||||
form {
|
||||
padding: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+104
-139
@@ -26,13 +26,7 @@ const boardDiv = document.getElementById("board");
|
||||
const statusMessage = document.getElementById("statusMessage");
|
||||
const queenList = document.getElementById("queenList");
|
||||
const pieceCountBadge = document.getElementById("pieceCountBadge");
|
||||
const navDashboardLink = document.getElementById("navDashboardLink");
|
||||
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 gameTitle = document.getElementById("gameTitle");
|
||||
const boardModeText = document.getElementById("boardModeText");
|
||||
@@ -42,9 +36,6 @@ const infoModalTitle = document.getElementById("infoModalTitle");
|
||||
const infoModeRuleText = document.getElementById("infoModeRuleText");
|
||||
const infoModalProblemHeading = document.getElementById("infoModalProblemHeading");
|
||||
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 timerValue = document.getElementById("timerValue");
|
||||
const gameProgressState = document.getElementById("gameProgressState");
|
||||
@@ -57,20 +48,10 @@ const btnPrepareSolve = document.getElementById("btnPrepareSolve");
|
||||
const btnInstantSolve = document.getElementById("btnInstantSolve");
|
||||
const btnNextStep = document.getElementById("btnNextStep");
|
||||
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 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 solverSpeedValue = document.getElementById("solverSpeedValue");
|
||||
const solverSpeedLabel = document.getElementById("solverSpeedLabel");
|
||||
const settingsSidebar = document.getElementById("settingsSidebar");
|
||||
const settingsTrigger = document.getElementById("settingsTrigger");
|
||||
const settingsTriggerArrow = document.getElementById("settingsTriggerArrow");
|
||||
@@ -85,46 +66,14 @@ const boardSizeValue = document.getElementById("boardSizeValue");
|
||||
const boardSizeValueMirror = document.getElementById("boardSizeValueMirror");
|
||||
const sidebarBoardSizeValue = document.getElementById("sidebarBoardSizeValue");
|
||||
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 saveNameInput = document.getElementById("saveNameInput");
|
||||
const saveNoteInput = document.getElementById("saveNoteInput");
|
||||
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 savePointSizeFilter = document.getElementById("savePointSizeFilter");
|
||||
const savePointSortFilter = document.getElementById("savePointSortFilter");
|
||||
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 infoModal = document.getElementById("infoModal");
|
||||
const infoModalBackdrop = document.getElementById("infoModalBackdrop");
|
||||
@@ -152,7 +101,7 @@ const MODE_COPY = {
|
||||
gameTitle: "Türme-Spielbrett",
|
||||
boardText: "Platziere Türme so, dass keine zwei dieselbe Spalte teilen.",
|
||||
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?",
|
||||
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: {
|
||||
pageTitle: "Dashboard",
|
||||
navBrand: "8-Damen- und 8-Türme Solver",
|
||||
navDashboard: "Dashboard",
|
||||
navLogout: "Logout",
|
||||
accountLabel: "Account",
|
||||
accountShortcutAria: "Konto und Login-Einstellungen öffnen",
|
||||
introKicker: "Puzzle-Modus",
|
||||
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.",
|
||||
modeSwitchAria: "Spielmodus",
|
||||
btnQueens: "Damen",
|
||||
btnRooks: "Türme",
|
||||
infoButton: "Regeln & Hilfe",
|
||||
gameStatsAria: "Spielstatus",
|
||||
stepsLabel: "Schritte",
|
||||
timeLabel: "Zeit",
|
||||
statusLabel: "Status",
|
||||
@@ -256,6 +213,11 @@ const UI_TRANSLATIONS = {
|
||||
historyHeadTime: "Zeit",
|
||||
historyHeadStatus: "Status",
|
||||
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}`,
|
||||
historyEmpty: "Noch keine gespeicherten Zeiten vorhanden.",
|
||||
placedWord: "gesetzt",
|
||||
@@ -312,16 +274,27 @@ const UI_TRANSLATIONS = {
|
||||
pieceCount: ({ count }) => `${count} gesetzt`,
|
||||
pieceEmpty: ({ modeLabel }) => `Noch keine ${modeLabel.toLowerCase()} 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`,
|
||||
languageSwitchAria: "Sprache wechseln"
|
||||
},
|
||||
en: {
|
||||
pageTitle: "Dashboard",
|
||||
navBrand: "8 Queens and 8 Rooks Solver",
|
||||
navDashboard: "Dashboard",
|
||||
navLogout: "Logout",
|
||||
accountLabel: "Account",
|
||||
accountShortcutAria: "Open account and login settings",
|
||||
introKicker: "Puzzle Mode",
|
||||
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.",
|
||||
modeSwitchAria: "Game mode",
|
||||
btnQueens: "Queens",
|
||||
btnRooks: "Rooks",
|
||||
infoButton: "Rules & Help",
|
||||
gameStatsAria: "Game status",
|
||||
stepsLabel: "Steps",
|
||||
timeLabel: "Time",
|
||||
statusLabel: "Status",
|
||||
@@ -387,6 +360,11 @@ const UI_TRANSLATIONS = {
|
||||
historyHeadTime: "Time",
|
||||
historyHeadStatus: "Status",
|
||||
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`,
|
||||
historyEmpty: "No saved times yet.",
|
||||
placedWord: "placed",
|
||||
@@ -443,6 +421,9 @@ const UI_TRANSLATIONS = {
|
||||
pieceCount: ({ count }) => `${count} placed`,
|
||||
pieceEmpty: ({ modeLabel }) => `No ${modeLabel.toLowerCase()} placed yet.`,
|
||||
saveDefaultName: ({ piece, size, count }) => `${piece} ${size}x${size} - ${count} placed`,
|
||||
filterQueens: "Queens",
|
||||
filterRooks: "Rooks",
|
||||
solverStepInfo: ({ currentStep, totalSteps }) => `Step ${currentStep} / ${totalSteps}`,
|
||||
speedValue: ({ speed }) => `${speed} ms`,
|
||||
languageSwitchAria: "Switch language"
|
||||
}
|
||||
@@ -486,7 +467,7 @@ function detectBrowserLanguage() {
|
||||
}
|
||||
|
||||
function t(key, params = {}) {
|
||||
const value = UI_TRANSLATIONS[currentLanguage][key];
|
||||
const value = translations[currentLanguage][key];
|
||||
return typeof value === "function" ? value(params) : value;
|
||||
}
|
||||
|
||||
@@ -494,6 +475,69 @@ 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"];
|
||||
}
|
||||
@@ -644,85 +688,6 @@ function updateSolvedCelebration() {
|
||||
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")) {
|
||||
settingsTrigger.setAttribute("aria-expanded", String(isOpen));
|
||||
settingsTriggerArrow.textContent = isOpen ? "←" : "→";
|
||||
@@ -814,7 +779,10 @@ function updateSolverControls() {
|
||||
btnPlayPause.textContent = t("btnPlayPause");
|
||||
}
|
||||
|
||||
solverStepInfo.textContent = `Schritt ${currentStep} / ${solverSteps.length}`;
|
||||
solverStepInfo.textContent = t("solverStepInfo", {
|
||||
currentStep,
|
||||
totalSteps: solverSteps.length
|
||||
});
|
||||
}
|
||||
|
||||
function discardSolverTrace(message = getDefaultSolverMessage()) {
|
||||
@@ -1682,11 +1650,10 @@ function restoreSessionFromStorage() {
|
||||
}
|
||||
|
||||
async function initializeDashboard() {
|
||||
currentLanguage = normalizeLanguage(getStoredLanguage() || detectBrowserLanguage());
|
||||
pendingBoardSize = boardSize;
|
||||
updateModeButtons();
|
||||
updateBoardSizeDisplays(boardSize);
|
||||
applyLanguageContent();
|
||||
setLanguage(getStoredLanguage() || detectBrowserLanguage());
|
||||
updateSettingsTrigger(false);
|
||||
setSolverMessage(t("solverReady", { modeLabel: getModeLabel(mode) }));
|
||||
|
||||
@@ -1701,9 +1668,7 @@ async function initializeDashboard() {
|
||||
|
||||
if (langSwitch) {
|
||||
langSwitch.addEventListener("click", async () => {
|
||||
currentLanguage = currentLanguage === "de" ? "en" : "de";
|
||||
storeLanguage(currentLanguage);
|
||||
applyLanguageContent();
|
||||
setLanguage(currentLanguage === "de" ? "en" : "de");
|
||||
resetSaveForm();
|
||||
await refreshTimeHistory();
|
||||
|
||||
|
||||
@@ -30,13 +30,13 @@ const translations = {
|
||||
"hero.challengeValue": "Zeit und Schritte messen",
|
||||
"problem.eyebrow": "Warum dieses Problem spannend ist",
|
||||
"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.title": "Von der Idee bis zur gelösten Stellung.",
|
||||
"features.card1Title": "Interaktives Brett",
|
||||
"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.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.card3Text": "Miss deinen Fortschritt während des Spiels und vergleiche unterschiedliche Versuche.",
|
||||
"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.step2Title": "Spielmodus wählen",
|
||||
"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.",
|
||||
"cta.eyebrow": "Bereit für den ersten Versuch?",
|
||||
"cta.title": "Mach aus einem bekannten Problem ein sichtbares Lernerlebnis.",
|
||||
|
||||
+224
-3
@@ -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");
|
||||
|
||||
magneticButtons.forEach((button) => {
|
||||
@@ -37,8 +258,8 @@ document.querySelectorAll("form").forEach((form) => {
|
||||
|
||||
if (loadingLabel) {
|
||||
submitter.dataset.loadingLabelApplied = "true";
|
||||
submitter.dataset.originalLabel = submitter.innerText;
|
||||
submitter.innerText = loadingLabel;
|
||||
submitter.dataset.originalLabel = submitter.textContent;
|
||||
submitter.textContent = loadingLabel;
|
||||
submitter.style.opacity = "0.7";
|
||||
}
|
||||
});
|
||||
@@ -52,7 +273,7 @@ document.querySelectorAll(".qr-code[data-qr-value]").forEach((container) => {
|
||||
}
|
||||
|
||||
if (typeof window.QRCode !== "function") {
|
||||
container.textContent = "QR preview could not be loaded.";
|
||||
container.textContent = t("qrLoadError");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user