+63
-17
@@ -23,6 +23,52 @@ MAX_BOARD_SIZE = 13
|
|||||||
main_bp = Blueprint("main", __name__)
|
main_bp = Blueprint("main", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
def create_empty_board(board_size):
|
||||||
|
"""Create a square board matrix with no placed pieces."""
|
||||||
|
return [[0 for _ in range(board_size)] for _ in range(board_size)]
|
||||||
|
|
||||||
|
|
||||||
|
def build_board_from_positions(piece_positions, board_size):
|
||||||
|
"""Convert the legacy one-piece-per-row format into a board matrix."""
|
||||||
|
board = create_empty_board(board_size)
|
||||||
|
|
||||||
|
for row, col in enumerate(piece_positions):
|
||||||
|
if isinstance(col, int) and 0 <= col < board_size:
|
||||||
|
board[row][col] = 1
|
||||||
|
|
||||||
|
return board
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_board(candidate_board, board_size):
|
||||||
|
"""Normalize stored boards so legacy and matrix formats can both be used."""
|
||||||
|
if not isinstance(candidate_board, list) or len(candidate_board) != board_size:
|
||||||
|
return create_empty_board(board_size)
|
||||||
|
|
||||||
|
if all(isinstance(row, list) and len(row) == board_size for row in candidate_board):
|
||||||
|
normalized_board = create_empty_board(board_size)
|
||||||
|
|
||||||
|
for row_index, row in enumerate(candidate_board):
|
||||||
|
for col_index, value in enumerate(row):
|
||||||
|
normalized_board[row_index][col_index] = 1 if value else 0
|
||||||
|
|
||||||
|
return normalized_board
|
||||||
|
|
||||||
|
return build_board_from_positions(candidate_board, board_size)
|
||||||
|
|
||||||
|
|
||||||
|
def iter_piece_positions(board):
|
||||||
|
"""Yield every occupied square from the normalized board matrix."""
|
||||||
|
for row_index, row in enumerate(board):
|
||||||
|
for col_index, value in enumerate(row):
|
||||||
|
if value:
|
||||||
|
yield row_index, col_index
|
||||||
|
|
||||||
|
|
||||||
|
def count_placed_pieces(board):
|
||||||
|
"""Count how many pieces are currently placed on the board."""
|
||||||
|
return sum(1 for _ in iter_piece_positions(board))
|
||||||
|
|
||||||
|
|
||||||
def parse_board_size(raw_size, default=DEFAULT_BOARD_SIZE):
|
def parse_board_size(raw_size, default=DEFAULT_BOARD_SIZE):
|
||||||
"""Convert a user-supplied board size into a safe integer range."""
|
"""Convert a user-supplied board size into a safe integer range."""
|
||||||
try:
|
try:
|
||||||
@@ -119,7 +165,7 @@ def build_queens_trace(board_size):
|
|||||||
"type": "solution",
|
"type": "solution",
|
||||||
"row": None,
|
"row": None,
|
||||||
"col": None,
|
"col": None,
|
||||||
"board": queen_positions[:],
|
"board": build_board_from_positions(queen_positions, board_size),
|
||||||
"message": f"Lösung gefunden. Alle {board_size} Damen stehen ohne Konflikte."
|
"message": f"Lösung gefunden. Alle {board_size} Damen stehen ohne Konflikte."
|
||||||
})
|
})
|
||||||
return True
|
return True
|
||||||
@@ -136,7 +182,7 @@ def build_queens_trace(board_size):
|
|||||||
"type": "place",
|
"type": "place",
|
||||||
"row": row,
|
"row": row,
|
||||||
"col": col,
|
"col": col,
|
||||||
"board": queen_positions[:],
|
"board": build_board_from_positions(queen_positions, board_size),
|
||||||
"message": f"Dame auf {position} gesetzt."
|
"message": f"Dame auf {position} gesetzt."
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -148,7 +194,7 @@ def build_queens_trace(board_size):
|
|||||||
"type": "remove",
|
"type": "remove",
|
||||||
"row": row,
|
"row": row,
|
||||||
"col": col,
|
"col": col,
|
||||||
"board": queen_positions[:],
|
"board": build_board_from_positions(queen_positions, board_size),
|
||||||
"message": f"Backtracking: Dame von {position} entfernt."
|
"message": f"Backtracking: Dame von {position} entfernt."
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -159,7 +205,7 @@ def build_queens_trace(board_size):
|
|||||||
return {
|
return {
|
||||||
"mode": "queens",
|
"mode": "queens",
|
||||||
"board_size": board_size,
|
"board_size": board_size,
|
||||||
"initial_board": [-1] * board_size,
|
"initial_board": create_empty_board(board_size),
|
||||||
"steps": steps,
|
"steps": steps,
|
||||||
"solved": solved
|
"solved": solved
|
||||||
}
|
}
|
||||||
@@ -168,11 +214,11 @@ def build_queens_trace(board_size):
|
|||||||
def build_rooks_trace(board_size):
|
def build_rooks_trace(board_size):
|
||||||
"""Build a simple placement trace for the rook mode."""
|
"""Build a simple placement trace for the rook mode."""
|
||||||
rook_positions = solve_rooks(board_size)
|
rook_positions = solve_rooks(board_size)
|
||||||
board = [-1] * board_size
|
board = create_empty_board(board_size)
|
||||||
steps = []
|
steps = []
|
||||||
|
|
||||||
for row, col in enumerate(rook_positions):
|
for row, col in enumerate(rook_positions):
|
||||||
board[row] = col
|
board[row][col] = 1
|
||||||
position = get_position_label(row, col, board_size)
|
position = get_position_label(row, col, board_size)
|
||||||
# As with the queens trace, each step includes a full board snapshot for
|
# As with the queens trace, each step includes a full board snapshot for
|
||||||
# straightforward frontend rendering.
|
# straightforward frontend rendering.
|
||||||
@@ -180,7 +226,7 @@ def build_rooks_trace(board_size):
|
|||||||
"type": "place",
|
"type": "place",
|
||||||
"row": row,
|
"row": row,
|
||||||
"col": col,
|
"col": col,
|
||||||
"board": board[:],
|
"board": [board_row[:] for board_row in board],
|
||||||
"message": f"Turm auf {position} gesetzt."
|
"message": f"Turm auf {position} gesetzt."
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -188,14 +234,14 @@ def build_rooks_trace(board_size):
|
|||||||
"type": "solution",
|
"type": "solution",
|
||||||
"row": None,
|
"row": None,
|
||||||
"col": None,
|
"col": None,
|
||||||
"board": board[:],
|
"board": [board_row[:] for board_row in board],
|
||||||
"message": f"Loesung gefunden. Alle {board_size} Tuerme stehen ohne Konflikte."
|
"message": f"Loesung gefunden. Alle {board_size} Tuerme stehen ohne Konflikte."
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"mode": "rooks",
|
"mode": "rooks",
|
||||||
"board_size": board_size,
|
"board_size": board_size,
|
||||||
"initial_board": [-1] * board_size,
|
"initial_board": create_empty_board(board_size),
|
||||||
"steps": steps,
|
"steps": steps,
|
||||||
"solved": True
|
"solved": True
|
||||||
}
|
}
|
||||||
@@ -203,7 +249,7 @@ def build_rooks_trace(board_size):
|
|||||||
|
|
||||||
def serialize_game_state(game_state):
|
def serialize_game_state(game_state):
|
||||||
"""Convert a database model into JSON-friendly data for the frontend."""
|
"""Convert a database model into JSON-friendly data for the frontend."""
|
||||||
board = json.loads(game_state.board)
|
board = normalize_board(json.loads(game_state.board), game_state.board_size)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": game_state.id,
|
"id": game_state.id,
|
||||||
@@ -213,7 +259,7 @@ def serialize_game_state(game_state):
|
|||||||
"save_name": game_state.save_name,
|
"save_name": game_state.save_name,
|
||||||
"save_note": game_state.save_note,
|
"save_note": game_state.save_note,
|
||||||
"is_favorite": game_state.is_favorite,
|
"is_favorite": game_state.is_favorite,
|
||||||
"pieces_placed": sum(1 for column in board if column != -1),
|
"pieces_placed": count_placed_pieces(board),
|
||||||
"step_count": game_state.step_count,
|
"step_count": game_state.step_count,
|
||||||
"elapsed_seconds": game_state.elapsed_seconds,
|
"elapsed_seconds": game_state.elapsed_seconds,
|
||||||
"is_solved": game_state.is_solved,
|
"is_solved": game_state.is_solved,
|
||||||
@@ -285,17 +331,17 @@ def check():
|
|||||||
row = data["row"]
|
row = data["row"]
|
||||||
col = data["col"]
|
col = data["col"]
|
||||||
piece_mode = data.get("mode", "queens")
|
piece_mode = data.get("mode", "queens")
|
||||||
|
normalized_board = normalize_board(board, len(board))
|
||||||
|
|
||||||
# The board is stored as a list where the index is the row and the value is
|
for current_row, current_col in iter_piece_positions(normalized_board):
|
||||||
# the column of the placed piece. -1 means the row is still empty.
|
if current_row == row and current_col == col:
|
||||||
for current_row, current_col in enumerate(board):
|
|
||||||
if current_col == -1 or current_row == row:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
same_row = current_row == row
|
||||||
same_column = current_col == col
|
same_column = current_col == col
|
||||||
same_diagonal = abs(current_col - col) == abs(current_row - row)
|
same_diagonal = abs(current_col - col) == abs(current_row - row)
|
||||||
|
|
||||||
if same_column:
|
if same_row or same_column:
|
||||||
return jsonify({"valid": False})
|
return jsonify({"valid": False})
|
||||||
|
|
||||||
# Diagonal conflicts only matter for queens, not for rooks.
|
# Diagonal conflicts only matter for queens, not for rooks.
|
||||||
@@ -425,7 +471,7 @@ def load_game():
|
|||||||
|
|
||||||
# If the user has never saved a game, return a clean default board.
|
# If the user has never saved a game, return a clean default board.
|
||||||
if not latest_game:
|
if not latest_game:
|
||||||
return jsonify({"board": [-1] * DEFAULT_BOARD_SIZE, "mode": "queens"})
|
return jsonify({"board": create_empty_board(DEFAULT_BOARD_SIZE), "mode": "queens"})
|
||||||
|
|
||||||
return jsonify(serialize_game_state(latest_game))
|
return jsonify(serialize_game_state(latest_game))
|
||||||
|
|
||||||
|
|||||||
@@ -483,9 +483,17 @@ textarea {
|
|||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
.board {
|
.board-stage {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
justify-items: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-shell {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.board-grid {
|
.board-grid {
|
||||||
@@ -544,30 +552,30 @@ textarea {
|
|||||||
background: #d65555 !important;
|
background: #d65555 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.solver-panel {
|
|
||||||
margin-top: 18px;
|
|
||||||
padding: 18px;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid rgba(31, 49, 64, 0.1);
|
|
||||||
background: rgba(255, 250, 242, 0.74);
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.solver-panel-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.solver-panel h3 {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.solver-message {
|
.solver-message {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.board-navigator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
width: min(100%, 620px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-nav-button {
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solver-message-inline {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 760px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.solver-step-info {
|
.solver-step-info {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
@@ -579,63 +587,31 @@ textarea {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.solver-groups {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1.35fr) minmax(260px, 0.65fr);
|
|
||||||
gap: 16px;
|
|
||||||
margin-top: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.solver-group {
|
|
||||||
padding: 18px;
|
|
||||||
border: 1px solid rgba(31, 49, 64, 0.1);
|
|
||||||
background: rgba(255, 255, 255, 0.58);
|
|
||||||
}
|
|
||||||
|
|
||||||
.solver-group h4 {
|
|
||||||
margin: 0 0 8px;
|
|
||||||
color: #182029;
|
|
||||||
font-size: 1.15rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.solver-group-text {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.solver-controls {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.instant-solver-actions {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.instant-solver-actions .btn {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.solver-speed {
|
.solver-speed {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
align-items: center;
|
grid-template-areas:
|
||||||
|
"label label"
|
||||||
|
"input value";
|
||||||
|
align-items: end;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.solver-speed input[type="range"] {
|
.solver-speed input[type="range"] {
|
||||||
|
grid-area: input;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.solver-speed-label {
|
.solver-speed-label {
|
||||||
|
grid-area: label;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--navy);
|
color: var(--navy);
|
||||||
}
|
}
|
||||||
|
|
||||||
.solver-speed-value {
|
.solver-speed-value {
|
||||||
|
grid-area: value;
|
||||||
min-width: 62px;
|
min-width: 62px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
@@ -650,19 +626,6 @@ textarea {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls-save-row {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 10px;
|
|
||||||
width: min(420px, 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-sidebar-actions {
|
|
||||||
margin-top: 18px;
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header,
|
.section-header,
|
||||||
.history-header,
|
.history-header,
|
||||||
.sidebar-header,
|
.sidebar-header,
|
||||||
@@ -786,6 +749,34 @@ textarea {
|
|||||||
width: min(400px, 94vw);
|
width: min(400px, 94vw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-section {
|
||||||
|
margin-top: 22px;
|
||||||
|
padding-top: 22px;
|
||||||
|
border-top: 1px solid rgba(31, 49, 64, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section h4 {
|
||||||
|
margin: 0;
|
||||||
|
color: #182029;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section .section-kicker {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-action-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-label {
|
.sidebar-label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
@@ -980,14 +971,6 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 980px) {
|
@media (max-width: 980px) {
|
||||||
.solver-groups {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.solver-controls {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-card-header,
|
.game-card-header,
|
||||||
.history-header,
|
.history-header,
|
||||||
.section-header {
|
.section-header {
|
||||||
@@ -1040,7 +1023,7 @@ textarea {
|
|||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.board {
|
.board-shell {
|
||||||
margin-left: -4px;
|
margin-left: -4px;
|
||||||
margin-right: -4px;
|
margin-right: -4px;
|
||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
@@ -1061,6 +1044,10 @@ textarea {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.board-navigator {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.mode-switch .btn {
|
.mode-switch .btn {
|
||||||
min-width: min(100%, 280px);
|
min-width: min(100%, 280px);
|
||||||
}
|
}
|
||||||
@@ -1070,14 +1057,11 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.controls>.btn,
|
.controls>.btn,
|
||||||
|
.board-nav-button,
|
||||||
.save-point-actions .btn {
|
.save-point-actions .btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls-save-row {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: min(100%, calc(100vw - 24px));
|
width: min(100%, calc(100vw - 24px));
|
||||||
}
|
}
|
||||||
|
|||||||
+380
-258
File diff suppressed because it is too large
Load Diff
+67
-108
@@ -1,61 +1,65 @@
|
|||||||
|
// settings for preview board
|
||||||
const HERO_BOARD_SIZE = 8;
|
const HERO_BOARD_SIZE = 8;
|
||||||
const heroBoard = document.getElementById("heroBoard");
|
const heroBoard = document.getElementById("heroBoard");
|
||||||
|
|
||||||
|
//translation for English and German
|
||||||
|
const AppLanguage = window.AppLanguage;
|
||||||
const langSwitch = document.getElementById("langSwitch");
|
const langSwitch = document.getElementById("langSwitch");
|
||||||
const supportedLanguages = ["de", "en"];
|
|
||||||
const initialLanguage = window.LANDING_INITIAL_LANGUAGE || "de";
|
const initialLanguage = window.LANDING_INITIAL_LANGUAGE || "de";
|
||||||
|
let currentLanguage = initialLanguage;
|
||||||
|
|
||||||
const translations = {
|
const translations = {
|
||||||
de: {
|
de: {
|
||||||
"meta.title": "8-Damen- und 8-Türme Solver",
|
"meta.title": "8-Damen- und 8-Türme Solver",
|
||||||
"nav.brand": "8-Damen- und 8-Türme Solver",
|
"nav.brand": "8-Damen- und 8-Türme Solver",
|
||||||
"nav.problem": "Problem",
|
"nav.problem": "Problem",
|
||||||
"nav.features": "Features",
|
"nav.features": "Funktionen",
|
||||||
"nav.journey": "Ablauf",
|
"nav.journey": "Ablauf",
|
||||||
"nav.login": "Login",
|
"nav.login": "Anmelden",
|
||||||
"nav.signup": "Create Account",
|
"nav.signup": "Account erstellen",
|
||||||
"hero.eyebrow": "Interaktives Lernen mit Strategie, Logik und Backtracking",
|
"hero.eyebrow": "Interaktives Lernen mit Strategie, Logik und Backtracking",
|
||||||
"hero.title": "Entdecke das Acht-Damen- und Acht-Türme-Problem.",
|
"hero.title": "Entdecke das Acht-Damen- und Acht-Türme-Problem.",
|
||||||
"hero.text": "Diese App macht aus einem klassischen Informatik- und Mathematikproblem ein interaktives Erlebnis: Setze Damen oder Türme, verfolge den Solver Schritt fuer Schritt, miss deine Zeit und speichere clevere Zwischenstände.",
|
"hero.text": "Diese App macht aus einem klassischen Informatik- und Mathematikproblem ein interaktives Erlebnis: Setze Damen oder Türme, verfolge den Solver Schritt für Schritt, miss deine Zeit und speichere clevere Zwischenstände.",
|
||||||
"hero.primaryCta": "Jetzt starten",
|
"hero.primaryCta": "Jetzt starten",
|
||||||
"hero.secondaryCta": "Bereits Account? Login",
|
"hero.secondaryCta": "Bereits ein Account? Anmelden",
|
||||||
"hero.fact1Title": "Skalierbar",
|
"hero.fact1Title": "Skalierbar",
|
||||||
"hero.fact1Text": "Brettgrösse flexibel anpassen",
|
"hero.fact1Text": "Brettgrösse flexibel anpassen",
|
||||||
"hero.fact2Title": "Schritt für Schritt",
|
"hero.fact2Title": "Schritt für Schritt",
|
||||||
"hero.fact2Text": "Solver mit Animation",
|
"hero.fact2Text": "Solver mit Animation",
|
||||||
"hero.fact3Title": "Save-Points",
|
"hero.fact3Title": "Speicherstände",
|
||||||
"hero.fact3Text": "Zeit und Fortschritt behalten",
|
"hero.fact3Text": "Zeit und Fortschritt behalten",
|
||||||
"hero.solverLabel": "Solver",
|
"hero.solverLabel": "Lösungshelfer",
|
||||||
"hero.solverValue": "Backtracking live",
|
"hero.solverValue": "Backtracking in Echtzeit",
|
||||||
"hero.challengeLabel": "Challenge",
|
"hero.challengeLabel": "Herausforderung",
|
||||||
"hero.challengeValue": "Zeit und Schritte messen",
|
"hero.challengeValue": "Zeit und Schritte messen",
|
||||||
"problem.eyebrow": "Warum dieses Problem spannend ist",
|
"problem.eyebrow": "Was ist das Acht-Damen-Problem?",
|
||||||
"problem.title": "Die Acht-Damen- und Acht-Türme-Probleme sind mehr als Schachrätsel.",
|
"problem.title": "Eine klassische schachmathematische Aufgabe mit überraschend vielen Lösungen.",
|
||||||
"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.",
|
"problem.text": "Das Acht-Damen-Problem ist eine schachmathematische Aufgabe: Acht Damen sollen auf einem Schachbrett so aufgestellt werden, dass keine zwei Damen dieselbe Reihe, Spalte oder Diagonale teilen. Das Problem lässt sich auf beliebige Brettgrössen verallgemeinern, indem n Damen auf einem n x n-Brett platziert werden.<br><br>Für das klassische 8x8-Brett gibt es 92 Lösungen. Zählt man Stellungen, die nur durch Spiegelung oder Drehung entstehen, als gleich, bleiben 12 eindeutige Lösungen übrig.<br><br><a href=\"https://de-academic.com/dic.nsf/dewiki/13248\" target=\"_blank\" rel=\"noopener noreferrer\">Quelle: de-academic.com</a>",
|
||||||
"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 Lösungsweg Schritt fuer Schritt und verstehe, wie Backtracking arbeitet.",
|
"features.card2Text": "Beobachte den Lösungsweg Schritt für 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": "Speicherstände",
|
||||||
"features.card4Text": "Speichere Zwischenstände mit Notizen, Zeiten und Schrittzahl fuer späteres Weiterarbeiten.",
|
"features.card4Text": "Speichere Zwischenstände mit Notizen, Zeiten und Schrittzahl für späteres Weiterarbeiten.",
|
||||||
"features.card5Title": "Skalierbare Bretter",
|
"features.card5Title": "Skalierbare Bretter",
|
||||||
"features.card5Text": "Passe die Brettgrösse an und erkunde die Probleme nicht nur auf dem klassischen 8x8-Brett.",
|
"features.card5Text": "Passe die Brettgrösse an und erkunde die Probleme nicht nur auf dem klassischen 8x8-Brett.",
|
||||||
"journey.eyebrow": "So funktioniert dein Einstieg",
|
"journey.eyebrow": "So funktioniert dein Einstieg",
|
||||||
"journey.title": "In wenigen Schritten vom ersten Besuch zur eigenen Lösung.",
|
"journey.title": "In wenigen Schritten vom ersten Besuch zur eigenen Lösung.",
|
||||||
"journey.step1Title": "Account erstellen",
|
"journey.step1Title": "Account erstellen",
|
||||||
"journey.step1Text": "Registriere dich und erhalte Zugang zu Dashboard, Save-Points und deinem Fortschritt.",
|
"journey.step1Text": "Registriere dich und erhalte Zugang zum Dashboard, zu Speicherständen und zu 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": "Lösen 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 zeigen, damit du den Lösungsweg besser nachvollziehen kannst.",
|
||||||
"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.",
|
||||||
"cta.text": "Starte mit einem Account, speichere deine Zwischenstände und finde heraus, wie schnell du eine konfliktfreie Lösung erreichst.",
|
"cta.text": "Starte mit einem Account, speichere deine Zwischenstände und finde heraus, wie schnell du eine konfliktfreie Lösung erreichst.",
|
||||||
"cta.primaryCta": "Create Account",
|
"cta.primaryCta": "Account erstellen",
|
||||||
"cta.secondaryCta": "Login",
|
"cta.secondaryCta": "Anmelden",
|
||||||
"footerText": "Schulprojekt rund um das Acht-Damen- und Acht-Türme-Problem.",
|
"footerText": "Schulprojekt rund um das Acht-Damen- und Acht-Türme-Problem.",
|
||||||
"footerPrivacy": "Datenschutzerklärung",
|
"footerPrivacy": "Datenschutzerklärung",
|
||||||
"footerImprint": "Impressum",
|
"footerImprint": "Impressum",
|
||||||
@@ -67,13 +71,13 @@ const translations = {
|
|||||||
"nav.problem": "Problem",
|
"nav.problem": "Problem",
|
||||||
"nav.features": "Features",
|
"nav.features": "Features",
|
||||||
"nav.journey": "Journey",
|
"nav.journey": "Journey",
|
||||||
"nav.login": "Login",
|
"nav.login": "Sign in",
|
||||||
"nav.signup": "Create Account",
|
"nav.signup": "Create Account",
|
||||||
"hero.eyebrow": "Interactive learning with strategy, logic, and backtracking",
|
"hero.eyebrow": "Interactive learning with strategy, logic, and backtracking",
|
||||||
"hero.title": "Discover the Eight Queens and Eight Rooks puzzle.",
|
"hero.title": "Discover the Eight Queens and Eight Rooks puzzle.",
|
||||||
"hero.text": "This app turns a classic computer science and mathematics problem into an interactive experience: place queens or rooks, follow the solver step by step, track your time, and save clever checkpoints.",
|
"hero.text": "This app turns a classic computer science and mathematics problem into an interactive experience: place queens or rooks, follow the solver step by step, track your time, and save clever checkpoints.",
|
||||||
"hero.primaryCta": "Get Started",
|
"hero.primaryCta": "Get Started",
|
||||||
"hero.secondaryCta": "Already have an account? Login",
|
"hero.secondaryCta": "Already have an account? Sign in",
|
||||||
"hero.fact1Title": "Scalable",
|
"hero.fact1Title": "Scalable",
|
||||||
"hero.fact1Text": "Adjust the board size freely",
|
"hero.fact1Text": "Adjust the board size freely",
|
||||||
"hero.fact2Title": "Step by step",
|
"hero.fact2Title": "Step by step",
|
||||||
@@ -84,9 +88,9 @@ const translations = {
|
|||||||
"hero.solverValue": "Backtracking in motion",
|
"hero.solverValue": "Backtracking in motion",
|
||||||
"hero.challengeLabel": "Challenge",
|
"hero.challengeLabel": "Challenge",
|
||||||
"hero.challengeValue": "Measure time and steps",
|
"hero.challengeValue": "Measure time and steps",
|
||||||
"problem.eyebrow": "Why this puzzle matters",
|
"problem.eyebrow": "What is the Eight Queens problem?",
|
||||||
"problem.title": "The Eight Queens and Eight Rooks problems are more than chess puzzles.",
|
"problem.title": "A classic chess-related math puzzle with surprisingly many solutions.",
|
||||||
"problem.text": "In the Eight Queens problem, you search for a way to place eight queens on a chessboard so that no two queens can attack each other. The Eight Rooks problem follows the same idea for rooks, but with simpler conflict rules. Both tasks show how pattern recognition, systematic search, and backtracking work.<br><br>The queens puzzle quickly makes it obvious why strategy matters: there are countless possible placements, but only a few truly valid solutions. That is what makes this app interesting, because you do not just see the result, you also see the path to it.",
|
"problem.text": "The Eight Queens problem is a chess-related mathematical puzzle: eight queens must be placed on a chessboard so that no two queens share the same row, column, or diagonal. The idea can be generalized to boards of any size by placing n queens on an n x n board.<br><br>On the classic 8x8 board there are 92 solutions. If positions that only differ by reflection or rotation are counted as the same, 12 unique solutions remain.<br><br><a href=\"https://de-academic.com/dic.nsf/dewiki/13248\" target=\"_blank\" rel=\"noopener noreferrer\">Source: de-academic.com</a>",
|
||||||
"features.eyebrow": "What the application offers",
|
"features.eyebrow": "What the application offers",
|
||||||
"features.title": "From the first idea to a solved position.",
|
"features.title": "From the first idea to a solved position.",
|
||||||
"features.card1Title": "Interactive board",
|
"features.card1Title": "Interactive board",
|
||||||
@@ -111,7 +115,7 @@ const translations = {
|
|||||||
"cta.title": "Turn a famous puzzle into a visible learning experience.",
|
"cta.title": "Turn a famous puzzle into a visible learning experience.",
|
||||||
"cta.text": "Start with an account, save your checkpoints, and find out how quickly you can reach a conflict-free solution.",
|
"cta.text": "Start with an account, save your checkpoints, and find out how quickly you can reach a conflict-free solution.",
|
||||||
"cta.primaryCta": "Create Account",
|
"cta.primaryCta": "Create Account",
|
||||||
"cta.secondaryCta": "Login",
|
"cta.secondaryCta": "Sign in",
|
||||||
"footerText": "School project about the Eight Queens and Eight Rooks problem.",
|
"footerText": "School project about the Eight Queens and Eight Rooks problem.",
|
||||||
"footerPrivacy": "Privacy Policy",
|
"footerPrivacy": "Privacy Policy",
|
||||||
"footerImprint": "Imprint",
|
"footerImprint": "Imprint",
|
||||||
@@ -119,6 +123,43 @@ const translations = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function t(key) {
|
||||||
|
return translations[currentLanguage]?.[key] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTranslations(language) {
|
||||||
|
currentLanguage = language;
|
||||||
|
|
||||||
|
AppLanguage.applyTranslations({
|
||||||
|
translate: (key) => t(key),
|
||||||
|
allowHtml: (element, translatedText) => element.tagName === "TITLE" || String(translatedText).includes("<br"),
|
||||||
|
attributeMappings: [
|
||||||
|
{
|
||||||
|
selector: "[data-i18n-aria-label]",
|
||||||
|
datasetKey: "i18nAriaLabel",
|
||||||
|
attribute: "aria-label"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const languageController = AppLanguage.createController({
|
||||||
|
button: langSwitch,
|
||||||
|
defaultLanguage: initialLanguage,
|
||||||
|
onApply(language, helpers) {
|
||||||
|
applyTranslations(language);
|
||||||
|
helpers.updateLanguageSwitch({
|
||||||
|
ariaLabel: language === "de" ? "Switch language to English" : "Sprache auf Deutsch wechseln",
|
||||||
|
title: language === "de" ? "Switch to English" : "Zu Deutsch wechseln"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
languageController.init();
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// simple pattern for preview board
|
||||||
const queenPatterns = [
|
const queenPatterns = [
|
||||||
[0, 4, 7, 5, 2, 6, 1, 3],
|
[0, 4, 7, 5, 2, 6, 1, 3],
|
||||||
[1, 3, 5, 7, 2, 0, 6, 4],
|
[1, 3, 5, 7, 2, 0, 6, 4],
|
||||||
@@ -127,80 +168,6 @@ const queenPatterns = [
|
|||||||
|
|
||||||
let currentPatternIndex = 0;
|
let currentPatternIndex = 0;
|
||||||
|
|
||||||
function normalizeLanguage(language) {
|
|
||||||
const shortLanguage = String(language || "").trim().slice(0, 2).toLowerCase();
|
|
||||||
return supportedLanguages.includes(shortLanguage) ? shortLanguage : "de";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStoredLanguage() {
|
|
||||||
try {
|
|
||||||
return window.localStorage.getItem("preferredLanguage");
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function storeLanguage(language) {
|
|
||||||
try {
|
|
||||||
window.localStorage.setItem("preferredLanguage", language);
|
|
||||||
} catch (error) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectBrowserLanguage() {
|
|
||||||
const browserLanguage = navigator.language || navigator.userLanguage || initialLanguage;
|
|
||||||
return normalizeLanguage(browserLanguage);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLanguage(language) {
|
|
||||||
const resolvedLanguage = normalizeLanguage(language);
|
|
||||||
const dictionary = translations[resolvedLanguage];
|
|
||||||
const i18nElements = document.querySelectorAll("[data-i18n]");
|
|
||||||
|
|
||||||
document.documentElement.lang = resolvedLanguage;
|
|
||||||
document.body.dataset.language = resolvedLanguage;
|
|
||||||
|
|
||||||
i18nElements.forEach((element) => {
|
|
||||||
const key = element.dataset.i18n;
|
|
||||||
const translatedText = dictionary[key];
|
|
||||||
|
|
||||||
if (!translatedText) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element.tagName === "TITLE" || translatedText.includes("<br")) {
|
|
||||||
element.innerHTML = translatedText;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
element.textContent = translatedText;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll("[data-i18n-aria-label]").forEach((element) => {
|
|
||||||
const key = element.dataset.i18nAriaLabel;
|
|
||||||
const translatedText = dictionary[key];
|
|
||||||
|
|
||||||
if (!translatedText) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
element.setAttribute("aria-label", translatedText);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (langSwitch) {
|
|
||||||
langSwitch.textContent = resolvedLanguage.toUpperCase();
|
|
||||||
langSwitch.setAttribute("aria-label", resolvedLanguage === "de" ? "Switch language to English" : "Sprache auf Deutsch wechseln");
|
|
||||||
langSwitch.title = resolvedLanguage === "de" ? "Switch to English" : "Zu Deutsch wechseln";
|
|
||||||
}
|
|
||||||
|
|
||||||
storeLanguage(resolvedLanguage);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPreferredLanguage() {
|
|
||||||
return normalizeLanguage(getStoredLanguage() || detectBrowserLanguage() || initialLanguage);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createHeroBoard() {
|
function createHeroBoard() {
|
||||||
if (!heroBoard) {
|
if (!heroBoard) {
|
||||||
return;
|
return;
|
||||||
@@ -241,16 +208,8 @@ function rotatePattern() {
|
|||||||
applyPattern(queenPatterns[currentPatternIndex]);
|
applyPattern(queenPatterns[currentPatternIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (langSwitch) {
|
|
||||||
langSwitch.addEventListener("click", () => {
|
|
||||||
const nextLanguage = document.documentElement.lang === "de" ? "en" : "de";
|
|
||||||
setLanguage(nextLanguage);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
createHeroBoard();
|
createHeroBoard();
|
||||||
applyPattern(queenPatterns[currentPatternIndex]);
|
applyPattern(queenPatterns[currentPatternIndex]);
|
||||||
setLanguage(getPreferredLanguage());
|
|
||||||
|
|
||||||
if (heroBoard) {
|
if (heroBoard) {
|
||||||
window.setInterval(rotatePattern, 2200);
|
window.setInterval(rotatePattern, 2200);
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
// handels the language translation
|
||||||
|
|
||||||
|
(function attachAppLanguage(window, document) {
|
||||||
|
const DEFAULT_LANGUAGE = "de";
|
||||||
|
const DEFAULT_STORAGE_KEY = "preferredLanguage";
|
||||||
|
const DEFAULT_SUPPORTED_LANGUAGES = ["de", "en"];
|
||||||
|
|
||||||
|
function resolveSupportedLanguages(supportedLanguages = DEFAULT_SUPPORTED_LANGUAGES) {
|
||||||
|
const normalizedLanguages = Array.isArray(supportedLanguages)
|
||||||
|
? supportedLanguages
|
||||||
|
.map((language) => String(language || "").trim().slice(0, 2).toLowerCase())
|
||||||
|
.filter(Boolean)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return normalizedLanguages.length > 0 ? normalizedLanguages : DEFAULT_SUPPORTED_LANGUAGES.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLanguage(
|
||||||
|
language,
|
||||||
|
{
|
||||||
|
supportedLanguages = DEFAULT_SUPPORTED_LANGUAGES,
|
||||||
|
defaultLanguage = DEFAULT_LANGUAGE
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
const normalizedSupportedLanguages = resolveSupportedLanguages(supportedLanguages);
|
||||||
|
const normalizedDefaultLanguage = normalizedSupportedLanguages.includes(defaultLanguage)
|
||||||
|
? defaultLanguage
|
||||||
|
: normalizedSupportedLanguages[0] || DEFAULT_LANGUAGE;
|
||||||
|
const shortLanguage = String(language || "").trim().slice(0, 2).toLowerCase();
|
||||||
|
|
||||||
|
return normalizedSupportedLanguages.includes(shortLanguage) ? shortLanguage : normalizedDefaultLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStoredLanguage({ storageKey = DEFAULT_STORAGE_KEY } = {}) {
|
||||||
|
try {
|
||||||
|
return window.localStorage.getItem(storageKey);
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeLanguage(language, { storageKey = DEFAULT_STORAGE_KEY } = {}) {
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(storageKey, language);
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectBrowserLanguage(options = {}) {
|
||||||
|
return normalizeLanguage(navigator.language || navigator.userLanguage || options.defaultLanguage, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPreferredLanguage(
|
||||||
|
{
|
||||||
|
supportedLanguages = DEFAULT_SUPPORTED_LANGUAGES,
|
||||||
|
defaultLanguage = DEFAULT_LANGUAGE,
|
||||||
|
storageKey = DEFAULT_STORAGE_KEY,
|
||||||
|
fallbackLanguage = defaultLanguage
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
return normalizeLanguage(
|
||||||
|
getStoredLanguage({ storageKey }) || detectBrowserLanguage({ supportedLanguages, defaultLanguage }) || fallbackLanguage,
|
||||||
|
{
|
||||||
|
supportedLanguages,
|
||||||
|
defaultLanguage: fallbackLanguage
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextLanguage(language, supportedLanguages = DEFAULT_SUPPORTED_LANGUAGES) {
|
||||||
|
const normalizedSupportedLanguages = resolveSupportedLanguages(supportedLanguages);
|
||||||
|
const currentLanguage = normalizeLanguage(language, {
|
||||||
|
supportedLanguages: normalizedSupportedLanguages,
|
||||||
|
defaultLanguage: normalizedSupportedLanguages[0]
|
||||||
|
});
|
||||||
|
const currentIndex = normalizedSupportedLanguages.indexOf(currentLanguage);
|
||||||
|
|
||||||
|
if (currentIndex === -1 || normalizedSupportedLanguages.length < 2) {
|
||||||
|
return currentLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedSupportedLanguages[(currentIndex + 1) % normalizedSupportedLanguages.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDocumentLanguage(language) {
|
||||||
|
if (document.documentElement) {
|
||||||
|
document.documentElement.lang = language;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.body) {
|
||||||
|
document.body.dataset.language = language;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTextTranslations({
|
||||||
|
translate,
|
||||||
|
selector = "[data-i18n]",
|
||||||
|
allowHtml = () => false
|
||||||
|
} = {}) {
|
||||||
|
if (typeof translate !== "function") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll(selector).forEach((element) => {
|
||||||
|
const key = element.dataset.i18n;
|
||||||
|
const translatedText = translate(key, element);
|
||||||
|
|
||||||
|
if (translatedText === undefined || translatedText === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowHtml(element, translatedText, key)) {
|
||||||
|
element.innerHTML = String(translatedText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.textContent = String(translatedText);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyAttributeTranslations({
|
||||||
|
translate,
|
||||||
|
mappings = []
|
||||||
|
} = {}) {
|
||||||
|
if (typeof translate !== "function") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mappings.forEach(({ selector, datasetKey, attribute }) => {
|
||||||
|
document.querySelectorAll(selector).forEach((element) => {
|
||||||
|
const key = element.dataset[datasetKey];
|
||||||
|
const translatedText = translate(key, element, attribute);
|
||||||
|
|
||||||
|
if (translatedText === undefined || translatedText === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.setAttribute(attribute, String(translatedText));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTranslations({
|
||||||
|
translate,
|
||||||
|
selector = "[data-i18n]",
|
||||||
|
allowHtml,
|
||||||
|
attributeMappings = []
|
||||||
|
} = {}) {
|
||||||
|
applyTextTranslations({ translate, selector, allowHtml });
|
||||||
|
applyAttributeTranslations({ translate, mappings: attributeMappings });
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateLanguageSwitch(
|
||||||
|
button,
|
||||||
|
{
|
||||||
|
language,
|
||||||
|
label = String(language || "").toUpperCase(),
|
||||||
|
ariaLabel,
|
||||||
|
title
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
if (!button) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.textContent = label;
|
||||||
|
|
||||||
|
if (ariaLabel !== undefined) {
|
||||||
|
button.setAttribute("aria-label", ariaLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (title !== undefined) {
|
||||||
|
button.title = title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createController({
|
||||||
|
button = document.getElementById("langSwitch"),
|
||||||
|
defaultLanguage = DEFAULT_LANGUAGE,
|
||||||
|
supportedLanguages = DEFAULT_SUPPORTED_LANGUAGES,
|
||||||
|
storageKey = DEFAULT_STORAGE_KEY,
|
||||||
|
onApply = () => { }
|
||||||
|
} = {}) {
|
||||||
|
const options = {
|
||||||
|
defaultLanguage,
|
||||||
|
supportedLanguages,
|
||||||
|
storageKey
|
||||||
|
};
|
||||||
|
|
||||||
|
let currentLanguage = normalizeLanguage(defaultLanguage, options);
|
||||||
|
|
||||||
|
function apply(language, source = "apply") {
|
||||||
|
currentLanguage = normalizeLanguage(language, options);
|
||||||
|
setDocumentLanguage(currentLanguage);
|
||||||
|
|
||||||
|
onApply(currentLanguage, {
|
||||||
|
source,
|
||||||
|
button,
|
||||||
|
applyTranslations,
|
||||||
|
updateLanguageSwitch: (switchOptions = {}) => updateLanguageSwitch(button, {
|
||||||
|
language: currentLanguage,
|
||||||
|
...switchOptions
|
||||||
|
}),
|
||||||
|
getNextLanguage: () => getNextLanguage(currentLanguage, supportedLanguages),
|
||||||
|
normalizeLanguage: (value) => normalizeLanguage(value, options)
|
||||||
|
});
|
||||||
|
|
||||||
|
storeLanguage(currentLanguage, { storageKey });
|
||||||
|
return currentLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
return apply(getNextLanguage(currentLanguage, supportedLanguages), "toggle");
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(initialLanguage = getPreferredLanguage({
|
||||||
|
supportedLanguages,
|
||||||
|
defaultLanguage,
|
||||||
|
storageKey,
|
||||||
|
fallbackLanguage: defaultLanguage
|
||||||
|
})) {
|
||||||
|
if (button) {
|
||||||
|
button.addEventListener("click", toggle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apply(initialLanguage, "init");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
apply,
|
||||||
|
getCurrentLanguage: () => currentLanguage,
|
||||||
|
getPreferredLanguage: (fallbackLanguage = defaultLanguage) => getPreferredLanguage({
|
||||||
|
supportedLanguages,
|
||||||
|
defaultLanguage,
|
||||||
|
storageKey,
|
||||||
|
fallbackLanguage
|
||||||
|
}),
|
||||||
|
init,
|
||||||
|
toggle
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.AppLanguage = {
|
||||||
|
DEFAULT_LANGUAGE,
|
||||||
|
DEFAULT_STORAGE_KEY,
|
||||||
|
DEFAULT_SUPPORTED_LANGUAGES: DEFAULT_SUPPORTED_LANGUAGES.slice(),
|
||||||
|
applyAttributeTranslations,
|
||||||
|
applyTextTranslations,
|
||||||
|
applyTranslations,
|
||||||
|
createController,
|
||||||
|
detectBrowserLanguage,
|
||||||
|
getNextLanguage,
|
||||||
|
getPreferredLanguage,
|
||||||
|
getStoredLanguage,
|
||||||
|
normalizeLanguage,
|
||||||
|
setDocumentLanguage,
|
||||||
|
storeLanguage,
|
||||||
|
updateLanguageSwitch
|
||||||
|
};
|
||||||
|
})(window, document);
|
||||||
+36
-104
@@ -1,9 +1,9 @@
|
|||||||
const SUPPORTED_LANGUAGES = ["de", "en"];
|
//translation for English and German
|
||||||
const DEFAULT_LANGUAGE = "de";
|
|
||||||
const LANGUAGE_STORAGE_KEY = "preferredLanguage";
|
|
||||||
|
|
||||||
|
const AppLanguage = window.AppLanguage;
|
||||||
const langSwitch = document.getElementById("langSwitch");
|
const langSwitch = document.getElementById("langSwitch");
|
||||||
const legalPage = document.body.dataset.legalPage;
|
const legalPage = document.body.dataset.legalPage;
|
||||||
|
let currentLanguage = AppLanguage.DEFAULT_LANGUAGE;
|
||||||
|
|
||||||
const translations = {
|
const translations = {
|
||||||
de: {
|
de: {
|
||||||
@@ -27,10 +27,10 @@ const translations = {
|
|||||||
contactEmailLabel: "E-Mail:",
|
contactEmailLabel: "E-Mail:",
|
||||||
projectResponsibilityTitle: "Projektverantwortung",
|
projectResponsibilityTitle: "Projektverantwortung",
|
||||||
projectResponsibilityText: "Dieses Webprojekt wurde im Rahmen eines Schulprojekts zum Acht-Damen- und Acht-Türme-Problem erstellt. Inhaltlich verantwortlich ist die jeweils oben genannte verantwortliche Stelle.",
|
projectResponsibilityText: "Dieses Webprojekt wurde im Rahmen eines Schulprojekts zum Acht-Damen- und Acht-Türme-Problem erstellt. Inhaltlich verantwortlich ist die jeweils oben genannte verantwortliche Stelle.",
|
||||||
contentLiabilityTitle: "Haftung fuer Inhalte",
|
contentLiabilityTitle: "Haftung für Inhalte",
|
||||||
contentLiabilityText: "Die Inhalte dieser Website wurden mit Sorgfalt erstellt. Für die Richtigkeit, Vollstaendigkeit und Aktualitaet der Inhalte kann jedoch keine Gewaehr übernommen werden. Bei Bekanntwerden von Rechtsverletzungen werden entsprechende Inhalte umgehend überarbeitet oder entfernt.",
|
contentLiabilityText: "Die Inhalte dieser Website wurden mit Sorgfalt erstellt. Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte kann jedoch keine Gewähr übernommen werden. Bei Bekanntwerden von Rechtsverletzungen werden entsprechende Inhalte umgehend überarbeitet oder entfernt.",
|
||||||
linksLiabilityTitle: "Haftung fuer Links",
|
linksLiabilityTitle: "Haftung für Links",
|
||||||
linksLiabilityText: "Diese Website kann Links zu externen Angeboten enthalten. Fuer deren Inhalte sind ausschliesslich die jeweiligen Betreiber verantwortlich. Eine permanente inhaltliche Kontrolle verlinkter Seiten ist ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar.",
|
linksLiabilityText: "Diese Website kann Links zu externen Angeboten enthalten. Für deren Inhalte sind ausschliesslich die jeweiligen Betreiber verantwortlich. Eine permanente inhaltliche Kontrolle verlinkter Seiten ist ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar.",
|
||||||
versionTitle: "Stand",
|
versionTitle: "Stand",
|
||||||
versionText: "Stand dieser Vorlage: April 2026"
|
versionText: "Stand dieser Vorlage: April 2026"
|
||||||
},
|
},
|
||||||
@@ -52,7 +52,7 @@ const translations = {
|
|||||||
processingPurposeItem4: "Speichern und Laden von Spielständen",
|
processingPurposeItem4: "Speichern und Laden von Spielständen",
|
||||||
processingPurposeItem5: "technischer Betrieb, Fehleranalyse und Missbrauchsschutz",
|
processingPurposeItem5: "technischer Betrieb, Fehleranalyse und Missbrauchsschutz",
|
||||||
legalBasisTitle: "4. Rechtsgrundlage",
|
legalBasisTitle: "4. Rechtsgrundlage",
|
||||||
legalBasisText: "Soweit personenbezogene Daten verarbeitet werden, erfolgt dies in der Regel zur Bereitstellung der Anwendung und ihrer Funktionen sowie aufgrund des berechtigten Interesses an einem sicheren und funktionsfaehigen Webangebot. Falls eine ausdrückliche Einwilligung erforderlich ist, wird diese gesondert eingeholt.",
|
legalBasisText: "Soweit personenbezogene Daten verarbeitet werden, erfolgt dies in der Regel zur Bereitstellung der Anwendung und ihrer Funktionen sowie aufgrund des berechtigten Interesses an einem sicheren und funktionsfähigen Webangebot. Falls eine ausdrückliche Einwilligung erforderlich ist, wird diese gesondert eingeholt.",
|
||||||
storageTitle: "5. Speicherung und Löschung",
|
storageTitle: "5. Speicherung und Löschung",
|
||||||
storageText: "Personenbezogene Daten werden nur so lange gespeichert, wie sie für den jeweiligen Zweck benötigt werden. Benutzerkonten und zugehörige Spielstände bleiben grundsätzlich gespeichert, bis sie gelöscht oder für das Projekt nicht mehr benötigt werden.",
|
storageText: "Personenbezogene Daten werden nur so lange gespeichert, wie sie für den jeweiligen Zweck benötigt werden. Benutzerkonten und zugehörige Spielstände bleiben grundsätzlich gespeichert, bis sie gelöscht oder für das Projekt nicht mehr benötigt werden.",
|
||||||
thirdPartyTitle: "6. Weitergabe an Dritte",
|
thirdPartyTitle: "6. Weitergabe an Dritte",
|
||||||
@@ -61,11 +61,11 @@ const translations = {
|
|||||||
rightsText: "Betroffene Personen haben im Rahmen des anwendbaren Datenschutzrechts insbesondere das Recht auf:",
|
rightsText: "Betroffene Personen haben im Rahmen des anwendbaren Datenschutzrechts insbesondere das Recht auf:",
|
||||||
rightsItem1: "Auskunft über die verarbeiteten personenbezogenen Daten",
|
rightsItem1: "Auskunft über die verarbeiteten personenbezogenen Daten",
|
||||||
rightsItem2: "Berichtigung unrichtiger Daten",
|
rightsItem2: "Berichtigung unrichtiger Daten",
|
||||||
rightsItem3: "Löschung oder Einschraenkung der Verarbeitung",
|
rightsItem3: "Löschung oder Einschränkung der Verarbeitung",
|
||||||
rightsItem4: "Widerspruch gegen die Verarbeitung",
|
rightsItem4: "Widerspruch gegen die Verarbeitung",
|
||||||
rightsItem5: "Herausgabe der bereitgestellten Daten, soweit anwendbar",
|
rightsItem5: "Herausgabe der bereitgestellten Daten, soweit anwendbar",
|
||||||
securityTitle: "8. Datensicherheit",
|
securityTitle: "8. Datensicherheit",
|
||||||
securityText: "Es werden technische und organisatorische Massnahmen getroffen, um gespeicherte Daten vor Verlust, Missbrauch oder unberechtigtem Zugriff zu schuetzen. Dazu gehoeren insbesondere passwortgeschuetzte Konten und die Möglichkeit, Zwei-Faktor-Authentifizierung zu aktivieren.",
|
securityText: "Es werden technische und organisatorische Massnahmen getroffen, um gespeicherte Daten vor Verlust, Missbrauch oder unberechtigtem Zugriff zu schützen. Dazu gehoeren insbesondere passwortgeschützte Konten und die Möglichkeit, Zwei-Faktor-Authentifizierung zu aktivieren.",
|
||||||
versionTitle: "9. Stand",
|
versionTitle: "9. Stand",
|
||||||
versionText: "Stand dieser Vorlage: April 2026"
|
versionText: "Stand dieser Vorlage: April 2026"
|
||||||
}
|
}
|
||||||
@@ -136,107 +136,39 @@ const translations = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeLanguage(language) {
|
function t(key) {
|
||||||
const shortLanguage = String(language || "").trim().slice(0, 2).toLowerCase();
|
const commonTranslation = translations[currentLanguage]?.common?.[key];
|
||||||
return SUPPORTED_LANGUAGES.includes(shortLanguage) ? shortLanguage : DEFAULT_LANGUAGE;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStoredLanguage() {
|
|
||||||
try {
|
|
||||||
return window.localStorage.getItem(LANGUAGE_STORAGE_KEY);
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function storeLanguage(language) {
|
|
||||||
try {
|
|
||||||
window.localStorage.setItem(LANGUAGE_STORAGE_KEY, language);
|
|
||||||
} catch (error) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function detectBrowserLanguage() {
|
|
||||||
return normalizeLanguage(navigator.language || navigator.userLanguage || DEFAULT_LANGUAGE);
|
|
||||||
}
|
|
||||||
|
|
||||||
function t(language, key) {
|
|
||||||
const commonTranslation = translations[language]?.common?.[key];
|
|
||||||
|
|
||||||
if (commonTranslation) {
|
if (commonTranslation) {
|
||||||
return commonTranslation;
|
return commonTranslation;
|
||||||
}
|
}
|
||||||
|
|
||||||
return translations[language]?.[legalPage]?.[key] ?? null;
|
return translations[currentLanguage]?.[legalPage]?.[key] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function translateFooter(language) {
|
function applyTranslations() {
|
||||||
const footerText = document.querySelector(".page-footer-text");
|
AppLanguage.applyTranslations({
|
||||||
const footerNav = document.querySelector(".page-footer-links");
|
translate: (key) => t(key),
|
||||||
const privacyLink = document.querySelector('.page-footer-links a[href$="/privacy-policy"]');
|
attributeMappings: [
|
||||||
const imprintLink = document.querySelector('.page-footer-links a[href$="/impressum"]');
|
{
|
||||||
|
selector: "[data-i18n-aria-label]",
|
||||||
if (footerText) {
|
datasetKey: "i18nAriaLabel",
|
||||||
footerText.textContent = t(language, "footerText") || footerText.textContent;
|
attribute: "aria-label"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
if (footerNav) {
|
|
||||||
footerNav.setAttribute("aria-label", t(language, "footerNavAria") || footerNav.getAttribute("aria-label") || "");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (privacyLink) {
|
|
||||||
privacyLink.textContent = t(language, "footerPrivacy") || privacyLink.textContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imprintLink) {
|
|
||||||
imprintLink.textContent = t(language, "footerImprint") || imprintLink.textContent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyTranslations(language) {
|
|
||||||
document.documentElement.lang = language;
|
|
||||||
document.body.dataset.language = language;
|
|
||||||
|
|
||||||
document.querySelectorAll("[data-i18n]").forEach((element) => {
|
|
||||||
const translatedText = t(language, element.dataset.i18n);
|
|
||||||
|
|
||||||
if (!translatedText) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
element.textContent = translatedText;
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll("[data-i18n-aria-label]").forEach((element) => {
|
|
||||||
const translatedText = t(language, element.dataset.i18nAriaLabel);
|
|
||||||
|
|
||||||
if (!translatedText) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
element.setAttribute("aria-label", translatedText);
|
|
||||||
});
|
|
||||||
|
|
||||||
translateFooter(language);
|
|
||||||
|
|
||||||
if (langSwitch) {
|
|
||||||
langSwitch.textContent = language.toUpperCase();
|
|
||||||
langSwitch.setAttribute("aria-label", t(language, "languageSwitchAria") || "Sprache wechseln");
|
|
||||||
langSwitch.title = language === "de" ? "Switch to English" : "Zu Deutsch wechseln";
|
|
||||||
}
|
|
||||||
|
|
||||||
storeLanguage(language);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLanguage(language) {
|
|
||||||
applyTranslations(normalizeLanguage(language));
|
|
||||||
}
|
|
||||||
|
|
||||||
setLanguage(getStoredLanguage() || detectBrowserLanguage());
|
|
||||||
|
|
||||||
if (langSwitch) {
|
|
||||||
langSwitch.addEventListener("click", () => {
|
|
||||||
setLanguage(document.documentElement.lang === "de" ? "en" : "de");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const languageController = AppLanguage.createController({
|
||||||
|
button: langSwitch,
|
||||||
|
onApply(language, helpers) {
|
||||||
|
currentLanguage = language;
|
||||||
|
applyTranslations();
|
||||||
|
helpers.updateLanguageSwitch({
|
||||||
|
ariaLabel: t("languageSwitchAria") || "Sprache wechseln",
|
||||||
|
title: language === "de" ? "Switch to English" : "Zu Deutsch wechseln"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
languageController.init();
|
||||||
|
|||||||
+35
-64
@@ -1,13 +1,14 @@
|
|||||||
const SUPPORTED_LANGUAGES = ["de", "en"];
|
//translation for English and German
|
||||||
const DEFAULT_LANGUAGE = "de";
|
|
||||||
const LANGUAGE_STORAGE_KEY = "preferredLanguage";
|
|
||||||
|
|
||||||
|
const AppLanguage = window.AppLanguage;
|
||||||
const langSwitch = document.getElementById("langSwitch");
|
const langSwitch = document.getElementById("langSwitch");
|
||||||
|
|
||||||
const translations = {
|
const translations = {
|
||||||
de: {
|
de: {
|
||||||
pageTitle: "Login-Einstellungen",
|
pageTitle: "Login-Einstellungen",
|
||||||
navBrand: "8-Damen- und 8-Türme Solver",
|
navBrand: "8-Damen- und 8-Türme Solver",
|
||||||
|
navLogin: "Anmelden",
|
||||||
|
navRegister: "Account erstellen",
|
||||||
navDashboard: "Dashboard",
|
navDashboard: "Dashboard",
|
||||||
navSettings: "Login-Einstellungen",
|
navSettings: "Login-Einstellungen",
|
||||||
navLogout: "Logout",
|
navLogout: "Logout",
|
||||||
@@ -51,7 +52,9 @@ const translations = {
|
|||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
pageTitle: "Login Settings",
|
pageTitle: "Login Settings",
|
||||||
navBrand: "8-Damen- und 8-Türme Solver",
|
navBrand: "8 Queens and 8 Rooks Solver",
|
||||||
|
navLogin: "Login",
|
||||||
|
navRegister: "Create account",
|
||||||
navDashboard: "Dashboard",
|
navDashboard: "Dashboard",
|
||||||
navSettings: "Login settings",
|
navSettings: "Login settings",
|
||||||
navLogout: "Logout",
|
navLogout: "Logout",
|
||||||
@@ -112,32 +115,7 @@ const serverMessageTranslations = {
|
|||||||
en: {}
|
en: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
let currentLanguage = DEFAULT_LANGUAGE;
|
let currentLanguage = AppLanguage.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) {
|
function t(key) {
|
||||||
return translations[currentLanguage]?.[key] ?? translations.en[key] ?? key;
|
return translations[currentLanguage]?.[key] ?? translations.en[key] ?? key;
|
||||||
@@ -172,29 +150,25 @@ function translateServerMessage(message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyTextTranslations() {
|
function applyTextTranslations() {
|
||||||
document.documentElement.lang = currentLanguage;
|
AppLanguage.applyTranslations({
|
||||||
|
translate: (key) => t(key),
|
||||||
document.querySelectorAll("[data-i18n]").forEach((element) => {
|
attributeMappings: [
|
||||||
const translatedText = t(element.dataset.i18n);
|
{
|
||||||
|
selector: "[data-i18n-aria-label]",
|
||||||
if (element.tagName === "TITLE") {
|
datasetKey: "i18nAriaLabel",
|
||||||
element.textContent = translatedText;
|
attribute: "aria-label"
|
||||||
return;
|
},
|
||||||
|
{
|
||||||
|
selector: "[data-i18n-loading-label]",
|
||||||
|
datasetKey: "i18nLoadingLabel",
|
||||||
|
attribute: "data-loading-label"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
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) => {
|
document.querySelectorAll("[data-i18n-loading-label]").forEach((element) => {
|
||||||
const translatedLoadingLabel = t(element.dataset.i18nLoadingLabel);
|
|
||||||
element.dataset.loadingLabel = translatedLoadingLabel;
|
|
||||||
|
|
||||||
if (element.dataset.loadingLabelApplied === "true") {
|
if (element.dataset.loadingLabelApplied === "true") {
|
||||||
element.textContent = translatedLoadingLabel;
|
element.textContent = element.dataset.loadingLabel;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -205,30 +179,27 @@ function applyTextTranslations() {
|
|||||||
|
|
||||||
element.textContent = translateServerMessage(element.dataset.originalText);
|
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) {
|
const languageController = AppLanguage.createController({
|
||||||
currentLanguage = normalizeLanguage(language);
|
button: langSwitch,
|
||||||
|
onApply(language, helpers) {
|
||||||
|
currentLanguage = language;
|
||||||
applyTextTranslations();
|
applyTextTranslations();
|
||||||
storeLanguage(currentLanguage);
|
helpers.updateLanguageSwitch({
|
||||||
}
|
ariaLabel: t("languageSwitchAria"),
|
||||||
|
title: currentLanguage === "de" ? "Switch to English" : "Zu Deutsch wechseln"
|
||||||
setLanguage(getStoredLanguage() || detectBrowserLanguage());
|
|
||||||
|
|
||||||
if (langSwitch) {
|
|
||||||
langSwitch.addEventListener("click", () => {
|
|
||||||
setLanguage(currentLanguage === "de" ? "en" : "de");
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
languageController.init();
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
const magneticButtons = document.querySelectorAll(".magnetic");
|
const magneticButtons = document.querySelectorAll(".magnetic");
|
||||||
|
|
||||||
|
// magnetic button effect for desktop user
|
||||||
magneticButtons.forEach((button) => {
|
magneticButtons.forEach((button) => {
|
||||||
button.addEventListener("mousemove", (event) => {
|
button.addEventListener("mousemove", (event) => {
|
||||||
const rect = button.getBoundingClientRect();
|
const rect = button.getBoundingClientRect();
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<button class="settings-trigger" id="settingsTrigger" onclick="toggleSidebar()" type="button"
|
<button class="settings-trigger" id="settingsTrigger" onclick="toggleSidebar()" type="button"
|
||||||
aria-controls="settingsSidebar" aria-expanded="false">
|
aria-controls="settingsSidebar" aria-expanded="false">
|
||||||
<span class="settings-trigger-arrow" id="settingsTriggerArrow">→</span>
|
<span class="settings-trigger-arrow" id="settingsTriggerArrow">→</span>
|
||||||
<span class="settings-trigger-label" id="settingsTriggerLabel">Brett-Einstellungen</span>
|
<span class="settings-trigger-label" id="settingsTriggerLabel">Spiel-Einstellungen</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -65,14 +65,14 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="mode-switch" data-i18n-aria-label="modeSwitchAria" 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">
|
|
||||||
<span aria-hidden="true">♛</span>
|
|
||||||
<span data-i18n="btnQueens">Damen</span>
|
|
||||||
</button>
|
|
||||||
<button class="btn secondary" onclick="setMode('rooks')" id="btnRooks" type="button">
|
<button class="btn secondary" onclick="setMode('rooks')" id="btnRooks" type="button">
|
||||||
<span aria-hidden="true">♜</span>
|
<span aria-hidden="true">♜</span>
|
||||||
<span data-i18n="btnRooks">Türme</span>
|
<span data-i18n="btnRooks">Türme</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn active" onclick="setMode('queens')" id="btnQueens" type="button">
|
||||||
|
<span aria-hidden="true">♛</span>
|
||||||
|
<span data-i18n="btnQueens">Damen</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -82,13 +82,14 @@
|
|||||||
<p class="section-kicker" id="gameModeEyebrow">Damen-Modus</p>
|
<p class="section-kicker" id="gameModeEyebrow">Damen-Modus</p>
|
||||||
<h2 id="gameTitle">Damen-Spielbrett</h2>
|
<h2 id="gameTitle">Damen-Spielbrett</h2>
|
||||||
<p class="board-mode-text" id="boardModeText">
|
<p class="board-mode-text" id="boardModeText">
|
||||||
Platziere Damen so, dass keine zwei dieselbe Spalte oder Diagonale teilen.
|
Platziere Damen frei auf dem Brett. Konflikte entstehen in derselben Reihe, Spalte oder
|
||||||
|
Diagonale.
|
||||||
</p>
|
</p>
|
||||||
</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"
|
data-i18n="infoButton" data-i18n-aria-label="infoButton"
|
||||||
aria-label="Spielregeln und Hilfe anzeigen">Regeln & Hilfe</button>
|
aria-label="Spielregeln und Hilfe anzeigen">Spielanleitung</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="board-size-info" id="boardSizeInfo"><span id="boardSizeInfoPrefix">Aktuelle Brettgrösse im
|
<p class="board-size-info" id="boardSizeInfo"><span id="boardSizeInfoPrefix">Aktuelle Brettgrösse im
|
||||||
@@ -118,69 +119,31 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="board" id="board"></div>
|
<div class="board-stage">
|
||||||
|
<div class="board-shell">
|
||||||
|
<div id="board"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="board-navigator" id="boardNavigator" hidden>
|
||||||
|
<button class="btn secondary board-nav-button" onclick="previousSolverStep()" id="btnPrevStep"
|
||||||
|
type="button" data-i18n="btnPrevStep">
|
||||||
|
Zurück
|
||||||
|
</button>
|
||||||
|
<p class="solver-step-info" id="solverStepInfo">Schritt 0 / 0</p>
|
||||||
|
<button class="btn secondary board-nav-button" onclick="nextSolverStep()" id="btnNextStep"
|
||||||
|
type="button" data-i18n="btnNextStep">
|
||||||
|
Vorwärts
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="solver-message solver-message-inline" id="solverMessage">Lade eine Animation und steuere sie
|
||||||
|
am Brett oder spiele sie automatisch ab.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button class="btn secondary" onclick="resetBoard()" id="btnReset" type="button"
|
<button class="btn secondary" onclick="resetBoard()" id="btnReset" type="button"
|
||||||
data-i18n="btnReset">Brett
|
data-i18n="btnReset">Brett
|
||||||
zurücksetzen</button>
|
zurücksetzen</button>
|
||||||
<div class="controls-save-row">
|
|
||||||
<button class="btn" onclick="openSavePanel()" id="btnSave" type="button"
|
|
||||||
data-i18n="btnSave">Speichern</button>
|
|
||||||
<button class="btn secondary" onclick="openSavePointsPanel()" id="btnLoad" type="button"
|
|
||||||
data-i18n="btnLoad">Save-Points</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="solver-panel">
|
|
||||||
<div class="solver-panel-header">
|
|
||||||
<div>
|
|
||||||
<p class="section-kicker" id="solverKicker" data-i18n="solverKicker">Solver</p>
|
|
||||||
<h3 id="solverTitle" data-i18n="solverTitle">Lösungswerkzeuge</h3>
|
|
||||||
<p class="solver-message" id="solverMessage">Bereite einen Ablauf vor und spiele ihn Schritt
|
|
||||||
für Schritt ab.</p>
|
|
||||||
</div>
|
|
||||||
<p class="solver-step-info" id="solverStepInfo">Schritt 0 / 0</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="solver-groups">
|
|
||||||
<section class="solver-group">
|
|
||||||
<h4 id="animationSolverTitle" data-i18n="animationSolverTitle">Animations-Solver</h4>
|
|
||||||
<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
|
|
||||||
Animation automatisch ab.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="solver-controls">
|
|
||||||
<button class="btn" onclick="prepareSolutionTrace()" id="btnPrepareSolve" type="button"
|
|
||||||
data-i18n="btnPrepareSolve">Animation vorbereiten</button>
|
|
||||||
<button class="btn secondary" onclick="nextSolverStep()" id="btnNextStep" type="button"
|
|
||||||
data-i18n="btnNextStep">Nächster Schritt</button>
|
|
||||||
<button class="btn secondary" onclick="toggleSolverPlayback()" id="btnPlayPause"
|
|
||||||
type="button" data-i18n="btnPlayPause">Animation abspielen</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="solver-speed">
|
|
||||||
<label class="solver-speed-label" id="solverSpeedLabel" for="solverSpeedInput"
|
|
||||||
data-i18n="solverSpeedLabel">Animationsgeschwindigkeit</label>
|
|
||||||
<input id="solverSpeedInput" type="range" min="80" max="900" step="20" value="320"
|
|
||||||
oninput="updateSolverSpeed(this.value)">
|
|
||||||
<span class="solver-speed-value" id="solverSpeedValue">320 ms</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="solver-group instant-solver-group">
|
|
||||||
<h4 id="instantSolverTitle" data-i18n="instantSolverTitle">Sofort-Lösung</h4>
|
|
||||||
<p class="solver-group-text" id="instantSolverText" data-i18n="instantSolverText">
|
|
||||||
Lade direkt eine gültige Lösung, ohne Zwischenschritte oder Animation.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="instant-solver-actions">
|
|
||||||
<button class="btn accent" onclick="instantSolve()" id="btnInstantSolve" type="button"
|
|
||||||
data-i18n="btnInstantSolve">Sofort lösen</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -234,25 +197,68 @@
|
|||||||
|
|
||||||
<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" data-i18n="settingsSidebarTitle">Brett-Einstellungen</h3>
|
<h3 id="settingsSidebarTitle" data-i18n="settingsSidebarTitle">Spiel-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" data-i18n="settingsSidebarClose">Schliessen</button>
|
type="button" data-i18n="settingsSidebarClose">Schliessen</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section class="settings-section">
|
||||||
|
<p class="section-kicker" id="boardSettingsKicker" data-i18n="boardSettingsKicker">Brett</p>
|
||||||
|
<h4 id="boardSettingsTitle" data-i18n="boardSettingsTitle">Brettgrösse</h4>
|
||||||
<p id="settingsSidebarText" data-i18n="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"
|
<label class="sidebar-label" id="boardSizeInputLabel" for="boardSizeInput"
|
||||||
data-i18n="boardSizeInputLabel">Grösse</label>
|
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"
|
<button class="btn" onclick="applyBoardSize()" id="applyBoardSizeButton" type="button"
|
||||||
data-i18n="applyBoardSizeButton">Grösse anwenden</button>
|
data-i18n="applyBoardSizeButton">Grösse anwenden</button>
|
||||||
<p class="sidebar-hint" id="settingsSidebarHint" data-i18n="settingsSidebarHint">Beim Ändern wird das Brett zur
|
<p class="sidebar-hint" id="settingsSidebarHint" data-i18n="settingsSidebarHint">Beim Ändern wird das Brett
|
||||||
Sicherheit zurückgesetzt. Auf
|
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>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-section">
|
||||||
|
<p class="section-kicker" id="saveSettingsKicker" data-i18n="saveSettingsKicker">Speichern</p>
|
||||||
|
<h4 id="saveSettingsTitle" data-i18n="saveSettingsTitle">Fortschritt speichern</h4>
|
||||||
|
<p id="saveSettingsText" data-i18n="saveSettingsText">Öffne hier die Optionen zum Speichern oder Laden
|
||||||
|
deiner Save-Points.</p>
|
||||||
|
|
||||||
|
<div class="settings-action-grid">
|
||||||
|
<button class="btn" onclick="openSavePanel()" id="btnSave" type="button"
|
||||||
|
data-i18n="btnSave">Speichern</button>
|
||||||
|
<button class="btn secondary" onclick="openSavePointsPanel()" id="btnLoad" type="button"
|
||||||
|
data-i18n="btnLoad">Save-Points</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-section">
|
||||||
|
<p class="section-kicker" id="solutionSettingsKicker" data-i18n="solutionSettingsKicker">Lösung</p>
|
||||||
|
<h4 id="solutionSettingsTitle" data-i18n="solutionSettingsTitle">Lösungswerkzeuge</h4>
|
||||||
|
<p id="solutionSettingsText" data-i18n="solutionSettingsText">Lade eine Animation, steuere sie am Brett mit
|
||||||
|
Vorwärts und Zurück oder spiele sie automatisch ab.</p>
|
||||||
|
|
||||||
|
<div class="settings-action-grid">
|
||||||
|
<button class="btn" onclick="prepareSolutionTrace()" id="btnPrepareSolve" type="button"
|
||||||
|
data-i18n="btnPrepareSolve">Animation laden</button>
|
||||||
|
<button class="btn secondary" onclick="toggleSolverPlayback()" id="btnPlayPause" type="button"
|
||||||
|
data-i18n="btnPlayPause">Animation abspielen</button>
|
||||||
|
<button class="btn accent" onclick="instantSolve()" id="btnInstantSolve" type="button"
|
||||||
|
data-i18n="btnInstantSolve">Sofort lösen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="solver-speed">
|
||||||
|
<label class="solver-speed-label" id="solverSpeedLabel" for="solverSpeedInput"
|
||||||
|
data-i18n="solverSpeedLabel">Animationsgeschwindigkeit</label>
|
||||||
|
<input id="solverSpeedInput" type="range" min="80" max="900" step="20" value="320"
|
||||||
|
oninput="updateSolverSpeed(this.value)">
|
||||||
|
<span class="solver-speed-value" id="solverSpeedValue">320 ms</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<aside class="sidebar save-sidebar" id="saveSidebar" aria-hidden="true">
|
<aside class="sidebar save-sidebar" id="saveSidebar" aria-hidden="true">
|
||||||
@@ -338,23 +344,27 @@
|
|||||||
<div class="modal-backdrop" id="infoModalBackdrop" onclick="toggleInfoModal(false)"></div>
|
<div class="modal-backdrop" id="infoModalBackdrop" onclick="toggleInfoModal(false)"></div>
|
||||||
<section class="modal" id="infoModal" aria-hidden="true" aria-labelledby="infoModalTitle">
|
<section class="modal" id="infoModal" aria-hidden="true" aria-labelledby="infoModalTitle">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3 id="infoModalTitle">Spielregeln und Acht-Damen-Problem</h3>
|
<h3 id="infoModalTitle">Spielanleitung</h3>
|
||||||
<button class="btn secondary sidebar-close" onclick="toggleInfoModal(false)" type="button"
|
<button class="btn secondary sidebar-close" onclick="toggleInfoModal(false)" type="button"
|
||||||
data-i18n="infoModalClose">Schliessen</button>
|
data-i18n="infoModalClose">Schliessen</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<p data-i18n="infoModalIntro">Setze pro Reihe genau eine Figur auf das Brett. Eine gültige Lösung hat keine
|
<p data-i18n="infoModalIntro">Das Schachbrett besteht aus vielen quadratischen Feldern. Jedes Feld kann
|
||||||
Konflikte.</p>
|
leer sein oder genau eine Figur enthalten. Deine Aufgabe ist es, Figuren auf das Brett zu setzen,
|
||||||
|
ohne dass sie sich gegenseitig angreifen. Das Puzzle ist erst gelöst, wenn genau so viele Figuren auf
|
||||||
|
dem Brett stehen wie das Brett Felder pro Seite hat, also zum Beispiel 8 Figuren auf einem
|
||||||
|
8x8-Brett.</p>
|
||||||
|
|
||||||
<h4 data-i18n="infoModalModeHeading">So funktioniert der aktuelle Modus</h4>
|
<h4 data-i18n="infoModalModeHeading">Grundregeln</h4>
|
||||||
<p id="infoModeRuleText">Im Damen-Modus dürfen sich Damen weder in derselben Spalte noch diagonal
|
<p id="infoModeRuleText">In diesem Modus spielst du mit Damen. Eine Dame ist die Figur mit dem Symbol ♛.
|
||||||
angreifen. Mit einem Klick setzt oder entfernst du eine Figur.</p>
|
Sie darf keine andere Dame in derselben waagerechten Reihe, senkrechten Spalte oder schrägen
|
||||||
|
Diagonale treffen. Mit einem Klick setzt du eine Dame auf ein freies Feld oder entfernst sie wieder.</p>
|
||||||
|
|
||||||
<h4 id="infoModalProblemHeading">Was ist das Acht-Damen-Problem?</h4>
|
<h4 id="infoModalProblemHeading">Was ist eine Dame?</h4>
|
||||||
<p id="infoModalProblemText">Das klassische Acht-Damen-Problem fragt nach einer Anordnung von acht Damen
|
<p id="infoModalProblemText">Eine Dame ist eine Schachfigur. In diesem Spiel musst du keine Schachregeln
|
||||||
auf einem 8x8-Brett, bei der sich keine zwei Damen schlagen. Es ist ein bekanntes Beispiel für
|
lernen, sondern nur wissen: Eine Dame kann gerade und schräg über das Brett wirken. Darum müssen
|
||||||
kombinatorische Suche und Backtracking.</p>
|
Damen genug Abstand haben, damit sie sich nicht gegenseitig erreichen.</p>
|
||||||
|
|
||||||
<h4 data-i18n="infoModalTimeHeading">Was zeigen Zeit und Schritte?</h4>
|
<h4 data-i18n="infoModalTimeHeading">Was zeigen Zeit und Schritte?</h4>
|
||||||
<p data-i18n="infoModalTimeText">Der Schrittzähler erfasst deine Züge auf dem Brett. Die Zeit läuft für das
|
<p data-i18n="infoModalTimeText">Der Schrittzähler erfasst deine Züge auf dem Brett. Die Zeit läuft für das
|
||||||
@@ -364,7 +374,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/language.js') }}" defer></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/dashboard.js') }}" defer></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -76,6 +76,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
{% include "partials/footer.html" %}
|
{% include "partials/footer.html" %}
|
||||||
|
<script src="{{ url_for('static', filename='js/language.js') }}" defer></script>
|
||||||
<script src="{{ url_for('static', filename='js/legal.js') }}" defer></script>
|
<script src="{{ url_for('static', filename='js/legal.js') }}" defer></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
+37
-46
@@ -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 data-i18n="meta.title">8-Damen- und 8-Tuerme Solver</title>
|
<title data-i18n="meta.title">8-Damen- und 8-Türme Solver</title>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/index.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/footer.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/footer.css') }}">
|
||||||
</head>
|
</head>
|
||||||
@@ -13,16 +13,16 @@
|
|||||||
<header class="landing-nav">
|
<header class="landing-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 data-i18n="nav.brand">8-Damen- und 8-Tuerme Solver</span>
|
<span data-i18n="nav.brand">8-Damen- und 8-Türme Solver</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<nav class="landing-links">
|
<nav class="landing-links">
|
||||||
<a href="#problem" data-i18n="nav.problem">Problem</a>
|
<a href="#problem" data-i18n="nav.problem">Problem</a>
|
||||||
<a href="#features" data-i18n="nav.features">Features</a>
|
<a href="#features" data-i18n="nav.features">Funktionen</a>
|
||||||
<a href="#journey" data-i18n="nav.journey">Ablauf</a>
|
<a href="#journey" data-i18n="nav.journey">Ablauf</a>
|
||||||
<button class="lang-switch" id="langSwitch" type="button" aria-label="Sprache wechseln">DE</button>
|
<button class="lang-switch" id="langSwitch" type="button" aria-label="Sprache wechseln">DE</button>
|
||||||
<a class="nav-cta secondary" href="{{ url_for('login.login') }}" data-i18n="nav.login">Login</a>
|
<a class="nav-cta secondary" href="{{ url_for('login.login') }}" data-i18n="nav.login">Anmelden</a>
|
||||||
<a class="nav-cta" href="{{ url_for('login.register') }}" data-i18n="nav.signup">Create Account</a>
|
<a class="nav-cta" href="{{ url_for('login.register') }}" data-i18n="nav.signup">Account erstellen</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -33,42 +33,28 @@
|
|||||||
</p>
|
</p>
|
||||||
<h1 data-i18n="hero.title">Entdecke das Acht-Damen- und Acht-Türme-Problem.</h1>
|
<h1 data-i18n="hero.title">Entdecke das Acht-Damen- und Acht-Türme-Problem.</h1>
|
||||||
<p class="hero-text" data-i18n="hero.text">
|
<p class="hero-text" data-i18n="hero.text">
|
||||||
Diese App macht aus einem klassischen Informatik- und Mathematikproblem ein interaktives Erlebnis:
|
Diese Webanwendung macht aus einem klassischen Informatik- und Mathematikproblem ein interaktives
|
||||||
|
Erlebnis:
|
||||||
Setze Damen oder Türme, verfolge den Solver Schritt für Schritt, miss deine Zeit und speichere
|
Setze Damen oder Türme, verfolge den Solver Schritt für Schritt, miss deine Zeit und speichere
|
||||||
clevere Zwischenstände.
|
Zwischenstände.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="hero-actions">
|
<div class="hero-actions">
|
||||||
<a class="hero-button primary" href="{{ url_for('login.register') }}"
|
<a class="hero-button primary" href="{{ url_for('login.register') }}"
|
||||||
data-i18n="hero.primaryCta">Jetzt starten</a>
|
data-i18n="hero.primaryCta">Jetzt starten</a>
|
||||||
<a class="hero-button secondary" href="{{ url_for('login.login') }}"
|
<a class="hero-button secondary" href="{{ url_for('login.login') }}"
|
||||||
data-i18n="hero.secondaryCta">Bereits Account? Login</a>
|
data-i18n="hero.secondaryCta">Bereits ein Account? Anmelden</a>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hero-facts">
|
|
||||||
<article>
|
|
||||||
<strong data-i18n="hero.fact1Title">Skalierbar</strong>
|
|
||||||
<span data-i18n="hero.fact1Text">Brettgrösse flexibel anpassen</span>
|
|
||||||
</article>
|
|
||||||
<article>
|
|
||||||
<strong data-i18n="hero.fact2Title">Schritt für Schritt</strong>
|
|
||||||
<span data-i18n="hero.fact2Text">Solver mit Animation</span>
|
|
||||||
</article>
|
|
||||||
<article>
|
|
||||||
<strong data-i18n="hero.fact3Title">Save-Points</strong>
|
|
||||||
<span data-i18n="hero.fact3Text">Zeit und Fortschritt behalten</span>
|
|
||||||
</article>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hero-visual">
|
<div class="hero-visual">
|
||||||
<div class="board-showcase" id="heroBoard" aria-hidden="true"></div>
|
<div class="board-showcase" id="heroBoard" aria-hidden="true"></div>
|
||||||
<div class="floating-card solver-card">
|
<div class="floating-card solver-card">
|
||||||
<p class="floating-label" data-i18n="hero.solverLabel">Solver</p>
|
<p class="floating-label" data-i18n="hero.solverLabel">Lösungshelfer</p>
|
||||||
<p class="floating-value" data-i18n="hero.solverValue">Backtracking live</p>
|
<p class="floating-value" data-i18n="hero.solverValue">Backtracking in Echtzeit</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="floating-card timer-card">
|
<div class="floating-card timer-card">
|
||||||
<p class="floating-label" data-i18n="hero.challengeLabel">Challenge</p>
|
<p class="floating-label" data-i18n="hero.challengeLabel">Herausforderung</p>
|
||||||
<p class="floating-value" data-i18n="hero.challengeValue">Zeit und Schritte messen</p>
|
<p class="floating-value" data-i18n="hero.challengeValue">Zeit und Schritte messen</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -76,17 +62,21 @@
|
|||||||
|
|
||||||
<section class="info-band" id="problem">
|
<section class="info-band" id="problem">
|
||||||
<div class="info-band-inner">
|
<div class="info-band-inner">
|
||||||
<p class="eyebrow" data-i18n="problem.eyebrow">Warum dieses Problem spannend ist</p>
|
<p class="eyebrow" data-i18n="problem.eyebrow">Was ist das Acht-Damen-Problem?</p>
|
||||||
<h2 data-i18n="problem.title">Die Acht-Damen- und Acht-Tuerme-Probleme sind mehr als Schachrätsel.</h2>
|
<h2 data-i18n="problem.title">Eine klassische schachmathematische Aufgabe mit überraschend vielen
|
||||||
|
Lösungen.</h2>
|
||||||
<p data-i18n="problem.text">
|
<p data-i18n="problem.text">
|
||||||
Beim Acht-Damen-Problem suchst du eine Anordnung von acht Damen auf einem Schachbrett, sodass sich
|
Das Acht-Damen-Problem ist eine schachmathematische Aufgabe: Acht Damen sollen auf einem
|
||||||
keine zwei Damen schlagen. Beim Acht-Türme-Problem gilt dieselbe Idee für Türme, nur mit
|
Schachbrett so aufgestellt werden, dass keine zwei Damen dieselbe Reihe, Spalte oder Diagonale
|
||||||
einfacheren Konfliktregeln. Beide Aufgaben zeigen, wie Mustererkennung, systematische Suche und
|
teilen. Das Problem lässt sich auf beliebige Brettgrössen verallgemeinern, indem n Damen auf einem
|
||||||
Backtracking funktionieren.
|
n x n-Brett platziert werden.
|
||||||
<br><br>
|
<br><br>
|
||||||
Gerade beim Damenproblem wird schnell sichtbar, warum Strategie wichtig ist: Es gibt unzählige
|
Für das klassische 8x8-Brett gibt es 92 Lösungen. Zählt man Stellungen, die nur durch Spiegelung
|
||||||
mögliche Platzierungen, aber nur sehr wenige wirklich gültige Lösungen. Genau das macht diese
|
oder Drehung entstehen, als gleich, bleiben 12 eindeutige Lösungen übrig.
|
||||||
App spannend, weil man nicht nur das Ergebnis sieht, sondern auch den Weg dorthin.
|
<br><br>
|
||||||
|
<a href="https://de-academic.com/dic.nsf/dewiki/13248" target="_blank"
|
||||||
|
rel="noopener noreferrer">Quelle:
|
||||||
|
de-academic.com</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -101,7 +91,7 @@
|
|||||||
<article class="feature-card">
|
<article class="feature-card">
|
||||||
<span class="feature-icon">♟</span>
|
<span class="feature-icon">♟</span>
|
||||||
<h3 data-i18n="features.card1Title">Interaktives Brett</h3>
|
<h3 data-i18n="features.card1Title">Interaktives Brett</h3>
|
||||||
<p data-i18n="features.card1Text">Setze Damen oder Tuerme direkt auf dem Brett und prüfe, ob deine
|
<p data-i18n="features.card1Text">Setze Damen oder Türme direkt auf dem Brett und prüfe, ob deine
|
||||||
Anordnung konfliktfrei ist.</p>
|
Anordnung konfliktfrei ist.</p>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
@@ -109,7 +99,8 @@
|
|||||||
<span class="feature-icon">⚙</span>
|
<span class="feature-icon">⚙</span>
|
||||||
<h3 data-i18n="features.card2Title">Solver mit Animation</h3>
|
<h3 data-i18n="features.card2Title">Solver mit Animation</h3>
|
||||||
<p data-i18n="features.card2Text">Beobachte den Lösungsweg Schritt für Schritt und verstehe, wie
|
<p data-i18n="features.card2Text">Beobachte den Lösungsweg Schritt für Schritt und verstehe, wie
|
||||||
Backtracking arbeitet.</p>
|
Backtracking arbeitet.
|
||||||
|
</p>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="feature-card">
|
<article class="feature-card">
|
||||||
@@ -121,8 +112,8 @@
|
|||||||
|
|
||||||
<article class="feature-card">
|
<article class="feature-card">
|
||||||
<span class="feature-icon">💾</span>
|
<span class="feature-icon">💾</span>
|
||||||
<h3 data-i18n="features.card4Title">Save-Points</h3>
|
<h3 data-i18n="features.card4Title">Speicherstände</h3>
|
||||||
<p data-i18n="features.card4Text">Speichere Zwischenstaende mit Notizen, Zeiten und Schrittzahl für
|
<p data-i18n="features.card4Text">Speichere Zwischenstände mit Notizen, Zeiten und Schrittzahl für
|
||||||
späteres Weiterarbeiten.</p>
|
späteres Weiterarbeiten.</p>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
@@ -145,8 +136,8 @@
|
|||||||
<article class="journey-step">
|
<article class="journey-step">
|
||||||
<span class="step-number">01</span>
|
<span class="step-number">01</span>
|
||||||
<h3 data-i18n="journey.step1Title">Account erstellen</h3>
|
<h3 data-i18n="journey.step1Title">Account erstellen</h3>
|
||||||
<p data-i18n="journey.step1Text">Registriere dich und erhalte Zugang zu Dashboard, Save-Points und
|
<p data-i18n="journey.step1Text">Registriere dich und erhalte Zugang zum Dashboard, zu
|
||||||
deinem Fortschritt.</p>
|
Speicherständen und zu deinem Fortschritt.</p>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="journey-step">
|
<article class="journey-step">
|
||||||
@@ -160,8 +151,7 @@
|
|||||||
<span class="step-number">03</span>
|
<span class="step-number">03</span>
|
||||||
<h3 data-i18n="journey.step3Title">Lösen oder analysieren</h3>
|
<h3 data-i18n="journey.step3Title">Lösen oder analysieren</h3>
|
||||||
<p data-i18n="journey.step3Text">Teste eigene Ideen oder lass dir die Lösung visuell vom Solver
|
<p data-i18n="journey.step3Text">Teste eigene Ideen oder lass dir die Lösung visuell vom Solver
|
||||||
lösen lassen, dabei wird 1:1
|
zeigen, damit du den Lösungsweg besser nachvollziehen kannst.</p>
|
||||||
gezeigt wie der Solver vorgeht.</p>
|
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -177,9 +167,9 @@
|
|||||||
|
|
||||||
<div class="hero-actions">
|
<div class="hero-actions">
|
||||||
<a class="hero-button primary" href="{{ url_for('login.register') }}"
|
<a class="hero-button primary" href="{{ url_for('login.register') }}"
|
||||||
data-i18n="cta.primaryCta">Create Account</a>
|
data-i18n="cta.primaryCta">Account erstellen</a>
|
||||||
<a class="hero-button secondary" href="{{ url_for('login.login') }}"
|
<a class="hero-button secondary" href="{{ url_for('login.login') }}"
|
||||||
data-i18n="cta.secondaryCta">Login</a>
|
data-i18n="cta.secondaryCta">Anmelden</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -190,7 +180,8 @@
|
|||||||
<script>
|
<script>
|
||||||
window.LANDING_INITIAL_LANGUAGE = "{{ initial_language }}";
|
window.LANDING_INITIAL_LANGUAGE = "{{ initial_language }}";
|
||||||
</script>
|
</script>
|
||||||
<script src="{{ url_for('static', filename='js/index.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/language.js') }}" defer></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/index.js') }}" defer></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -13,19 +13,20 @@
|
|||||||
<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 class="nav-brand" 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('login.login') }}">Login</a></li>
|
<li><a href="{{ url_for('login.login') }}" data-i18n="navLogin">Login</a></li>
|
||||||
<li><a href="{{ url_for('login.register') }}">Create account</a></li>
|
<li><a href="{{ url_for('login.register') }}" data-i18n="navRegister">Create account</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main class="auth-shell">
|
<main class="auth-shell">
|
||||||
<section class="auth-panel">
|
<section class="auth-panel">
|
||||||
<h1>Login to your account</h1>
|
<h1>Login to your account</h1>
|
||||||
<p class="muted">If two-factor authentication is enabled, enter the current 6-digit code from your authenticator app after the password check.</p>
|
<p class="muted">If two-factor authentication is enabled, enter the current 6-digit code from your
|
||||||
|
authenticator app after the password check.</p>
|
||||||
|
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
@@ -53,6 +54,7 @@
|
|||||||
|
|
||||||
{% include "partials/footer.html" %}
|
{% include "partials/footer.html" %}
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/language.js') }}" defer></script>
|
||||||
<script src="{{ url_for('static', filename='js/login.js') }}" defer></script>
|
<script src="{{ url_for('static', filename='js/login.js') }}" defer></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -155,6 +155,7 @@
|
|||||||
|
|
||||||
{% include "partials/footer.html" %}
|
{% include "partials/footer.html" %}
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/language.js') }}" defer></script>
|
||||||
<script src="{{ url_for('static', filename='js/login.js') }}" defer></script>
|
<script src="{{ url_for('static', filename='js/login.js') }}" defer></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -126,6 +126,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
{% include "partials/footer.html" %}
|
{% include "partials/footer.html" %}
|
||||||
|
<script src="{{ url_for('static', filename='js/language.js') }}" defer></script>
|
||||||
<script src="{{ url_for('static', filename='js/legal.js') }}" defer></script>
|
<script src="{{ url_for('static', filename='js/legal.js') }}" defer></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,12 @@
|
|||||||
<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('login.login') }}">Login</a></li>
|
<li><a href="{{ url_for('login.login') }}" data-i18n="navLogin">Login</a></li>
|
||||||
<li><a href="{{ url_for('login.register') }}">Create account</a></li>
|
<li><a href="{{ url_for('login.register') }}" data-i18n="navRegister">Create account</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -54,6 +54,7 @@
|
|||||||
|
|
||||||
{% include "partials/footer.html" %}
|
{% include "partials/footer.html" %}
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/language.js') }}" defer></script>
|
||||||
<script src="{{ url_for('static', filename='js/login.js') }}" defer></script>
|
<script src="{{ url_for('static', filename='js/login.js') }}" defer></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,12 @@
|
|||||||
<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('login.login') }}">Login</a></li>
|
<li><a href="{{ url_for('login.login') }}" data-i18n="navLogin">Login</a></li>
|
||||||
<li><a href="{{ url_for('login.register') }}">Create account</a></li>
|
<li><a href="{{ url_for('login.register') }}" data-i18n="navRegister">Create account</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -51,6 +51,7 @@
|
|||||||
|
|
||||||
{% include "partials/footer.html" %}
|
{% include "partials/footer.html" %}
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='js/language.js') }}" defer></script>
|
||||||
<script src="{{ url_for('static', filename='js/login.js') }}" defer></script>
|
<script src="{{ url_for('static', filename='js/login.js') }}" defer></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user