fix(ios): use plural-aware localized counts

This commit is contained in:
Vincent Koc
2026-07-12 18:33:47 +02:00
committed by Vincent Koc
parent 1591aa4c32
commit cc85d8256f
11 changed files with 45 additions and 69 deletions
@@ -305,17 +305,16 @@ struct AgentProDreamingDestination: View {
}
private func dreamDiaryDayView(_ day: DreamDiaryDay) -> some View {
VStack(alignment: .leading, spacing: 8) {
let count = day.entryCount
let entryCountText = String(
AttributedString(localized: "^[\(count) entry](inflect: true)").characters)
return VStack(alignment: .leading, spacing: 8) {
HStack(alignment: .firstTextBaseline) {
Text(day.title)
.font(OpenClawType.subheadSemiBold)
.lineLimit(1)
Spacer(minLength: 8)
Text(verbatim: day.entryCount == 1
? String(localized: "1 entry")
: String(
format: String(localized: "%@ entries"),
day.entryCount.formatted()))
Text(verbatim: entryCountText)
.font(OpenClawType.caption2SemiBold)
.foregroundStyle(OpenClawBrand.accent)
}
@@ -334,21 +334,17 @@ struct AgentProNodesDestination: View {
let tags = (entry.tags ?? []).prefix(2).joined(separator: ", ")
let scopesCount = entry.scopes?.count ?? 0
let rolesCount = entry.roles?.count ?? 0
let scopesText = String(
AttributedString(localized: "^[\(scopesCount) scope](inflect: true)").characters)
let rolesText = String(
AttributedString(localized: "^[\(rolesCount) role](inflect: true)").characters)
let labels = [
Self.normalized(entry.instanceid).map {
String(format: String(localized: "instance %@"), $0)
},
tags.isEmpty ? nil : tags,
scopesCount > 0
? String(
format: String(localized: "%@ scopes"),
scopesCount.formatted())
: nil,
rolesCount > 0
? String(
format: String(localized: "%@ roles"),
rolesCount.formatted())
: nil,
scopesCount > 0 ? scopesText : nil,
rolesCount > 0 ? rolesText : nil,
].compactMap(\.self)
return labels.isEmpty ? nil : labels.joined(separator: "")
}
@@ -644,10 +644,8 @@ extension AgentProTab {
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())
return String(
AttributedString(localized: "^[\(count) agent](inflect: true) total").characters)
}
var instancesColor: Color {
@@ -82,11 +82,6 @@ extension AgentProTab {
}
private static func tokenCountText(_ count: Int) -> String {
if count == 1 {
return String(localized: "1 token")
}
return String(
format: String(localized: "%@ tokens"),
Self.compactNumber(count))
String(AttributedString(localized: "^[\(count) token](inflect: true)").characters)
}
}
@@ -879,10 +879,8 @@ struct CommandSessionsScreen: View {
if count == 0 {
return self.emptyTitle
}
if count == 1 {
return String(localized: "1 session")
}
return String(format: String(localized: "%@ sessions"), count.formatted())
return String(
AttributedString(localized: "^[\(count) session](inflect: true)").characters)
}
private var visibleSessions: [OpenClawChatSessionEntry] {
@@ -153,15 +153,14 @@ struct IPadSkillWorkshopScreen: View {
}
private var compactFiltersCard: some View {
ProCard(radius: OpenClawProMetric.cardRadius) {
let count = self.filteredProposals.count
let countText = String(
AttributedString(localized: "^[\(count) proposal](inflect: true)").characters)
return ProCard(radius: OpenClawProMetric.cardRadius) {
VStack(alignment: .leading, spacing: 12) {
HStack(alignment: .firstTextBaseline, spacing: 10) {
VStack(alignment: .leading, spacing: 3) {
Text(verbatim: self.filteredProposals.count == 1
? String(localized: "1 proposal")
: String(
format: String(localized: "%@ proposals"),
self.filteredProposals.count.formatted()))
Text(verbatim: countText)
.font(OpenClawType.headline)
Text(self.statusFilterLabel)
.font(OpenClawType.caption)
@@ -187,14 +187,13 @@ struct IPadWorkboardScreen: View {
}
private var compactQueueControls: some View {
ProCard(radius: OpenClawProMetric.cardRadius) {
let count = self.filteredCards.count
let countText = String(
AttributedString(localized: "^[\(count) card](inflect: true)").characters)
return ProCard(radius: OpenClawProMetric.cardRadius) {
VStack(alignment: .leading, spacing: 9) {
HStack(alignment: .firstTextBaseline, spacing: 10) {
Text(verbatim: self.filteredCards.count == 1
? String(localized: "1 card")
: String(
format: String(localized: "%@ cards"),
self.filteredCards.count.formatted()))
Text(verbatim: countText)
.font(OpenClawType.headline)
Spacer(minLength: 8)
self.compactRefreshButton
@@ -282,18 +282,16 @@ private struct ExecApprovalPromptCard: View {
}
if remainingSeconds < 3600 {
let minutes = Int(ceil(Double(remainingSeconds) / 60.0))
return minutes == 1
? String(localized: "about 1 minute")
: String(
format: String(localized: "about %@ minutes"),
minutes.formatted())
return String(
AttributedString(
localized: "about ^[\(minutes) minute](inflect: true)")
.characters)
}
let hours = Int(ceil(Double(remainingSeconds) / 3600.0))
return hours == 1
? String(localized: "about 1 hour")
: String(
format: String(localized: "about %@ hours"),
hours.formatted())
return String(
AttributedString(
localized: "about ^[\(hours) hour](inflect: true)")
.characters)
}
}
+6 -12
View File
@@ -470,12 +470,9 @@ private struct WatchControlSurfaceView: View {
}
private var approvalHeadline: String {
if self.approvalCount == 1 {
return String(localized: "1 approval waiting")
}
let count = self.approvalCount
return String(
format: String(localized: "%@ approvals waiting"),
self.approvalCount.formatted())
AttributedString(localized: "^[\(count) approval](inflect: true) waiting").characters)
}
private var approvalSubtitle: String {
@@ -555,12 +552,9 @@ private struct WatchControlSurfaceView: View {
return status
}
if self.chatCount > 0 {
if self.chatCount == 1 {
return String(localized: "1 recent message")
}
let count = self.chatCount
return String(
format: String(localized: "%@ recent messages"),
self.chatCount.formatted())
AttributedString(localized: "^[\(count) recent message](inflect: true)").characters)
}
return self.store.hasAppSnapshot
? String(localized: "No messages synced")
@@ -1665,9 +1659,9 @@ private struct WatchExecApprovalDetailView: View {
if deltaSeconds < 60 {
return String(localized: "less than 1 minute")
}
let minutes = deltaSeconds / 60
return String(
format: String(localized: "%@ minutes"),
(deltaSeconds / 60).formatted())
AttributedString(localized: "^[\(minutes) minute](inflect: true)").characters)
}
}
+3 -3
View File
@@ -138,8 +138,8 @@ const LOCALIZED_WRAPPER_CONTRACTS: Record<string, readonly string[]> = {
],
"apps/ios/Sources/Gateway/ExecApprovalPromptDialog.swift": [
"private struct ExecApprovalPromptMetadataRow: View {\n let label: LocalizedStringKey",
'format: String(localized: "about %@ minutes")',
'format: String(localized: "about %@ hours")',
'localized: "about ^[\\(minutes) minute](inflect: true)"',
'localized: "about ^[\\(hours) hour](inflect: true)"',
],
"apps/ios/Sources/Voice/TalkGatewayPermissionState.swift": [
'String(format: String(localized: "Missing %@"), scope)',
@@ -156,7 +156,7 @@ const LOCALIZED_WRAPPER_CONTRACTS: Record<string, readonly string[]> = {
],
"apps/ios/Sources/Design/AgentProTab+Overview.swift": [
"subtitle: .verbatim(self.agentTotalText)",
'format: String(localized: "%@ agents total")',
'AttributedString(localized: "^[\\(count) agent](inflect: true) total")',
"func agentMenuRow(\n icon: String,\n title: OpenClawTextValue,\n detail: OpenClawTextValue",
"func metricTile(\n icon: String,\n title: OpenClawTextValue,\n value: String,\n detail: OpenClawTextValue",
],
+2 -2
View File
@@ -25,8 +25,8 @@ describe("Apple app i18n catalogs", () => {
};
for (const key of [
"%@ agents total",
"%@ approvals waiting",
"^[%lld agent](inflect: true) total",
"^[%lld approval](inflect: true) waiting",
"Approval needed",
"Agent: %@",
"Connect a nearby Gateway",