diff --git a/apps/ios/Sources/Design/SettingsChannelsDestination.swift b/apps/ios/Sources/Design/SettingsChannelsDestination.swift index 66731207973a..069e5d6f0ab2 100644 --- a/apps/ios/Sources/Design/SettingsChannelsDestination.swift +++ b/apps/ios/Sources/Design/SettingsChannelsDestination.swift @@ -38,7 +38,7 @@ struct SettingsChannelsDestination: View { VStack(alignment: .leading, spacing: 3) { Text("Channels / Integrations") .font(OpenClawType.headline) - Text(self.summaryDetail) + Text(verbatim: self.summaryDetail) .font(OpenClawType.caption) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) @@ -67,7 +67,7 @@ struct SettingsChannelsDestination: View { ProStatusRow( icon: "exclamationmark.triangle", title: "Channel status unavailable", - detail: errorText, + detail: .verbatim(errorText), value: "error", color: OpenClawBrand.warn) } else if !self.canRead { @@ -136,25 +136,26 @@ struct SettingsChannelsDestination: View { } private var headerValue: String? { - if self.isLoading { return "Loading" } - guard self.canRead else { return "Offline" } - return "\(self.channelEntries.count)" + if self.isLoading { return String(localized: "Loading") } + guard self.canRead else { return String(localized: "Offline") } + return self.channelEntries.count.formatted() } private var summaryDetail: String { guard self.canRead else { - return "Connect to load channel integrations." + return String(localized: "Connect to load channel integrations.") } if let errorText { return errorText } - return "Installed channel clients, account state, and message-routing readiness." + return String( + localized: "Installed channel clients, account state, and message-routing readiness.") } private var summaryValue: String { - guard self.canRead else { return "offline" } - if self.isLoading { return "loading" } - if self.errorText != nil { return "error" } + guard self.canRead else { return String(localized: "offline") } + if self.isLoading { return String(localized: "loading") } + if self.errorText != nil { return String(localized: "error") } let configured = self.channelEntries.count(where: { $0.configured }) return "\(configured)/\(self.channelEntries.count)" } @@ -301,7 +302,10 @@ struct SettingsChannelsDestination: View { } static func fallbackDetail(_ id: String) -> String { - self.fallbackMetadata[id.lowercased()]?.detail ?? "Channel integration" + if id.lowercased() == "clickclack" { + return String(localized: "Self-hosted chat bot routing.") + } + return String(localized: "Channel integration") } static func fallbackSystemImage(_ id: String) -> String { @@ -311,18 +315,21 @@ struct SettingsChannelsDestination: View { private static let fallbackMetadata: [String: SettingsChannelFallbackMetadata] = [ "clickclack": SettingsChannelFallbackMetadata( label: "ClickClack", - detail: "Self-hosted chat bot routing.", systemImage: "bubble.left.and.bubble.right"), ] private static func relativeTime(_ milliseconds: Int) -> String { let age = max(0, Int(Date().timeIntervalSince1970 * 1000) - milliseconds) let minutes = age / 60000 - if minutes < 1 { return "now" } - if minutes < 60 { return "\(minutes)m ago" } + if minutes < 1 { return String(localized: "now") } + if minutes < 60 { + return String(format: String(localized: "%@m ago"), minutes.formatted()) + } let hours = minutes / 60 - if hours < 24 { return "\(hours)h ago" } - return "\(hours / 24)d ago" + if hours < 24 { + return String(format: String(localized: "%@h ago"), hours.formatted()) + } + return String(format: String(localized: "%@d ago"), (hours / 24).formatted()) } private static func message(for error: Error) -> String { @@ -346,14 +353,14 @@ private struct SettingsChannelRow: View { HStack(alignment: .top, spacing: 12) { ProIconBadge(systemName: self.entry.systemImage, color: self.entry.color) VStack(alignment: .leading, spacing: 4) { - Text(self.entry.label) + Text(verbatim: self.entry.label) .font(OpenClawType.subheadSemiBold) - Text(self.entry.detailText) + Text(verbatim: self.entry.detailText) .font(OpenClawType.caption) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) if let lastError = self.entry.lastError { - Text(lastError) + Text(verbatim: lastError) .font(OpenClawType.caption2Medium) .foregroundStyle(OpenClawBrand.warn) .lineLimit(2) @@ -385,9 +392,9 @@ private struct SettingsChannelRow: View { .foregroundStyle(account.color) .frame(width: 28, height: 28) VStack(alignment: .leading, spacing: 2) { - Text(account.displayName) + Text(verbatim: account.displayName) .font(OpenClawType.captionSemiBold) - Text(account.detailText) + Text(verbatim: account.detailText) .font(OpenClawType.caption2) .foregroundStyle(.secondary) .lineLimit(1) @@ -465,16 +472,19 @@ private struct SettingsChannelEntry: Identifiable { } var statusValue: String { - if self.connected { return "connected" } - if self.running { return "running" } - if self.linked { return "linked" } - if self.configured { return "configured" } - return "not set" + if self.connected { return String(localized: "connected") } + if self.running { return String(localized: "running") } + if self.linked { return String(localized: "linked") } + if self.configured { return String(localized: "configured") } + return String(localized: "not set") } var detailText: String { if let lastActivityText { - return "\(self.detail) • active \(lastActivityText)" + return String( + format: String(localized: "%@ • active %@"), + self.detail, + lastActivityText) } if let unavailableReason { return unavailableReason @@ -485,7 +495,6 @@ private struct SettingsChannelEntry: Identifiable { private struct SettingsChannelFallbackMetadata { let label: String - let detail: String let systemImage: String } @@ -508,24 +517,33 @@ private struct SettingsChannelAccount: Identifiable { var detailText: String { let state = if self.connected { - "connected" + String(localized: "connected") } else if self.running { - "running" + String(localized: "running") } else if self.linked { - "linked" + String(localized: "linked") } else if self.configured { - "configured" + String(localized: "configured") } else { - "not configured" + String(localized: "not configured") } - let enabledText = self.enabled ? "enabled" : "disabled" + let enabledText = self.enabled + ? String(localized: "enabled") + : String(localized: "disabled") if let healthState, !healthState.isEmpty { - return "\(state), \(enabledText), \(healthState)" + return String( + format: String(localized: "%@, %@, %@"), + state, + enabledText, + healthState) } if let lastError, !lastError.isEmpty { - return "\(state), \(enabledText), error" + return String( + format: String(localized: "%@, %@, error"), + state, + enabledText) } - return "\(state), \(enabledText)" + return String(format: String(localized: "%@, %@"), state, enabledText) } var color: Color { @@ -553,7 +571,7 @@ private enum SettingsChannelError: Error { var message: String { switch self { case .invalidPayload: - "Could not encode channel request." + String(localized: "Could not encode channel request.") } } } diff --git a/apps/ios/Sources/Design/SettingsProTabActions.swift b/apps/ios/Sources/Design/SettingsProTabActions.swift index 10d3fbb64058..721d924479b7 100644 --- a/apps/ios/Sources/Design/SettingsProTabActions.swift +++ b/apps/ios/Sources/Design/SettingsProTabActions.swift @@ -7,24 +7,24 @@ import UserNotifications extension SettingsProTab { func detailStatusCard( icon: String, - title: String, - detail: String, - value: String, + title: OpenClawTextValue, + detail: OpenClawTextValue, + value: OpenClawTextValue, color: Color) -> some View { Section { HStack(spacing: 12) { SettingsIcon(systemName: icon, color: color) VStack(alignment: .leading, spacing: 2) { - Text(title) + title.text .font(OpenClawType.headline) - Text(detail) + detail.text .font(OpenClawType.caption) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) } Spacer(minLength: 8) - Text(value) + value.text .font(OpenClawType.subheadMedium) .foregroundStyle(color) } @@ -36,67 +36,71 @@ extension SettingsProTab { self.diagnosticCheckRow( icon: "stethoscope", title: "Last Run", - detail: self.diagnosticsLastRunText, - value: self.diagnosticsRunValue, + detail: .verbatim(self.diagnosticsLastRunText), + value: .verbatim(self.diagnosticsRunValue), color: self.diagnosticsRunColor) self.diagnosticCheckRow( icon: "antenna.radiowaves.left.and.right", title: "Gateway Link", - detail: self.gatewayStatusDetail, - value: self.gatewayStatusValue, + detail: .verbatim(self.gatewayStatusDetail), + value: .verbatim(self.gatewayStatusValue), color: self.gatewayStatusColor) self.diagnosticCheckRow( icon: "dot.radiowaves.left.and.right", title: "Discovery", - detail: self.gatewayController.discoveryStatusText, - value: "\(self.gatewayController.gateways.count)", + detail: .verbatim(self.gatewayController.discoveryStatusText), + value: .verbatim(self.gatewayController.gateways.count.formatted()), color: self.gatewayController.gateways.isEmpty ? .secondary : OpenClawBrand.accent) self.diagnosticCheckRow( icon: "waveform", title: "Talk Config", - detail: self.gatewayTalkConfigDetail, - value: self.gatewayTalkConfigValue, + detail: .verbatim(self.gatewayTalkConfigDetail), + value: .verbatim(self.gatewayTalkConfigValue), color: self.gatewayTalkConfigColor) self.diagnosticCheckRow( icon: "bell", title: "Notifications", detail: "Approval and event alert channel", - value: self.notificationStatusText, + value: .verbatim(self.notificationStatusText), color: self.notificationStatusColor) self.diagnosticCheckRow( icon: "rectangle.on.rectangle", title: "Screen Capture", detail: "Live foreground capture state", - value: self.appModel.screenRecordActive ? "live" : "idle", + value: .verbatim(self.appModel.screenRecordActive + ? String(localized: "live") + : String(localized: "idle")), color: self.appModel.screenRecordActive ? OpenClawBrand.ok : .secondary) self.diagnosticCheckRow( icon: "mic", title: "Voice Wake", - detail: self.appModel.voiceWake.statusText, - value: self.voiceWakeEnabled ? "on" : "off", + detail: .verbatim(self.appModel.voiceWake.statusText), + value: .verbatim(self.voiceWakeEnabled + ? String(localized: "on") + : String(localized: "off")), color: self.voiceWakeEnabled ? OpenClawBrand.ok : .secondary) } } func diagnosticCheckRow( icon: String, - title: String, - detail: String, - value: String, + title: OpenClawTextValue, + detail: OpenClawTextValue, + value: OpenClawTextValue, color: Color) -> some View { HStack(spacing: 12) { SettingsIcon(systemName: icon, color: color) VStack(alignment: .leading, spacing: 2) { - Text(title) + title.text .font(OpenClawType.subheadSemiBold) - Text(detail) + detail.text .font(OpenClawType.caption) .foregroundStyle(.secondary) .lineLimit(1) } Spacer(minLength: 8) - Text(value) + value.text .font(OpenClawType.subhead) .foregroundStyle(.secondary) } @@ -119,7 +123,9 @@ extension SettingsProTab { func switchGateway(to entry: GatewaySettingsStore.GatewayRegistryEntry) async { guard self.connectingGateway == nil else { return } self.connectingGateway = .gateway(entry.id) - self.setupStatusText = "Switching to \(entry.name)…" + self.setupStatusText = String( + format: String(localized: "Switching to %@…"), + entry.name) defer { self.connectingGateway = nil self.refreshGatewayRegistry() @@ -135,14 +141,18 @@ extension SettingsProTab { guard let entry = self.pendingForgetGateway else { return } self.pendingForgetGateway = nil guard self.gatewayController.forgetGateway(stableID: entry.stableID) else { - self.setupStatusText = "Could not forget \(entry.name)." + self.setupStatusText = String( + format: String(localized: "Could not forget %@."), + entry.name) self.refreshGatewayRegistry() return } if GatewayStableIdentifier.matches(self.gatewayCredentialFieldStableID, entry.stableID) { self.clearManualCredentialFields() } - self.setupStatusText = "Forgot \(entry.name)." + self.setupStatusText = String( + format: String(localized: "Forgot %@."), + entry.name) self.refreshGatewayRegistry() } @@ -156,11 +166,13 @@ extension SettingsProTab { let endpoint = if let host = entry.host, let port = entry.port { "\(host):\(port)" } else { - "Saved endpoint unavailable" + String(localized: "Saved endpoint unavailable") } return entry.useTLS ? "\(endpoint) • TLS" : endpoint case .discovered: - return entry.useTLS ? "Discovered • TLS" : "Discovered" + return entry.useTLS + ? String(localized: "Discovered • TLS") + : String(localized: "Discovered") } } @@ -285,11 +297,11 @@ extension SettingsProTab { guard await self.applySetupCode(attemptID: attemptID) else { return } let host = self.manualGatewayHost.trimmingCharacters(in: .whitespacesAndNewlines) guard self.resolvedManualPort(host: host) != nil else { - self.setupStatusText = "Failed: invalid port" + self.setupStatusText = String(localized: "Failed: invalid port") return } guard await self.preflightGateway(host: host) else { return } - self.setupStatusText = "Setup code applied. Connecting..." + self.setupStatusText = String(localized: "Setup code applied. Connecting...") await self.connectManual(setupAttemptID: attemptID) } @@ -303,8 +315,13 @@ extension SettingsProTab { self.setupCode = "" self.setupStatusText = nil self.stagedGatewaySetupLink = link - let security = link.tls ? "TLS" : "plain" - self.setupStatusText = "Setup link loaded for \(link.host):\(link.port) (\(security)). Tap Connect to apply." + let security = link.tls ? String(localized: "TLS") : String(localized: "plain") + self.setupStatusText = String( + format: String( + localized: "Setup link loaded for %@:%@ (%@). Tap Connect to apply."), + link.host, + link.port.formatted(), + security) } @discardableResult @@ -312,21 +329,22 @@ extension SettingsProTab { let raw = self.setupCode.trimmingCharacters(in: .whitespacesAndNewlines) let stagedLink = self.stagedGatewaySetupLink guard !raw.isEmpty || stagedLink != nil else { - self.setupStatusText = "Paste a setup code to continue." + self.setupStatusText = String(localized: "Paste a setup code to continue.") return false } if AppleReviewDemoMode.isSetupCode(raw) { self.stagedGatewaySetupLink = nil self.setupCode = "" - self.setupStatusText = "Apple Review demo mode enabled." + self.setupStatusText = String(localized: "Apple Review demo mode enabled.") self.appModel.enterAppleReviewDemoMode() self.pendingTargetSuppression.releaseAutoConnect(.setupLink, controller: self.gatewayController) return false } guard let parsedLink = raw.isEmpty ? stagedLink : GatewayConnectDeepLink.fromSetupInput(raw) else { - self.setupStatusText = "Setup code not recognized or uses an insecure ws:// gateway URL." + self.setupStatusText = String( + localized: "Setup code not recognized or uses an insecure ws:// gateway URL.") return false } let link = await self.gatewayController.selectReachableSetupLink(parsedLink) @@ -371,13 +389,13 @@ extension SettingsProTab { self.pendingTargetSuppression.replace(owner: .qrScanner, lease: lease) self.scannerScanID = self.scannerResultHandoff.beginScan() self.connectingGateway = nil - self.setupStatusText = "Opening QR scanner..." + self.setupStatusText = String(localized: "Opening QR scanner...") self.showQRScanner = true } func queueScannedResult(_ result: QRScannerResult, scanID: UInt64) { guard self.scannerResultHandoff.queue(result, scanID: scanID) else { return } - self.setupStatusText = "QR loaded. Closing scanner..." + self.setupStatusText = String(localized: "QR loaded. Closing scanner...") self.showQRScanner = false } @@ -407,7 +425,7 @@ extension SettingsProTab { self.showQRScanner = false self.setupCode = "" self.stagedGatewaySetupLink = nil - self.setupStatusText = "Apple Review demo mode enabled." + self.setupStatusText = String(localized: "Apple Review demo mode enabled.") self.appModel.enterAppleReviewDemoMode() self.pendingTargetSuppression.releaseAutoConnect(.qrScanner, controller: self.gatewayController) } @@ -431,10 +449,13 @@ extension SettingsProTab { let link = await self.gatewayController.selectReachableSetupLink(parsedLink) guard self.setupAttemptID == attemptID else { return } await self.applyGatewayLink(link) - self.setupStatusText = "QR loaded. Connecting to \(link.host):\(link.port)..." + self.setupStatusText = String( + format: String(localized: "QR loaded. Connecting to %@:%@..."), + link.host, + link.port.formatted()) let host = self.manualGatewayHost.trimmingCharacters(in: .whitespacesAndNewlines) guard self.resolvedManualPort(host: host) != nil else { - self.setupStatusText = "Failed: invalid port" + self.setupStatusText = String(localized: "Failed: invalid port") return } guard await self.preflightGateway(host: host) else { return } @@ -455,15 +476,15 @@ extension SettingsProTab { } let host = self.manualGatewayHost.trimmingCharacters(in: .whitespacesAndNewlines) guard !host.isEmpty else { - self.setupStatusText = "Failed: host required" + self.setupStatusText = String(localized: "Failed: host required") return } guard self.manualPortIsValid else { - self.setupStatusText = "Failed: invalid port" + self.setupStatusText = String(localized: "Failed: invalid port") return } guard let port = self.resolvedManualPort(host: host) else { - self.setupStatusText = "Failed: invalid port" + self.setupStatusText = String(localized: "Failed: invalid port") return } self.connectingGateway = .manual @@ -520,7 +541,8 @@ extension SettingsProTab { let trimmed = host.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return false } if Self.isTailnetHostOrIP(trimmed), !Self.hasTailnetIPv4() { - self.setupStatusText = "Tailscale is off on this device. Turn it on, then try again." + self.setupStatusText = String( + localized: "Tailscale is off on this device. Turn it on, then try again.") return false } self.gatewayController.requestLocalNetworkAccess(reason: "settings_preflight") @@ -897,30 +919,31 @@ extension SettingsProTab { func title(for route: SettingsRoute) -> String { switch route { - case .gateway: "Gateway" - case .appleWatch: "Apple Watch" - case .approvals: "Approvals" - case .permissions: "Permissions" - case .channels: "Channels" - case .voice: "Voice & Talk" - case .diagnostics: "Diagnostics" - case .privacy: "Privacy" - case .notifications: "Notifications" - case .licenses: "Licenses" - case .about: "About" + case .gateway: String(localized: "Gateway") + case .appleWatch: String(localized: "Apple Watch") + case .approvals: String(localized: "Approvals") + case .permissions: String(localized: "Permissions") + case .channels: String(localized: "Channels") + case .voice: String(localized: "Voice & Talk") + case .diagnostics: String(localized: "Diagnostics") + case .privacy: String(localized: "Privacy") + case .notifications: String(localized: "Notifications") + case .licenses: String(localized: "Licenses") + case .about: String(localized: "About") } } func sendDirectWatchSetup() async { guard !self.isSendingWatchDirectSetup else { return } self.isSendingWatchDirectSetup = true - self.watchDirectSetupStatusText = "Preparing one-time setup…" + self.watchDirectSetupStatusText = String(localized: "Preparing one-time setup…") defer { self.isSendingWatchDirectSetup = false } do { let result = try await self.appModel.sendDirectWatchSetup() self.watchDirectSetupStatusText = result.deliveredImmediately - ? "Setup sent. Open OpenClaw on the watch to connect." - : "Setup queued for the watch. Open OpenClaw before the code expires." + ? String(localized: "Setup sent. Open OpenClaw on the watch to connect.") + : String( + localized: "Setup queued for the watch. Open OpenClaw before the code expires.") } catch { self.watchDirectSetupStatusText = error.localizedDescription } @@ -1009,16 +1032,18 @@ extension SettingsProTab { var tailnetWarningText: String? { let host = self.manualGatewayHost.trimmingCharacters(in: .whitespacesAndNewlines) guard !host.isEmpty, Self.isTailnetHostOrIP(host), !Self.hasTailnetIPv4() else { return nil } - return "This gateway is on your tailnet. Turn on Tailscale on this device, then tap Connect." + return String( + localized: "This gateway is on your tailnet. Turn on Tailscale on this device, then tap Connect.") } func friendlyGatewayMessage(from raw: String) -> String? { let lower = raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() if lower.contains("pairing required") { - return "Pairing required. Run /pair approve in your OpenClaw chat, then connect again." + return String( + localized: "Pairing required. Run /pair approve in your OpenClaw chat, then connect again.") } if lower.contains("device nonce required") || lower.contains("device nonce mismatch") { - return "Secure handshake failed. Check Tailscale, then connect again." + return String(localized: "Secure handshake failed. Check Tailscale, then connect again.") } if lower.contains("tls fingerprint verification timed out") || lower.contains("no tls endpoint detected") @@ -1026,19 +1051,25 @@ extension SettingsProTab { return raw.trimmingCharacters(in: .whitespacesAndNewlines) } if lower.contains("timed out") { - return "Connection timed out. Make sure Tailscale is connected, then try again." + return String( + localized: "Connection timed out. Make sure Tailscale is connected, then try again.") } if lower.contains("unauthorized role") { - return "Connected, but some controls are restricted for nodes. This is expected." + return String( + localized: "Connected, but some controls are restricted for nodes. This is expected.") } return nil } func isTransientSetupStatus(_ raw: String) -> Bool { let lower = raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - return lower == "setup code applied. connecting..." - || lower.hasPrefix("qr loaded. connecting to ") - || lower == "checking gateway reachability..." + let setupApplied = String(localized: "Setup code applied. Connecting...").lowercased() + let checkingReachability = String(localized: "Checking gateway reachability...").lowercased() + let qrFormat = String(localized: "QR loaded. Connecting to %@:%@...").lowercased() + let qrPrefix = qrFormat.components(separatedBy: "%@").first ?? qrFormat + return lower == setupApplied + || (!qrPrefix.isEmpty && lower.hasPrefix(qrPrefix)) + || lower == checkingReachability } var shouldShowRealtimeVoicePicker: Bool { @@ -1076,15 +1107,19 @@ extension SettingsProTab { } var talkApiKeyStatus: String { - guard self.appModel.talkMode.gatewayTalkConfigLoaded else { return "Not loaded" } - return self.appModel.talkMode.gatewayTalkApiKeyConfigured ? "Configured" : "Not configured" + guard self.appModel.talkMode.gatewayTalkConfigLoaded else { + return String(localized: "Not loaded") + } + return self.appModel.talkMode.gatewayTalkApiKeyConfigured + ? String(localized: "Configured") + : String(localized: "Not configured") } var gatewayTalkActiveVoiceDetail: String { let title = self.appModel.talkMode.gatewayTalkActiveModeTitle.trimmingCharacters(in: .whitespacesAndNewlines) let subtitle = (self.appModel.talkMode.gatewayTalkActiveModeSubtitle ?? "") .trimmingCharacters(in: .whitespacesAndNewlines) - if title.isEmpty { return "Not active" } + if title.isEmpty { return String(localized: "Not active") } if subtitle.isEmpty { return title } return "\(title) • \(subtitle)" } @@ -1113,13 +1148,17 @@ extension SettingsProTab { } var gatewayStatusDetail: String { - if self.appModel.isAppleReviewDemoModeEnabled { return "Apple Review demo mode" } - return self.gatewayConnected ? "Connected" : self.appModel.gatewayDisplayStatusText + if self.appModel.isAppleReviewDemoModeEnabled { + return String(localized: "Apple Review demo mode") + } + return self.gatewayConnected + ? String(localized: "Connected") + : self.appModel.gatewayDisplayStatusText } var gatewayStatusValue: String { - if self.appModel.isAppleReviewDemoModeEnabled { return "demo" } - return self.gatewayConnected ? "online" : "offline" + if self.appModel.isAppleReviewDemoModeEnabled { return String(localized: "demo") } + return self.gatewayConnected ? String(localized: "online") : String(localized: "offline") } var gatewayStatusColor: Color { @@ -1137,22 +1176,27 @@ extension SettingsProTab { var approvalEmptyDetail: String { if self.appModel.isAppleReviewDemoModeEnabled { - return "Live gateway requests are disabled in demo mode." + return String(localized: "Live gateway requests are disabled in demo mode.") } if self.notificationsNeedAttention { - return "Foreground approvals still appear while OpenClaw is connected." + return String( + localized: "Foreground approvals still appear while OpenClaw is connected.") } - return self.gatewayConnected ? "Gateway requests will appear here." : "Connect to the gateway." + return self.gatewayConnected + ? String(localized: "Gateway requests will appear here.") + : String(localized: "Connect to the gateway.") } var gatewayTalkConfigDetail: String { - if self.appModel.isAppleReviewDemoModeEnabled { return "Demo mode only" } + if self.appModel.isAppleReviewDemoModeEnabled { return String(localized: "Demo mode only") } return self.appModel.talkMode.gatewayTalkTransportLabel } var gatewayTalkConfigValue: String { - if self.appModel.isAppleReviewDemoModeEnabled { return "demo" } - return self.appModel.talkMode.gatewayTalkConfigLoaded ? "loaded" : "missing" + if self.appModel.isAppleReviewDemoModeEnabled { return String(localized: "demo") } + return self.appModel.talkMode.gatewayTalkConfigLoaded + ? String(localized: "loaded") + : String(localized: "missing") } var gatewayTalkConfigColor: Color { @@ -1161,7 +1205,7 @@ extension SettingsProTab { } var gatewayAddress: String { - self.appModel.gatewayRemoteAddress ?? "Waiting for gateway" + self.appModel.gatewayRemoteAddress ?? String(localized: "Waiting for gateway") } var gatewayServer: String { @@ -1177,7 +1221,12 @@ extension SettingsProTab { } var approvalWaitingText: String { - self.pendingApprovalCount == 1 ? "1 waiting" : "\(self.pendingApprovalCount) waiting" + if self.pendingApprovalCount == 1 { + return String(localized: "1 waiting") + } + return String( + format: String(localized: "%@ waiting"), + self.pendingApprovalCount.formatted()) } var notificationsNeedAttention: Bool { @@ -1186,41 +1235,54 @@ extension SettingsProTab { var approvalItems: [SettingsApprovalItem] { guard let pendingApproval else { return [] } + let pendingTitle = pendingApproval.commandPreview.map(OpenClawTextValue.verbatim) + ?? OpenClawTextValue.localized("Review gateway action") + let agentDetail = String( + format: String(localized: "Agent: %@"), + self.appModel.activeAgentName) return [ SettingsApprovalItem( id: "pending-real", icon: "terminal.fill", - title: pendingApproval.commandPreview ?? "Review gateway action", - detail: "Agent: \(self.appModel.activeAgentName)", - priority: self.appModel.pendingExecApprovalPromptResolving ? "Resolving" : "High", + title: pendingTitle, + detail: .verbatim(agentDetail), + priority: self.appModel.pendingExecApprovalPromptResolving + ? .localized("Resolving") + : .localized("High"), color: OpenClawBrand.danger), SettingsApprovalItem( id: "pending-context", icon: "doc.text.fill", - title: pendingApproval.allowsAllowAlways ? "Permission can be saved" : "One-time approval", + title: pendingApproval.allowsAllowAlways + ? .localized("Permission can be saved") + : .localized("One-time approval"), detail: "Gateway request", - priority: pendingApproval.allowsAllowAlways ? "Medium" : "Review", + priority: pendingApproval.allowsAllowAlways + ? .localized("Medium") + : .localized("Review"), color: OpenClawBrand.warn), ] } var voiceDetail: String { - if self.talkEnabled, self.voiceWakeEnabled { return "Talk + Wake" } - if self.talkEnabled { return "Talk on" } - if self.voiceWakeEnabled { return "Wake on" } - return "Off" + if self.talkEnabled, self.voiceWakeEnabled { return String(localized: "Talk + Wake") } + if self.talkEnabled { return String(localized: "Talk on") } + if self.voiceWakeEnabled { return String(localized: "Wake on") } + return String(localized: "Off") } var diagnosticsHealthValue: String { - if self.appModel.isAppleReviewDemoModeEnabled { return "demo" } - if self.gatewayConnected { return "ready" } - if self.gatewayController.gateways.isEmpty { return "check" } - return "partial" + if self.appModel.isAppleReviewDemoModeEnabled { return String(localized: "demo") } + if self.gatewayConnected { return String(localized: "ready") } + if self.gatewayController.gateways.isEmpty { return String(localized: "check") } + return String(localized: "partial") } var diagnosticsRunValue: String { - guard let diagnosticsIssueCount else { return "pending" } - return diagnosticsIssueCount == 0 ? "pass" : "\(diagnosticsIssueCount)" + guard let diagnosticsIssueCount else { return String(localized: "pending") } + return diagnosticsIssueCount == 0 + ? String(localized: "pass") + : diagnosticsIssueCount.formatted() } var diagnosticsRunColor: Color { @@ -1230,7 +1292,7 @@ extension SettingsProTab { var locationPermissionDetailText: String? { if self.isChangingLocationMode { - return "Requesting iOS location permission…" + return String(localized: "Requesting iOS location permission…") } return self.locationSettingsPresentation.statusText } @@ -1294,15 +1356,17 @@ extension SettingsProTab { let host = PushBuildConfig.current.relayBaseURL.flatMap { URLComponents(url: $0, resolvingAgainstBaseURL: false)?.host } ?? "ios-push-relay.openclaw.ai" - return """ - This build uses OpenClaw's hosted push relay at \(host) for notification \ - delivery data. - """ + return String( + format: String( + localized: "This build uses OpenClaw's hosted push relay at %@ for notification delivery data."), + host) } - return "This build is not configured to use OpenClaw's hosted push relay." + return String( + localized: "This build is not configured to use OpenClaw's hosted push relay.") } var notificationRelayDisclosureMessage: String { - "Enabling this sends delivery data through OpenClaw's hosted push relay." + String( + localized: "Enabling this sends delivery data through OpenClaw's hosted push relay.") } } diff --git a/apps/ios/Sources/Design/SettingsProTabSections.swift b/apps/ios/Sources/Design/SettingsProTabSections.swift index f3c83952d7a9..54988ec518f1 100644 --- a/apps/ios/Sources/Design/SettingsProTabSections.swift +++ b/apps/ios/Sources/Design/SettingsProTabSections.swift @@ -118,9 +118,11 @@ extension SettingsProTab { self.gatewayQuickSwitchMenu } } - SettingsDetailRow("Address", value: self.gatewayAddress) - SettingsDetailRow("Server", value: self.gatewayServer) - SettingsDetailRow("Agents", value: "\(self.appModel.gatewayAgents.count)") + SettingsDetailRow("Address", value: .verbatim(self.gatewayAddress)) + SettingsDetailRow("Server", value: .verbatim(self.gatewayServer)) + SettingsDetailRow( + "Agents", + value: .verbatim(self.appModel.gatewayAgents.count.formatted())) self.gatewayActions } } @@ -202,7 +204,7 @@ extension SettingsProTab { func settingsListRow( icon: String, iconColor: Color, - title: String, + title: LocalizedStringKey, route: SettingsRoute, badgeValue: String? = nil) -> some View { @@ -278,16 +280,22 @@ extension SettingsProTab { self.detailStatusCard( icon: "antenna.radiowaves.left.and.right", title: "Gateway", - detail: self.gatewayStatusDetail, - value: self.gatewayStatusValue, + detail: .verbatim(self.gatewayStatusDetail), + value: .verbatim(self.gatewayStatusValue), color: self.gatewayStatusColor) self.detailListCard { - SettingsDetailRow("Address", value: self.gatewayAddress) - SettingsDetailRow("Server", value: self.gatewayServer) - SettingsDetailRow("Discovered", value: "\(self.gatewayController.gateways.count)") - SettingsDetailRow("Default Agent", value: self.appModel.activeAgentName) - SettingsDetailRow("Agents", value: "\(self.appModel.gatewayAgents.count)") + SettingsDetailRow("Address", value: .verbatim(self.gatewayAddress)) + SettingsDetailRow("Server", value: .verbatim(self.gatewayServer)) + SettingsDetailRow( + "Discovered", + value: .verbatim(self.gatewayController.gateways.count.formatted())) + SettingsDetailRow( + "Default Agent", + value: .verbatim(self.appModel.activeAgentName)) + SettingsDetailRow( + "Agents", + value: .verbatim(self.appModel.gatewayAgents.count.formatted())) } Section { @@ -352,13 +360,16 @@ extension SettingsProTab { self.detailStatusCard( icon: "checkmark.shield.fill", title: "Approvals", - detail: self.notificationsNeedAttention - ? "Out-of-app approval alerts need notification permission." - : (self.pendingApprovalCount == 0 ? "No gateway actions are waiting for review." : - "Review pending gateway actions."), + detail: .verbatim(self.notificationsNeedAttention + ? String(localized: "Out-of-app approval alerts need notification permission.") + : (self.pendingApprovalCount == 0 + ? String(localized: "No gateway actions are waiting for review.") + : String(localized: "Review pending gateway actions."))), value: self.notificationsNeedAttention - ? "Alerts Off" - : (self.pendingApprovalCount == 0 ? "clear" : self.approvalWaitingText), + ? .verbatim(String(localized: "Alerts Off")) + : (self.pendingApprovalCount == 0 + ? .verbatim(String(localized: "clear")) + : .verbatim(self.approvalWaitingText)), color: self.notificationsNeedAttention ? OpenClawBrand.warn : (self.pendingApprovalCount == 0 ? OpenClawBrand.ok : OpenClawBrand.warn)) @@ -376,10 +387,17 @@ extension SettingsProTab { self.detailStatusCard( icon: "applewatch", title: "Apple Watch", - detail: watchStatus.appInstalled - ? "Relay remains available; direct mode adds an independent Gateway node." - : "Install the OpenClaw watch app before enabling direct mode.", - value: watchStatus.reachable ? "Reachable" : (watchStatus.appInstalled ? "Installed" : "Unavailable"), + detail: .verbatim(watchStatus.appInstalled + ? String( + localized: "Relay remains available; direct mode adds an independent Gateway node.") + : String( + localized: "Install the OpenClaw watch app before enabling direct mode.")), + value: .verbatim( + watchStatus.reachable + ? String(localized: "Reachable") + : (watchStatus.appInstalled + ? String(localized: "Installed") + : String(localized: "Unavailable"))), color: watchStatus.appInstalled ? OpenClawBrand.ok : OpenClawBrand.warn) Section { @@ -586,8 +604,8 @@ extension SettingsProTab { self.detailStatusCard( icon: "waveform", title: "Voice & Talk", - detail: self.appModel.talkMode.gatewayTalkVoiceModeTitle, - value: self.voiceDetail, + detail: .verbatim(self.appModel.talkMode.gatewayTalkVoiceModeTitle), + value: .verbatim(self.voiceDetail), color: self.talkEnabled || self.voiceWakeEnabled ? OpenClawBrand.accent : .secondary) self.voiceFeatureCard @@ -602,7 +620,7 @@ extension SettingsProTab { icon: "checklist.checked", title: "Health Check", detail: "Run app, permission, and gateway-adjacent checks without editing setup.", - value: self.diagnosticsHealthValue, + value: .verbatim(self.diagnosticsHealthValue), color: self.gatewayDiagnosticConnected ? OpenClawBrand.ok : OpenClawBrand.warn) Section { @@ -618,10 +636,14 @@ extension SettingsProTab { self.diagnosticChecksCard self.detailListCard { - SettingsDetailRow("Device", value: DeviceInfoHelper.deviceFamily()) - SettingsDetailRow("Platform", value: DeviceInfoHelper.platformStringForDisplay()) - SettingsDetailRow("App", value: DeviceInfoHelper.openClawVersionString()) - SettingsDetailRow("Model", value: DeviceInfoHelper.modelIdentifier()) + SettingsDetailRow("Device", value: .verbatim(DeviceInfoHelper.deviceFamily())) + SettingsDetailRow( + "Platform", + value: .verbatim(DeviceInfoHelper.platformStringForDisplay())) + SettingsDetailRow( + "App", + value: .verbatim(DeviceInfoHelper.openClawVersionString())) + SettingsDetailRow("Model", value: .verbatim(DeviceInfoHelper.modelIdentifier())) } self.diagnosticsAdvancedCard @@ -670,7 +692,9 @@ extension SettingsProTab { .labelsHidden() .disabled(self.notificationStatus == .checking || self.isRequestingNotificationAuthorization) .accessibilityIdentifier("settings-notifications-toggle") - .accessibilityValue(self.notificationServingActive ? "On" : "Off") + .accessibilityValue(self.notificationServingActive + ? String(localized: "On") + : String(localized: "Off")) .accessibilityHint("Turns OpenClaw notification delivery on or off") } @@ -792,8 +816,10 @@ extension SettingsProTab { // Concise public details only; deep hardware identifiers live in Diagnostics. detailListCard { - SettingsDetailRow("Device", value: DeviceInfoHelper.deviceFamily()) - SettingsDetailRow("iOS", value: DeviceInfoHelper.iOSVersionStringForDisplay()) + SettingsDetailRow("Device", value: .verbatim(DeviceInfoHelper.deviceFamily())) + SettingsDetailRow( + "iOS", + value: .verbatim(DeviceInfoHelper.iOSVersionStringForDisplay())) } Section { @@ -826,7 +852,12 @@ extension SettingsProTab { /// About link row with explicit branded label; shorthand `Link("Title", ...)` /// would bypass the typography audit and OpenClawType styling. - func aboutLinkRow(title: String, icon: String, color: Color, url: URL) -> some View { + func aboutLinkRow( + title: LocalizedStringKey, + icon: String, + color: Color, + url: URL) -> some View + { Link(destination: url) { HStack { Label { @@ -843,10 +874,10 @@ extension SettingsProTab { } .contentShape(Rectangle()) } - .accessibilityLabel(title) + .accessibilityLabel(Text(title)) } - func toggleCard(title: String, isOn: Binding) -> some View { + func toggleCard(title: LocalizedStringKey, isOn: Binding) -> some View { Section { self.settingsToggle(title, isOn: isOn) } @@ -878,7 +909,9 @@ extension SettingsProTab { .disabled(self.isChangingLocationMode) .accessibilityIdentifier("settings-location-sharing-toggle") .accessibilityLabel("Location Sharing") - .accessibilityValue(self.locationSettingsPresentation.sharingControlIsOn ? "On" : "Off") + .accessibilityValue(self.locationSettingsPresentation.sharingControlIsOn + ? String(localized: "On") + : String(localized: "Off")) if self.locationSettingsPresentation.showsAccessLevel, let accessLevelText = self.locationSettingsPresentation.accessLevelText @@ -1199,13 +1232,16 @@ extension SettingsProTab { } } - func gatewaySecureField(_ placeholder: String, text: Binding) -> some View { + func gatewaySecureField( + _ placeholder: LocalizedStringKey, + text: Binding) -> some View + { ZStack(alignment: .leading) { SecureField("", text: text) .font(OpenClawType.subhead) .textInputAutocapitalization(.never) .autocorrectionDisabled() - .accessibilityLabel(placeholder) + .accessibilityLabel(Text(placeholder)) if text.wrappedValue.isEmpty { Text(placeholder) .font(OpenClawType.subheadSemiBold) @@ -1244,7 +1280,8 @@ extension SettingsProTab { } label: { SettingsDetailRow( "Wake Words", - value: VoiceWakePreferences.displayString(for: self.voiceWake.triggerWords)) + value: .verbatim( + VoiceWakePreferences.displayString(for: self.voiceWake.triggerWords))) } } } @@ -1279,13 +1316,19 @@ extension SettingsProTab { } .font(OpenClawType.body) } - SettingsDetailRow("Voice Mode", value: self.appModel.talkMode.gatewayTalkVoiceModeTitle) - SettingsDetailRow("Active Voice", value: self.gatewayTalkActiveVoiceDetail) + SettingsDetailRow( + "Voice Mode", + value: .localized(self.appModel.talkMode.gatewayTalkVoiceModeTitle)) + SettingsDetailRow( + "Active Voice", + value: .verbatim(self.gatewayTalkActiveVoiceDetail)) if let issue = self.gatewayTalkLastIssueDetail { - SettingsDetailRow("Last Voice Issue", value: issue) + SettingsDetailRow("Last Voice Issue", value: .verbatim(issue)) } - SettingsDetailRow("Transport", value: self.appModel.talkMode.gatewayTalkTransportLabel) - SettingsDetailRow("API Key", value: self.talkApiKeyStatus) + SettingsDetailRow( + "Transport", + value: .localized(self.appModel.talkMode.gatewayTalkTransportLabel)) + SettingsDetailRow("API Key", value: .verbatim(self.talkApiKeyStatus)) } } } @@ -1324,7 +1367,9 @@ extension SettingsProTab { NavigationLink { GatewayDiscoveryDebugLogView() } label: { - SettingsDetailRow("Discovery Logs", value: self.gatewayController.discoveryStatusText) + SettingsDetailRow( + "Discovery Logs", + value: .verbatim(self.gatewayController.discoveryStatusText)) } } } @@ -1333,12 +1378,12 @@ extension SettingsProTab { Section("Device") { TextField("Device Name", text: self.$displayName) .font(OpenClawType.body) - SettingsDetailRow("Instance ID", value: self.instanceId) + SettingsDetailRow("Instance ID", value: .verbatim(self.instanceId)) } } func settingsToggle( - _ title: String, + _ title: LocalizedStringKey, isOn: Binding, onChange: ((Bool) -> Void)? = nil) -> some View { @@ -1355,8 +1400,10 @@ extension SettingsProTab { .contentShape(Rectangle()) } .buttonStyle(.plain) - .accessibilityLabel(title) - .accessibilityValue(isOn.wrappedValue ? "On" : "Off") + .accessibilityLabel(Text(title)) + .accessibilityValue(isOn.wrappedValue + ? String(localized: "On") + : String(localized: "Off")) .onChange(of: isOn.wrappedValue) { _, enabled in onChange?(enabled) } diff --git a/apps/ios/Sources/Design/SettingsProTabSupport.swift b/apps/ios/Sources/Design/SettingsProTabSupport.swift index a739c5ba25b0..928d715c83d0 100644 --- a/apps/ios/Sources/Design/SettingsProTabSupport.swift +++ b/apps/ios/Sources/Design/SettingsProTabSupport.swift @@ -27,17 +27,17 @@ enum SettingsLayout { /// detail row on this view so row typography cannot drift between sections; /// plain `LabeledContent(String, value:)` renders unbranded system fonts. struct SettingsDetailRow: View { - let label: String - let value: String + let label: LocalizedStringKey + let value: OpenClawTextValue - init(_ label: String, value: String) { + init(_ label: LocalizedStringKey, value: OpenClawTextValue) { self.label = label self.value = value } var body: some View { LabeledContent { - Text(self.value) + self.value.text .font(OpenClawType.subhead) .lineLimit(1) .truncationMode(.middle) @@ -176,15 +176,33 @@ struct SettingsBuildMetadataStrip: View { let timestamp = self.metadata.buildTimestamp let built = self.metadata.localizedBuildDate() ?? timestamp if let commit, let timestamp, let built { - return Text("Version \(version), commit \(commit), built \(built), timestamp \(timestamp)") + return Text(verbatim: String( + format: String( + localized: "Version %1$@, commit %2$@, built %3$@, timestamp %4$@"), + version, + commit, + built, + timestamp)) } if let commit { - return Text("Version \(version), commit \(commit), build date unavailable") + return Text(verbatim: String( + format: String( + localized: "Version %1$@, commit %2$@, build date unavailable"), + version, + commit)) } if let timestamp, let built { - return Text("Version \(version), commit unavailable, built \(built), timestamp \(timestamp)") + return Text(verbatim: String( + format: String( + localized: "Version %1$@, commit unavailable, built %2$@, timestamp %3$@"), + version, + built, + timestamp)) } - return Text("Version \(version), commit unavailable, build date unavailable") + return Text(verbatim: String( + format: String( + localized: "Version %@, commit unavailable, build date unavailable"), + version)) } private func copyCommit() { @@ -200,9 +218,9 @@ struct SettingsBuildMetadataStrip: View { struct SettingsApprovalItem: Identifiable { let id: String let icon: String - let title: String - let detail: String - let priority: String + let title: OpenClawTextValue + let detail: OpenClawTextValue + let priority: OpenClawTextValue let color: Color } @@ -220,16 +238,16 @@ struct SettingsApprovalRow: View { .fill(self.item.color) } VStack(alignment: .leading, spacing: 2) { - Text(self.item.title) + self.item.title.text .font(OpenClawType.subheadSemiBold) .lineLimit(1) - Text(self.item.detail) + self.item.detail.text .font(OpenClawType.caption2Medium) .foregroundStyle(.secondary) .lineLimit(1) } Spacer(minLength: 8) - Text(self.item.priority) + self.item.priority.text .font(OpenClawType.captionBold) .foregroundStyle(self.item.color) .padding(.horizontal, 9) @@ -279,32 +297,35 @@ enum SettingsNotificationPresentation: Equatable { var text: String { switch self { - case .checking: "Checking" - case .enabled: "Enabled" - case .off: "Off" - case .setup: "Setup" - case .denied: "Denied" - case .notSet: "Not Enabled" - case .unknown: "Unknown" + case .checking: String(localized: "Checking") + case .enabled: String(localized: "Enabled") + case .off: String(localized: "Off") + case .setup: String(localized: "Setup") + case .denied: String(localized: "Denied") + case .notSet: String(localized: "Not Enabled") + case .unknown: String(localized: "Unknown") } } var detail: String { switch self { case .checking: - "Checking iOS notification permission." + String(localized: "Checking iOS notification permission.") case .enabled: - "OpenClaw can show approval prompts and event alerts when the app is not active." + String( + localized: "OpenClaw can show approval prompts and event alerts when the app is not active.") case .off: - "OpenClaw notifications are off." + String(localized: "OpenClaw notifications are off.") case .setup: - "Finish notification setup to receive alerts when the app is not active." + String( + localized: "Finish notification setup to receive alerts when the app is not active.") case .denied: - "Notifications have been denied. Enable them in iOS Settings." + String(localized: "Notifications have been denied. Enable them in iOS Settings.") case .notSet: - "Enable notifications to receive approval prompts and event alerts outside the app." + String( + localized: "Enable notifications to receive approval prompts and event alerts outside the app.") case .unknown: - "OpenClaw cannot determine the current notification permission state." + String(localized: "OpenClaw cannot determine the current notification permission state.") } } @@ -493,8 +514,8 @@ private struct SettingsGatewayStatesPreview: View { ProCard(padding: 0, radius: SettingsLayout.cardRadius) { ProStatusRow( icon: value == "online" ? "antenna.radiowaves.left.and.right" : "wifi.slash", - title: title, - detail: detail, + title: .localized(title), + detail: .localized(detail), value: value, color: color, actionTitle: value == "setup" ? "Scan QR" : nil, diff --git a/apps/ios/Sources/Design/TalkProTab.swift b/apps/ios/Sources/Design/TalkProTab.swift index 5f716a6893c7..62ae65cd23b4 100644 --- a/apps/ios/Sources/Design/TalkProTab.swift +++ b/apps/ios/Sources/Design/TalkProTab.swift @@ -155,22 +155,24 @@ struct TalkProTab: View { private var conversationSection: some View { Section("Conversation") { - SettingsDetailRow("Agent", value: self.appModel.chatAgentName) - SettingsDetailRow("Session", value: self.appModel.chatSessionKey) - SettingsDetailRow("Runtime", value: self.appModel.talkMode.statusText) + SettingsDetailRow("Agent", value: .verbatim(self.appModel.chatAgentName)) + SettingsDetailRow("Session", value: .verbatim(self.appModel.chatSessionKey)) + SettingsDetailRow("Runtime", value: .localized(self.appModel.talkMode.statusText)) } } private var voiceModeSection: some View { Section("Voice Mode") { - SettingsDetailRow("Configured", value: self.appModel.talkMode.gatewayTalkVoiceModeTitle) - SettingsDetailRow("Active", value: self.activeModeText) - SettingsDetailRow("Transport", value: self.transportText) + SettingsDetailRow( + "Configured", + value: .localized(self.appModel.talkMode.gatewayTalkVoiceModeTitle)) + SettingsDetailRow("Active", value: .verbatim(self.activeModeText)) + SettingsDetailRow("Transport", value: .localized(self.transportText)) if let issueText = self.talkIssueText { - SettingsDetailRow("Last issue", value: issueText) + SettingsDetailRow("Last issue", value: .verbatim(issueText)) } - SettingsDetailRow("Permission", value: self.permissionText) - SettingsDetailRow("Speech language", value: self.speechLocaleText) + SettingsDetailRow("Permission", value: .localized(self.permissionText)) + SettingsDetailRow("Speech language", value: .verbatim(self.speechLocaleText)) } } diff --git a/apps/ios/Sources/Design/TalkRuntimeIssueBanner.swift b/apps/ios/Sources/Design/TalkRuntimeIssueBanner.swift index a5f42db8fc8e..fa84bc6006ce 100644 --- a/apps/ios/Sources/Design/TalkRuntimeIssueBanner.swift +++ b/apps/ios/Sources/Design/TalkRuntimeIssueBanner.swift @@ -9,9 +9,9 @@ struct TalkRuntimeIssueBanner: View { var body: some View { OpenClawNoticeBanner( icon: self.iconName, - title: self.issue.fallbackBannerTitle, - message: self.issue.fallbackBannerMessage, - ownerLabel: self.issue.fallbackBannerOwnerLabel, + title: .verbatim(self.issue.fallbackBannerTitle), + message: .verbatim(self.issue.fallbackBannerMessage), + ownerLabel: .verbatim(self.issue.fallbackBannerOwnerLabel), tint: self.tint, detail: .accent(self.issue.displayMessage), primaryActionTitle: "Open Settings", diff --git a/apps/ios/Sources/Gateway/DeepLinkAgentPromptAlert.swift b/apps/ios/Sources/Gateway/DeepLinkAgentPromptAlert.swift index 6a58aacf1583..e51d74b99a1b 100644 --- a/apps/ios/Sources/Gateway/DeepLinkAgentPromptAlert.swift +++ b/apps/ios/Sources/Gateway/DeepLinkAgentPromptAlert.swift @@ -16,14 +16,16 @@ struct DeepLinkAgentPromptAlert: ViewModifier { Alert( title: Text("Run OpenClaw agent?") .font(OpenClawType.headline), - message: Text( - """ + message: Text(verbatim: String( + format: String(localized: """ Message: - \(prompt.messagePreview) + %1$@ URL: - \(prompt.urlPreview) - """) + %2$@ + """), + prompt.messagePreview, + prompt.urlPreview)) .font(OpenClawType.subhead), primaryButton: .cancel( Text("Cancel") diff --git a/apps/ios/Sources/Gateway/ExecApprovalPromptDialog.swift b/apps/ios/Sources/Gateway/ExecApprovalPromptDialog.swift index 17192bb53a4b..6b44e0be68c9 100644 --- a/apps/ios/Sources/Gateway/ExecApprovalPromptDialog.swift +++ b/apps/ios/Sources/Gateway/ExecApprovalPromptDialog.swift @@ -275,22 +275,30 @@ private struct ExecApprovalPromptCard: View { guard let expiresAtMs else { return nil } let remainingSeconds = Int((Double(expiresAtMs) / 1000.0) - Date().timeIntervalSince1970) if remainingSeconds <= 0 { - return "expired" + return String(localized: "expired") } if remainingSeconds < 60 { - return "under a minute" + return String(localized: "under a minute") } if remainingSeconds < 3600 { let minutes = Int(ceil(Double(remainingSeconds) / 60.0)) - return minutes == 1 ? "about 1 minute" : "about \(minutes) minutes" + return minutes == 1 + ? String(localized: "about 1 minute") + : String( + format: String(localized: "about %@ minutes"), + minutes.formatted()) } let hours = Int(ceil(Double(remainingSeconds) / 3600.0)) - return hours == 1 ? "about 1 hour" : "about \(hours) hours" + return hours == 1 + ? String(localized: "about 1 hour") + : String( + format: String(localized: "about %@ hours"), + hours.formatted()) } } private struct ExecApprovalPromptMetadataRow: View { - let label: String + let label: LocalizedStringKey let value: String var body: some View { @@ -298,7 +306,7 @@ private struct ExecApprovalPromptMetadataRow: View { Text(self.label) .font(OpenClawType.caption) .foregroundStyle(.secondary) - Text(self.value) + Text(verbatim: self.value) .font(OpenClawType.footnote) .textSelection(.enabled) } diff --git a/apps/ios/Sources/Gateway/GatewayConnectionController+Capabilities.swift b/apps/ios/Sources/Gateway/GatewayConnectionController+Capabilities.swift index 0de000d5e9b7..a154bdb8996a 100644 --- a/apps/ios/Sources/Gateway/GatewayConnectionController+Capabilities.swift +++ b/apps/ios/Sources/Gateway/GatewayConnectionController+Capabilities.swift @@ -39,11 +39,11 @@ extension GatewayConnectionController { let requiresTLS = !trimmedHost.isEmpty && !LoopbackHost.isLocalNetworkHost(trimmedHost) let effectiveTLS = requestedTLS || requiresTLS let helperText: String? = if requiresTLS { - "Secure connection is required for this host." + String(localized: "Secure connection is required for this host.") } else if effectiveTLS { nil } else { - "Use only on a trusted private network." + String(localized: "Use only on a trusted private network.") } return GatewayManualTransportPresentation( requiresTLS: requiresTLS, diff --git a/apps/ios/Sources/Gateway/GatewayConnectionController.swift b/apps/ios/Sources/Gateway/GatewayConnectionController.swift index e93ea0df97d0..2a17ae47cfe7 100644 --- a/apps/ios/Sources/Gateway/GatewayConnectionController.swift +++ b/apps/ios/Sources/Gateway/GatewayConnectionController.swift @@ -1281,22 +1281,43 @@ extension GatewayConnectionController { switch failure { case .endpointUnreachable: if host.lowercased().trimmingCharacters(in: CharacterSet(charactersIn: ".")).hasSuffix(".ts.net") { - String(localized: """ - Can't reach gateway at \(host):\(port). \ - Verify Tailscale Serve is enabled and publishes this Gateway. - """) + String( + format: String(localized: """ + Can't reach gateway at %1$@:%2$@. \ + Verify Tailscale Serve is enabled and publishes this Gateway. + """), + host, + port.formatted()) } else { - String(localized: "Can't reach gateway at \(host):\(port). Check Tailscale or LAN.") + String( + format: String( + localized: "Can't reach gateway at %1$@:%2$@. Check Tailscale or LAN."), + host, + port.formatted()) } case .tlsHandshakeTimeout: - "TLS fingerprint verification timed out for \(host):\(port). " - + "Secure endpoint was reached, but TLS did not finish in time." + String( + format: String(localized: """ + TLS fingerprint verification timed out for %1$@:%2$@. \ + Secure endpoint was reached, but TLS did not finish in time. + """), + host, + port.formatted()) case .tlsUnavailable: - "No secure gateway endpoint was detected at \(host):\(port). " - + "Enable gateway TLS or Tailscale Serve, or use a trusted private LAN address " - + "with Unencrypted selected." + String( + format: String(localized: """ + No secure gateway endpoint was detected at %1$@:%2$@. \ + Enable gateway TLS or Tailscale Serve, or use a trusted private LAN address \ + with Unencrypted selected. + """), + host, + port.formatted()) case .certificateUnavailable: - "Could not read the TLS certificate from \(host):\(port)." + String( + format: String( + localized: "Could not read the TLS certificate from %1$@:%2$@."), + host, + port.formatted()) } } diff --git a/apps/ios/Sources/Gateway/GatewayProblemView.swift b/apps/ios/Sources/Gateway/GatewayProblemView.swift index 199294f23fd1..3c445f5bbd2f 100644 --- a/apps/ios/Sources/Gateway/GatewayProblemView.swift +++ b/apps/ios/Sources/Gateway/GatewayProblemView.swift @@ -11,12 +11,12 @@ struct GatewayProblemBanner: View { var body: some View { OpenClawNoticeBanner( icon: self.iconName, - title: self.problem.title, - message: self.problem.message, - ownerLabel: self.ownerLabel, + title: .localized(self.problem.title), + message: .localized(self.problem.message), + ownerLabel: .localized(self.ownerLabel), tint: self.tint, detail: self.problem.requestId.map(OpenClawNoticeDetail.requestID), - primaryActionTitle: self.primaryActionTitle, + primaryActionTitle: self.primaryActionTitle.map(OpenClawTextValue.localized), onPrimaryAction: self.onPrimaryAction, secondaryActionTitle: "Details", onSecondaryAction: self.onShowDetails) @@ -88,12 +88,12 @@ struct GatewayProblemDetailsSheet: View { List { Section { VStack(alignment: .leading, spacing: 10) { - Text(self.problem.title) + Text(LocalizedStringKey(self.problem.title)) .font(OpenClawType.title3) - Text(self.problem.message) + Text(LocalizedStringKey(self.problem.message)) .font(OpenClawType.body) .foregroundStyle(.secondary) - Text(self.ownerSummary) + Text(LocalizedStringKey(self.ownerSummary)) .font(OpenClawType.footnoteSemiBold) .foregroundStyle(.secondary) } @@ -170,7 +170,7 @@ struct GatewayProblemDetailsSheet: View { if let copyFeedback { Section { - Text(copyFeedback) + Text(verbatim: copyFeedback) .font(OpenClawType.footnote) .foregroundStyle(.secondary) } @@ -189,7 +189,7 @@ struct GatewayProblemDetailsSheet: View { self.dismiss() onPrimaryAction() } label: { - Text(primaryActionTitle) + Text(LocalizedStringKey(primaryActionTitle)) .font(OpenClawType.subheadSemiBold) } .font(OpenClawType.subheadSemiBold) diff --git a/apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift b/apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift index 42c304cce90c..7259c01e6c7f 100644 --- a/apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift +++ b/apps/ios/Sources/Gateway/GatewayQuickSetupSheet.swift @@ -84,7 +84,7 @@ struct GatewayQuickSetupSheet: View { Image(systemName: "lock.shield.fill") .foregroundStyle(OpenClawBrand.warn) VStack(alignment: .leading, spacing: 3) { - Text(availability.actionTitle) + Text(LocalizedStringKey(availability.actionTitle)) .font(OpenClawType.subheadSemiBold) Text(guidanceText) .font(OpenClawType.caption) @@ -369,7 +369,7 @@ private struct GatewayQuickSetupErrorView: View { Image(systemName: "exclamationmark.triangle.fill") .foregroundStyle(OpenClawBrand.warn) .padding(.top, 1) - Text(self.message) + Text(verbatim: self.message) .font(OpenClawType.footnote) .foregroundStyle(.secondary) .textSelection(.enabled) diff --git a/apps/ios/Sources/LiveActivity/LiveActivityManager.swift b/apps/ios/Sources/LiveActivity/LiveActivityManager.swift index 5617ed9d3e20..6487042377cc 100644 --- a/apps/ios/Sources/LiveActivity/LiveActivityManager.swift +++ b/apps/ios/Sources/LiveActivity/LiveActivityManager.swift @@ -17,7 +17,11 @@ final class LiveActivityManager { self.hydrateCurrentAndPruneDuplicates() } - func showConnecting(statusText: String = "Connecting...", agentName: String, sessionKey: String) { + func showConnecting( + statusText: String = String(localized: "Connecting..."), + agentName: String, + sessionKey: String) + { self.hydrateCurrentAndPruneDuplicates() if self.currentActivity != nil { @@ -77,7 +81,7 @@ final class LiveActivityManager { self.updateCurrent(state: self.attentionState(statusText: statusText), staleDate: nil) } - func handleConnecting(statusText: String = "Connecting...") { + func handleConnecting(statusText: String = String(localized: "Connecting...")) { self.updateCurrent( state: self.connectingState(statusText: statusText), staleDate: Date().addingTimeInterval(self.connectingStaleSeconds)) @@ -172,7 +176,7 @@ final class LiveActivityManager { private func disconnectedState() -> OpenClawActivityAttributes.ContentState { OpenClawActivityAttributes.ContentState( - statusText: "Disconnected", + statusText: String(localized: "Disconnected"), isIdle: false, isDisconnected: true, isConnecting: false, diff --git a/apps/ios/Sources/Model/NodeAppModel.swift b/apps/ios/Sources/Model/NodeAppModel.swift index a356986cb0a3..8317e8a09472 100644 --- a/apps/ios/Sources/Model/NodeAppModel.swift +++ b/apps/ios/Sources/Model/NodeAppModel.swift @@ -3735,7 +3735,9 @@ extension NodeAppModel { } if problem.needsPairingApproval || problem.pauseReconnect { LiveActivityManager.shared.showAttention( - statusText: problem.needsPairingApproval ? "Approval needed" : "Action required", + statusText: problem.needsPairingApproval + ? String(localized: "Approval needed") + : String(localized: "Action required"), agentName: self.activeAgentName, sessionKey: self.mainSessionKey) } @@ -3752,7 +3754,9 @@ extension NodeAppModel { } if problem.needsPairingApproval || problem.pauseReconnect { LiveActivityManager.shared.showAttention( - statusText: problem.needsPairingApproval ? "Approval needed" : "Action required", + statusText: problem.needsPairingApproval + ? String(localized: "Approval needed") + : String(localized: "Action required"), agentName: self.activeAgentName, sessionKey: self.mainSessionKey) } @@ -4417,7 +4421,9 @@ extension NodeAppModel { self.gatewayServerName = nil self.gatewayRemoteAddress = nil LiveActivityManager.shared.showConnecting( - statusText: (attempt == 0) ? "Connecting..." : "Reconnecting...", + statusText: (attempt == 0) + ? String(localized: "Connecting...") + : String(localized: "Reconnecting..."), agentName: self.activeAgentName, sessionKey: self.mainSessionKey) } @@ -5847,7 +5853,7 @@ extension NodeAppModel { guard self.isOperatorGatewayConnected else { return WatchChatPreview( items: [], - statusText: "Connect iPhone chat to read messages") + statusText: String(localized: "Connect iPhone chat to read messages")) } payload = try await IOSGatewayChatTransport(gateway: self.operatorSession) .requestHistory(sessionKey: self.chatSessionKey) @@ -5859,7 +5865,9 @@ extension NodeAppModel { statusText: items.isEmpty ? "No chat messages yet" : nil) } catch { GatewayDiagnostics.log("watch app snapshot: chat preview failed error=\(error.localizedDescription)") - return WatchChatPreview(items: [], statusText: "Chat unavailable") + return WatchChatPreview( + items: [], + statusText: String(localized: "Chat unavailable")) } } diff --git a/apps/ios/Sources/Onboarding/OnboardingWizardConnectionSections.swift b/apps/ios/Sources/Onboarding/OnboardingWizardConnectionSections.swift index b96197813886..2069314e96dd 100644 --- a/apps/ios/Sources/Onboarding/OnboardingWizardConnectionSections.swift +++ b/apps/ios/Sources/Onboarding/OnboardingWizardConnectionSections.swift @@ -139,7 +139,7 @@ struct OnboardingConnectPhaseView: View { OpenClawNoticeBanner( icon: "exclamationmark.triangle.fill", title: "Connection Failed", - message: message, + message: .verbatim(message), ownerLabel: "Needs attention", tint: OpenClawBrand.danger, primaryActionTitle: allowsRetry ? "Retry" : nil, diff --git a/apps/ios/Sources/Onboarding/OnboardingWizardView.swift b/apps/ios/Sources/Onboarding/OnboardingWizardView.swift index b4676df62866..01d54716462c 100644 --- a/apps/ios/Sources/Onboarding/OnboardingWizardView.swift +++ b/apps/ios/Sources/Onboarding/OnboardingWizardView.swift @@ -586,17 +586,23 @@ struct OnboardingWizardView: View { } footer: { let requestLine: String = { if let id = self.currentProblem?.requestId ?? self.issue.requestId, !id.isEmpty { - return "Request ID: \(id)" + return String( + format: String(localized: "Request ID: %@"), + id) } - return "Request ID: check `openclaw devices list`." + return String(localized: "Request ID: check `openclaw devices list`.") }() let commandLine = self.currentProblem?.actionCommand ?? "openclaw devices approve " - Text( - "Approve this device on the gateway.\n" - + "1) `\(commandLine)`\n" - + "2) `/pair approve` in your OpenClaw chat\n" - + "\(requestLine)\n" - + "OpenClaw will also retry automatically when you return to this app.") + Text(verbatim: String( + format: String(localized: """ + Approve this device on the gateway. + 1) `%1$@` + 2) `/pair approve` in your OpenClaw chat + %2$@ + OpenClaw will also retry automatically when you return to this app. + """), + commandLine, + requestLine)) .font(OpenClawType.caption) } } diff --git a/apps/ios/Sources/RootTabs.swift b/apps/ios/Sources/RootTabs.swift index 3561fcc965a1..5d005ad12376 100644 --- a/apps/ios/Sources/RootTabs.swift +++ b/apps/ios/Sources/RootTabs.swift @@ -340,7 +340,10 @@ struct RootTabs: View { self.sidebarHorizontalSeparator } .accessibilityElement(children: .combine) - .accessibilityLabel("OpenClaw \(self.sidebarGatewayStatusTitle)") + .accessibilityLabel( + String( + format: String(localized: "OpenClaw %@"), + self.sidebarGatewayStatusTitle)) } private var sidebarGatewayStatusTitle: String { diff --git a/apps/ios/Sources/Settings/PrivacyAccessSectionView.swift b/apps/ios/Sources/Settings/PrivacyAccessSectionView.swift index 1b4508213469..94788a1becfb 100644 --- a/apps/ios/Sources/Settings/PrivacyAccessSectionView.swift +++ b/apps/ios/Sources/Settings/PrivacyAccessSectionView.swift @@ -77,18 +77,18 @@ struct PrivacyAccessSectionView: View { { VStack(alignment: .leading, spacing: 6) { HStack { - Label(title, systemImage: icon) + Label(LocalizedStringKey(title), systemImage: icon) .font(OpenClawType.subheadSemiBold) Spacer() - OpenClawStatusBadge(label: status, tone: self.statusTone(for: status)) + OpenClawStatusBadge(label: .localized(status), tone: self.statusTone(for: status)) .accessibilityIdentifier("privacy-access-\(title)-status") } - Text(detail) + Text(LocalizedStringKey(detail)) .font(OpenClawType.footnote) .foregroundStyle(.secondary) if let actionTitle, let action { Button(action: action) { - Text(actionTitle) + Text(LocalizedStringKey(actionTitle)) .font(OpenClawType.footnoteSemiBold) } .buttonStyle(.bordered) diff --git a/apps/ios/Sources/Voice/TalkGatewayPermissionState.swift b/apps/ios/Sources/Voice/TalkGatewayPermissionState.swift index 3cc9203f7ee0..f4120bc81a99 100644 --- a/apps/ios/Sources/Voice/TalkGatewayPermissionState.swift +++ b/apps/ios/Sources/Voice/TalkGatewayPermissionState.swift @@ -1,3 +1,5 @@ +import Foundation + enum TalkGatewayPermissionState: Equatable { case unknown case ready @@ -11,21 +13,21 @@ enum TalkGatewayPermissionState: Equatable { var statusLabel: String { switch self { case .unknown: - "Not checked" + String(localized: "Not checked") case .ready: - "Ready" + String(localized: "Ready") case let .missingScope(scope): - "Missing \(scope)" + String(format: String(localized: "Missing %@"), scope) case .requestingUpgrade: - "Requesting approval" + String(localized: "Requesting approval") case .upgradeRequested: - "Approval requested" + String(localized: "Approval requested") case .requestFailed: - "Request failed" + String(localized: "Request failed") case .apiKeyMissing: - "API key missing" + String(localized: "API key missing") case .loadFailed: - "Load failed" + String(localized: "Load failed") } } diff --git a/apps/ios/Sources/Voice/TalkModeGatewayConfig.swift b/apps/ios/Sources/Voice/TalkModeGatewayConfig.swift index 34e37bb7b4ae..7cf33f826176 100644 --- a/apps/ios/Sources/Voice/TalkModeGatewayConfig.swift +++ b/apps/ios/Sources/Voice/TalkModeGatewayConfig.swift @@ -37,23 +37,23 @@ struct TalkRuntimeIssue: Equatable { var displayMessage: String { if !self.message.isEmpty { return self.message } - return "Realtime voice did not start." + return String(localized: "Realtime voice did not start.") } var fallbackStatusText: String { - "Listening (iOS Speech fallback)" + String(localized: "Listening (iOS Speech fallback)") } var fallbackBannerTitle: String { - "Using iOS Speech fallback" + String(localized: "Using iOS Speech fallback") } var fallbackBannerOwnerLabel: String { - "Fallback active" + String(localized: "Fallback active") } var fallbackBannerMessage: String { - "Realtime voice did not start. Talk is running with iOS speech recognition and TTS." + String(localized: "Realtime voice did not start. Talk is running with iOS speech recognition and TTS.") } var technicalDetails: String { diff --git a/apps/ios/Sources/Voice/TalkModeManager+Permissions.swift b/apps/ios/Sources/Voice/TalkModeManager+Permissions.swift index 38378d4fb869..f5b624be3ea5 100644 --- a/apps/ios/Sources/Voice/TalkModeManager+Permissions.swift +++ b/apps/ios/Sources/Voice/TalkModeManager+Permissions.swift @@ -62,15 +62,25 @@ extension TalkModeManager { { switch status { case .denied: - return "\(kind) permission denied" + return String( + format: String(localized: "%@ permission denied"), + kind) case .restricted: - return "\(kind) permission restricted" + return String( + format: String(localized: "%@ permission restricted"), + kind) case .notDetermined: - return "\(kind) permission not granted" + return String( + format: String(localized: "%@ permission not granted"), + kind) case .authorized: - return "\(kind) permission denied" + return String( + format: String(localized: "%@ permission denied"), + kind) @unknown default: - return "\(kind) permission denied" + return String( + format: String(localized: "%@ permission denied"), + kind) } } } diff --git a/apps/ios/Sources/Voice/TalkModeManager.swift b/apps/ios/Sources/Voice/TalkModeManager.swift index 50911e265c7b..f4824daa1ccd 100644 --- a/apps/ios/Sources/Voice/TalkModeManager.swift +++ b/apps/ios/Sources/Voice/TalkModeManager.swift @@ -238,7 +238,7 @@ final class TalkModeManager: NSObject { private var realtimeModelId: String? private var realtimeVoiceId: String? private var configuredVoiceModeDescriptor = TalkVoiceModeDescriptor( - title: "Not loaded", + title: String(localized: "Not loaded"), subtitle: nil, providerId: nil, modelId: nil, @@ -395,7 +395,9 @@ final class TalkModeManager: NSObject { self.gatewayTalkActiveModeSubtitle = nil guard shouldRestart else { if self.isEnabled { - self.statusText = self.gatewayConnected ? "Ready" : "Offline" + self.statusText = self.gatewayConnected + ? String(localized: "Ready") + : String(localized: "Offline") } return } @@ -416,7 +418,7 @@ final class TalkModeManager: NSObject { self.scheduleRealtimeRestart(after: nil, generation: restartGeneration) return } - self.statusText = "Reconnecting" + self.statusText = String(localized: "Reconnecting") self.scheduleRealtimeRestart(after: delay, generation: restartGeneration) } @@ -463,7 +465,7 @@ final class TalkModeManager: NSObject { self.gatewayTalkActiveModeTitle = "Not active" self.gatewayTalkActiveModeSubtitle = nil if self.isEnabled, !self.isSpeaking { - self.statusText = "Offline" + self.statusText = String(localized: "Offline") } self.realtimePrefetchGeneration &+= 1 self.realtimePrefetchTask?.cancel() @@ -512,23 +514,24 @@ final class TalkModeManager: NSObject { self.isListening = false self.isSpeaking = false self.isUserSpeechDetected = false - self.statusText = "Ready" + self.statusText = String(localized: "Ready") self.gatewayTalkConfigLoaded = true self.gatewayTalkApiKeyConfigured = true self.gatewayTalkDefaultModelId = "gpt-realtime-2" self.gatewayTalkDefaultVoiceId = "marin" self.gatewayTalkProviderLabel = "OpenAI" - self.gatewayTalkTransportLabel = "Gateway Relay" + self.gatewayTalkTransportLabel = String(localized: "Gateway Relay") self.gatewayTalkUsesRealtime = true self.gatewayTalkUsesRealtimeRelay = true self.gatewayTalkRealtimeProviderLabel = "OpenAI" self.gatewayTalkRealtimeModelId = "gpt-realtime-2" self.gatewayTalkRealtimeVoiceId = "marin" - self.gatewayTalkVoiceModeTitle = "Realtime Voice" - self.gatewayTalkVoiceModeSubtitle = "Gateway relay ready" - self.gatewayTalkVoiceModeAccessibilityValue = "Realtime Voice, Gateway relay ready" - self.gatewayTalkActiveModeTitle = "Ready" - self.gatewayTalkActiveModeSubtitle = "Listening starts from this phone" + self.gatewayTalkVoiceModeTitle = String(localized: "Realtime Voice") + self.gatewayTalkVoiceModeSubtitle = String(localized: "Gateway relay ready") + self.gatewayTalkVoiceModeAccessibilityValue = String( + localized: "Realtime Voice, Gateway relay ready") + self.gatewayTalkActiveModeTitle = String(localized: "Ready") + self.gatewayTalkActiveModeSubtitle = String(localized: "Listening starts from this phone") self.gatewayTalkLastIssueText = nil self.gatewayTalkCurrentFallbackIssue = nil self.gatewayTalkPermissionState = .ready @@ -594,7 +597,7 @@ final class TalkModeManager: NSObject { } #endif self.logger.info("start") - self.statusText = "Requesting permissions…" + self.statusText = String(localized: "Requesting permissions…") let permissionStartedAt = Self.nowSeconds() let micOk = if self.allowSimulatorCapture { true @@ -606,14 +609,14 @@ final class TalkModeManager: NSObject { + "elapsedMs=\(Self.elapsedMs(since: permissionStartedAt))") guard micOk else { self.logger.warning("start blocked: microphone permission denied") - self.statusText = "Microphone permission denied" + self.statusText = String(localized: "Microphone permission denied") return } guard self.isCurrentStartAttempt(attemptID) else { return } await self.ensureTalkConfigLoadedForStart() guard self.isCurrentStartAttempt(attemptID) else { return } if self.gatewayTalkPermissionState.requiresTalkPermissionAction { - self.statusText = "Gateway permission required" + self.statusText = String(localized: "Gateway permission required") GatewayDiagnostics.log("talk.timeline manager start blocked gateway permission") return } @@ -642,7 +645,7 @@ final class TalkModeManager: NSObject { self.stopNativeCaptureAndDiscardTranscript() self.deactivateAudioSession() self.statusText = Self.permissionMessage( - kind: "Speech recognition", + kind: String(localized: "Speech recognition"), status: SFSpeechRecognizer.authorizationStatus()) return } @@ -665,7 +668,9 @@ final class TalkModeManager: NSObject { } catch { self.stopNativeCaptureAndDiscardTranscript() self.deactivateAudioSession() - self.statusText = "Start failed: \(error.localizedDescription)" + self.statusText = String( + format: String(localized: "Start failed: %@"), + error.localizedDescription) self.logger.error("start failed: \(error.localizedDescription, privacy: .public)") } } @@ -675,7 +680,7 @@ final class TalkModeManager: NSObject { guard self.captureMode != .pushToTalk else { return false } guard self.finishingPushToTalk == nil else { return false } guard self.foregroundAudioCaptureAllowed else { - self.statusText = "Paused" + self.statusText = String(localized: "Paused") GatewayDiagnostics.log("talk start ignored: app backgrounded") return false } @@ -688,7 +693,7 @@ final class TalkModeManager: NSObject { return false } guard self.gatewayConnected else { - self.statusText = "Offline" + self.statusText = String(localized: "Offline") GatewayDiagnostics.log("talk.timeline manager start blocked gateway offline") return false } @@ -736,7 +741,7 @@ final class TalkModeManager: NSObject { self.gatewayTalkProviderLabel = TalkModeProviderSelection.openAIRealtime.label self.gatewayTalkUsesRealtime = true self.gatewayTalkUsesRealtimeRelay = false - self.gatewayTalkTransportLabel = "Native WebRTC" + self.gatewayTalkTransportLabel = String(localized: "Native WebRTC") self.gatewayTalkRealtimeProviderLabel = Self.displayName(forProvider: self.realtimeProvider ?? "openai") self.gatewayTalkRealtimeModelId = self.realtimeModelId self.gatewayTalkRealtimeVoiceId = self.realtimeVoiceId @@ -753,7 +758,7 @@ final class TalkModeManager: NSObject { self.isUserSpeechDetected = false self.isPushToTalkActive = false self.captureMode = .idle - self.statusText = "Off" + self.statusText = String(localized: "Off") self.pendingRealtimeIssue = nil self.gatewayTalkCurrentFallbackIssue = nil self.gatewayTalkActiveModeTitle = "Not active" @@ -794,7 +799,7 @@ final class TalkModeManager: NSObject { self.foregroundPushToTalkAllowed = false guard self.isEnabled || self.activePTTCaptureId != nil || self.finishingPushToTalk != nil else { return } if keepContinuousActive { - self.statusText = self.isListening ? "Listening" : self.statusText + self.statusText = self.isListening ? String(localized: "Listening") : self.statusText return } self.cancelFinishingPushToTalk() @@ -803,7 +808,7 @@ final class TalkModeManager: NSObject { self.isListening = false self.isPushToTalkActive = false self.captureMode = .idle - self.statusText = "Paused" + self.statusText = String(localized: "Paused") self.gatewayTalkActiveModeTitle = "Paused" self.gatewayTalkActiveModeSubtitle = nil self.lastTranscript = "" @@ -928,12 +933,12 @@ final class TalkModeManager: NSObject { try self.ensurePushToTalkStartCurrent(captureId: captureId, canStartCapture: canStartCapture) } #endif - self.statusText = "Requesting permissions…" + self.statusText = String(localized: "Requesting permissions…") if !self.allowSimulatorCapture { let micOk = await Self.requestMicrophonePermission() try self.ensurePushToTalkStartCurrent(captureId: captureId, canStartCapture: canStartCapture) guard micOk else { - self.statusText = "Microphone permission denied" + self.statusText = String(localized: "Microphone permission denied") throw NSError(domain: "TalkMode", code: 4, userInfo: [ NSLocalizedDescriptionKey: "Microphone permission denied", ]) @@ -942,7 +947,7 @@ final class TalkModeManager: NSObject { try self.ensurePushToTalkStartCurrent(captureId: captureId, canStartCapture: canStartCapture) guard speechOk else { self.statusText = Self.permissionMessage( - kind: "Speech recognition", + kind: String(localized: "Speech recognition"), status: SFSpeechRecognizer.authorizationStatus()) throw NSError(domain: "TalkMode", code: 5, userInfo: [ NSLocalizedDescriptionKey: "Speech recognition permission denied", @@ -958,7 +963,7 @@ final class TalkModeManager: NSObject { try self.ensurePushToTalkStartCurrent(captureId: captureId, canStartCapture: canStartCapture) self.isListening = true self.isPushToTalkActive = true - self.statusText = "Listening (PTT)" + self.statusText = String(localized: "Listening (PTT)") } catch { if self.activePTTCaptureId == captureId { self.stopRecognition() @@ -971,9 +976,11 @@ final class TalkModeManager: NSObject { let isPermissionError = nsError.domain == "TalkMode" && (nsError.code == 4 || nsError.code == 5) let isCancelled = error is CancellationError || (nsError.domain == "TalkMode" && nsError.code == 9) if isCancelled { - self.statusText = "Ready" + self.statusText = String(localized: "Ready") } else if !isPermissionError { - self.statusText = "Start failed: \(error.localizedDescription)" + self.statusText = String( + format: String(localized: "Start failed: %@"), + error.localizedDescription) } } let shouldResume = self.isEnabled @@ -1004,7 +1011,7 @@ final class TalkModeManager: NSObject { self.pttTimeoutTask?.cancel() self.pttTimeoutTask = nil self.pttAutoStopEnabled = false - self.statusText = "Ready" + self.statusText = String(localized: "Ready") self.finishActivePushToTalk(captureId) let payload = OpenClawTalkPTTStopPayload( captureId: captureId, @@ -1029,7 +1036,7 @@ final class TalkModeManager: NSObject { self.lastHeard = nil guard !transcript.isEmpty else { - self.statusText = "Ready" + self.statusText = String(localized: "Ready") let shouldResume = self.isEnabled self.finishActivePushToTalk(captureId) let payload = OpenClawTalkPTTStopPayload( @@ -1042,7 +1049,7 @@ final class TalkModeManager: NSObject { } guard self.gatewayConnected else { - self.statusText = "Gateway not connected" + self.statusText = String(localized: "Gateway not connected") let shouldResume = self.isEnabled self.finishActivePushToTalk(captureId) let payload = OpenClawTalkPTTStopPayload( @@ -1148,7 +1155,7 @@ final class TalkModeManager: NSObject { self.pttTimeoutTask?.cancel() self.pttTimeoutTask = nil self.finishActivePushToTalk(captureId) - self.statusText = "Ready" + self.statusText = String(localized: "Ready") let payload = OpenClawTalkPTTStopPayload( captureId: captureId, @@ -1183,7 +1190,7 @@ final class TalkModeManager: NSObject { } private func pushToTalkOfflineError() -> NSError { - self.statusText = "Offline" + self.statusText = String(localized: "Offline") return NSError(domain: "TalkMode", code: 7, userInfo: [ NSLocalizedDescriptionKey: "Gateway not connected", ]) @@ -1216,7 +1223,7 @@ final class TalkModeManager: NSObject { { precondition(self.finishingPushToTalk == nil) let generation = self.beginTranscriptProcessing() - self.statusText = "Thinking…" + self.statusText = String(localized: "Thinking…") let task = Task { @MainActor [weak self] in guard let self else { return } @@ -1256,7 +1263,9 @@ final class TalkModeManager: NSObject { self.invalidateTranscriptProcessing() self.stopSpeaking(storeInterruption: false) if hadFinishingPushToTalk { - self.statusText = self.gatewayConnected ? "Ready" : "Offline" + self.statusText = self.gatewayConnected + ? String(localized: "Ready") + : String(localized: "Offline") } } @@ -1277,7 +1286,9 @@ final class TalkModeManager: NSObject { self.pttAudioOwnershipEndHandler?(captureId) let transientStatuses = ["Thinking…", "Generating voice…", "Speaking…", "Speaking (System)…"] if transientStatuses.contains(self.statusText) { - self.statusText = self.gatewayConnected ? "Ready" : "Offline" + self.statusText = self.gatewayConnected + ? String(localized: "Ready") + : String(localized: "Offline") } self.scheduleContinuousResume(self.isEnabled) } @@ -1451,7 +1462,7 @@ final class TalkModeManager: NSObject { if isCancellation { GatewayDiagnostics.log("talk speech: cancelled") if self.captureMode == .continuous, self.isEnabled, !self.isSpeaking { - self.statusText = "Listening" + self.statusText = String(localized: "Listening") } self.logger.debug("speech recognition cancelled") return false @@ -1461,9 +1472,13 @@ final class TalkModeManager: NSObject { if !self.isSpeaking { if msg.localizedCaseInsensitiveContains("no speech detected") { // Treat as transient silence. Don't scare users with an error banner. - self.statusText = self.isEnabled ? "Listening" : "Speech error: \(msg)" + self.statusText = self.isEnabled + ? String(localized: "Listening") + : String(format: String(localized: "Speech error: %@"), msg) } else { - self.statusText = "Speech error: \(msg)" + self.statusText = String( + format: String(localized: "Speech error: %@"), + msg) } } self.logger.debug("speech recognition error: \(msg, privacy: .public)") @@ -1490,7 +1505,7 @@ final class TalkModeManager: NSObject { try self.startRecognition() self.isListening = true if self.statusText.localizedCaseInsensitiveContains("speech error") { - self.statusText = "Listening" + self.statusText = String(localized: "Listening") } GatewayDiagnostics.log("talk speech: recognition restarted") } catch { @@ -1679,7 +1694,7 @@ final class TalkModeManager: NSObject { } } guard let gateway = self.gateway else { - self.statusText = "Gateway not connected" + self.statusText = String(localized: "Gateway not connected") self.scheduleContinuousResume(restartAfter) return } @@ -1747,7 +1762,7 @@ final class TalkModeManager: NSObject { self.isListening = false self.isUserSpeechDetected = false self.captureMode = .idle - self.statusText = "Thinking…" + self.statusText = String(localized: "Thinking…") self.lastTranscript = "" self.lastHeard = nil self.stopRecognition() @@ -1759,7 +1774,7 @@ final class TalkModeManager: NSObject { shouldApply: { self.isCurrentTranscriptProcessing(generation) }) guard self.isCurrentTranscriptProcessing(generation) else { return } guard await gateway.currentRoute() == gatewayRoute else { - self.statusText = "Gateway not connected" + self.statusText = String(localized: "Gateway not connected") return } guard self.isCurrentTranscriptProcessing(generation) else { return } @@ -1798,7 +1813,9 @@ final class TalkModeManager: NSObject { "chat.send ok runId=\(runId, privacy: .public) status=\(normalizedStatus, privacy: .public)") GatewayDiagnostics.log("talk: chat.send ok runId=\(runId) status=\(normalizedStatus)") if Self.isTerminalChatSendFailure(acknowledgement.status) { - streamingOwner.terminalStatus = normalizedStatus == "error" ? "Chat error" : "Aborted" + streamingOwner.terminalStatus = normalizedStatus == "error" + ? String(localized: "Chat error") + : String(localized: "Aborted") self.logger.warning( """ chat.send terminal ack runId=\(runId, privacy: .public) \ @@ -1819,13 +1836,15 @@ final class TalkModeManager: NSObject { else { return } guard self.isCurrentTranscriptProcessing(generation) else { return } if completedSuccessfully, !self.isEnabled { - streamingOwner.terminalStatus = "Ready" + streamingOwner.terminalStatus = String(localized: "Ready") } } catch is CancellationError { return } catch { guard self.isCurrentTranscriptProcessing(generation) else { return } - streamingOwner.terminalStatus = "Talk failed: \(error.localizedDescription)" + streamingOwner.terminalStatus = String( + format: String(localized: "Talk failed: %@"), + error.localizedDescription) self.logger.error("finalize failed: \(error.localizedDescription, privacy: .public)") GatewayDiagnostics.log("talk: failed error=\(error.localizedDescription)") } @@ -1882,7 +1901,7 @@ final class TalkModeManager: NSObject { streamingOwner.task?.cancel() await self.finishIncrementalSpeech() guard self.isCurrentTranscriptProcessing(generation) else { return nil } - streamingOwner.terminalStatus = "Aborted" + streamingOwner.terminalStatus = String(localized: "Aborted") return nil } else if completion.state == .error { self.logger.warning("chat completion error runId=\(runId, privacy: .public)") @@ -1890,7 +1909,7 @@ final class TalkModeManager: NSObject { streamingOwner.task?.cancel() await self.finishIncrementalSpeech() guard self.isCurrentTranscriptProcessing(generation) else { return nil } - streamingOwner.terminalStatus = "Chat error" + streamingOwner.terminalStatus = String(localized: "Chat error") return nil } } @@ -1918,7 +1937,7 @@ final class TalkModeManager: NSObject { streamingOwner.task?.cancel() await self.finishIncrementalSpeech() guard self.isCurrentTranscriptProcessing(generation) else { return nil } - streamingOwner.terminalStatus = "No reply" + streamingOwner.terminalStatus = String(localized: "No reply") return nil } self.logger.info("assistant text ok chars=\(assistantText.count, privacy: .public)") @@ -2007,7 +2026,7 @@ final class TalkModeManager: NSObject { return .unavailable(realtimeIssue(message: "Gateway not connected", phase: "start")) } guard self.foregroundAudioCaptureAllowed else { - self.statusText = "Paused" + self.statusText = String(localized: "Paused") GatewayDiagnostics.log("talk realtime ignored: app backgrounded") return .ignored } @@ -2424,7 +2443,7 @@ final class TalkModeManager: NSObject { self.speechGeneration += 1 let speechGeneration = self.speechGeneration - self.statusText = "Generating voice…" + self.statusText = String(localized: "Generating voice…") self.isSpeaking = true self.lastSpokenText = cleaned defer { @@ -2456,7 +2475,9 @@ final class TalkModeManager: NSObject { try await self.playSystemVoice(text: cleaned, language: language) } catch { guard !Task.isCancelled, self.speechGeneration == speechGeneration else { return } - self.statusText = "Speak failed: \(error.localizedDescription)" + self.statusText = String( + format: String(localized: "Speak failed: %@"), + error.localizedDescription) self.logger.error("system voice failed: \(error.localizedDescription, privacy: .public)") } } @@ -2519,7 +2540,7 @@ final class TalkModeManager: NSObject { self.startSpeechInterruptionRecognitionIfNeeded() - self.statusText = "Speaking…" + self.statusText = String(localized: "Speaking…") let result = await self.playElevenLabsStream( rawStream, sampleRate: TalkTTSValidation.pcmSampleRate(from: outputFormat)) @@ -2555,7 +2576,9 @@ final class TalkModeManager: NSObject { try await self.playSystemVoice(text: cleaned, language: language) } catch { guard !Task.isCancelled, self.speechGeneration == speechGeneration else { return } - self.statusText = "Speak failed: \(error.localizedDescription)" + self.statusText = String( + format: String(localized: "Speak failed: %@"), + error.localizedDescription) self.logger.error("system voice failed: \(error.localizedDescription, privacy: .public)") } } @@ -2606,7 +2629,7 @@ final class TalkModeManager: NSObject { transport: "native", isRealtime: false)) self.startSpeechInterruptionRecognitionIfNeeded() - self.statusText = "Speaking…" + self.statusText = String(localized: "Speaking…") let result: StreamingPlaybackResult switch audio.playbackMode { case let .pcm(sampleRate): @@ -2649,7 +2672,7 @@ final class TalkModeManager: NSObject { transport: "native", isRealtime: false)) self.startSpeechInterruptionRecognitionIfNeeded() - self.statusText = "Speaking (System)…" + self.statusText = String(localized: "Speaking (System)…") try await TalkSystemSpeechSynthesizer.shared.speak(text: text, language: language) } @@ -2844,7 +2867,7 @@ final class TalkModeManager: NSObject { while !Task.isCancelled { guard !self.incrementalSpeechQueue.isEmpty else { break } let segment = self.incrementalSpeechQueue.removeFirst() - self.statusText = "Speaking…" + self.statusText = String(localized: "Speaking…") self.isSpeaking = true self.lastSpokenText = segment guard await self.updateIncrementalContextIfNeeded(speechGeneration: speechGeneration) else { return } @@ -3522,13 +3545,13 @@ extension TalkModeManager { case "google": "Google" case "system": - "iOS System Voice" + String(localized: "iOS System Voice") case "realtime": - "Realtime Voice" + String(localized: "Realtime Voice") case let provider where !provider.isEmpty: provider default: - "Gateway Default" + String(localized: "Gateway Default") } } @@ -3547,7 +3570,7 @@ extension TalkModeManager { self.gatewayTalkLastIssueText = nil self.gatewayTalkActiveModeTitle = self.configuredVoiceModeDescriptor.title self.gatewayTalkActiveModeSubtitle = self.configuredVoiceModeDescriptor.subtitle - self.statusText = "Listening (Realtime)" + self.statusText = String(localized: "Listening (Realtime)") } private func handleRealtimeRelayStatus(_ status: String) { @@ -3582,7 +3605,7 @@ extension TalkModeManager { self.gatewayTalkCurrentFallbackIssue = nil self.gatewayTalkActiveModeTitle = "iOS Speech + TTS" self.gatewayTalkActiveModeSubtitle = nil - self.statusText = "Listening" + self.statusText = String(localized: "Listening") } private func markNativeFallbackActive(after issue: TalkRuntimeIssue) { @@ -3847,7 +3870,11 @@ extension TalkModeManager { ? Self.displayName(forProvider: routing.activeProvider) : providerSelection.label let transport = usesRealtimeConfig ? (usesRealtimeRelay ? "gateway-relay" : "webrtc") : "native" - let transportLabel = usesRealtimeRelay ? "Gateway Relay" : (usesRealtimeConfig ? "Native WebRTC" : "Native") + let transportLabel = usesRealtimeRelay + ? String(localized: "Gateway Relay") + : (usesRealtimeConfig + ? String(localized: "Native WebRTC") + : String(localized: "Native")) self.gatewayTalkProviderLabel = providerLabel self.gatewayTalkUsesRealtime = usesRealtimeConfig self.gatewayTalkUsesRealtimeRelay = usesRealtimeRelay @@ -3908,7 +3935,7 @@ extension TalkModeManager { self.silenceWindow = TimeInterval(Self.defaultSilenceTimeoutMs) / 1000 if let missingScope = Self.missingTalkScope(from: error) { self.gatewayTalkPermissionState = .missingScope(missingScope) - self.statusText = "Gateway permission required" + self.statusText = String(localized: "Gateway permission required") GatewayDiagnostics.log("talk config missing gateway scope=\(missingScope)") } else { self.gatewayTalkPermissionState = .loadFailed(error.localizedDescription) @@ -3923,15 +3950,15 @@ extension TalkModeManager { self.realtimeModelId = nil self.realtimeVoiceId = nil self.configuredProviderModelId = nil - self.gatewayTalkProviderLabel = "Not loaded" - self.gatewayTalkTransportLabel = "Not loaded" + self.gatewayTalkProviderLabel = String(localized: "Not loaded") + self.gatewayTalkTransportLabel = String(localized: "Not loaded") self.gatewayTalkUsesRealtime = false self.gatewayTalkUsesRealtimeRelay = false self.gatewayTalkRealtimeProviderLabel = nil self.gatewayTalkRealtimeModelId = nil self.gatewayTalkRealtimeVoiceId = nil self.applyVoiceModeDescriptor(TalkVoiceModeDescriptor( - title: "Not loaded", + title: String(localized: "Not loaded"), subtitle: nil, providerId: nil, modelId: nil, @@ -3949,7 +3976,7 @@ extension TalkModeManager { func markTalkPermissionUpgradeRequested(requestId: String?) { self.gatewayTalkPermissionState = .upgradeRequested(requestId: requestId) - self.statusText = "Approval requested" + self.statusText = String(localized: "Approval requested") } private static func missingTalkScope(from error: Error) -> String? { @@ -4393,11 +4420,11 @@ extension TalkModeManager { func _test_realtimeStatusPreservesPushToTalkCapture() -> Bool { self.captureMode = .pushToTalk self.isListening = false - self.statusText = "Listening (PTT)" + self.statusText = String(localized: "Listening (PTT)") self.handleRealtimeRelayStatus("Listening (Realtime)") return self.captureMode == .pushToTalk && !self.isListening && - self.statusText == "Listening (PTT)" + self.statusText == String(localized: "Listening (PTT)") } func _test_prepareRealtimeRelayStart() { diff --git a/apps/ios/Sources/Voice/VoiceWakeManager.swift b/apps/ios/Sources/Voice/VoiceWakeManager.swift index 34735ba4b4f8..66f7a6966d3c 100644 --- a/apps/ios/Sources/Voice/VoiceWakeManager.swift +++ b/apps/ios/Sources/Voice/VoiceWakeManager.swift @@ -44,7 +44,7 @@ private enum VoiceWakeAudioError: LocalizedError { var errorDescription: String? { switch self { case .invalidInputFormat: - "Microphone input format unavailable" + String(localized: "Microphone input format unavailable") } } } @@ -227,7 +227,7 @@ final class VoiceWakeManager: NSObject { self.tearDownRecognitionPipeline() } if self.isEnabled { - self.statusText = "Paused" + self.statusText = String(localized: "Paused") } } else if self.isEnabled { self.scheduleStart() @@ -263,7 +263,7 @@ final class VoiceWakeManager: NSObject { guard self.suppressionReasons.isEmpty else { self.isListening = false - self.statusText = "Paused" + self.statusText = String(localized: "Paused") return } @@ -273,15 +273,16 @@ final class VoiceWakeManager: NSObject { // The iOS Simulator’s audio stack is unreliable for long-running microphone capture. // (We’ve observed CoreAudio deadlocks after TCC permission prompts.) self.isListening = false - self.statusText = "Voice Wake isn’t supported on Simulator" + self.statusText = String(localized: "Voice Wake isn’t supported on Simulator") return } - self.statusText = "Requesting permissions…" + self.statusText = String(localized: "Requesting permissions…") let micOk = await Self.requestMicrophonePermission() guard micOk else { - self.statusText = Self.microphonePermissionMessage(kind: "Microphone") + self.statusText = Self.microphonePermissionMessage( + kind: String(localized: "Microphone")) self.isListening = false return } @@ -289,7 +290,7 @@ final class VoiceWakeManager: NSObject { let speechOk = await Self.requestSpeechPermission() guard speechOk else { self.statusText = Self.permissionMessage( - kind: "Speech recognition", + kind: String(localized: "Speech recognition"), status: SFSpeechRecognizer.authorizationStatus()) self.isListening = false return @@ -297,14 +298,16 @@ final class VoiceWakeManager: NSObject { self.speechRecognizer = SFSpeechRecognizer() guard self.speechRecognizer != nil else { - self.statusText = "Speech recognizer unavailable" + self.statusText = String(localized: "Speech recognizer unavailable") self.isListening = false return } guard self.isEnabled, self.suppressionReasons.isEmpty else { self.isListening = false - self.statusText = self.isEnabled ? "Paused" : "Off" + self.statusText = self.isEnabled + ? String(localized: "Paused") + : String(localized: "Off") return } @@ -312,18 +315,20 @@ final class VoiceWakeManager: NSObject { try self.configureOwnedAudioSession() try self.startRecognition() self.isListening = true - self.statusText = "Listening" + self.statusText = String(localized: "Listening") } catch { self.isListening = false self.tearDownRecognitionPipeline() - self.statusText = "Start failed: \(error.localizedDescription)" + self.statusText = String( + format: String(localized: "Start failed: %@"), + error.localizedDescription) } } func stop() { self.isEnabled = false self.isListening = false - self.statusText = "Off" + self.statusText = String(localized: "Off") self.cancelScheduledStart() self.tearDownRecognitionPipeline() } @@ -437,7 +442,9 @@ final class VoiceWakeManager: NSObject { { guard self.recognitionGeneration == recognitionGeneration else { return } if let errorText { - self.statusText = "Recognizer error: \(errorText)" + self.statusText = String( + format: String(localized: "Recognizer error: %@"), + errorText) self.isListening = false self.tearDownRecognitionPipeline() self.scheduleStart(after: self.recognitionErrorRestartDelayNs) @@ -450,7 +457,7 @@ final class VoiceWakeManager: NSObject { if cmd == self.lastDispatched { return } self.lastDispatched = cmd self.lastTriggeredCommand = cmd - self.statusText = "Triggered" + self.statusText = String(localized: "Triggered") self.commandGeneration &+= 1 let commandGeneration = self.commandGeneration @@ -606,23 +613,37 @@ final class VoiceWakeManager: NSObject { { switch status { case .denied: - return "\(kind) permission denied" + return String( + format: String(localized: "%@ permission denied"), + kind) case .restricted: - return "\(kind) permission restricted" + return String( + format: String(localized: "%@ permission restricted"), + kind) case .notDetermined: - return "\(kind) permission not granted" + return String( + format: String(localized: "%@ permission not granted"), + kind) case .authorized: - return "\(kind) permission denied" + return String( + format: String(localized: "%@ permission denied"), + kind) @unknown default: - return "\(kind) permission denied" + return String( + format: String(localized: "%@ permission denied"), + kind) } } private nonisolated static func deniedByDefaultPermissionMessage(kind: String, isUndetermined: Bool) -> String { if isUndetermined { - return "\(kind) permission not granted" + return String( + format: String(localized: "%@ permission not granted"), + kind) } - return "\(kind) permission denied" + return String( + format: String(localized: "%@ permission denied"), + kind) } }