Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -1472,3 +1472,20 @@
"Total usage" = "总用量";
"claude_oauth_keychain_access_revoked" = "Claude Code 轮换令牌后撤销了对 Claude 钥匙串的访问权限。点击“刷新”以重新授权,或将 Claude 用量来源切换为 CLI/Web。";
"claude_showing_last_known_usage" = "正在显示 %@ 采集的最后已知用量。";

/* 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