//! Multi-language support for the admin dashboard.
//!
//! Language is selected per-browser via the `admin_lang` cookie (set by the
//! language switcher in the sidebar / login page). Falls back to the
//! `Accept-Language` header, then English.
//!
//! All translations live in this file as a big `match` table — no external
//! resource files, no `phf` dependency, easy to grep. To add a new key,
//! drop a new arm in `t()`. To add a new language, extend the `Lang` enum
//! and add a branch to every `match lang` block.
//!
//! Format-string keys use `{0}` / `{1}` placeholders that callers fill in
//! via `tf1` / `tf2`. We don't use `format!()` because the template comes
//! from a runtime lookup (not a literal).

use crate::api::error::ApiError;
use async_trait::async_trait;
use axum::extract::{FromRequest, RequestParts};
use axum::http::header;

/// Supported UI languages. Default = English.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Lang {
    En,
    De,
    Fr,
    Ro,
    Es,
}

impl Lang {
    pub fn code(self) -> &'static str {
        match self {
            Lang::En => "en",
            Lang::De => "de",
            Lang::Fr => "fr",
            Lang::Ro => "ro",
            Lang::Es => "es",
        }
    }

    pub fn parse(s: &str) -> Option<Self> {
        match s.trim().to_ascii_lowercase().as_str() {
            "en" | "en-us" | "en-gb" => Some(Lang::En),
            "de" | "de-de" | "de-ch" | "de-at" => Some(Lang::De),
            "fr" | "fr-fr" | "fr-ch" | "fr-be" | "fr-ca" => Some(Lang::Fr),
            "ro" | "ro-ro" | "ro-md" => Some(Lang::Ro),
            "es" | "es-es" | "es-mx" | "es-ar" | "es-cl" | "es-co" | "es-pe" | "es-419" => {
                Some(Lang::Es)
            }
            _ => None,
        }
    }

    /// Cookie name we look at and write.
    pub const COOKIE: &'static str = "admin_lang";
}

impl Default for Lang {
    fn default() -> Self {
        Lang::En
    }
}

/// Resolve the user's preferred language from the `admin_lang` cookie first,
/// then `Accept-Language`, otherwise English. Pure function so both the
/// extractor and standalone serve handlers can call it.
pub fn lang_from_headers(headers: &axum::http::HeaderMap) -> Lang {
    if let Some(cookie_hdr) = headers.get(header::COOKIE) {
        if let Ok(s) = cookie_hdr.to_str() {
            for pair in s.split(';') {
                if let Some((name, value)) = pair.trim().split_once('=') {
                    if name.trim() == Lang::COOKIE {
                        if let Some(l) = Lang::parse(value.trim()) {
                            return l;
                        }
                    }
                }
            }
        }
    }
    if let Some(al) = headers.get(header::ACCEPT_LANGUAGE) {
        if let Ok(s) = al.to_str() {
            // First language tag in the list (ignore q= weights — good enough).
            for tag in s.split(',') {
                let primary = tag.split(';').next().unwrap_or("").trim();
                if let Some(l) = Lang::parse(primary) {
                    return l;
                }
            }
        }
    }
    Lang::En
}

#[async_trait]
impl<B: Send> FromRequest<B> for Lang {
    type Rejection = ApiError;

    async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
        Ok(lang_from_headers(req.headers()))
    }
}

// ---------- translations ----------

