mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(ios): localize setup and status surfaces
This commit is contained in:
@@ -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.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Bool>) -> some View {
|
||||
func toggleCard(title: LocalizedStringKey, isOn: Binding<Bool>) -> 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<String>) -> some View {
|
||||
func gatewaySecureField(
|
||||
_ placeholder: LocalizedStringKey,
|
||||
text: Binding<String>) -> 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<Bool>,
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 <requestId>"
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user