M16: Verbinden-Tab zweisprachig (DE/EN)

ConnectView.swift auf L10n.t(...) umgestellt: Verbindungsformular, Status,
Geräte-/Routerboard-Infos, Update-/Firmware-Dialoge, Zertifikats-/SSH-
Warnungen. Dialog-Helper-Funktionen bekommen appLanguage-Parameter (liegen
außerhalb der View-Struct). Nebenbei fehlenden "Passwort"-Label-Eintrag im
Experte-Katalog nachgetragen. L10n.swift jetzt 444 Einträge, keine
Duplikate. 60 Tests grün, live bestätigt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDmUd93KxsYGr2kLTotWnG
This commit is contained in:
Kay
2026-09-15 16:18:02 +02:00
co-authored by Claude Sonnet 5
parent 7af550ae85
commit b6bb4bc289
6 changed files with 241 additions and 104 deletions
+18
View File
@@ -855,3 +855,21 @@ Test, WLAN/Bonding/PPPoE-Live-Tests).
Duplikate geprüft). 60 Tests grün, live bestätigt ("funktioniert").
Damit ist der komplette Experte-Tab zweisprachig. HANDOFF.md/
README.md/Manual.md aktualisiert, Commit + Push.
- "mach den Verbinden-Tab zweisprachig" → `ConnectView.swift`
durchgearbeitet: Verbindungsformular, Verbindungsstatus, Geräte-/
Routerboard-Infos, Software-/Firmware-Update-Bereich samt aller
Bestätigungsdialoge, Zertifikats-/SSH-Hostkey-Warnungen. Mehrere
Nachrichten mit eingebetteten Laufzeitwerten (Fingerabdruck, Version,
Datum) in übersetzte statische Textteile + unverändert eingefügten
Wert zerlegt, statt einen Dictionary-Key mit Platzhalter zu bauen. Die
vier `View`-Extension-Hilfsfunktionen für die Dialoge bekamen dafür
einen zusätzlichen `appLanguage`-Parameter (liegen außerhalb der
View-Struct, kein direkter `@AppStorage`-Zugriff). Dabei eine Lücke im
bereits als "komplett" gemeldeten Experte-Tab-Katalog gefunden und
geschlossen: Feld-Label "Passwort" (in fünf Schemas verwendet) hatte
keinen eigenen Übersetzungseintrag. `L10n.swift` jetzt 444 Einträge,
keine Duplikate (Python-Skript-Check). 60 Tests grün, live bestätigt
("sieht gut aus" / "sieht gut aus"). Bewusst nicht übersetzt:
Backend-/Laufzeit-Text von RouterOS oder app-eigenen Fehlerpfaden
(technische/englische Ausgaben, keine kuratierten UI-Strings).
HANDOFF.md/README.md/Manual.md aktualisiert, Commit + Push.
+34 -9
View File
@@ -1038,19 +1038,44 @@ Unit-Tests grün (inkl. neuer `ExpertViewModelTests` und
zweiten Eintrag entfernt (dieselbe Übersetzung greift ohnehin für
beide Stellen). Build + 60 Tests grün, **vom Nutzer live in Xcode
bestätigt** ("funktioniert"). Damit ist der komplette Experte-Tab
zweisprachig; offen bleiben die übrigen fünf Tabs (Verbinden/
Einrichten/Übersicht/Geräte/Sicherungen), die noch auf festen
deutschen String-Literalen laufen.
zweisprachig. Direkt danach denselben Mechanismus auf den Verbinden-
Tab (`ConnectView.swift`) ausgerollt: Verbindungsformular,
Verbindungsstatus (inkl. dynamischem "Verbunden (REST-API/SSH)"),
Geräte-/Routerboard-Infos, Software-/Firmware-Update-Bereich samt
aller Bestätigungsdialoge, Zertifikats-/SSH-Hostkey-TOFU-Warnungen.
Da `ConnectView` mehrere Nachrichten mit eingebetteten Laufzeitwerten
baut (Fingerabdruck, Router-Version, Datum der letzten Sicherung),
wurden diese in übersetzte statische Textteile + unverändert
eingefügten dynamischen Wert zerlegt statt als ein Dictionary-Key mit
Platzhalter (z.B. `L10n.t("Fingerabdruck:", appLanguage) +
" \(fingerprint)"`). Die vier `View`-Extension-Hilfsfunktionen
(`withUpdateDialogs`/`withSoftwareUpdateDialogs`/
`withFirmwareUpgradeDialogs`/`withRebootDialogs`) bekamen dafür einen
zusätzlichen `appLanguage: String`-Parameter, da sie außerhalb der
View-Struct liegen und nicht direkt auf `@AppStorage` zugreifen
können. Nebenbei eine Lücke im Experte-Tab-Katalog geschlossen: das
Feld-Label "Passwort" (in fünf verschiedenen Schemas verwendet) hatte
bisher keinen eigenen Übersetzungseintrag. `L10n.swift`s
`translations`-Dictionary jetzt 444 Einträge, auf Duplikate per
Python-Skript geprüft (keine gefunden). Build + 60 Tests grün, **vom
Nutzer live in Xcode bestätigt** ("sieht gut aus"). Bewusst nicht
übersetzt: Laufzeit-/Backend-Text, der direkt von RouterOS oder dem
App-eigenen Fehler-/Ergebnis-Pfad kommt (z.B. `update.status`,
`firmwareUpgradeResultMessage`, alle `viewModel.*Error`-Alerts) — das
sind technische/englische RouterOS-Ausgaben bzw. app-generierte
Fehlertexte, keine kuratierten UI-Strings; offen bleiben die übrigen
vier Tabs (Einrichten/Übersicht/Geräte/Sicherungen), die noch auf
festen deutschen String-Literalen laufen.
## Nächste Schritte
1. ~~M16: restliche `RouterOSSchemaCatalog.swift`-Sektionen übersetzen~~
— erledigt, kompletter Experte-Tab (alle 9 Menü-Familien) jetzt
zweisprachig, live bestätigt. Offen: dieselbe `L10n.t(...)`-Umstellung
auf die übrigen Tabs (Verbinden/Einrichten/Geräte/Sicherungen/
Übersicht) ausrollen — dort läuft aktuell noch alles auf festen
deutschen String-Literalen ohne Sprachumschaltung, mit dem Nutzer
vereinbart Tab für Tab weiterzumachen.
und ~~Verbinden-Tab zweisprachig~~ — beides erledigt, live bestätigt.
Offen: dieselbe `L10n.t(...)`-Umstellung auf die übrigen vier Tabs
(Einrichten/Übersicht/Geräte/Sicherungen) ausrollen — dort läuft
aktuell noch alles auf festen deutschen String-Literalen ohne
Sprachumschaltung, mit dem Nutzer vereinbart Tab für Tab
weiterzumachen.
2. WLAN (M5) an einem Gerät mit echtem WLAN-Chip nachholen.
2. Rest von M7: Fehlerzustände/Politur, REST-Schreibtest an einem Gerät
mit aktivem `www-ssl`.
+6 -5
View File
@@ -43,8 +43,9 @@ automatisch gewählt, ohne dass du dich darum kümmern musst.
Ein DE/EN-Umschalt-Button in der Toolbar wechselt die Sprache der App
(persistiert über Neustarts). Aktuell vollständig übersetzt: alle
Tab-Namen sowie der komplette Experte-Tab (alle Menü-Bereiche, Buttons,
Tooltips). Weitere Tabs folgen schrittweise.
Tab-Namen, der komplette Experte-Tab (alle Menü-Bereiche, Buttons,
Tooltips) sowie der komplette Verbinden-Tab. Weitere Tabs folgen
schrittweise.
## 1. Verbinden
@@ -174,9 +175,9 @@ router over its REST API (preferred) or SSH (fallback) — chosen
automatically, no configuration needed.
A DE/EN toggle button in the toolbar switches the app's language
(persists across restarts). Fully translated so far: every tab name and
the entire Expert tab (all menu areas, buttons, tooltips). More tabs
will follow incrementally.
(persists across restarts). Fully translated so far: every tab name,
the entire Expert tab (all menu areas, buttons, tooltips), and the
entire Connect tab. More tabs will follow incrementally.
## 1. Connect
+2 -2
View File
@@ -53,8 +53,8 @@ Befehl, der ausgeführt wird — nichts passiert unangekündigt.
Modell-Abgleich als Brick-Schutz, wählbarer Speicherort, "Werks­ein­
stellungen wiederherstellen"-Notfallschalter.
- **Deutsch/Englisch** — Umschalt-Button in der Toolbar (persistiert über
Neustarts). Kompletter Experte-Tab vollständig übersetzt (alle 9
Menü-Familien, Formular-Chrome/Tooltips), weitere Tabs folgen
Neustarts). Vollständig übersetzt: kompletter Experte-Tab (alle 9
Menü-Familien) und der Verbinden-Tab; weitere Tabs folgen
schrittweise.
## Architektur
+86 -1
View File
@@ -528,6 +528,91 @@ enum L10n {
"Meist 587 (STARTTLS) oder 465 (SSL).": "Usually 587 (STARTTLS) or 465 (SSL).",
"Absenderadresse": "Sender Address",
"Z.B. router@example.com.": "E.g. router@example.com.",
"Zugangsdaten des Mailservers.": "Credentials for the mail server."
"Zugangsdaten des Mailservers.": "Credentials for the mail server.",
"Passwort": "Password",
// MARK: - Connect tab (ConnectView.swift)
"Verbindung": "Connection",
"IP-Adresse oder Hostname": "IP Address or Hostname",
"Die Adresse deines Routers im Netzwerk. Werkseinstellung bei Mikrotik ist meist 192.168.88.1.":
"Your router's address on the network. MikroTik's factory default is usually 192.168.88.1.",
"Der Admin-Benutzername deines Routers. Werkseinstellung ist meist \"admin\".":
"Your router's admin username. The factory default is usually \"admin\".",
"Das Passwort für diesen Benutzer. Bei unverändertem Werkszustand oft leer.":
"The password for this user. Often empty in an unchanged factory state.",
"Passwort merken": "Remember Password",
"Speichert das Passwort verschlüsselt in der macOS-Schlüsselbundverwaltung, damit du es nicht jedes Mal neu eingeben musst.":
"Stores the password encrypted in macOS Keychain so you don't have to type it in every time.",
"Verbinde…": "Connecting…",
"Verbunden": "Connected",
"Trennen": "Disconnect",
"Beendet die Verbindung zum Router. Zugangsdaten bleiben erhalten (falls gemerkt).":
"Ends the connection to the router. Credentials are kept (if remembered).",
"Sichere…": "Backing up…",
"Jetzt sichern": "Back Up Now",
"Sichert die aktuelle Router-Konfiguration — sinnvoll direkt nach dem Verbinden, bevor du im Einrichten-Tab etwas änderst.":
"Backs up the current router configuration — useful right after connecting, before you change anything in the Setup tab.",
"Zuletzt gesichert:": "Last backed up:",
"Unbekanntes Zertifikat": "Unknown Certificate",
"Vertrauen und verbinden": "Trust and Connect",
"Der Router hat sich mit einem unbekannten Zertifikat gemeldet.": "The router presented an unknown certificate.",
"Fingerabdruck:": "Fingerprint:",
"Nur bestätigen, wenn dies dein eigenes Gerät im lokalen Netzwerk ist.":
"Only confirm if this is your own device on the local network.",
"Unbekannter SSH-Schlüssel": "Unknown SSH Key",
"Der Router hat sich mit einem unbekannten SSH-Schlüssel gemeldet.": "The router presented an unknown SSH key.",
"Nur bestätigen, wenn dies dein eigenes Gerät im lokalen Netzwerk ist. Falls du diesen Router schon einmal verbunden hattest und sich der Fingerabdruck geändert hat, könnte das auf ein manipuliertes Netzwerk hindeuten — im Zweifel nicht bestätigen.":
"Only confirm if this is your own device on the local network. If you've connected to this router before and the fingerprint has changed, that could indicate a tampered network — when in doubt, don't confirm.",
"Sicherung fehlgeschlagen": "Backup Failed",
"Gerät": "Device",
"Modell": "Model",
"RouterOS-Version": "RouterOS Version",
"Architektur": "Architecture",
"Laufzeit": "Uptime",
"Modell (RB-Code)": "Model (RB code)",
"Seriennummer": "Serial Number",
"Firmware-Typ": "Firmware Type",
"Firmware (aktuell)": "Firmware (current)",
"Firmware (minimal)": "Firmware (minimum)",
"Firmware (verfügbar)": "Firmware (available)",
"Firmware-Update verfügbar": "Firmware Update Available",
"Aktualisiere…": "Updating…",
"Firmware aktualisieren": "Update Firmware",
"Firmware aktuell": "Firmware Up to Date",
"Starte neu…": "Restarting…",
"Jetzt neu starten": "Restart Now",
"Nötig, damit die neu geschriebene Firmware aktiv wird — passiert nicht automatisch.":
"Needed for the newly written firmware to take effect — doesn't happen automatically.",
"Software-Update": "Software Update",
"Kanal": "Channel",
"Installiert": "Installed",
"Neueste Version": "Latest Version",
"Installiere…": "Installing…",
"Update installieren": "Install Update",
"Prüfe…": "Checking…",
"Jetzt prüfen": "Check Now",
"Prüft bei MikroTik, ob eine neuere RouterOS-Version verfügbar ist. Braucht Internetzugang auf dem Router.":
"Checks with MikroTik whether a newer RouterOS version is available. Needs internet access on the router.",
"Update-Befehl gesendet — der Router lädt herunter, installiert und startet danach automatisch neu.":
"Update command sent — the router will download, install, and then reboot automatically.",
"Verbinde dich mit deinem Router, um Geräteinformationen zu sehen.":
"Connect to your router to see device information.",
"RouterOS-Update installieren?": "Install RouterOS Update?",
"Installieren": "Install",
"Lädt RouterOS": "Downloading RouterOS",
"die neueste Version": "the latest version",
" herunter, installiert es und startet den Router danach automatisch neu — die Verbindung geht dabei verloren. Vorher am besten eine Sicherung erstellen (\"Jetzt sichern\" oben).":
" and installs it, then automatically reboots the router — the connection will be lost. It's best to create a backup first (\"Back Up Now\" above).",
"Update-Prüfung fehlgeschlagen": "Update Check Failed",
"Update-Installation fehlgeschlagen": "Update Installation Failed",
"Routerboard-Firmware aktualisieren?": "Update Routerboard Firmware?",
"Schreibt die neue Bootloader-Firmware auf den Router. Danach ist ein manueller Neustart nötig — passiert nicht automatisch. Dieser Befehl fragt in einer interaktiven Sitzung normalerweise erst nach Bestätigung; ob das über diese App genauso abläuft, ist nicht abschließend getestet (ähnlich interaktive Befehle liefen bisher aber problemlos). Falls die App hier ungewöhnlich lange ohne Rückmeldung bleibt, bitte melden.":
"Writes the new bootloader firmware to the router. A manual reboot is then needed — it doesn't happen automatically. This command normally asks for confirmation in an interactive session; whether that's also the case over this app hasn't been conclusively tested (similarly interactive commands have gone smoothly so far). If the app hangs here unusually long without responding, please report it.",
"Firmware-Aktualisierung fehlgeschlagen": "Firmware Update Failed",
"Router jetzt neu starten?": "Restart Router Now?",
"Neu starten": "Restart",
"Startet den Router sofort neu. Die Verbindung geht dabei kurz verloren, danach im Tab \"Verbinden\" erneut verbinden.":
"Restarts the router immediately. The connection will briefly drop — reconnect in the \"Connect\" tab afterwards.",
"Neustart fehlgeschlagen": "Restart Failed"
]
}
@@ -7,6 +7,7 @@ struct ConnectView: View {
@State private var showUpdateInstallConfirmation = false
@State private var showFirmwareUpgradeConfirmation = false
@State private var showRebootConfirmation = false
@AppStorage("appLanguage") private var appLanguage: String = "de"
init(connectionService: ConnectionService) {
self.connectionService = connectionService
@@ -15,8 +16,8 @@ struct ConnectView: View {
var body: some View {
mainContent
.withUpdateDialogs(viewModel: viewModel, connectionService: connectionService, showUpdateInstallConfirmation: $showUpdateInstallConfirmation, showFirmwareUpgradeConfirmation: $showFirmwareUpgradeConfirmation)
.withRebootDialogs(viewModel: viewModel, connectionService: connectionService, isPresented: $showRebootConfirmation)
.withUpdateDialogs(viewModel: viewModel, connectionService: connectionService, appLanguage: appLanguage, showUpdateInstallConfirmation: $showUpdateInstallConfirmation, showFirmwareUpgradeConfirmation: $showFirmwareUpgradeConfirmation)
.withRebootDialogs(viewModel: viewModel, connectionService: connectionService, appLanguage: appLanguage, isPresented: $showRebootConfirmation)
}
/// Split out of `body` same reasoning as `BackupListView.restoreAwareContent` (Bug 19 in
@@ -25,19 +26,19 @@ struct ConnectView: View {
private var mainContent: some View {
NavigationSplitView {
Form {
Section("Verbindung") {
TextField("IP-Adresse oder Hostname", text: $viewModel.host)
.help("Die Adresse deines Routers im Netzwerk. Werkseinstellung bei Mikrotik ist meist 192.168.88.1.")
TextField("Benutzername", text: $viewModel.username)
.help("Der Admin-Benutzername deines Routers. Werkseinstellung ist meist \"admin\".")
SecureField("Passwort", text: $viewModel.password)
.help("Das Passwort für diesen Benutzer. Bei unverändertem Werkszustand oft leer.")
Toggle("Passwort merken", isOn: $viewModel.rememberPassword)
.help("Speichert das Passwort verschlüsselt in der macOS-Schlüsselbundverwaltung, damit du es nicht jedes Mal neu eingeben musst.")
Section(L10n.t("Verbindung", appLanguage)) {
TextField(L10n.t("IP-Adresse oder Hostname", appLanguage), text: $viewModel.host)
.help(L10n.t("Die Adresse deines Routers im Netzwerk. Werkseinstellung bei Mikrotik ist meist 192.168.88.1.", appLanguage))
TextField(L10n.t("Benutzername", appLanguage), text: $viewModel.username)
.help(L10n.t("Der Admin-Benutzername deines Routers. Werkseinstellung ist meist \"admin\".", appLanguage))
SecureField(L10n.t("Passwort", appLanguage), text: $viewModel.password)
.help(L10n.t("Das Passwort für diesen Benutzer. Bei unverändertem Werkszustand oft leer.", appLanguage))
Toggle(L10n.t("Passwort merken", appLanguage), isOn: $viewModel.rememberPassword)
.help(L10n.t("Speichert das Passwort verschlüsselt in der macOS-Schlüsselbundverwaltung, damit du es nicht jedes Mal neu eingeben musst.", appLanguage))
}
Section {
Button("Verbinden") {
Button(L10n.t("Verbinden", appLanguage)) {
viewModel.connect()
}
.disabled(viewModel.host.isEmpty || viewModel.username.isEmpty)
@@ -55,38 +56,42 @@ struct ConnectView: View {
backupViewModel.load()
}
.alert(
"Unbekanntes Zertifikat",
L10n.t("Unbekanntes Zertifikat", appLanguage),
isPresented: certificateAlertBinding,
presenting: certificateFingerprint
) { fingerprint in
Button("Vertrauen und verbinden") {
Button(L10n.t("Vertrauen und verbinden", appLanguage)) {
viewModel.trustAndRetry(fingerprint: fingerprint)
}
Button("Abbrechen", role: .cancel) {}
Button(L10n.t("Abbrechen", appLanguage), role: .cancel) {}
} message: { fingerprint in
Text("Der Router hat sich mit einem unbekannten Zertifikat gemeldet.\nFingerabdruck: \(fingerprint)\n\nNur bestätigen, wenn dies dein eigenes Gerät im lokalen Netzwerk ist.")
Text(L10n.t("Der Router hat sich mit einem unbekannten Zertifikat gemeldet.", appLanguage)
+ "\n" + L10n.t("Fingerabdruck:", appLanguage) + " \(fingerprint)\n\n"
+ L10n.t("Nur bestätigen, wenn dies dein eigenes Gerät im lokalen Netzwerk ist.", appLanguage))
}
.alert(
"Unbekannter SSH-Schlüssel",
L10n.t("Unbekannter SSH-Schlüssel", appLanguage),
isPresented: sshHostKeyAlertBinding,
presenting: sshHostKeyFingerprint
) { fingerprint in
Button("Vertrauen und verbinden") {
Button(L10n.t("Vertrauen und verbinden", appLanguage)) {
viewModel.trustSSHHostKeyAndRetry(fingerprint: fingerprint)
}
Button("Abbrechen", role: .cancel) {}
Button(L10n.t("Abbrechen", appLanguage), role: .cancel) {}
} message: { fingerprint in
Text("Der Router hat sich mit einem unbekannten SSH-Schlüssel gemeldet.\nFingerabdruck: \(fingerprint)\n\nNur bestätigen, wenn dies dein eigenes Gerät im lokalen Netzwerk ist. Falls du diesen Router schon einmal verbunden hattest und sich der Fingerabdruck geändert hat, könnte das auf ein manipuliertes Netzwerk hindeuten — im Zweifel nicht bestätigen.")
Text(L10n.t("Der Router hat sich mit einem unbekannten SSH-Schlüssel gemeldet.", appLanguage)
+ "\n" + L10n.t("Fingerabdruck:", appLanguage) + " \(fingerprint)\n\n"
+ L10n.t("Nur bestätigen, wenn dies dein eigenes Gerät im lokalen Netzwerk ist. Falls du diesen Router schon einmal verbunden hattest und sich der Fingerabdruck geändert hat, könnte das auf ein manipuliertes Netzwerk hindeuten — im Zweifel nicht bestätigen.", appLanguage))
}
.alert(
"Sicherung fehlgeschlagen",
L10n.t("Sicherung fehlgeschlagen", appLanguage),
isPresented: Binding(
get: { backupViewModel.errorMessage != nil },
set: { _ in backupViewModel.errorMessage = nil }
),
presenting: backupViewModel.errorMessage
) { _ in
Button("OK") {}
Button(L10n.t("OK", appLanguage)) {}
} message: { message in
Text(message)
}
@@ -129,18 +134,18 @@ struct ConnectView: View {
Section {
HStack {
ProgressView()
Text("Verbinde…")
Text(L10n.t("Verbinde…", appLanguage))
}
}
case .connected(let kind):
Section {
Label("Verbunden (\(kind == .rest ? "REST-API" : "SSH"))", systemImage: "checkmark.circle.fill")
Label(L10n.t("Verbunden", appLanguage) + " (\(kind == .rest ? "REST-API" : "SSH"))", systemImage: "checkmark.circle.fill")
.foregroundStyle(.green)
Button("Trennen", role: .destructive) {
Button(L10n.t("Trennen", appLanguage), role: .destructive) {
Task { await connectionService.disconnect() }
}
.help("Beendet die Verbindung zum Router. Zugangsdaten bleiben erhalten (falls gemerkt).")
.help(L10n.t("Beendet die Verbindung zum Router. Zugangsdaten bleiben erhalten (falls gemerkt).", appLanguage))
Button {
if let credentials = connectionService.credentials {
@@ -150,18 +155,18 @@ struct ConnectView: View {
if backupViewModel.isCreatingBackup {
HStack {
ProgressView()
Text("Sichere…")
Text(L10n.t("Sichere…", appLanguage))
}
} else {
Label("Jetzt sichern", systemImage: "square.and.arrow.down")
Label(L10n.t("Jetzt sichern", appLanguage), systemImage: "square.and.arrow.down")
}
}
.disabled(backupViewModel.isCreatingBackup)
.help("Sichert die aktuelle Router-Konfiguration — sinnvoll direkt nach dem Verbinden, bevor du im Einrichten-Tab etwas änderst.")
.help(L10n.t("Sichert die aktuelle Router-Konfiguration — sinnvoll direkt nach dem Verbinden, bevor du im Einrichten-Tab etwas änderst.", appLanguage))
if let lastBackup = backupViewModel.backups.first(where: { $0.host == connectionService.credentials?.host }) {
Label(
"Zuletzt gesichert: \(lastBackup.createdAt.formatted(date: .abbreviated, time: .standard))",
L10n.t("Zuletzt gesichert:", appLanguage) + " \(lastBackup.createdAt.formatted(date: .abbreviated, time: .standard))",
systemImage: "checkmark"
)
.font(.caption)
@@ -180,40 +185,40 @@ struct ConnectView: View {
private var deviceDetail: some View {
if let info = connectionService.deviceInfo {
Form {
Section("Gerät") {
LabeledContent("Modell", value: info.boardName)
LabeledContent("RouterOS-Version", value: info.routerOSVersion)
LabeledContent("Architektur", value: info.architecture)
LabeledContent("Laufzeit", value: info.uptime)
Section(L10n.t("Gerät", appLanguage)) {
LabeledContent(L10n.t("Modell", appLanguage), value: info.boardName)
LabeledContent(L10n.t("RouterOS-Version", appLanguage), value: info.routerOSVersion)
LabeledContent(L10n.t("Architektur", appLanguage), value: info.architecture)
LabeledContent(L10n.t("Laufzeit", appLanguage), value: info.uptime)
}
if let board = connectionService.routerBoardInfo {
Section("Routerboard") {
LabeledContent("Modell (RB-Code)", value: board.model)
LabeledContent("Revision", value: board.revision)
LabeledContent("Seriennummer", value: board.serialNumber)
LabeledContent(L10n.t("Modell (RB-Code)", appLanguage), value: board.model)
LabeledContent(L10n.t("Revision", appLanguage), value: board.revision)
LabeledContent(L10n.t("Seriennummer", appLanguage), value: board.serialNumber)
.textSelection(.enabled)
LabeledContent("Firmware-Typ", value: board.firmwareType)
LabeledContent("Firmware (aktuell)", value: board.currentFirmware)
LabeledContent("Firmware (minimal)", value: board.minimumFirmware)
LabeledContent("Firmware (verfügbar)", value: board.upgradeFirmware)
LabeledContent(L10n.t("Firmware-Typ", appLanguage), value: board.firmwareType)
LabeledContent(L10n.t("Firmware (aktuell)", appLanguage), value: board.currentFirmware)
LabeledContent(L10n.t("Firmware (minimal)", appLanguage), value: board.minimumFirmware)
LabeledContent(L10n.t("Firmware (verfügbar)", appLanguage), value: board.upgradeFirmware)
// Confirmed via docs: same value = up to date, different = a bootloader
// update is available (only ever becomes available after installing a
// newer RouterOS package first the firmware ships bundled inside it).
if board.currentFirmware != board.upgradeFirmware {
Label("Firmware-Update verfügbar", systemImage: "arrow.up.circle.fill")
Label(L10n.t("Firmware-Update verfügbar", appLanguage), systemImage: "arrow.up.circle.fill")
.foregroundStyle(.orange)
Button {
showFirmwareUpgradeConfirmation = true
} label: {
if viewModel.isUpgradingFirmware {
HStack { ProgressView(); Text("Aktualisiere…") }
HStack { ProgressView(); Text(L10n.t("Aktualisiere…", appLanguage)) }
} else {
Label("Firmware aktualisieren", systemImage: "arrow.up.circle")
Label(L10n.t("Firmware aktualisieren", appLanguage), systemImage: "arrow.up.circle")
}
}
.disabled(viewModel.isUpgradingFirmware)
} else {
Label("Firmware aktuell", systemImage: "checkmark.circle")
Label(L10n.t("Firmware aktuell", appLanguage), systemImage: "checkmark.circle")
.font(.caption)
.foregroundStyle(.secondary)
}
@@ -223,30 +228,30 @@ struct ConnectView: View {
showRebootConfirmation = true
} label: {
if viewModel.isRebooting {
HStack { ProgressView(); Text("Starte neu…") }
HStack { ProgressView(); Text(L10n.t("Starte neu…", appLanguage)) }
} else {
Label("Jetzt neu starten", systemImage: "arrow.clockwise")
Label(L10n.t("Jetzt neu starten", appLanguage), systemImage: "arrow.clockwise")
}
}
.disabled(viewModel.isRebooting)
.help("Nötig, damit die neu geschriebene Firmware aktiv wird — passiert nicht automatisch.")
.help(L10n.t("Nötig, damit die neu geschriebene Firmware aktiv wird — passiert nicht automatisch.", appLanguage))
}
}
}
Section("Software-Update") {
Section(L10n.t("Software-Update", appLanguage)) {
if let update = viewModel.packageUpdateInfo {
LabeledContent("Kanal", value: update.channel)
LabeledContent("Installiert", value: update.installedVersion)
LabeledContent("Neueste Version", value: update.latestVersion)
LabeledContent("Status", value: update.status)
LabeledContent(L10n.t("Kanal", appLanguage), value: update.channel)
LabeledContent(L10n.t("Installiert", appLanguage), value: update.installedVersion)
LabeledContent(L10n.t("Neueste Version", appLanguage), value: update.latestVersion)
LabeledContent(L10n.t("Status", appLanguage), value: update.status)
if update.status == "New version is available" {
Button {
showUpdateInstallConfirmation = true
} label: {
if viewModel.isInstallingUpdate {
HStack { ProgressView(); Text("Installiere…") }
HStack { ProgressView(); Text(L10n.t("Installiere…", appLanguage)) }
} else {
Label("Update installieren", systemImage: "arrow.down.circle")
Label(L10n.t("Update installieren", appLanguage), systemImage: "arrow.down.circle")
}
}
.disabled(viewModel.isInstallingUpdate)
@@ -258,17 +263,17 @@ struct ConnectView: View {
}
} label: {
if viewModel.isCheckingForUpdates {
HStack { ProgressView(); Text("Prüfe…") }
HStack { ProgressView(); Text(L10n.t("Prüfe…", appLanguage)) }
} else {
Label("Jetzt prüfen", systemImage: "arrow.triangle.2.circlepath")
Label(L10n.t("Jetzt prüfen", appLanguage), systemImage: "arrow.triangle.2.circlepath")
}
}
.disabled(viewModel.isCheckingForUpdates)
.help("Prüft bei MikroTik, ob eine neuere RouterOS-Version verfügbar ist. Braucht Internetzugang auf dem Router.")
.help(L10n.t("Prüft bei MikroTik, ob eine neuere RouterOS-Version verfügbar ist. Braucht Internetzugang auf dem Router.", appLanguage))
if viewModel.didSendUpdateInstall {
Label(
"Update-Befehl gesendet — der Router lädt herunter, installiert und startet danach automatisch neu.",
L10n.t("Update-Befehl gesendet — der Router lädt herunter, installiert und startet danach automatisch neu.", appLanguage),
systemImage: "arrow.clockwise"
)
.font(.caption)
@@ -291,9 +296,9 @@ struct ConnectView: View {
.formStyle(.grouped)
} else {
ContentUnavailableView(
"Nicht verbunden",
LocalizedStringKey(L10n.t("Nicht verbunden", appLanguage)),
systemImage: "network.slash",
description: Text("Verbinde dich mit deinem Router, um Geräteinformationen zu sehen.")
description: Text(L10n.t("Verbinde dich mit deinem Router, um Geräteinformationen zu sehen.", appLanguage))
)
}
}
@@ -306,29 +311,32 @@ private extension View {
func withUpdateDialogs(
viewModel: ConnectViewModel,
connectionService: ConnectionService,
appLanguage: String,
showUpdateInstallConfirmation: Binding<Bool>,
showFirmwareUpgradeConfirmation: Binding<Bool>
) -> some View {
self
.withSoftwareUpdateDialogs(viewModel: viewModel, connectionService: connectionService, isPresented: showUpdateInstallConfirmation)
.withFirmwareUpgradeDialogs(viewModel: viewModel, connectionService: connectionService, isPresented: showFirmwareUpgradeConfirmation)
.withSoftwareUpdateDialogs(viewModel: viewModel, connectionService: connectionService, appLanguage: appLanguage, isPresented: showUpdateInstallConfirmation)
.withFirmwareUpgradeDialogs(viewModel: viewModel, connectionService: connectionService, appLanguage: appLanguage, isPresented: showFirmwareUpgradeConfirmation)
}
func withSoftwareUpdateDialogs(viewModel: ConnectViewModel, connectionService: ConnectionService, isPresented: Binding<Bool>) -> some View {
func withSoftwareUpdateDialogs(viewModel: ConnectViewModel, connectionService: ConnectionService, appLanguage: String, isPresented: Binding<Bool>) -> some View {
self
.confirmationDialog(
"RouterOS-Update installieren?",
L10n.t("RouterOS-Update installieren?", appLanguage),
isPresented: isPresented,
titleVisibility: .visible
) {
Button("Installieren", role: .destructive) {
Button(L10n.t("Installieren", appLanguage), role: .destructive) {
if let credentials = connectionService.credentials {
viewModel.installUpdate(for: credentials)
}
}
Button("Abbrechen", role: .cancel) {}
Button(L10n.t("Abbrechen", appLanguage), role: .cancel) {}
} message: {
Text("Lädt RouterOS \(viewModel.packageUpdateInfo?.latestVersion ?? "die neueste Version") herunter, installiert es und startet den Router danach automatisch neu — die Verbindung geht dabei verloren. Vorher am besten eine Sicherung erstellen (\"Jetzt sichern\" oben).")
Text(L10n.t("Lädt RouterOS", appLanguage)
+ " \(viewModel.packageUpdateInfo?.latestVersion ?? L10n.t("die neueste Version", appLanguage))"
+ L10n.t(" herunter, installiert es und startet den Router danach automatisch neu — die Verbindung geht dabei verloren. Vorher am besten eine Sicherung erstellen (\"Jetzt sichern\" oben).", appLanguage))
}
.onChange(of: viewModel.didSendUpdateInstall) { _, didSend in
if didSend {
@@ -336,76 +344,76 @@ private extension View {
}
}
.alert(
"Update-Prüfung fehlgeschlagen",
L10n.t("Update-Prüfung fehlgeschlagen", appLanguage),
isPresented: Binding(
get: { viewModel.updateCheckError != nil },
set: { _ in viewModel.updateCheckError = nil }
),
presenting: viewModel.updateCheckError
) { _ in
Button("OK") {}
Button(L10n.t("OK", appLanguage)) {}
} message: { message in
Text(message)
}
.alert(
"Update-Installation fehlgeschlagen",
L10n.t("Update-Installation fehlgeschlagen", appLanguage),
isPresented: Binding(
get: { viewModel.installUpdateError != nil },
set: { _ in viewModel.installUpdateError = nil }
),
presenting: viewModel.installUpdateError
) { _ in
Button("OK") {}
Button(L10n.t("OK", appLanguage)) {}
} message: { message in
Text(message)
}
}
func withFirmwareUpgradeDialogs(viewModel: ConnectViewModel, connectionService: ConnectionService, isPresented: Binding<Bool>) -> some View {
func withFirmwareUpgradeDialogs(viewModel: ConnectViewModel, connectionService: ConnectionService, appLanguage: String, isPresented: Binding<Bool>) -> some View {
self
.confirmationDialog(
"Routerboard-Firmware aktualisieren?",
L10n.t("Routerboard-Firmware aktualisieren?", appLanguage),
isPresented: isPresented,
titleVisibility: .visible
) {
Button("Aktualisieren", role: .destructive) {
Button(L10n.t("Aktualisieren", appLanguage), role: .destructive) {
if let credentials = connectionService.credentials {
viewModel.upgradeRouterboardFirmware(for: credentials)
}
}
Button("Abbrechen", role: .cancel) {}
Button(L10n.t("Abbrechen", appLanguage), role: .cancel) {}
} message: {
Text("Schreibt die neue Bootloader-Firmware auf den Router. Danach ist ein manueller Neustart nötig — passiert nicht automatisch. Dieser Befehl fragt in einer interaktiven Sitzung normalerweise erst nach Bestätigung; ob das über diese App genauso abläuft, ist nicht abschließend getestet (ähnlich interaktive Befehle liefen bisher aber problemlos). Falls die App hier ungewöhnlich lange ohne Rückmeldung bleibt, bitte melden.")
Text(L10n.t("Schreibt die neue Bootloader-Firmware auf den Router. Danach ist ein manueller Neustart nötig — passiert nicht automatisch. Dieser Befehl fragt in einer interaktiven Sitzung normalerweise erst nach Bestätigung; ob das über diese App genauso abläuft, ist nicht abschließend getestet (ähnlich interaktive Befehle liefen bisher aber problemlos). Falls die App hier ungewöhnlich lange ohne Rückmeldung bleibt, bitte melden.", appLanguage))
}
.alert(
"Firmware-Aktualisierung fehlgeschlagen",
L10n.t("Firmware-Aktualisierung fehlgeschlagen", appLanguage),
isPresented: Binding(
get: { viewModel.firmwareUpgradeError != nil },
set: { _ in viewModel.firmwareUpgradeError = nil }
),
presenting: viewModel.firmwareUpgradeError
) { _ in
Button("OK") {}
Button(L10n.t("OK", appLanguage)) {}
} message: { message in
Text(message)
}
}
func withRebootDialogs(viewModel: ConnectViewModel, connectionService: ConnectionService, isPresented: Binding<Bool>) -> some View {
func withRebootDialogs(viewModel: ConnectViewModel, connectionService: ConnectionService, appLanguage: String, isPresented: Binding<Bool>) -> some View {
self
.confirmationDialog(
"Router jetzt neu starten?",
L10n.t("Router jetzt neu starten?", appLanguage),
isPresented: isPresented,
titleVisibility: .visible
) {
Button("Neu starten", role: .destructive) {
Button(L10n.t("Neu starten", appLanguage), role: .destructive) {
if let credentials = connectionService.credentials {
viewModel.reboot(for: credentials)
}
}
Button("Abbrechen", role: .cancel) {}
Button(L10n.t("Abbrechen", appLanguage), role: .cancel) {}
} message: {
Text("Startet den Router sofort neu. Die Verbindung geht dabei kurz verloren, danach im Tab \"Verbinden\" erneut verbinden.")
Text(L10n.t("Startet den Router sofort neu. Die Verbindung geht dabei kurz verloren, danach im Tab \"Verbinden\" erneut verbinden.", appLanguage))
}
.onChange(of: viewModel.didSendReboot) { _, didSend in
if didSend {
@@ -413,14 +421,14 @@ private extension View {
}
}
.alert(
"Neustart fehlgeschlagen",
L10n.t("Neustart fehlgeschlagen", appLanguage),
isPresented: Binding(
get: { viewModel.rebootError != nil },
set: { _ in viewModel.rebootError = nil }
),
presenting: viewModel.rebootError
) { _ in
Button("OK") {}
Button(L10n.t("OK", appLanguage)) {}
} message: { message in
Text(message)
}