mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix(ios): use plural-aware localized counts
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user