from typing import Dict AVAILABLE_LANGUAGES: Dict[str, str] = { "de": "Deutsch", "en": "English", } translations: Dict[str, Dict[str, str]] = { "de": { "brand.tagline": "Deine gemieteten Server im Blick", "nav.admin": "Admin-Dashboard", "nav.users": "User", "nav.overview": "Übersicht", "nav.map": "Karte", "nav.archive": "Archiv", "nav.new_server": "Server anlegen", "nav.logout": "Logout", "nav.login": "Login", "nav.register": "Registrieren", "nav.logged_in_as": "Eingeloggt als", "nav.console": "Konsole öffnen", "footer.left": "Selfhosted VPS overview", "footer.right": "PWA ready · Dark Mode", "warning.no_encryption": "Hinweis: ENCRYPTION_KEY nicht gesetzt – Passwörter werden nicht gespeichert.", "warning.no_encryption_short": "ENCRYPTION_KEY ist nicht gesetzt – eingegebene Management-Passwörter werden nicht gespeichert.", "server_list.title": "Deine Server", "server_list.subtitle": "Übersicht aller gemieteten VPS, dedizierten Server und Storage-Systeme.", "stats.total": "Gesamt", "stats.active": "aktive Server", "stats.costs": "Laufende Kosten", "stats.per_month": "pro Monat", "stats.mixed_currencies": "(gemischte Währungen)", "stats.expiring_soon": "Laufen bald aus", "stats.expired": "Abgelaufen", "stats.expiring_soon_hint": "≤ 30 Tage", "stats.expired_hint": "Vertrag beendet", "table.name": "Name", "table.provider": "Provider", "table.type": "Typ", "table.location": "Location", "table.ipv4": "IPv4", "table.costs": "Kosten", "table.action": "Aktion", "status.expired": "abgelaufen", "status.expiring": "läuft bald aus", "server_list.empty": "Noch keine Server erfasst. Leg den ersten mit „Server anlegen“ oben rechts an.", "price.month": "Monat", "price.year": "Jahr", "archive.title": "Archivierte Server", "archive.subtitle": "Ausgeblendete (archivierte) Server. Hier kannst du sie wiederherstellen.", "archive.restore": "Wiederherstellen", "archive.empty": "Keine archivierten Server vorhanden.", "server_detail.server": "Server", "server_detail.contract_end": "Vertragsende", "server_detail.days_ago": "vor {days} Tagen", "server_detail.today": "heute", "server_detail.in_days": "in {days} Tagen", "server_detail.edit": "Bearbeiten", "server_detail.archive": "Archivieren", "server_detail.archive_confirm": "Diesen Server archivieren?", "section.net_costs": "Netz & Kosten", "section.hardware": "Hardware", "section.access": "Zugänge", "section.notes": "Notizen", "field.ipv4": "IPv4", "field.ipv6": "IPv6", "field.costs": "Kosten", "field.contract": "Vertrag", "field.cpu": "CPU", "field.ram": "RAM", "field.storage": "Storage", "field.management": "Management", "field.mgmt_user": "Mgmt-User", "field.mgmt_password": "Mgmt-Passwort", "field.ssh_user": "SSH User", "field.ssh_key_hint": "SSH Key Hint", "note.ssh_keys": "Hinweis: Es werden nur Key-Namen gespeichert, keine privaten SSH-Schlüssel.", "notes.empty": "Keine Notizen.", "mgmt.password_encrypted_missing": "verschlüsselt gespeichert (Key fehlt?)", "back.overview": "Zurück zur Übersicht", "updated_at": "Zuletzt aktualisiert:", "form.title.new": "Neuen Server anlegen", "form.title.edit": "Server bearbeiten", "form.subtitle": "Trage alle relevanten Infos zu deinem VPS / Server ein.", "section.general": "Allgemein", "label.name": "Name", "label.provider": "Provider", "label.hostname": "Hostname", "label.type": "Typ", "label.location": "Location", "label.tags": "Tags (kommagetrennt)", "label.ipv4": "IPv4", "label.ipv6": "IPv6", "section.network": "Netzwerk", "section.costs": "Kosten", "label.amount": "Betrag", "label.currency": "Währung", "label.billing": "Abrechnung", "label.contract_start": "Vertragsbeginn", "label.contract_end": "Vertragsende", "section.hardware.small": "Hardware", "label.cpu_model": "CPU-Modell", "label.cpu_cores": "CPU Cores", "label.ram_mb": "RAM (MB)", "label.storage_gb": "Storage (GB)", "label.storage_type": "Storage-Typ", "section.access.small": "Zugänge", "hint.ssh": "SSH: hier nur Key-Namen oder Hints eintragen, keine privaten Keys.", "label.mgmt_url": "Management URL", "label.mgmt_user": "Management User", "label.mgmt_password": "Management Passwort", "label.ssh_user": "SSH User", "label.ssh_key_hint": "SSH Key Hint", "section.notes.small": "Notizen", "btn.cancel": "Abbrechen", "btn.save": "Speichern", "btn.save_changes": "Änderungen speichern", "map.title": "Server-Karte", "map.subtitle": "Zeigt alle nicht archivierten Server mit gesetzter Location auf einer Karte. Für grobe Übersicht reicht die Stadt – keine exakten GPS-Daten notwendig.", "map.note": "Hinweis: Marker werden anhand der Location-Namen grob auf der Weltkarte platziert. Mehrere Server in derselben Stadt werden leicht versetzt dargestellt, damit sie klickbar bleiben.", "admin.title": "Admin-Dashboard", "admin.subtitle": "Globale Übersicht über alle nicht archivierten Server und Benutzer.", "admin.users": "Benutzer", "admin.users_caption": "Accounts", "admin.servers": "Server", "admin.servers_caption": "nicht archiviert", "admin.costs": "Laufende Kosten", "admin.per_month": "pro Monat", "admin.contract_status": "Vertragstatus", "admin.expiring": "Bald auslaufend", "admin.expired": "Abgelaufen", "admin.by_provider": "Nach Provider", "admin.provider": "Provider", "admin.count": "Server", "admin.monthly_costs": "Monatskosten", "admin.expiring_soon": "Laufen bald aus", "admin.expired_count": "Abgelaufen", "admin.contracts_expiring": "Laufen bald aus (≤ 30 Tage)", "admin.contracts_none_soon": "Keine Verträge laufen in den nächsten 30 Tagen aus.", "admin.contracts_expired": "Abgelaufene Verträge", "admin.contracts_none_expired": "Keine abgelaufenen Verträge gefunden.", "users.title": "Benutzerverwaltung", "users.subtitle": "Admins können Benutzer aktivieren/deaktivieren. Der eigene Account kann nicht deaktiviert werden.", "users.username": "Benutzername", "users.email": "E-Mail", "users.role": "Rolle", "users.status": "Status", "users.action": "Aktion", "role.admin": "Admin", "role.user": "User", "status.active": "aktiv", "status.inactive": "deaktiviert", "action.deactivate": "Deaktivieren", "action.activate": "Aktivieren", "users.own_account": "Eigener Account", "login.title": "Login", "login.subtitle": "Melde dich an, um deine Server zu verwalten.", "login.username": "Benutzername", "login.password": "Passwort", "login.no_account": "Noch kein Account?", "login.register": "Registrieren", "login.submit": "Einloggen", "register.title": "Registrieren", "register.subtitle": "Erstelle einen neuen Account. Der erste Benutzer wird automatisch Admin.", "register.username": "Benutzername", "register.email": "E-Mail (optional)", "register.password": "Passwort", "register.password_confirm": "Passwort bestätigen", "register.submit": "Account anlegen", }, "en": { "brand.tagline": "Your rented servers at a glance", "nav.admin": "Admin Dashboard", "nav.users": "Users", "nav.overview": "Overview", "nav.map": "Map", "nav.archive": "Archive", "nav.new_server": "Add server", "nav.logout": "Logout", "nav.login": "Login", "nav.register": "Register", "nav.logged_in_as": "Logged in as", "nav.console": "Open console", "footer.left": "Selfhosted VPS overview", "footer.right": "PWA ready · Dark Mode", "warning.no_encryption": "Note: ENCRYPTION_KEY not set – passwords will not be stored.", "warning.no_encryption_short": "ENCRYPTION_KEY is not set – management passwords will not be stored.", "server_list.title": "Your servers", "server_list.subtitle": "Overview of all rented VPS, dedicated servers and storage systems.", "stats.total": "Total", "stats.active": "active servers", "stats.costs": "Recurring costs", "stats.per_month": "per month", "stats.mixed_currencies": "(mixed currencies)", "stats.expiring_soon": "Expiring soon", "stats.expired": "Expired", "stats.expiring_soon_hint": "≤ 30 days", "stats.expired_hint": "Contract ended", "table.name": "Name", "table.provider": "Provider", "table.type": "Type", "table.location": "Location", "table.ipv4": "IPv4", "table.costs": "Costs", "table.action": "Action", "status.expired": "expired", "status.expiring": "expiring soon", "server_list.empty": "No servers yet. Create the first one via “Add server” in the top right.", "price.month": "Month", "price.year": "Year", "archive.title": "Archived servers", "archive.subtitle": "Hidden (archived) servers. Restore them here.", "archive.restore": "Restore", "archive.empty": "No archived servers found.", "server_detail.server": "Server", "server_detail.contract_end": "Contract end", "server_detail.days_ago": "{days} days ago", "server_detail.today": "today", "server_detail.in_days": "in {days} days", "server_detail.edit": "Edit", "server_detail.archive": "Archive", "server_detail.archive_confirm": "Archive this server?", "section.net_costs": "Network & Costs", "section.hardware": "Hardware", "section.access": "Access", "section.notes": "Notes", "field.ipv4": "IPv4", "field.ipv6": "IPv6", "field.costs": "Costs", "field.contract": "Contract", "field.cpu": "CPU", "field.ram": "RAM", "field.storage": "Storage", "field.management": "Management", "field.mgmt_user": "Mgmt user", "field.mgmt_password": "Mgmt password", "field.ssh_user": "SSH user", "field.ssh_key_hint": "SSH key hint", "note.ssh_keys": "Note: Only key names are stored, no private SSH keys.", "notes.empty": "No notes.", "mgmt.password_encrypted_missing": "stored encrypted (key missing?)", "back.overview": "Back to overview", "updated_at": "Last updated:", "form.title.new": "Create new server", "form.title.edit": "Edit server", "form.subtitle": "Enter all relevant details about your VPS / server.", "section.general": "General", "label.name": "Name", "label.provider": "Provider", "label.hostname": "Hostname", "label.type": "Type", "label.location": "Location", "label.tags": "Tags (comma-separated)", "label.ipv4": "IPv4", "label.ipv6": "IPv6", "section.network": "Network", "section.costs": "Costs", "label.amount": "Amount", "label.currency": "Currency", "label.billing": "Billing", "label.contract_start": "Contract start", "label.contract_end": "Contract end", "section.hardware.small": "Hardware", "label.cpu_model": "CPU model", "label.cpu_cores": "CPU cores", "label.ram_mb": "RAM (MB)", "label.storage_gb": "Storage (GB)", "label.storage_type": "Storage type", "section.access.small": "Access", "hint.ssh": "SSH: only enter key names or hints, no private keys.", "label.mgmt_url": "Management URL", "label.mgmt_user": "Management user", "label.mgmt_password": "Management password", "label.ssh_user": "SSH user", "label.ssh_key_hint": "SSH key hint", "section.notes.small": "Notes", "btn.cancel": "Cancel", "btn.save": "Save", "btn.save_changes": "Save changes", "map.title": "Server map", "map.subtitle": "Shows all non-archived servers with a location on a map. City names are sufficient for rough positioning.", "map.note": "Note: Markers are placed roughly based on location names. Multiple servers in one city are slightly offset to remain clickable.", "admin.title": "Admin dashboard", "admin.subtitle": "Global overview of all non-archived servers and users.", "admin.users": "Users", "admin.users_caption": "Accounts", "admin.servers": "Servers", "admin.servers_caption": "not archived", "admin.costs": "Recurring costs", "admin.per_month": "per month", "admin.contract_status": "Contract status", "admin.expiring": "Expiring soon", "admin.expired": "Expired", "admin.by_provider": "By provider", "admin.provider": "Provider", "admin.count": "Servers", "admin.monthly_costs": "Monthly costs", "admin.expiring_soon": "Expiring soon", "admin.expired_count": "Expired", "admin.contracts_expiring": "Expiring soon (≤ 30 days)", "admin.contracts_none_soon": "No contracts expiring in the next 30 days.", "admin.contracts_expired": "Expired contracts", "admin.contracts_none_expired": "No expired contracts found.", "users.title": "User management", "users.subtitle": "Admins can activate/deactivate users. You cannot deactivate your own account.", "users.username": "Username", "users.email": "Email", "users.role": "Role", "users.status": "Status", "users.action": "Action", "role.admin": "Admin", "role.user": "User", "status.active": "active", "status.inactive": "deactivated", "action.deactivate": "Deactivate", "action.activate": "Activate", "users.own_account": "Own account", "login.title": "Login", "login.subtitle": "Sign in to manage your servers.", "login.username": "Username", "login.password": "Password", "login.no_account": "No account yet?", "login.register": "Register", "login.submit": "Log in", "register.title": "Register", "register.subtitle": "Create a new account. The first user becomes admin automatically.", "register.username": "Username", "register.email": "Email (optional)", "register.password": "Password", "register.password_confirm": "Confirm password", "register.submit": "Create account", }, } def resolve_locale(request) -> str: """Detect locale from session override or Accept-Language; default to de.""" try: if "session" in request.scope: session_lang = request.session.get("lang") if session_lang in AVAILABLE_LANGUAGES: return session_lang except Exception: pass accept = request.headers.get("accept-language", "") for part in accept.split(","): code = part.split(";")[0].strip().lower() if code.startswith("de"): return "de" if code.startswith("en"): return "en" return "de" def translate(key: str, locale: str = "de", **kwargs) -> str: """Return translated string for a key.""" text = translations.get(locale, {}).get( key, translations["en"].get(key, key) ) try: return text.format(**kwargs) except Exception: return text