Skip to content

Commit 5878c4e

Browse files
haixing23steipete
andauthored
fix(i18n): localize provider usage details in Simplified Chinese (#3084)
Co-authored-by: Peter Steinberger <steipete@gmail.com>
1 parent 0caf970 commit 5878c4e

7 files changed

Lines changed: 303 additions & 0 deletions

File tree

Sources/CodexBar/MenuCardView+ModelHelpers.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,9 @@ extension UsageMenuCardView.Model {
194194
presentation.resetText = regen.resetText
195195
Self.apply(regen.pace, to: &presentation)
196196
}
197+
if input.provider == .deepseek, let detail = presentation.detailText {
198+
presentation.detailText = Self.localizedDeepSeekBalanceDescription(detail)
199+
}
197200
if policy.movesPrimaryDetailToStatus(snapshot: input.snapshot) {
198201
presentation.statusText = presentation.detailText
199202
presentation.detailText = nil
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import CodexBarCore
2+
import Foundation
3+
4+
extension UsageMenuCardView.Model {
5+
static func localizedProviderDetails(
6+
_ details: [ProviderDetailSection],
7+
provider: UsageProvider) -> [ProviderDetailSection]
8+
{
9+
details.compactMap { section in
10+
let rows = section.rows.compactMap { row in
11+
try? ProviderDetailSection.Row(
12+
label: L(row.label),
13+
value: self.localizedProviderDetailValue(row.value, provider: provider),
14+
secondaryValue: row.secondaryValue.map {
15+
self.localizedProviderDetailValue($0, provider: provider)
16+
})
17+
}
18+
let chart = section.chart.flatMap { chart in
19+
try? ProviderDetailSection.Chart(
20+
kind: chart.kind,
21+
title: chart.title.map(L),
22+
unit: chart.unit.map(L),
23+
points: chart.points)
24+
}
25+
return try? ProviderDetailSection(
26+
title: section.title.map(L),
27+
rows: rows,
28+
chart: chart)
29+
}
30+
}
31+
32+
static func localizedDeepSeekBalanceDescription(_ description: String) -> String {
33+
let paidSeparator = " (Paid: "
34+
let grantedSeparator = " / Granted: "
35+
guard let paidRange = description.range(of: paidSeparator),
36+
let grantedRange = description.range(
37+
of: grantedSeparator,
38+
range: paidRange.upperBound..<description.endIndex),
39+
description.last == ")"
40+
else {
41+
return description
42+
}
43+
44+
let total = String(description[..<paidRange.lowerBound])
45+
let paid = String(description[paidRange.upperBound..<grantedRange.lowerBound])
46+
let granted = String(description[grantedRange.upperBound..<description.index(before: description.endIndex)])
47+
return L("%@ (Paid: %@ / Granted: %@)", total, paid, granted)
48+
}
49+
50+
static func localizedZaiPeriodicResetText(_ window: RateWindow) -> String? {
51+
guard window.resetsAt == nil,
52+
window.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines) == "5-hour"
53+
else {
54+
return nil
55+
}
56+
return L("Resets every 5 hours")
57+
}
58+
59+
private static func localizedProviderDetailValue(_ value: String, provider: UsageProvider) -> String {
60+
switch provider {
61+
case .deepseek:
62+
self.localizedTokenSuffix(value)
63+
case .zai:
64+
self.localizedZaiValue(value)
65+
default:
66+
value
67+
}
68+
}
69+
70+
private static func localizedTokenSuffix(_ value: String) -> String {
71+
let suffix = " tokens"
72+
guard value.hasSuffix(suffix) else { return value }
73+
return L("%@ tokens", String(value.dropLast(suffix.count)))
74+
}
75+
76+
private static func localizedZaiValue(_ value: String) -> String {
77+
let usedSuffix = " used"
78+
if value.hasSuffix(usedSuffix) {
79+
return L("%@ used", String(value.dropLast(usedSuffix.count)))
80+
}
81+
82+
let limitSeparator = " limit · "
83+
let remainingSuffix = " remaining"
84+
guard let limitRange = value.range(of: limitSeparator),
85+
value.hasSuffix(remainingSuffix)
86+
else {
87+
return value
88+
}
89+
let limit = String(value[..<limitRange.lowerBound])
90+
let remainingEnd = value.index(value.endIndex, offsetBy: -remainingSuffix.count)
91+
let remaining = String(value[limitRange.upperBound..<remainingEnd])
92+
return L("%@ limit · %@ remaining", limit, remaining)
93+
}
94+
}

Sources/CodexBar/MenuCardView.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,6 +1059,7 @@ extension UsageMenuCardView.Model {
10591059
if input.provider == .sub2api {
10601060
details = Self.sub2APILocalizedDetails(details)
10611061
}
1062+
details = Self.localizedProviderDetails(details, provider: input.provider)
10621063
guard input.hidePersonalInfo else { return details }
10631064
return details.compactMap { section in
10641065
let rows = section.rows.compactMap { row in
@@ -1397,6 +1398,9 @@ extension UsageMenuCardView.Model {
13971398
Self.applyPrimaryPacePresentation(&presentation, input: input, primary: primary)
13981399
}
13991400
Self.applyPrimaryFinalOverrides(&presentation, input: input, primary: primary)
1401+
if input.provider == .zai, let resetText = Self.localizedZaiPeriodicResetText(primary) {
1402+
presentation.resetText = resetText
1403+
}
14001404
if let bindingProjection {
14011405
let resetWindow = RateWindow(
14021406
usedPercent: bindingProjection.usedPercent,

Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,3 +1500,20 @@
15001500
"menu_bar_layout_conditional_default_session_spent" = "会话即将用尽";
15011501
"menu_bar_layout_conditional_default_either_high" = "会话或每周用量偏高";
15021502
"menu_bar_layout_conditional_default_scoped_weekly" = "模型专属每周已用超过 60%";
1503+
1504+
/* Provider usage details */
1505+
"Detailed usage" = "用量明细";
1506+
"Cache-hit input" = "缓存命中输入";
1507+
"Cache-miss input" = "缓存未命中输入";
1508+
"Daily tokens" = "每日 token";
1509+
"Quota details" = "配额详情";
1510+
"Token quota" = "Token 配额";
1511+
"Credit quota" = "额度配额";
1512+
"Session token quota" = "会话 Token 配额";
1513+
"Session credit quota" = "会话额度配额";
1514+
"MCP quota" = "MCP 配额";
1515+
"Hourly tokens" = "每小时 token";
1516+
"%@ used" = "已使用 %@";
1517+
"%@ limit · %@ remaining" = "上限 %@ · 剩余 %@";
1518+
"%@ (Paid: %@ / Granted: %@)" = "%@(付费:%@ / 赠送:%@)";
1519+
"Resets every 5 hours" = "每 5 小时重置";

Tests/CodexBarTests/MenuCardDeepSeekTests.swift

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,84 @@ struct MenuCardDeepSeekTests {
152152
#expect(details.rows.first { $0.label == "This month" }?.value == "¥0.0456 · 456 tokens")
153153
}
154154

155+
@Test
156+
func `model localizes deepseek usage details in simplified chinese`() throws {
157+
let now = Date()
158+
let metadata = try #require(ProviderDefaults.metadata[.deepseek])
159+
let snapshot = Self.makeSnapshot(now: now, usageSummary: Self.sampleDeepSeekSummary(now: now))
160+
161+
let model = CodexBarLocalizationOverride.$appLanguage.withValue("zh-Hans") {
162+
UsageMenuCardView.Model.make(.init(
163+
provider: .deepseek,
164+
metadata: metadata,
165+
snapshot: snapshot,
166+
credits: nil,
167+
creditsError: nil,
168+
dashboard: nil,
169+
dashboardError: nil,
170+
tokenSnapshot: nil,
171+
tokenError: nil,
172+
account: AccountInfo(email: nil, plan: nil),
173+
isRefreshing: false,
174+
lastError: nil,
175+
usageBarsShowUsed: false,
176+
resetTimeDisplayStyle: .countdown,
177+
tokenCostUsageEnabled: true,
178+
showOptionalCreditsAndExtraUsage: true,
179+
hidePersonalInfo: false,
180+
now: now))
181+
}
182+
183+
let details = try #require(model.providerDetails.first)
184+
#expect(details.title == "用量明细")
185+
#expect(details.rows.map(\.label) == [
186+
"今日",
187+
"本月",
188+
"请求",
189+
"最常用模型",
190+
"缓存命中输入",
191+
"缓存未命中输入",
192+
"输出",
193+
])
194+
#expect(details.rows[0].value == "¥0.0123 · 123 token 用量")
195+
#expect(details.rows[1].value == "¥0.0456 · 456 token 用量")
196+
#expect(details.rows[3].value == "deepseek-chat")
197+
#expect(details.chart?.title == "每日 token")
198+
#expect(details.chart?.unit == "token")
199+
}
200+
201+
@Test
202+
func `model localizes deepseek balance components in simplified chinese`() throws {
203+
let now = Date()
204+
let metadata = try #require(ProviderDefaults.metadata[.deepseek])
205+
let snapshot = Self.makeSnapshot(now: now)
206+
207+
let model = CodexBarLocalizationOverride.$appLanguage.withValue("zh-Hans") {
208+
UsageMenuCardView.Model.make(.init(
209+
provider: .deepseek,
210+
metadata: metadata,
211+
snapshot: snapshot,
212+
credits: nil,
213+
creditsError: nil,
214+
dashboard: nil,
215+
dashboardError: nil,
216+
tokenSnapshot: nil,
217+
tokenError: nil,
218+
account: AccountInfo(email: nil, plan: nil),
219+
isRefreshing: false,
220+
lastError: nil,
221+
usageBarsShowUsed: false,
222+
resetTimeDisplayStyle: .countdown,
223+
tokenCostUsageEnabled: false,
224+
showOptionalCreditsAndExtraUsage: true,
225+
hidePersonalInfo: false,
226+
now: now))
227+
}
228+
229+
let balance = try #require(model.metrics.first)
230+
#expect(balance.statusText == "$9.32(付费:$9.32 / 赠送:$0.00)")
231+
}
232+
155233
@Test
156234
func `model explains unavailable deepseek usage when cost summary is enabled`() throws {
157235
let now = Date()

Tests/CodexBarTests/UserFacingLocalizationCoverageTests.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import CodexBarCore
12
import Foundation
23
import Testing
34
@testable import CodexBar
@@ -117,6 +118,35 @@ struct UserFacingLocalizationCoverageTests {
117118
"Raw user-facing localization markers remain:\n\(violations.joined(separator: "\n"))")
118119
}
119120

121+
@Test
122+
func `provider detail localization preserves technical identifiers`() throws {
123+
let details = try [
124+
ProviderDetailSection(
125+
title: "Usage",
126+
rows: [
127+
.init(label: "Balance", value: "$12.34"),
128+
.init(label: "Top model", value: "deepseek-v4-flash"),
129+
],
130+
chart: .init(
131+
kind: .bars,
132+
title: "Usage",
133+
unit: "tokens",
134+
points: [.init(label: "2026-08-20", value: 42)])),
135+
]
136+
137+
let localized = CodexBarLocalizationOverride.$appLanguage.withValue("zh-Hans") {
138+
UsageMenuCardView.Model.localizedProviderDetails(details, provider: .groq)
139+
}
140+
141+
let section = try #require(localized.first)
142+
#expect(section.title == "用量")
143+
#expect(section.rows.map(\.label) == ["余额", "最常用模型"])
144+
#expect(section.rows.map(\.value) == ["$12.34", "deepseek-v4-flash"])
145+
#expect(section.chart?.title == "用量")
146+
#expect(section.chart?.unit == "token")
147+
#expect(section.chart?.points.first?.label == "2026-08-20")
148+
}
149+
120150
@Test
121151
func `spend dashboard model breakdown state stays precise and localized`() throws {
122152
let root = URL(fileURLWithPath: #filePath)

Tests/CodexBarTests/ZaiMenuCardTests.swift

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,83 @@ struct ZaiMenuCardTests {
7878
#expect(mcp.secondaryValue == "100 limit · 50 remaining")
7979
}
8080

81+
@MainActor
82+
@Test
83+
func `model localizes zai usage sections in simplified chinese`() throws {
84+
let model = try CodexBarLocalizationOverride.$appLanguage.withValue("zh-Hans") {
85+
try Self.costSummaryModel(style: .inlineSummary)
86+
}
87+
88+
#expect(model.providerDetails.map(\.title) == ["配额详情", "每小时 token", "每日 token"])
89+
#expect(model.providerDetails[0].rows[0].label == "Token 配额")
90+
#expect(model.providerDetails[0].rows[0].value == "已使用 3%")
91+
#expect(model.providerDetails[1].rows[0].label == "GLM-5.3")
92+
#expect(model.providerDetails[1].chart?.title == "每小时 token")
93+
#expect(model.providerDetails[1].chart?.unit == "token")
94+
}
95+
96+
@Test
97+
func `model localizes zai quota values and periodic reset in simplified chinese`() throws {
98+
let now = Date()
99+
let details = try ProviderDetailSection(title: "Quota details", rows: [
100+
.init(label: "Token quota", value: "45% used"),
101+
.init(label: "Session token quota", value: "0% used"),
102+
.init(label: "MCP quota", value: "6.4% used", secondaryValue: "1000 limit · 936 remaining"),
103+
.init(label: "search-prime", value: "64"),
104+
])
105+
let snapshot = UsageSnapshot(
106+
primary: RateWindow(usedPercent: 0, windowMinutes: 300, resetsAt: nil, resetDescription: "5-hour"),
107+
secondary: RateWindow(
108+
usedPercent: 45,
109+
windowMinutes: 10080,
110+
resetsAt: now.addingTimeInterval(3 * 24 * 60 * 60),
111+
resetDescription: nil),
112+
extraRateWindows: [NamedRateWindow(
113+
id: "zai-mcp",
114+
title: "MCP",
115+
window: RateWindow(usedPercent: 6.4, windowMinutes: nil, resetsAt: nil, resetDescription: "MCP"))],
116+
details: [details],
117+
updatedAt: now,
118+
identity: ProviderIdentitySnapshot(
119+
providerID: .zai,
120+
accountEmail: nil,
121+
accountOrganization: nil,
122+
loginMethod: "Pro"))
123+
let metadata = try #require(ProviderDefaults.metadata[.zai])
124+
125+
let model = CodexBarLocalizationOverride.$appLanguage.withValue("zh-Hans") {
126+
UsageMenuCardView.Model.make(.init(
127+
provider: .zai,
128+
metadata: metadata,
129+
snapshot: snapshot,
130+
credits: nil,
131+
creditsError: nil,
132+
dashboard: nil,
133+
dashboardError: nil,
134+
tokenSnapshot: nil,
135+
tokenError: nil,
136+
account: AccountInfo(email: nil, plan: nil),
137+
isRefreshing: false,
138+
lastError: nil,
139+
usageBarsShowUsed: false,
140+
resetTimeDisplayStyle: .countdown,
141+
tokenCostUsageEnabled: false,
142+
showOptionalCreditsAndExtraUsage: true,
143+
hidePersonalInfo: false,
144+
now: now))
145+
}
146+
147+
#expect(model.metrics.first?.resetText == "每 5 小时重置")
148+
let rows = try #require(model.providerDetails.first?.rows)
149+
#expect(rows[0].value == "已使用 45%")
150+
#expect(rows[1].label == "会话 Token 配额")
151+
#expect(rows[1].value == "已使用 0%")
152+
#expect(rows[2].label == "MCP 配额")
153+
#expect(rows[2].value == "已使用 6.4%")
154+
#expect(rows[2].secondaryValue == "上限 1000 · 剩余 936")
155+
#expect(rows[3].label == "search-prime")
156+
}
157+
81158
@MainActor
82159
private static func costSummaryModel(style: CostSummaryDisplayStyle) throws -> UsageMenuCardView.Model {
83160
let settings = testSettingsStore(suiteName: "ZaiMenuCardTests-cost-summary-\(style.rawValue)")

0 commit comments

Comments
 (0)