diff --git a/README.md b/README.md
index d3b66f0..0d38322 100644
--- a/README.md
+++ b/README.md
@@ -143,8 +143,9 @@ On iOS the system decides when background work runs, and it may be rare.
### Languages
-Hungarian and English, following the phone's language; from Android 13 the
-app's language can be set on its own in the system settings.
+Hungarian, English, German, Spanish and French. The app follows the phone's
+language, or the one picked under Settings → Language; background
+notifications come in the same language.
## Screens
@@ -251,7 +252,7 @@ lib/
monitor/ background checks, the outage log, notifications, the schedule
ui/ the screens, the theme and shared widgets
demo/ the invented servers of the demo build
- l10n/ Hungarian and English
+ l10n/ Hungarian, English, German, Spanish and French
test/ unit and widget tests; fixtures from a real server and ssh-keygen
assets/ the icon and JetBrains Mono (OFL)
docs/ the cover and the screenshots
diff --git a/android/app/src/main/res/xml/locales_config.xml b/android/app/src/main/res/xml/locales_config.xml
index 5da3692..85a3299 100644
--- a/android/app/src/main/res/xml/locales_config.xml
+++ b/android/app/src/main/res/xml/locales_config.xml
@@ -3,4 +3,7 @@
+
+
+
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 658b7ec..1838209 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -21,6 +21,9 @@
hu
en
+ de
+ es
+ fr
CFBundleDisplayName
ServerDeck
diff --git a/lib/app.dart b/lib/app.dart
index 8dcedd4..1f13e15 100644
--- a/lib/app.dart
+++ b/lib/app.dart
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'data/app_data.dart';
import 'l10n/generated/app_localizations.dart';
+import 'l10n/languages.dart';
import 'ui/app_lock.dart';
import 'ui/servers_screen.dart';
import 'ui/theme.dart';
@@ -16,11 +17,15 @@ class ServerDeckApp extends ConsumerWidget {
final themeMode = ref.watch(
appDataProvider.select((d) => d.value?.settings.themeMode),
);
+ final language = ref.watch(
+ appDataProvider.select((d) => d.value?.settings.language),
+ );
return MaterialApp(
onGenerateTitle: (context) => AppLocalizations.of(context).appTitle,
debugShowCheckedModeBanner: false,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
+ locale: chosenLocale(language),
theme: buildTheme(Brightness.light),
darkTheme: buildTheme(Brightness.dark),
themeMode: themeMode ?? ThemeMode.system,
diff --git a/lib/data/models.dart b/lib/data/models.dart
index 32e9eeb..0b1337f 100644
--- a/lib/data/models.dart
+++ b/lib/data/models.dart
@@ -293,7 +293,11 @@ class LogSource {
enum LogKind { journal, unit, file }
class Settings {
- const Settings({this.appLock = false, this.themeMode = ThemeMode.system});
+ const Settings({
+ this.appLock = false,
+ this.themeMode = ThemeMode.system,
+ this.language,
+ });
/// Ask for a fingerprint, face or the device PIN on start and on return.
final bool appLock;
@@ -301,20 +305,31 @@ class Settings {
/// Light, dark, or whatever the phone is set to.
final ThemeMode themeMode;
- Settings copyWith({bool? appLock, ThemeMode? themeMode}) => Settings(
+ /// A language code the app is translated to, or null for the phone's
+ /// language.
+ final String? language;
+
+ Settings copyWith({
+ bool? appLock,
+ ThemeMode? themeMode,
+ String? Function()? language,
+ }) => Settings(
appLock: appLock ?? this.appLock,
themeMode: themeMode ?? this.themeMode,
+ language: language != null ? language() : this.language,
);
Map toJson() => {
'appLock': appLock,
'themeMode': themeMode.name,
+ 'language': language,
};
factory Settings.fromJson(Map json) => Settings(
appLock: json['appLock'] as bool? ?? false,
themeMode:
ThemeMode.values.asNameMap()[json['themeMode']] ?? ThemeMode.system,
+ language: json['language'] as String?,
);
}
diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb
new file mode 100644
index 0000000..63a723f
--- /dev/null
+++ b/lib/l10n/app_de.arb
@@ -0,0 +1,274 @@
+{
+ "@@locale": "de",
+ "appTitle": "ServerDeck",
+ "ok": "OK",
+ "cancel": "Abbrechen",
+ "save": "Speichern",
+ "delete": "Löschen",
+ "edit": "Bearbeiten",
+ "rename": "Umbenennen",
+ "paste": "Einfügen",
+ "fieldRequired": "Pflichtfeld",
+ "serversTitle": "Server",
+ "serversEmptyTitle": "Noch keine Server",
+ "serversEmpty": "Füge einen Server mit seiner Adresse und deinem Benutzernamen hinzu. ServerDeck verbindet sich über SSH; auf dem Server muss nichts installiert werden.",
+ "serverAdd": "Server hinzufügen",
+ "serverEditTitle": "Server bearbeiten",
+ "serverDeleteTitle": "{name} löschen?",
+ "serverDeleteMessage": "Das gespeicherte Passwort, die Befehle und die Logquellen werden mitgelöscht. Die Schlüssel bleiben.",
+ "serverSectionConnection": "Verbindung",
+ "serverSectionAuth": "Anmeldung",
+ "serverHost": "Adresse",
+ "serverUser": "Benutzer",
+ "serverPort": "Port",
+ "serverPortInvalid": "1–65535",
+ "serverName": "Name",
+ "serverNameHint": "Leer lassen für die Adresse",
+ "statusChecking": "…",
+ "statusDown": "offline",
+ "authKey": "Schlüssel",
+ "authPassword": "Passwort",
+ "authKeyPick": "SSH-Schlüssel",
+ "authKeyMissing": "Wähle einen Schlüssel",
+ "authKeyNone": "Noch keine Schlüssel. Erzeuge einen und trage seinen öffentlichen Teil in ~/.ssh/authorized_keys auf dem Server ein.",
+ "authPasswordKept": "Gespeichert. Leer lassen, um es zu behalten.",
+ "keysTitle": "SSH-Schlüssel",
+ "keysEmptyTitle": "Noch keine Schlüssel",
+ "keysEmpty": "Erzeuge einen neuen Ed25519-Schlüssel oder importiere einen vorhandenen. Der private Schlüssel bleibt im verschlüsselten Speicher des Telefons.",
+ "keyAdd": "Schlüssel hinzufügen",
+ "keyGenerateNew": "Neuen Schlüssel erzeugen",
+ "keyGenerateHint": "Ed25519, direkt auf dem Telefon erzeugt",
+ "keyGenerated": "Schlüssel bereit. Kopiere seinen öffentlichen Teil auf den Server.",
+ "keyImport": "Schlüssel importieren",
+ "keyImportHint": "Privater OpenSSH-, RSA- oder ECDSA-Schlüssel",
+ "keyImportNote": "Ein passwortgeschützter Schlüssel wird einmal entsperrt und im verschlüsselten Speicher des Telefons abgelegt. Die Passphrase wird nicht gespeichert.",
+ "keyImportedName": "Importierter Schlüssel",
+ "keyName": "Name",
+ "keyPrivate": "Privater Schlüssel",
+ "keyPassphrase": "Passphrase des Schlüssels",
+ "keyCopy": "Öffentlichen Schlüssel kopieren",
+ "keyCopied": "Öffentlicher Schlüssel kopiert",
+ "keyDeleteTitle": "{name} löschen?",
+ "keyDeleteMessage": "Der private Schlüssel wird endgültig vom Telefon gelöscht.",
+ "keyDeleteUsed": "Der private Schlüssel wird endgültig vom Telefon gelöscht. Diese Server nutzen ihn und brauchen dann einen anderen Schlüssel: {servers}",
+ "keyErrorNotAKey": "Das ist kein privater Schlüssel. Füge den ganzen Text ein, der mit -----BEGIN beginnt.",
+ "keyErrorNeedsPassphrase": "Dieser Schlüssel ist geschützt. Gib seine Passphrase ein.",
+ "keyErrorWrongPassphrase": "Falsche Passphrase.",
+ "keyErrorUnsupported": "ServerDeck kennt diesen Schlüsseltyp nicht. Verwende einen Ed25519-, RSA- oder ECDSA-Schlüssel.",
+ "sessionOnline": "online",
+ "sessionConnecting": "verbinde",
+ "sessionReconnect": "Neu verbinden",
+ "sessionConnectingTo": "Verbinde…",
+ "problemUnreachableTitle": "Server nicht erreichbar",
+ "problemUnreachable": "Keine Verbindung zu {host} auf Port {port} möglich. Prüfe die Adresse, den Port und die Verbindung des Telefons.",
+ "problemTimeoutTitle": "Keine Antwort",
+ "problemTimeout": "Der Server hat nicht innerhalb von 12 Sekunden geantwortet. Vielleicht ist er überlastet, oder eine Firewall verwirft die Verbindung.",
+ "problemAuthTitle": "Anmeldung fehlgeschlagen",
+ "problemAuthKey": "Der Server hat den Schlüssel für {user} nicht akzeptiert. Trage den öffentlichen Teil des Schlüssels in ~/.ssh/authorized_keys auf dem Server ein.",
+ "problemAuthPassword": "Der Server hat das Passwort für {user} nicht akzeptiert.",
+ "problemHostKeyChangedTitle": "Der Schlüssel des Servers hat sich geändert",
+ "problemHostKeyChanged": "Der Server hat einen anderen Identitätsschlüssel gezeigt als den, den du zuvor akzeptiert hast. Das kann eine Neuinstallation sein oder jemand dazwischen. ServerDeck hat sich nicht verbunden.",
+ "problemHostKeyRefusedTitle": "Schlüssel abgelehnt",
+ "problemHostKeyRefused": "Du hast den Schlüssel des Servers nicht akzeptiert, deshalb hat sich ServerDeck nicht angemeldet.",
+ "problemMissingTitle": "Keine Anmeldedaten",
+ "problemMissing": "Für diesen Server ist weder Schlüssel noch Passwort festgelegt. Bearbeite den Server und füge eines hinzu.",
+ "problemDisconnectedTitle": "Verbindung verloren",
+ "problemDisconnected": "Die Verbindung ist abgebrochen: ein Netzwechsel, ein Neustart des Servers, oder das Telefon war im Ruhemodus.",
+ "problemOtherTitle": "Verbindung fehlgeschlagen",
+ "hostKeyNewTitle": "Neuer Server",
+ "hostKeyNew": "Du verbindest dich zum ersten Mal mit {host}. Das ist der Identitätsschlüssel des Servers:",
+ "hostKeyCheck": "Zur Sicherheit gibt dieser Befehl auf dem Server denselben Fingerabdruck aus:",
+ "hostKeyAccept": "Akzeptieren",
+ "hostKeyReject": "Ablehnen",
+ "hostKeyKnown": "Akzeptiert",
+ "hostKeyNow": "Jetzt",
+ "hostKeyForget": "Alten Schlüssel vergessen",
+ "hostKeyForgetTitle": "Alten Schlüssel vergessen?",
+ "hostKeyForgetMessage": "Tu das nur, wenn du weißt, warum sich der Schlüssel geändert hat, zum Beispiel weil du den Server neu installiert hast. ServerDeck fragt dann nach dem neuen Schlüssel.",
+ "statsFailedTitle": "Server konnte nicht gelesen werden",
+ "statsFailed": "Die Antwort des Servers ergab keinen Sinn. Die Statistiken brauchen Linux (/proc).",
+ "statsStale": "Die letzte Aktualisierung ist fehlgeschlagen. Die Werte oben sind älter.",
+ "statCpu": "CPU",
+ "statMemory": "Speicher",
+ "statDisk": "Festplatte",
+ "statCores": "{count, plural, =1{1 Kern} other{{count} Kerne}}",
+ "statHistory": "Letzte 3 Minuten",
+ "statHistoryHint": "Wird alle 3 Sekunden aktualisiert, solange dieser Bildschirm offen ist.",
+ "statNetIn": "Ein",
+ "statNetOut": "Aus",
+ "statLoad": "Last",
+ "statLoad1": "1 Min.",
+ "statLoad5": "5 Min.",
+ "statLoad15": "15 Min.",
+ "statDisks": "Festplatten",
+ "statSwap": "Swap",
+ "statUptime": "Läuft seit",
+ "uptimeDays": "{days, plural, =1{1 Tag} other{{days} Tage}}, {hours} Std.",
+ "uptimeHours": "{hours} Std. {minutes} Min.",
+ "tabOverview": "Übersicht",
+ "tabServices": "Dienste",
+ "servicesFailedTitle": "Dienste konnten nicht aufgelistet werden",
+ "servicesSearch": "Nach Name oder Beschreibung suchen",
+ "servicesRunning": "Laufend · {count}",
+ "servicesFailed": "Fehlgeschlagen · {count}",
+ "servicesAll": "Alle · {count}",
+ "servicesNone": "Keine Treffer.",
+ "servicesNoneFailed": "Keine fehlgeschlagenen Dienste.",
+ "containersNone": "Keine Container.",
+ "stateRunning": "läuft",
+ "stateExited": "beendet",
+ "stateFailed": "fehlgeschlagen",
+ "stateActivating": "startet",
+ "stateInactive": "gestoppt",
+ "stateUnhealthy": "fehlerhaft",
+ "statePaused": "pausiert",
+ "stateRestarting": "startet neu",
+ "stateExitedContainer": "beendet",
+ "stateCreated": "erstellt",
+ "actionRestart": "Neu starten",
+ "actionReload": "Neu laden",
+ "actionStop": "Stoppen",
+ "actionStart": "Starten",
+ "actionConfirmTitle": "{action}: {subject}?",
+ "actionConfirmMessage": "Das wird auf dem Server ausgeführt:\n{command}",
+ "actionDone": "{action} erledigt: {subject}",
+ "actionFailed": "Das hat nicht geklappt (Exit-Code {code}). {reason}",
+ "actionNeedsSudo": "Dafür sind Administratorrechte nötig. Melde dich als root an oder erlaube sudo ohne Passwort für diesen Befehl.",
+ "tabLogs": "Logs",
+ "logsJournal": "Systemjournal",
+ "logsAdd": "Quelle",
+ "logsAddTitle": "Neue Logquelle",
+ "logsKindUnit": "Dienst",
+ "logsKindFile": "Datei",
+ "logsUnitLabel": "Name des Dienstes",
+ "logsFileLabel": "Dateipfad",
+ "logsFilter": "Filter",
+ "logsErrorsOnly": "Nur Fehler",
+ "logsPause": "Pause",
+ "logsResume": "Fortsetzen",
+ "logsClear": "Bildschirm leeren",
+ "logsNewLines": "{count, plural, =1{1 neue Zeile} other{{count} neue Zeilen}}",
+ "logsEnded": "Das Verfolgen des Logs wurde beendet (Exit-Code {code}).",
+ "logsDeleteTitle": "{name} löschen?",
+ "logsDeleteMessage": "Nur die gespeicherte Quelle wird gelöscht, nicht das Log auf dem Server.",
+ "tabCommands": "Befehle",
+ "commandAdd": "Neuer Befehl",
+ "commandEdit": "Befehl bearbeiten",
+ "commandQuick": "Einmaliger Befehl…",
+ "commandRun": "Ausführen",
+ "commandRunTitle": "{name} ausführen?",
+ "commandSaved": "Gespeicherte Befehle",
+ "commandTemplates": "Für den Anfang",
+ "commandTemplatesHint": "Tippe auf einen, um ihn zu deinen gespeicherten Befehlen hinzuzufügen. Sie lesen nur.",
+ "commandLabel": "Befehl",
+ "commandEverywhere": "Auf allen Servern",
+ "commandEverywhereHint": "Erscheint bei all deinen Servern",
+ "commandConfirm": "Vor dem Ausführen fragen",
+ "commandCopy": "Ausgabe kopieren",
+ "commandCopied": "Ausgabe kopiert",
+ "commandError": "Fehler",
+ "commandStopped": "gestoppt",
+ "commandExit": "Exit {code}",
+ "commandRunning": "läuft",
+ "commandNoOutput": "Keine Ausgabe.",
+ "tplDisk": "Speicherbelegung",
+ "tplFolders": "Größte Ordner",
+ "tplUpdates": "Ausstehende Updates",
+ "tplWho": "Wer ist angemeldet",
+ "tplApache": "Apache-Konfiguration testen",
+ "tplNginx": "Nginx-Konfiguration testen",
+ "tplTop": "Top-CPU",
+ "tplPorts": "Offene Ports",
+ "tabTerminal": "Terminal",
+ "terminalClosed": "[Shell geschlossen]",
+ "terminalReopen": "Neue Shell",
+ "lockReason": "ServerDeck entsperren",
+ "lockTitle": "ServerDeck ist gesperrt",
+ "lockUnlock": "Entsperren",
+ "settingsTitle": "Einstellungen",
+ "settingsSecurity": "Sicherheit",
+ "settingsLock": "App-Sperre",
+ "settingsLockHint": "Fingerabdruck, Gesicht oder die PIN des Telefons beim Start und nach einer Minute im Hintergrund.",
+ "settingsLockUnsupported": "Richte zuerst eine Displaysperre auf dem Telefon ein.",
+ "settingsKnownHosts": "Bekannte Serverschlüssel",
+ "settingsKnownHostsEmpty": "Noch keine Serverschlüssel akzeptiert.",
+ "settingsKnownHostsHint": "Beim ersten Kontakt akzeptierte Identitätsschlüssel. Ändert sich der Schlüssel eines Servers, verbindet sich ServerDeck erst wieder, wenn du den alten hier vergisst.",
+ "settingsAbout": "Über",
+ "settingsAboutText": "SSH-Serververwaltung. Open Source, MIT-Lizenz. Sammelt nichts; alles bleibt auf dem Telefon.",
+ "installKey": "Schlüssel mit Passwort installieren",
+ "installKeyAction": "Installieren",
+ "installKeyHint": "ServerDeck meldet sich einmal mit dem Passwort von {user} an und fügt den öffentlichen Teil des Schlüssels zu ~/.ssh/authorized_keys hinzu. Das Passwort wird nicht gespeichert.",
+ "installKeyDone": "Der Schlüssel ist auf dem Server. Verbinde damit…",
+ "sessionError": "Fehler",
+ "themeTitle": "Darstellung",
+ "themeSystem": "System",
+ "themeLight": "Hell",
+ "themeDark": "Dunkel",
+ "languageTitle": "Sprache",
+ "languageSystem": "Sprache des Telefons",
+ "monitorDownTitle": "{name} ist ausgefallen",
+ "monitorUpTitle": "{name} ist wieder erreichbar",
+ "monitorUpBody": "Die Prüfung klappt wieder.",
+ "monitorUpBodyFor": "Ausgefallen für {duration}.",
+ "monitorHostKeyTitle": "{name}: Der Schlüssel des Servers hat sich geändert",
+ "monitorHostKeyBody": "ServerDeck hat sich nicht angemeldet. Sieh dir den Server an, bevor du den neuen Schlüssel akzeptierst.",
+ "monitorProblemUnreachable": "Keine Verbindung möglich.",
+ "monitorProblemTimeout": "Keine Antwort.",
+ "monitorProblemAuth": "Er antwortet, hat die Anmeldung aber nicht akzeptiert.",
+ "monitorProblemDisconnected": "Er hat vor der Anmeldung aufgelegt.",
+ "monitorProblemHostKeyChanged": "Sein Schlüssel hat sich geändert.",
+ "monitorProblemHostKeyUnknown": "Sein Schlüssel ist noch nicht akzeptiert: Öffne ihn einmal in der App.",
+ "monitorProblemMissing": "Er hat weder Schlüssel noch Passwort.",
+ "monitorProblemOther": "Die Prüfung ist fehlgeschlagen.",
+ "durationSeconds": "{n, plural, =1{1 Sekunde} other{{n} Sekunden}}",
+ "durationMinutes": "{n, plural, =1{1 Minute} other{{n} Minuten}}",
+ "durationHours": "{h} Std. {m} Min.",
+ "durationDays": "{d, plural, =1{1 Tag} other{{d} Tage}} {h} Std.",
+ "monitorSection": "Hintergrundüberwachung",
+ "monitorEnable": "Im Hintergrund überwachen",
+ "monitorEnableHint": "Benachrichtigung, wenn er ausfällt und wenn er wieder da ist.",
+ "monitorEvery": "Wie oft",
+ "monitorHow": "Wie",
+ "monitorModeSsh": "Anmelden",
+ "monitorModePort": "Nur Port",
+ "monitorModeSshHint": "Meldet sich über SSH an und führt einen leeren Befehl aus. So wird auch geprüft, ob der Schlüssel noch funktioniert, und fail2ban zählt es nie als Angriff.",
+ "monitorModePortHint": "Öffnet und schließt nur den Port. Schneller, aber ein strenges fail2ban zählt eine Verbindung ohne Anmeldung vielleicht mit.",
+ "everyMinutes": "{n} Min.",
+ "everyHours": "{n, plural, =1{1 Stunde} other{{n} Stunden}}",
+ "monitorTitle": "Überwachung",
+ "monitorClear": "Protokoll leeren",
+ "monitorClearTitle": "Überwachungsprotokoll leeren?",
+ "monitorClearMessage": "Die Prüfungen und Ausfälle werden gelöscht. Die Überwachung läuft weiter.",
+ "monitorEmptyTitle": "Keine Server überwacht",
+ "monitorEmpty": "Schalte die Hintergrundüberwachung ein, wenn du einen Server bearbeitest. ServerDeck prüft ihn im Hintergrund und sagt dir, wenn er ausfällt.",
+ "monitorServers": "Server",
+ "monitorIncidents": "Ausfälle",
+ "monitorNoIncidents": "Noch keine Ausfälle.",
+ "monitorTimingHint": "Android führt Hintergrundarbeit höchstens alle 15 Minuten aus, und nur mit Netz. Energiesparmodus oder Tiefschlaf können sie verzögern; fehlen Prüfungen, nimm ServerDeck aus der Akkuoptimierung heraus.",
+ "agoNow": "gerade eben",
+ "agoMinutes": "vor {n} Min.",
+ "agoHours": "vor {n} Std.",
+ "monitorWaiting": "Noch keine Prüfung",
+ "monitorSomeDown": "{n, plural, =1{1 Server ist ausgefallen} other{{n} Server sind ausgefallen}}",
+ "monitorAllUp": "Alle Server sind erreichbar",
+ "monitorCount": "{n, plural, =1{1 Server überwacht} other{{n} Server überwacht}}",
+ "monitorLast": "zuletzt {when}",
+ "monitorChecking": "Prüfe…",
+ "monitorCheckNow": "Jetzt prüfen",
+ "monitorChecked": "Geprüft",
+ "monitorStateUnknown": "noch keine Daten",
+ "monitorDownFor": "ausgefallen · {d}",
+ "window24h": "24 Stunden",
+ "window7d": "7 Tage",
+ "window30d": "30 Tage",
+ "shortMinutes": "{n} Min.",
+ "shortHours": "{n} Std.",
+ "shortDays": "{n} T.",
+ "monitorOngoing": "andauernd",
+ "everyNMinutes": "alle {n} Min.",
+ "everyNHours": "{n, plural, =1{stündlich} other{alle {n} Std.}}",
+ "monitorPhoneOffline": "Das Telefon ist nicht mit dem Internet verbunden, deshalb kann gerade nichts geprüft werden.",
+ "monitorForgetTitle": "Diesen Ausfall löschen?",
+ "monitorForgetMessage": "Wenn es kein echter Ausfall war (das Telefon war zum Beispiel offline), werden der Eintrag und seine fehlgeschlagenen Prüfungen gelöscht und die Verfügbarkeit neu berechnet.",
+ "monitorIncidentsHint": "Lange auf einen beendeten Ausfall drücken, um ihn zu löschen."
+}
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 34b4e1d..21f156e 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -204,6 +204,8 @@
"themeSystem": "System",
"themeLight": "Light",
"themeDark": "Dark",
+ "languageTitle": "Language",
+ "languageSystem": "The phone's language",
"monitorDownTitle": "{name} is down",
"monitorUpTitle": "{name} is back up",
"monitorUpBody": "The check works again.",
@@ -269,4 +271,4 @@
"monitorForgetTitle": "Delete this outage?",
"monitorForgetMessage": "If it wasn't really an outage (the phone was offline, say), the entry and its failed checks go, and uptime is worked out again.",
"monitorIncidentsHint": "Long-press a closed outage to delete it."
-}
\ No newline at end of file
+}
diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb
new file mode 100644
index 0000000..9acc3e1
--- /dev/null
+++ b/lib/l10n/app_es.arb
@@ -0,0 +1,274 @@
+{
+ "@@locale": "es",
+ "appTitle": "ServerDeck",
+ "ok": "Aceptar",
+ "cancel": "Cancelar",
+ "save": "Guardar",
+ "delete": "Eliminar",
+ "edit": "Editar",
+ "rename": "Renombrar",
+ "paste": "Pegar",
+ "fieldRequired": "Obligatorio",
+ "serversTitle": "Servidores",
+ "serversEmptyTitle": "Aún no hay servidores",
+ "serversEmpty": "Añade un servidor con su dirección y tu nombre de usuario. ServerDeck se conecta por SSH; no hace falta instalar nada en el servidor.",
+ "serverAdd": "Añadir servidor",
+ "serverEditTitle": "Editar servidor",
+ "serverDeleteTitle": "¿Eliminar {name}?",
+ "serverDeleteMessage": "También se eliminan su contraseña guardada, sus comandos y sus fuentes de registros. Las claves se conservan.",
+ "serverSectionConnection": "Conexión",
+ "serverSectionAuth": "Inicio de sesión",
+ "serverHost": "Dirección",
+ "serverUser": "Usuario",
+ "serverPort": "Puerto",
+ "serverPortInvalid": "1–65535",
+ "serverName": "Nombre",
+ "serverNameHint": "La dirección, si se deja vacío",
+ "statusChecking": "…",
+ "statusDown": "sin conexión",
+ "authKey": "Clave",
+ "authPassword": "Contraseña",
+ "authKeyPick": "Clave SSH",
+ "authKeyMissing": "Elige una clave",
+ "authKeyNone": "Aún no hay claves. Genera una y pon su parte pública en ~/.ssh/authorized_keys en el servidor.",
+ "authPasswordKept": "Guardada. Déjalo vacío para conservarla.",
+ "keysTitle": "Claves SSH",
+ "keysEmptyTitle": "Aún no hay claves",
+ "keysEmpty": "Genera una nueva clave Ed25519 o importa una que ya tengas. La clave privada se queda en el almacenamiento cifrado del teléfono.",
+ "keyAdd": "Añadir clave",
+ "keyGenerateNew": "Generar una clave nueva",
+ "keyGenerateHint": "Ed25519, creada aquí en el teléfono",
+ "keyGenerated": "Clave lista. Copia su parte pública al servidor.",
+ "keyImport": "Importar una clave",
+ "keyImportHint": "Clave privada OpenSSH, RSA o ECDSA",
+ "keyImportNote": "Una clave protegida con frase de contraseña se desbloquea una vez y se guarda en el almacenamiento cifrado del teléfono. La frase de contraseña no se guarda.",
+ "keyImportedName": "Clave importada",
+ "keyName": "Nombre",
+ "keyPrivate": "Clave privada",
+ "keyPassphrase": "Frase de contraseña de la clave",
+ "keyCopy": "Copiar la clave pública",
+ "keyCopied": "Clave pública copiada",
+ "keyDeleteTitle": "¿Eliminar {name}?",
+ "keyDeleteMessage": "La clave privada se elimina del teléfono para siempre.",
+ "keyDeleteUsed": "La clave privada se elimina del teléfono para siempre. Estos servidores la usan y necesitarán otra clave: {servers}",
+ "keyErrorNotAKey": "Esto no es una clave privada. Pega todo el texto que empieza por -----BEGIN.",
+ "keyErrorNeedsPassphrase": "Esta clave está protegida. Introduce su frase de contraseña.",
+ "keyErrorWrongPassphrase": "Frase de contraseña incorrecta.",
+ "keyErrorUnsupported": "ServerDeck no conoce este tipo de clave. Usa una clave Ed25519, RSA o ECDSA.",
+ "sessionOnline": "en línea",
+ "sessionConnecting": "conectando",
+ "sessionReconnect": "Reconectar",
+ "sessionConnectingTo": "Conectando…",
+ "problemUnreachableTitle": "Servidor inaccesible",
+ "problemUnreachable": "No se pudo abrir una conexión con {host} en el puerto {port}. Revisa la dirección, el puerto y la conexión del teléfono.",
+ "problemTimeoutTitle": "Sin respuesta a tiempo",
+ "problemTimeout": "El servidor no respondió en 12 segundos. Puede estar sobrecargado, o un cortafuegos está descartando la conexión.",
+ "problemAuthTitle": "Error de inicio de sesión",
+ "problemAuthKey": "El servidor no aceptó la clave de {user}. Pon la parte pública de la clave en ~/.ssh/authorized_keys en el servidor.",
+ "problemAuthPassword": "El servidor no aceptó la contraseña de {user}.",
+ "problemHostKeyChangedTitle": "La clave del servidor ha cambiado",
+ "problemHostKeyChanged": "El servidor mostró una clave de identidad distinta de la que aceptaste antes. Puede ser una reinstalación o alguien en medio. ServerDeck no se ha conectado.",
+ "problemHostKeyRefusedTitle": "Clave rechazada",
+ "problemHostKeyRefused": "No aceptaste la clave del servidor, así que ServerDeck no inició sesión.",
+ "problemMissingTitle": "Sin datos de inicio de sesión",
+ "problemMissing": "Este servidor no tiene clave ni contraseña. Edita el servidor y añade una.",
+ "problemDisconnectedTitle": "Conexión perdida",
+ "problemDisconnected": "La conexión se cortó: un cambio de red, un reinicio del servidor o el teléfono entró en reposo.",
+ "problemOtherTitle": "No se pudo conectar",
+ "hostKeyNewTitle": "Servidor nuevo",
+ "hostKeyNew": "Es la primera vez que te conectas a {host}. Esta es la clave de identidad del servidor:",
+ "hostKeyCheck": "Para estar seguro, este comando en el servidor muestra la misma huella:",
+ "hostKeyAccept": "Aceptar",
+ "hostKeyReject": "Rechazar",
+ "hostKeyKnown": "Aceptada",
+ "hostKeyNow": "Ahora",
+ "hostKeyForget": "Olvidar la clave antigua",
+ "hostKeyForgetTitle": "¿Olvidar la clave antigua?",
+ "hostKeyForgetMessage": "Hazlo solo si sabes por qué cambió la clave, por ejemplo porque reinstalaste el servidor. ServerDeck preguntará entonces por la clave nueva.",
+ "statsFailedTitle": "No se pudo leer el servidor",
+ "statsFailed": "La respuesta del servidor no tenía sentido. Las estadísticas necesitan Linux (/proc).",
+ "statsStale": "La última actualización falló. Las cifras de arriba son anteriores.",
+ "statCpu": "CPU",
+ "statMemory": "Memoria",
+ "statDisk": "Disco",
+ "statCores": "{count, plural, =1{1 núcleo} other{{count} núcleos}}",
+ "statHistory": "Últimos 3 minutos",
+ "statHistoryHint": "Se actualiza cada 3 segundos mientras esta pantalla está abierta.",
+ "statNetIn": "Entrada",
+ "statNetOut": "Salida",
+ "statLoad": "Carga",
+ "statLoad1": "1 min",
+ "statLoad5": "5 min",
+ "statLoad15": "15 min",
+ "statDisks": "Discos",
+ "statSwap": "Swap",
+ "statUptime": "Activo",
+ "uptimeDays": "{days, plural, =1{1 día} other{{days} días}}, {hours} h",
+ "uptimeHours": "{hours} h {minutes} min",
+ "tabOverview": "Resumen",
+ "tabServices": "Servicios",
+ "servicesFailedTitle": "No se pudieron listar los servicios",
+ "servicesSearch": "Buscar por nombre o descripción",
+ "servicesRunning": "En ejecución · {count}",
+ "servicesFailed": "Con error · {count}",
+ "servicesAll": "Todos · {count}",
+ "servicesNone": "No hay coincidencias.",
+ "servicesNoneFailed": "No hay servicios con error.",
+ "containersNone": "No hay contenedores.",
+ "stateRunning": "en ejecución",
+ "stateExited": "finalizado",
+ "stateFailed": "con error",
+ "stateActivating": "iniciando",
+ "stateInactive": "detenido",
+ "stateUnhealthy": "no saludable",
+ "statePaused": "en pausa",
+ "stateRestarting": "reiniciando",
+ "stateExitedContainer": "finalizado",
+ "stateCreated": "creado",
+ "actionRestart": "Reiniciar",
+ "actionReload": "Recargar",
+ "actionStop": "Detener",
+ "actionStart": "Iniciar",
+ "actionConfirmTitle": "¿{action} {subject}?",
+ "actionConfirmMessage": "Esto se ejecuta en el servidor:\n{command}",
+ "actionDone": "{action}: hecho en {subject}",
+ "actionFailed": "No funcionó (código de salida {code}). {reason}",
+ "actionNeedsSudo": "Esto necesita permisos de administrador. Inicia sesión como root o permite sudo sin contraseña para este comando.",
+ "tabLogs": "Registros",
+ "logsJournal": "Diario del sistema",
+ "logsAdd": "Fuente",
+ "logsAddTitle": "Nueva fuente de registros",
+ "logsKindUnit": "Servicio",
+ "logsKindFile": "Archivo",
+ "logsUnitLabel": "Nombre del servicio",
+ "logsFileLabel": "Ruta del archivo",
+ "logsFilter": "Filtro",
+ "logsErrorsOnly": "Solo errores",
+ "logsPause": "Pausar",
+ "logsResume": "Reanudar",
+ "logsClear": "Limpiar la pantalla",
+ "logsNewLines": "{count, plural, =1{1 línea nueva} other{{count} líneas nuevas}}",
+ "logsEnded": "El seguimiento del registro se detuvo (código de salida {code}).",
+ "logsDeleteTitle": "¿Eliminar {name}?",
+ "logsDeleteMessage": "Solo se elimina la fuente guardada, no el registro del servidor.",
+ "tabCommands": "Comandos",
+ "commandAdd": "Nuevo comando",
+ "commandEdit": "Editar comando",
+ "commandQuick": "Comando puntual…",
+ "commandRun": "Ejecutar",
+ "commandRunTitle": "¿Ejecutar {name}?",
+ "commandSaved": "Comandos guardados",
+ "commandTemplates": "Para empezar",
+ "commandTemplatesHint": "Toca uno para añadirlo a tus comandos guardados. Solo leen.",
+ "commandLabel": "Comando",
+ "commandEverywhere": "En todos los servidores",
+ "commandEverywhereHint": "Aparece en todos tus servidores",
+ "commandConfirm": "Preguntar antes de ejecutar",
+ "commandCopy": "Copiar la salida",
+ "commandCopied": "Salida copiada",
+ "commandError": "error",
+ "commandStopped": "detenido",
+ "commandExit": "salida {code}",
+ "commandRunning": "en ejecución",
+ "commandNoOutput": "Sin salida.",
+ "tplDisk": "Uso del disco",
+ "tplFolders": "Carpetas más grandes",
+ "tplUpdates": "Actualizaciones pendientes",
+ "tplWho": "Quién ha iniciado sesión",
+ "tplApache": "Prueba de configuración de Apache",
+ "tplNginx": "Prueba de configuración de Nginx",
+ "tplTop": "Mayor uso de CPU",
+ "tplPorts": "Puertos a la escucha",
+ "tabTerminal": "Terminal",
+ "terminalClosed": "[shell cerrada]",
+ "terminalReopen": "Nueva shell",
+ "lockReason": "Desbloquear ServerDeck",
+ "lockTitle": "ServerDeck está bloqueado",
+ "lockUnlock": "Desbloquear",
+ "settingsTitle": "Ajustes",
+ "settingsSecurity": "Seguridad",
+ "settingsLock": "Bloqueo de la app",
+ "settingsLockHint": "Huella, rostro o el PIN del teléfono al iniciar y tras un minuto en segundo plano.",
+ "settingsLockUnsupported": "Primero configura un bloqueo de pantalla en el teléfono.",
+ "settingsKnownHosts": "Claves de servidor conocidas",
+ "settingsKnownHostsEmpty": "Aún no se ha aceptado ninguna clave de servidor.",
+ "settingsKnownHostsHint": "Claves de identidad aceptadas en el primer contacto. Si la clave de un servidor cambia, ServerDeck no se conectará hasta que olvides aquí la antigua.",
+ "settingsAbout": "Acerca de",
+ "settingsAboutText": "Gestor de servidores SSH. Código abierto, licencia MIT. No recopila nada; todo se queda en el teléfono.",
+ "installKey": "Instalar la clave con una contraseña",
+ "installKeyAction": "Instalar",
+ "installKeyHint": "ServerDeck inicia sesión una vez con la contraseña de {user} y añade la parte pública de la clave a ~/.ssh/authorized_keys. La contraseña no se guarda.",
+ "installKeyDone": "La clave está en el servidor. Conectando con ella…",
+ "sessionError": "error",
+ "themeTitle": "Apariencia",
+ "themeSystem": "Sistema",
+ "themeLight": "Claro",
+ "themeDark": "Oscuro",
+ "languageTitle": "Idioma",
+ "languageSystem": "Idioma del teléfono",
+ "monitorDownTitle": "{name} está caído",
+ "monitorUpTitle": "{name} vuelve a funcionar",
+ "monitorUpBody": "La comprobación vuelve a funcionar.",
+ "monitorUpBodyFor": "Estuvo caído durante {duration}.",
+ "monitorHostKeyTitle": "{name}: la clave del servidor ha cambiado",
+ "monitorHostKeyBody": "ServerDeck no inició sesión. Revisa el servidor antes de aceptar la clave nueva.",
+ "monitorProblemUnreachable": "No se pudo abrir una conexión.",
+ "monitorProblemTimeout": "Sin respuesta a tiempo.",
+ "monitorProblemAuth": "Responde, pero no aceptó el inicio de sesión.",
+ "monitorProblemDisconnected": "Cortó antes del inicio de sesión.",
+ "monitorProblemHostKeyChanged": "Su clave ha cambiado.",
+ "monitorProblemHostKeyUnknown": "Su clave aún no está aceptada: ábrelo una vez en la app.",
+ "monitorProblemMissing": "No tiene clave ni contraseña.",
+ "monitorProblemOther": "La comprobación falló.",
+ "durationSeconds": "{n, plural, =1{1 segundo} other{{n} segundos}}",
+ "durationMinutes": "{n, plural, =1{1 minuto} other{{n} minutos}}",
+ "durationHours": "{h} h {m} min",
+ "durationDays": "{d, plural, =1{1 día} other{{d} días}} {h} h",
+ "monitorSection": "Supervisión en segundo plano",
+ "monitorEnable": "Supervisar en segundo plano",
+ "monitorEnableHint": "Recibe un aviso cuando se cae y cuando vuelve.",
+ "monitorEvery": "Con qué frecuencia",
+ "monitorHow": "Cómo",
+ "monitorModeSsh": "Iniciar sesión",
+ "monitorModePort": "Solo puerto",
+ "monitorModeSshHint": "Inicia sesión por SSH y ejecuta un comando vacío. Así también comprueba que la clave sigue funcionando, y fail2ban nunca lo cuenta como un ataque.",
+ "monitorModePortHint": "Solo abre y cierra el puerto. Más rápido, pero un fail2ban estricto puede contar una conexión sin inicio de sesión.",
+ "everyMinutes": "{n} min",
+ "everyHours": "{n, plural, =1{1 hora} other{{n} horas}}",
+ "monitorTitle": "Supervisión",
+ "monitorClear": "Borrar el registro",
+ "monitorClearTitle": "¿Borrar el registro de supervisión?",
+ "monitorClearMessage": "Se eliminan las comprobaciones y las caídas. La supervisión continúa.",
+ "monitorEmptyTitle": "Ningún servidor supervisado",
+ "monitorEmpty": "Activa la Supervisión en segundo plano al editar un servidor. ServerDeck lo comprueba en segundo plano y te avisa cuando se cae.",
+ "monitorServers": "Servidores",
+ "monitorIncidents": "Caídas",
+ "monitorNoIncidents": "Aún no hay caídas.",
+ "monitorTimingHint": "Android ejecuta tareas en segundo plano como mucho cada 15 minutos, y solo con red. El ahorro de batería o el reposo profundo pueden retrasarlas; si faltan comprobaciones, quita ServerDeck de la optimización de batería.",
+ "agoNow": "ahora mismo",
+ "agoMinutes": "hace {n} min",
+ "agoHours": "hace {n} h",
+ "monitorWaiting": "Aún sin comprobar",
+ "monitorSomeDown": "{n, plural, =1{1 servidor está caído} other{{n} servidores están caídos}}",
+ "monitorAllUp": "Todos los servidores funcionan",
+ "monitorCount": "{n, plural, =1{1 servidor supervisado} other{{n} servidores supervisados}}",
+ "monitorLast": "última: {when}",
+ "monitorChecking": "Comprobando…",
+ "monitorCheckNow": "Comprobar ahora",
+ "monitorChecked": "Comprobado",
+ "monitorStateUnknown": "aún sin datos",
+ "monitorDownFor": "caído · {d}",
+ "window24h": "24 horas",
+ "window7d": "7 días",
+ "window30d": "30 días",
+ "shortMinutes": "{n} min",
+ "shortHours": "{n} h",
+ "shortDays": "{n} d",
+ "monitorOngoing": "en curso",
+ "everyNMinutes": "cada {n} min",
+ "everyNHours": "{n, plural, =1{cada hora} other{cada {n} h}}",
+ "monitorPhoneOffline": "El teléfono no tiene conexión a internet, así que ahora no se puede comprobar nada.",
+ "monitorForgetTitle": "¿Eliminar esta caída?",
+ "monitorForgetMessage": "Si no fue una caída real (por ejemplo, el teléfono estaba sin conexión), se eliminan la entrada y sus comprobaciones fallidas, y se vuelve a calcular la disponibilidad.",
+ "monitorIncidentsHint": "Mantén pulsada una caída cerrada para eliminarla."
+}
diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb
new file mode 100644
index 0000000..ca5866c
--- /dev/null
+++ b/lib/l10n/app_fr.arb
@@ -0,0 +1,274 @@
+{
+ "@@locale": "fr",
+ "appTitle": "ServerDeck",
+ "ok": "OK",
+ "cancel": "Annuler",
+ "save": "Enregistrer",
+ "delete": "Supprimer",
+ "edit": "Modifier",
+ "rename": "Renommer",
+ "paste": "Coller",
+ "fieldRequired": "Obligatoire",
+ "serversTitle": "Serveurs",
+ "serversEmptyTitle": "Aucun serveur",
+ "serversEmpty": "Ajoutez un serveur avec son adresse et votre nom d’utilisateur. ServerDeck se connecte en SSH ; rien n’est à installer sur le serveur.",
+ "serverAdd": "Ajouter un serveur",
+ "serverEditTitle": "Modifier le serveur",
+ "serverDeleteTitle": "Supprimer {name} ?",
+ "serverDeleteMessage": "Son mot de passe enregistré, ses commandes et ses sources de journaux sont supprimés aussi. Les clés sont conservées.",
+ "serverSectionConnection": "Connexion",
+ "serverSectionAuth": "Identification",
+ "serverHost": "Adresse",
+ "serverUser": "Utilisateur",
+ "serverPort": "Port",
+ "serverPortInvalid": "1–65535",
+ "serverName": "Nom",
+ "serverNameHint": "L’adresse, si laissé vide",
+ "statusChecking": "…",
+ "statusDown": "hors ligne",
+ "authKey": "Clé",
+ "authPassword": "Mot de passe",
+ "authKeyPick": "Clé SSH",
+ "authKeyMissing": "Choisissez une clé",
+ "authKeyNone": "Aucune clé. Générez-en une et placez sa partie publique dans ~/.ssh/authorized_keys sur le serveur.",
+ "authPasswordKept": "Enregistré. Laissez vide pour le conserver.",
+ "keysTitle": "Clés SSH",
+ "keysEmptyTitle": "Aucune clé",
+ "keysEmpty": "Générez une nouvelle clé Ed25519 ou importez une clé existante. La clé privée reste dans le stockage chiffré du téléphone.",
+ "keyAdd": "Ajouter une clé",
+ "keyGenerateNew": "Générer une nouvelle clé",
+ "keyGenerateHint": "Ed25519, créée ici sur le téléphone",
+ "keyGenerated": "Clé prête. Copiez sa partie publique sur le serveur.",
+ "keyImport": "Importer une clé",
+ "keyImportHint": "Clé privée OpenSSH, RSA ou ECDSA",
+ "keyImportNote": "Une clé protégée par une phrase secrète est déverrouillée une fois et enregistrée dans le stockage chiffré du téléphone. La phrase secrète n’est pas conservée.",
+ "keyImportedName": "Clé importée",
+ "keyName": "Nom",
+ "keyPrivate": "Clé privée",
+ "keyPassphrase": "Phrase secrète de la clé",
+ "keyCopy": "Copier la clé publique",
+ "keyCopied": "Clé publique copiée",
+ "keyDeleteTitle": "Supprimer {name} ?",
+ "keyDeleteMessage": "La clé privée est supprimée définitivement du téléphone.",
+ "keyDeleteUsed": "La clé privée est supprimée définitivement du téléphone. Ces serveurs l’utilisent et auront besoin d’une autre clé : {servers}",
+ "keyErrorNotAKey": "Ce n’est pas une clé privée. Collez tout le texte qui commence par -----BEGIN.",
+ "keyErrorNeedsPassphrase": "Cette clé est protégée. Saisissez sa phrase secrète.",
+ "keyErrorWrongPassphrase": "Phrase secrète incorrecte.",
+ "keyErrorUnsupported": "ServerDeck ne connaît pas ce type de clé. Utilisez une clé Ed25519, RSA ou ECDSA.",
+ "sessionOnline": "en ligne",
+ "sessionConnecting": "connexion",
+ "sessionReconnect": "Se reconnecter",
+ "sessionConnectingTo": "Connexion…",
+ "problemUnreachableTitle": "Serveur injoignable",
+ "problemUnreachable": "Impossible d’ouvrir une connexion vers {host} sur le port {port}. Vérifiez l’adresse, le port et la connexion du téléphone.",
+ "problemTimeoutTitle": "Pas de réponse à temps",
+ "problemTimeout": "Le serveur n’a pas répondu en 12 secondes. Il est peut-être surchargé, ou un pare-feu bloque la connexion.",
+ "problemAuthTitle": "Échec de l’identification",
+ "problemAuthKey": "Le serveur n’a pas accepté la clé de {user}. Placez la partie publique de la clé dans ~/.ssh/authorized_keys sur le serveur.",
+ "problemAuthPassword": "Le serveur n’a pas accepté le mot de passe de {user}.",
+ "problemHostKeyChangedTitle": "La clé du serveur a changé",
+ "problemHostKeyChanged": "Le serveur a présenté une clé d’identité différente de celle que vous aviez acceptée. Il peut s’agir d’une réinstallation, ou de quelqu’un entre les deux. ServerDeck ne s’est pas connecté.",
+ "problemHostKeyRefusedTitle": "Clé refusée",
+ "problemHostKeyRefused": "Vous n’avez pas accepté la clé du serveur, ServerDeck ne s’est donc pas identifié.",
+ "problemMissingTitle": "Aucun identifiant",
+ "problemMissing": "Ce serveur n’a ni clé ni mot de passe. Modifiez le serveur et ajoutez-en un.",
+ "problemDisconnectedTitle": "Connexion perdue",
+ "problemDisconnected": "La connexion a été coupée : un changement de réseau, un redémarrage du serveur, ou le téléphone s’est mis en veille.",
+ "problemOtherTitle": "Connexion impossible",
+ "hostKeyNewTitle": "Nouveau serveur",
+ "hostKeyNew": "C’est la première fois que vous vous connectez à {host}. Voici la clé d’identité du serveur :",
+ "hostKeyCheck": "Pour en être sûr, cette commande sur le serveur affiche la même empreinte :",
+ "hostKeyAccept": "Accepter",
+ "hostKeyReject": "Refuser",
+ "hostKeyKnown": "Acceptée",
+ "hostKeyNow": "Maintenant",
+ "hostKeyForget": "Oublier l’ancienne clé",
+ "hostKeyForgetTitle": "Oublier l’ancienne clé ?",
+ "hostKeyForgetMessage": "Ne le faites que si vous savez pourquoi la clé a changé, par exemple parce que vous avez réinstallé le serveur. ServerDeck vous demandera alors d’accepter la nouvelle clé.",
+ "statsFailedTitle": "Impossible de lire le serveur",
+ "statsFailed": "La réponse du serveur n’avait pas de sens. Les statistiques nécessitent Linux (/proc).",
+ "statsStale": "La dernière actualisation a échoué. Les valeurs ci-dessus sont plus anciennes.",
+ "statCpu": "CPU",
+ "statMemory": "Mémoire",
+ "statDisk": "Disque",
+ "statCores": "{count, plural, =1{1 cœur} other{{count} cœurs}}",
+ "statHistory": "3 dernières minutes",
+ "statHistoryHint": "Actualisé toutes les 3 secondes tant que cet écran est ouvert.",
+ "statNetIn": "Entrant",
+ "statNetOut": "Sortant",
+ "statLoad": "Charge",
+ "statLoad1": "1 min",
+ "statLoad5": "5 min",
+ "statLoad15": "15 min",
+ "statDisks": "Disques",
+ "statSwap": "Swap",
+ "statUptime": "En marche",
+ "uptimeDays": "{days, plural, =1{1 jour} other{{days} jours}}, {hours} h",
+ "uptimeHours": "{hours} h {minutes} min",
+ "tabOverview": "Aperçu",
+ "tabServices": "Services",
+ "servicesFailedTitle": "Impossible de lister les services",
+ "servicesSearch": "Rechercher par nom ou description",
+ "servicesRunning": "Actifs · {count}",
+ "servicesFailed": "En échec · {count}",
+ "servicesAll": "Tous · {count}",
+ "servicesNone": "Aucun résultat.",
+ "servicesNoneFailed": "Aucun service en échec.",
+ "containersNone": "Aucun conteneur.",
+ "stateRunning": "actif",
+ "stateExited": "terminé",
+ "stateFailed": "en échec",
+ "stateActivating": "démarrage",
+ "stateInactive": "arrêté",
+ "stateUnhealthy": "défaillant",
+ "statePaused": "en pause",
+ "stateRestarting": "redémarrage",
+ "stateExitedContainer": "terminé",
+ "stateCreated": "créé",
+ "actionRestart": "Redémarrer",
+ "actionReload": "Recharger",
+ "actionStop": "Arrêter",
+ "actionStart": "Démarrer",
+ "actionConfirmTitle": "{action} {subject} ?",
+ "actionConfirmMessage": "Ceci s’exécute sur le serveur :\n{command}",
+ "actionDone": "{action} : terminé pour {subject}",
+ "actionFailed": "Cela n’a pas fonctionné (code de sortie {code}). {reason}",
+ "actionNeedsSudo": "Il faut des droits d’administrateur. Connectez-vous en root, ou autorisez sudo sans mot de passe pour cette commande.",
+ "tabLogs": "Journaux",
+ "logsJournal": "Journal système",
+ "logsAdd": "Source",
+ "logsAddTitle": "Nouvelle source de journal",
+ "logsKindUnit": "Service",
+ "logsKindFile": "Fichier",
+ "logsUnitLabel": "Nom du service",
+ "logsFileLabel": "Chemin du fichier",
+ "logsFilter": "Filtrer",
+ "logsErrorsOnly": "Erreurs uniquement",
+ "logsPause": "Pause",
+ "logsResume": "Reprendre",
+ "logsClear": "Effacer l’écran",
+ "logsNewLines": "{count, plural, =1{1 nouvelle ligne} other{{count} nouvelles lignes}}",
+ "logsEnded": "Le suivi du journal s’est arrêté (code de sortie {code}).",
+ "logsDeleteTitle": "Supprimer {name} ?",
+ "logsDeleteMessage": "Seule la source enregistrée est supprimée, pas le journal sur le serveur.",
+ "tabCommands": "Commandes",
+ "commandAdd": "Nouvelle commande",
+ "commandEdit": "Modifier la commande",
+ "commandQuick": "Commande ponctuelle…",
+ "commandRun": "Exécuter",
+ "commandRunTitle": "Exécuter {name} ?",
+ "commandSaved": "Commandes enregistrées",
+ "commandTemplates": "Pour commencer",
+ "commandTemplatesHint": "Touchez-en une pour l’ajouter à vos commandes enregistrées. Elles ne font que lire.",
+ "commandLabel": "Commande",
+ "commandEverywhere": "Sur tous les serveurs",
+ "commandEverywhereHint": "Apparaît pour tous vos serveurs",
+ "commandConfirm": "Demander avant d’exécuter",
+ "commandCopy": "Copier la sortie",
+ "commandCopied": "Sortie copiée",
+ "commandError": "erreur",
+ "commandStopped": "arrêtée",
+ "commandExit": "sortie {code}",
+ "commandRunning": "en cours",
+ "commandNoOutput": "Aucune sortie.",
+ "tplDisk": "Espace disque",
+ "tplFolders": "Plus gros dossiers",
+ "tplUpdates": "Mises à jour en attente",
+ "tplWho": "Qui est connecté",
+ "tplApache": "Test de la config Apache",
+ "tplNginx": "Test de la config Nginx",
+ "tplTop": "Top CPU",
+ "tplPorts": "Ports en écoute",
+ "tabTerminal": "Terminal",
+ "terminalClosed": "[shell fermé]",
+ "terminalReopen": "Nouveau shell",
+ "lockReason": "Déverrouiller ServerDeck",
+ "lockTitle": "ServerDeck est verrouillé",
+ "lockUnlock": "Déverrouiller",
+ "settingsTitle": "Paramètres",
+ "settingsSecurity": "Sécurité",
+ "settingsLock": "Verrouillage de l’app",
+ "settingsLockHint": "Empreinte, visage ou code PIN du téléphone au démarrage et après une minute en arrière-plan.",
+ "settingsLockUnsupported": "Configurez d’abord un verrouillage d’écran sur le téléphone.",
+ "settingsKnownHosts": "Clés de serveur connues",
+ "settingsKnownHostsEmpty": "Aucune clé de serveur acceptée.",
+ "settingsKnownHostsHint": "Clés d’identité acceptées au premier contact. Quand la clé d’un serveur change, ServerDeck ne se connecte plus tant que vous n’oubliez pas l’ancienne ici.",
+ "settingsAbout": "À propos",
+ "settingsAboutText": "Gestionnaire de serveurs SSH. Open source, licence MIT. Ne collecte rien ; tout reste sur le téléphone.",
+ "installKey": "Installer la clé avec un mot de passe",
+ "installKeyAction": "Installer",
+ "installKeyHint": "ServerDeck se connecte une fois avec le mot de passe de {user} et ajoute la partie publique de la clé à ~/.ssh/authorized_keys. Le mot de passe n’est pas conservé.",
+ "installKeyDone": "La clé est sur le serveur. Connexion avec la clé…",
+ "sessionError": "erreur",
+ "themeTitle": "Apparence",
+ "themeSystem": "Système",
+ "themeLight": "Clair",
+ "themeDark": "Sombre",
+ "languageTitle": "Langue",
+ "languageSystem": "Langue du téléphone",
+ "monitorDownTitle": "{name} est hors service",
+ "monitorUpTitle": "{name} est de nouveau en ligne",
+ "monitorUpBody": "La vérification fonctionne de nouveau.",
+ "monitorUpBodyFor": "Il a été hors service pendant {duration}.",
+ "monitorHostKeyTitle": "{name} : la clé du serveur a changé",
+ "monitorHostKeyBody": "ServerDeck ne s’est pas connecté. Examinez le serveur avant d’accepter la nouvelle clé.",
+ "monitorProblemUnreachable": "Impossible d’ouvrir une connexion.",
+ "monitorProblemTimeout": "Pas de réponse à temps.",
+ "monitorProblemAuth": "Il répond, mais n’a pas accepté l’identification.",
+ "monitorProblemDisconnected": "Il a coupé avant l’identification.",
+ "monitorProblemHostKeyChanged": "Sa clé a changé.",
+ "monitorProblemHostKeyUnknown": "Sa clé n’est pas encore acceptée : ouvrez-le une fois dans l’app.",
+ "monitorProblemMissing": "Il n’a ni clé ni mot de passe.",
+ "monitorProblemOther": "La vérification a échoué.",
+ "durationSeconds": "{n, plural, =1{1 seconde} other{{n} secondes}}",
+ "durationMinutes": "{n, plural, =1{1 minute} other{{n} minutes}}",
+ "durationHours": "{h} h {m} min",
+ "durationDays": "{d, plural, =1{1 jour} other{{d} jours}} {h} h",
+ "monitorSection": "Surveillance en arrière-plan",
+ "monitorEnable": "Surveiller en arrière-plan",
+ "monitorEnableHint": "Soyez prévenu quand il tombe et quand il revient.",
+ "monitorEvery": "Fréquence",
+ "monitorHow": "Méthode",
+ "monitorModeSsh": "Connexion",
+ "monitorModePort": "Port seul",
+ "monitorModeSshHint": "Se connecte en SSH et exécute une commande vide. Cela vérifie aussi que la clé fonctionne toujours, et fail2ban ne le compte jamais comme une attaque.",
+ "monitorModePortHint": "Ouvre et ferme seulement le port. Plus rapide, mais un fail2ban strict peut compter une connexion sans identification.",
+ "everyMinutes": "{n} min",
+ "everyHours": "{n, plural, =1{1 heure} other{{n} heures}}",
+ "monitorTitle": "Surveillance",
+ "monitorClear": "Effacer le journal",
+ "monitorClearTitle": "Effacer le journal de surveillance ?",
+ "monitorClearMessage": "Les vérifications et les pannes sont supprimées. La surveillance continue.",
+ "monitorEmptyTitle": "Aucun serveur surveillé",
+ "monitorEmpty": "Activez la surveillance en arrière-plan en modifiant un serveur. ServerDeck le vérifie en arrière-plan et vous prévient quand il tombe.",
+ "monitorServers": "Serveurs",
+ "monitorIncidents": "Pannes",
+ "monitorNoIncidents": "Aucune panne.",
+ "monitorTimingHint": "Android exécute les tâches en arrière-plan au plus toutes les 15 minutes, et seulement avec un réseau. L’économiseur de batterie ou la veille profonde peuvent les retarder ; si des vérifications manquent, retirez ServerDeck de l’optimisation de la batterie.",
+ "agoNow": "à l’instant",
+ "agoMinutes": "il y a {n} min",
+ "agoHours": "il y a {n} h",
+ "monitorWaiting": "Pas encore vérifié",
+ "monitorSomeDown": "{n, plural, =1{1 serveur est hors service} other{{n} serveurs sont hors service}}",
+ "monitorAllUp": "Tous les serveurs sont en ligne",
+ "monitorCount": "{n, plural, =1{1 serveur surveillé} other{{n} serveurs surveillés}}",
+ "monitorLast": "dernière : {when}",
+ "monitorChecking": "Vérification…",
+ "monitorCheckNow": "Vérifier maintenant",
+ "monitorChecked": "Vérifié",
+ "monitorStateUnknown": "pas encore de données",
+ "monitorDownFor": "hors service · {d}",
+ "window24h": "24 heures",
+ "window7d": "7 jours",
+ "window30d": "30 jours",
+ "shortMinutes": "{n} min",
+ "shortHours": "{n} h",
+ "shortDays": "{n} j",
+ "monitorOngoing": "en cours",
+ "everyNMinutes": "toutes les {n} min",
+ "everyNHours": "{n, plural, =1{toutes les heures} other{toutes les {n} h}}",
+ "monitorPhoneOffline": "Le téléphone n’est pas connecté à internet, rien ne peut donc être vérifié maintenant.",
+ "monitorForgetTitle": "Supprimer cette panne ?",
+ "monitorForgetMessage": "Si ce n’était pas une vraie panne (le téléphone était hors ligne, par exemple), l’entrée et ses vérifications échouées sont supprimées, et la disponibilité est recalculée.",
+ "monitorIncidentsHint": "Appuyez longuement sur une panne terminée pour la supprimer."
+}
diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb
index 5a68e9c..8bfd0e1 100644
--- a/lib/l10n/app_hu.arb
+++ b/lib/l10n/app_hu.arb
@@ -383,6 +383,8 @@
"themeSystem": "Rendszer",
"themeLight": "Világos",
"themeDark": "Sötét",
+ "languageTitle": "Nyelv",
+ "languageSystem": "A telefon nyelve",
"monitorDownTitle": "{name} nem elérhető",
"monitorUpTitle": "{name} újra elérhető",
"monitorUpBody": "Az ellenőrzés újra sikerült.",
@@ -601,4 +603,4 @@
"monitorForgetTitle": "Törlöd ezt a leállást?",
"monitorForgetMessage": "Ha valójában nem volt leállás (például a telefon volt offline), a bejegyzés és a hozzá tartozó sikertelen ellenőrzések törlődnek, a rendelkezésre állás pedig újraszámolódik.",
"monitorIncidentsHint": "Egy lezárt leállás hosszan nyomva törölhető."
-}
\ No newline at end of file
+}
diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart
index c3ce5be..3afedcd 100644
--- a/lib/l10n/generated/app_localizations.dart
+++ b/lib/l10n/generated/app_localizations.dart
@@ -5,7 +5,10 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
+import 'app_localizations_de.dart';
import 'app_localizations_en.dart';
+import 'app_localizations_es.dart';
+import 'app_localizations_fr.dart';
import 'app_localizations_hu.dart';
// ignore_for_file: type=lint
@@ -94,7 +97,10 @@ abstract class AppLocalizations {
/// A list of this localizations delegate's supported locales.
static const List supportedLocales = [
+ Locale('de'),
Locale('en'),
+ Locale('es'),
+ Locale('fr'),
Locale('hu'),
];
@@ -1322,6 +1328,18 @@ abstract class AppLocalizations {
/// **'Sötét'**
String get themeDark;
+ /// No description provided for @languageTitle.
+ ///
+ /// In hu, this message translates to:
+ /// **'Nyelv'**
+ String get languageTitle;
+
+ /// No description provided for @languageSystem.
+ ///
+ /// In hu, this message translates to:
+ /// **'A telefon nyelve'**
+ String get languageSystem;
+
/// No description provided for @monitorDownTitle.
///
/// In hu, this message translates to:
@@ -1724,7 +1742,7 @@ class _AppLocalizationsDelegate
@override
bool isSupported(Locale locale) =>
- ['en', 'hu'].contains(locale.languageCode);
+ ['de', 'en', 'es', 'fr', 'hu'].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
@@ -1733,8 +1751,14 @@ class _AppLocalizationsDelegate
AppLocalizations lookupAppLocalizations(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
+ case 'de':
+ return AppLocalizationsDe();
case 'en':
return AppLocalizationsEn();
+ case 'es':
+ return AppLocalizationsEs();
+ case 'fr':
+ return AppLocalizationsFr();
case 'hu':
return AppLocalizationsHu();
}
diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart
new file mode 100644
index 0000000..930486b
--- /dev/null
+++ b/lib/l10n/generated/app_localizations_de.dart
@@ -0,0 +1,1016 @@
+// ignore: unused_import
+import 'package:intl/intl.dart' as intl;
+
+import 'app_localizations.dart';
+
+// ignore_for_file: type=lint
+
+/// The translations for German (`de`).
+class AppLocalizationsDe extends AppLocalizations {
+ AppLocalizationsDe([String locale = 'de']) : super(locale);
+
+ @override
+ String get appTitle => 'ServerDeck';
+
+ @override
+ String get ok => 'OK';
+
+ @override
+ String get cancel => 'Abbrechen';
+
+ @override
+ String get save => 'Speichern';
+
+ @override
+ String get delete => 'Löschen';
+
+ @override
+ String get edit => 'Bearbeiten';
+
+ @override
+ String get rename => 'Umbenennen';
+
+ @override
+ String get paste => 'Einfügen';
+
+ @override
+ String get fieldRequired => 'Pflichtfeld';
+
+ @override
+ String get serversTitle => 'Server';
+
+ @override
+ String get serversEmptyTitle => 'Noch keine Server';
+
+ @override
+ String get serversEmpty =>
+ 'Füge einen Server mit seiner Adresse und deinem Benutzernamen hinzu. ServerDeck verbindet sich über SSH; auf dem Server muss nichts installiert werden.';
+
+ @override
+ String get serverAdd => 'Server hinzufügen';
+
+ @override
+ String get serverEditTitle => 'Server bearbeiten';
+
+ @override
+ String serverDeleteTitle(String name) {
+ return '$name löschen?';
+ }
+
+ @override
+ String get serverDeleteMessage =>
+ 'Das gespeicherte Passwort, die Befehle und die Logquellen werden mitgelöscht. Die Schlüssel bleiben.';
+
+ @override
+ String get serverSectionConnection => 'Verbindung';
+
+ @override
+ String get serverSectionAuth => 'Anmeldung';
+
+ @override
+ String get serverHost => 'Adresse';
+
+ @override
+ String get serverUser => 'Benutzer';
+
+ @override
+ String get serverPort => 'Port';
+
+ @override
+ String get serverPortInvalid => '1–65535';
+
+ @override
+ String get serverName => 'Name';
+
+ @override
+ String get serverNameHint => 'Leer lassen für die Adresse';
+
+ @override
+ String get statusChecking => '…';
+
+ @override
+ String get statusDown => 'offline';
+
+ @override
+ String get authKey => 'Schlüssel';
+
+ @override
+ String get authPassword => 'Passwort';
+
+ @override
+ String get authKeyPick => 'SSH-Schlüssel';
+
+ @override
+ String get authKeyMissing => 'Wähle einen Schlüssel';
+
+ @override
+ String get authKeyNone =>
+ 'Noch keine Schlüssel. Erzeuge einen und trage seinen öffentlichen Teil in ~/.ssh/authorized_keys auf dem Server ein.';
+
+ @override
+ String get authPasswordKept => 'Gespeichert. Leer lassen, um es zu behalten.';
+
+ @override
+ String get keysTitle => 'SSH-Schlüssel';
+
+ @override
+ String get keysEmptyTitle => 'Noch keine Schlüssel';
+
+ @override
+ String get keysEmpty =>
+ 'Erzeuge einen neuen Ed25519-Schlüssel oder importiere einen vorhandenen. Der private Schlüssel bleibt im verschlüsselten Speicher des Telefons.';
+
+ @override
+ String get keyAdd => 'Schlüssel hinzufügen';
+
+ @override
+ String get keyGenerateNew => 'Neuen Schlüssel erzeugen';
+
+ @override
+ String get keyGenerateHint => 'Ed25519, direkt auf dem Telefon erzeugt';
+
+ @override
+ String get keyGenerated =>
+ 'Schlüssel bereit. Kopiere seinen öffentlichen Teil auf den Server.';
+
+ @override
+ String get keyImport => 'Schlüssel importieren';
+
+ @override
+ String get keyImportHint => 'Privater OpenSSH-, RSA- oder ECDSA-Schlüssel';
+
+ @override
+ String get keyImportNote =>
+ 'Ein passwortgeschützter Schlüssel wird einmal entsperrt und im verschlüsselten Speicher des Telefons abgelegt. Die Passphrase wird nicht gespeichert.';
+
+ @override
+ String get keyImportedName => 'Importierter Schlüssel';
+
+ @override
+ String get keyName => 'Name';
+
+ @override
+ String get keyPrivate => 'Privater Schlüssel';
+
+ @override
+ String get keyPassphrase => 'Passphrase des Schlüssels';
+
+ @override
+ String get keyCopy => 'Öffentlichen Schlüssel kopieren';
+
+ @override
+ String get keyCopied => 'Öffentlicher Schlüssel kopiert';
+
+ @override
+ String keyDeleteTitle(String name) {
+ return '$name löschen?';
+ }
+
+ @override
+ String get keyDeleteMessage =>
+ 'Der private Schlüssel wird endgültig vom Telefon gelöscht.';
+
+ @override
+ String keyDeleteUsed(String servers) {
+ return 'Der private Schlüssel wird endgültig vom Telefon gelöscht. Diese Server nutzen ihn und brauchen dann einen anderen Schlüssel: $servers';
+ }
+
+ @override
+ String get keyErrorNotAKey =>
+ 'Das ist kein privater Schlüssel. Füge den ganzen Text ein, der mit -----BEGIN beginnt.';
+
+ @override
+ String get keyErrorNeedsPassphrase =>
+ 'Dieser Schlüssel ist geschützt. Gib seine Passphrase ein.';
+
+ @override
+ String get keyErrorWrongPassphrase => 'Falsche Passphrase.';
+
+ @override
+ String get keyErrorUnsupported =>
+ 'ServerDeck kennt diesen Schlüsseltyp nicht. Verwende einen Ed25519-, RSA- oder ECDSA-Schlüssel.';
+
+ @override
+ String get sessionOnline => 'online';
+
+ @override
+ String get sessionConnecting => 'verbinde';
+
+ @override
+ String get sessionReconnect => 'Neu verbinden';
+
+ @override
+ String get sessionConnectingTo => 'Verbinde…';
+
+ @override
+ String get problemUnreachableTitle => 'Server nicht erreichbar';
+
+ @override
+ String problemUnreachable(String host, int port) {
+ return 'Keine Verbindung zu $host auf Port $port möglich. Prüfe die Adresse, den Port und die Verbindung des Telefons.';
+ }
+
+ @override
+ String get problemTimeoutTitle => 'Keine Antwort';
+
+ @override
+ String get problemTimeout =>
+ 'Der Server hat nicht innerhalb von 12 Sekunden geantwortet. Vielleicht ist er überlastet, oder eine Firewall verwirft die Verbindung.';
+
+ @override
+ String get problemAuthTitle => 'Anmeldung fehlgeschlagen';
+
+ @override
+ String problemAuthKey(String user) {
+ return 'Der Server hat den Schlüssel für $user nicht akzeptiert. Trage den öffentlichen Teil des Schlüssels in ~/.ssh/authorized_keys auf dem Server ein.';
+ }
+
+ @override
+ String problemAuthPassword(String user) {
+ return 'Der Server hat das Passwort für $user nicht akzeptiert.';
+ }
+
+ @override
+ String get problemHostKeyChangedTitle =>
+ 'Der Schlüssel des Servers hat sich geändert';
+
+ @override
+ String get problemHostKeyChanged =>
+ 'Der Server hat einen anderen Identitätsschlüssel gezeigt als den, den du zuvor akzeptiert hast. Das kann eine Neuinstallation sein oder jemand dazwischen. ServerDeck hat sich nicht verbunden.';
+
+ @override
+ String get problemHostKeyRefusedTitle => 'Schlüssel abgelehnt';
+
+ @override
+ String get problemHostKeyRefused =>
+ 'Du hast den Schlüssel des Servers nicht akzeptiert, deshalb hat sich ServerDeck nicht angemeldet.';
+
+ @override
+ String get problemMissingTitle => 'Keine Anmeldedaten';
+
+ @override
+ String get problemMissing =>
+ 'Für diesen Server ist weder Schlüssel noch Passwort festgelegt. Bearbeite den Server und füge eines hinzu.';
+
+ @override
+ String get problemDisconnectedTitle => 'Verbindung verloren';
+
+ @override
+ String get problemDisconnected =>
+ 'Die Verbindung ist abgebrochen: ein Netzwechsel, ein Neustart des Servers, oder das Telefon war im Ruhemodus.';
+
+ @override
+ String get problemOtherTitle => 'Verbindung fehlgeschlagen';
+
+ @override
+ String get hostKeyNewTitle => 'Neuer Server';
+
+ @override
+ String hostKeyNew(String host) {
+ return 'Du verbindest dich zum ersten Mal mit $host. Das ist der Identitätsschlüssel des Servers:';
+ }
+
+ @override
+ String get hostKeyCheck =>
+ 'Zur Sicherheit gibt dieser Befehl auf dem Server denselben Fingerabdruck aus:';
+
+ @override
+ String get hostKeyAccept => 'Akzeptieren';
+
+ @override
+ String get hostKeyReject => 'Ablehnen';
+
+ @override
+ String get hostKeyKnown => 'Akzeptiert';
+
+ @override
+ String get hostKeyNow => 'Jetzt';
+
+ @override
+ String get hostKeyForget => 'Alten Schlüssel vergessen';
+
+ @override
+ String get hostKeyForgetTitle => 'Alten Schlüssel vergessen?';
+
+ @override
+ String get hostKeyForgetMessage =>
+ 'Tu das nur, wenn du weißt, warum sich der Schlüssel geändert hat, zum Beispiel weil du den Server neu installiert hast. ServerDeck fragt dann nach dem neuen Schlüssel.';
+
+ @override
+ String get statsFailedTitle => 'Server konnte nicht gelesen werden';
+
+ @override
+ String get statsFailed =>
+ 'Die Antwort des Servers ergab keinen Sinn. Die Statistiken brauchen Linux (/proc).';
+
+ @override
+ String get statsStale =>
+ 'Die letzte Aktualisierung ist fehlgeschlagen. Die Werte oben sind älter.';
+
+ @override
+ String get statCpu => 'CPU';
+
+ @override
+ String get statMemory => 'Speicher';
+
+ @override
+ String get statDisk => 'Festplatte';
+
+ @override
+ String statCores(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: '$count Kerne',
+ one: '1 Kern',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String get statHistory => 'Letzte 3 Minuten';
+
+ @override
+ String get statHistoryHint =>
+ 'Wird alle 3 Sekunden aktualisiert, solange dieser Bildschirm offen ist.';
+
+ @override
+ String get statNetIn => 'Ein';
+
+ @override
+ String get statNetOut => 'Aus';
+
+ @override
+ String get statLoad => 'Last';
+
+ @override
+ String get statLoad1 => '1 Min.';
+
+ @override
+ String get statLoad5 => '5 Min.';
+
+ @override
+ String get statLoad15 => '15 Min.';
+
+ @override
+ String get statDisks => 'Festplatten';
+
+ @override
+ String get statSwap => 'Swap';
+
+ @override
+ String get statUptime => 'Läuft seit';
+
+ @override
+ String uptimeDays(int days, int hours) {
+ String _temp0 = intl.Intl.pluralLogic(
+ days,
+ locale: localeName,
+ other: '$days Tage',
+ one: '1 Tag',
+ );
+ return '$_temp0, $hours Std.';
+ }
+
+ @override
+ String uptimeHours(int hours, int minutes) {
+ return '$hours Std. $minutes Min.';
+ }
+
+ @override
+ String get tabOverview => 'Übersicht';
+
+ @override
+ String get tabServices => 'Dienste';
+
+ @override
+ String get servicesFailedTitle => 'Dienste konnten nicht aufgelistet werden';
+
+ @override
+ String get servicesSearch => 'Nach Name oder Beschreibung suchen';
+
+ @override
+ String servicesRunning(int count) {
+ return 'Laufend · $count';
+ }
+
+ @override
+ String servicesFailed(int count) {
+ return 'Fehlgeschlagen · $count';
+ }
+
+ @override
+ String servicesAll(int count) {
+ return 'Alle · $count';
+ }
+
+ @override
+ String get servicesNone => 'Keine Treffer.';
+
+ @override
+ String get servicesNoneFailed => 'Keine fehlgeschlagenen Dienste.';
+
+ @override
+ String get containersNone => 'Keine Container.';
+
+ @override
+ String get stateRunning => 'läuft';
+
+ @override
+ String get stateExited => 'beendet';
+
+ @override
+ String get stateFailed => 'fehlgeschlagen';
+
+ @override
+ String get stateActivating => 'startet';
+
+ @override
+ String get stateInactive => 'gestoppt';
+
+ @override
+ String get stateUnhealthy => 'fehlerhaft';
+
+ @override
+ String get statePaused => 'pausiert';
+
+ @override
+ String get stateRestarting => 'startet neu';
+
+ @override
+ String get stateExitedContainer => 'beendet';
+
+ @override
+ String get stateCreated => 'erstellt';
+
+ @override
+ String get actionRestart => 'Neu starten';
+
+ @override
+ String get actionReload => 'Neu laden';
+
+ @override
+ String get actionStop => 'Stoppen';
+
+ @override
+ String get actionStart => 'Starten';
+
+ @override
+ String actionConfirmTitle(String action, String subject) {
+ return '$action: $subject?';
+ }
+
+ @override
+ String actionConfirmMessage(String command) {
+ return 'Das wird auf dem Server ausgeführt:\n$command';
+ }
+
+ @override
+ String actionDone(String action, String subject) {
+ return '$action erledigt: $subject';
+ }
+
+ @override
+ String actionFailed(int code, String reason) {
+ return 'Das hat nicht geklappt (Exit-Code $code). $reason';
+ }
+
+ @override
+ String get actionNeedsSudo =>
+ 'Dafür sind Administratorrechte nötig. Melde dich als root an oder erlaube sudo ohne Passwort für diesen Befehl.';
+
+ @override
+ String get tabLogs => 'Logs';
+
+ @override
+ String get logsJournal => 'Systemjournal';
+
+ @override
+ String get logsAdd => 'Quelle';
+
+ @override
+ String get logsAddTitle => 'Neue Logquelle';
+
+ @override
+ String get logsKindUnit => 'Dienst';
+
+ @override
+ String get logsKindFile => 'Datei';
+
+ @override
+ String get logsUnitLabel => 'Name des Dienstes';
+
+ @override
+ String get logsFileLabel => 'Dateipfad';
+
+ @override
+ String get logsFilter => 'Filter';
+
+ @override
+ String get logsErrorsOnly => 'Nur Fehler';
+
+ @override
+ String get logsPause => 'Pause';
+
+ @override
+ String get logsResume => 'Fortsetzen';
+
+ @override
+ String get logsClear => 'Bildschirm leeren';
+
+ @override
+ String logsNewLines(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: '$count neue Zeilen',
+ one: '1 neue Zeile',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String logsEnded(int code) {
+ return 'Das Verfolgen des Logs wurde beendet (Exit-Code $code).';
+ }
+
+ @override
+ String logsDeleteTitle(String name) {
+ return '$name löschen?';
+ }
+
+ @override
+ String get logsDeleteMessage =>
+ 'Nur die gespeicherte Quelle wird gelöscht, nicht das Log auf dem Server.';
+
+ @override
+ String get tabCommands => 'Befehle';
+
+ @override
+ String get commandAdd => 'Neuer Befehl';
+
+ @override
+ String get commandEdit => 'Befehl bearbeiten';
+
+ @override
+ String get commandQuick => 'Einmaliger Befehl…';
+
+ @override
+ String get commandRun => 'Ausführen';
+
+ @override
+ String commandRunTitle(String name) {
+ return '$name ausführen?';
+ }
+
+ @override
+ String get commandSaved => 'Gespeicherte Befehle';
+
+ @override
+ String get commandTemplates => 'Für den Anfang';
+
+ @override
+ String get commandTemplatesHint =>
+ 'Tippe auf einen, um ihn zu deinen gespeicherten Befehlen hinzuzufügen. Sie lesen nur.';
+
+ @override
+ String get commandLabel => 'Befehl';
+
+ @override
+ String get commandEverywhere => 'Auf allen Servern';
+
+ @override
+ String get commandEverywhereHint => 'Erscheint bei all deinen Servern';
+
+ @override
+ String get commandConfirm => 'Vor dem Ausführen fragen';
+
+ @override
+ String get commandCopy => 'Ausgabe kopieren';
+
+ @override
+ String get commandCopied => 'Ausgabe kopiert';
+
+ @override
+ String get commandError => 'Fehler';
+
+ @override
+ String get commandStopped => 'gestoppt';
+
+ @override
+ String commandExit(int code) {
+ return 'Exit $code';
+ }
+
+ @override
+ String get commandRunning => 'läuft';
+
+ @override
+ String get commandNoOutput => 'Keine Ausgabe.';
+
+ @override
+ String get tplDisk => 'Speicherbelegung';
+
+ @override
+ String get tplFolders => 'Größte Ordner';
+
+ @override
+ String get tplUpdates => 'Ausstehende Updates';
+
+ @override
+ String get tplWho => 'Wer ist angemeldet';
+
+ @override
+ String get tplApache => 'Apache-Konfiguration testen';
+
+ @override
+ String get tplNginx => 'Nginx-Konfiguration testen';
+
+ @override
+ String get tplTop => 'Top-CPU';
+
+ @override
+ String get tplPorts => 'Offene Ports';
+
+ @override
+ String get tabTerminal => 'Terminal';
+
+ @override
+ String get terminalClosed => '[Shell geschlossen]';
+
+ @override
+ String get terminalReopen => 'Neue Shell';
+
+ @override
+ String get lockReason => 'ServerDeck entsperren';
+
+ @override
+ String get lockTitle => 'ServerDeck ist gesperrt';
+
+ @override
+ String get lockUnlock => 'Entsperren';
+
+ @override
+ String get settingsTitle => 'Einstellungen';
+
+ @override
+ String get settingsSecurity => 'Sicherheit';
+
+ @override
+ String get settingsLock => 'App-Sperre';
+
+ @override
+ String get settingsLockHint =>
+ 'Fingerabdruck, Gesicht oder die PIN des Telefons beim Start und nach einer Minute im Hintergrund.';
+
+ @override
+ String get settingsLockUnsupported =>
+ 'Richte zuerst eine Displaysperre auf dem Telefon ein.';
+
+ @override
+ String get settingsKnownHosts => 'Bekannte Serverschlüssel';
+
+ @override
+ String get settingsKnownHostsEmpty =>
+ 'Noch keine Serverschlüssel akzeptiert.';
+
+ @override
+ String get settingsKnownHostsHint =>
+ 'Beim ersten Kontakt akzeptierte Identitätsschlüssel. Ändert sich der Schlüssel eines Servers, verbindet sich ServerDeck erst wieder, wenn du den alten hier vergisst.';
+
+ @override
+ String get settingsAbout => 'Über';
+
+ @override
+ String get settingsAboutText =>
+ 'SSH-Serververwaltung. Open Source, MIT-Lizenz. Sammelt nichts; alles bleibt auf dem Telefon.';
+
+ @override
+ String get installKey => 'Schlüssel mit Passwort installieren';
+
+ @override
+ String get installKeyAction => 'Installieren';
+
+ @override
+ String installKeyHint(String user) {
+ return 'ServerDeck meldet sich einmal mit dem Passwort von $user an und fügt den öffentlichen Teil des Schlüssels zu ~/.ssh/authorized_keys hinzu. Das Passwort wird nicht gespeichert.';
+ }
+
+ @override
+ String get installKeyDone =>
+ 'Der Schlüssel ist auf dem Server. Verbinde damit…';
+
+ @override
+ String get sessionError => 'Fehler';
+
+ @override
+ String get themeTitle => 'Darstellung';
+
+ @override
+ String get themeSystem => 'System';
+
+ @override
+ String get themeLight => 'Hell';
+
+ @override
+ String get themeDark => 'Dunkel';
+
+ @override
+ String get languageTitle => 'Sprache';
+
+ @override
+ String get languageSystem => 'Sprache des Telefons';
+
+ @override
+ String monitorDownTitle(String name) {
+ return '$name ist ausgefallen';
+ }
+
+ @override
+ String monitorUpTitle(String name) {
+ return '$name ist wieder erreichbar';
+ }
+
+ @override
+ String get monitorUpBody => 'Die Prüfung klappt wieder.';
+
+ @override
+ String monitorUpBodyFor(String duration) {
+ return 'Ausgefallen für $duration.';
+ }
+
+ @override
+ String monitorHostKeyTitle(String name) {
+ return '$name: Der Schlüssel des Servers hat sich geändert';
+ }
+
+ @override
+ String get monitorHostKeyBody =>
+ 'ServerDeck hat sich nicht angemeldet. Sieh dir den Server an, bevor du den neuen Schlüssel akzeptierst.';
+
+ @override
+ String get monitorProblemUnreachable => 'Keine Verbindung möglich.';
+
+ @override
+ String get monitorProblemTimeout => 'Keine Antwort.';
+
+ @override
+ String get monitorProblemAuth =>
+ 'Er antwortet, hat die Anmeldung aber nicht akzeptiert.';
+
+ @override
+ String get monitorProblemDisconnected =>
+ 'Er hat vor der Anmeldung aufgelegt.';
+
+ @override
+ String get monitorProblemHostKeyChanged =>
+ 'Sein Schlüssel hat sich geändert.';
+
+ @override
+ String get monitorProblemHostKeyUnknown =>
+ 'Sein Schlüssel ist noch nicht akzeptiert: Öffne ihn einmal in der App.';
+
+ @override
+ String get monitorProblemMissing => 'Er hat weder Schlüssel noch Passwort.';
+
+ @override
+ String get monitorProblemOther => 'Die Prüfung ist fehlgeschlagen.';
+
+ @override
+ String durationSeconds(int n) {
+ String _temp0 = intl.Intl.pluralLogic(
+ n,
+ locale: localeName,
+ other: '$n Sekunden',
+ one: '1 Sekunde',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String durationMinutes(int n) {
+ String _temp0 = intl.Intl.pluralLogic(
+ n,
+ locale: localeName,
+ other: '$n Minuten',
+ one: '1 Minute',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String durationHours(int h, int m) {
+ return '$h Std. $m Min.';
+ }
+
+ @override
+ String durationDays(int d, int h) {
+ String _temp0 = intl.Intl.pluralLogic(
+ d,
+ locale: localeName,
+ other: '$d Tage',
+ one: '1 Tag',
+ );
+ return '$_temp0 $h Std.';
+ }
+
+ @override
+ String get monitorSection => 'Hintergrundüberwachung';
+
+ @override
+ String get monitorEnable => 'Im Hintergrund überwachen';
+
+ @override
+ String get monitorEnableHint =>
+ 'Benachrichtigung, wenn er ausfällt und wenn er wieder da ist.';
+
+ @override
+ String get monitorEvery => 'Wie oft';
+
+ @override
+ String get monitorHow => 'Wie';
+
+ @override
+ String get monitorModeSsh => 'Anmelden';
+
+ @override
+ String get monitorModePort => 'Nur Port';
+
+ @override
+ String get monitorModeSshHint =>
+ 'Meldet sich über SSH an und führt einen leeren Befehl aus. So wird auch geprüft, ob der Schlüssel noch funktioniert, und fail2ban zählt es nie als Angriff.';
+
+ @override
+ String get monitorModePortHint =>
+ 'Öffnet und schließt nur den Port. Schneller, aber ein strenges fail2ban zählt eine Verbindung ohne Anmeldung vielleicht mit.';
+
+ @override
+ String everyMinutes(int n) {
+ return '$n Min.';
+ }
+
+ @override
+ String everyHours(int n) {
+ String _temp0 = intl.Intl.pluralLogic(
+ n,
+ locale: localeName,
+ other: '$n Stunden',
+ one: '1 Stunde',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String get monitorTitle => 'Überwachung';
+
+ @override
+ String get monitorClear => 'Protokoll leeren';
+
+ @override
+ String get monitorClearTitle => 'Überwachungsprotokoll leeren?';
+
+ @override
+ String get monitorClearMessage =>
+ 'Die Prüfungen und Ausfälle werden gelöscht. Die Überwachung läuft weiter.';
+
+ @override
+ String get monitorEmptyTitle => 'Keine Server überwacht';
+
+ @override
+ String get monitorEmpty =>
+ 'Schalte die Hintergrundüberwachung ein, wenn du einen Server bearbeitest. ServerDeck prüft ihn im Hintergrund und sagt dir, wenn er ausfällt.';
+
+ @override
+ String get monitorServers => 'Server';
+
+ @override
+ String get monitorIncidents => 'Ausfälle';
+
+ @override
+ String get monitorNoIncidents => 'Noch keine Ausfälle.';
+
+ @override
+ String get monitorTimingHint =>
+ 'Android führt Hintergrundarbeit höchstens alle 15 Minuten aus, und nur mit Netz. Energiesparmodus oder Tiefschlaf können sie verzögern; fehlen Prüfungen, nimm ServerDeck aus der Akkuoptimierung heraus.';
+
+ @override
+ String get agoNow => 'gerade eben';
+
+ @override
+ String agoMinutes(int n) {
+ return 'vor $n Min.';
+ }
+
+ @override
+ String agoHours(int n) {
+ return 'vor $n Std.';
+ }
+
+ @override
+ String get monitorWaiting => 'Noch keine Prüfung';
+
+ @override
+ String monitorSomeDown(int n) {
+ String _temp0 = intl.Intl.pluralLogic(
+ n,
+ locale: localeName,
+ other: '$n Server sind ausgefallen',
+ one: '1 Server ist ausgefallen',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String get monitorAllUp => 'Alle Server sind erreichbar';
+
+ @override
+ String monitorCount(int n) {
+ String _temp0 = intl.Intl.pluralLogic(
+ n,
+ locale: localeName,
+ other: '$n Server überwacht',
+ one: '1 Server überwacht',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String monitorLast(String when) {
+ return 'zuletzt $when';
+ }
+
+ @override
+ String get monitorChecking => 'Prüfe…';
+
+ @override
+ String get monitorCheckNow => 'Jetzt prüfen';
+
+ @override
+ String get monitorChecked => 'Geprüft';
+
+ @override
+ String get monitorStateUnknown => 'noch keine Daten';
+
+ @override
+ String monitorDownFor(String d) {
+ return 'ausgefallen · $d';
+ }
+
+ @override
+ String get window24h => '24 Stunden';
+
+ @override
+ String get window7d => '7 Tage';
+
+ @override
+ String get window30d => '30 Tage';
+
+ @override
+ String shortMinutes(int n) {
+ return '$n Min.';
+ }
+
+ @override
+ String shortHours(int n) {
+ return '$n Std.';
+ }
+
+ @override
+ String shortDays(int n) {
+ return '$n T.';
+ }
+
+ @override
+ String get monitorOngoing => 'andauernd';
+
+ @override
+ String everyNMinutes(int n) {
+ return 'alle $n Min.';
+ }
+
+ @override
+ String everyNHours(int n) {
+ String _temp0 = intl.Intl.pluralLogic(
+ n,
+ locale: localeName,
+ other: 'alle $n Std.',
+ one: 'stündlich',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String get monitorPhoneOffline =>
+ 'Das Telefon ist nicht mit dem Internet verbunden, deshalb kann gerade nichts geprüft werden.';
+
+ @override
+ String get monitorForgetTitle => 'Diesen Ausfall löschen?';
+
+ @override
+ String get monitorForgetMessage =>
+ 'Wenn es kein echter Ausfall war (das Telefon war zum Beispiel offline), werden der Eintrag und seine fehlgeschlagenen Prüfungen gelöscht und die Verfügbarkeit neu berechnet.';
+
+ @override
+ String get monitorIncidentsHint =>
+ 'Lange auf einen beendeten Ausfall drücken, um ihn zu löschen.';
+}
diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart
index 25b5c57..7d4ee4a 100644
--- a/lib/l10n/generated/app_localizations_en.dart
+++ b/lib/l10n/generated/app_localizations_en.dart
@@ -711,6 +711,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get themeDark => 'Dark';
+ @override
+ String get languageTitle => 'Language';
+
+ @override
+ String get languageSystem => 'The phone\'s language';
+
@override
String monitorDownTitle(String name) {
return '$name is down';
diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart
new file mode 100644
index 0000000..cb25a07
--- /dev/null
+++ b/lib/l10n/generated/app_localizations_es.dart
@@ -0,0 +1,1012 @@
+// ignore: unused_import
+import 'package:intl/intl.dart' as intl;
+
+import 'app_localizations.dart';
+
+// ignore_for_file: type=lint
+
+/// The translations for Spanish Castilian (`es`).
+class AppLocalizationsEs extends AppLocalizations {
+ AppLocalizationsEs([String locale = 'es']) : super(locale);
+
+ @override
+ String get appTitle => 'ServerDeck';
+
+ @override
+ String get ok => 'Aceptar';
+
+ @override
+ String get cancel => 'Cancelar';
+
+ @override
+ String get save => 'Guardar';
+
+ @override
+ String get delete => 'Eliminar';
+
+ @override
+ String get edit => 'Editar';
+
+ @override
+ String get rename => 'Renombrar';
+
+ @override
+ String get paste => 'Pegar';
+
+ @override
+ String get fieldRequired => 'Obligatorio';
+
+ @override
+ String get serversTitle => 'Servidores';
+
+ @override
+ String get serversEmptyTitle => 'Aún no hay servidores';
+
+ @override
+ String get serversEmpty =>
+ 'Añade un servidor con su dirección y tu nombre de usuario. ServerDeck se conecta por SSH; no hace falta instalar nada en el servidor.';
+
+ @override
+ String get serverAdd => 'Añadir servidor';
+
+ @override
+ String get serverEditTitle => 'Editar servidor';
+
+ @override
+ String serverDeleteTitle(String name) {
+ return '¿Eliminar $name?';
+ }
+
+ @override
+ String get serverDeleteMessage =>
+ 'También se eliminan su contraseña guardada, sus comandos y sus fuentes de registros. Las claves se conservan.';
+
+ @override
+ String get serverSectionConnection => 'Conexión';
+
+ @override
+ String get serverSectionAuth => 'Inicio de sesión';
+
+ @override
+ String get serverHost => 'Dirección';
+
+ @override
+ String get serverUser => 'Usuario';
+
+ @override
+ String get serverPort => 'Puerto';
+
+ @override
+ String get serverPortInvalid => '1–65535';
+
+ @override
+ String get serverName => 'Nombre';
+
+ @override
+ String get serverNameHint => 'La dirección, si se deja vacío';
+
+ @override
+ String get statusChecking => '…';
+
+ @override
+ String get statusDown => 'sin conexión';
+
+ @override
+ String get authKey => 'Clave';
+
+ @override
+ String get authPassword => 'Contraseña';
+
+ @override
+ String get authKeyPick => 'Clave SSH';
+
+ @override
+ String get authKeyMissing => 'Elige una clave';
+
+ @override
+ String get authKeyNone =>
+ 'Aún no hay claves. Genera una y pon su parte pública en ~/.ssh/authorized_keys en el servidor.';
+
+ @override
+ String get authPasswordKept => 'Guardada. Déjalo vacío para conservarla.';
+
+ @override
+ String get keysTitle => 'Claves SSH';
+
+ @override
+ String get keysEmptyTitle => 'Aún no hay claves';
+
+ @override
+ String get keysEmpty =>
+ 'Genera una nueva clave Ed25519 o importa una que ya tengas. La clave privada se queda en el almacenamiento cifrado del teléfono.';
+
+ @override
+ String get keyAdd => 'Añadir clave';
+
+ @override
+ String get keyGenerateNew => 'Generar una clave nueva';
+
+ @override
+ String get keyGenerateHint => 'Ed25519, creada aquí en el teléfono';
+
+ @override
+ String get keyGenerated => 'Clave lista. Copia su parte pública al servidor.';
+
+ @override
+ String get keyImport => 'Importar una clave';
+
+ @override
+ String get keyImportHint => 'Clave privada OpenSSH, RSA o ECDSA';
+
+ @override
+ String get keyImportNote =>
+ 'Una clave protegida con frase de contraseña se desbloquea una vez y se guarda en el almacenamiento cifrado del teléfono. La frase de contraseña no se guarda.';
+
+ @override
+ String get keyImportedName => 'Clave importada';
+
+ @override
+ String get keyName => 'Nombre';
+
+ @override
+ String get keyPrivate => 'Clave privada';
+
+ @override
+ String get keyPassphrase => 'Frase de contraseña de la clave';
+
+ @override
+ String get keyCopy => 'Copiar la clave pública';
+
+ @override
+ String get keyCopied => 'Clave pública copiada';
+
+ @override
+ String keyDeleteTitle(String name) {
+ return '¿Eliminar $name?';
+ }
+
+ @override
+ String get keyDeleteMessage =>
+ 'La clave privada se elimina del teléfono para siempre.';
+
+ @override
+ String keyDeleteUsed(String servers) {
+ return 'La clave privada se elimina del teléfono para siempre. Estos servidores la usan y necesitarán otra clave: $servers';
+ }
+
+ @override
+ String get keyErrorNotAKey =>
+ 'Esto no es una clave privada. Pega todo el texto que empieza por -----BEGIN.';
+
+ @override
+ String get keyErrorNeedsPassphrase =>
+ 'Esta clave está protegida. Introduce su frase de contraseña.';
+
+ @override
+ String get keyErrorWrongPassphrase => 'Frase de contraseña incorrecta.';
+
+ @override
+ String get keyErrorUnsupported =>
+ 'ServerDeck no conoce este tipo de clave. Usa una clave Ed25519, RSA o ECDSA.';
+
+ @override
+ String get sessionOnline => 'en línea';
+
+ @override
+ String get sessionConnecting => 'conectando';
+
+ @override
+ String get sessionReconnect => 'Reconectar';
+
+ @override
+ String get sessionConnectingTo => 'Conectando…';
+
+ @override
+ String get problemUnreachableTitle => 'Servidor inaccesible';
+
+ @override
+ String problemUnreachable(String host, int port) {
+ return 'No se pudo abrir una conexión con $host en el puerto $port. Revisa la dirección, el puerto y la conexión del teléfono.';
+ }
+
+ @override
+ String get problemTimeoutTitle => 'Sin respuesta a tiempo';
+
+ @override
+ String get problemTimeout =>
+ 'El servidor no respondió en 12 segundos. Puede estar sobrecargado, o un cortafuegos está descartando la conexión.';
+
+ @override
+ String get problemAuthTitle => 'Error de inicio de sesión';
+
+ @override
+ String problemAuthKey(String user) {
+ return 'El servidor no aceptó la clave de $user. Pon la parte pública de la clave en ~/.ssh/authorized_keys en el servidor.';
+ }
+
+ @override
+ String problemAuthPassword(String user) {
+ return 'El servidor no aceptó la contraseña de $user.';
+ }
+
+ @override
+ String get problemHostKeyChangedTitle => 'La clave del servidor ha cambiado';
+
+ @override
+ String get problemHostKeyChanged =>
+ 'El servidor mostró una clave de identidad distinta de la que aceptaste antes. Puede ser una reinstalación o alguien en medio. ServerDeck no se ha conectado.';
+
+ @override
+ String get problemHostKeyRefusedTitle => 'Clave rechazada';
+
+ @override
+ String get problemHostKeyRefused =>
+ 'No aceptaste la clave del servidor, así que ServerDeck no inició sesión.';
+
+ @override
+ String get problemMissingTitle => 'Sin datos de inicio de sesión';
+
+ @override
+ String get problemMissing =>
+ 'Este servidor no tiene clave ni contraseña. Edita el servidor y añade una.';
+
+ @override
+ String get problemDisconnectedTitle => 'Conexión perdida';
+
+ @override
+ String get problemDisconnected =>
+ 'La conexión se cortó: un cambio de red, un reinicio del servidor o el teléfono entró en reposo.';
+
+ @override
+ String get problemOtherTitle => 'No se pudo conectar';
+
+ @override
+ String get hostKeyNewTitle => 'Servidor nuevo';
+
+ @override
+ String hostKeyNew(String host) {
+ return 'Es la primera vez que te conectas a $host. Esta es la clave de identidad del servidor:';
+ }
+
+ @override
+ String get hostKeyCheck =>
+ 'Para estar seguro, este comando en el servidor muestra la misma huella:';
+
+ @override
+ String get hostKeyAccept => 'Aceptar';
+
+ @override
+ String get hostKeyReject => 'Rechazar';
+
+ @override
+ String get hostKeyKnown => 'Aceptada';
+
+ @override
+ String get hostKeyNow => 'Ahora';
+
+ @override
+ String get hostKeyForget => 'Olvidar la clave antigua';
+
+ @override
+ String get hostKeyForgetTitle => '¿Olvidar la clave antigua?';
+
+ @override
+ String get hostKeyForgetMessage =>
+ 'Hazlo solo si sabes por qué cambió la clave, por ejemplo porque reinstalaste el servidor. ServerDeck preguntará entonces por la clave nueva.';
+
+ @override
+ String get statsFailedTitle => 'No se pudo leer el servidor';
+
+ @override
+ String get statsFailed =>
+ 'La respuesta del servidor no tenía sentido. Las estadísticas necesitan Linux (/proc).';
+
+ @override
+ String get statsStale =>
+ 'La última actualización falló. Las cifras de arriba son anteriores.';
+
+ @override
+ String get statCpu => 'CPU';
+
+ @override
+ String get statMemory => 'Memoria';
+
+ @override
+ String get statDisk => 'Disco';
+
+ @override
+ String statCores(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: '$count núcleos',
+ one: '1 núcleo',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String get statHistory => 'Últimos 3 minutos';
+
+ @override
+ String get statHistoryHint =>
+ 'Se actualiza cada 3 segundos mientras esta pantalla está abierta.';
+
+ @override
+ String get statNetIn => 'Entrada';
+
+ @override
+ String get statNetOut => 'Salida';
+
+ @override
+ String get statLoad => 'Carga';
+
+ @override
+ String get statLoad1 => '1 min';
+
+ @override
+ String get statLoad5 => '5 min';
+
+ @override
+ String get statLoad15 => '15 min';
+
+ @override
+ String get statDisks => 'Discos';
+
+ @override
+ String get statSwap => 'Swap';
+
+ @override
+ String get statUptime => 'Activo';
+
+ @override
+ String uptimeDays(int days, int hours) {
+ String _temp0 = intl.Intl.pluralLogic(
+ days,
+ locale: localeName,
+ other: '$days días',
+ one: '1 día',
+ );
+ return '$_temp0, $hours h';
+ }
+
+ @override
+ String uptimeHours(int hours, int minutes) {
+ return '$hours h $minutes min';
+ }
+
+ @override
+ String get tabOverview => 'Resumen';
+
+ @override
+ String get tabServices => 'Servicios';
+
+ @override
+ String get servicesFailedTitle => 'No se pudieron listar los servicios';
+
+ @override
+ String get servicesSearch => 'Buscar por nombre o descripción';
+
+ @override
+ String servicesRunning(int count) {
+ return 'En ejecución · $count';
+ }
+
+ @override
+ String servicesFailed(int count) {
+ return 'Con error · $count';
+ }
+
+ @override
+ String servicesAll(int count) {
+ return 'Todos · $count';
+ }
+
+ @override
+ String get servicesNone => 'No hay coincidencias.';
+
+ @override
+ String get servicesNoneFailed => 'No hay servicios con error.';
+
+ @override
+ String get containersNone => 'No hay contenedores.';
+
+ @override
+ String get stateRunning => 'en ejecución';
+
+ @override
+ String get stateExited => 'finalizado';
+
+ @override
+ String get stateFailed => 'con error';
+
+ @override
+ String get stateActivating => 'iniciando';
+
+ @override
+ String get stateInactive => 'detenido';
+
+ @override
+ String get stateUnhealthy => 'no saludable';
+
+ @override
+ String get statePaused => 'en pausa';
+
+ @override
+ String get stateRestarting => 'reiniciando';
+
+ @override
+ String get stateExitedContainer => 'finalizado';
+
+ @override
+ String get stateCreated => 'creado';
+
+ @override
+ String get actionRestart => 'Reiniciar';
+
+ @override
+ String get actionReload => 'Recargar';
+
+ @override
+ String get actionStop => 'Detener';
+
+ @override
+ String get actionStart => 'Iniciar';
+
+ @override
+ String actionConfirmTitle(String action, String subject) {
+ return '¿$action $subject?';
+ }
+
+ @override
+ String actionConfirmMessage(String command) {
+ return 'Esto se ejecuta en el servidor:\n$command';
+ }
+
+ @override
+ String actionDone(String action, String subject) {
+ return '$action: hecho en $subject';
+ }
+
+ @override
+ String actionFailed(int code, String reason) {
+ return 'No funcionó (código de salida $code). $reason';
+ }
+
+ @override
+ String get actionNeedsSudo =>
+ 'Esto necesita permisos de administrador. Inicia sesión como root o permite sudo sin contraseña para este comando.';
+
+ @override
+ String get tabLogs => 'Registros';
+
+ @override
+ String get logsJournal => 'Diario del sistema';
+
+ @override
+ String get logsAdd => 'Fuente';
+
+ @override
+ String get logsAddTitle => 'Nueva fuente de registros';
+
+ @override
+ String get logsKindUnit => 'Servicio';
+
+ @override
+ String get logsKindFile => 'Archivo';
+
+ @override
+ String get logsUnitLabel => 'Nombre del servicio';
+
+ @override
+ String get logsFileLabel => 'Ruta del archivo';
+
+ @override
+ String get logsFilter => 'Filtro';
+
+ @override
+ String get logsErrorsOnly => 'Solo errores';
+
+ @override
+ String get logsPause => 'Pausar';
+
+ @override
+ String get logsResume => 'Reanudar';
+
+ @override
+ String get logsClear => 'Limpiar la pantalla';
+
+ @override
+ String logsNewLines(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: '$count líneas nuevas',
+ one: '1 línea nueva',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String logsEnded(int code) {
+ return 'El seguimiento del registro se detuvo (código de salida $code).';
+ }
+
+ @override
+ String logsDeleteTitle(String name) {
+ return '¿Eliminar $name?';
+ }
+
+ @override
+ String get logsDeleteMessage =>
+ 'Solo se elimina la fuente guardada, no el registro del servidor.';
+
+ @override
+ String get tabCommands => 'Comandos';
+
+ @override
+ String get commandAdd => 'Nuevo comando';
+
+ @override
+ String get commandEdit => 'Editar comando';
+
+ @override
+ String get commandQuick => 'Comando puntual…';
+
+ @override
+ String get commandRun => 'Ejecutar';
+
+ @override
+ String commandRunTitle(String name) {
+ return '¿Ejecutar $name?';
+ }
+
+ @override
+ String get commandSaved => 'Comandos guardados';
+
+ @override
+ String get commandTemplates => 'Para empezar';
+
+ @override
+ String get commandTemplatesHint =>
+ 'Toca uno para añadirlo a tus comandos guardados. Solo leen.';
+
+ @override
+ String get commandLabel => 'Comando';
+
+ @override
+ String get commandEverywhere => 'En todos los servidores';
+
+ @override
+ String get commandEverywhereHint => 'Aparece en todos tus servidores';
+
+ @override
+ String get commandConfirm => 'Preguntar antes de ejecutar';
+
+ @override
+ String get commandCopy => 'Copiar la salida';
+
+ @override
+ String get commandCopied => 'Salida copiada';
+
+ @override
+ String get commandError => 'error';
+
+ @override
+ String get commandStopped => 'detenido';
+
+ @override
+ String commandExit(int code) {
+ return 'salida $code';
+ }
+
+ @override
+ String get commandRunning => 'en ejecución';
+
+ @override
+ String get commandNoOutput => 'Sin salida.';
+
+ @override
+ String get tplDisk => 'Uso del disco';
+
+ @override
+ String get tplFolders => 'Carpetas más grandes';
+
+ @override
+ String get tplUpdates => 'Actualizaciones pendientes';
+
+ @override
+ String get tplWho => 'Quién ha iniciado sesión';
+
+ @override
+ String get tplApache => 'Prueba de configuración de Apache';
+
+ @override
+ String get tplNginx => 'Prueba de configuración de Nginx';
+
+ @override
+ String get tplTop => 'Mayor uso de CPU';
+
+ @override
+ String get tplPorts => 'Puertos a la escucha';
+
+ @override
+ String get tabTerminal => 'Terminal';
+
+ @override
+ String get terminalClosed => '[shell cerrada]';
+
+ @override
+ String get terminalReopen => 'Nueva shell';
+
+ @override
+ String get lockReason => 'Desbloquear ServerDeck';
+
+ @override
+ String get lockTitle => 'ServerDeck está bloqueado';
+
+ @override
+ String get lockUnlock => 'Desbloquear';
+
+ @override
+ String get settingsTitle => 'Ajustes';
+
+ @override
+ String get settingsSecurity => 'Seguridad';
+
+ @override
+ String get settingsLock => 'Bloqueo de la app';
+
+ @override
+ String get settingsLockHint =>
+ 'Huella, rostro o el PIN del teléfono al iniciar y tras un minuto en segundo plano.';
+
+ @override
+ String get settingsLockUnsupported =>
+ 'Primero configura un bloqueo de pantalla en el teléfono.';
+
+ @override
+ String get settingsKnownHosts => 'Claves de servidor conocidas';
+
+ @override
+ String get settingsKnownHostsEmpty =>
+ 'Aún no se ha aceptado ninguna clave de servidor.';
+
+ @override
+ String get settingsKnownHostsHint =>
+ 'Claves de identidad aceptadas en el primer contacto. Si la clave de un servidor cambia, ServerDeck no se conectará hasta que olvides aquí la antigua.';
+
+ @override
+ String get settingsAbout => 'Acerca de';
+
+ @override
+ String get settingsAboutText =>
+ 'Gestor de servidores SSH. Código abierto, licencia MIT. No recopila nada; todo se queda en el teléfono.';
+
+ @override
+ String get installKey => 'Instalar la clave con una contraseña';
+
+ @override
+ String get installKeyAction => 'Instalar';
+
+ @override
+ String installKeyHint(String user) {
+ return 'ServerDeck inicia sesión una vez con la contraseña de $user y añade la parte pública de la clave a ~/.ssh/authorized_keys. La contraseña no se guarda.';
+ }
+
+ @override
+ String get installKeyDone =>
+ 'La clave está en el servidor. Conectando con ella…';
+
+ @override
+ String get sessionError => 'error';
+
+ @override
+ String get themeTitle => 'Apariencia';
+
+ @override
+ String get themeSystem => 'Sistema';
+
+ @override
+ String get themeLight => 'Claro';
+
+ @override
+ String get themeDark => 'Oscuro';
+
+ @override
+ String get languageTitle => 'Idioma';
+
+ @override
+ String get languageSystem => 'Idioma del teléfono';
+
+ @override
+ String monitorDownTitle(String name) {
+ return '$name está caído';
+ }
+
+ @override
+ String monitorUpTitle(String name) {
+ return '$name vuelve a funcionar';
+ }
+
+ @override
+ String get monitorUpBody => 'La comprobación vuelve a funcionar.';
+
+ @override
+ String monitorUpBodyFor(String duration) {
+ return 'Estuvo caído durante $duration.';
+ }
+
+ @override
+ String monitorHostKeyTitle(String name) {
+ return '$name: la clave del servidor ha cambiado';
+ }
+
+ @override
+ String get monitorHostKeyBody =>
+ 'ServerDeck no inició sesión. Revisa el servidor antes de aceptar la clave nueva.';
+
+ @override
+ String get monitorProblemUnreachable => 'No se pudo abrir una conexión.';
+
+ @override
+ String get monitorProblemTimeout => 'Sin respuesta a tiempo.';
+
+ @override
+ String get monitorProblemAuth =>
+ 'Responde, pero no aceptó el inicio de sesión.';
+
+ @override
+ String get monitorProblemDisconnected => 'Cortó antes del inicio de sesión.';
+
+ @override
+ String get monitorProblemHostKeyChanged => 'Su clave ha cambiado.';
+
+ @override
+ String get monitorProblemHostKeyUnknown =>
+ 'Su clave aún no está aceptada: ábrelo una vez en la app.';
+
+ @override
+ String get monitorProblemMissing => 'No tiene clave ni contraseña.';
+
+ @override
+ String get monitorProblemOther => 'La comprobación falló.';
+
+ @override
+ String durationSeconds(int n) {
+ String _temp0 = intl.Intl.pluralLogic(
+ n,
+ locale: localeName,
+ other: '$n segundos',
+ one: '1 segundo',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String durationMinutes(int n) {
+ String _temp0 = intl.Intl.pluralLogic(
+ n,
+ locale: localeName,
+ other: '$n minutos',
+ one: '1 minuto',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String durationHours(int h, int m) {
+ return '$h h $m min';
+ }
+
+ @override
+ String durationDays(int d, int h) {
+ String _temp0 = intl.Intl.pluralLogic(
+ d,
+ locale: localeName,
+ other: '$d días',
+ one: '1 día',
+ );
+ return '$_temp0 $h h';
+ }
+
+ @override
+ String get monitorSection => 'Supervisión en segundo plano';
+
+ @override
+ String get monitorEnable => 'Supervisar en segundo plano';
+
+ @override
+ String get monitorEnableHint =>
+ 'Recibe un aviso cuando se cae y cuando vuelve.';
+
+ @override
+ String get monitorEvery => 'Con qué frecuencia';
+
+ @override
+ String get monitorHow => 'Cómo';
+
+ @override
+ String get monitorModeSsh => 'Iniciar sesión';
+
+ @override
+ String get monitorModePort => 'Solo puerto';
+
+ @override
+ String get monitorModeSshHint =>
+ 'Inicia sesión por SSH y ejecuta un comando vacío. Así también comprueba que la clave sigue funcionando, y fail2ban nunca lo cuenta como un ataque.';
+
+ @override
+ String get monitorModePortHint =>
+ 'Solo abre y cierra el puerto. Más rápido, pero un fail2ban estricto puede contar una conexión sin inicio de sesión.';
+
+ @override
+ String everyMinutes(int n) {
+ return '$n min';
+ }
+
+ @override
+ String everyHours(int n) {
+ String _temp0 = intl.Intl.pluralLogic(
+ n,
+ locale: localeName,
+ other: '$n horas',
+ one: '1 hora',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String get monitorTitle => 'Supervisión';
+
+ @override
+ String get monitorClear => 'Borrar el registro';
+
+ @override
+ String get monitorClearTitle => '¿Borrar el registro de supervisión?';
+
+ @override
+ String get monitorClearMessage =>
+ 'Se eliminan las comprobaciones y las caídas. La supervisión continúa.';
+
+ @override
+ String get monitorEmptyTitle => 'Ningún servidor supervisado';
+
+ @override
+ String get monitorEmpty =>
+ 'Activa la Supervisión en segundo plano al editar un servidor. ServerDeck lo comprueba en segundo plano y te avisa cuando se cae.';
+
+ @override
+ String get monitorServers => 'Servidores';
+
+ @override
+ String get monitorIncidents => 'Caídas';
+
+ @override
+ String get monitorNoIncidents => 'Aún no hay caídas.';
+
+ @override
+ String get monitorTimingHint =>
+ 'Android ejecuta tareas en segundo plano como mucho cada 15 minutos, y solo con red. El ahorro de batería o el reposo profundo pueden retrasarlas; si faltan comprobaciones, quita ServerDeck de la optimización de batería.';
+
+ @override
+ String get agoNow => 'ahora mismo';
+
+ @override
+ String agoMinutes(int n) {
+ return 'hace $n min';
+ }
+
+ @override
+ String agoHours(int n) {
+ return 'hace $n h';
+ }
+
+ @override
+ String get monitorWaiting => 'Aún sin comprobar';
+
+ @override
+ String monitorSomeDown(int n) {
+ String _temp0 = intl.Intl.pluralLogic(
+ n,
+ locale: localeName,
+ other: '$n servidores están caídos',
+ one: '1 servidor está caído',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String get monitorAllUp => 'Todos los servidores funcionan';
+
+ @override
+ String monitorCount(int n) {
+ String _temp0 = intl.Intl.pluralLogic(
+ n,
+ locale: localeName,
+ other: '$n servidores supervisados',
+ one: '1 servidor supervisado',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String monitorLast(String when) {
+ return 'última: $when';
+ }
+
+ @override
+ String get monitorChecking => 'Comprobando…';
+
+ @override
+ String get monitorCheckNow => 'Comprobar ahora';
+
+ @override
+ String get monitorChecked => 'Comprobado';
+
+ @override
+ String get monitorStateUnknown => 'aún sin datos';
+
+ @override
+ String monitorDownFor(String d) {
+ return 'caído · $d';
+ }
+
+ @override
+ String get window24h => '24 horas';
+
+ @override
+ String get window7d => '7 días';
+
+ @override
+ String get window30d => '30 días';
+
+ @override
+ String shortMinutes(int n) {
+ return '$n min';
+ }
+
+ @override
+ String shortHours(int n) {
+ return '$n h';
+ }
+
+ @override
+ String shortDays(int n) {
+ return '$n d';
+ }
+
+ @override
+ String get monitorOngoing => 'en curso';
+
+ @override
+ String everyNMinutes(int n) {
+ return 'cada $n min';
+ }
+
+ @override
+ String everyNHours(int n) {
+ String _temp0 = intl.Intl.pluralLogic(
+ n,
+ locale: localeName,
+ other: 'cada $n h',
+ one: 'cada hora',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String get monitorPhoneOffline =>
+ 'El teléfono no tiene conexión a internet, así que ahora no se puede comprobar nada.';
+
+ @override
+ String get monitorForgetTitle => '¿Eliminar esta caída?';
+
+ @override
+ String get monitorForgetMessage =>
+ 'Si no fue una caída real (por ejemplo, el teléfono estaba sin conexión), se eliminan la entrada y sus comprobaciones fallidas, y se vuelve a calcular la disponibilidad.';
+
+ @override
+ String get monitorIncidentsHint =>
+ 'Mantén pulsada una caída cerrada para eliminarla.';
+}
diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart
new file mode 100644
index 0000000..8ebbb25
--- /dev/null
+++ b/lib/l10n/generated/app_localizations_fr.dart
@@ -0,0 +1,1012 @@
+// ignore: unused_import
+import 'package:intl/intl.dart' as intl;
+
+import 'app_localizations.dart';
+
+// ignore_for_file: type=lint
+
+/// The translations for French (`fr`).
+class AppLocalizationsFr extends AppLocalizations {
+ AppLocalizationsFr([String locale = 'fr']) : super(locale);
+
+ @override
+ String get appTitle => 'ServerDeck';
+
+ @override
+ String get ok => 'OK';
+
+ @override
+ String get cancel => 'Annuler';
+
+ @override
+ String get save => 'Enregistrer';
+
+ @override
+ String get delete => 'Supprimer';
+
+ @override
+ String get edit => 'Modifier';
+
+ @override
+ String get rename => 'Renommer';
+
+ @override
+ String get paste => 'Coller';
+
+ @override
+ String get fieldRequired => 'Obligatoire';
+
+ @override
+ String get serversTitle => 'Serveurs';
+
+ @override
+ String get serversEmptyTitle => 'Aucun serveur';
+
+ @override
+ String get serversEmpty =>
+ 'Ajoutez un serveur avec son adresse et votre nom d’utilisateur. ServerDeck se connecte en SSH ; rien n’est à installer sur le serveur.';
+
+ @override
+ String get serverAdd => 'Ajouter un serveur';
+
+ @override
+ String get serverEditTitle => 'Modifier le serveur';
+
+ @override
+ String serverDeleteTitle(String name) {
+ return 'Supprimer $name ?';
+ }
+
+ @override
+ String get serverDeleteMessage =>
+ 'Son mot de passe enregistré, ses commandes et ses sources de journaux sont supprimés aussi. Les clés sont conservées.';
+
+ @override
+ String get serverSectionConnection => 'Connexion';
+
+ @override
+ String get serverSectionAuth => 'Identification';
+
+ @override
+ String get serverHost => 'Adresse';
+
+ @override
+ String get serverUser => 'Utilisateur';
+
+ @override
+ String get serverPort => 'Port';
+
+ @override
+ String get serverPortInvalid => '1–65535';
+
+ @override
+ String get serverName => 'Nom';
+
+ @override
+ String get serverNameHint => 'L’adresse, si laissé vide';
+
+ @override
+ String get statusChecking => '…';
+
+ @override
+ String get statusDown => 'hors ligne';
+
+ @override
+ String get authKey => 'Clé';
+
+ @override
+ String get authPassword => 'Mot de passe';
+
+ @override
+ String get authKeyPick => 'Clé SSH';
+
+ @override
+ String get authKeyMissing => 'Choisissez une clé';
+
+ @override
+ String get authKeyNone =>
+ 'Aucune clé. Générez-en une et placez sa partie publique dans ~/.ssh/authorized_keys sur le serveur.';
+
+ @override
+ String get authPasswordKept => 'Enregistré. Laissez vide pour le conserver.';
+
+ @override
+ String get keysTitle => 'Clés SSH';
+
+ @override
+ String get keysEmptyTitle => 'Aucune clé';
+
+ @override
+ String get keysEmpty =>
+ 'Générez une nouvelle clé Ed25519 ou importez une clé existante. La clé privée reste dans le stockage chiffré du téléphone.';
+
+ @override
+ String get keyAdd => 'Ajouter une clé';
+
+ @override
+ String get keyGenerateNew => 'Générer une nouvelle clé';
+
+ @override
+ String get keyGenerateHint => 'Ed25519, créée ici sur le téléphone';
+
+ @override
+ String get keyGenerated =>
+ 'Clé prête. Copiez sa partie publique sur le serveur.';
+
+ @override
+ String get keyImport => 'Importer une clé';
+
+ @override
+ String get keyImportHint => 'Clé privée OpenSSH, RSA ou ECDSA';
+
+ @override
+ String get keyImportNote =>
+ 'Une clé protégée par une phrase secrète est déverrouillée une fois et enregistrée dans le stockage chiffré du téléphone. La phrase secrète n’est pas conservée.';
+
+ @override
+ String get keyImportedName => 'Clé importée';
+
+ @override
+ String get keyName => 'Nom';
+
+ @override
+ String get keyPrivate => 'Clé privée';
+
+ @override
+ String get keyPassphrase => 'Phrase secrète de la clé';
+
+ @override
+ String get keyCopy => 'Copier la clé publique';
+
+ @override
+ String get keyCopied => 'Clé publique copiée';
+
+ @override
+ String keyDeleteTitle(String name) {
+ return 'Supprimer $name ?';
+ }
+
+ @override
+ String get keyDeleteMessage =>
+ 'La clé privée est supprimée définitivement du téléphone.';
+
+ @override
+ String keyDeleteUsed(String servers) {
+ return 'La clé privée est supprimée définitivement du téléphone. Ces serveurs l’utilisent et auront besoin d’une autre clé : $servers';
+ }
+
+ @override
+ String get keyErrorNotAKey =>
+ 'Ce n’est pas une clé privée. Collez tout le texte qui commence par -----BEGIN.';
+
+ @override
+ String get keyErrorNeedsPassphrase =>
+ 'Cette clé est protégée. Saisissez sa phrase secrète.';
+
+ @override
+ String get keyErrorWrongPassphrase => 'Phrase secrète incorrecte.';
+
+ @override
+ String get keyErrorUnsupported =>
+ 'ServerDeck ne connaît pas ce type de clé. Utilisez une clé Ed25519, RSA ou ECDSA.';
+
+ @override
+ String get sessionOnline => 'en ligne';
+
+ @override
+ String get sessionConnecting => 'connexion';
+
+ @override
+ String get sessionReconnect => 'Se reconnecter';
+
+ @override
+ String get sessionConnectingTo => 'Connexion…';
+
+ @override
+ String get problemUnreachableTitle => 'Serveur injoignable';
+
+ @override
+ String problemUnreachable(String host, int port) {
+ return 'Impossible d’ouvrir une connexion vers $host sur le port $port. Vérifiez l’adresse, le port et la connexion du téléphone.';
+ }
+
+ @override
+ String get problemTimeoutTitle => 'Pas de réponse à temps';
+
+ @override
+ String get problemTimeout =>
+ 'Le serveur n’a pas répondu en 12 secondes. Il est peut-être surchargé, ou un pare-feu bloque la connexion.';
+
+ @override
+ String get problemAuthTitle => 'Échec de l’identification';
+
+ @override
+ String problemAuthKey(String user) {
+ return 'Le serveur n’a pas accepté la clé de $user. Placez la partie publique de la clé dans ~/.ssh/authorized_keys sur le serveur.';
+ }
+
+ @override
+ String problemAuthPassword(String user) {
+ return 'Le serveur n’a pas accepté le mot de passe de $user.';
+ }
+
+ @override
+ String get problemHostKeyChangedTitle => 'La clé du serveur a changé';
+
+ @override
+ String get problemHostKeyChanged =>
+ 'Le serveur a présenté une clé d’identité différente de celle que vous aviez acceptée. Il peut s’agir d’une réinstallation, ou de quelqu’un entre les deux. ServerDeck ne s’est pas connecté.';
+
+ @override
+ String get problemHostKeyRefusedTitle => 'Clé refusée';
+
+ @override
+ String get problemHostKeyRefused =>
+ 'Vous n’avez pas accepté la clé du serveur, ServerDeck ne s’est donc pas identifié.';
+
+ @override
+ String get problemMissingTitle => 'Aucun identifiant';
+
+ @override
+ String get problemMissing =>
+ 'Ce serveur n’a ni clé ni mot de passe. Modifiez le serveur et ajoutez-en un.';
+
+ @override
+ String get problemDisconnectedTitle => 'Connexion perdue';
+
+ @override
+ String get problemDisconnected =>
+ 'La connexion a été coupée : un changement de réseau, un redémarrage du serveur, ou le téléphone s’est mis en veille.';
+
+ @override
+ String get problemOtherTitle => 'Connexion impossible';
+
+ @override
+ String get hostKeyNewTitle => 'Nouveau serveur';
+
+ @override
+ String hostKeyNew(String host) {
+ return 'C’est la première fois que vous vous connectez à $host. Voici la clé d’identité du serveur :';
+ }
+
+ @override
+ String get hostKeyCheck =>
+ 'Pour en être sûr, cette commande sur le serveur affiche la même empreinte :';
+
+ @override
+ String get hostKeyAccept => 'Accepter';
+
+ @override
+ String get hostKeyReject => 'Refuser';
+
+ @override
+ String get hostKeyKnown => 'Acceptée';
+
+ @override
+ String get hostKeyNow => 'Maintenant';
+
+ @override
+ String get hostKeyForget => 'Oublier l’ancienne clé';
+
+ @override
+ String get hostKeyForgetTitle => 'Oublier l’ancienne clé ?';
+
+ @override
+ String get hostKeyForgetMessage =>
+ 'Ne le faites que si vous savez pourquoi la clé a changé, par exemple parce que vous avez réinstallé le serveur. ServerDeck vous demandera alors d’accepter la nouvelle clé.';
+
+ @override
+ String get statsFailedTitle => 'Impossible de lire le serveur';
+
+ @override
+ String get statsFailed =>
+ 'La réponse du serveur n’avait pas de sens. Les statistiques nécessitent Linux (/proc).';
+
+ @override
+ String get statsStale =>
+ 'La dernière actualisation a échoué. Les valeurs ci-dessus sont plus anciennes.';
+
+ @override
+ String get statCpu => 'CPU';
+
+ @override
+ String get statMemory => 'Mémoire';
+
+ @override
+ String get statDisk => 'Disque';
+
+ @override
+ String statCores(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: '$count cœurs',
+ one: '1 cœur',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String get statHistory => '3 dernières minutes';
+
+ @override
+ String get statHistoryHint =>
+ 'Actualisé toutes les 3 secondes tant que cet écran est ouvert.';
+
+ @override
+ String get statNetIn => 'Entrant';
+
+ @override
+ String get statNetOut => 'Sortant';
+
+ @override
+ String get statLoad => 'Charge';
+
+ @override
+ String get statLoad1 => '1 min';
+
+ @override
+ String get statLoad5 => '5 min';
+
+ @override
+ String get statLoad15 => '15 min';
+
+ @override
+ String get statDisks => 'Disques';
+
+ @override
+ String get statSwap => 'Swap';
+
+ @override
+ String get statUptime => 'En marche';
+
+ @override
+ String uptimeDays(int days, int hours) {
+ String _temp0 = intl.Intl.pluralLogic(
+ days,
+ locale: localeName,
+ other: '$days jours',
+ one: '1 jour',
+ );
+ return '$_temp0, $hours h';
+ }
+
+ @override
+ String uptimeHours(int hours, int minutes) {
+ return '$hours h $minutes min';
+ }
+
+ @override
+ String get tabOverview => 'Aperçu';
+
+ @override
+ String get tabServices => 'Services';
+
+ @override
+ String get servicesFailedTitle => 'Impossible de lister les services';
+
+ @override
+ String get servicesSearch => 'Rechercher par nom ou description';
+
+ @override
+ String servicesRunning(int count) {
+ return 'Actifs · $count';
+ }
+
+ @override
+ String servicesFailed(int count) {
+ return 'En échec · $count';
+ }
+
+ @override
+ String servicesAll(int count) {
+ return 'Tous · $count';
+ }
+
+ @override
+ String get servicesNone => 'Aucun résultat.';
+
+ @override
+ String get servicesNoneFailed => 'Aucun service en échec.';
+
+ @override
+ String get containersNone => 'Aucun conteneur.';
+
+ @override
+ String get stateRunning => 'actif';
+
+ @override
+ String get stateExited => 'terminé';
+
+ @override
+ String get stateFailed => 'en échec';
+
+ @override
+ String get stateActivating => 'démarrage';
+
+ @override
+ String get stateInactive => 'arrêté';
+
+ @override
+ String get stateUnhealthy => 'défaillant';
+
+ @override
+ String get statePaused => 'en pause';
+
+ @override
+ String get stateRestarting => 'redémarrage';
+
+ @override
+ String get stateExitedContainer => 'terminé';
+
+ @override
+ String get stateCreated => 'créé';
+
+ @override
+ String get actionRestart => 'Redémarrer';
+
+ @override
+ String get actionReload => 'Recharger';
+
+ @override
+ String get actionStop => 'Arrêter';
+
+ @override
+ String get actionStart => 'Démarrer';
+
+ @override
+ String actionConfirmTitle(String action, String subject) {
+ return '$action $subject ?';
+ }
+
+ @override
+ String actionConfirmMessage(String command) {
+ return 'Ceci s’exécute sur le serveur :\n$command';
+ }
+
+ @override
+ String actionDone(String action, String subject) {
+ return '$action : terminé pour $subject';
+ }
+
+ @override
+ String actionFailed(int code, String reason) {
+ return 'Cela n’a pas fonctionné (code de sortie $code). $reason';
+ }
+
+ @override
+ String get actionNeedsSudo =>
+ 'Il faut des droits d’administrateur. Connectez-vous en root, ou autorisez sudo sans mot de passe pour cette commande.';
+
+ @override
+ String get tabLogs => 'Journaux';
+
+ @override
+ String get logsJournal => 'Journal système';
+
+ @override
+ String get logsAdd => 'Source';
+
+ @override
+ String get logsAddTitle => 'Nouvelle source de journal';
+
+ @override
+ String get logsKindUnit => 'Service';
+
+ @override
+ String get logsKindFile => 'Fichier';
+
+ @override
+ String get logsUnitLabel => 'Nom du service';
+
+ @override
+ String get logsFileLabel => 'Chemin du fichier';
+
+ @override
+ String get logsFilter => 'Filtrer';
+
+ @override
+ String get logsErrorsOnly => 'Erreurs uniquement';
+
+ @override
+ String get logsPause => 'Pause';
+
+ @override
+ String get logsResume => 'Reprendre';
+
+ @override
+ String get logsClear => 'Effacer l’écran';
+
+ @override
+ String logsNewLines(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: '$count nouvelles lignes',
+ one: '1 nouvelle ligne',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String logsEnded(int code) {
+ return 'Le suivi du journal s’est arrêté (code de sortie $code).';
+ }
+
+ @override
+ String logsDeleteTitle(String name) {
+ return 'Supprimer $name ?';
+ }
+
+ @override
+ String get logsDeleteMessage =>
+ 'Seule la source enregistrée est supprimée, pas le journal sur le serveur.';
+
+ @override
+ String get tabCommands => 'Commandes';
+
+ @override
+ String get commandAdd => 'Nouvelle commande';
+
+ @override
+ String get commandEdit => 'Modifier la commande';
+
+ @override
+ String get commandQuick => 'Commande ponctuelle…';
+
+ @override
+ String get commandRun => 'Exécuter';
+
+ @override
+ String commandRunTitle(String name) {
+ return 'Exécuter $name ?';
+ }
+
+ @override
+ String get commandSaved => 'Commandes enregistrées';
+
+ @override
+ String get commandTemplates => 'Pour commencer';
+
+ @override
+ String get commandTemplatesHint =>
+ 'Touchez-en une pour l’ajouter à vos commandes enregistrées. Elles ne font que lire.';
+
+ @override
+ String get commandLabel => 'Commande';
+
+ @override
+ String get commandEverywhere => 'Sur tous les serveurs';
+
+ @override
+ String get commandEverywhereHint => 'Apparaît pour tous vos serveurs';
+
+ @override
+ String get commandConfirm => 'Demander avant d’exécuter';
+
+ @override
+ String get commandCopy => 'Copier la sortie';
+
+ @override
+ String get commandCopied => 'Sortie copiée';
+
+ @override
+ String get commandError => 'erreur';
+
+ @override
+ String get commandStopped => 'arrêtée';
+
+ @override
+ String commandExit(int code) {
+ return 'sortie $code';
+ }
+
+ @override
+ String get commandRunning => 'en cours';
+
+ @override
+ String get commandNoOutput => 'Aucune sortie.';
+
+ @override
+ String get tplDisk => 'Espace disque';
+
+ @override
+ String get tplFolders => 'Plus gros dossiers';
+
+ @override
+ String get tplUpdates => 'Mises à jour en attente';
+
+ @override
+ String get tplWho => 'Qui est connecté';
+
+ @override
+ String get tplApache => 'Test de la config Apache';
+
+ @override
+ String get tplNginx => 'Test de la config Nginx';
+
+ @override
+ String get tplTop => 'Top CPU';
+
+ @override
+ String get tplPorts => 'Ports en écoute';
+
+ @override
+ String get tabTerminal => 'Terminal';
+
+ @override
+ String get terminalClosed => '[shell fermé]';
+
+ @override
+ String get terminalReopen => 'Nouveau shell';
+
+ @override
+ String get lockReason => 'Déverrouiller ServerDeck';
+
+ @override
+ String get lockTitle => 'ServerDeck est verrouillé';
+
+ @override
+ String get lockUnlock => 'Déverrouiller';
+
+ @override
+ String get settingsTitle => 'Paramètres';
+
+ @override
+ String get settingsSecurity => 'Sécurité';
+
+ @override
+ String get settingsLock => 'Verrouillage de l’app';
+
+ @override
+ String get settingsLockHint =>
+ 'Empreinte, visage ou code PIN du téléphone au démarrage et après une minute en arrière-plan.';
+
+ @override
+ String get settingsLockUnsupported =>
+ 'Configurez d’abord un verrouillage d’écran sur le téléphone.';
+
+ @override
+ String get settingsKnownHosts => 'Clés de serveur connues';
+
+ @override
+ String get settingsKnownHostsEmpty => 'Aucune clé de serveur acceptée.';
+
+ @override
+ String get settingsKnownHostsHint =>
+ 'Clés d’identité acceptées au premier contact. Quand la clé d’un serveur change, ServerDeck ne se connecte plus tant que vous n’oubliez pas l’ancienne ici.';
+
+ @override
+ String get settingsAbout => 'À propos';
+
+ @override
+ String get settingsAboutText =>
+ 'Gestionnaire de serveurs SSH. Open source, licence MIT. Ne collecte rien ; tout reste sur le téléphone.';
+
+ @override
+ String get installKey => 'Installer la clé avec un mot de passe';
+
+ @override
+ String get installKeyAction => 'Installer';
+
+ @override
+ String installKeyHint(String user) {
+ return 'ServerDeck se connecte une fois avec le mot de passe de $user et ajoute la partie publique de la clé à ~/.ssh/authorized_keys. Le mot de passe n’est pas conservé.';
+ }
+
+ @override
+ String get installKeyDone =>
+ 'La clé est sur le serveur. Connexion avec la clé…';
+
+ @override
+ String get sessionError => 'erreur';
+
+ @override
+ String get themeTitle => 'Apparence';
+
+ @override
+ String get themeSystem => 'Système';
+
+ @override
+ String get themeLight => 'Clair';
+
+ @override
+ String get themeDark => 'Sombre';
+
+ @override
+ String get languageTitle => 'Langue';
+
+ @override
+ String get languageSystem => 'Langue du téléphone';
+
+ @override
+ String monitorDownTitle(String name) {
+ return '$name est hors service';
+ }
+
+ @override
+ String monitorUpTitle(String name) {
+ return '$name est de nouveau en ligne';
+ }
+
+ @override
+ String get monitorUpBody => 'La vérification fonctionne de nouveau.';
+
+ @override
+ String monitorUpBodyFor(String duration) {
+ return 'Il a été hors service pendant $duration.';
+ }
+
+ @override
+ String monitorHostKeyTitle(String name) {
+ return '$name : la clé du serveur a changé';
+ }
+
+ @override
+ String get monitorHostKeyBody =>
+ 'ServerDeck ne s’est pas connecté. Examinez le serveur avant d’accepter la nouvelle clé.';
+
+ @override
+ String get monitorProblemUnreachable => 'Impossible d’ouvrir une connexion.';
+
+ @override
+ String get monitorProblemTimeout => 'Pas de réponse à temps.';
+
+ @override
+ String get monitorProblemAuth =>
+ 'Il répond, mais n’a pas accepté l’identification.';
+
+ @override
+ String get monitorProblemDisconnected => 'Il a coupé avant l’identification.';
+
+ @override
+ String get monitorProblemHostKeyChanged => 'Sa clé a changé.';
+
+ @override
+ String get monitorProblemHostKeyUnknown =>
+ 'Sa clé n’est pas encore acceptée : ouvrez-le une fois dans l’app.';
+
+ @override
+ String get monitorProblemMissing => 'Il n’a ni clé ni mot de passe.';
+
+ @override
+ String get monitorProblemOther => 'La vérification a échoué.';
+
+ @override
+ String durationSeconds(int n) {
+ String _temp0 = intl.Intl.pluralLogic(
+ n,
+ locale: localeName,
+ other: '$n secondes',
+ one: '1 seconde',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String durationMinutes(int n) {
+ String _temp0 = intl.Intl.pluralLogic(
+ n,
+ locale: localeName,
+ other: '$n minutes',
+ one: '1 minute',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String durationHours(int h, int m) {
+ return '$h h $m min';
+ }
+
+ @override
+ String durationDays(int d, int h) {
+ String _temp0 = intl.Intl.pluralLogic(
+ d,
+ locale: localeName,
+ other: '$d jours',
+ one: '1 jour',
+ );
+ return '$_temp0 $h h';
+ }
+
+ @override
+ String get monitorSection => 'Surveillance en arrière-plan';
+
+ @override
+ String get monitorEnable => 'Surveiller en arrière-plan';
+
+ @override
+ String get monitorEnableHint =>
+ 'Soyez prévenu quand il tombe et quand il revient.';
+
+ @override
+ String get monitorEvery => 'Fréquence';
+
+ @override
+ String get monitorHow => 'Méthode';
+
+ @override
+ String get monitorModeSsh => 'Connexion';
+
+ @override
+ String get monitorModePort => 'Port seul';
+
+ @override
+ String get monitorModeSshHint =>
+ 'Se connecte en SSH et exécute une commande vide. Cela vérifie aussi que la clé fonctionne toujours, et fail2ban ne le compte jamais comme une attaque.';
+
+ @override
+ String get monitorModePortHint =>
+ 'Ouvre et ferme seulement le port. Plus rapide, mais un fail2ban strict peut compter une connexion sans identification.';
+
+ @override
+ String everyMinutes(int n) {
+ return '$n min';
+ }
+
+ @override
+ String everyHours(int n) {
+ String _temp0 = intl.Intl.pluralLogic(
+ n,
+ locale: localeName,
+ other: '$n heures',
+ one: '1 heure',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String get monitorTitle => 'Surveillance';
+
+ @override
+ String get monitorClear => 'Effacer le journal';
+
+ @override
+ String get monitorClearTitle => 'Effacer le journal de surveillance ?';
+
+ @override
+ String get monitorClearMessage =>
+ 'Les vérifications et les pannes sont supprimées. La surveillance continue.';
+
+ @override
+ String get monitorEmptyTitle => 'Aucun serveur surveillé';
+
+ @override
+ String get monitorEmpty =>
+ 'Activez la surveillance en arrière-plan en modifiant un serveur. ServerDeck le vérifie en arrière-plan et vous prévient quand il tombe.';
+
+ @override
+ String get monitorServers => 'Serveurs';
+
+ @override
+ String get monitorIncidents => 'Pannes';
+
+ @override
+ String get monitorNoIncidents => 'Aucune panne.';
+
+ @override
+ String get monitorTimingHint =>
+ 'Android exécute les tâches en arrière-plan au plus toutes les 15 minutes, et seulement avec un réseau. L’économiseur de batterie ou la veille profonde peuvent les retarder ; si des vérifications manquent, retirez ServerDeck de l’optimisation de la batterie.';
+
+ @override
+ String get agoNow => 'à l’instant';
+
+ @override
+ String agoMinutes(int n) {
+ return 'il y a $n min';
+ }
+
+ @override
+ String agoHours(int n) {
+ return 'il y a $n h';
+ }
+
+ @override
+ String get monitorWaiting => 'Pas encore vérifié';
+
+ @override
+ String monitorSomeDown(int n) {
+ String _temp0 = intl.Intl.pluralLogic(
+ n,
+ locale: localeName,
+ other: '$n serveurs sont hors service',
+ one: '1 serveur est hors service',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String get monitorAllUp => 'Tous les serveurs sont en ligne';
+
+ @override
+ String monitorCount(int n) {
+ String _temp0 = intl.Intl.pluralLogic(
+ n,
+ locale: localeName,
+ other: '$n serveurs surveillés',
+ one: '1 serveur surveillé',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String monitorLast(String when) {
+ return 'dernière : $when';
+ }
+
+ @override
+ String get monitorChecking => 'Vérification…';
+
+ @override
+ String get monitorCheckNow => 'Vérifier maintenant';
+
+ @override
+ String get monitorChecked => 'Vérifié';
+
+ @override
+ String get monitorStateUnknown => 'pas encore de données';
+
+ @override
+ String monitorDownFor(String d) {
+ return 'hors service · $d';
+ }
+
+ @override
+ String get window24h => '24 heures';
+
+ @override
+ String get window7d => '7 jours';
+
+ @override
+ String get window30d => '30 jours';
+
+ @override
+ String shortMinutes(int n) {
+ return '$n min';
+ }
+
+ @override
+ String shortHours(int n) {
+ return '$n h';
+ }
+
+ @override
+ String shortDays(int n) {
+ return '$n j';
+ }
+
+ @override
+ String get monitorOngoing => 'en cours';
+
+ @override
+ String everyNMinutes(int n) {
+ return 'toutes les $n min';
+ }
+
+ @override
+ String everyNHours(int n) {
+ String _temp0 = intl.Intl.pluralLogic(
+ n,
+ locale: localeName,
+ other: 'toutes les $n h',
+ one: 'toutes les heures',
+ );
+ return '$_temp0';
+ }
+
+ @override
+ String get monitorPhoneOffline =>
+ 'Le téléphone n’est pas connecté à internet, rien ne peut donc être vérifié maintenant.';
+
+ @override
+ String get monitorForgetTitle => 'Supprimer cette panne ?';
+
+ @override
+ String get monitorForgetMessage =>
+ 'Si ce n’était pas une vraie panne (le téléphone était hors ligne, par exemple), l’entrée et ses vérifications échouées sont supprimées, et la disponibilité est recalculée.';
+
+ @override
+ String get monitorIncidentsHint =>
+ 'Appuyez longuement sur une panne terminée pour la supprimer.';
+}
diff --git a/lib/l10n/generated/app_localizations_hu.dart b/lib/l10n/generated/app_localizations_hu.dart
index eaceca7..1528514 100644
--- a/lib/l10n/generated/app_localizations_hu.dart
+++ b/lib/l10n/generated/app_localizations_hu.dart
@@ -694,6 +694,12 @@ class AppLocalizationsHu extends AppLocalizations {
@override
String get themeDark => 'Sötét';
+ @override
+ String get languageTitle => 'Nyelv';
+
+ @override
+ String get languageSystem => 'A telefon nyelve';
+
@override
String monitorDownTitle(String name) {
return '$name nem elérhető';
diff --git a/lib/l10n/languages.dart b/lib/l10n/languages.dart
new file mode 100644
index 0000000..a8fa939
--- /dev/null
+++ b/lib/l10n/languages.dart
@@ -0,0 +1,19 @@
+import 'dart:ui';
+
+import 'generated/app_localizations.dart';
+
+/// The languages the app is translated to, each named in itself, so anyone
+/// can find their own in the list whatever language the app is in now.
+const languageNames = {
+ 'hu': 'Magyar',
+ 'en': 'English',
+ 'de': 'Deutsch',
+ 'es': 'Español',
+ 'fr': 'Français',
+};
+
+/// The locale to show the app in: the chosen [language], or null to follow
+/// the phone (also when the choice is not a translation any more).
+Locale? chosenLocale(String? language) => AppLocalizations.supportedLocales
+ .where((l) => l.languageCode == language)
+ .firstOrNull;
diff --git a/lib/monitor/background.dart b/lib/monitor/background.dart
index 6020056..d4c9579 100644
--- a/lib/monitor/background.dart
+++ b/lib/monitor/background.dart
@@ -70,7 +70,7 @@ Future<(MonitorLog, List)> runMonitorOnce({
},
);
await file.save(next);
- await notifyEvents(events);
+ await notifyEvents(events, language: data.settings.language);
return (next, events);
}
diff --git a/lib/monitor/notify.dart b/lib/monitor/notify.dart
index a363ade..bff62cb 100644
--- a/lib/monitor/notify.dart
+++ b/lib/monitor/notify.dart
@@ -4,6 +4,7 @@ import 'dart:ui';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import '../l10n/generated/app_localizations.dart';
+import '../l10n/languages.dart';
import 'checker.dart';
final _plugin = FlutterLocalNotificationsPlugin();
@@ -57,14 +58,13 @@ Future requestNotificationPermission() async {
return false;
}
-/// The phone's language, or English: the background has no widget tree to
-/// ask.
-AppLocalizations _strings() {
+/// The language chosen in the app, else the phone's, else English: the
+/// background has no widget tree to ask.
+AppLocalizations _strings(String? language) {
final code = Platform.localeName.split(RegExp('[_-]')).first;
- final supported = AppLocalizations.supportedLocales.map(
- (l) => l.languageCode,
+ return lookupAppLocalizations(
+ chosenLocale(language) ?? chosenLocale(code) ?? const Locale('en'),
);
- return lookupAppLocalizations(Locale(supported.contains(code) ? code : 'en'));
}
String describeProblem(AppLocalizations l, String? problem) =>
@@ -89,10 +89,11 @@ String describeDuration(AppLocalizations l, Duration d) {
/// One notification per server, so "back up" replaces "down".
int _idFor(String serverId) => serverId.hashCode & 0x7fffffff;
-Future notifyEvents(List events) async {
+/// [language] is the one chosen in the app's settings, if any.
+Future notifyEvents(List events, {String? language}) async {
if (events.isEmpty) return;
await initNotifications();
- final l = _strings();
+ final l = _strings(language);
for (final e in events) {
final name = e.server.name;
final (title, body) = switch (e.kind) {
diff --git a/lib/ui/settings_screen.dart b/lib/ui/settings_screen.dart
index 00dd4ad..52101bb 100644
--- a/lib/ui/settings_screen.dart
+++ b/lib/ui/settings_screen.dart
@@ -5,6 +5,7 @@ import 'package:package_info_plus/package_info_plus.dart';
import '../data/app_data.dart';
import '../data/models.dart';
+import '../l10n/languages.dart';
import 'app_lock.dart';
import 'theme_toggle.dart';
import 'theme.dart';
@@ -53,6 +54,22 @@ class SettingsScreen extends ConsumerWidget {
.read(appDataProvider.notifier)
.updateSettings(data.settings.copyWith(themeMode: s.first)),
),
+ SectionLabel(l.languageTitle),
+ DropdownButtonFormField(
+ initialValue: chosenLocale(data.settings.language)?.languageCode,
+ decoration: const InputDecoration(
+ prefixIcon: Icon(Icons.translate_rounded),
+ ),
+ items: [
+ DropdownMenuItem(value: null, child: Text(l.languageSystem)),
+ for (final MapEntry(key: code, value: name)
+ in languageNames.entries)
+ DropdownMenuItem(value: code, child: Text(name)),
+ ],
+ onChanged: (code) => ref
+ .read(appDataProvider.notifier)
+ .updateSettings(data.settings.copyWith(language: () => code)),
+ ),
SectionLabel(l.settingsSecurity),
Card(
child: SwitchListTile(
diff --git a/pubspec.yaml b/pubspec.yaml
index 35c1484..943aa33 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -2,7 +2,7 @@ name: serverdeck
description: "Manage your Linux servers from your phone over SSH."
publish_to: 'none'
-version: 1.1.2+4
+version: 1.2.0+5
environment:
sdk: ^3.13.4
diff --git a/test/app_data_test.dart b/test/app_data_test.dart
index f55a6f6..30fe560 100644
--- a/test/app_data_test.dart
+++ b/test/app_data_test.dart
@@ -116,7 +116,7 @@ void main() {
await notifier().saveServer(web.copyWith(port: 2222));
await notifier().addKey('k', generateEd25519('t'));
await notifier().updateSettings(
- const Settings(appLock: true, themeMode: ThemeMode.dark),
+ const Settings(appLock: true, themeMode: ThemeMode.dark, language: 'de'),
);
final back = AppData.fromJson(file.data.toJson());
expect(back.servers.single.port, 2222);
@@ -125,6 +125,9 @@ void main() {
expect(back.keys.single.name, 'k');
expect(back.settings.appLock, isTrue);
expect(back.settings.themeMode, ThemeMode.dark);
+ expect(back.settings.language, 'de');
expect(Settings.fromJson(const {}).themeMode, ThemeMode.system);
+ expect(Settings.fromJson(const {}).language, isNull);
+ expect(back.settings.copyWith(language: () => null).language, isNull);
});
}
diff --git a/test/app_test.dart b/test/app_test.dart
index 5e63ef6..37bfacc 100644
--- a/test/app_test.dart
+++ b/test/app_test.dart
@@ -1,7 +1,9 @@
+import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:serverdeck/app.dart';
import 'package:serverdeck/data/app_data.dart';
+import 'package:serverdeck/data/models.dart';
import 'package:serverdeck/data/storage.dart';
void main() {
@@ -20,4 +22,44 @@ void main() {
expect(find.text('Servers'), findsWidgets);
expect(find.text('No servers yet'), findsOneWidget);
});
+
+ testWidgets('shows the language picked in the settings', (tester) async {
+ await tester.pumpWidget(
+ ProviderScope(
+ overrides: [
+ dataFileProvider.overrideWithValue(
+ MemoryDataFile(const AppData(settings: Settings(language: 'fr'))),
+ ),
+ secretStoreProvider.overrideWithValue(MemorySecretStore()),
+ ],
+ child: const ServerDeckApp(),
+ ),
+ );
+ await tester.pumpAndSettle();
+ expect(find.text('Aucun serveur'), findsOneWidget);
+ });
+
+ testWidgets('a language picked in the settings is used and saved', (
+ tester,
+ ) async {
+ final file = MemoryDataFile();
+ await tester.pumpWidget(
+ ProviderScope(
+ overrides: [
+ dataFileProvider.overrideWithValue(file),
+ secretStoreProvider.overrideWithValue(MemorySecretStore()),
+ ],
+ child: const ServerDeckApp(),
+ ),
+ );
+ await tester.pumpAndSettle();
+ await tester.tap(find.byIcon(Icons.settings_rounded));
+ await tester.pumpAndSettle();
+ await tester.tap(find.text("The phone's language"));
+ await tester.pumpAndSettle();
+ await tester.tap(find.text('Deutsch').last);
+ await tester.pumpAndSettle();
+ expect(find.text('Einstellungen'), findsOneWidget);
+ expect(file.data.settings.language, 'de');
+ });
}