Files
ebbe.bassandClaude Sonnet 5.5 0dc079a8ac feat: add page shell, preferences and translations
Base layout with top bar, role-based menu, profile menu and phone layout; theme, language and German form of address kept in cookies; German formal, German informal and English text; Fluent avatar bundled in the image by a Node build stage; placeholder pages for the Publisher and Admin groups.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3t4WL7bd9bdHHNjEsxzqC
2026-10-17 21:05:40 +02:00

83 lines
2.5 KiB
Python

"""Per-device preferences: theme, language and German form of address (AD-15).
They live in cookies and are never stored on the server. Values are checked against fixed
lists, so a forged cookie can only fall back to the default.
"""
from __future__ import annotations
from dataclasses import dataclass
from django.http import HttpRequest, HttpResponse
from django.utils import translation
THEME_COOKIE = "pm_theme"
LANGUAGE_COOKIE = "pm_lang"
FORM_COOKIE = "pm_form"
COOKIE_MAX_AGE = 365 * 24 * 60 * 60
THEMES = ("light", "dark", "system")
LANGUAGES = ("de", "en")
FORMS = ("sie", "du")
DEFAULT_THEME = "system"
DEFAULT_FORM = "sie"
INFORMAL_GERMAN = "de-du" # catalogue with only the informal differences; falls back to "de"
_CHOICES = {
THEME_COOKIE: ("theme", THEMES),
LANGUAGE_COOKIE: ("language", LANGUAGES),
FORM_COOKIE: ("form", FORMS),
}
@dataclass(frozen=True)
class Preferences:
theme: str = DEFAULT_THEME
language: str = "en"
form: str = DEFAULT_FORM
@property
def language_code(self) -> str:
"""The code the translations are activated with."""
if self.language == "de" and self.form == "du":
return INFORMAL_GERMAN
return self.language
def from_request(request: HttpRequest) -> Preferences:
theme = request.COOKIES.get(THEME_COOKIE)
language = request.COOKIES.get(LANGUAGE_COOKIE)
form = request.COOKIES.get(FORM_COOKIE)
if language not in LANGUAGES:
# Default from Accept-Language: German or English, English otherwise.
language = translation.get_language_from_request(request, check_path=False)
language = language if language in LANGUAGES else "en"
return Preferences(
theme=theme if theme in THEMES else DEFAULT_THEME,
language=language,
form=form if form in FORMS else DEFAULT_FORM,
)
def chosen(values) -> dict[str, str]:
"""The cookie name and value for every valid preference in `values` (a POST or a dict)."""
result = {}
for cookie, (field, allowed) in _CHOICES.items():
value = values.get(field)
if value in allowed:
result[cookie] = value
return result
def set_cookies(response: HttpResponse, cookies: dict[str, str], *, secure: bool) -> None:
for name, value in cookies.items():
response.set_cookie(
name,
value,
max_age=COOKIE_MAX_AGE,
secure=secure,
httponly=False, # shell.js applies a change without a reload
samesite="Lax",
)