Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions android/app/src/main/res/xml/locales_config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="hu" />
<locale android:name="en" />
<locale android:name="de" />
<locale android:name="es" />
<locale android:name="fr" />
</locale-config>
3 changes: 3 additions & 0 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
<array>
<string>hu</string>
<string>en</string>
<string>de</string>
<string>es</string>
<string>fr</string>
</array>
<key>CFBundleDisplayName</key>
<string>ServerDeck</string>
Expand Down
5 changes: 5 additions & 0 deletions lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down
19 changes: 17 additions & 2 deletions lib/data/models.dart
Original file line number Diff line number Diff line change
Expand Up @@ -293,28 +293,43 @@ 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;

/// 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<String, Object?> toJson() => {
'appLock': appLock,
'themeMode': themeMode.name,
'language': language,
};

factory Settings.fromJson(Map<String, Object?> json) => Settings(
appLock: json['appLock'] as bool? ?? false,
themeMode:
ThemeMode.values.asNameMap()[json['themeMode']] ?? ThemeMode.system,
language: json['language'] as String?,
);
}

Expand Down
274 changes: 274 additions & 0 deletions lib/l10n/app_de.arb
Original file line number Diff line number Diff line change
@@ -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."
}
4 changes: 3 additions & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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."
}
}
Loading
Loading