fix(ios): localize design and chat surfaces

This commit is contained in:
Vincent Koc
2026-07-12 07:17:13 +02:00
committed by Vincent Koc
parent 6d4afffec3
commit ee3104340d
26 changed files with 650 additions and 284 deletions
@@ -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: "")
}
+12 -2
View File
@@ -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))
}
}
+8 -8
View File
@@ -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)
}
+6 -2
View File
@@ -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)
+55 -28
View File
@@ -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
}