Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
104 changes: 104 additions & 0 deletions .github/pr-proof/zh-hans-provider-detail-edge-cases.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
$WORKTREE/Tests/CodexBarTests/OpenCodeGoUsageFetcherCLIWaitTests.swift:213:79: warning: conformance of 'OpenCodeGoCLIWaitStubURLProtocol' to protocol 'Sendable' is already unavailable
211 | }
212 |
213 | private final class OpenCodeGoCLIWaitStubURLProtocol: URLProtocol, @unchecked Sendable {
| | `- warning: conformance of 'OpenCodeGoCLIWaitStubURLProtocol' to protocol 'Sendable' is already unavailable
| `- note: 'OpenCodeGoCLIWaitStubURLProtocol' inherits conformance to protocol 'Sendable' from superclass here
214 | private static let handlerBox = LockIsolated<((URLRequest) throws -> (HTTPURLResponse, Data))?>(nil)
215 | static var handler: ((URLRequest) throws -> (HTTPURLResponse, Data))? {
Build complete! (34.86s)
Test Suite 'Selected tests' started at 2026-08-20 10:52:41.809.
Test Suite 'CodexBarPackageTests.xctest' started at 2026-08-20 10:52:41.809.
Test Suite 'CodexBarPackageTests.xctest' passed at 2026-08-20 10:52:41.809.
Executed 0 tests, with 0 failures (0 unexpected) in 0.000 (0.000) seconds
Test Suite 'Selected tests' passed at 2026-08-20 10:52:41.809.
Executed 0 tests, with 0 failures (0 unexpected) in 0.000 (0.001) seconds
◇ Test run started.
↳ Testing Library Version: 1902
↳ Target Platform: arm64e-apple-macos14.0
◇ Suite MenuCardDeepSeekTests started.
◇ Test "model hides deepseek usage when cost summary is disabled despite extras enabled" started.
◇ Test "model localizes deepseek balance fallback messages in simplified chinese" started.
◇ Test "model shows deepseek usage when cost summary and extras are enabled" started.
◇ Test "browser only sign in remains visible when cost summary is disabled" started.
◇ Test "model hides deepseek usage when extras are disabled despite cost summary enabled" started.
◇ Test "model explains that detailed usage needs a platform session" started.
◇ Test "model shows balance as status text instead of percentage detail" started.
◇ Test "model shows balance without stale deepseek usage while refreshing" started.
◇ Test "model asks for a profile when multiple deepseek sessions are valid" started.
◇ Test "model localizes deepseek balance components in simplified chinese" started.
◇ Test "model explains unavailable deepseek usage when cost summary is enabled" started.
◇ Test "model localizes deepseek usage details in simplified chinese" started.
✔ Test "model localizes deepseek balance fallback messages in simplified chinese" passed after 0.008 seconds.
✔ Test "model explains unavailable deepseek usage when cost summary is enabled" passed after 0.008 seconds.
✔ Test "browser only sign in remains visible when cost summary is disabled" passed after 0.008 seconds.
✔ Test "model explains that detailed usage needs a platform session" passed after 0.008 seconds.
✔ Test "model asks for a profile when multiple deepseek sessions are valid" passed after 0.008 seconds.
✔ Test "model hides deepseek usage when cost summary is disabled despite extras enabled" passed after 0.008 seconds.
✔ Test "model localizes deepseek balance components in simplified chinese" passed after 0.008 seconds.
✔ Test "model shows balance as status text instead of percentage detail" passed after 0.008 seconds.
✔ Test "model hides deepseek usage when extras are disabled despite cost summary enabled" passed after 0.008 seconds.
✔ Test "model shows deepseek usage when cost summary and extras are enabled" passed after 0.008 seconds.
✔ Test "model shows balance without stale deepseek usage while refreshing" passed after 0.008 seconds.
✔ Test "model localizes deepseek usage details in simplified chinese" passed after 0.008 seconds.
✔ Suite MenuCardDeepSeekTests passed after 0.009 seconds.
✔ Test run with 12 tests in 1 suite passed after 0.009 seconds.
Build complete! (0.19s)
Test Suite 'Selected tests' started at 2026-08-20 10:52:42.835.
Test Suite 'CodexBarPackageTests.xctest' started at 2026-08-20 10:52:42.835.
Test Suite 'CodexBarPackageTests.xctest' passed at 2026-08-20 10:52:42.835.
Executed 0 tests, with 0 failures (0 unexpected) in 0.000 (0.000) seconds
Test Suite 'Selected tests' passed at 2026-08-20 10:52:42.835.
Executed 0 tests, with 0 failures (0 unexpected) in 0.000 (0.001) seconds
◇ Test run started.
↳ Testing Library Version: 1902
↳ Target Platform: arm64e-apple-macos14.0
◇ Suite ZaiMenuCardTests started.
◇ Test "inline style keeps zai inline cost summary details" started.
◇ Test "zai metrics titles are 5-hour weekly and MCP when session token limit present" started.
◇ Test "model localizes zai quota values and periodic reset in simplified chinese" started.
◇ Test "model localizes zai usage sections in simplified chinese" started.
◇ Test "submenu only hides zai inline cost summary details" started.
✔ Test "model localizes zai quota values and periodic reset in simplified chinese" passed after 0.006 seconds.
✔ Test "zai metrics titles are 5-hour weekly and MCP when session token limit present" passed after 0.006 seconds.
2026-08-20T10:52:43+0800 info com.steipete.codexbar.logging: path=$HOME/Library/Logs/CodexBar/CodexBar.log [CodexBarCore] File logging disabled
2026-08-20T10:52:43+0800 info com.steipete.codexbar.settings: enabled=0 [CodexBarCore] Claude web extras updated
2026-08-20T10:52:43+0800 info com.steipete.codexbar.settings: enabled=0 [CodexBarCore] OpenAI web access updated
✔ Test "inline style keeps zai inline cost summary details" passed after 0.010 seconds.
2026-08-20T10:52:43+0800 info com.steipete.codexbar.logging: path=$HOME/Library/Logs/CodexBar/CodexBar.log [CodexBarCore] File logging disabled
2026-08-20T10:52:43+0800 info com.steipete.codexbar.settings: enabled=0 [CodexBarCore] Claude web extras updated
2026-08-20T10:52:43+0800 info com.steipete.codexbar.settings: enabled=0 [CodexBarCore] OpenAI web access updated
✔ Test "model localizes zai usage sections in simplified chinese" passed after 0.016 seconds.
2026-08-20T10:52:43+0800 info com.steipete.codexbar.logging: path=$HOME/Library/Logs/CodexBar/CodexBar.log [CodexBarCore] File logging disabled
2026-08-20T10:52:43+0800 info com.steipete.codexbar.settings: enabled=0 [CodexBarCore] Claude web extras updated
2026-08-20T10:52:43+0800 info com.steipete.codexbar.settings: enabled=0 [CodexBarCore] OpenAI web access updated
✔ Test "submenu only hides zai inline cost summary details" passed after 0.021 seconds.
✔ Suite ZaiMenuCardTests passed after 0.021 seconds.
✔ Test run with 5 tests in 1 suite passed after 0.021 seconds.
Build complete! (0.18s)
Test Suite 'Selected tests' started at 2026-08-20 10:52:43.837.
Test Suite 'CodexBarPackageTests.xctest' started at 2026-08-20 10:52:43.837.
Test Suite 'CodexBarPackageTests.xctest' passed at 2026-08-20 10:52:43.837.
Executed 0 tests, with 0 failures (0 unexpected) in 0.000 (0.000) seconds
Test Suite 'Selected tests' passed at 2026-08-20 10:52:43.837.
Executed 0 tests, with 0 failures (0 unexpected) in 0.000 (0.001) seconds
◇ Test run started.
↳ Testing Library Version: 1902
↳ Target Platform: arm64e-apple-macos14.0
◇ Suite UserFacingLocalizationCoverageTests started.
◇ Test "spend dashboard chart keeps validated points when aggregate total is unavailable" started.
◇ Test "hourly chart labels include the date across multiple days" started.
◇ Test "spend dashboard model breakdown state stays precise and localized" started.
◇ Test "hourly chart labels disambiguate repeated DST fallback hours" started.
◇ Test "provider detail localization preserves technical identifiers" started.
◇ Test "selected user-facing UI surfaces avoid raw English literals" started.
✔ Test "spend dashboard model breakdown state stays precise and localized" passed after 0.001 seconds.
✔ Test "hourly chart labels disambiguate repeated DST fallback hours" passed after 0.002 seconds.
✔ Test "spend dashboard chart keeps validated points when aggregate total is unavailable" passed after 0.005 seconds.
✔ Test "hourly chart labels include the date across multiple days" passed after 0.005 seconds.
✔ Test "provider detail localization preserves technical identifiers" passed after 0.005 seconds.
✔ Test "selected user-facing UI surfaces avoid raw English literals" passed after 0.023 seconds.
✔ Suite UserFacingLocalizationCoverageTests passed after 0.023 seconds.
✔ Test run with 6 tests in 1 suite passed after 0.023 seconds.
app locale checker tests OK
App locales OK: Checked 22 catalogs against 1472 English keys.
Binary file added .github/pr-proof/zh-hans-zai-provider-menu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion Sources/CodexBar/MenuCardView+ModelHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@ extension UsageMenuCardView.Model {
snapshot: UsageSnapshot) -> (primary: String, secondary: String, tertiary: String, showsTertiary: Bool)
{
if input.provider == .factory, snapshot.tertiary != nil {
return ("5-hour", L("Weekly"), L("Monthly"), true)
return (L("5-hour"), L("Weekly"), L("Monthly"), true)
}
// Legacy request-based Cursor plans track a request quota, not the token-based "Total" pool.
let primaryLabel = if input.provider == .cursor, snapshot.detailRow(label: "Request quota") != nil {
Expand Down
83 changes: 74 additions & 9 deletions Sources/CodexBar/MenuCardView+ProviderDetailLocalization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ extension UsageMenuCardView.Model {
_ details: [ProviderDetailSection],
provider: UsageProvider) -> [ProviderDetailSection]
{
details.compactMap { section in
// Provider-specific by design: only DeepSeek and z.ai expose these localized detail contracts.
guard provider == .deepseek || provider == .zai else { return details }
return details.compactMap { section in
let rows = section.rows.compactMap { row in
try? ProviderDetailSection.Row(
label: L(row.label),
Expand All @@ -30,6 +32,19 @@ extension UsageMenuCardView.Model {
}

static func localizedDeepSeekBalanceDescription(_ description: String) -> String {
let unavailable = "Balance unavailable for API calls"
if description == unavailable {
return L(unavailable)
}

let addCreditsSeparator = " — add credits at "
if let range = description.range(of: addCreditsSeparator) {
return L(
"%@ — add credits at %@",
String(description[..<range.lowerBound]),
String(description[range.upperBound...]))
}

let paidSeparator = " (Paid: "
let grantedSeparator = " / Granted: "
guard let paidRange = description.range(of: paidSeparator),
Expand Down Expand Up @@ -76,21 +91,71 @@ extension UsageMenuCardView.Model {
}

private static func localizedZaiValue(_ value: String) -> String {
if value == "Peak" || value == "Off-peak" {
return L(value)
}
for rate in ["peak", "off-peak"] {
let prefix = "\(rate) "
if value.hasPrefix(prefix) {
let countdown = String(value.dropFirst(prefix.count))
return "\(L(rate)) \(self.localizedZaiCountdown(countdown))"
}
}

let usedSuffix = " used"
if value.hasSuffix(usedSuffix) {
return L("%@ used", String(value.dropLast(usedSuffix.count)))
}

let limitSeparator = " limit · "
let remainingSuffix = " remaining"
guard let limitRange = value.range(of: limitSeparator),
value.hasSuffix(remainingSuffix)
else {
return value
if let limitRange = value.range(of: limitSeparator),
value.hasSuffix(remainingSuffix)
{
let limit = String(value[..<limitRange.lowerBound])
let remainingEnd = value.index(value.endIndex, offsetBy: -remainingSuffix.count)
let remaining = String(value[limitRange.upperBound..<remainingEnd])
return L("%@ limit · %@ remaining", limit, remaining)
}

let limitSuffix = " limit"
if value.hasSuffix(limitSuffix) {
return L("%@ limit", String(value.dropLast(limitSuffix.count)))
}
if value.hasSuffix(remainingSuffix) {
return L("%@ remaining", String(value.dropLast(remainingSuffix.count)))
}
return value
}

private static func localizedZaiCountdown(_ value: String) -> String {
guard value.hasPrefix("in ") else { return L(value) }
let parts = value.dropFirst(3).split(separator: " ").map(String.init)
if parts.count == 2 {
let first = parts[0]
let second = parts[1]
if first.hasSuffix("d"), second.hasSuffix("h") {
return L("in %@d %@h", String(first.dropLast()), String(second.dropLast()))
}
if first.hasSuffix("d"), second.hasSuffix("m") {
return L("in %@d %@m", String(first.dropLast()), String(second.dropLast()))
}
if first.hasSuffix("h"), second.hasSuffix("m") {
return L("in %@h %@m", String(first.dropLast()), String(second.dropLast()))
}
}
if parts.count == 1 {
let part = parts[0]
if part.hasSuffix("d") {
return L("in %@d", String(part.dropLast()))
}
if part.hasSuffix("h") {
return L("in %@h", String(part.dropLast()))
}
if part.hasSuffix("m") {
return L("in %@m", String(part.dropLast()))
}
}
let limit = String(value[..<limitRange.lowerBound])
let remainingEnd = value.index(value.endIndex, offsetBy: -remainingSuffix.count)
let remaining = String(value[limitRange.upperBound..<remainingEnd])
return L("%@ limit · %@ remaining", limit, remaining)
return value
}
}
2 changes: 1 addition & 1 deletion Sources/CodexBar/MenuDescriptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -678,7 +678,7 @@ struct MenuDescriptor {
snapshot: UsageSnapshot) -> (primary: String, secondary: String, tertiary: String, showsTertiary: Bool)
{
if provider == .factory, snapshot.tertiary != nil {
return ("5-hour", L("Weekly"), L("Monthly"), true)
return (L("5-hour"), L("Weekly"), L("Monthly"), true)
}
let primaryLabel = if provider == .codex {
CodexConsumerProjection.rateTitle(
Expand Down
17 changes: 17 additions & 0 deletions Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1517,3 +1517,20 @@
"%@ limit · %@ remaining" = "上限 %@ · 剩余 %@";
"%@ (Paid: %@ / Granted: %@)" = "%@(付费:%@ / 赠送:%@)";
"Resets every 5 hours" = "每 5 小时重置";
"5-hour" = "5 小时";
"%@ — add credits at %@" = "%@ — 请前往 %@ 充值";
"Balance unavailable for API calls" = "API 调用余额不可用";
"%@ limit" = "上限 %@";
"%@ remaining" = "剩余 %@";
"Quota rate" = "配额费率";
"Peak" = "高峰";
"Off-peak" = "非高峰";
"peak" = "高峰";
"off-peak" = "非高峰";
"in %@d %@h" = "%@ 天 %@ 小时后";
"in %@d %@m" = "%@ 天 %@ 分钟后";
"in %@h %@m" = "%@ 小时 %@ 分钟后";
"in %@d" = "%@ 天后";
"in %@h" = "%@ 小时后";
"in %@m" = "%@ 分钟后";
"now" = "现在";
14 changes: 14 additions & 0 deletions Tests/CodexBarTests/MenuCardDeepSeekTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,20 @@ struct MenuCardDeepSeekTests {
#expect(balance.statusText == "$9.32(付费:$9.32 / 赠送:$0.00)")
}

@Test
func `model localizes deepseek balance fallback messages in simplified chinese`() {
CodexBarLocalizationOverride.$appLanguage.withValue("zh-Hans") {
#expect(
UsageMenuCardView.Model.localizedDeepSeekBalanceDescription(
"¥0.00 — add credits at platform.deepseek.com")
== "¥0.00 — 请前往 platform.deepseek.com 充值")
#expect(
UsageMenuCardView.Model.localizedDeepSeekBalanceDescription(
"Balance unavailable for API calls")
== "API 调用余额不可用")
}
}

@Test
func `model explains unavailable deepseek usage when cost summary is enabled`() throws {
let now = Date()
Expand Down
36 changes: 36 additions & 0 deletions Tests/CodexBarTests/MenuCardModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,42 @@ struct FactoryMenuCardModelTests {
#expect(model.metrics.map(\.title) == ["5-hour", "Weekly", "Monthly"])
}

@Test
func `factory time window labels localize in simplified chinese`() throws {
try CodexBarLocalizationOverride.$appLanguage.withValue("zh-Hans") {
let now = Date()
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 12, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(usedPercent: 34, windowMinutes: 10080, resetsAt: nil, resetDescription: nil),
tertiary: RateWindow(usedPercent: 56, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: now,
identity: nil)
let metadata = try #require(ProviderDefaults.metadata[.factory])

let model = UsageMenuCardView.Model.make(.init(
provider: .factory,
metadata: metadata,
snapshot: snapshot,
credits: nil,
creditsError: nil,
dashboard: nil,
dashboardError: nil,
tokenSnapshot: nil,
tokenError: nil,
account: AccountInfo(email: nil, plan: nil),
isRefreshing: false,
lastError: nil,
usageBarsShowUsed: true,
resetTimeDisplayStyle: .countdown,
tokenCostUsageEnabled: false,
showOptionalCreditsAndExtraUsage: true,
hidePersonalInfo: false,
now: now))

#expect(model.metrics.map(\.title) == ["5 小时", "每周", "每月"])
}
}

@Test
func `factory legacy billing keeps pool labels`() throws {
let now = Date()
Expand Down
39 changes: 39 additions & 0 deletions Tests/CodexBarTests/PopupLocalizationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,45 @@ struct PopupLocalizationTests {
}
}

@Test
func `factory descriptor localizes every time window label`() throws {
try CodexBarLocalizationOverride.$appLanguage.withValue("zh-Hans") {
let suite = "PopupLocalizationTests-factory-rate-windows"
let settings = try Self.makeSettingsStore(suite: suite)
let store = UsageStore(
fetcher: UsageFetcher(environment: [:]),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
startupBehavior: .testing)
store._setSnapshotForTesting(
UsageSnapshot(
primary: RateWindow(usedPercent: 12, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
secondary: RateWindow(
usedPercent: 34,
windowMinutes: 10080,
resetsAt: nil,
resetDescription: nil),
tertiary: RateWindow(usedPercent: 56, windowMinutes: nil, resetsAt: nil, resetDescription: nil),
updatedAt: Date(),
identity: nil),
provider: .factory)

let descriptor = MenuDescriptor.build(
provider: .factory,
store: store,
settings: settings,
account: AccountInfo(email: nil, plan: nil),
updateReady: false,
includeContextualActions: false)
let lines = Self.textLines(from: descriptor)

#expect(lines.contains { $0.hasPrefix("5 小时:") })
#expect(lines.contains { $0.hasPrefix("每周:") })
#expect(lines.contains { $0.hasPrefix("每月:") })
#expect(!lines.contains { $0.hasPrefix("5-hour:") })
}
}

@Test
func `generic provider details keep canonical labels alongside localized core metrics`() throws {
try CodexBarLocalizationOverride.$appLanguage.withValue("zh-Hant") {
Expand Down
Loading