Skip to content
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## 0.29.2 — Unreleased

### Fixed
- Localization: improve Simplified Chinese visible menu, dashboard, and usage labels (#1145). Thanks @Yuxin-Qiao!

## 0.29.1 — 2026-05-26

### Added
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/CodexbarApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ struct CodexBarApp: App {
let preferencesSelection = PreferencesSelection()
let settings = SettingsStore()
Self.applyLanguagePreference(from: settings)
configureUsageFormatterLocalizationProvider()
let managedCodexAccountCoordinator = ManagedCodexAccountCoordinator()
managedCodexAccountCoordinator.onManagedAccountsDidChange = {
_ = settings.persistResolvedCodexActiveSourceCorrectionIfNeeded()
Expand Down
20 changes: 13 additions & 7 deletions Sources/CodexBar/CostHistoryChartMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ struct CostHistoryChartMenuView: View {
let model = Self.makeModel(provider: self.provider, daily: self.daily)
VStack(alignment: .leading, spacing: 10) {
if model.points.isEmpty {
Text("No cost history data.")
Text(L("No cost history data."))
.font(.footnote)
.foregroundStyle(.secondary)
.accessibilityLabel("No cost history data available.")
.accessibilityLabel(L("No cost history data."))
} else {
Chart {
ForEach(model.points) { point in
Expand Down Expand Up @@ -171,7 +171,10 @@ struct CostHistoryChartMenuView: View {
}

if let total = self.totalCostUSD {
Text("Est. total (\(Self.windowLabel(days: self.historyDays))): \(UsageFormatter.usdString(total))")
Text(String(
format: L("Est. total (%@): %@"),
Self.windowLabel(days: self.historyDays),
UsageFormatter.usdString(total)))
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
Expand Down Expand Up @@ -205,8 +208,11 @@ struct CostHistoryChartMenuView: View {
private static let expandedDetailRowHeight: CGFloat = 44
private static let detailSpacing: CGFloat = 6

private static func windowLabel(days: Int) -> String {
days == 1 ? "today" : "\(days)d"
static func windowLabel(days: Int) -> String {
if days == 1 {
return L("Today")
}
return String(format: L("Last %d days"), days)
}

private static func detailRowHeight(for row: DetailRow) -> CGFloat {
Expand Down Expand Up @@ -416,13 +422,13 @@ struct CostHistoryChartMenuView: View {
let point = model.pointsByDateKey[key],
let date = Self.dateFromDayKey(key)
else {
return DetailContent(primary: "Hover a bar for details", rows: [])
return DetailContent(primary: L("Hover a bar for details"), rows: [])
}

let dayLabel = date.formatted(.dateTime.month(.abbreviated).day())
let cost = UsageFormatter.usdString(point.costUSD)
let primary = if let tokens = point.totalTokens {
"\(dayLabel): \(cost) · \(UsageFormatter.tokenCountString(tokens)) tokens"
String(format: L("%@: %@ · %@ tokens"), dayLabel, cost, UsageFormatter.tokenCountString(tokens))
} else {
"\(dayLabel): \(cost)"
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/CreditsHistoryChartMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ struct CreditsHistoryChartMenuView: View {
let day = model.breakdownByDayKey[key],
let date = Self.dateFromDayKey(key)
else {
return ("Hover a bar for details", nil)
return (L("Hover a bar for details"), nil)
}

let dayLabel = date.formatted(.dateTime.month(.abbreviated).day())
Expand Down
11 changes: 6 additions & 5 deletions Sources/CodexBar/Date+RelativeDescription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,22 @@ import Foundation

enum RelativeTimeFormatters {
@MainActor
static let full: RelativeDateTimeFormatter = {
static func full(locale: Locale) -> RelativeDateTimeFormatter {
let formatter = RelativeDateTimeFormatter()
formatter.locale = Locale(identifier: "en_US")
formatter.locale = locale
formatter.unitsStyle = .full
return formatter
}()
}
}

extension Date {
@MainActor
func relativeDescription(now: Date = .now) -> String {
let seconds = abs(now.timeIntervalSince(self))
if seconds < 15 {
return "just now"
return L("just now")
}
return RelativeTimeFormatters.full.localizedString(for: self, relativeTo: now)
let locale = codexBarLocalizedLocale()
return RelativeTimeFormatters.full(locale: locale).localizedString(for: self, relativeTo: now)
}
}
23 changes: 16 additions & 7 deletions Sources/CodexBar/InlineUsageDashboardContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,6 @@ extension UsageMenuCardView.Model {
snapshot: CostUsageTokenSnapshot) -> InlineUsageDashboardModel
{
let historyDays = max(1, min(365, snapshot.historyDays))
let historyLabel = historyDays == 1 ? "Today" : "\(historyDays)d"
let periodLabel = historyDays == 1 ? "today" : "\(historyDays) day"
let points = snapshot.daily.suffix(historyDays).compactMap { entry -> InlineUsageDashboardModel.Point? in
guard let cost = entry.costUSD else { return nil }
Expand All @@ -178,32 +177,42 @@ extension UsageMenuCardView.Model {
let latest = snapshot.daily.max { lhs, rhs in lhs.date < rhs.date }
var details: [String] = []
if let topModel = Self.topCostModel(from: snapshot.daily) {
details.append("Top model: \(Self.shortModelName(topModel))")
details.append("\(L("Top model")): \(Self.shortModelName(topModel))")
}
if provider == .bedrock {
details.append("AWS Cost Explorer billing can lag.")
} else {
} else if provider == .claude {
details.append(UsageFormatter.costEstimateHint(provider: provider))
} else {
details.append(L("cost_estimate_hint"))
}
let providerName = ProviderDefaults.metadata[provider]?.displayName ?? provider.rawValue
return InlineUsageDashboardModel(
accessibilityLabel: "\(providerName) \(periodLabel) cost trend",
valueStyle: .currencyUSD,
kpis: [
.init(
title: provider == .bedrock ? "Latest" : "Today",
title: provider == .bedrock ? L("Latest") : L("Today"),
value: latest?.costUSD.map(UsageFormatter.usdString) ?? "—",
emphasis: true),
.init(
title: "\(historyLabel) cost",
title: historyDays == 1
? L("Today")
: historyDays == 30
? L("30d cost")
: "\(String(format: L("Last %d days"), historyDays)) \(L("Cost"))",
value: snapshot.last30DaysCostUSD.map(UsageFormatter.usdString) ?? "—",
emphasis: false),
.init(
title: "\(historyLabel) tokens",
title: historyDays == 1
? L("Today tokens")
: historyDays == 30
? L("30d tokens")
: String(format: L("%@ tokens"), String(format: L("Last %d days"), historyDays)),
value: snapshot.last30DaysTokens.map(UsageFormatter.tokenCountString) ?? "—",
emphasis: false),
.init(
title: "Latest tokens",
title: L("Latest tokens"),
value: latest?.totalTokens.map(UsageFormatter.tokenCountString) ?? "—",
emphasis: false),
],
Expand Down
26 changes: 26 additions & 0 deletions Sources/CodexBar/Localization.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CodexBarCore
import Foundation

private func appLanguageDefaults() -> UserDefaults {
Expand Down Expand Up @@ -79,6 +80,21 @@ func L(_ key: String, _ arguments: CVarArg...) -> String {
String(format: L(key), arguments: arguments)
}

func codexBarLocalizedLocale() -> Locale {
let language = appLanguageDefaults().string(forKey: "appLanguage") ?? ""
guard !language.isEmpty else { return .current }
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 Align fallback locale with selected localization

When appLanguage is unset, codexBarLocalizedLocale() now returns .current, but localizedBundle() falls back to available app localizations (often English). On systems whose preferred locale is unsupported (for example de-DE), this produces mixed-language output like English labels with German relative-time/date fragments in UsageFormatter and chart/detail text. This is a regression from the prior stable-English formatter behavior and will affect users running in system-language mode without an explicit app-language override.

Useful? React with 👍 / 👎.

switch language.lowercased() {
case "zh-hans":
return Locale(identifier: "zh-Hans")
case "zh-hant":
return Locale(identifier: "zh-Hant")
case "pt-br":
return Locale(identifier: "pt-BR")
default:
return Locale(identifier: language)
}
}

func codexBarLocalizedString(_ key: String, bundle: Bundle, resourceBundle: Bundle) -> String {
let value = bundle.localizedString(forKey: key, value: nil, table: nil)
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
Expand All @@ -95,3 +111,13 @@ func codexBarLocalizedString(_ key: String, bundle: Bundle, resourceBundle: Bund
let fallback = englishBundle.localizedString(forKey: key, value: nil, table: nil)
return fallback.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? key : fallback
}

func configureUsageFormatterLocalizationProvider() {
UsageFormatter.setLocalizationProvider { key in
let resourceBundle = codexBarLocalizationResourceBundle()
return codexBarLocalizedString(key, bundle: localizedBundle(), resourceBundle: resourceBundle)
}
UsageFormatter.setLocaleProvider {
codexBarLocalizedLocale()
}
}
46 changes: 27 additions & 19 deletions Sources/CodexBar/MenuCardView+Costs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ extension UsageMenuCardView.Model {
if let error, !error.isEmpty {
return error.trimmingCharacters(in: .whitespacesAndNewlines)
}
return metadata.creditsHint
return L(metadata.creditsHint)
}

static func tokenUsageSection(
Expand All @@ -31,18 +31,12 @@ extension UsageMenuCardView.Model {

let sessionCost = snapshot.sessionCostUSD.map { UsageFormatter.usdString($0) } ?? "—"
let sessionTokens = snapshot.sessionTokens.map { UsageFormatter.tokenCountString($0) }
let sessionLabel = provider == .bedrock ? Self.bedrockLatestBillingDayLabel(from: snapshot) : L("Today")
let sessionLine: String = {
if provider == .bedrock {
let label = Self.bedrockLatestBillingDayLabel(from: snapshot)
if let sessionTokens {
return "\(label): \(sessionCost) · \(sessionTokens) tokens"
}
return "\(label): \(sessionCost)"
}
if let sessionTokens {
return "Today: \(sessionCost) · \(sessionTokens) tokens"
return String(format: L("%@: %@ · %@ tokens"), sessionLabel, sessionCost, sessionTokens)
}
return "Today: \(sessionCost)"
return "\(sessionLabel): \(sessionCost)"
}()

let monthCost = snapshot.last30DaysCostUSD.map { UsageFormatter.usdString($0) } ?? "—"
Expand All @@ -52,7 +46,7 @@ extension UsageMenuCardView.Model {
let windowLabel = Self.costHistoryWindowLabel(days: snapshot.historyDays)
let monthLine: String = {
if let monthTokens {
return "\(windowLabel): \(monthCost) · \(monthTokens) tokens"
return String(format: L("%@: %@ · %@ tokens"), windowLabel, monthCost, monthTokens)
}
return "\(windowLabel): \(monthCost)"
}()
Expand All @@ -68,27 +62,27 @@ extension UsageMenuCardView.Model {
static func tokenUsageHint(provider: UsageProvider) -> String? {
switch provider {
case .codex:
"Estimated from local Codex logs for the selected account."
L("Estimated from local Codex logs for the selected account.")
case .claude:
UsageFormatter.costEstimateHint(provider: provider)
case .vertexai:
UsageFormatter.costEstimateHint
L("cost_estimate_hint")
case .bedrock:
"Reported by AWS Cost Explorer; daily billing data can lag."
L("AWS Cost Explorer billing can lag.")
default:
nil
}
}

static func costHistoryWindowLabel(days: Int) -> String {
days == 1 ? "Today" : "Last \(days) days"
days == 1 ? L("Today") : String(format: L("Last %d days"), days)
}

private static func bedrockLatestBillingDayLabel(from snapshot: CostUsageTokenSnapshot) -> String {
guard let entry = bedrockLatestBillingDay(from: snapshot.daily),
let displayDate = bedrockDisplayDate(from: entry.date)
else { return "Latest billing day" }
return "Latest billing day (\(displayDate))"
else { return L("Latest billing day") }
return String(format: L("Latest billing day (%@)"), displayDate)
}

private static func bedrockLatestBillingDay(from entries: [CostUsageDailyReport.Entry])
Expand Down Expand Up @@ -155,7 +149,7 @@ extension UsageMenuCardView.Model {

if provider == .openai || provider == .claude, cost.limit <= 0 {
let spend = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode)
let periodLabel = cost.period ?? "Last 30 days"
let periodLabel = Self.localizedPeriodLabel(cost.period ?? "Last 30 days")
return ProviderCostSection(
title: "API spend",
percentUsed: nil,
Expand All @@ -180,7 +174,7 @@ extension UsageMenuCardView.Model {
}

let percentUsed = Self.clamped((cost.used / cost.limit) * 100)
let periodLabel = cost.period ?? "This month"
let periodLabel = Self.localizedPeriodLabel(cost.period ?? "This month")

return ProviderCostSection(
title: title,
Expand All @@ -189,6 +183,20 @@ extension UsageMenuCardView.Model {
percentLine: String(format: "%.0f%% used", min(100, max(0, percentUsed))))
}

private static func localizedPeriodLabel(_ label: String) -> String {
let trimmed = label.trimmingCharacters(in: .whitespacesAndNewlines)
switch trimmed.lowercased() {
case "last 30 days":
return L("Last 30 days")
case "this month":
return L("This month")
case "today":
return L("Today")
default:
return L(trimmed)
}
}

static func clamped(_ value: Double) -> Double {
min(100, max(0, value))
}
Expand Down
Loading