forked from kay/RouterOS
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:
+18
@@ -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
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -53,8 +53,8 @@ Befehl, der ausgeführt wird — nichts passiert unangekündigt.
|
||||
Modell-Abgleich als Brick-Schutz, wählbarer Speicherort, "Werksein
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user