feat: i18n — Locale-basierte Strings (DE/EN) statt hardcoded Deutsch
Liest LANG aus Umgebung oder /etc/locale.conf und liefert deutsche oder englische UI-Strings. Alle hardcoded Strings in greeter.py durch Strings-Dataclass ersetzt. Fallback auf Englisch bei unbekannter Locale.
This commit is contained in:
parent
6554dc625d
commit
0f72df8603
@ -15,6 +15,7 @@ gi.require_version("Gdk", "4.0")
|
|||||||
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf
|
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf
|
||||||
|
|
||||||
from moongreet.config import load_config
|
from moongreet.config import load_config
|
||||||
|
from moongreet.i18n import load_strings, Strings
|
||||||
from moongreet.ipc import create_session, post_auth_response, start_session, cancel_session
|
from moongreet.ipc import create_session, post_auth_response, start_session, cancel_session
|
||||||
from moongreet.users import User, get_users, get_avatar_path, get_user_gtk_theme
|
from moongreet.users import User, get_users, get_avatar_path, get_user_gtk_theme
|
||||||
from moongreet.sessions import Session, get_sessions
|
from moongreet.sessions import Session, get_sessions
|
||||||
@ -27,13 +28,15 @@ DEFAULT_AVATAR_PATH = PACKAGE_DATA / "default-avatar.svg"
|
|||||||
AVATAR_SIZE = 128
|
AVATAR_SIZE = 128
|
||||||
|
|
||||||
|
|
||||||
def faillock_warning(attempt_count: int) -> str | None:
|
def faillock_warning(attempt_count: int, strings: Strings | None = None) -> str | None:
|
||||||
"""Return a warning if the user is approaching or has reached the faillock limit."""
|
"""Return a warning if the user is approaching or has reached the faillock limit."""
|
||||||
|
if strings is None:
|
||||||
|
strings = load_strings()
|
||||||
remaining = FAILLOCK_MAX_ATTEMPTS - attempt_count
|
remaining = FAILLOCK_MAX_ATTEMPTS - attempt_count
|
||||||
if remaining <= 0:
|
if remaining <= 0:
|
||||||
return "Konto ist möglicherweise gesperrt"
|
return strings.faillock_locked
|
||||||
if remaining == 1:
|
if remaining == 1:
|
||||||
return f"Noch {remaining} Versuch vor Kontosperrung!"
|
return strings.faillock_attempts_remaining.format(n=remaining)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -46,6 +49,7 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
|||||||
self.set_default_size(1920, 1080)
|
self.set_default_size(1920, 1080)
|
||||||
|
|
||||||
self._config = load_config()
|
self._config = load_config()
|
||||||
|
self._strings = load_strings()
|
||||||
self._users = get_users()
|
self._users = get_users()
|
||||||
self._sessions = get_sessions()
|
self._sessions = get_sessions()
|
||||||
self._selected_user: User | None = None
|
self._selected_user: User | None = None
|
||||||
@ -140,7 +144,7 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
|||||||
# Password entry
|
# Password entry
|
||||||
self._password_entry = Gtk.PasswordEntry()
|
self._password_entry = Gtk.PasswordEntry()
|
||||||
self._password_entry.set_hexpand(True)
|
self._password_entry.set_hexpand(True)
|
||||||
self._password_entry.set_property("placeholder-text", "Passwort")
|
self._password_entry.set_property("placeholder-text", self._strings.password_placeholder)
|
||||||
self._password_entry.set_property("show-peek-icon", True)
|
self._password_entry.set_property("show-peek-icon", True)
|
||||||
self._password_entry.add_css_class("password-entry")
|
self._password_entry.add_css_class("password-entry")
|
||||||
self._password_entry.connect("activate", self._on_login_activate)
|
self._password_entry.connect("activate", self._on_login_activate)
|
||||||
@ -190,14 +194,14 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
|||||||
reboot_btn = Gtk.Button()
|
reboot_btn = Gtk.Button()
|
||||||
reboot_btn.set_icon_name("system-reboot-symbolic")
|
reboot_btn.set_icon_name("system-reboot-symbolic")
|
||||||
reboot_btn.add_css_class("power-button")
|
reboot_btn.add_css_class("power-button")
|
||||||
reboot_btn.set_tooltip_text("Neustart")
|
reboot_btn.set_tooltip_text(self._strings.reboot_tooltip)
|
||||||
reboot_btn.connect("clicked", self._on_reboot_clicked)
|
reboot_btn.connect("clicked", self._on_reboot_clicked)
|
||||||
power_box.append(reboot_btn)
|
power_box.append(reboot_btn)
|
||||||
|
|
||||||
shutdown_btn = Gtk.Button()
|
shutdown_btn = Gtk.Button()
|
||||||
shutdown_btn.set_icon_name("system-shutdown-symbolic")
|
shutdown_btn.set_icon_name("system-shutdown-symbolic")
|
||||||
shutdown_btn.add_css_class("power-button")
|
shutdown_btn.add_css_class("power-button")
|
||||||
shutdown_btn.set_tooltip_text("Herunterfahren")
|
shutdown_btn.set_tooltip_text(self._strings.shutdown_tooltip)
|
||||||
shutdown_btn.connect("clicked", self._on_shutdown_clicked)
|
shutdown_btn.connect("clicked", self._on_shutdown_clicked)
|
||||||
power_box.append(shutdown_btn)
|
power_box.append(shutdown_btn)
|
||||||
|
|
||||||
@ -341,7 +345,7 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
|||||||
password = entry.get_text()
|
password = entry.get_text()
|
||||||
session = self._get_selected_session()
|
session = self._get_selected_session()
|
||||||
if not session:
|
if not session:
|
||||||
self._show_error("Keine Session ausgewählt")
|
self._show_error(self._strings.no_session_selected)
|
||||||
return
|
return
|
||||||
|
|
||||||
self._attempt_login(self._selected_user, password, session)
|
self._attempt_login(self._selected_user, password, session)
|
||||||
@ -350,15 +354,15 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
|||||||
"""Validate that GREETD_SOCK points to an absolute path and a real socket."""
|
"""Validate that GREETD_SOCK points to an absolute path and a real socket."""
|
||||||
path = Path(sock_path)
|
path = Path(sock_path)
|
||||||
if not path.is_absolute():
|
if not path.is_absolute():
|
||||||
self._show_error("GREETD_SOCK ist kein absoluter Pfad")
|
self._show_error(self._strings.greetd_sock_not_absolute)
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
mode = path.stat().st_mode
|
mode = path.stat().st_mode
|
||||||
if not stat.S_ISSOCK(mode):
|
if not stat.S_ISSOCK(mode):
|
||||||
self._show_error("GREETD_SOCK zeigt nicht auf einen Socket")
|
self._show_error(self._strings.greetd_sock_not_socket)
|
||||||
return False
|
return False
|
||||||
except OSError:
|
except OSError:
|
||||||
self._show_error("GREETD_SOCK nicht erreichbar")
|
self._show_error(self._strings.greetd_sock_unreachable)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -375,7 +379,7 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
|||||||
"""Attempt to authenticate and start a session via greetd IPC."""
|
"""Attempt to authenticate and start a session via greetd IPC."""
|
||||||
sock_path = os.environ.get("GREETD_SOCK")
|
sock_path = os.environ.get("GREETD_SOCK")
|
||||||
if not sock_path:
|
if not sock_path:
|
||||||
self._show_error("GREETD_SOCK nicht gesetzt")
|
self._show_error(self._strings.greetd_sock_not_set)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not self._validate_greetd_sock(sock_path):
|
if not self._validate_greetd_sock(sock_path):
|
||||||
@ -391,7 +395,7 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
|||||||
response = create_session(sock, user.username)
|
response = create_session(sock, user.username)
|
||||||
|
|
||||||
if response.get("type") == "error":
|
if response.get("type") == "error":
|
||||||
self._show_greetd_error(response, "Authentifizierung fehlgeschlagen")
|
self._show_greetd_error(response, self._strings.auth_failed)
|
||||||
self._close_greetd_sock()
|
self._close_greetd_sock()
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -401,8 +405,8 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
|||||||
|
|
||||||
if response.get("type") == "error":
|
if response.get("type") == "error":
|
||||||
self._failed_attempts[user.username] = self._failed_attempts.get(user.username, 0) + 1
|
self._failed_attempts[user.username] = self._failed_attempts.get(user.username, 0) + 1
|
||||||
self._show_greetd_error(response, "Falsches Passwort")
|
self._show_greetd_error(response, self._strings.wrong_password)
|
||||||
warning = faillock_warning(self._failed_attempts[user.username])
|
warning = faillock_warning(self._failed_attempts[user.username], self._strings)
|
||||||
if warning:
|
if warning:
|
||||||
current = self._error_label.get_text()
|
current = self._error_label.get_text()
|
||||||
self._error_label.set_text(f"{current}\n{warning}")
|
self._error_label.set_text(f"{current}\n{warning}")
|
||||||
@ -413,14 +417,14 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
|||||||
# Multi-stage auth (e.g. TOTP) is not supported
|
# Multi-stage auth (e.g. TOTP) is not supported
|
||||||
cancel_session(sock)
|
cancel_session(sock)
|
||||||
self._close_greetd_sock()
|
self._close_greetd_sock()
|
||||||
self._show_error("Mehrstufige Authentifizierung wird nicht unterstützt")
|
self._show_error(self._strings.multi_stage_unsupported)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Step 3: Start session
|
# Step 3: Start session
|
||||||
if response.get("type") == "success":
|
if response.get("type") == "success":
|
||||||
cmd = shlex.split(session.exec_cmd)
|
cmd = shlex.split(session.exec_cmd)
|
||||||
if not cmd or not Path(cmd[0]).is_absolute():
|
if not cmd or not Path(cmd[0]).is_absolute():
|
||||||
self._show_error("Ungültiger Session-Befehl")
|
self._show_error(self._strings.invalid_session_command)
|
||||||
cancel_session(sock)
|
cancel_session(sock)
|
||||||
self._close_greetd_sock()
|
self._close_greetd_sock()
|
||||||
return
|
return
|
||||||
@ -432,16 +436,16 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
|||||||
self.get_application().quit()
|
self.get_application().quit()
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
self._show_greetd_error(response, "Session konnte nicht gestartet werden")
|
self._show_greetd_error(response, self._strings.session_start_failed)
|
||||||
|
|
||||||
self._close_greetd_sock()
|
self._close_greetd_sock()
|
||||||
|
|
||||||
except ConnectionError as e:
|
except ConnectionError as e:
|
||||||
self._close_greetd_sock()
|
self._close_greetd_sock()
|
||||||
self._show_error(f"Verbindungsfehler: {e}")
|
self._show_error(self._strings.connection_error.format(error=e))
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self._close_greetd_sock()
|
self._close_greetd_sock()
|
||||||
self._show_error(f"Socket-Fehler: {e}")
|
self._show_error(self._strings.socket_error.format(error=e))
|
||||||
|
|
||||||
def _cancel_pending_session(self) -> None:
|
def _cancel_pending_session(self) -> None:
|
||||||
"""Cancel any in-progress greetd session."""
|
"""Cancel any in-progress greetd session."""
|
||||||
@ -483,14 +487,14 @@ class GreeterWindow(Gtk.ApplicationWindow):
|
|||||||
try:
|
try:
|
||||||
reboot()
|
reboot()
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
self._show_error("Neustart fehlgeschlagen")
|
self._show_error(self._strings.reboot_failed)
|
||||||
|
|
||||||
def _on_shutdown_clicked(self, button: Gtk.Button) -> None:
|
def _on_shutdown_clicked(self, button: Gtk.Button) -> None:
|
||||||
"""Handle shutdown button click."""
|
"""Handle shutdown button click."""
|
||||||
try:
|
try:
|
||||||
shutdown()
|
shutdown()
|
||||||
except subprocess.CalledProcessError:
|
except subprocess.CalledProcessError:
|
||||||
self._show_error("Herunterfahren fehlgeschlagen")
|
self._show_error(self._strings.shutdown_failed)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _load_last_user() -> str | None:
|
def _load_last_user() -> str | None:
|
||||||
|
|||||||
113
src/moongreet/i18n.py
Normal file
113
src/moongreet/i18n.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
# ABOUTME: Locale detection and string lookup for the greeter UI.
|
||||||
|
# ABOUTME: Reads system locale (LANG or /etc/locale.conf) and provides DE or EN strings.
|
||||||
|
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
DEFAULT_LOCALE_CONF = Path("/etc/locale.conf")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Strings:
|
||||||
|
"""All user-visible strings for the greeter UI."""
|
||||||
|
|
||||||
|
# UI labels
|
||||||
|
password_placeholder: str
|
||||||
|
reboot_tooltip: str
|
||||||
|
shutdown_tooltip: str
|
||||||
|
|
||||||
|
# Error messages
|
||||||
|
no_session_selected: str
|
||||||
|
greetd_sock_not_set: str
|
||||||
|
greetd_sock_not_absolute: str
|
||||||
|
greetd_sock_not_socket: str
|
||||||
|
greetd_sock_unreachable: str
|
||||||
|
auth_failed: str
|
||||||
|
wrong_password: str
|
||||||
|
multi_stage_unsupported: str
|
||||||
|
invalid_session_command: str
|
||||||
|
session_start_failed: str
|
||||||
|
reboot_failed: str
|
||||||
|
shutdown_failed: str
|
||||||
|
|
||||||
|
# Templates (use .format())
|
||||||
|
connection_error: str
|
||||||
|
socket_error: str
|
||||||
|
faillock_attempts_remaining: str
|
||||||
|
faillock_locked: str
|
||||||
|
|
||||||
|
|
||||||
|
_STRINGS_DE = Strings(
|
||||||
|
password_placeholder="Passwort",
|
||||||
|
reboot_tooltip="Neustart",
|
||||||
|
shutdown_tooltip="Herunterfahren",
|
||||||
|
no_session_selected="Keine Session ausgewählt",
|
||||||
|
greetd_sock_not_set="GREETD_SOCK nicht gesetzt",
|
||||||
|
greetd_sock_not_absolute="GREETD_SOCK ist kein absoluter Pfad",
|
||||||
|
greetd_sock_not_socket="GREETD_SOCK zeigt nicht auf einen Socket",
|
||||||
|
greetd_sock_unreachable="GREETD_SOCK nicht erreichbar",
|
||||||
|
auth_failed="Authentifizierung fehlgeschlagen",
|
||||||
|
wrong_password="Falsches Passwort",
|
||||||
|
multi_stage_unsupported="Mehrstufige Authentifizierung wird nicht unterstützt",
|
||||||
|
invalid_session_command="Ungültiger Session-Befehl",
|
||||||
|
session_start_failed="Session konnte nicht gestartet werden",
|
||||||
|
reboot_failed="Neustart fehlgeschlagen",
|
||||||
|
shutdown_failed="Herunterfahren fehlgeschlagen",
|
||||||
|
connection_error="Verbindungsfehler: {error}",
|
||||||
|
socket_error="Socket-Fehler: {error}",
|
||||||
|
faillock_attempts_remaining="Noch {n} Versuch(e) vor Kontosperrung!",
|
||||||
|
faillock_locked="Konto ist möglicherweise gesperrt",
|
||||||
|
)
|
||||||
|
|
||||||
|
_STRINGS_EN = Strings(
|
||||||
|
password_placeholder="Password",
|
||||||
|
reboot_tooltip="Reboot",
|
||||||
|
shutdown_tooltip="Shut down",
|
||||||
|
no_session_selected="No session selected",
|
||||||
|
greetd_sock_not_set="GREETD_SOCK not set",
|
||||||
|
greetd_sock_not_absolute="GREETD_SOCK is not an absolute path",
|
||||||
|
greetd_sock_not_socket="GREETD_SOCK does not point to a socket",
|
||||||
|
greetd_sock_unreachable="GREETD_SOCK unreachable",
|
||||||
|
auth_failed="Authentication failed",
|
||||||
|
wrong_password="Wrong password",
|
||||||
|
multi_stage_unsupported="Multi-stage authentication is not supported",
|
||||||
|
invalid_session_command="Invalid session command",
|
||||||
|
session_start_failed="Failed to start session",
|
||||||
|
reboot_failed="Reboot failed",
|
||||||
|
shutdown_failed="Shutdown failed",
|
||||||
|
connection_error="Connection error: {error}",
|
||||||
|
socket_error="Socket error: {error}",
|
||||||
|
faillock_attempts_remaining="{n} attempt(s) remaining before lockout!",
|
||||||
|
faillock_locked="Account may be locked",
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOCALE_MAP: dict[str, Strings] = {
|
||||||
|
"de": _STRINGS_DE,
|
||||||
|
"en": _STRINGS_EN,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def detect_locale(locale_conf_path: Path = DEFAULT_LOCALE_CONF) -> str:
|
||||||
|
"""Determine the system language from LANG env var or /etc/locale.conf."""
|
||||||
|
lang = os.environ.get("LANG")
|
||||||
|
|
||||||
|
if not lang and locale_conf_path.exists():
|
||||||
|
for line in locale_conf_path.read_text().splitlines():
|
||||||
|
if line.startswith("LANG="):
|
||||||
|
lang = line.split("=", 1)[1].strip()
|
||||||
|
break
|
||||||
|
|
||||||
|
if not lang or lang in ("C", "POSIX"):
|
||||||
|
return "en"
|
||||||
|
|
||||||
|
# Extract language prefix: "de_DE.UTF-8" → "de"
|
||||||
|
lang = lang.split("_")[0].split(".")[0]
|
||||||
|
return lang
|
||||||
|
|
||||||
|
|
||||||
|
def load_strings(locale: str | None = None) -> Strings:
|
||||||
|
"""Return the string table for the given locale, defaulting to English."""
|
||||||
|
if locale is None:
|
||||||
|
locale = detect_locale()
|
||||||
|
return _LOCALE_MAP.get(locale, _STRINGS_EN)
|
||||||
118
tests/test_i18n.py
Normal file
118
tests/test_i18n.py
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
# ABOUTME: Tests for locale detection and string lookup.
|
||||||
|
# ABOUTME: Verifies DE/EN selection based on system locale.
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from moongreet.i18n import detect_locale, load_strings, Strings
|
||||||
|
|
||||||
|
|
||||||
|
class TestDetectLocale:
|
||||||
|
"""Tests for system locale detection."""
|
||||||
|
|
||||||
|
def test_reads_lang_env(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setenv("LANG", "de_DE.UTF-8")
|
||||||
|
|
||||||
|
result = detect_locale()
|
||||||
|
|
||||||
|
assert result == "de"
|
||||||
|
|
||||||
|
def test_reads_lang_without_region(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setenv("LANG", "en_US.UTF-8")
|
||||||
|
|
||||||
|
result = detect_locale()
|
||||||
|
|
||||||
|
assert result == "en"
|
||||||
|
|
||||||
|
def test_falls_back_to_locale_conf(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.delenv("LANG", raising=False)
|
||||||
|
locale_conf = tmp_path / "locale.conf"
|
||||||
|
locale_conf.write_text("LANG=de_AT.UTF-8\n")
|
||||||
|
|
||||||
|
result = detect_locale(locale_conf_path=locale_conf)
|
||||||
|
|
||||||
|
assert result == "de"
|
||||||
|
|
||||||
|
def test_defaults_to_english(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.delenv("LANG", raising=False)
|
||||||
|
missing = tmp_path / "nonexistent"
|
||||||
|
|
||||||
|
result = detect_locale(locale_conf_path=missing)
|
||||||
|
|
||||||
|
assert result == "en"
|
||||||
|
|
||||||
|
def test_handles_bare_language_code(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setenv("LANG", "de")
|
||||||
|
|
||||||
|
result = detect_locale()
|
||||||
|
|
||||||
|
assert result == "de"
|
||||||
|
|
||||||
|
def test_handles_c_locale(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setenv("LANG", "C")
|
||||||
|
|
||||||
|
result = detect_locale()
|
||||||
|
|
||||||
|
assert result == "en"
|
||||||
|
|
||||||
|
def test_handles_posix_locale(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setenv("LANG", "POSIX")
|
||||||
|
|
||||||
|
result = detect_locale()
|
||||||
|
|
||||||
|
assert result == "en"
|
||||||
|
|
||||||
|
|
||||||
|
class TestLoadStrings:
|
||||||
|
"""Tests for loading the correct string table."""
|
||||||
|
|
||||||
|
def test_loads_german_strings(self) -> None:
|
||||||
|
strings = load_strings("de")
|
||||||
|
|
||||||
|
assert strings.password_placeholder == "Passwort"
|
||||||
|
assert strings.reboot_tooltip == "Neustart"
|
||||||
|
assert strings.shutdown_tooltip == "Herunterfahren"
|
||||||
|
|
||||||
|
def test_loads_english_strings(self) -> None:
|
||||||
|
strings = load_strings("en")
|
||||||
|
|
||||||
|
assert strings.password_placeholder == "Password"
|
||||||
|
assert strings.reboot_tooltip == "Reboot"
|
||||||
|
assert strings.shutdown_tooltip == "Shut down"
|
||||||
|
|
||||||
|
def test_unknown_locale_falls_back_to_english(self) -> None:
|
||||||
|
strings = load_strings("fr")
|
||||||
|
|
||||||
|
assert strings.password_placeholder == "Password"
|
||||||
|
|
||||||
|
def test_returns_strings_dataclass(self) -> None:
|
||||||
|
strings = load_strings("de")
|
||||||
|
|
||||||
|
assert isinstance(strings, Strings)
|
||||||
|
|
||||||
|
def test_error_messages_are_present(self) -> None:
|
||||||
|
strings = load_strings("en")
|
||||||
|
|
||||||
|
assert strings.wrong_password
|
||||||
|
assert strings.auth_failed
|
||||||
|
assert strings.reboot_failed
|
||||||
|
assert strings.shutdown_failed
|
||||||
|
assert strings.no_session_selected
|
||||||
|
assert strings.multi_stage_unsupported
|
||||||
|
assert strings.invalid_session_command
|
||||||
|
assert strings.session_start_failed
|
||||||
|
assert strings.faillock_locked
|
||||||
|
|
||||||
|
def test_faillock_warning_template(self) -> None:
|
||||||
|
strings = load_strings("de")
|
||||||
|
|
||||||
|
# Template should accept an int for remaining attempts
|
||||||
|
result = strings.faillock_attempts_remaining.format(n=1)
|
||||||
|
assert "1" in result
|
||||||
|
|
||||||
|
def test_connection_error_template(self) -> None:
|
||||||
|
strings = load_strings("en")
|
||||||
|
|
||||||
|
result = strings.connection_error.format(error="timeout")
|
||||||
|
assert "timeout" in result
|
||||||
Loading…
x
Reference in New Issue
Block a user