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'); + }); }