/// Look up a translation key. Returns `<missing translation>` if the key is
/// unknown — that's deliberately ugly so any gap shows up in the UI rather
/// than silently falling back to English.
pub fn t(lang: Lang, key: &str) -> &'static str {
    let (en, de, fr, ro, es) = match key {
        // ---- generic ----
        "common.loading" => (
            "Loading…",
            "Wird geladen…",
            "Chargement…",
            "Se încarcă…",
            "Cargando…",
        ),
        "common.create" => ("Create", "Erstellen", "Créer", "Creează", "Crear"),
        "common.save" => ("Save", "Speichern", "Enregistrer", "Salvează", "Guardar"),
        "common.delete" => ("Delete", "Löschen", "Supprimer", "Șterge", "Eliminar"),
        "common.add" => ("Add", "Hinzufügen", "Ajouter", "Adaugă", "Añadir"),
        "common.remove" => ("Remove", "Entfernen", "Retirer", "Elimină", "Quitar"),
        "common.update" => (
            "Update",
            "Aktualisieren",
            "Mettre à jour",
            "Actualizează",
            "Actualizar",
        ),
        "common.cancel" => ("Cancel", "Abbrechen", "Annuler", "Anulează", "Cancelar"),
        "common.confirm" => (
            "Confirm",
            "Bestätigen",
            "Confirmer",
            "Confirmă",
            "Confirmar",
        ),
        "common.actions" => ("Actions", "Aktionen", "Actions", "Acțiuni", "Acciones"),
        "common.back" => (
            "← Back",
            "← Zurück",
            "← Retour",
            "← Înapoi",
            "← Volver",
        ),
        "common.never" => ("never", "nie", "jamais", "niciodată", "nunca"),
        "common.already_gone" => (
            "Already gone.",
            "Bereits entfernt.",
            "Déjà supprimé.",
            "Deja șters.",
            "Ya se eliminó.",
        ),
        "common.not_found" => (
            "Not found.",
            "Nicht gefunden.",
            "Introuvable.",
            "Negăsit.",
            "No encontrado.",
        ),
        "common.language" => ("Language", "Sprache", "Langue", "Limbă", "Idioma"),

        // ---- shell / sidebar ----
        "shell.app_title" => (
            "RustDesk Admin",
            "RustDesk Admin",
            "RustDesk Admin",
            "RustDesk Admin",
            "RustDesk Admin",
        ),
        "nav.users" => (
            "Users",
            "Benutzer",
            "Utilisateurs",
            "Utilizatori",
            "Usuarios",
        ),
        "nav.devices" => (
            "Devices",
            "Geräte",
            "Appareils",
            "Dispozitive",
            "Dispositivos",
        ),
        "nav.groups" => (
            "Device groups",
            "Gerätegruppen",
            "Groupes d'appareils",
            "Grupuri de dispozitive",
            "Grupos de dispositivos",
        ),
        "nav.strategies" => (
            "Strategies",
            "Strategien",
            "Stratégies",
            "Strategii",
            "Estrategias",
        ),
        "nav.address_books" => (
            "Address books",
            "Adressbücher",
            "Carnets d'adresses",
            "Agende",
            "Libretas de direcciones",
        ),
        "nav.audit" => (
            "Audit log",
            "Audit-Protokoll",
            "Journal d'audit",
            "Jurnal de audit",
            "Registro de auditoría",
        ),
        "nav.deploy" => (
            "Deploy",
            "Bereitstellen",
            "Déployer",
            "Implementare",
            "Implementación",
        ),
        "nav.profile" => (
            "My profile",
            "Mein Profil",
            "Mon profil",
            "Profilul meu",
            "Mi perfil",
        ),
        "nav.signout" => (
            "Sign out",
            "Abmelden",
            "Se déconnecter",
            "Deconectare",
            "Cerrar sesión",
        ),
        "nav.signed_in_as" => (
            "Signed in as",
            "Angemeldet als",
            "Connecté en tant que",
            "Conectat ca",
            "Conectado como",
        ),

        // ---- login ----
        "login.title" => (
            "RustDesk Admin",
            "RustDesk Admin",
            "RustDesk Admin",
            "RustDesk Admin",
            "RustDesk Admin",
        ),
        "login.subtitle" => (
            "Sign in to manage the server.",
            "Melden Sie sich an, um den Server zu verwalten.",
            "Connectez-vous pour gérer le serveur.",
            "Conectați-vă pentru a administra serverul.",
            "Inicia sesión para administrar el servidor.",
        ),
        "login.username" => (
            "Username",
            "Benutzername",
            "Nom d'utilisateur",
            "Utilizator",
            "Usuario",
        ),
        "login.password" => (
            "Password",
            "Passwort",
            "Mot de passe",
            "Parolă",
            "Contraseña",
        ),
        "login.totp_label" => (
            "6-digit TOTP code",
            "6-stelliger TOTP-Code",
            "Code TOTP à 6 chiffres",
            "Cod TOTP din 6 cifre",
            "Código TOTP de 6 dígitos",
        ),
        "login.signin" => (
            "Sign in",
            "Anmelden",
            "Se connecter",
            "Conectare",
            "Iniciar sesión",
        ),
        "login.or" => ("or", "oder", "ou", "sau", "o"),
        "login.signin_with" => (
            "Sign in with",
            "Anmelden mit",
            "Se connecter avec",
            "Conectare cu",
            "Iniciar sesión con",
        ),
        "login.bad_credentials" => (
            "Bad credentials",
            "Ungültige Anmeldedaten",
            "Identifiants incorrects",
            "Date de autentificare incorecte",
            "Credenciales incorrectas",
        ),
        "login.account_disabled" => (
            "Account disabled",
            "Konto deaktiviert",
            "Compte désactivé",
            "Cont dezactivat",
            "Cuenta deshabilitada",
        ),
        "login.admin_required" => (
            "Admin access required",
            "Administrator-Zugriff erforderlich",
            "Accès administrateur requis",
            "Acces de administrator necesar",
            "Se requiere acceso de administrador",
        ),
        "login.bad_totp" => (
            "Bad TOTP code",
            "Ungültiger TOTP-Code",
            "Code TOTP incorrect",
            "Cod TOTP incorect",
            "Código TOTP incorrecto",
        ),
        "login.totp_required" => (
            "Enter your 6-digit authenticator code.",
            "Geben Sie den 6-stelligen Authentifizierungs-Code ein.",
            "Saisissez votre code d'authentification à 6 chiffres.",
            "Introduceți codul de autentificare din 6 cifre.",
            "Introduce tu código de autenticación de 6 dígitos.",
        ),
        "login.internal_totp" => (
            "Internal TOTP error",
            "Interner TOTP-Fehler",
            "Erreur TOTP interne",
            "Eroare TOTP internă",
            "Error TOTP interno",
        ),

        // ---- users ----
        "users.heading" => (
            "Users",
            "Benutzer",
            "Utilisateurs",
            "Utilizatori",
            "Usuarios",
        ),
        "users.create_heading" => (
            "Create user",
            "Benutzer erstellen",
            "Créer un utilisateur",
            "Creează utilizator",
            "Crear usuario",
        ),
        "users.username" => (
            "username",
            "Benutzername",
            "nom d'utilisateur",
            "utilizator",
            "usuario",
        ),
        "users.display_name" => (
            "display name",
            "Anzeigename",
            "nom affiché",
            "nume afișat",
            "nombre visible",
        ),
        "users.email_optional" => (
            "email (optional)",
            "E-Mail (optional)",
            "e-mail (optionnel)",
            "email (opțional)",
            "correo (opcional)",
        ),
        "users.password" => (
            "password",
            "Passwort",
            "mot de passe",
            "parolă",
            "contraseña",
        ),
        "users.admin_label" => ("admin", "Administrator", "admin", "admin", "admin"),
        "users.username_required" => (
            "Username required",
            "Benutzername erforderlich",
            "Nom d'utilisateur requis",
            "Numele de utilizator este obligatoriu",
            "Se requiere el usuario",
        ),
        "users.password_min" => (
            "Password must be at least 4 characters",
            "Das Passwort muss mindestens 4 Zeichen lang sein",
            "Le mot de passe doit contenir au moins 4 caractères",
            "Parola trebuie să aibă cel puțin 4 caractere",
            "La contraseña debe tener al menos 4 caracteres",
        ),
        "users.created" => (
            "Created user '{0}'.",
            "Benutzer „{0}\" erstellt.",
            "Utilisateur « {0} » créé.",
            "Utilizator „{0}\" creat.",
            "Usuario «{0}» creado.",
        ),
        "users.profile_updated" => (
            "Profile updated.",
            "Profil aktualisiert.",
            "Profil mis à jour.",
            "Profil actualizat.",
            "Perfil actualizado.",
        ),
        "users.password_updated" => (
            "Password updated.",
            "Passwort aktualisiert.",
            "Mot de passe mis à jour.",
            "Parolă actualizată.",
            "Contraseña actualizada.",
        ),
        "users.user_not_found" => (
            "User not found.",
            "Benutzer nicht gefunden.",
            "Utilisateur introuvable.",
            "Utilizator negăsit.",
            "Usuario no encontrado.",
        ),
        "users.user_deleted" => (
            "User deleted.",
            "Benutzer gelöscht.",
            "Utilisateur supprimé.",
            "Utilizator șters.",
            "Usuario eliminado.",
        ),
        "users.cant_revoke_self" => (
            "You can't revoke your own admin flag here. Edit another admin's row instead.",
            "Sie können Ihren eigenen Administrator-Status hier nicht entziehen. Bearbeiten Sie stattdessen einen anderen Administrator.",
            "Vous ne pouvez pas retirer votre propre statut d'administrateur ici. Modifiez la ligne d'un autre administrateur.",
            "Nu vă puteți revoca propriul statut de administrator aici. Editați rândul altui administrator.",
            "No puedes revocar tu propio rol de administrador desde aquí. Edita la fila de otro administrador.",
        ),
        "users.cant_disable_self" => (
            "You can't disable your own account from here.",
            "Sie können Ihr eigenes Konto hier nicht deaktivieren.",
            "Vous ne pouvez pas désactiver votre propre compte d'ici.",
            "Nu vă puteți dezactiva propriul cont de aici.",
            "No puedes deshabilitar tu propia cuenta desde aquí.",
        ),
        "users.cant_delete_self" => (
            "You can't delete the account you're signed in with.",
            "Sie können das Konto, mit dem Sie angemeldet sind, nicht löschen.",
            "Vous ne pouvez pas supprimer le compte avec lequel vous êtes connecté.",
            "Nu puteți șterge contul cu care sunteți conectat.",
            "No puedes eliminar la cuenta con la que has iniciado sesión.",
        ),
        "users.totp_removed" => (
            "TOTP removed.",
            "TOTP entfernt.",
            "TOTP supprimé.",
            "TOTP eliminat.",
            "TOTP eliminado.",
        ),
        "users.no_totp" => (
            "User had no TOTP.",
            "Benutzer hatte kein TOTP.",
            "L'utilisateur n'avait pas de TOTP.",
            "Utilizatorul nu avea TOTP.",
            "El usuario no tenía TOTP.",
        ),
        "users.oidc_password_disabled" => (
            "This account is linked to OIDC — set the password at the identity provider instead.",
            "Dieses Konto ist mit OIDC verknüpft — legen Sie das Passwort beim Identitätsanbieter fest.",
            "Ce compte est lié à OIDC — définissez le mot de passe chez le fournisseur d'identité.",
            "Acest cont este legat de OIDC — setați parola la furnizorul de identitate.",
            "Esta cuenta está vinculada a OIDC — establece la contraseña en el proveedor de identidad.",
        ),
        "users.col_username" => (
            "Username",
            "Benutzername",
            "Nom d'utilisateur",
            "Utilizator",
            "Usuario",
        ),
        "users.col_display_name" => (
            "Display name",
            "Anzeigename",
            "Nom affiché",
            "Nume afișat",
            "Nombre visible",
        ),
        "users.col_email" => ("Email", "E-Mail", "E-mail", "Email", "Correo"),
        "users.col_status" => ("Status", "Status", "Statut", "Status", "Estado"),
        "users.col_admin" => (
            "Admin",
            "Administrator",
            "Admin",
            "Admin",
            "Administrador",
        ),
        "users.col_totp" => ("TOTP", "TOTP", "TOTP", "TOTP", "TOTP"),
        "users.col_last_seen" => (
            "Last seen",
            "Zuletzt gesehen",
            "Dernière connexion",
            "Văzut ultima dată",
            "Última conexión",
        ),
        "users.status_active" => ("active", "aktiv", "actif", "activ", "activo"),
        "users.status_disabled" => (
            "disabled",
            "deaktiviert",
            "désactivé",
            "dezactivat",
            "deshabilitado",
        ),
        "users.status_unverified" => (
            "unverified",
            "unbestätigt",
            "non vérifié",
            "neverificat",
            "sin verificar",
        ),
        "users.totp_enrolled" => (
            "enrolled",
            "registriert",
            "activé",
            "activat",
            "activado",
        ),
        "users.password_set" => (
            "Set",
            "Setzen",
            "Définir",
            "Setează",
            "Establecer",
        ),
        "users.new_password" => (
            "new password",
            "neues Passwort",
            "nouveau mot de passe",
            "parolă nouă",
            "nueva contraseña",
        ),
        "users.oidc_disabled" => (
            "Linked to OIDC — password sign-in is disabled.",
            "Mit OIDC verknüpft — Passwort-Anmeldung ist deaktiviert.",
            "Lié à OIDC — la connexion par mot de passe est désactivée.",
            "Legat de OIDC — autentificarea cu parolă este dezactivată.",
            "Vinculado a OIDC — el inicio de sesión con contraseña está deshabilitado.",
        ),
        "users.save_profile" => (
            "Save profile",
            "Profil speichern",
            "Enregistrer le profil",
            "Salvează profil",
            "Guardar perfil",
        ),
        "users.grant_admin" => (
            "Grant admin",
            "Admin gewähren",
            "Donner admin",
            "Acordă admin",
            "Conceder admin",
        ),
        "users.revoke_admin" => (
            "Revoke admin",
            "Admin entziehen",
            "Retirer admin",
            "Revocă admin",
            "Revocar admin",
        ),
        "users.disable_user" => (
            "Disable user",
            "Benutzer deaktivieren",
            "Désactiver l'utilisateur",
            "Dezactivează utilizator",
            "Deshabilitar usuario",
        ),
        "users.enable_user" => (
            "Enable user",
            "Benutzer aktivieren",
            "Activer l'utilisateur",
            "Activează utilizator",
            "Habilitar usuario",
        ),
        "users.disable_totp" => (
            "Disable TOTP",
            "TOTP deaktivieren",
            "Désactiver TOTP",
            "Dezactivează TOTP",
            "Deshabilitar TOTP",
        ),
        "users.confirm_disable_totp" => (
            "Disable TOTP for {0}? They'll be able to sign in without a 6-digit code until they re-enroll.",
            "TOTP für {0} deaktivieren? Diese Person kann sich ohne 6-stelligen Code anmelden, bis TOTP erneut aktiviert wird.",
            "Désactiver TOTP pour {0} ? Cette personne pourra se connecter sans code à 6 chiffres jusqu'à la réinscription.",
            "Dezactivați TOTP pentru {0}? Acest utilizator se va putea conecta fără cod din 6 cifre până la reînregistrare.",
            "¿Deshabilitar TOTP para {0}? Podrá iniciar sesión sin un código de 6 dígitos hasta que vuelva a activarlo.",
        ),
        "users.delete_user" => (
            "Delete user",
            "Benutzer löschen",
            "Supprimer l'utilisateur",
            "Șterge utilizator",
            "Eliminar usuario",
        ),
        "users.confirm_delete" => (
            "Delete user {0}? This cascades into their tokens, group memberships and AB shares.",
            "Benutzer {0} löschen? Das entfernt auch seine Tokens, Gruppenmitgliedschaften und Adressbuch-Freigaben.",
            "Supprimer l'utilisateur {0} ? Cela supprime aussi ses jetons, appartenances aux groupes et partages de carnet.",
            "Ștergeți utilizatorul {0}? Aceasta elimină și tokenurile, apartenențele la grupuri și partajările de agendă.",
            "¿Eliminar al usuario {0}? Esto también elimina sus tokens, pertenencias a grupos y compartidos de libreta.",
        ),
        "users.totp_enrolled_for" => (
            "TOTP enrolled for {0}",
            "TOTP registriert für {0}",
            "TOTP activé pour {0}",
            "TOTP activat pentru {0}",
            "TOTP activado para {0}",
        ),
        "users.secret_b32" => (
            "Secret (base32):",
            "Geheimnis (Base32):",
            "Secret (base32) :",
            "Secret (base32):",
            "Secreto (base32):",
        ),
        "users.otpauth_url" => (
            "otpauth URL:",
            "otpauth-URL:",
            "URL otpauth :",
            "URL otpauth:",
            "URL otpauth:",
        ),
        "users.otpauth_hint" => (
            "Show this once to the user (or scan the URL as a QR code) — it isn't displayed again.",
            "Zeigen Sie dies dem Benutzer einmal (oder scannen Sie die URL als QR-Code) — sie wird nicht erneut angezeigt.",
            "Montrez ceci à l'utilisateur une seule fois (ou scannez l'URL comme QR code) — elle ne sera plus affichée.",
            "Afișați acest lucru o singură dată utilizatorului (sau scanați URL-ul ca un cod QR) — nu va mai fi afișat din nou.",
            "Muéstralo al usuario una sola vez (o escanea la URL como código QR) — no se mostrará de nuevo.",
        ),

        // ---- devices ----
        "devices.heading" => (
            "Devices",
            "Geräte",
            "Appareils",
            "Dispozitive",
            "Dispositivos",
        ),
        "devices.tagline" => (
            "Force-disconnect / force-sysinfo are delivered on the peer's next heartbeat tick (~15 s).",
            "Trennung / Sysinfo werden beim nächsten Heartbeat des Peers ausgelöst (~15 s).",
            "Déconnexion / sysinfo sont appliqués au prochain heartbeat du pair (~15 s).",
            "Deconectarea / sysinfo se aplică la următorul heartbeat al peer-ului (~15 s).",
            "La desconexión y sysinfo forzadas se aplican en el próximo heartbeat del par (~15 s).",
        ),
        "devices.col_peer_id" => (
            "Peer ID",
            "Peer-ID",
            "ID du pair",
            "ID peer",
            "ID del par",
        ),
        "devices.col_owner" => (
            "Owner",
            "Besitzer",
            "Propriétaire",
            "Proprietar",
            "Propietario",
        ),
        "devices.col_hostname" => (
            "Hostname",
            "Hostname",
            "Nom d'hôte",
            "Hostname",
            "Nombre de host",
        ),
        "devices.col_user" => ("User", "Benutzer", "Utilisateur", "Utilizator", "Usuario"),
        "devices.col_unattended_pwd" => (
            "Unattended pwd",
            "Unbeaufsichtigtes Pwd",
            "Mot de passe sans surveillance",
            "Parolă nesupravegheată",
            "Contraseña desatendida",
        ),
        "devices.col_os" => ("OS", "Betriebssystem", "OS", "SO", "SO"),
        "devices.col_version" => ("Version", "Version", "Version", "Versiune", "Versión"),
        "devices.col_last_heartbeat" => (
            "Last heartbeat",
            "Letzter Heartbeat",
            "Dernier heartbeat",
            "Ultimul heartbeat",
            "Último heartbeat",
        ),
        "devices.col_conns" => (
            "Conns",
            "Verbindungen",
            "Connexions",
            "Conexiuni",
            "Conexiones",
        ),
        "devices.no_devices" => (
            "No devices have heartbeated yet.",
            "Noch keine Geräte mit Heartbeat.",
            "Aucun appareil n'a encore envoyé de heartbeat.",
            "Niciun dispozitiv nu a trimis încă heartbeat.",
            "Ningún dispositivo ha enviado heartbeat aún.",
        ),
        "devices.online" => (
            "Online — last heartbeat {0}s ago",
            "Online — letzter Heartbeat vor {0}s",
            "En ligne — dernier heartbeat il y a {0}s",
            "Online — ultimul heartbeat acum {0}s",
            "En línea — último heartbeat hace {0}s",
        ),
        "devices.no_heartbeat" => (
            "No heartbeat recorded",
            "Kein Heartbeat aufgezeichnet",
            "Aucun heartbeat enregistré",
            "Niciun heartbeat înregistrat",
            "Sin heartbeat registrado",
        ),
        "devices.offline" => (
            "Offline — last heartbeat {0} ago",
            "Offline — letzter Heartbeat vor {0}",
            "Hors ligne — dernier heartbeat il y a {0}",
            "Offline — ultimul heartbeat acum {0}",
            "Sin conexión — último heartbeat hace {0}",
        ),
        "devices.connect_web" => (
            "Connect (web client)",
            "Verbinden (Web-Client)",
            "Connecter (client web)",
            "Conectează (client web)",
            "Conectar (cliente web)",
        ),
        "devices.details" => (
            "Details & inventory",
            "Details & Inventar",
            "Détails & inventaire",
            "Detalii și inventar",
            "Detalles e inventario",
        ),
        "devices.force_disconnect" => (
            "Force disconnect",
            "Trennung erzwingen",
            "Forcer la déconnexion",
            "Forțează deconectarea",
            "Forzar desconexión",
        ),
        "devices.force_sysinfo" => (
            "Force sysinfo refresh",
            "Sysinfo-Aktualisierung erzwingen",
            "Forcer la mise à jour sysinfo",
            "Forțează reîmprospătarea sysinfo",
            "Forzar actualización sysinfo",
        ),
        "devices.confirm_disconnect" => (
            "Disconnect all active sessions on {0}?",
            "Alle aktiven Sitzungen von {0} trennen?",
            "Déconnecter toutes les sessions actives de {0} ?",
            "Deconectați toate sesiunile active de pe {0}?",
            "¿Desconectar todas las sesiones activas en {0}?",
        ),
        "devices.delete_device" => (
            "Delete device",
            "Gerät löschen",
            "Supprimer l'appareil",
            "Șterge dispozitivul",
            "Eliminar dispositivo",
        ),
        "devices.confirm_delete" => (
            "Delete {0}? This removes the dashboard row and the rendezvous identity. Audit logs and recordings are kept.",
            "{0} löschen? Damit werden der Dashboard-Eintrag und die Rendezvous-Identität entfernt. Audit-Protokolle und Aufzeichnungen bleiben erhalten.",
            "Supprimer {0} ? Cela supprime la ligne du tableau de bord et l'identité rendezvous. Les journaux d'audit et enregistrements sont conservés.",
            "Ștergeți {0}? Aceasta elimină rândul din panou și identitatea rendezvous. Jurnalele de audit și înregistrările sunt păstrate.",
            "¿Eliminar {0}? Esto elimina la fila del panel y la identidad rendezvous. Se conservan los registros de auditoría y grabaciones.",
        ),
        "devices.queued_disconnect" => (
            "Queued disconnect for {0} (conns={1})",
            "Trennung für {0} eingereiht (conns={1})",
            "Déconnexion mise en file pour {0} (conns={1})",
            "Deconectare pusă în coadă pentru {0} (conns={1})",
            "Desconexión en cola para {0} (conns={1})",
        ),
        "devices.queued_sysinfo" => (
            "Queued sysinfo refresh for {0}",
            "Sysinfo-Aktualisierung für {0} eingereiht",
            "Mise à jour sysinfo mise en file pour {0}",
            "Reîmprospătare sysinfo pusă în coadă pentru {0}",
            "Actualización sysinfo en cola para {0}",
        ),
        "devices.deleted" => (
            "Deleted device {0}.",
            "Gerät {0} gelöscht.",
            "Appareil {0} supprimé.",
            "Dispozitiv {0} șters.",
            "Dispositivo {0} eliminado.",
        ),
        "devices.already_gone" => (
            "Device already gone.",
            "Gerät bereits entfernt.",
            "Appareil déjà supprimé.",
            "Dispozitivul a fost deja șters.",
            "El dispositivo ya se eliminó.",
        ),
        "devices.col_auth" => (
            "Auth",
            "Auth",
            "Auth",
            "Auth",
            "Auth",
        ),
        "devices.auth_signed" => (
            "Signed",
            "Signiert",
            "Signé",
            "Semnat",
            "Firmado",
        ),
        "devices.auth_signed_tooltip" => (
            "Heartbeat and sysinfo posts must carry a valid Ed25519 signature. Unsigned requests for this peer are rejected.",
            "Heartbeat- und Sysinfo-Posts müssen eine gültige Ed25519-Signatur enthalten. Unsignierte Anfragen für dieses Gerät werden abgelehnt.",
            "Les requêtes heartbeat et sysinfo doivent porter une signature Ed25519 valide. Les requêtes non signées pour ce pair sont rejetées.",
            "Cererile heartbeat și sysinfo trebuie să poarte o semnătură Ed25519 validă. Cererile nesemnate pentru acest peer sunt respinse.",
            "Las solicitudes heartbeat y sysinfo deben llevar una firma Ed25519 válida. Las solicitudes sin firma para este par se rechazan.",
        ),
        "devices.auth_unsigned" => (
            "Unsigned",
            "Unsigniert",
            "Non signé",
            "Nesemnat",
            "Sin firma",
        ),
        "devices.auth_unsigned_tooltip" => (
            "Legacy / stock-client path. The server accepts unsigned heartbeat and sysinfo posts identifying as this peer — any caller knowing the id+uuid could inject inventory. The first valid signature flips this to Signed automatically.",
            "Legacy-/Stock-Client-Pfad. Der Server akzeptiert unsignierte Heartbeat- und Sysinfo-Posts mit dieser Peer-Identität — jeder Aufrufer, der id+uuid kennt, kann Inventar einschleusen. Die erste gültige Signatur schaltet automatisch auf Signiert.",
            "Chemin client hérité / standard. Le serveur accepte des requêtes heartbeat et sysinfo non signées identifiées comme ce pair — tout appelant connaissant id+uuid peut injecter de l'inventaire. La première signature valide bascule automatiquement sur Signé.",
            "Calea client legacy / standard. Serverul acceptă cereri heartbeat și sysinfo nesemnate care se identifică drept acest peer — orice apelant care cunoaște id+uuid poate injecta inventar. Prima semnătură validă comută automat la Semnat.",
            "Ruta cliente heredada / estándar. El servidor acepta solicitudes heartbeat y sysinfo sin firmar identificadas como este par — cualquier llamador que conozca id+uuid puede inyectar inventario. La primera firma válida cambia automáticamente a Firmado.",
        ),
        "devices.mark_managed" => (
            "Require signed API",
            "Signierte API erzwingen",
            "Exiger l'API signée",
            "Cere API semnat",
            "Requerir API firmada",
        ),
        "devices.mark_unsigned" => (
            "Allow unsigned API",
            "Unsignierte API erlauben",
            "Autoriser l'API non signée",
            "Permite API nesemnat",
            "Permitir API sin firma",
        ),
        "devices.confirm_managed_off" => (
            "Downgrade {0} to the unsigned API path? Anyone who knows the id+uuid will again be able to post inventory and heartbeats as this device.",
            "{0} auf den unsignierten API-Pfad herabstufen? Jeder, der id+uuid kennt, kann dann wieder Inventar und Heartbeats als dieses Gerät posten.",
            "Rétrograder {0} vers le chemin API non signé ? Toute personne connaissant id+uuid pourra de nouveau publier de l'inventaire et des heartbeats en tant que cet appareil.",
            "Retrogradați {0} la calea API nesemnat? Oricine cunoaște id+uuid va putea din nou să publice inventar și heartbeat-uri ca acest dispozitiv.",
            "¿Degradar {0} a la ruta API sin firma? Cualquiera que conozca id+uuid podrá volver a publicar inventario y heartbeats como este dispositivo.",
        ),
        "devices.managed_now_on" => (
            "{0} now requires signed API requests.",
            "{0} erfordert nun signierte API-Anfragen.",
            "{0} exige désormais des requêtes API signées.",
            "{0} cere acum cereri API semnate.",
            "{0} ahora requiere solicitudes API firmadas.",
        ),
        "devices.managed_now_off" => (
            "{0} now accepts unsigned API requests.",
            "{0} akzeptiert nun unsignierte API-Anfragen.",
            "{0} accepte désormais des requêtes API non signées.",
            "{0} acceptă acum cereri API nesemnate.",
            "{0} ahora acepta solicitudes API sin firma.",
        ),
        "devices.managed_no_peer" => (
            "Peer {0} not found in the rendezvous identity table — cannot set managed flag. The agent must complete a rendezvous handshake first.",
            "Peer {0} nicht in der Rendezvous-Identitätstabelle gefunden — Managed-Flag kann nicht gesetzt werden. Der Agent muss zuerst einen Rendezvous-Handshake abschließen.",
            "Pair {0} introuvable dans la table d'identité rendezvous — impossible de définir l'indicateur managed. L'agent doit d'abord effectuer un handshake rendezvous.",
            "Peer-ul {0} nu a fost găsit în tabelul de identitate rendezvous — nu se poate seta indicatorul managed. Agentul trebuie să finalizeze mai întâi un handshake rendezvous.",
            "Par {0} no encontrado en la tabla de identidad rendezvous — no se puede establecer el indicador managed. El agente debe completar primero un apretón de manos rendezvous.",
        ),
        "devices.back" => (
            "← Back to devices",
            "← Zurück zu Geräten",
            "← Retour aux appareils",
            "← Înapoi la dispozitive",
            "← Volver a dispositivos",
        ),
        "devices.detail_view" => (
            "Detail view",
            "Detailansicht",
            "Vue détaillée",
            "Vizualizare detaliată",
            "Vista detallada",
        ),
        "devices.device_label" => (
            "Device",
            "Gerät",
            "Appareil",
            "Dispozitiv",
            "Dispositivo",
        ),
        "devices.detail_active_user" => (
            "Active user",
            "Aktiver Benutzer",
            "Utilisateur actif",
            "Utilizator activ",
            "Usuario activo",
        ),
        "devices.detail_agent" => ("Agent", "Agent", "Agent", "Agent", "Agente"),
        "devices.detail_os_runtime" => (
            "OS (runtime)",
            "Betriebssystem (Laufzeit)",
            "OS (runtime)",
            "SO (runtime)",
            "SO (runtime)",
        ),
        "devices.detail_cpu_runtime" => (
            "CPU (runtime)",
            "CPU (Laufzeit)",
            "CPU (runtime)",
            "CPU (runtime)",
            "CPU (runtime)",
        ),
        "devices.detail_memory_runtime" => (
            "Memory (runtime)",
            "Speicher (Laufzeit)",
            "Mémoire (runtime)",
            "Memorie (runtime)",
            "Memoria (runtime)",
        ),
        "devices.inventory" => (
            "Inventory",
            "Inventar",
            "Inventaire",
            "Inventar",
            "Inventario",
        ),
        "devices.inventory_pending" => (
            "Inventory data not yet reported. The agent collects it on startup and uploads on the next sysinfo cycle (≤120 s).",
            "Inventardaten noch nicht gemeldet. Der Agent sammelt sie beim Start und überträgt sie beim nächsten Sysinfo-Zyklus (≤120 s).",
            "Données d'inventaire non encore signalées. L'agent les collecte au démarrage et les transmet au prochain cycle sysinfo (≤120 s).",
            "Datele de inventar nu au fost încă raportate. Agentul le colectează la pornire și le încarcă la următorul ciclu sysinfo (≤120 s).",
            "Aún no se han reportado datos de inventario. El agente los recopila al iniciar y los envía en el próximo ciclo sysinfo (≤120 s).",
        ),
        "devices.serial_number" => (
            "Serial number",
            "Seriennummer",
            "Numéro de série",
            "Număr de serie",
            "Número de serie",
        ),
        "devices.manufacturer" => (
            "Manufacturer",
            "Hersteller",
            "Fabricant",
            "Producător",
            "Fabricante",
        ),
        "devices.model" => ("Model", "Modell", "Modèle", "Model", "Modelo"),
        "devices.windows_domain" => (
            "Windows domain",
            "Windows-Domäne",
            "Domaine Windows",
            "Domeniu Windows",
            "Dominio de Windows",
        ),
        "devices.os_distro" => (
            "OS distribution",
            "OS-Distribution",
            "Distribution OS",
            "Distribuție SO",
            "Distribución del SO",
        ),
        "devices.os_release" => (
            "OS release",
            "OS-Release",
            "Version OS",
            "Versiune SO",
            "Versión del SO",
        ),
        "devices.cpu_model" => (
            "CPU model",
            "CPU-Modell",
            "Modèle CPU",
            "Model CPU",
            "Modelo de CPU",
        ),
        "devices.cpu_speed" => (
            "CPU speed (GHz)",
            "CPU-Takt (GHz)",
            "Vitesse CPU (GHz)",
            "Viteză CPU (GHz)",
            "Velocidad de CPU (GHz)",
        ),
        "devices.cpu_phys_cores" => (
            "CPU physical cores",
            "Physische CPU-Kerne",
            "Cœurs physiques CPU",
            "Nuclee fizice CPU",
            "Núcleos físicos de CPU",
        ),
        "devices.cpu_logical_cores" => (
            "CPU logical cores",
            "Logische CPU-Kerne",
            "Cœurs logiques CPU",
            "Nuclee logice CPU",
            "Núcleos lógicos de CPU",
        ),
        "devices.ram_gb" => ("RAM (GB)", "RAM (GB)", "RAM (Go)", "RAM (GB)", "RAM (GB)"),
        "devices.disks" => (
            "Disks",
            "Festplatten",
            "Disques",
            "Discuri",
            "Discos",
        ),
        "devices.disk_name" => ("Name", "Name", "Nom", "Nume", "Nombre"),
        "devices.disk_model" => ("Model", "Modell", "Modèle", "Model", "Modelo"),
        "devices.disk_size" => (
            "Size (GB)",
            "Größe (GB)",
            "Taille (Go)",
            "Dimensiune (GB)",
            "Tamaño (GB)",
        ),
        "devices.disk_media" => ("Media", "Medium", "Support", "Suport", "Soporte"),
        "devices.network_interfaces" => (
            "Network interfaces",
            "Netzwerkschnittstellen",
            "Interfaces réseau",
            "Interfețe de rețea",
            "Interfaces de red",
        ),
        "devices.nic_description" => (
            "Description",
            "Beschreibung",
            "Description",
            "Descriere",
            "Descripción",
        ),
        "devices.nic_status" => ("Status", "Status", "Statut", "Status", "Estado"),
        "devices.wifi_current" => (
            "Wi-Fi (current connection)",
            "WLAN (aktuelle Verbindung)",
            "Wi-Fi (connexion actuelle)",
            "Wi-Fi (conexiune curentă)",
            "Wi-Fi (conexión actual)",
        ),
        "devices.wifi_signal" => ("Signal", "Signal", "Signal", "Semnal", "Señal"),
        "devices.wifi_auth" => (
            "Authentication",
            "Authentifizierung",
            "Authentification",
            "Autentificare",
            "Autenticación",
        ),
        "devices.wifi_cipher" => (
            "Cipher",
            "Verschlüsselung",
            "Chiffrement",
            "Cifru",
            "Cifrado",
        ),
        "devices.wifi_rate" => ("Rate", "Datenrate", "Débit", "Rată", "Velocidad"),
        "devices.wifi_not_connected" => (
            "Not connected to a Wi-Fi network.",
            "Nicht mit einem WLAN-Netzwerk verbunden.",
            "Non connecté à un réseau Wi-Fi.",
            "Neconectat la o rețea Wi-Fi.",
            "Sin conexión a una red Wi-Fi.",
        ),
        "devices.wifi_nearby" => (
            "Nearby SSIDs ({0})",
            "Verfügbare SSIDs ({0})",
            "SSID à proximité ({0})",
            "SSID-uri din apropiere ({0})",
            "SSID cercanos ({0})",
        ),
        "devices.installed_software" => (
            "Installed software",
            "Installierte Software",
            "Logiciels installés",
            "Software instalat",
            "Software instalado",
        ),
        "devices.installed_software_count" => (
            "Show {0} entries",
            "{0} Einträge anzeigen",
            "Afficher {0} entrées",
            "Afișați {0} intrări",
            "Mostrar {0} entradas",
        ),
        "devices.col_software_name" => ("Name", "Name", "Nom", "Nume", "Nombre"),
        "devices.col_software_version" => (
            "Version",
            "Version",
            "Version",
            "Versiune",
            "Versión",
        ),
        "devices.col_software_publisher" => (
            "Publisher",
            "Herausgeber",
            "Éditeur",
            "Editor",
            "Editor",
        ),
        "devices.public_ip" => (
            "Public IP (egress, last lookup)",
            "Öffentliche IP (Egress, letzte Abfrage)",
            "IP publique (sortie, dernière recherche)",
            "IP publică (egress, ultima căutare)",
            "IP pública (salida, última consulta)",
        ),
        "devices.public_ip_failed" => (
            "— (lookup failed or blocked)",
            "— (Abfrage fehlgeschlagen oder blockiert)",
            "— (échec de la recherche ou bloqué)",
            "— (căutare eșuată sau blocată)",
            "— (consulta fallida o bloqueada)",
        ),
        "devices.bitlocker" => (
            "BitLocker recovery key (system drive)",
            "BitLocker-Wiederherstellungsschlüssel (Systemlaufwerk)",
            "Clé de récupération BitLocker (disque système)",
            "Cheie de recuperare BitLocker (unitate de sistem)",
            "Clave de recuperación BitLocker (unidad del sistema)",
        ),
        "devices.bitlocker_unavailable" => (
            "— (not reported; non-Pro SKU, not encrypted, or no admin rights)",
            "— (nicht gemeldet; keine Pro-Edition, nicht verschlüsselt oder keine Admin-Rechte)",
            "— (non signalé ; édition non Pro, non chiffré, ou pas de droits admin)",
            "— (neraportat; ediție non-Pro, necriptat sau fără drepturi de admin)",
            "— (no reportado; edición no Pro, sin cifrar o sin permisos de administrador)",
        ),
        "devices.no_device_with_id" => (
            "No device with peer ID",
            "Kein Gerät mit Peer-ID",
            "Aucun appareil avec l'ID de pair",
            "Niciun dispozitiv cu ID-ul peer",
            "Ningún dispositivo con el ID de par",
        ),
        "devices.in_dashboard" => (
            "in the dashboard.",
            "im Dashboard.",
            "dans le tableau de bord.",
            "în panou.",
            "en el panel.",
        ),
        "devices.devices_count" => (
            "{0} device(s).",
            "{0} Gerät(e).",
            "{0} appareil(s).",
            "{0} dispozitiv(e).",
            "{0} dispositivo(s).",
        ),

        // ---- groups ----
        "groups.heading" => (
            "Device groups",
            "Gerätegruppen",
            "Groupes d'appareils",
            "Grupuri de dispozitive",
            "Grupos de dispositivos",
        ),
        "groups.create_heading" => (
            "Create group",
            "Gruppe erstellen",
            "Créer un groupe",
            "Creează grup",
            "Crear grupo",
        ),
        "groups.group_name" => (
            "group name",
            "Gruppenname",
            "nom du groupe",
            "nume grup",
            "nombre del grupo",
        ),
        "groups.name_required" => (
            "Name required",
            "Name erforderlich",
            "Nom requis",
            "Nume necesar",
            "Se requiere el nombre",
        ),
        "groups.created" => (
            "Group '{0}' created.",
            "Gruppe „{0}\" erstellt.",
            "Groupe « {0} » créé.",
            "Grupul „{0}\" creat.",
            "Grupo «{0}» creado.",
        ),
        "groups.deleted" => (
            "Group deleted.",
            "Gruppe gelöscht.",
            "Groupe supprimé.",
            "Grup șters.",
            "Grupo eliminado.",
        ),
        "groups.no_groups" => (
            "No device groups yet.",
            "Noch keine Gerätegruppen.",
            "Aucun groupe d'appareils.",
            "Nu există încă grupuri.",
            "Aún no hay grupos de dispositivos.",
        ),
        "groups.delete_group" => (
            "Delete group",
            "Gruppe löschen",
            "Supprimer le groupe",
            "Șterge grup",
            "Eliminar grupo",
        ),
        "groups.confirm_delete" => (
            "Delete group {0}? Members aren't deleted; just unassigned.",
            "Gruppe {0} löschen? Mitglieder werden nicht gelöscht, nur die Zuordnung wird entfernt.",
            "Supprimer le groupe {0} ? Les membres ne sont pas supprimés, seulement désassignés.",
            "Ștergeți grupul {0}? Membrii nu sunt șterși, doar dezasignați.",
            "¿Eliminar el grupo {0}? Los miembros no se eliminan; solo se desasignan.",
        ),
        "groups.users" => (
            "Users",
            "Benutzer",
            "Utilisateurs",
            "Utilizatori",
            "Usuarios",
        ),
        "groups.no_user_members" => (
            "No user members yet.",
            "Noch keine Benutzer als Mitglieder.",
            "Aucun utilisateur membre.",
            "Niciun utilizator membru.",
            "Aún no hay usuarios miembros.",
        ),
        "groups.add_user" => (
            "Add user",
            "Benutzer hinzufügen",
            "Ajouter utilisateur",
            "Adaugă utilizator",
            "Añadir usuario",
        ),
        "groups.devices_section" => (
            "Devices",
            "Geräte",
            "Appareils",
            "Dispozitive",
            "Dispositivos",
        ),
        "groups.no_peer_members" => (
            "No devices added directly. (Devices owned by user members above are also visible to them.)",
            "Keine Geräte direkt hinzugefügt. (Geräte der oben genannten Benutzer-Mitglieder sind ebenfalls sichtbar.)",
            "Aucun appareil ajouté directement. (Les appareils possédés par les membres ci-dessus leur sont aussi visibles.)",
            "Nu există dispozitive adăugate direct. (Dispozitivele deținute de membrii de mai sus sunt vizibile pentru aceștia.)",
            "No se añadieron dispositivos directamente. (Los dispositivos de los miembros anteriores también son visibles para ellos.)",
        ),
        "groups.unowned" => (
            "unowned",
            "ohne Besitzer",
            "sans propriétaire",
            "fără proprietar",
            "sin propietario",
        ),
        "groups.owner_label" => (
            "owner: {0}",
            "Besitzer: {0}",
            "propriétaire : {0}",
            "proprietar: {0}",
            "propietario: {0}",
        ),
        "groups.add_device" => (
            "Add device",
            "Gerät hinzufügen",
            "Ajouter appareil",
            "Adaugă dispozitiv",
            "Añadir dispositivo",
        ),
        "groups.peer_id_placeholder" => (
            "Device ID (e.g. 123456789)",
            "Geräte-ID (z. B. 123456789)",
            "ID d'appareil (ex. 123456789)",
            "ID dispozitiv (ex. 123456789)",
            "ID de dispositivo (p. ej. 123456789)",
        ),
        "groups.peer_id_required" => (
            "Device ID required",
            "Geräte-ID erforderlich",
            "ID d'appareil requis",
            "ID dispozitiv necesar",
            "Se requiere el ID del dispositivo",
        ),
        "groups.no_device_yet" => (
            "No device '{0}' has reported in yet.",
            "Es hat sich noch kein Gerät „{0}\" gemeldet.",
            "Aucun appareil « {0} » ne s'est encore manifesté.",
            "Niciun dispozitiv „{0}\" nu s-a raportat încă.",
            "Ningún dispositivo «{0}» se ha reportado aún.",
        ),

        // ---- strategies ----
        "strategies.heading" => (
            "Strategies",
            "Strategien",
            "Stratégies",
            "Strategii",
            "Estrategias",
        ),
        "strategies.tagline" => (
            "Pushed to clients via heartbeat. Assign each strategy to device groups or individual peers below — peer assignments override group assignments.",
            "Wird Clients per Heartbeat zugestellt. Strategien unten Gerätegruppen oder einzelnen Geräten zuweisen — Gerätezuweisungen haben Vorrang vor Gruppenzuweisungen.",
            "Distribué aux clients via heartbeat. Affectez chaque stratégie ci-dessous à des groupes d'appareils ou à des appareils individuels — les affectations par appareil prévalent sur celles par groupe.",
            "Trimis clienților prin heartbeat. Atribuiți fiecare strategie mai jos unor grupuri de dispozitive sau dispozitive individuale — atribuirile pe dispozitiv au prioritate față de cele pe grup.",
            "Se envía a los clientes por heartbeat. Asigna cada estrategia a grupos de dispositivos o dispositivos individuales abajo — las asignaciones por dispositivo tienen prioridad sobre las de grupo.",
        ),
        "strategies.create_heading" => (
            "Create strategy",
            "Strategie erstellen",
            "Créer une stratégie",
            "Creează strategie",
            "Crear estrategia",
        ),
        "strategies.name_unique" => (
            "name (unique)",
            "Name (eindeutig)",
            "nom (unique)",
            "nume (unic)",
            "nombre (único)",
        ),
        "strategies.json_obj_required" => (
            "config_options must be a JSON object",
            "config_options muss ein JSON-Objekt sein",
            "config_options doit être un objet JSON",
            "config_options trebuie să fie un obiect JSON",
            "config_options debe ser un objeto JSON",
        ),
        "strategies.invalid_json" => (
            "invalid JSON: {0}",
            "ungültiges JSON: {0}",
            "JSON invalide : {0}",
            "JSON invalid: {0}",
            "JSON inválido: {0}",
        ),
        "strategies.created" => (
            "Strategy '{0}' created.",
            "Strategie „{0}\" erstellt.",
            "Stratégie « {0} » créée.",
            "Strategie „{0}\" creată.",
            "Estrategia «{0}» creada.",
        ),
        "strategies.updated" => (
            "Strategy updated.",
            "Strategie aktualisiert.",
            "Stratégie mise à jour.",
            "Strategie actualizată.",
            "Estrategia actualizada.",
        ),
        "strategies.deleted" => (
            "Strategy deleted.",
            "Strategie gelöscht.",
            "Stratégie supprimée.",
            "Strategie ștearsă.",
            "Estrategia eliminada.",
        ),
        "strategies.no_strategies" => (
            "No strategies yet.",
            "Noch keine Strategien.",
            "Aucune stratégie.",
            "Nu există încă strategii.",
            "Aún no hay estrategias.",
        ),
        "strategies.config_label" => (
            "config_options (JSON object)",
            "config_options (JSON-Objekt)",
            "config_options (objet JSON)",
            "config_options (obiect JSON)",
            "config_options (objeto JSON)",
        ),
        "strategies.confirm_delete" => (
            "Delete strategy {0}? Assignments will be cleaned up too.",
            "Strategie {0} löschen? Zuweisungen werden ebenfalls bereinigt.",
            "Supprimer la stratégie {0} ? Les affectations seront également nettoyées.",
            "Ștergeți strategia {0}? Atribuirile vor fi curățate de asemenea.",
            "¿Eliminar la estrategia {0}? También se limpiarán las asignaciones.",
        ),
        "strategies.id_modified" => (
            "id={0}, modified_at={1}",
            "id={0}, modified_at={1}",
            "id={0}, modified_at={1}",
            "id={0}, modified_at={1}",
            "id={0}, modified_at={1}",
        ),
        "strategies.filter_heading" => (
            "Targets",
            "Ziele",
            "Cibles",
            "Ținte",
            "Destinos",
        ),
        "strategies.filter_hint" => (
            "Resolution order per client: direct peer > device group > user. The highest-priority match wins.",
            "Auflösungsreihenfolge je Client: Gerät direkt > Gerätegruppe > Benutzer. Die höchste Priorität gewinnt.",
            "Ordre de résolution par client : appareil direct > groupe d'appareils > utilisateur. La priorité la plus élevée l'emporte.",
            "Ordinea de rezolvare per client: dispozitiv direct > grup de dispozitive > utilizator. Prioritatea cea mai mare câștigă.",
            "Orden de resolución por cliente: dispositivo directo > grupo de dispositivos > usuario. Gana la prioridad más alta.",
        ),
        "strategies.groups_label" => (
            "Device groups",
            "Gerätegruppen",
            "Groupes d'appareils",
            "Grupuri de dispozitive",
            "Grupos de dispositivos",
        ),
        "strategies.peers_label" => (
            "Individual devices",
            "Einzelne Geräte",
            "Appareils individuels",
            "Dispozitive individuale",
            "Dispositivos individuales",
        ),
        "strategies.no_group_assignments" => (
            "No groups assigned.",
            "Keine Gruppen zugewiesen.",
            "Aucun groupe affecté.",
            "Niciun grup atribuit.",
            "Sin grupos asignados.",
        ),
        "strategies.no_peer_assignments" => (
            "No devices assigned.",
            "Keine Geräte zugewiesen.",
            "Aucun appareil affecté.",
            "Niciun dispozitiv atribuit.",
            "Sin dispositivos asignados.",
        ),
        "strategies.assign_group" => (
            "Assign group",
            "Gruppe zuweisen",
            "Affecter le groupe",
            "Atribuie grupul",
            "Asignar grupo",
        ),
        "strategies.assign_peer" => (
            "Assign device",
            "Gerät zuweisen",
            "Affecter l'appareil",
            "Atribuie dispozitivul",
            "Asignar dispositivo",
        ),
        "strategies.all_groups_assigned" => (
            "All device groups are already assigned.",
            "Alle Gerätegruppen sind bereits zugewiesen.",
            "Tous les groupes d'appareils sont déjà affectés.",
            "Toate grupurile de dispozitive sunt deja atribuite.",
            "Todos los grupos de dispositivos ya están asignados.",
        ),
        "strategies.no_groups_exist" => (
            "No device groups exist yet — create one on the Groups page.",
            "Es gibt noch keine Gerätegruppen — auf der Seite „Gruppen\" anlegen.",
            "Aucun groupe d'appareils n'existe — créez-en un sur la page Groupes.",
            "Nu există încă grupuri de dispozitive — creați unul în pagina Grupuri.",
            "Aún no hay grupos de dispositivos — crea uno en la página Grupos.",
        ),
        "strategies.group_assigned" => (
            "Device group assigned.",
            "Gerätegruppe zugewiesen.",
            "Groupe d'appareils affecté.",
            "Grup de dispozitive atribuit.",
            "Grupo de dispositivos asignado.",
        ),
        "strategies.peer_assigned" => (
            "Device '{0}' assigned to this strategy (any previous peer-level strategy was replaced).",
            "Gerät „{0}\" dieser Strategie zugewiesen (eine vorhandene gerätebezogene Strategie wurde ersetzt).",
            "Appareil « {0} » affecté à cette stratégie (toute stratégie précédente au niveau appareil a été remplacée).",
            "Dispozitivul „{0}\" a fost atribuit acestei strategii (orice strategie anterioară la nivel de dispozitiv a fost înlocuită).",
            "Dispositivo «{0}» asignado a esta estrategia (cualquier estrategia previa a nivel de dispositivo fue reemplazada).",
        ),
        "strategies.unassigned" => (
            "Assignment removed.",
            "Zuweisung entfernt.",
            "Affectation supprimée.",
            "Atribuirea a fost eliminată.",
            "Asignación eliminada.",
        ),

        // ---- address books ----
        "ab.heading" => (
            "Address books",
            "Adressbücher",
            "Carnets d'adresses",
            "Agende",
            "Libretas de direcciones",
        ),
        "ab.tagline" => (
            "Personal books are owned by users and managed from their desktop client. Shared books are server-side: create one here, share it with users / rules, and the client picks it up on its next AB sync.",
            "Persönliche Adressbücher gehören Benutzern und werden im Desktop-Client verwaltet. Geteilte Adressbücher sind serverseitig: Erstellen Sie hier eines, teilen Sie es mit Benutzern/Regeln, und der Client übernimmt es bei der nächsten AB-Synchronisierung.",
            "Les carnets personnels appartiennent aux utilisateurs et sont gérés depuis leur client de bureau. Les carnets partagés sont côté serveur : créez-en un ici, partagez-le avec des utilisateurs / règles, et le client le récupère à la prochaine synchronisation.",
            "Agendele personale aparțin utilizatorilor și sunt gestionate din clientul desktop. Agendele partajate sunt pe server: creați una aici, partajați-o cu utilizatori / reguli, iar clientul o preia la următoarea sincronizare.",
            "Las libretas personales son de los usuarios y se gestionan desde su cliente de escritorio. Las libretas compartidas están en el servidor: crea una aquí, compártela con usuarios/reglas, y el cliente la recogerá en su próxima sincronización.",
        ),
        "ab.new_shared" => (
            "New shared book",
            "Neues geteiltes Adressbuch",
            "Nouveau carnet partagé",
            "Agendă partajată nouă",
            "Nueva libreta compartida",
        ),
        "ab.name_required" => (
            "Name is required.",
            "Name ist erforderlich.",
            "Le nom est requis.",
            "Numele este obligatoriu.",
            "El nombre es obligatorio.",
        ),
        "ab.created" => (
            "Shared address book '{0}' created.",
            "Geteiltes Adressbuch „{0}\" erstellt.",
            "Carnet partagé « {0} » créé.",
            "Agenda partajată „{0}\" creată.",
            "Libreta compartida «{0}» creada.",
        ),
        "ab.exists" => (
            "An address book with that name already exists.",
            "Ein Adressbuch mit diesem Namen existiert bereits.",
            "Un carnet avec ce nom existe déjà.",
            "Există deja o agendă cu acest nume.",
            "Ya existe una libreta con ese nombre.",
        ),
        "ab.create_failed" => (
            "Create failed: {0}",
            "Erstellen fehlgeschlagen: {0}",
            "Échec de la création : {0}",
            "Creare eșuată: {0}",
            "Error al crear: {0}",
        ),
        "ab.deleted" => ("Deleted.", "Gelöscht.", "Supprimé.", "Șters.", "Eliminado."),
        "ab.not_found" => (
            "Address book not found.",
            "Adressbuch nicht gefunden.",
            "Carnet d'adresses introuvable.",
            "Agenda nu a fost găsită.",
            "Libreta de direcciones no encontrada.",
        ),
        "ab.no_books" => (
            "No address books exist yet.",
            "Es existieren noch keine Adressbücher.",
            "Aucun carnet n'existe encore.",
            "Nu există încă agende.",
            "Aún no existen libretas de direcciones.",
        ),
        "ab.col_owner" => (
            "Owner",
            "Besitzer",
            "Propriétaire",
            "Proprietar",
            "Propietario",
        ),
        "ab.col_kind" => ("Kind", "Art", "Type", "Tip", "Tipo"),
        "ab.col_name" => ("Name", "Name", "Nom", "Nume", "Nombre"),
        "ab.col_peers" => ("Peers", "Peers", "Pairs", "Peers", "Pares"),
        "ab.col_guid" => ("GUID", "GUID", "GUID", "GUID", "GUID"),
        "ab.col_created" => ("Created", "Erstellt", "Créé", "Creat", "Creado"),
        "ab.kind_personal" => (
            "personal",
            "persönlich",
            "personnel",
            "personal",
            "personal",
        ),
        "ab.kind_shared" => ("shared", "geteilt", "partagé", "partajat", "compartida"),
        "ab.manage_shares" => (
            "Manage shares",
            "Freigaben verwalten",
            "Gérer les partages",
            "Gestionează partajări",
            "Gestionar compartidos",
        ),
        "ab.delete_book" => (
            "Delete book",
            "Adressbuch löschen",
            "Supprimer le carnet",
            "Șterge agenda",
            "Eliminar libreta",
        ),
        "ab.confirm_delete_shared" => (
            "Delete shared book '{0}'? Peers, tags, and shares will be removed.",
            "Geteiltes Adressbuch „{0}\" löschen? Peers, Tags und Freigaben werden entfernt.",
            "Supprimer le carnet partagé « {0} » ? Les pairs, étiquettes et partages seront retirés.",
            "Ștergeți agenda partajată „{0}\"? Peer-urile, etichetele și partajările vor fi eliminate.",
            "¿Eliminar la libreta compartida «{0}»? Se eliminarán pares, etiquetas y compartidos.",
        ),
        "ab.confirm_delete_personal" => (
            "Delete {0}'s personal book? This wipes all peers and tags on the server. If {0}'s desktop client is still signed in, it will recreate an empty personal book on its next sync (~30 s).",
            "Persönliches Adressbuch von {0} löschen? Damit werden alle Peers und Tags auf dem Server gelöscht. Wenn der Desktop-Client von {0} noch angemeldet ist, wird beim nächsten Sync (~30 s) ein leeres persönliches Adressbuch erstellt.",
            "Supprimer le carnet personnel de {0} ? Cela efface tous les pairs et étiquettes sur le serveur. Si le client de bureau de {0} est encore connecté, il recréera un carnet personnel vide à la prochaine synchronisation (~30 s).",
            "Ștergeți agenda personală a lui {0}? Aceasta șterge toți peer-ii și etichetele de pe server. Dacă clientul desktop al lui {0} este încă conectat, va recrea o agendă personală goală la următoarea sincronizare (~30 s).",
            "¿Eliminar la libreta personal de {0}? Esto borra todos los pares y etiquetas en el servidor. Si el cliente de escritorio de {0} sigue conectado, recreará una libreta personal vacía en la próxima sincronización (~30 s).",
        ),
        "ab.manage_heading" => (
            "Manage shares",
            "Freigaben verwalten",
            "Gérer les partages",
            "Gestionează partajări",
            "Gestionar compartidos",
        ),
        "ab.add_or_update" => (
            "Add or update a share",
            "Freigabe hinzufügen oder aktualisieren",
            "Ajouter ou mettre à jour un partage",
            "Adaugă sau actualizează o partajare",
            "Añadir o actualizar un compartido",
        ),
        "ab.user_label" => (
            "User",
            "Benutzer",
            "Utilisateur",
            "Utilizator",
            "Usuario",
        ),
        "ab.no_users" => (
            "No users defined",
            "Keine Benutzer definiert",
            "Aucun utilisateur défini",
            "Niciun utilizator definit",
            "Sin usuarios definidos",
        ),
        "ab.existing_will_update" => (
            " (existing — will update rule)",
            " (existiert — Regel wird aktualisiert)",
            " (existant — la règle sera mise à jour)",
            " (existent — regula va fi actualizată)",
            " (existente — se actualizará la regla)",
        ),
        "ab.rule" => ("Rule", "Regel", "Règle", "Regulă", "Regla"),
        "ab.rule_read" => ("Read", "Lesen", "Lecture", "Citire", "Lectura"),
        "ab.rule_read_write" => (
            "Read + write",
            "Lesen + Schreiben",
            "Lecture + écriture",
            "Citire + scriere",
            "Lectura + escritura",
        ),
        "ab.rule_full" => (
            "Full",
            "Vollständig",
            "Complet",
            "Complet",
            "Completo",
        ),
        "ab.invalid_rule" => (
            "Invalid rule.",
            "Ungültige Regel.",
            "Règle invalide.",
            "Regulă invalidă.",
            "Regla inválida.",
        ),
        "ab.share_saved" => (
            "Share saved.",
            "Freigabe gespeichert.",
            "Partage enregistré.",
            "Partajare salvată.",
            "Compartido guardado.",
        ),
        "ab.share_removed" => (
            "Share removed.",
            "Freigabe entfernt.",
            "Partage supprimé.",
            "Partajare eliminată.",
            "Compartido eliminado.",
        ),
        "ab.current_shares" => (
            "Current shares ({0})",
            "Aktuelle Freigaben ({0})",
            "Partages actuels ({0})",
            "Partajări curente ({0})",
            "Compartidos actuales ({0})",
        ),
        "ab.no_shares" => (
            "No shares yet. The book is invisible to non-owners until you add at least one user share.",
            "Noch keine Freigaben. Das Adressbuch ist für Nicht-Besitzer unsichtbar, bis mindestens eine Benutzer-Freigabe hinzugefügt wird.",
            "Aucun partage. Le carnet est invisible pour les non-propriétaires jusqu'à ce qu'au moins un partage utilisateur soit ajouté.",
            "Nu există partajări. Agenda este invizibilă pentru cei care nu sunt proprietari până când adăugați cel puțin o partajare către un utilizator.",
            "Aún no hay compartidos. La libreta es invisible para los no propietarios hasta que añadas al menos un compartido con un usuario.",
        ),
        "ab.confirm_remove" => (
            "Remove {0}'s access?",
            "Zugriff von {0} entfernen?",
            "Retirer l'accès de {0} ?",
            "Eliminați accesul lui {0}?",
            "¿Quitar el acceso de {0}?",
        ),
        "ab.personal_managed_at_client" => (
            "Personal address books are managed from the desktop client.",
            "Persönliche Adressbücher werden im Desktop-Client verwaltet.",
            "Les carnets d'adresses personnels sont gérés depuis le client de bureau.",
            "Agendele personale sunt gestionate din clientul desktop.",
            "Las libretas personales se gestionan desde el cliente de escritorio.",
        ),

        // ---- audit ----
        "audit.heading" => (
            "Audit log",
            "Audit-Protokoll",
            "Journal d'audit",
            "Jurnal de audit",
            "Registro de auditoría",
        ),
        "audit.latest" => (
            "Latest {0} rows.",
            "Letzte {0} Einträge.",
            "Dernières {0} lignes.",
            "Ultimele {0} rânduri.",
            "Últimas {0} filas.",
        ),
        "audit.tab_conn" => (
            "Connections",
            "Verbindungen",
            "Connexions",
            "Conexiuni",
            "Conexiones",
        ),
        "audit.tab_file" => (
            "File transfers",
            "Dateiübertragungen",
            "Transferts de fichiers",
            "Transferuri de fișiere",
            "Transferencias de archivos",
        ),
        "audit.tab_alarm" => ("Alarms", "Alarme", "Alarmes", "Alarme", "Alarmas"),
        "audit.col_when" => ("When", "Wann", "Quand", "Când", "Cuándo"),
        "audit.col_peer" => ("Peer", "Peer", "Pair", "Peer", "Par"),
        "audit.col_conn_session" => (
            "Conn / Session",
            "Verb. / Sitzung",
            "Conn / Session",
            "Conn / Sesiune",
            "Conn / Sesión",
        ),
        "audit.col_ip" => ("IP", "IP", "IP", "IP", "IP"),
        "audit.col_action" => ("Action", "Aktion", "Action", "Acțiune", "Acción"),
        "audit.col_note" => ("Note", "Notiz", "Note", "Notă", "Nota"),
        "audit.col_direction" => (
            "Direction",
            "Richtung",
            "Direction",
            "Direcție",
            "Dirección",
        ),
        "audit.col_path" => ("Path", "Pfad", "Chemin", "Cale", "Ruta"),
        "audit.col_remote" => ("Remote", "Remote", "Distant", "Remote", "Remoto"),
        "audit.col_type" => ("Type", "Typ", "Type", "Tip", "Tipo"),
        "audit.col_info" => ("Info", "Info", "Info", "Info", "Info"),
        "audit.no_conn" => (
            "No connection audit rows yet.",
            "Noch keine Verbindungs-Audit-Einträge.",
            "Aucune ligne d'audit de connexion.",
            "Nu există încă rânduri de audit pentru conexiuni.",
            "Aún no hay filas de auditoría de conexiones.",
        ),
        "audit.no_file" => (
            "No file-transfer audit rows yet.",
            "Noch keine Dateiübertragungs-Audit-Einträge.",
            "Aucune ligne d'audit de transfert de fichiers.",
            "Nu există încă rânduri de audit pentru transferuri.",
            "Aún no hay filas de auditoría de transferencias de archivos.",
        ),
        "audit.no_alarm" => (
            "No alarm audit rows yet.",
            "Noch keine Alarm-Audit-Einträge.",
            "Aucune ligne d'audit d'alarme.",
            "Nu există încă rânduri de audit pentru alarme.",
            "Aún no hay filas de auditoría de alarmas.",
        ),
        "audit.dir_to_remote" => (
            "→ remote",
            "→ remote",
            "→ distant",
            "→ remote",
            "→ remoto",
        ),
        "audit.dir_from_remote" => (
            "← remote",
            "← remote",
            "← distant",
            "← remote",
            "← remoto",
        ),

        // ---- profile ----
        "profile.heading" => ("Profile", "Profil", "Profil", "Profil", "Perfil"),
        "profile.signed_in_as" => (
            "Signed in as",
            "Angemeldet als",
            "Connecté en tant que",
            "Conectat ca",
            "Conectado como",
        ),
        "profile.info_heading" => (
            "Profile info",
            "Profilinformationen",
            "Informations du profil",
            "Informații profil",
            "Información del perfil",
        ),
        "profile.display_name" => (
            "Display name",
            "Anzeigename",
            "Nom affiché",
            "Nume afișat",
            "Nombre visible",
        ),
        "profile.email" => ("Email", "E-Mail", "E-mail", "Email", "Correo"),
        "profile.profile_updated" => (
            "Profile updated.",
            "Profil aktualisiert.",
            "Profil mis à jour.",
            "Profil actualizat.",
            "Perfil actualizado.",
        ),
        "profile.password_heading" => (
            "Change password",
            "Passwort ändern",
            "Changer le mot de passe",
            "Schimbă parola",
            "Cambiar contraseña",
        ),
        "profile.password_oidc" => (
            "Password",
            "Passwort",
            "Mot de passe",
            "Parolă",
            "Contraseña",
        ),
        "profile.password_oidc_msg" => (
            "Your account signs in via your organisation's identity provider — there's no local password to change here.",
            "Ihr Konto meldet sich über den Identitätsanbieter Ihrer Organisation an — hier gibt es kein lokales Passwort zum Ändern.",
            "Votre compte se connecte via le fournisseur d'identité de votre organisation — il n'y a pas de mot de passe local à changer ici.",
            "Contul dvs. se autentifică prin furnizorul de identitate al organizației dvs. — nu există o parolă locală de schimbat aici.",
            "Tu cuenta inicia sesión a través del proveedor de identidad de tu organización — no hay una contraseña local que cambiar aquí.",
        ),
        "profile.current_password" => (
            "current password",
            "aktuelles Passwort",
            "mot de passe actuel",
            "parola curentă",
            "contraseña actual",
        ),
        "profile.new_password" => (
            "new password",
            "neues Passwort",
            "nouveau mot de passe",
            "parolă nouă",
            "nueva contraseña",
        ),
        "profile.confirm_new" => (
            "confirm new",
            "neues bestätigen",
            "confirmer",
            "confirmă",
            "confirmar nueva",
        ),
        "profile.update_password" => (
            "Update password",
            "Passwort aktualisieren",
            "Mettre à jour le mot de passe",
            "Actualizează parola",
            "Actualizar contraseña",
        ),
        "profile.password_updated" => (
            "Password updated.",
            "Passwort aktualisiert.",
            "Mot de passe mis à jour.",
            "Parolă actualizată.",
            "Contraseña actualizada.",
        ),
        "profile.password_oidc_change" => (
            "Your account signs in via the identity provider — change the password there.",
            "Ihr Konto meldet sich über den Identitätsanbieter an — ändern Sie das Passwort dort.",
            "Votre compte se connecte via le fournisseur d'identité — changez le mot de passe là-bas.",
            "Contul dvs. se autentifică prin furnizorul de identitate — schimbați parola acolo.",
            "Tu cuenta inicia sesión a través del proveedor de identidad — cambia la contraseña ahí.",
        ),
        "profile.password_min" => (
            "New password must be at least 4 characters.",
            "Das neue Passwort muss mindestens 4 Zeichen lang sein.",
            "Le nouveau mot de passe doit contenir au moins 4 caractères.",
            "Parola nouă trebuie să aibă cel puțin 4 caractere.",
            "La nueva contraseña debe tener al menos 4 caracteres.",
        ),
        "profile.password_mismatch" => (
            "New password and confirmation don't match.",
            "Neues Passwort und Bestätigung stimmen nicht überein.",
            "Le nouveau mot de passe et la confirmation ne correspondent pas.",
            "Parola nouă și confirmarea nu se potrivesc.",
            "La nueva contraseña y la confirmación no coinciden.",
        ),
        "profile.current_incorrect" => (
            "Current password is incorrect.",
            "Aktuelles Passwort ist falsch.",
            "Le mot de passe actuel est incorrect.",
            "Parola curentă este incorectă.",
            "La contraseña actual es incorrecta.",
        ),
        "profile.tfa" => (
            "Two-factor authentication",
            "Zwei-Faktor-Authentifizierung",
            "Authentification à deux facteurs",
            "Autentificare în doi pași",
            "Autenticación en dos pasos",
        ),
        "profile.tfa_oidc_msg" => (
            "MFA is managed by your identity provider — local TOTP isn't used for OIDC sign-ins.",
            "MFA wird von Ihrem Identitätsanbieter verwaltet — lokales TOTP wird für OIDC-Anmeldungen nicht verwendet.",
            "La MFA est gérée par votre fournisseur d'identité — le TOTP local n'est pas utilisé pour les connexions OIDC.",
            "MFA este gestionată de furnizorul de identitate — TOTP local nu este folosit pentru autentificările OIDC.",
            "La MFA la gestiona tu proveedor de identidad — el TOTP local no se usa en los inicios de sesión OIDC.",
        ),
        "profile.tfa_enrolled_msg" => (
            "Sign-ins require a 6-digit code from your authenticator.",
            "Anmeldungen erfordern einen 6-stelligen Code aus Ihrer Authenticator-App.",
            "Les connexions nécessitent un code à 6 chiffres de votre authentificateur.",
            "Autentificările necesită un cod din 6 cifre din aplicația de autentificare.",
            "Los inicios de sesión requieren un código de 6 dígitos de tu autenticador.",
        ),
        "profile.tfa_disable" => (
            "Disable TOTP",
            "TOTP deaktivieren",
            "Désactiver TOTP",
            "Dezactivează TOTP",
            "Deshabilitar TOTP",
        ),
        "profile.tfa_confirm_remove" => (
            "Remove TOTP from your account?",
            "TOTP aus Ihrem Konto entfernen?",
            "Supprimer TOTP de votre compte ?",
            "Eliminați TOTP din contul dvs.?",
            "¿Quitar TOTP de tu cuenta?",
        ),
        "profile.tfa_intro" => (
            "Add a TOTP authenticator (1Password, Authy, Google Authenticator, etc.) so sign-ins also require a 6-digit code.",
            "Fügen Sie einen TOTP-Authenticator hinzu (1Password, Authy, Google Authenticator usw.), damit Anmeldungen zusätzlich einen 6-stelligen Code erfordern.",
            "Ajoutez un authentificateur TOTP (1Password, Authy, Google Authenticator, etc.) pour que les connexions nécessitent aussi un code à 6 chiffres.",
            "Adăugați un autentificator TOTP (1Password, Authy, Google Authenticator etc.) pentru ca autentificările să necesite și un cod din 6 cifre.",
            "Añade un autenticador TOTP (1Password, Authy, Google Authenticator, etc.) para que los inicios de sesión también requieran un código de 6 dígitos.",
        ),
        "profile.tfa_enroll" => (
            "Enroll TOTP",
            "TOTP registrieren",
            "Activer TOTP",
            "Activează TOTP",
            "Activar TOTP",
        ),
        "profile.tfa_already" => (
            "You already have TOTP enrolled. Disable it first if you want to re-enroll.",
            "Sie haben TOTP bereits registriert. Deaktivieren Sie es zuerst, wenn Sie es erneut registrieren möchten.",
            "TOTP est déjà activé. Désactivez-le d'abord pour le réactiver.",
            "TOTP este deja activat. Dezactivați-l mai întâi dacă doriți să îl reactivați.",
            "Ya tienes TOTP activado. Desactívalo primero si quieres volver a activarlo.",
        ),
        "profile.tfa_confirm_heading" => (
            "Confirm TOTP enrollment",
            "TOTP-Registrierung bestätigen",
            "Confirmer l'activation TOTP",
            "Confirmă activarea TOTP",
            "Confirmar activación TOTP",
        ),
        "profile.tfa_confirm_intro" => (
            "Scan the QR code with your authenticator app, then enter the 6-digit code it shows to confirm. Nothing is enrolled until you submit a valid code.",
            "Scannen Sie den QR-Code mit Ihrer Authenticator-App und geben Sie dann den 6-stelligen Code zur Bestätigung ein. Es wird nichts gespeichert, bis Sie einen gültigen Code übermitteln.",
            "Scannez le QR code avec votre application d'authentification, puis saisissez le code à 6 chiffres pour confirmer. Rien n'est activé tant que vous n'avez pas soumis un code valide.",
            "Scanați codul QR cu aplicația dvs. de autentificare, apoi introduceți codul din 6 cifre pentru a confirma. Nimic nu este activat până nu trimiteți un cod valid.",
            "Escanea el código QR con tu aplicación de autenticación, luego introduce el código de 6 dígitos para confirmar. Nada queda activado hasta que envíes un código válido.",
        ),
        "profile.tfa_secret_manual" => (
            "Secret (manual entry):",
            "Geheimnis (manuelle Eingabe):",
            "Secret (saisie manuelle) :",
            "Secret (introducere manuală):",
            "Secreto (introducción manual):",
        ),
        "profile.tfa_missing_secret" => (
            "Missing secret in confirm form.",
            "Geheimnis im Bestätigungsformular fehlt.",
            "Secret manquant dans le formulaire de confirmation.",
            "Lipsește secretul din formularul de confirmare.",
            "Falta el secreto en el formulario de confirmación.",
        ),
        "profile.tfa_bad_code" => (
            "Code didn't match. Try again — make sure the time on the authenticator device is in sync.",
            "Code stimmt nicht. Versuchen Sie es erneut — stellen Sie sicher, dass die Uhrzeit auf dem Authenticator-Gerät synchronisiert ist.",
            "Le code ne correspond pas. Réessayez — assurez-vous que l'heure de l'appareil d'authentification est synchronisée.",
            "Codul nu se potrivește. Încercați din nou — asigurați-vă că ora de pe dispozitivul de autentificare este sincronizată.",
            "El código no coincide. Inténtalo de nuevo — asegúrate de que la hora del dispositivo de autenticación esté sincronizada.",
        ),
        "profile.tfa_enrolled" => (
            "TOTP enrolled. Future sign-ins will require a 6-digit code.",
            "TOTP registriert. Zukünftige Anmeldungen erfordern einen 6-stelligen Code.",
            "TOTP activé. Les connexions futures nécessiteront un code à 6 chiffres.",
            "TOTP activat. Autentificările viitoare vor necesita un cod din 6 cifre.",
            "TOTP activado. Los próximos inicios de sesión requerirán un código de 6 dígitos.",
        ),
        "profile.tfa_current_pw_incorrect" => (
            "Current password is incorrect — TOTP not removed.",
            "Aktuelles Passwort ist falsch — TOTP wurde nicht entfernt.",
            "Le mot de passe actuel est incorrect — TOTP non supprimé.",
            "Parola curentă este incorectă — TOTP nu a fost eliminat.",
            "La contraseña actual es incorrecta — TOTP no se eliminó.",
        ),
        "profile.tfa_removed" => (
            "TOTP removed.",
            "TOTP entfernt.",
            "TOTP supprimé.",
            "TOTP eliminat.",
            "TOTP eliminado.",
        ),

        // ---- deploy ----
        "deploy.heading" => (
            "Deploy",
            "Bereitstellen",
            "Déployer",
            "Implementare",
            "Implementación",
        ),
        "deploy.intro" => (
            "Generate a config blob the stock client accepts via <code>rustdesk --config &lt;blob&gt;</code>, or the equivalent renamed-installer filename. The public key below is read from <code>id_ed25519.pub</code> on the server; override if you bootstrapped a custom keypair.",
            "Erzeugen Sie ein Konfig-Blob, das der Standard-Client per <code>rustdesk --config &lt;blob&gt;</code> akzeptiert, oder den entsprechenden umbenannten Installer-Dateinamen. Der öffentliche Schlüssel unten wird aus <code>id_ed25519.pub</code> auf dem Server gelesen; überschreiben Sie ihn bei einem benutzerdefinierten Schlüsselpaar.",
            "Générez un blob de configuration que le client standard accepte via <code>rustdesk --config &lt;blob&gt;</code>, ou le nom d'installeur renommé équivalent. La clé publique ci-dessous est lue depuis <code>id_ed25519.pub</code> sur le serveur ; remplacez-la si vous avez initialisé une paire de clés personnalisée.",
            "Generați un blob de configurare pe care clientul standard îl acceptă prin <code>rustdesk --config &lt;blob&gt;</code>, sau numele de fișier echivalent al instalatorului redenumit. Cheia publică de mai jos este citită din <code>id_ed25519.pub</code> de pe server; suprascrieți dacă ați inițializat o pereche de chei personalizată.",
            "Genera un blob de configuración que el cliente estándar acepta vía <code>rustdesk --config &lt;blob&gt;</code>, o el nombre de archivo equivalente del instalador renombrado. La clave pública de abajo se lee de <code>id_ed25519.pub</code> en el servidor; sobrescríbela si inicializaste un par de claves personalizado.",
        ),
        "deploy.host_label" => (
            "Rendezvous host (required)",
            "Rendezvous-Host (erforderlich)",
            "Hôte rendezvous (requis)",
            "Gazdă rendezvous (obligatoriu)",
            "Host rendezvous (obligatorio)",
        ),
        "deploy.host_hint" => (
            "The hostname or IP clients reach hbbs at (TCP/UDP 21116).",
            "Der Hostname oder die IP, über die Clients hbbs erreichen (TCP/UDP 21116).",
            "Le nom d'hôte ou l'IP par laquelle les clients atteignent hbbs (TCP/UDP 21116).",
            "Numele de gazdă sau IP-ul prin care clienții ajung la hbbs (TCP/UDP 21116).",
            "El nombre de host o la IP por la que los clientes llegan a hbbs (TCP/UDP 21116).",
        ),
        "deploy.api_label" => (
            "API URL (optional)",
            "API-URL (optional)",
            "URL API (optionnel)",
            "URL API (opțional)",
            "URL de la API (opcional)",
        ),
        "deploy.api_hint" => (
            "Full URL of this admin/login API. Defaults to <code>https://&lt;host&gt;</code>; edit if your API runs on a different scheme/port. Leave blank to disable login on the client.",
            "Vollständige URL dieser Admin-/Login-API. Standard ist <code>https://&lt;host&gt;</code>; bearbeiten Sie sie, wenn Ihre API auf einem anderen Schema/Port läuft. Lassen Sie das Feld leer, um die Anmeldung im Client zu deaktivieren.",
            "URL complète de cette API admin/login. Par défaut <code>https://&lt;host&gt;</code> ; modifiez si votre API fonctionne sur un autre schéma/port. Laissez vide pour désactiver la connexion sur le client.",
            "URL-ul complet al acestei API admin/login. Implicit <code>https://&lt;host&gt;</code>; editați dacă API-ul rulează pe un alt protocol/port. Lăsați gol pentru a dezactiva autentificarea pe client.",
            "URL completa de esta API admin/login. Por defecto <code>https://&lt;host&gt;</code>; edítala si tu API funciona en otro esquema/puerto. Déjala en blanco para desactivar el inicio de sesión en el cliente.",
        ),
        "deploy.relay_label" => (
            "Relay host (optional)",
            "Relay-Host (optional)",
            "Hôte relais (optionnel)",
            "Gazdă relay (opțional)",
            "Host relay (opcional)",
        ),
        "deploy.relay_hint" => (
            "Only set if hbbr runs on a separate host; otherwise leave blank.",
            "Nur ausfüllen, wenn hbbr auf einem separaten Host läuft; ansonsten leer lassen.",
            "À renseigner uniquement si hbbr fonctionne sur un hôte séparé ; sinon laisser vide.",
            "Setați doar dacă hbbr rulează pe o gazdă separată; altfel lăsați gol.",
            "Establece esto solo si hbbr se ejecuta en un host distinto; en caso contrario, déjalo en blanco.",
        ),
        "deploy.key_label" => (
            "Public key",
            "Öffentlicher Schlüssel",
            "Clé publique",
            "Cheie publică",
            "Clave pública",
        ),
        "deploy.key_hint" => (
            "Base64 contents of <code>id_ed25519.pub</code>.",
            "Base64-Inhalt von <code>id_ed25519.pub</code>.",
            "Contenu base64 de <code>id_ed25519.pub</code>.",
            "Conținut base64 al <code>id_ed25519.pub</code>.",
            "Contenido en base64 de <code>id_ed25519.pub</code>.",
        ),
        "deploy.generate" => (
            "Generate",
            "Generieren",
            "Générer",
            "Generează",
            "Generar",
        ),
        "deploy.host_required" => (
            "Host is required.",
            "Host ist erforderlich.",
            "L'hôte est requis.",
            "Gazda este obligatorie.",
            "El host es obligatorio.",
        ),
        "deploy.artifact_heading" => (
            "Deployment artifact",
            "Bereitstellungs-Artefakt",
            "Artefact de déploiement",
            "Artefact de implementare",
            "Artefacto de implementación",
        ),
        "deploy.artifact_intro" => (
            "Pick whichever path fits your rollout. All three produce the same client config.",
            "Wählen Sie den Weg, der zu Ihrem Rollout passt. Alle drei erzeugen die gleiche Client-Konfiguration.",
            "Choisissez le chemin qui correspond à votre déploiement. Les trois produisent la même configuration client.",
            "Alegeți calea care se potrivește implementării. Toate trei produc aceeași configurație de client.",
            "Elige la opción que mejor se ajuste a tu despliegue. Las tres producen la misma configuración del cliente.",
        ),
        "deploy.cmd_a_label" => (
            "A. Post-install command (Windows, Administrator)",
            "A. Befehl nach Installation (Windows, Administrator)",
            "A. Commande post-installation (Windows, administrateur)",
            "A. Comandă post-instalare (Windows, administrator)",
            "A. Comando post-instalación (Windows, administrador)",
        ),
        "deploy.cmd_a_hint" => (
            "Requires the client to be installed and running as admin. Equivalent on macOS/Linux: <code class=\"text-slate-300\">{0}</code>.",
            "Erfordert, dass der Client installiert ist und als Administrator läuft. Entsprechend auf macOS/Linux: <code class=\"text-slate-300\">{0}</code>.",
            "Nécessite que le client soit installé et exécuté en tant qu'admin. Équivalent sur macOS/Linux : <code class=\"text-slate-300\">{0}</code>.",
            "Necesită ca clientul să fie instalat și rulat ca administrator. Echivalent pe macOS/Linux: <code class=\"text-slate-300\">{0}</code>.",
            "Requiere que el cliente esté instalado y ejecutándose como administrador. Equivalente en macOS/Linux: <code class=\"text-slate-300\">{0}</code>.",
        ),
        "deploy.cmd_b_label" => (
            "B. Renamed installer (drop-in)",
            "B. Umbenannter Installer (Drop-in)",
            "B. Installeur renommé (drop-in)",
            "B. Instalator redenumit (drop-in)",
            "B. Instalador renombrado (drop-in)",
        ),
        "deploy.cmd_b_hint" => (
            "Rename the official RustDesk installer to this exact name and run it; the client reads its own filename on first launch and writes the config into the registry.",
            "Benennen Sie den offiziellen RustDesk-Installer genau in diesen Namen um und führen Sie ihn aus; der Client liest beim ersten Start seinen eigenen Dateinamen und schreibt die Konfiguration in die Registry.",
            "Renommez l'installeur officiel RustDesk avec exactement ce nom et lancez-le ; le client lit son propre nom de fichier au premier lancement et écrit la configuration dans le registre.",
            "Redenumiți instalatorul oficial RustDesk exact la acest nume și rulați-l; clientul citește propriul nume de fișier la prima lansare și scrie configurația în registru.",
            "Renombra el instalador oficial de RustDesk exactamente con este nombre y ejecútalo; el cliente lee su propio nombre de archivo en el primer arranque y escribe la configuración en el registro.",
        ),
        "deploy.cmd_c_label" => (
            "C. HelloAgent (Windows, MDM one-liner)",
            "C. HelloAgent (Windows, MDM-Einzeiler)",
            "C. HelloAgent (Windows, ligne unique MDM)",
            "C. HelloAgent (Windows, comandă unică MDM)",
            "C. HelloAgent (Windows, comando único MDM)",
        ),
        "deploy.cmd_c_hint" => (
            "Headless agent — registers the Windows service and imports this config in a single command. Run elevated.",
            "Headless-Agent — registriert den Windows-Dienst und importiert diese Konfiguration in einem einzigen Befehl. Mit erhöhten Rechten ausführen.",
            "Agent sans interface — enregistre le service Windows et importe cette configuration en une seule commande. Exécuter en mode élevé.",
            "Agent fără interfață — înregistrează serviciul Windows și importă această configurație într-o singură comandă. Rulați cu privilegii ridicate.",
            "Agente sin interfaz — registra el servicio de Windows e importa esta configuración en un solo comando. Ejecútalo con privilegios elevados.",
        ),
        "deploy.raw_blob" => (
            "Raw blob",
            "Rohes Blob",
            "Blob brut",
            "Blob brut",
            "Blob sin procesar",
        ),

        // unknown key — return a stable placeholder so callers see something
        // they can grep for. We can't return `key` itself since the function
        // signature promises a `'static` borrow.
        _ => return "<missing translation>",
    };
    match lang {
        Lang::En => en,
        Lang::De => de,
        Lang::Fr => fr,
        Lang::Ro => ro,
        Lang::Es => es,
    }
}

/// One-arg formatted lookup. Replaces `{0}` in the template.
pub fn tf1(lang: Lang, key: &str, a: &str) -> String {
    t(lang, key).replace("{0}", a)
}

/// Two-arg formatted lookup. Replaces `{0}` and `{1}` in the template.
pub fn tf2(lang: Lang, key: &str, a: &str, b: &str) -> String {
    t(lang, key).replace("{0}", a).replace("{1}", b)
}
