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
3 changes: 3 additions & 0 deletions Sources/CodexBar/MenuCardView+ModelHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ extension UsageMenuCardView.Model {
presentation.resetText = regen.resetText
Self.apply(regen.pace, to: &presentation)
}
if input.provider == .deepseek, let detail = presentation.detailText {
presentation.detailText = Self.localizedDeepSeekBalanceDescription(detail)
}
if policy.movesPrimaryDetailToStatus(snapshot: input.snapshot) {
presentation.statusText = presentation.detailText
presentation.detailText = nil
Expand Down
94 changes: 94 additions & 0 deletions Sources/CodexBar/MenuCardView+ProviderDetailLocalization.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import CodexBarCore
import Foundation

extension UsageMenuCardView.Model {
static func localizedProviderDetails(
_ details: [ProviderDetailSection],
provider: UsageProvider) -> [ProviderDetailSection]
{
details.compactMap { section in
let rows = section.rows.compactMap { row in
try? ProviderDetailSection.Row(
label: L(row.label),
value: self.localizedProviderDetailValue(row.value, provider: provider),
secondaryValue: row.secondaryValue.map {
self.localizedProviderDetailValue($0, provider: provider)
})
}
let chart = section.chart.flatMap { chart in
try? ProviderDetailSection.Chart(
kind: chart.kind,
title: chart.title.map(L),
unit: chart.unit.map(L),
points: chart.points)
}
return try? ProviderDetailSection(
title: section.title.map(L),
rows: rows,
chart: chart)
}
}

static func localizedDeepSeekBalanceDescription(_ description: String) -> String {
let paidSeparator = " (Paid: "
let grantedSeparator = " / Granted: "
guard let paidRange = description.range(of: paidSeparator),
let grantedRange = description.range(
of: grantedSeparator,
range: paidRange.upperBound..<description.endIndex),
description.last == ")"
else {
return description
Comment on lines +35 to +41

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Translate DeepSeek's unavailable balance states

When a DeepSeek account has no balance or API access is unavailable, DeepSeekUsageFetcher.swift constructs "¥0.00 — add credits at platform.deepseek.com" or "Balance unavailable for API calls". This guard only recognizes the paid/granted form, so those important status messages remain English under Simplified Chinese; handle and localize these alternate balance descriptions as well.

Useful? React with 👍 / 👎.

}

let total = String(description[..<paidRange.lowerBound])
let paid = String(description[paidRange.upperBound..<grantedRange.lowerBound])
let granted = String(description[grantedRange.upperBound..<description.index(before: description.endIndex)])
return L("%@ (Paid: %@ / Granted: %@)", total, paid, granted)
}

static func localizedZaiPeriodicResetText(_ window: RateWindow) -> String? {
guard window.resetsAt == nil,
window.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines) == "5-hour"
else {
return nil
}
return L("Resets every 5 hours")
}

private static func localizedProviderDetailValue(_ value: String, provider: UsageProvider) -> String {
switch provider {
case .deepseek:
self.localizedTokenSuffix(value)
case .zai:
self.localizedZaiValue(value)
Comment on lines +63 to +64

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Localize the z.ai credit-plan rate row

For every z.ai CREDIT_LIMIT plan, zai.js appends a Quota rate row whose value is Peak or Off-peak and whose secondary text is a dynamic English countdown. The z.ai localization path does not recognize any of these values, and no Simplified Chinese key was added for the row label, so credit-plan users still receive an entirely English detail row.

Useful? React with 👍 / 👎.

default:
value
}
}

private static func localizedTokenSuffix(_ value: String) -> String {
let suffix = " tokens"
guard value.hasSuffix(suffix) else { return value }
return L("%@ tokens", String(value.dropLast(suffix.count)))
}

private static func localizedZaiValue(_ value: String) -> String {
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
Comment on lines +82 to +87

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Translate one-sided z.ai quota amounts

When the z.ai response supplies only usage or only remaining—both fields are independently optional in zai.js—the plugin emits a secondary value such as "1000 limit" or "936 remaining". This guard recognizes only the combined "1000 limit · 936 remaining" form and returns either one-sided form unchanged, leaving part of the Simplified Chinese quota card in English.

Useful? React with 👍 / 👎.

}
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)
}
}
4 changes: 4 additions & 0 deletions Sources/CodexBar/MenuCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,7 @@ extension UsageMenuCardView.Model {
if input.provider == .sub2api {
details = Self.sub2APILocalizedDetails(details)
}
details = Self.localizedProviderDetails(details, provider: input.provider)
guard input.hidePersonalInfo else { return details }
return details.compactMap { section in
let rows = section.rows.compactMap { row in
Expand Down Expand Up @@ -1397,6 +1398,9 @@ extension UsageMenuCardView.Model {
Self.applyPrimaryPacePresentation(&presentation, input: input, primary: primary)
}
Self.applyPrimaryFinalOverrides(&presentation, input: input, primary: primary)
if input.provider == .zai, let resetText = Self.localizedZaiPeriodicResetText(primary) {
presentation.resetText = resetText
}
if let bindingProjection {
let resetWindow = RateWindow(
usedPercent: bindingProjection.usedPercent,
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 @@ -1500,3 +1500,20 @@
"menu_bar_layout_conditional_default_session_spent" = "会话即将用尽";
"menu_bar_layout_conditional_default_either_high" = "会话或每周用量偏高";
"menu_bar_layout_conditional_default_scoped_weekly" = "模型专属每周已用超过 60%";

/* Provider usage details */
"Detailed usage" = "用量明细";
"Cache-hit input" = "缓存命中输入";
"Cache-miss input" = "缓存未命中输入";
"Daily tokens" = "每日 token";
"Quota details" = "配额详情";
"Token quota" = "Token 配额";
"Credit quota" = "额度配额";
"Session token quota" = "会话 Token 配额";
"Session credit quota" = "会话额度配额";
"MCP quota" = "MCP 配额";
"Hourly tokens" = "每小时 token";
"%@ used" = "已使用 %@";
"%@ limit · %@ remaining" = "上限 %@ · 剩余 %@";
"%@ (Paid: %@ / Granted: %@)" = "%@(付费:%@ / 赠送:%@)";
"Resets every 5 hours" = "每 5 小时重置";
78 changes: 78 additions & 0 deletions Tests/CodexBarTests/MenuCardDeepSeekTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,84 @@ struct MenuCardDeepSeekTests {
#expect(details.rows.first { $0.label == "This month" }?.value == "¥0.0456 · 456 tokens")
}

@Test
func `model localizes deepseek usage details in simplified chinese`() throws {
let now = Date()
let metadata = try #require(ProviderDefaults.metadata[.deepseek])
let snapshot = Self.makeSnapshot(now: now, usageSummary: Self.sampleDeepSeekSummary(now: now))

let model = CodexBarLocalizationOverride.$appLanguage.withValue("zh-Hans") {
UsageMenuCardView.Model.make(.init(
provider: .deepseek,
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: false,
resetTimeDisplayStyle: .countdown,
tokenCostUsageEnabled: true,
showOptionalCreditsAndExtraUsage: true,
hidePersonalInfo: false,
now: now))
}

let details = try #require(model.providerDetails.first)
#expect(details.title == "用量明细")
#expect(details.rows.map(\.label) == [
"今日",
"本月",
"请求",
"最常用模型",
"缓存命中输入",
"缓存未命中输入",
"输出",
])
#expect(details.rows[0].value == "¥0.0123 · 123 token 用量")
#expect(details.rows[1].value == "¥0.0456 · 456 token 用量")
#expect(details.rows[3].value == "deepseek-chat")
#expect(details.chart?.title == "每日 token")
#expect(details.chart?.unit == "token")
}

@Test
func `model localizes deepseek balance components in simplified chinese`() throws {
let now = Date()
let metadata = try #require(ProviderDefaults.metadata[.deepseek])
let snapshot = Self.makeSnapshot(now: now)

let model = CodexBarLocalizationOverride.$appLanguage.withValue("zh-Hans") {
UsageMenuCardView.Model.make(.init(
provider: .deepseek,
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: false,
resetTimeDisplayStyle: .countdown,
tokenCostUsageEnabled: false,
showOptionalCreditsAndExtraUsage: true,
hidePersonalInfo: false,
now: now))
}

let balance = try #require(model.metrics.first)
#expect(balance.statusText == "$9.32(付费:$9.32 / 赠送:$0.00)")
}

@Test
func `model explains unavailable deepseek usage when cost summary is enabled`() throws {
let now = Date()
Expand Down
30 changes: 30 additions & 0 deletions Tests/CodexBarTests/UserFacingLocalizationCoverageTests.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CodexBarCore
import Foundation
import Testing
@testable import CodexBar
Expand Down Expand Up @@ -117,6 +118,35 @@ struct UserFacingLocalizationCoverageTests {
"Raw user-facing localization markers remain:\n\(violations.joined(separator: "\n"))")
}

@Test
func `provider detail localization preserves technical identifiers`() throws {
let details = try [
ProviderDetailSection(
title: "Usage",
rows: [
.init(label: "Balance", value: "$12.34"),
.init(label: "Top model", value: "deepseek-v4-flash"),
],
chart: .init(
kind: .bars,
title: "Usage",
unit: "tokens",
points: [.init(label: "2026-08-20", value: 42)])),
]

let localized = CodexBarLocalizationOverride.$appLanguage.withValue("zh-Hans") {
UsageMenuCardView.Model.localizedProviderDetails(details, provider: .groq)
}

let section = try #require(localized.first)
#expect(section.title == "用量")
#expect(section.rows.map(\.label) == ["余额", "最常用模型"])
#expect(section.rows.map(\.value) == ["$12.34", "deepseek-v4-flash"])
#expect(section.chart?.title == "用量")
#expect(section.chart?.unit == "token")
#expect(section.chart?.points.first?.label == "2026-08-20")
}

@Test
func `spend dashboard model breakdown state stays precise and localized`() throws {
let root = URL(fileURLWithPath: #filePath)
Expand Down
77 changes: 77 additions & 0 deletions Tests/CodexBarTests/ZaiMenuCardTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,83 @@ struct ZaiMenuCardTests {
#expect(mcp.secondaryValue == "100 limit · 50 remaining")
}

@MainActor
@Test
func `model localizes zai usage sections in simplified chinese`() throws {
let model = try CodexBarLocalizationOverride.$appLanguage.withValue("zh-Hans") {
try Self.costSummaryModel(style: .inlineSummary)
}

#expect(model.providerDetails.map(\.title) == ["配额详情", "每小时 token", "每日 token"])
#expect(model.providerDetails[0].rows[0].label == "Token 配额")
#expect(model.providerDetails[0].rows[0].value == "已使用 3%")
#expect(model.providerDetails[1].rows[0].label == "GLM-5.3")
#expect(model.providerDetails[1].chart?.title == "每小时 token")
#expect(model.providerDetails[1].chart?.unit == "token")
}

@Test
func `model localizes zai quota values and periodic reset in simplified chinese`() throws {
let now = Date()
let details = try ProviderDetailSection(title: "Quota details", rows: [
.init(label: "Token quota", value: "45% used"),
.init(label: "Session token quota", value: "0% used"),
.init(label: "MCP quota", value: "6.4% used", secondaryValue: "1000 limit · 936 remaining"),
.init(label: "search-prime", value: "64"),
])
let snapshot = UsageSnapshot(
primary: RateWindow(usedPercent: 0, windowMinutes: 300, resetsAt: nil, resetDescription: "5-hour"),
secondary: RateWindow(
usedPercent: 45,
windowMinutes: 10080,
resetsAt: now.addingTimeInterval(3 * 24 * 60 * 60),
resetDescription: nil),
extraRateWindows: [NamedRateWindow(
id: "zai-mcp",
title: "MCP",
window: RateWindow(usedPercent: 6.4, windowMinutes: nil, resetsAt: nil, resetDescription: "MCP"))],
details: [details],
updatedAt: now,
identity: ProviderIdentitySnapshot(
providerID: .zai,
accountEmail: nil,
accountOrganization: nil,
loginMethod: "Pro"))
let metadata = try #require(ProviderDefaults.metadata[.zai])

let model = CodexBarLocalizationOverride.$appLanguage.withValue("zh-Hans") {
UsageMenuCardView.Model.make(.init(
provider: .zai,
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: false,
resetTimeDisplayStyle: .countdown,
tokenCostUsageEnabled: false,
showOptionalCreditsAndExtraUsage: true,
hidePersonalInfo: false,
now: now))
}

#expect(model.metrics.first?.resetText == "每 5 小时重置")
let rows = try #require(model.providerDetails.first?.rows)
#expect(rows[0].value == "已使用 45%")
#expect(rows[1].label == "会话 Token 配额")
#expect(rows[1].value == "已使用 0%")
#expect(rows[2].label == "MCP 配额")
#expect(rows[2].value == "已使用 6.4%")
#expect(rows[2].secondaryValue == "上限 1000 · 剩余 936")
#expect(rows[3].label == "search-prime")
}

@MainActor
private static func costSummaryModel(style: CostSummaryDisplayStyle) throws -> UsageMenuCardView.Model {
let settings = testSettingsStore(suiteName: "ZaiMenuCardTests-cost-summary-\(style.rawValue)")
Expand Down