mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
fix(ios): localize design and chat surfaces
This commit is contained in:
@@ -65,7 +65,7 @@ struct AgentProDreamingDestination: View {
|
||||
if let headerLeadingAction {
|
||||
OpenClawAdaptiveHeaderRow(
|
||||
title: "Dreaming",
|
||||
subtitle: self.dreamingDetail,
|
||||
subtitle: .localized(self.dreamingDetail),
|
||||
titleFont: OpenClawType.title3SemiBold,
|
||||
subtitleFont: OpenClawType.subheadMedium)
|
||||
{
|
||||
@@ -253,7 +253,10 @@ struct AgentProDreamingDestination: View {
|
||||
icon: "book.closed",
|
||||
title: diary.found ? "Dream diary is empty" : "No dream diary yet",
|
||||
detail: diary.found
|
||||
? "\(diary.path) exists but has no readable content."
|
||||
? .verbatim(String(
|
||||
format: String(
|
||||
localized: "%@ exists but has no readable content."),
|
||||
diary.path))
|
||||
: "The gateway did not find DREAMS.md or dreams.md in the active agent workspace.")
|
||||
.padding(14)
|
||||
}
|
||||
@@ -308,7 +311,11 @@ struct AgentProDreamingDestination: View {
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
Spacer(minLength: 8)
|
||||
Text("\(day.entryCount) \(day.entryCount == 1 ? "entry" : "entries")")
|
||||
Text(verbatim: day.entryCount == 1
|
||||
? String(localized: "1 entry")
|
||||
: String(
|
||||
format: String(localized: "%@ entries"),
|
||||
day.entryCount.formatted()))
|
||||
.font(OpenClawType.caption2SemiBold)
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
}
|
||||
@@ -341,7 +348,7 @@ struct AgentProDreamingDestination: View {
|
||||
emptyDetail: String) -> some View
|
||||
{
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ProSectionHeader(title: title)
|
||||
ProSectionHeader(title: .localized(title))
|
||||
ProCard(padding: 0) {
|
||||
if entries.isEmpty {
|
||||
self.emptyDetailRow(
|
||||
@@ -382,7 +389,7 @@ struct AgentProDreamingDestination: View {
|
||||
.lineLimit(1)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
Text("\(entry.totalSignalCount)")
|
||||
Text(verbatim: entry.totalSignalCount.formatted())
|
||||
.font(OpenClawType.caption2SemiBold)
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
.lineLimit(1)
|
||||
|
||||
@@ -39,7 +39,7 @@ struct AgentProNodesDestination: View {
|
||||
if let headerLeadingAction {
|
||||
OpenClawAdaptiveHeaderRow(
|
||||
title: "Instances",
|
||||
subtitle: self.instancesDetail,
|
||||
subtitle: .verbatim(self.instancesDetail),
|
||||
titleFont: OpenClawType.title3SemiBold,
|
||||
subtitleFont: OpenClawType.subheadMedium)
|
||||
{
|
||||
@@ -58,7 +58,7 @@ struct AgentProNodesDestination: View {
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text("Instances")
|
||||
.font(OpenClawType.headline)
|
||||
Text(self.instancesDetail)
|
||||
Text(verbatim: self.instancesDetail)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
@@ -79,9 +79,15 @@ struct AgentProNodesDestination: View {
|
||||
ProValuePill(value: self.instancesValue, color: self.instancesColor)
|
||||
}
|
||||
HStack(spacing: 10) {
|
||||
self.detailMetric(label: "Connected", value: "\(self.overview?.presence.count ?? 0)")
|
||||
self.detailMetric(label: "Agents", value: "\(self.agentCount)")
|
||||
self.detailMetric(label: "Gateway", value: self.gatewayConnected ? "online" : "offline")
|
||||
self.detailMetric(
|
||||
label: "Connected",
|
||||
value: (self.overview?.presence.count ?? 0).formatted())
|
||||
self.detailMetric(label: "Agents", value: self.agentCount.formatted())
|
||||
self.detailMetric(
|
||||
label: "Gateway",
|
||||
value: self.gatewayConnected
|
||||
? String(localized: "online")
|
||||
: String(localized: "offline"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,7 +140,7 @@ struct AgentProNodesDestination: View {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
ProIconBadge(systemName: Self.presenceIcon(entry), color: Self.presenceColor(entry))
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(Self.presenceLabel(entry) ?? "Instance")
|
||||
Text(verbatim: Self.presenceLabel(entry) ?? String(localized: "Instance"))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
Text(Self.presenceDetail(entry))
|
||||
@@ -173,7 +179,7 @@ struct AgentProNodesDestination: View {
|
||||
HStack(spacing: 12) {
|
||||
ProIconBadge(systemName: Self.presenceIcon(entry), color: Self.presenceColor(entry))
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(Self.presenceLabel(entry) ?? "Instance")
|
||||
Text(verbatim: Self.presenceLabel(entry) ?? String(localized: "Instance"))
|
||||
.font(OpenClawType.headline)
|
||||
Text(Self.presenceDetail(entry))
|
||||
.font(OpenClawType.caption)
|
||||
@@ -213,18 +219,18 @@ struct AgentProNodesDestination: View {
|
||||
}
|
||||
.safeAreaPadding(.bottom, OpenClawProMetric.bottomScrollInset)
|
||||
}
|
||||
.navigationTitle(Self.presenceLabel(entry) ?? "Instance")
|
||||
.navigationTitle(Self.presenceLabel(entry) ?? String(localized: "Instance"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
private func nodeDetailRow(_ title: String, value: String?) -> some View {
|
||||
private func nodeDetailRow(_ title: OpenClawTextValue, value: String?) -> some View {
|
||||
let normalized = Self.normalized(value) ?? "n/a"
|
||||
return HStack(spacing: 10) {
|
||||
Text(title)
|
||||
title.text
|
||||
.font(OpenClawType.subhead)
|
||||
.foregroundStyle(.secondary)
|
||||
Spacer(minLength: 8)
|
||||
Text(normalized)
|
||||
Text(verbatim: normalized)
|
||||
.font(OpenClawType.subhead)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
@@ -235,13 +241,13 @@ struct AgentProNodesDestination: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.disabled(normalized == "n/a")
|
||||
.accessibilityLabel("Copy \(title)")
|
||||
.accessibilityLabel("Copy value")
|
||||
}
|
||||
.font(OpenClawType.subhead)
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
|
||||
private func nodeListCard(title: String, values: [String]) -> some View {
|
||||
private func nodeListCard(title: OpenClawTextValue, values: [String]) -> some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ProSectionHeader(title: title)
|
||||
ProCard {
|
||||
@@ -253,7 +259,7 @@ struct AgentProNodesDestination: View {
|
||||
} else {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ForEach(values, id: \.self) { value in
|
||||
Text(value)
|
||||
Text(verbatim: value)
|
||||
.font(OpenClawType.monoSmall)
|
||||
.textSelection(.enabled)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
@@ -265,12 +271,12 @@ struct AgentProNodesDestination: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func detailMetric(label: String, value: String) -> some View {
|
||||
private func detailMetric(label: OpenClawTextValue, value: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(label)
|
||||
label.text
|
||||
.font(OpenClawType.caption2Medium)
|
||||
.foregroundStyle(.secondary)
|
||||
Text(value)
|
||||
Text(verbatim: value)
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.8)
|
||||
@@ -282,13 +288,17 @@ struct AgentProNodesDestination: View {
|
||||
in: RoundedRectangle(cornerRadius: OpenClawRadius.sm, style: .continuous))
|
||||
}
|
||||
|
||||
private func emptyRow(icon: String, title: String, detail: String) -> some View {
|
||||
private func emptyRow(
|
||||
icon: String,
|
||||
title: OpenClawTextValue,
|
||||
detail: OpenClawTextValue) -> some View
|
||||
{
|
||||
HStack(spacing: 12) {
|
||||
ProIconBadge(systemName: icon, color: .secondary)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(title)
|
||||
title.text
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text(detail)
|
||||
detail.text
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
@@ -313,7 +323,7 @@ struct AgentProNodesDestination: View {
|
||||
if !parts.isEmpty {
|
||||
return parts.joined(separator: " • ")
|
||||
}
|
||||
return Self.normalized(entry.text) ?? "Presence beacon received."
|
||||
return Self.normalized(entry.text) ?? String(localized: "Presence beacon received.")
|
||||
}
|
||||
|
||||
private static func presenceMeta(_ entry: PresenceEntry) -> String? {
|
||||
@@ -321,10 +331,20 @@ struct AgentProNodesDestination: View {
|
||||
let scopesCount = entry.scopes?.count ?? 0
|
||||
let rolesCount = entry.roles?.count ?? 0
|
||||
let labels = [
|
||||
Self.normalized(entry.instanceid).map { "instance \($0)" },
|
||||
Self.normalized(entry.instanceid).map {
|
||||
String(format: String(localized: "instance %@"), $0)
|
||||
},
|
||||
tags.isEmpty ? nil : tags,
|
||||
scopesCount > 0 ? "\(scopesCount) scopes" : nil,
|
||||
rolesCount > 0 ? "\(rolesCount) roles" : nil,
|
||||
scopesCount > 0
|
||||
? String(
|
||||
format: String(localized: "%@ scopes"),
|
||||
scopesCount.formatted())
|
||||
: nil,
|
||||
rolesCount > 0
|
||||
? String(
|
||||
format: String(localized: "%@ roles"),
|
||||
rolesCount.formatted())
|
||||
: nil,
|
||||
].compactMap(\.self)
|
||||
return labels.isEmpty ? nil : labels.joined(separator: " • ")
|
||||
}
|
||||
|
||||
@@ -131,7 +131,12 @@ extension AgentProTab {
|
||||
|
||||
@MainActor
|
||||
func runCronJob(_ job: CronJob) async {
|
||||
await self.runCronAction(job, success: "Queued \(job.name).") {
|
||||
await self.runCronAction(
|
||||
job,
|
||||
success: String(
|
||||
format: String(localized: "Queued %@."),
|
||||
job.name))
|
||||
{
|
||||
let params = CronRunParams(id: job.id, mode: "force")
|
||||
_ = try await self.requestGateway(method: "cron.run", params: params, timeoutSeconds: 20)
|
||||
}
|
||||
@@ -139,7 +144,12 @@ extension AgentProTab {
|
||||
|
||||
@MainActor
|
||||
func setCronJob(_ job: CronJob, enabled: Bool) async {
|
||||
await self.runCronAction(job, success: enabled ? "Enabled \(job.name)." : "Paused \(job.name).") {
|
||||
let success = String(
|
||||
format: enabled
|
||||
? String(localized: "Enabled %@.")
|
||||
: String(localized: "Paused %@."),
|
||||
job.name)
|
||||
await self.runCronAction(job, success: success) {
|
||||
let params = CronUpdateParams(id: job.id, patch: CronUpdatePatch(enabled: enabled))
|
||||
_ = try await self.requestGateway(method: "cron.update", params: params, timeoutSeconds: 20)
|
||||
}
|
||||
|
||||
@@ -182,8 +182,8 @@ extension AgentProTab {
|
||||
func directHeader(for route: AgentRoute, title: String, subtitle: String) -> some View {
|
||||
if let headerLeadingAction = self.directHeaderLeadingAction(for: route) {
|
||||
OpenClawAdaptiveHeaderRow(
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
title: .localized(title),
|
||||
subtitle: .localized(subtitle),
|
||||
titleFont: OpenClawType.title3SemiBold,
|
||||
subtitleFont: OpenClawType.subheadMedium)
|
||||
{
|
||||
|
||||
@@ -6,8 +6,8 @@ extension AgentProTab {
|
||||
var rosterHeader: some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
OpenClawAdaptiveHeaderRow(
|
||||
title: self.headerTitle,
|
||||
subtitle: "\(self.sortedAgents.count) total",
|
||||
title: .localized(self.headerTitle),
|
||||
subtitle: .verbatim(self.agentTotalText),
|
||||
titleFont: OpenClawType.title2SemiBold,
|
||||
subtitleFont: OpenClawType.subheadMedium,
|
||||
subtitleLineLimit: 1)
|
||||
@@ -132,7 +132,9 @@ extension AgentProTab {
|
||||
Image(systemName: self.gatewayConnected ? "antenna.radiowaves.left.and.right" : "wifi.slash")
|
||||
}
|
||||
.tint(self.gatewayConnected ? OpenClawBrand.ok : .secondary)
|
||||
.accessibilityLabel(self.gatewayConnected ? "Gateway online" : "Gateway offline")
|
||||
.accessibilityLabel(self.gatewayConnected
|
||||
? String(localized: "Gateway online")
|
||||
: String(localized: "Gateway offline"))
|
||||
.accessibilityHint("Opens Settings / Gateway")
|
||||
}
|
||||
}
|
||||
@@ -169,28 +171,28 @@ extension AgentProTab {
|
||||
icon: "sparkles",
|
||||
title: "Skills",
|
||||
value: self.skillsValue,
|
||||
detail: self.skillsDetail,
|
||||
detail: .verbatim(self.skillsDetail),
|
||||
color: self.gatewayConnected ? OpenClawBrand.accent : .secondary,
|
||||
route: .skills)
|
||||
self.metricTile(
|
||||
icon: "externaldrive.connected.to.line.below",
|
||||
title: "Instances",
|
||||
value: self.instancesValue,
|
||||
detail: self.instancesDetail,
|
||||
detail: .verbatim(self.instancesDetail),
|
||||
color: self.instancesColor,
|
||||
route: .instances)
|
||||
self.metricTile(
|
||||
icon: "clock.arrow.circlepath",
|
||||
title: "Cron",
|
||||
value: self.cronValue,
|
||||
detail: self.cronDetail,
|
||||
detail: .verbatim(self.cronDetail),
|
||||
color: self.cronColor,
|
||||
route: .cron)
|
||||
self.metricTile(
|
||||
icon: "chart.line.uptrend.xyaxis",
|
||||
title: "Usage",
|
||||
value: self.usageValue,
|
||||
detail: self.usageDetail,
|
||||
detail: .verbatim(self.usageDetail),
|
||||
color: self.gatewayConnected ? OpenClawBrand.accent : .secondary,
|
||||
route: .usage)
|
||||
self.metricTile(
|
||||
@@ -220,7 +222,7 @@ extension AgentProTab {
|
||||
self.agentMenuRow(
|
||||
icon: "moon",
|
||||
title: "Dreaming",
|
||||
detail: self.dreamingDetail,
|
||||
detail: .verbatim(self.dreamingDetail),
|
||||
value: self.dreamingValue,
|
||||
color: self.dreamingColor,
|
||||
showsChevron: true)
|
||||
@@ -312,7 +314,9 @@ extension AgentProTab {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel(agentAccessibilityLabel(agent, isActive: isActive, state: state))
|
||||
.accessibilityHint(isActive ? "Selected agent" : "Selects this agent")
|
||||
.accessibilityHint(isActive
|
||||
? String(localized: "Selected agent")
|
||||
: String(localized: "Selects this agent"))
|
||||
}
|
||||
|
||||
func headerIconButton(
|
||||
@@ -352,8 +356,8 @@ extension AgentProTab {
|
||||
|
||||
func agentMenuRow(
|
||||
icon: String,
|
||||
title: String,
|
||||
detail: String,
|
||||
title: OpenClawTextValue,
|
||||
detail: OpenClawTextValue,
|
||||
value: String,
|
||||
color: Color,
|
||||
showsChevron: Bool = false) -> some View
|
||||
@@ -361,9 +365,9 @@ extension AgentProTab {
|
||||
HStack(spacing: 12) {
|
||||
ProIconBadge(systemName: icon, color: color)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(title)
|
||||
title.text
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text(detail)
|
||||
detail.text
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
@@ -384,9 +388,9 @@ extension AgentProTab {
|
||||
|
||||
func metricTile(
|
||||
icon: String,
|
||||
title: String,
|
||||
title: OpenClawTextValue,
|
||||
value: String,
|
||||
detail: String,
|
||||
detail: OpenClawTextValue,
|
||||
color: Color,
|
||||
route: AgentRoute? = nil) -> some View
|
||||
{
|
||||
@@ -435,9 +439,9 @@ extension AgentProTab {
|
||||
}
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(title)
|
||||
title.text
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
Text(detail)
|
||||
detail.text
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
@@ -453,7 +457,9 @@ extension AgentProTab {
|
||||
HStack(spacing: 12) {
|
||||
ProIconBadge(systemName: "clock.badge.questionmark", color: .secondary)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(self.gatewayConnected ? "No scheduled jobs" : "Cron unavailable")
|
||||
Text(self.gatewayConnected
|
||||
? LocalizedStringKey("No scheduled jobs")
|
||||
: LocalizedStringKey("Cron unavailable"))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text(self.gatewayConnected
|
||||
? "The gateway has no visible cron jobs."
|
||||
@@ -541,19 +547,31 @@ extension AgentProTab {
|
||||
}
|
||||
|
||||
var emptyAgentsTitle: String {
|
||||
if !self.gatewayConnected { return "Agents unavailable" }
|
||||
if !agentSearchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return "No matches" }
|
||||
if agentRosterFilter != .all { return "No \(agentRosterFilter.title.lowercased()) agents" }
|
||||
return "No agents reported"
|
||||
if !self.gatewayConnected { return String(localized: "Agents unavailable") }
|
||||
if !agentSearchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
return String(localized: "No matches")
|
||||
}
|
||||
switch agentRosterFilter {
|
||||
case .online:
|
||||
return String(localized: "No online agents")
|
||||
case .ready:
|
||||
return String(localized: "No ready agents")
|
||||
case .all:
|
||||
return String(localized: "No agents reported")
|
||||
}
|
||||
}
|
||||
|
||||
var emptyAgentsDetail: String {
|
||||
if !self.gatewayConnected { return "Connect a gateway to load the live agent roster." }
|
||||
if !agentSearchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
return "Try another search or clear the agent filters."
|
||||
if !self.gatewayConnected {
|
||||
return String(localized: "Connect a gateway to load the live agent roster.")
|
||||
}
|
||||
if agentRosterFilter != .all { return "Clear the filter to view the full roster." }
|
||||
return "The connected gateway did not return an agent list."
|
||||
if !agentSearchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
return String(localized: "Try another search or clear the agent filters.")
|
||||
}
|
||||
if agentRosterFilter != .all {
|
||||
return String(localized: "Clear the filter to view the full roster.")
|
||||
}
|
||||
return String(localized: "The connected gateway did not return an agent list.")
|
||||
}
|
||||
|
||||
var overviewTaskID: String {
|
||||
@@ -566,69 +584,102 @@ extension AgentProTab {
|
||||
}
|
||||
|
||||
var skillsValue: String {
|
||||
guard self.gatewayConnected else { return "offline" }
|
||||
guard self.gatewayConnected else { return String(localized: "offline") }
|
||||
guard let skills = overview?.skills else {
|
||||
return overviewLoading ? "..." : "live"
|
||||
return overviewLoading ? "..." : String(localized: "live")
|
||||
}
|
||||
return "\(skills.enabledCount)/\(skills.totalCount)"
|
||||
}
|
||||
|
||||
var skillsDetail: String {
|
||||
guard self.gatewayConnected else { return "Connect a gateway to load skills." }
|
||||
guard self.gatewayConnected else {
|
||||
return String(localized: "Connect a gateway to load skills.")
|
||||
}
|
||||
guard let skills = overview?.skills else {
|
||||
return overviewLoading ? "Loading skill status." : "Skill status is available from the gateway."
|
||||
return overviewLoading
|
||||
? String(localized: "Loading skill status.")
|
||||
: String(localized: "Skill status is available from the gateway.")
|
||||
}
|
||||
if skills.blockedCount > 0 {
|
||||
return "\(skills.enabledCount) enabled, \(skills.blockedCount) blocked"
|
||||
return String(
|
||||
format: String(localized: "%@ enabled, %@ blocked"),
|
||||
skills.enabledCount.formatted(),
|
||||
skills.blockedCount.formatted())
|
||||
}
|
||||
if skills.missingRequirementCount > 0 {
|
||||
return "\(skills.enabledCount) enabled, \(skills.missingRequirementCount) need setup"
|
||||
return String(
|
||||
format: String(localized: "%@ enabled, %@ need setup"),
|
||||
skills.enabledCount.formatted(),
|
||||
skills.missingRequirementCount.formatted())
|
||||
}
|
||||
return "\(skills.enabledCount) enabled, \(skills.totalCount) installed"
|
||||
return String(
|
||||
format: String(localized: "%@ enabled, %@ installed"),
|
||||
skills.enabledCount.formatted(),
|
||||
skills.totalCount.formatted())
|
||||
}
|
||||
|
||||
var instancesValue: String {
|
||||
guard self.gatewayConnected else { return "offline" }
|
||||
guard self.gatewayConnected else { return String(localized: "offline") }
|
||||
guard let count = overview?.presence.count else {
|
||||
return overviewLoading ? "..." : "live"
|
||||
return overviewLoading ? "..." : String(localized: "live")
|
||||
}
|
||||
return "\(count)"
|
||||
}
|
||||
|
||||
var instancesDetail: String {
|
||||
guard self.gatewayConnected else { return "Connect a gateway to load instances." }
|
||||
guard self.gatewayConnected else {
|
||||
return String(localized: "Connect a gateway to load instances.")
|
||||
}
|
||||
guard let presence = overview?.presence else {
|
||||
return overviewLoading ? "Loading instance presence." : "Instance presence is available."
|
||||
return overviewLoading
|
||||
? String(localized: "Loading instance presence.")
|
||||
: String(localized: "Instance presence is available.")
|
||||
}
|
||||
let labels = presence.prefix(2).compactMap(presenceLabel)
|
||||
if labels.isEmpty {
|
||||
return "No live instances reported."
|
||||
return String(localized: "No live instances reported.")
|
||||
}
|
||||
return labels.joined(separator: ", ")
|
||||
}
|
||||
|
||||
private var agentTotalText: String {
|
||||
let count = self.sortedAgents.count
|
||||
if count == 1 {
|
||||
return String(localized: "1 agent total")
|
||||
}
|
||||
return String(format: String(localized: "%@ agents total"), count.formatted())
|
||||
}
|
||||
|
||||
var instancesColor: Color {
|
||||
guard self.gatewayConnected else { return .secondary }
|
||||
return (overview?.presence.isEmpty == false) ? OpenClawBrand.accent : .secondary
|
||||
}
|
||||
|
||||
var cronValue: String {
|
||||
guard self.gatewayConnected else { return "offline" }
|
||||
guard self.gatewayConnected else { return String(localized: "offline") }
|
||||
guard let cronStatus = overview?.cronStatus else {
|
||||
return overviewLoading ? "..." : "live"
|
||||
return overviewLoading ? "..." : String(localized: "live")
|
||||
}
|
||||
return cronStatus.enabled ? "\(cronStatus.jobs)" : "off"
|
||||
return cronStatus.enabled ? cronStatus.jobs.formatted() : String(localized: "off")
|
||||
}
|
||||
|
||||
var cronDetail: String {
|
||||
guard self.gatewayConnected else { return "Connect a gateway to load cron." }
|
||||
guard self.gatewayConnected else {
|
||||
return String(localized: "Connect a gateway to load cron.")
|
||||
}
|
||||
guard let cronStatus = overview?.cronStatus else {
|
||||
return overviewLoading ? "Loading cron status." : "Cron status is available."
|
||||
return overviewLoading
|
||||
? String(localized: "Loading cron status.")
|
||||
: String(localized: "Cron status is available.")
|
||||
}
|
||||
if let nextWakeAtMs = cronStatus.nextwakeatms {
|
||||
return "Next wake \(Self.relativeTime(fromMilliseconds: nextWakeAtMs))"
|
||||
return String(
|
||||
format: String(localized: "Next wake %@"),
|
||||
Self.relativeTime(fromMilliseconds: nextWakeAtMs))
|
||||
}
|
||||
return cronStatus.enabled ? "Scheduler enabled" : "Scheduler disabled"
|
||||
return cronStatus.enabled
|
||||
? String(localized: "Scheduler enabled")
|
||||
: String(localized: "Scheduler disabled")
|
||||
}
|
||||
|
||||
var cronColor: Color {
|
||||
@@ -637,7 +688,7 @@ extension AgentProTab {
|
||||
}
|
||||
|
||||
var usageValue: String {
|
||||
guard self.gatewayConnected else { return "offline" }
|
||||
guard self.gatewayConnected else { return String(localized: "offline") }
|
||||
guard let usage = overview?.usage else {
|
||||
return overviewLoading ? "..." : "7d"
|
||||
}
|
||||
@@ -651,33 +702,51 @@ extension AgentProTab {
|
||||
}
|
||||
|
||||
var usageDetail: String {
|
||||
guard self.gatewayConnected else { return "Connect a gateway to load usage." }
|
||||
guard self.gatewayConnected else {
|
||||
return String(localized: "Connect a gateway to load usage.")
|
||||
}
|
||||
guard let usage = overview?.usage else {
|
||||
return overviewLoading ? "Loading recent usage." : "Recent usage is available."
|
||||
return overviewLoading
|
||||
? String(localized: "Loading recent usage.")
|
||||
: String(localized: "Recent usage is available.")
|
||||
}
|
||||
if let tokens = usage.totalTokens, tokens > 0 {
|
||||
return "\(Self.compactNumber(tokens)) tokens in \(usage.days ?? 7)d"
|
||||
return String(
|
||||
format: String(localized: "%@ tokens in %@d"),
|
||||
Self.compactNumber(tokens),
|
||||
(usage.days ?? 7).formatted())
|
||||
}
|
||||
return "No token usage reported for \(usage.days ?? 7)d."
|
||||
return String(
|
||||
format: String(localized: "No token usage reported for %@d."),
|
||||
(usage.days ?? 7).formatted())
|
||||
}
|
||||
|
||||
var dreamingValue: String {
|
||||
guard self.gatewayConnected else { return "offline" }
|
||||
guard self.gatewayConnected else { return String(localized: "offline") }
|
||||
guard let dreaming = overview?.dreaming else {
|
||||
return overviewLoading ? "..." : "live"
|
||||
return overviewLoading ? "..." : String(localized: "live")
|
||||
}
|
||||
return dreaming.enabled ? "on" : "off"
|
||||
return dreaming.enabled ? String(localized: "on") : String(localized: "off")
|
||||
}
|
||||
|
||||
var dreamingDetail: String {
|
||||
guard self.gatewayConnected else { return "Connect a gateway to load dreaming." }
|
||||
guard self.gatewayConnected else {
|
||||
return String(localized: "Connect a gateway to load dreaming.")
|
||||
}
|
||||
guard let dreaming = overview?.dreaming else {
|
||||
return overviewLoading ? "Loading dreaming status." : "Background memory status is available."
|
||||
return overviewLoading
|
||||
? String(localized: "Loading dreaming status.")
|
||||
: String(localized: "Background memory status is available.")
|
||||
}
|
||||
if let nextRunAtMs = dreaming.nextRunAtMs {
|
||||
return "Next cycle \(Self.relativeTime(fromMilliseconds: nextRunAtMs))"
|
||||
return String(
|
||||
format: String(localized: "Next cycle %@"),
|
||||
Self.relativeTime(fromMilliseconds: nextRunAtMs))
|
||||
}
|
||||
return "\(dreaming.totalSignalCount ?? 0) signals, \(dreaming.promotedToday ?? 0) promoted today"
|
||||
return String(
|
||||
format: String(localized: "%@ signals, %@ promoted today"),
|
||||
(dreaming.totalSignalCount ?? 0).formatted(),
|
||||
(dreaming.promotedToday ?? 0).formatted())
|
||||
}
|
||||
|
||||
var dreamingColor: Color {
|
||||
|
||||
@@ -181,7 +181,10 @@ extension AgentProTab {
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.small)
|
||||
.disabled(installing || !self.skillConfigBusyKeys.isEmpty)
|
||||
.accessibilityLabel("Install \(result.displayName)")
|
||||
.accessibilityLabel(
|
||||
String(
|
||||
format: String(localized: "Install %@"),
|
||||
result.displayName))
|
||||
}
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
@@ -293,18 +296,24 @@ extension AgentProTab {
|
||||
Text(skill.displayName)
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
Text(self.normalized(skill.description) ?? self.normalized(skill.source) ?? "Workspace skill")
|
||||
Text(verbatim: self.normalized(skill.description)
|
||||
?? self.normalized(skill.source)
|
||||
?? String(localized: "Workspace skill"))
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
if let missing = skill.missingSummary {
|
||||
Text("Missing: \(missing)")
|
||||
Text(verbatim: String(
|
||||
format: String(localized: "Missing: %@"),
|
||||
missing))
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(OpenClawBrand.warn)
|
||||
.lineLimit(1)
|
||||
}
|
||||
if let install = skill.installSummary {
|
||||
Text("Setup: \(install)")
|
||||
Text(verbatim: String(
|
||||
format: String(localized: "Setup: %@"),
|
||||
install))
|
||||
.font(OpenClawType.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
@@ -312,7 +321,7 @@ extension AgentProTab {
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
VStack(alignment: .trailing, spacing: 6) {
|
||||
self.skillToggle(skill, title: status.text)
|
||||
self.skillToggle(skill, title: self.localizedSkillStatus(status.text))
|
||||
HStack(spacing: 6) {
|
||||
if self.canInstallSkillRequirements(skill) {
|
||||
Button {
|
||||
@@ -323,7 +332,10 @@ extension AgentProTab {
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.mini)
|
||||
.disabled(self.isSkillConfigBusy(skill))
|
||||
.accessibilityLabel("Set up \(skill.displayName)")
|
||||
.accessibilityLabel(
|
||||
String(
|
||||
format: String(localized: "Set up %@"),
|
||||
skill.displayName))
|
||||
}
|
||||
Button {
|
||||
self.openSkillEditor(skill)
|
||||
@@ -332,9 +344,14 @@ extension AgentProTab {
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.controlSize(.mini)
|
||||
.accessibilityLabel("Edit \(skill.displayName)")
|
||||
.accessibilityLabel(
|
||||
String(
|
||||
format: String(localized: "Edit %@"),
|
||||
skill.displayName))
|
||||
}
|
||||
Text(busy ? "saving" : status.text)
|
||||
Text(verbatim: busy
|
||||
? String(localized: "saving")
|
||||
: self.localizedSkillStatus(status.text))
|
||||
.font(OpenClawType.caption2SemiBold)
|
||||
.foregroundStyle(status.color)
|
||||
.lineLimit(1)
|
||||
@@ -458,13 +475,15 @@ extension AgentProTab {
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(skill.displayName)
|
||||
.font(OpenClawType.headline)
|
||||
Text(self.normalized(skill.description) ?? self.normalized(skill.source) ?? "Workspace skill")
|
||||
Text(verbatim: self.normalized(skill.description)
|
||||
?? self.normalized(skill.source)
|
||||
?? String(localized: "Workspace skill"))
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(3)
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
ProValuePill(value: status.text, color: status.color)
|
||||
ProValuePill(value: self.localizedSkillStatus(status.text), color: status.color)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, OpenClawProMetric.pagePadding)
|
||||
@@ -561,7 +580,9 @@ extension AgentProTab {
|
||||
Text("Setup")
|
||||
.font(OpenClawType.headline)
|
||||
if let missing = skill.missingSummary {
|
||||
Text("Missing: \(missing)")
|
||||
Text(verbatim: String(
|
||||
format: String(localized: "Missing: %@"),
|
||||
missing))
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(OpenClawBrand.warn)
|
||||
} else {
|
||||
@@ -849,4 +870,21 @@ extension AgentProTab {
|
||||
}
|
||||
return ("enabled", OpenClawBrand.accent)
|
||||
}
|
||||
|
||||
func localizedSkillStatus(_ status: String) -> String {
|
||||
switch status {
|
||||
case "off":
|
||||
String(localized: "off")
|
||||
case "blocked":
|
||||
String(localized: "blocked")
|
||||
case "disabled":
|
||||
String(localized: "disabled")
|
||||
case "setup":
|
||||
String(localized: "setup")
|
||||
case "enabled":
|
||||
String(localized: "enabled")
|
||||
default:
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ extension AgentProTab {
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(day.date)
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
Text("\(Self.compactNumber(day.totalTokens ?? 0)) tokens")
|
||||
Text(verbatim: Self.tokenCountText(day.totalTokens ?? 0))
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
@@ -80,4 +80,13 @@ extension AgentProTab {
|
||||
.padding(.vertical, 10)
|
||||
.padding(.horizontal, 14)
|
||||
}
|
||||
|
||||
private static func tokenCountText(_ count: Int) -> String {
|
||||
if count == 1 {
|
||||
return String(localized: "1 token")
|
||||
}
|
||||
return String(
|
||||
format: String(localized: "%@ tokens"),
|
||||
Self.compactNumber(count))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,11 +54,11 @@ struct AgentProTab: View {
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .all: "All"
|
||||
case .enabled: "Enabled"
|
||||
case .off: "Off"
|
||||
case .setup: "Setup"
|
||||
case .blocked: "Blocked"
|
||||
case .all: String(localized: "All")
|
||||
case .enabled: String(localized: "Enabled")
|
||||
case .off: String(localized: "Off")
|
||||
case .setup: String(localized: "Setup")
|
||||
case .blocked: String(localized: "Blocked")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,9 +74,9 @@ struct AgentProTab: View {
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .all: "All"
|
||||
case .online: "Online"
|
||||
case .ready: "Ready"
|
||||
case .all: String(localized: "All")
|
||||
case .online: String(localized: "Online")
|
||||
case .ready: String(localized: "Ready")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ struct AgentWorkspaceFilesScreen: View {
|
||||
if let headerLeadingAction {
|
||||
OpenClawAdaptiveHeaderRow(
|
||||
title: "Files",
|
||||
subtitle: self.agentId,
|
||||
subtitle: .verbatim(self.agentId),
|
||||
titleFont: OpenClawType.title3SemiBold,
|
||||
subtitleFont: OpenClawType.subheadMedium)
|
||||
{
|
||||
@@ -137,7 +137,10 @@ struct AgentWorkspaceDirectoryList: View {
|
||||
if self.loadingMore {
|
||||
ProgressView()
|
||||
} else {
|
||||
Text("\(self.entries.count) of \(self.totalEntries)")
|
||||
Text(verbatim: String(
|
||||
format: String(localized: "%1$@ of %2$@"),
|
||||
self.entries.count.formatted(),
|
||||
self.totalEntries.formatted()))
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
@@ -433,10 +433,14 @@ struct ChatProTab: View {
|
||||
|
||||
private var messagePlaceholder: String {
|
||||
if self.gatewayConnected {
|
||||
return String(localized: "Message \(self.agentDisplayName)...")
|
||||
return String(
|
||||
format: String(localized: "Message %@..."),
|
||||
self.agentDisplayName)
|
||||
}
|
||||
if self.canQueueOffline {
|
||||
return String(localized: "Message \(self.agentDisplayName); sends when connected")
|
||||
return String(
|
||||
format: String(localized: "Message %@; sends when connected"),
|
||||
self.agentDisplayName)
|
||||
}
|
||||
return String(localized: "Connect to a gateway")
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ struct CommandSessionRow: View {
|
||||
.frame(width: 7, height: 7)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
Text(self.item.title)
|
||||
Text(verbatim: self.item.title)
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.82)
|
||||
@@ -69,12 +69,12 @@ struct CommandSessionRow: View {
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
Text(self.item.trailing)
|
||||
Text(verbatim: self.item.trailing)
|
||||
.font(OpenClawType.caption2Medium)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
HStack(spacing: 8) {
|
||||
Text(self.item.detail)
|
||||
Text(verbatim: self.item.detail)
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
@@ -98,7 +98,15 @@ struct CommandSessionRow: View {
|
||||
|
||||
private var progressLabel: String {
|
||||
guard let progress = item.progress else {
|
||||
return self.item.state
|
||||
switch self.item.state {
|
||||
case "offline": return String(localized: "offline")
|
||||
case "off": return String(localized: "off")
|
||||
case "idle": return String(localized: "idle")
|
||||
case "open": return String(localized: "open")
|
||||
case "default": return String(localized: "default")
|
||||
case "recent": return String(localized: "recent")
|
||||
default: return self.item.state
|
||||
}
|
||||
}
|
||||
if self.item.state == "offline" || self.item.state == "off" || self.item.state == "idle" {
|
||||
return self.item.state
|
||||
@@ -151,13 +159,17 @@ struct CommandSessionActionsModifier: ViewModifier {
|
||||
self.deleteButton
|
||||
} else {
|
||||
self.actionButton(
|
||||
self.session.pinned == true ? "Unpin" : "Pin",
|
||||
self.session.pinned == true
|
||||
? LocalizedStringKey("Unpin")
|
||||
: LocalizedStringKey("Pin"),
|
||||
systemImage: self.session.pinned == true ? "pin.slash" : "pin")
|
||||
{
|
||||
self.actions.togglePinned()
|
||||
}
|
||||
self.actionButton(
|
||||
self.session.unread == true ? "Mark as Read" : "Mark as Unread",
|
||||
self.session.unread == true
|
||||
? LocalizedStringKey("Mark as Read")
|
||||
: LocalizedStringKey("Mark as Unread"),
|
||||
systemImage: self.session.unread == true ? "envelope.open" : "envelope.badge")
|
||||
{
|
||||
self.actions.toggleUnread()
|
||||
@@ -181,7 +193,9 @@ struct CommandSessionActionsModifier: ViewModifier {
|
||||
Button {
|
||||
self.commitEditor()
|
||||
} label: {
|
||||
Text(self.editor == .rename ? "Save" : "Create")
|
||||
Text(self.editor == .rename
|
||||
? LocalizedStringKey("Save")
|
||||
: LocalizedStringKey("Create"))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
Button(role: .cancel) {
|
||||
@@ -250,15 +264,19 @@ struct CommandSessionActionsModifier: ViewModifier {
|
||||
}
|
||||
|
||||
private var editorTitle: String {
|
||||
self.editor == .newGroup ? "New Group" : "Rename Session"
|
||||
self.editor == .newGroup
|
||||
? String(localized: "New Group")
|
||||
: String(localized: "Rename Session")
|
||||
}
|
||||
|
||||
private var editorPlaceholder: String {
|
||||
self.editor == .newGroup ? "Group name" : "Session name"
|
||||
self.editor == .newGroup
|
||||
? String(localized: "Group name")
|
||||
: String(localized: "Session name")
|
||||
}
|
||||
|
||||
private func actionButton(
|
||||
_ title: String,
|
||||
_ title: LocalizedStringKey,
|
||||
systemImage: String,
|
||||
action: @escaping () -> Void) -> some View
|
||||
{
|
||||
@@ -330,8 +348,8 @@ struct CommandViewMoreRow: View {
|
||||
|
||||
struct CommandEmptyStateRow: View {
|
||||
let icon: String
|
||||
let title: String
|
||||
let detail: String
|
||||
let title: OpenClawTextValue
|
||||
let detail: OpenClawTextValue
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 10) {
|
||||
@@ -344,10 +362,10 @@ struct CommandEmptyStateRow: View {
|
||||
.fill(OpenClawBrand.ok.opacity(0.10))
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(self.title)
|
||||
self.title.text
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
Text(self.detail)
|
||||
self.detail.text
|
||||
.font(OpenClawType.caption2Medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
|
||||
@@ -120,8 +120,8 @@ struct CommandCenterTab: View {
|
||||
|
||||
private var header: some View {
|
||||
OpenClawAdaptiveHeaderRow(
|
||||
title: self.headerTitle,
|
||||
subtitle: self.gatewaySubtitle,
|
||||
title: .localized(self.headerTitle),
|
||||
subtitle: .localized(self.gatewaySubtitle),
|
||||
titleFont: OpenClawType.title3SemiBold,
|
||||
subtitleFont: OpenClawType.caption,
|
||||
subtitleLineLimit: 1)
|
||||
@@ -308,13 +308,13 @@ struct CommandCenterTab: View {
|
||||
private var gatewayConnectionText: String {
|
||||
switch self.gatewayDisplayState {
|
||||
case .connected:
|
||||
"Online"
|
||||
String(localized: "Online")
|
||||
case .connecting:
|
||||
"Connecting"
|
||||
String(localized: "Connecting")
|
||||
case .error:
|
||||
"Attention"
|
||||
String(localized: "Attention")
|
||||
case .disconnected:
|
||||
"Offline"
|
||||
String(localized: "Offline")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,12 +334,12 @@ struct CommandCenterTab: View {
|
||||
private var gatewayAddressText: String {
|
||||
self.normalized(self.appModel.gatewayRemoteAddress)
|
||||
?? self.normalized(self.appModel.gatewayServerName)
|
||||
?? "Unknown"
|
||||
?? String(localized: "Unknown")
|
||||
}
|
||||
|
||||
private var gatewayAgentCountText: String {
|
||||
guard self.gatewayConnected else { return "—" }
|
||||
return "\(self.appModel.gatewayAgents.count)"
|
||||
return self.appModel.gatewayAgents.count.formatted()
|
||||
}
|
||||
|
||||
private var defaultChatWorkItem: WorkItem {
|
||||
@@ -361,7 +361,7 @@ struct CommandCenterTab: View {
|
||||
private var defaultChatActivityText: String {
|
||||
let activityAt = self.defaultChatSessionEntry?.lastActivityAt ?? self.defaultChatSessionEntry?.updatedAt
|
||||
guard let activityAt, activityAt > 0 else {
|
||||
return "No recent activity"
|
||||
return String(localized: "No recent activity")
|
||||
}
|
||||
return Self.relativeTimeText(forMilliseconds: activityAt)
|
||||
}
|
||||
@@ -567,13 +567,13 @@ struct CommandCenterTab: View {
|
||||
let lowercased = trimmed.lowercased()
|
||||
guard !trimmed.isEmpty else { return nil }
|
||||
if lowercased.contains(":ios-") {
|
||||
return "iOS chat"
|
||||
return String(localized: "iOS chat")
|
||||
}
|
||||
if lowercased.hasPrefix("telegram:") {
|
||||
return "Telegram chat"
|
||||
return String(localized: "Telegram chat")
|
||||
}
|
||||
if lowercased.hasPrefix("user:+") {
|
||||
return "Direct chat"
|
||||
return String(localized: "Direct chat")
|
||||
}
|
||||
if lowercased.hasPrefix("cron:") {
|
||||
return Self.humanizedSessionKey(String(trimmed.dropFirst("cron:".count)))
|
||||
@@ -666,10 +666,16 @@ struct CommandCenterTab: View {
|
||||
|
||||
private var gatewaySubtitle: String {
|
||||
if let server = normalized(appModel.gatewayServerName) {
|
||||
return "\(self.appModel.activeAgentName) on \(server)"
|
||||
return String(
|
||||
format: String(localized: "%@ on %@"),
|
||||
self.appModel.activeAgentName,
|
||||
server)
|
||||
}
|
||||
if let address = normalized(appModel.gatewayRemoteAddress) {
|
||||
return "\(self.appModel.activeAgentName) via \(address)"
|
||||
return String(
|
||||
format: String(localized: "%@ via %@"),
|
||||
self.appModel.activeAgentName,
|
||||
address)
|
||||
}
|
||||
return self.appModel.gatewayDisplayStatusText
|
||||
}
|
||||
@@ -745,7 +751,9 @@ struct CommandSessionsScreen: View {
|
||||
Button {
|
||||
self.commitGroupEditor()
|
||||
} label: {
|
||||
Text(self.groupEditor == .create ? "Create" : "Save")
|
||||
Text(self.groupEditor == .create
|
||||
? LocalizedStringKey("Create")
|
||||
: LocalizedStringKey("Save"))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
Button(role: .cancel) {
|
||||
@@ -771,7 +779,10 @@ struct CommandSessionsScreen: View {
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
} message: { group in
|
||||
Text("Sessions in \u{201C}\(group)\u{201D} move back to Ungrouped.")
|
||||
Text(verbatim: String(
|
||||
format: String(
|
||||
localized: "Sessions in \u{201C}%@\u{201D} move back to Ungrouped."),
|
||||
group))
|
||||
.font(OpenClawType.caption)
|
||||
}
|
||||
}
|
||||
@@ -797,7 +808,9 @@ struct CommandSessionsScreen: View {
|
||||
CommandPanel(padding: 0) {
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 8) {
|
||||
Text(self.showArchived ? "Archived sessions" : "Recent sessions")
|
||||
Text(self.showArchived
|
||||
? LocalizedStringKey("Archived sessions")
|
||||
: LocalizedStringKey("Recent sessions"))
|
||||
.font(OpenClawType.subheadBold)
|
||||
Spacer(minLength: 8)
|
||||
if self.isLoading {
|
||||
@@ -821,17 +834,18 @@ struct CommandSessionsScreen: View {
|
||||
CommandEmptyStateRow(
|
||||
icon: "exclamationmark.triangle.fill",
|
||||
title: "Sessions unavailable",
|
||||
detail: loadErrorText)
|
||||
detail: .verbatim(loadErrorText))
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.bottom, 10)
|
||||
} else if self.visibleSessions.isEmpty {
|
||||
CommandEmptyStateRow(
|
||||
icon: self.appModel
|
||||
.isCommandSessionListAvailable ? "bubble.left.and.text.bubble.right.fill" : "wifi.slash",
|
||||
title: self.emptyTitle,
|
||||
detail: self.appModel
|
||||
.isCommandSessionListAvailable ? self.emptyDetail :
|
||||
"Connect to the gateway.")
|
||||
title: .verbatim(self.emptyTitle),
|
||||
detail: .verbatim(self.appModel
|
||||
.isCommandSessionListAvailable
|
||||
? self.emptyDetail
|
||||
: String(localized: "Connect to the gateway.")))
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.bottom, 10)
|
||||
} else {
|
||||
@@ -857,13 +871,18 @@ struct CommandSessionsScreen: View {
|
||||
|
||||
private var headerDetail: String {
|
||||
if self.isLoading, self.sessions.isEmpty {
|
||||
return self.showArchived ? "Loading archived sessions" : "Loading recent sessions"
|
||||
return self.showArchived
|
||||
? String(localized: "Loading archived sessions")
|
||||
: String(localized: "Loading recent sessions")
|
||||
}
|
||||
let count = self.visibleSessions.count
|
||||
if count == 0 {
|
||||
return self.emptyTitle
|
||||
}
|
||||
return "\(count) \(count == 1 ? "session" : "sessions")"
|
||||
if count == 1 {
|
||||
return String(localized: "1 session")
|
||||
}
|
||||
return String(format: String(localized: "%@ sessions"), count.formatted())
|
||||
}
|
||||
|
||||
private var visibleSessions: [OpenClawChatSessionEntry] {
|
||||
@@ -889,12 +908,18 @@ struct CommandSessionsScreen: View {
|
||||
}
|
||||
|
||||
private var emptyTitle: String {
|
||||
guard self.appModel.isCommandSessionListAvailable else { return "Gateway offline" }
|
||||
return self.showArchived ? "No archived sessions" : "No recent sessions"
|
||||
guard self.appModel.isCommandSessionListAvailable else {
|
||||
return String(localized: "Gateway offline")
|
||||
}
|
||||
return self.showArchived
|
||||
? String(localized: "No archived sessions")
|
||||
: String(localized: "No recent sessions")
|
||||
}
|
||||
|
||||
private var emptyDetail: String {
|
||||
self.showArchived ? "Archived sessions will appear here." : "Start a chat and it will appear here."
|
||||
self.showArchived
|
||||
? String(localized: "Archived sessions will appear here.")
|
||||
: String(localized: "Start a chat and it will appear here.")
|
||||
}
|
||||
|
||||
private var refreshID: String {
|
||||
@@ -943,7 +968,9 @@ struct CommandSessionsScreen: View {
|
||||
}
|
||||
|
||||
private var groupEditorTitle: String {
|
||||
self.groupEditor == .create ? "New Group" : "Rename Group"
|
||||
self.groupEditor == .create
|
||||
? String(localized: "New Group")
|
||||
: String(localized: "Rename Group")
|
||||
}
|
||||
|
||||
private var groupEditorBinding: Binding<Bool> {
|
||||
|
||||
@@ -79,7 +79,8 @@ struct IPadActivityScreen: View {
|
||||
ProStatusRow(
|
||||
icon: "hand.raised.fill",
|
||||
title: "Approval needed",
|
||||
detail: pendingExecApprovalPrompt.commandPreview ?? pendingExecApprovalPrompt.commandText,
|
||||
detail: .verbatim(
|
||||
pendingExecApprovalPrompt.commandPreview ?? pendingExecApprovalPrompt.commandText),
|
||||
value: "pending",
|
||||
color: OpenClawBrand.warn,
|
||||
actionTitle: nil,
|
||||
@@ -90,7 +91,7 @@ struct IPadActivityScreen: View {
|
||||
ProStatusRow(
|
||||
icon: self.gatewayConnected ? "network" : "wifi.slash",
|
||||
title: "Gateway",
|
||||
detail: self.gatewayDetailText,
|
||||
detail: .verbatim(self.gatewayDetailText),
|
||||
value: self.gatewayStateText.lowercased(),
|
||||
color: self.gatewayConnected ? OpenClawBrand.ok : .secondary,
|
||||
actionTitle: self.gatewayConnected ? nil : "Settings",
|
||||
@@ -101,7 +102,7 @@ struct IPadActivityScreen: View {
|
||||
ProStatusRow(
|
||||
icon: "square.and.arrow.down",
|
||||
title: "Share intake",
|
||||
detail: self.appModel.lastShareEventText,
|
||||
detail: .verbatim(self.appModel.lastShareEventText),
|
||||
value: "iPad",
|
||||
color: OpenClawBrand.accentForeground,
|
||||
actionTitle: nil,
|
||||
@@ -122,7 +123,7 @@ struct IPadActivityScreen: View {
|
||||
ProStatusRow(
|
||||
icon: "exclamationmark.triangle.fill",
|
||||
title: "Sessions unavailable",
|
||||
detail: loadErrorText,
|
||||
detail: .verbatim(loadErrorText),
|
||||
value: "error",
|
||||
color: OpenClawBrand.warn,
|
||||
actionTitle: nil,
|
||||
@@ -144,8 +145,8 @@ struct IPadActivityScreen: View {
|
||||
Divider().padding(.leading, 58)
|
||||
ProStatusRow(
|
||||
icon: row.icon,
|
||||
title: row.title,
|
||||
detail: row.detail,
|
||||
title: .localized(row.title),
|
||||
detail: .localized(row.detail),
|
||||
value: row.state,
|
||||
color: row.color,
|
||||
actionTitle: "Open",
|
||||
|
||||
@@ -303,8 +303,8 @@ private struct IPadActivityStatesPreview: View {
|
||||
action: {})
|
||||
ProStatusRow(
|
||||
icon: gatewayValue == "online" ? "network" : "wifi.slash",
|
||||
title: gatewayTitle,
|
||||
detail: gatewayDetail,
|
||||
title: .localized(gatewayTitle),
|
||||
detail: .localized(gatewayDetail),
|
||||
value: gatewayValue,
|
||||
color: gatewayColor,
|
||||
actionTitle: gatewayValue == "online" ? nil : "Settings",
|
||||
@@ -322,8 +322,8 @@ private struct IPadActivityStatesPreview: View {
|
||||
Divider().padding(.leading, 58)
|
||||
ProStatusRow(
|
||||
icon: row.icon,
|
||||
title: row.title,
|
||||
detail: row.detail,
|
||||
title: .localized(row.title),
|
||||
detail: .localized(row.detail),
|
||||
value: row.state,
|
||||
color: row.color,
|
||||
actionTitle: "Open",
|
||||
@@ -333,8 +333,8 @@ private struct IPadActivityStatesPreview: View {
|
||||
Divider().padding(.leading, 58)
|
||||
ProStatusRow(
|
||||
icon: row.icon,
|
||||
title: row.title,
|
||||
detail: row.detail,
|
||||
title: .localized(row.title),
|
||||
detail: .localized(row.detail),
|
||||
value: row.value,
|
||||
color: row.color,
|
||||
actionTitle: nil,
|
||||
|
||||
@@ -32,8 +32,8 @@ struct IPadSidebarScreenChrome<Content: View>: View {
|
||||
VStack(alignment: .leading, spacing: self.isCompactHeight ? 10 : 16) {
|
||||
if !self.usesNativeNavigationChrome {
|
||||
OpenClawAdaptiveHeaderRow(
|
||||
title: self.title,
|
||||
subtitle: self.subtitle,
|
||||
title: .localized(self.title),
|
||||
subtitle: .localized(self.subtitle),
|
||||
titleFont: self.isCompactHeight ? OpenClawType.headline : OpenClawType.title2SemiBold,
|
||||
subtitleLineLimit: self.isCompactHeight ? 1 : 2)
|
||||
{
|
||||
|
||||
@@ -157,7 +157,11 @@ struct IPadSkillWorkshopScreen: View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 10) {
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text("\(self.filteredProposals.count) proposals")
|
||||
Text(verbatim: self.filteredProposals.count == 1
|
||||
? String(localized: "1 proposal")
|
||||
: String(
|
||||
format: String(localized: "%@ proposals"),
|
||||
self.filteredProposals.count.formatted()))
|
||||
.font(OpenClawType.headline)
|
||||
Text(self.statusFilterLabel)
|
||||
.font(OpenClawType.caption)
|
||||
@@ -601,7 +605,7 @@ struct IPadSkillWorkshopScreen: View {
|
||||
let defaultID = Self.normalizedScopeID(self.appModel.gatewayDefaultAgentId)
|
||||
if let match = appModel.gatewayAgents.first(where: { Self.normalizedScopeID($0.id) == defaultID }) {
|
||||
let name = Self.normalizedScopeID(match.name)
|
||||
return name.isEmpty ? "Default agent" : name
|
||||
return name.isEmpty ? String(localized: "Default agent") : name
|
||||
}
|
||||
let activeName = Self.normalizedScopeID(self.appModel.activeAgentName)
|
||||
return activeName.isEmpty ? "Default agent" : activeName
|
||||
@@ -647,18 +651,18 @@ struct IPadSkillWorkshopScreen: View {
|
||||
|
||||
static func proposalStatusFilterLabel(_ filter: String) -> String {
|
||||
switch filter {
|
||||
case "pending": "Pending"
|
||||
case "held": "Held"
|
||||
case "applied": "Applied"
|
||||
case "rejected": "Rejected"
|
||||
default: "All"
|
||||
case "pending": String(localized: "Pending")
|
||||
case "held": String(localized: "Held")
|
||||
case "applied": String(localized: "Applied")
|
||||
case "rejected": String(localized: "Rejected")
|
||||
default: String(localized: "All")
|
||||
}
|
||||
}
|
||||
|
||||
static func proposalLaneLabel(_ status: String) -> String {
|
||||
switch status {
|
||||
case "quarantined": "Quarantined"
|
||||
case "stale": "Stale"
|
||||
case "quarantined": String(localized: "Quarantined")
|
||||
case "stale": String(localized: "Stale")
|
||||
case "pending", "applied", "rejected":
|
||||
self.proposalStatusFilterLabel(status)
|
||||
default:
|
||||
@@ -879,7 +883,9 @@ struct IPadSkillWorkshopScreen: View {
|
||||
agentId: self.selectedAgentParam,
|
||||
proposalId: proposal.id),
|
||||
timeoutSeconds: 30)
|
||||
self.noticeText = action == .apply ? "Proposal applied." : "Proposal rejected."
|
||||
self.noticeText = action == .apply
|
||||
? String(localized: "Proposal applied.")
|
||||
: String(localized: "Proposal rejected.")
|
||||
await self.loadProposals(force: true)
|
||||
} catch {
|
||||
self.errorText = Self.message(for: error)
|
||||
@@ -928,15 +934,19 @@ struct IPadSkillProposalKanbanColumn: View {
|
||||
ProCard(padding: 0, radius: OpenClawProMetric.cardRadius) {
|
||||
VStack(spacing: 0) {
|
||||
ProPanelHeader(
|
||||
title: IPadSkillWorkshopScreen.proposalLaneLabel(self.status),
|
||||
title: .localized(IPadSkillWorkshopScreen.proposalLaneLabel(self.status)),
|
||||
value: "\(self.proposals.count)",
|
||||
actionTitle: nil,
|
||||
action: nil)
|
||||
|
||||
if self.proposals.isEmpty {
|
||||
let lane = IPadSkillWorkshopScreen.proposalLaneLabel(self.status)
|
||||
ProStatusRow(
|
||||
icon: "hammer",
|
||||
title: "No \(IPadSkillWorkshopScreen.proposalLaneLabel(self.status).lowercased()) proposals",
|
||||
title: .verbatim(
|
||||
String(
|
||||
format: String(localized: "No proposals in %@"),
|
||||
lane)),
|
||||
detail: "Matching proposals appear here after gateway refresh.",
|
||||
value: "empty",
|
||||
color: .secondary,
|
||||
@@ -1213,11 +1223,15 @@ struct IPadSkillProposal: Identifiable {
|
||||
var ageLabel: String {
|
||||
let diff = max(0, Date().timeIntervalSince1970 * 1000 - self.updatedAtMs)
|
||||
let minutes = Int(diff / 60000)
|
||||
if minutes < 1 { return "now" }
|
||||
if minutes < 60 { return "\(minutes)m" }
|
||||
if minutes < 1 { return String(localized: "now") }
|
||||
if minutes < 60 {
|
||||
return String(format: String(localized: "%@m"), minutes.formatted())
|
||||
}
|
||||
let hours = minutes / 60
|
||||
if hours < 24 { return "\(hours)h" }
|
||||
return "\(hours / 24)d"
|
||||
if hours < 24 {
|
||||
return String(format: String(localized: "%@h"), hours.formatted())
|
||||
}
|
||||
return String(format: String(localized: "%@d"), (hours / 24).formatted())
|
||||
}
|
||||
|
||||
var statusColor: Color {
|
||||
|
||||
@@ -190,7 +190,11 @@ struct IPadWorkboardScreen: View {
|
||||
ProCard(radius: OpenClawProMetric.cardRadius) {
|
||||
VStack(alignment: .leading, spacing: 9) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 10) {
|
||||
Text("\(self.filteredCards.count) cards")
|
||||
Text(verbatim: self.filteredCards.count == 1
|
||||
? String(localized: "1 card")
|
||||
: String(
|
||||
format: String(localized: "%@ cards"),
|
||||
self.filteredCards.count.formatted()))
|
||||
.font(OpenClawType.headline)
|
||||
Spacer(minLength: 8)
|
||||
self.compactRefreshButton
|
||||
@@ -352,7 +356,10 @@ struct IPadWorkboardScreen: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundStyle(self.selectedStatus == status ? OpenClawBrand.accent : .primary)
|
||||
.accessibilityLabel("Show \(IPadWorkboardDefaults.label(for: status)) cards")
|
||||
.accessibilityLabel(
|
||||
String(
|
||||
format: String(localized: "Show %@ cards"),
|
||||
IPadWorkboardDefaults.label(for: status)))
|
||||
}
|
||||
|
||||
private var boardScopeMenu: some View {
|
||||
@@ -550,11 +557,14 @@ struct IPadWorkboardScreen: View {
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(self.isCreatingCard ? "Creating..." : "Create")
|
||||
Text(self.isCreatingCard
|
||||
? LocalizedStringKey("Creating...")
|
||||
: LocalizedStringKey("Create"))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
.disabled(self.isCreatingCard)
|
||||
.accessibilityHint(self.createUnavailableMessage ?? "Creates a workboard card")
|
||||
.accessibilityHint(
|
||||
self.createUnavailableMessage ?? String(localized: "Creates a workboard card"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -602,13 +612,13 @@ struct IPadWorkboardScreen: View {
|
||||
|
||||
private var createUnavailableMessage: String? {
|
||||
if self.isCreatingCard {
|
||||
return "Card creation is already in progress."
|
||||
return String(localized: "Card creation is already in progress.")
|
||||
}
|
||||
if !self.canWrite {
|
||||
return Self.compactWriteUnavailableMessage(canRead: self.canRead)
|
||||
}
|
||||
if self.trimmedDraftTitle.isEmpty {
|
||||
return "Enter a title to create a card."
|
||||
return String(localized: "Enter a title to create a card.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -627,11 +637,16 @@ struct IPadWorkboardScreen: View {
|
||||
}
|
||||
|
||||
static func workboardSubtitle(boardScopeLabel: String, selectedStatus: String) -> String {
|
||||
"\(boardScopeLabel) / \(IPadWorkboardDefaults.label(for: selectedStatus))"
|
||||
String(
|
||||
format: String(localized: "%@ / %@"),
|
||||
boardScopeLabel,
|
||||
IPadWorkboardDefaults.label(for: selectedStatus))
|
||||
}
|
||||
|
||||
static func compactWriteUnavailableMessage(canRead: Bool) -> String {
|
||||
canRead ? "Read-only gateway." : "Connect from Settings to create, move, and dispatch cards."
|
||||
canRead
|
||||
? String(localized: "Read-only gateway.")
|
||||
: String(localized: "Connect from Settings to create, move, and dispatch cards.")
|
||||
}
|
||||
|
||||
static func boardScopeOptions(knownBoardIDs: [String], cardBoardIDs: [String]) -> [String] {
|
||||
@@ -957,15 +972,19 @@ struct IPadWorkboardKanbanColumn: View {
|
||||
ProCard(padding: 0, radius: OpenClawProMetric.cardRadius) {
|
||||
VStack(spacing: 0) {
|
||||
ProPanelHeader(
|
||||
title: IPadWorkboardDefaults.label(for: self.status),
|
||||
title: .localized(IPadWorkboardDefaults.label(for: self.status)),
|
||||
value: "\(self.cards.count)",
|
||||
actionTitle: nil,
|
||||
action: nil)
|
||||
|
||||
if self.cards.isEmpty {
|
||||
let lane = IPadWorkboardDefaults.label(for: self.status)
|
||||
ProStatusRow(
|
||||
icon: "tray",
|
||||
title: "No \(IPadWorkboardDefaults.label(for: self.status).lowercased()) cards",
|
||||
title: .verbatim(
|
||||
String(
|
||||
format: String(localized: "No cards in %@"),
|
||||
lane)),
|
||||
detail: "Cards moved into this lane appear here.",
|
||||
value: "empty",
|
||||
color: .secondary,
|
||||
@@ -1051,12 +1070,16 @@ private struct IPadWorkboardKanbanCard: View {
|
||||
Button {
|
||||
self.move(status)
|
||||
} label: {
|
||||
Text("Move to \(IPadWorkboardDefaults.label(for: status))")
|
||||
Text(verbatim: String(
|
||||
format: String(localized: "Move to %@"),
|
||||
IPadWorkboardDefaults.label(for: status)))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
}
|
||||
Button(action: self.archive) {
|
||||
Text(self.card.metadata?.archivedAt == nil ? "Archive" : "Unarchive")
|
||||
Text(self.card.metadata?.archivedAt == nil
|
||||
? LocalizedStringKey("Archive")
|
||||
: LocalizedStringKey("Unarchive"))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
} label: {
|
||||
@@ -1183,7 +1206,9 @@ struct IPadWorkboardQueueRow: View {
|
||||
.tint(OpenClawBrand.accentHot)
|
||||
}
|
||||
Button(action: self.archive) {
|
||||
Text(self.card.metadata?.archivedAt == nil ? "Archive" : "Unarchive")
|
||||
Text(self.card.metadata?.archivedAt == nil
|
||||
? LocalizedStringKey("Archive")
|
||||
: LocalizedStringKey("Unarchive"))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
.tint(.secondary)
|
||||
@@ -1206,12 +1231,16 @@ struct IPadWorkboardQueueRow: View {
|
||||
Button {
|
||||
self.move(status)
|
||||
} label: {
|
||||
Text("Move to \(IPadWorkboardDefaults.label(for: status))")
|
||||
Text(verbatim: String(
|
||||
format: String(localized: "Move to %@"),
|
||||
IPadWorkboardDefaults.label(for: status)))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
}
|
||||
Button(action: self.archive) {
|
||||
Text(self.card.metadata?.archivedAt == nil ? "Archive" : "Unarchive")
|
||||
Text(self.card.metadata?.archivedAt == nil
|
||||
? LocalizedStringKey("Archive")
|
||||
: LocalizedStringKey("Unarchive"))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
}
|
||||
@@ -1309,7 +1338,9 @@ private struct IPadWorkboardCardDetailSheet: View {
|
||||
Button {
|
||||
self.archive()
|
||||
} label: {
|
||||
Text(self.card.metadata?.archivedAt == nil ? "Archive" : "Unarchive")
|
||||
Text(self.card.metadata?.archivedAt == nil
|
||||
? LocalizedStringKey("Archive")
|
||||
: LocalizedStringKey("Unarchive"))
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
}
|
||||
.disabled(!self.canWrite || self.isBusy)
|
||||
@@ -1363,11 +1394,21 @@ private enum IPadWorkboardDefaults {
|
||||
static let statuses = ["todo", "scheduled", "ready", "running", "review", "blocked", "done"]
|
||||
|
||||
static func label(for status: String) -> String {
|
||||
status
|
||||
.replacingOccurrences(of: "_", with: " ")
|
||||
.split(separator: " ")
|
||||
.map { $0.prefix(1).uppercased() + $0.dropFirst() }
|
||||
.joined(separator: " ")
|
||||
switch status {
|
||||
case "todo": String(localized: "Todo")
|
||||
case "scheduled": String(localized: "Scheduled")
|
||||
case "ready": String(localized: "Ready")
|
||||
case "running": String(localized: "Running")
|
||||
case "review": String(localized: "Review")
|
||||
case "blocked": String(localized: "Blocked")
|
||||
case "done": String(localized: "Done")
|
||||
default:
|
||||
status
|
||||
.replacingOccurrences(of: "_", with: " ")
|
||||
.split(separator: " ")
|
||||
.map { $0.prefix(1).uppercased() + $0.dropFirst() }
|
||||
.joined(separator: " ")
|
||||
}
|
||||
}
|
||||
|
||||
static func rank(_ status: String) -> Int {
|
||||
@@ -1478,20 +1519,25 @@ struct IPadWorkboardDispatchSummary: Decodable {
|
||||
startedCount + self.promotedCount + self.reclaimedCount + self.orchestratedCount +
|
||||
self.blockedCount + self.startFailureCount)
|
||||
if total == 0, self.startFailureCount == 0, self.blockedCount == 0 {
|
||||
return "No cards dispatched."
|
||||
return String(localized: "No cards dispatched.")
|
||||
}
|
||||
let outcomes = [
|
||||
Self.outcomeText(self.startedCount, "started"),
|
||||
Self.outcomeText(self.promotedCount, "promoted"),
|
||||
Self.outcomeText(self.reclaimedCount, "reclaimed"),
|
||||
Self.outcomeText(self.orchestratedCount, "orchestrated"),
|
||||
Self.outcomeText(self.blockedCount, "blocked"),
|
||||
Self.outcomeText(self.startFailureCount, "failed"),
|
||||
Self.outcomeText(self.startedCount, .started),
|
||||
Self.outcomeText(self.promotedCount, .promoted),
|
||||
Self.outcomeText(self.reclaimedCount, .reclaimed),
|
||||
Self.outcomeText(self.orchestratedCount, .orchestrated),
|
||||
Self.outcomeText(self.blockedCount, .blocked),
|
||||
Self.outcomeText(self.startFailureCount, .failed),
|
||||
].compactMap(\.self)
|
||||
guard !outcomes.isEmpty else {
|
||||
return "\(total) dispatched."
|
||||
return String(
|
||||
format: String(localized: "%@ dispatched."),
|
||||
total.formatted())
|
||||
}
|
||||
return "\(total) dispatched: \(outcomes.joined(separator: ", "))."
|
||||
return String(
|
||||
format: String(localized: "%@ dispatched: %@."),
|
||||
total.formatted(),
|
||||
outcomes.joined(separator: ", "))
|
||||
}
|
||||
|
||||
private static func arrayCount(
|
||||
@@ -1501,9 +1547,26 @@ struct IPadWorkboardDispatchSummary: Decodable {
|
||||
(try? container.decode([IPadWorkboardDispatchEntry].self, forKey: key).count) ?? 0
|
||||
}
|
||||
|
||||
private static func outcomeText(_ count: Int, _ label: String) -> String? {
|
||||
private enum Outcome {
|
||||
case started
|
||||
case promoted
|
||||
case reclaimed
|
||||
case orchestrated
|
||||
case blocked
|
||||
case failed
|
||||
}
|
||||
|
||||
private static func outcomeText(_ count: Int, _ outcome: Outcome) -> String? {
|
||||
guard count > 0 else { return nil }
|
||||
return "\(count) \(label)"
|
||||
let format = switch outcome {
|
||||
case .started: String(localized: "%@ started")
|
||||
case .promoted: String(localized: "%@ promoted")
|
||||
case .reclaimed: String(localized: "%@ reclaimed")
|
||||
case .orchestrated: String(localized: "%@ orchestrated")
|
||||
case .blocked: String(localized: "%@ blocked")
|
||||
case .failed: String(localized: "%@ failed")
|
||||
}
|
||||
return String(format: format, count.formatted())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,28 @@ enum OpenClawRadius {
|
||||
static let md: CGFloat = 12
|
||||
}
|
||||
|
||||
enum OpenClawTextValue: ExpressibleByStringLiteral {
|
||||
case localized(LocalizedStringKey)
|
||||
case verbatim(String)
|
||||
|
||||
init(stringLiteral value: String) {
|
||||
self = .localized(LocalizedStringKey(value))
|
||||
}
|
||||
|
||||
static func localized(_ value: String) -> Self {
|
||||
.localized(LocalizedStringKey(value))
|
||||
}
|
||||
|
||||
var text: Text {
|
||||
switch self {
|
||||
case let .localized(key):
|
||||
Text(key)
|
||||
case let .verbatim(value):
|
||||
Text(verbatim: value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OpenClawProBackground: View {
|
||||
var body: some View {
|
||||
Color(uiColor: .systemGroupedBackground)
|
||||
@@ -31,14 +53,14 @@ struct OpenClawProBackground: View {
|
||||
}
|
||||
|
||||
struct ProSectionHeader: View {
|
||||
let title: String
|
||||
var actionTitle: String?
|
||||
let title: OpenClawTextValue
|
||||
var actionTitle: OpenClawTextValue?
|
||||
var action: (() -> Void)?
|
||||
var uppercase = true
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(self.title)
|
||||
self.title.text
|
||||
.font(OpenClawType.footnoteMedium)
|
||||
.foregroundStyle(.secondary)
|
||||
.textCase(self.uppercase ? .uppercase : nil)
|
||||
@@ -46,12 +68,12 @@ struct ProSectionHeader: View {
|
||||
if let actionTitle {
|
||||
if let action {
|
||||
Button(action: action) {
|
||||
Text(actionTitle)
|
||||
actionTitle.text
|
||||
.font(OpenClawType.footnoteMedium)
|
||||
}
|
||||
.foregroundStyle(OpenClawBrand.accent)
|
||||
} else {
|
||||
Text(actionTitle)
|
||||
actionTitle.text
|
||||
.font(OpenClawType.footnoteMedium)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
@@ -326,14 +348,14 @@ enum OpenClawNoticeDetail {
|
||||
|
||||
struct OpenClawNoticeBanner: View {
|
||||
let icon: String
|
||||
let title: String
|
||||
let message: String
|
||||
let ownerLabel: String
|
||||
let title: OpenClawTextValue
|
||||
let message: OpenClawTextValue
|
||||
let ownerLabel: OpenClawTextValue
|
||||
let tint: Color
|
||||
var detail: OpenClawNoticeDetail?
|
||||
var primaryActionTitle: String?
|
||||
var primaryActionTitle: OpenClawTextValue?
|
||||
var onPrimaryAction: (() -> Void)?
|
||||
var secondaryActionTitle: String?
|
||||
var secondaryActionTitle: OpenClawTextValue?
|
||||
var onSecondaryAction: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
@@ -344,16 +366,16 @@ struct OpenClawNoticeBanner: View {
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||
Text(self.title)
|
||||
self.title.text
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.multilineTextAlignment(.leading)
|
||||
Spacer(minLength: 0)
|
||||
Text(self.ownerLabel)
|
||||
self.ownerLabel.text
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Text(self.message)
|
||||
self.message.text
|
||||
.font(OpenClawType.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
@@ -367,7 +389,7 @@ struct OpenClawNoticeBanner: View {
|
||||
HStack(spacing: 10) {
|
||||
if let primaryActionTitle, let onPrimaryAction {
|
||||
Button(action: onPrimaryAction) {
|
||||
Text(primaryActionTitle)
|
||||
primaryActionTitle.text
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
@@ -376,7 +398,7 @@ struct OpenClawNoticeBanner: View {
|
||||
}
|
||||
if let secondaryActionTitle, let onSecondaryAction {
|
||||
Button(action: onSecondaryAction) {
|
||||
Text(secondaryActionTitle)
|
||||
secondaryActionTitle.text
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
@@ -400,7 +422,9 @@ struct OpenClawNoticeBanner: View {
|
||||
.foregroundStyle(self.tint)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
case let .requestID(value):
|
||||
Text("Request ID: \(value)")
|
||||
Text(verbatim: String(
|
||||
format: String(localized: "Request ID: %@"),
|
||||
value))
|
||||
.font(OpenClawType.monoSmallMedium)
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
@@ -410,8 +434,8 @@ struct OpenClawNoticeBanner: View {
|
||||
}
|
||||
|
||||
struct OpenClawAdaptiveHeaderRow<Leading: View, Accessory: View>: View {
|
||||
let title: String
|
||||
let subtitle: String?
|
||||
let title: OpenClawTextValue
|
||||
let subtitle: OpenClawTextValue?
|
||||
var titleFont: Font = OpenClawType.title3SemiBold
|
||||
var subtitleFont: Font = OpenClawType.subhead
|
||||
var subtitleLineLimit: Int? = 2
|
||||
@@ -419,8 +443,8 @@ struct OpenClawAdaptiveHeaderRow<Leading: View, Accessory: View>: View {
|
||||
@ViewBuilder let accessory: Accessory
|
||||
|
||||
init(
|
||||
title: String,
|
||||
subtitle: String? = nil,
|
||||
title: OpenClawTextValue,
|
||||
subtitle: OpenClawTextValue? = nil,
|
||||
titleFont: Font = OpenClawType.title3SemiBold,
|
||||
subtitleFont: Font = OpenClawType.subhead,
|
||||
subtitleLineLimit: Int? = 2,
|
||||
@@ -478,13 +502,13 @@ struct OpenClawAdaptiveHeaderRow<Leading: View, Accessory: View>: View {
|
||||
|
||||
private var titleBlock: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(self.title)
|
||||
self.title.text
|
||||
.font(self.titleFont)
|
||||
.lineLimit(2)
|
||||
.minimumScaleFactor(0.86)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
if let subtitle, !subtitle.isEmpty {
|
||||
Text(subtitle)
|
||||
if let subtitle {
|
||||
subtitle.text
|
||||
.font(self.subtitleFont)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(self.subtitleLineLimit)
|
||||
@@ -538,7 +562,7 @@ enum OpenClawStatusTone {
|
||||
|
||||
struct OpenClawStatusBadge: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
let label: String
|
||||
let label: OpenClawTextValue
|
||||
let tone: OpenClawStatusTone
|
||||
|
||||
var body: some View {
|
||||
@@ -547,7 +571,7 @@ struct OpenClawStatusBadge: View {
|
||||
.fill(self.tone.color)
|
||||
.frame(width: 7, height: 7)
|
||||
.shadow(color: self.tone.color.opacity(0.55), radius: 3)
|
||||
Text(self.label)
|
||||
self.label.text
|
||||
.font(OpenClawType.caption2SemiBold)
|
||||
.foregroundStyle(self.tone.color)
|
||||
}
|
||||
@@ -626,8 +650,11 @@ struct OpenClawGatewayCompactPill: View {
|
||||
@Environment(NodeAppModel.self) private var appModel
|
||||
|
||||
var body: some View {
|
||||
OpenClawStatusBadge(label: self.title, tone: self.tone)
|
||||
.accessibilityLabel("Gateway \(self.title)")
|
||||
OpenClawStatusBadge(label: .localized(self.title), tone: self.tone)
|
||||
.accessibilityLabel(
|
||||
String(
|
||||
format: String(localized: "Gateway %@"),
|
||||
self.title))
|
||||
}
|
||||
|
||||
private var title: String {
|
||||
@@ -659,7 +686,7 @@ struct OpenClawGatewayCompactPill: View {
|
||||
|
||||
struct ProMetricTile: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
let title: String
|
||||
let title: OpenClawTextValue
|
||||
let value: String
|
||||
let icon: String
|
||||
let color: Color
|
||||
@@ -680,7 +707,7 @@ struct ProMetricTile: View {
|
||||
.font(OpenClawType.headlineBold)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.72)
|
||||
Text(self.title)
|
||||
self.title.text
|
||||
.font(OpenClawType.caption2Medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
@@ -695,7 +722,7 @@ struct ProMetricTile: View {
|
||||
struct ProMetric: Identifiable {
|
||||
let id = UUID()
|
||||
let icon: String
|
||||
let title: String
|
||||
let title: OpenClawTextValue
|
||||
let value: String
|
||||
let color: Color
|
||||
}
|
||||
@@ -727,9 +754,9 @@ struct ProMetricGrid: View {
|
||||
}
|
||||
|
||||
struct ProPanelHeader: View {
|
||||
let title: String
|
||||
let title: OpenClawTextValue
|
||||
var value: String?
|
||||
var actionTitle: String?
|
||||
var actionTitle: OpenClawTextValue?
|
||||
var actionIcon: String?
|
||||
var actionAccessibilityLabel: String?
|
||||
var isActionDisabled = false
|
||||
@@ -737,7 +764,7 @@ struct ProPanelHeader: View {
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
Text(self.title)
|
||||
self.title.text
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
if let value {
|
||||
Text(value)
|
||||
@@ -759,11 +786,14 @@ struct ProPanelHeader: View {
|
||||
Button(action: action) {
|
||||
Image(systemName: actionIcon)
|
||||
}
|
||||
.accessibilityLabel(self.actionAccessibilityLabel ?? actionTitle ?? self.title)
|
||||
.accessibilityLabel(
|
||||
self.actionAccessibilityLabel.map { Text(LocalizedStringKey($0)) }
|
||||
?? actionTitle?.text
|
||||
?? self.title.text)
|
||||
.disabled(self.isActionDisabled)
|
||||
} else if let actionTitle {
|
||||
Button(action: action) {
|
||||
Text(actionTitle)
|
||||
actionTitle.text
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.disabled(self.isActionDisabled)
|
||||
@@ -774,21 +804,21 @@ struct ProPanelHeader: View {
|
||||
|
||||
struct ProStatusRow: View {
|
||||
let icon: String
|
||||
let title: String
|
||||
let detail: String
|
||||
let title: OpenClawTextValue
|
||||
let detail: OpenClawTextValue
|
||||
let value: String?
|
||||
let color: Color
|
||||
var actionTitle: String?
|
||||
var actionTitle: OpenClawTextValue?
|
||||
var action: (() -> Void)?
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
ProIconBadge(systemName: self.icon, color: self.color)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(self.title)
|
||||
self.title.text
|
||||
.font(OpenClawType.subheadSemiBold)
|
||||
.lineLimit(1)
|
||||
Text(self.detail)
|
||||
self.detail.text
|
||||
.font(OpenClawType.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
@@ -800,7 +830,7 @@ struct ProStatusRow: View {
|
||||
}
|
||||
if let actionTitle, let action {
|
||||
Button(action: action) {
|
||||
Text(actionTitle)
|
||||
actionTitle.text
|
||||
.font(OpenClawType.captionSemiBold)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
||||
@@ -303,10 +303,17 @@ struct RootTabsPhoneControlHub: View {
|
||||
|
||||
private var gatewayAccessibilityLabel: Text {
|
||||
if let gatewayDisplayLabel {
|
||||
Text("Gateway \(self.gatewayStateText), \(gatewayDisplayLabel), \(self.sidebarActiveAgentTitle)")
|
||||
Text(verbatim: String(
|
||||
format: String(localized: "Gateway %1$@, %2$@, %3$@"),
|
||||
self.gatewayStateText,
|
||||
gatewayDisplayLabel,
|
||||
self.sidebarActiveAgentTitle))
|
||||
.font(OpenClawType.captionMedium)
|
||||
} else {
|
||||
Text("Gateway \(self.gatewayStateText), \(self.sidebarActiveAgentTitle)")
|
||||
Text(verbatim: String(
|
||||
format: String(localized: "Gateway %1$@, %2$@"),
|
||||
self.gatewayStateText,
|
||||
self.sidebarActiveAgentTitle))
|
||||
.font(OpenClawType.captionMedium)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +230,10 @@ struct OpenClawChatComposer: View {
|
||||
}
|
||||
.frame(width: 14, height: 14)
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel("Context \(percentage)% used")
|
||||
.accessibilityLabel(
|
||||
String(
|
||||
format: String(localized: "Context %@%% used"),
|
||||
percentage.formatted()))
|
||||
}
|
||||
|
||||
private var thinkingPicker: some View {
|
||||
|
||||
@@ -119,24 +119,34 @@ struct ChatMessageUsagePresentation: Equatable {
|
||||
|
||||
if let input {
|
||||
visualParts.append("↑\(self.tokens(input))")
|
||||
accessibilityParts.append(String(localized: "Input tokens: \(input)"))
|
||||
accessibilityParts.append(String(
|
||||
format: String(localized: "Input tokens: %@"),
|
||||
input.formatted()))
|
||||
}
|
||||
if let output {
|
||||
visualParts.append("↓\(self.tokens(output))")
|
||||
accessibilityParts.append(String(localized: "Output tokens: \(output)"))
|
||||
accessibilityParts.append(String(
|
||||
format: String(localized: "Output tokens: %@"),
|
||||
output.formatted()))
|
||||
}
|
||||
if let cacheRead {
|
||||
visualParts.append("R\(self.tokens(cacheRead))")
|
||||
accessibilityParts.append(String(localized: "Cache read tokens: \(cacheRead)"))
|
||||
accessibilityParts.append(String(
|
||||
format: String(localized: "Cache read tokens: %@"),
|
||||
cacheRead.formatted()))
|
||||
}
|
||||
if let cacheWrite {
|
||||
visualParts.append("W\(self.tokens(cacheWrite))")
|
||||
accessibilityParts.append(String(localized: "Cache write tokens: \(cacheWrite)"))
|
||||
accessibilityParts.append(String(
|
||||
format: String(localized: "Cache write tokens: %@"),
|
||||
cacheWrite.formatted()))
|
||||
}
|
||||
if let cost = usage.cost?.total, cost > 0 {
|
||||
let formattedCost = String(format: "$%.4f", locale: Locale(identifier: "en_US_POSIX"), cost)
|
||||
visualParts.append(formattedCost)
|
||||
accessibilityParts.append(String(localized: "Cost: \(formattedCost)"))
|
||||
accessibilityParts.append(String(
|
||||
format: String(localized: "Cost: %@"),
|
||||
formattedCost))
|
||||
}
|
||||
|
||||
// Context pressure mirrors the Control UI prompt size. Output is response data;
|
||||
@@ -155,11 +165,17 @@ struct ChatMessageUsagePresentation: Equatable {
|
||||
visualParts.append("\(warningPrefix)\(contextPercent)% \(String(localized: "ctx"))")
|
||||
switch pressure {
|
||||
case .normal:
|
||||
accessibilityParts.append(String(localized: "\(contextPercent) percent of context used"))
|
||||
accessibilityParts.append(String(
|
||||
format: String(localized: "%@ percent of context used"),
|
||||
contextPercent.formatted()))
|
||||
case .warning:
|
||||
accessibilityParts.append(String(localized: "Warning: \(contextPercent) percent of context used"))
|
||||
accessibilityParts.append(String(
|
||||
format: String(localized: "Warning: %@ percent of context used"),
|
||||
contextPercent.formatted()))
|
||||
case .danger:
|
||||
accessibilityParts.append(String(localized: "Critical: \(contextPercent) percent of context used"))
|
||||
accessibilityParts.append(String(
|
||||
format: String(localized: "Critical: %@ percent of context used"),
|
||||
contextPercent.formatted()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,7 +237,7 @@ struct ChatContextUsageIndicator: View {
|
||||
.frame(width: 13, height: 13)
|
||||
|
||||
if let percent = self.usage.percentUsed {
|
||||
Text("\(percent)%")
|
||||
Text(verbatim: "\(percent.formatted())%")
|
||||
.font(OpenClawChatTypography.captionSemiBold)
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
@@ -825,7 +825,9 @@ struct ChatLinkPreview: View {
|
||||
self.model.expanded = true
|
||||
} label: {
|
||||
HStack(spacing: 6) {
|
||||
Text("Preview · \(self.domain)")
|
||||
Text(verbatim: String(
|
||||
format: String(localized: "Preview · %@"),
|
||||
self.domain))
|
||||
.font(OpenClawChatTypography.captionSemiBold)
|
||||
.foregroundStyle(OpenClawChatTheme.assistantText.opacity(0.65))
|
||||
.lineLimit(1)
|
||||
@@ -843,7 +845,10 @@ struct ChatLinkPreview: View {
|
||||
.strokeBorder(OpenClawChatTheme.divider, lineWidth: 1))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Expand link preview for \(self.domain)")
|
||||
.accessibilityLabel(
|
||||
String(
|
||||
format: String(localized: "Expand link preview for %@"),
|
||||
self.domain))
|
||||
}
|
||||
|
||||
private var expandedCard: some View {
|
||||
@@ -898,7 +903,10 @@ struct ChatLinkPreview: View {
|
||||
.strokeBorder(OpenClawChatTheme.divider, lineWidth: 1))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Open \(self.domain)")
|
||||
.accessibilityLabel(
|
||||
String(
|
||||
format: String(localized: "Open %@"),
|
||||
self.domain))
|
||||
}
|
||||
|
||||
private var domain: String {
|
||||
|
||||
@@ -34,7 +34,11 @@ struct ChatAgentAvatar: View {
|
||||
Circle()
|
||||
.strokeBorder(Color.white.opacity(0.18), lineWidth: 1))
|
||||
.shadow(color: (self.tint ?? OpenClawChatTheme.accent).opacity(0.18), radius: 8, y: 4)
|
||||
.accessibilityLabel(self.name.map { "\($0) avatar" } ?? "Agent avatar")
|
||||
.accessibilityLabel(self.name.map {
|
||||
String(
|
||||
format: String(localized: "%@ avatar"),
|
||||
$0)
|
||||
} ?? String(localized: "Agent avatar"))
|
||||
}
|
||||
|
||||
private var displayText: String {
|
||||
@@ -856,7 +860,7 @@ struct ChatPendingToolsBubble: View {
|
||||
let display = ToolDisplayRegistry.resolve(name: call.name, args: call.args)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 8) {
|
||||
Text("\(display.emoji) \(display.label)")
|
||||
Text(verbatim: "\(display.emoji) \(display.label)")
|
||||
.font(OpenClawChatTypography.mono(size: 13, relativeTo: .footnote))
|
||||
.lineLimit(1)
|
||||
Spacer(minLength: 0)
|
||||
|
||||
@@ -23,7 +23,9 @@ extension OpenClawChatViewModel {
|
||||
try Data(contentsOf: fileURL)
|
||||
}.value
|
||||
} catch {
|
||||
self.errorText = String(localized: "Could not attach voice note: \(error.localizedDescription)")
|
||||
self.errorText = String(
|
||||
format: String(localized: "Could not attach voice note: %@"),
|
||||
error.localizedDescription)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -74,7 +76,7 @@ extension OpenClawChatViewModel {
|
||||
return UTType(mimeType: mimeType) ?? .data
|
||||
}()
|
||||
guard uti.conforms(to: .image) else {
|
||||
self.errorText = "Only image attachments are supported right now"
|
||||
self.errorText = String(localized: "Only image attachments are supported right now")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -84,12 +86,17 @@ extension OpenClawChatViewModel {
|
||||
try ChatImageProcessor.processForUpload(data: data)
|
||||
}.value
|
||||
} catch {
|
||||
self.errorText = "Could not process \(fileName): \(error.localizedDescription)"
|
||||
self.errorText = String(
|
||||
format: String(localized: "Could not process %1$@: %2$@"),
|
||||
fileName,
|
||||
error.localizedDescription)
|
||||
return
|
||||
}
|
||||
|
||||
if processed.count > Self.maxAttachmentBytes {
|
||||
self.errorText = "Attachment \(fileName) exceeds 5 MB limit after resizing"
|
||||
self.errorText = String(
|
||||
format: String(localized: "Attachment %@ exceeds 5 MB limit after resizing"),
|
||||
fileName)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,11 @@ public struct OpenClawChatWindowShell: View {
|
||||
.font(OpenClawChatTypography.body)
|
||||
}
|
||||
} message: {
|
||||
Text("This resets the conversation for \(self.activeSessionTitle). The session key stays the same.")
|
||||
Text(verbatim: String(
|
||||
format: String(localized: """
|
||||
This resets the conversation for %@. The session key stays the same.
|
||||
"""),
|
||||
self.activeSessionTitle))
|
||||
.font(OpenClawChatTypography.body)
|
||||
}
|
||||
.onChange(of: self.viewModel.pendingRunCount) { previous, current in
|
||||
@@ -286,7 +290,9 @@ private struct ChatContextUsageMenu: View {
|
||||
Text(self.tokensLine)
|
||||
.font(OpenClawChatTypography.body(size: 13, weight: .regular, relativeTo: .body))
|
||||
if let cost = self.usage.totalCost {
|
||||
Text("Session cost \(ChatContextUsageFormatter.cost(cost))")
|
||||
Text(verbatim: String(
|
||||
format: String(localized: "Session cost %@"),
|
||||
ChatContextUsageFormatter.cost(cost)))
|
||||
.font(OpenClawChatTypography.body(size: 13, weight: .regular, relativeTo: .body))
|
||||
}
|
||||
Divider()
|
||||
|
||||
@@ -173,7 +173,9 @@ public final class OpenClawVoiceNoteRecorder {
|
||||
try? FileManager.default.removeItem(at: fileURL)
|
||||
self.capture.cancel()
|
||||
self.onRecordingActiveChanged?(false)
|
||||
self.fail(message: String(localized: "Could not start recording: \(error.localizedDescription)"))
|
||||
self.fail(message: String(
|
||||
format: String(localized: "Could not start recording: %@"),
|
||||
error.localizedDescription))
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user