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
17 changes: 16 additions & 1 deletion Sources/CodexBar/Localization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,22 @@ func L(_ key: String, language: String) -> String {
}

func codexBarLocalizedLocale() -> Locale {
let language = resolvedAppLanguage()
codexBarLocale(forLanguage: resolvedAppLanguage())
}

/// Returns the locale of the resource bundle currently selected by `L`.
///
/// This can differ from `Locale.current` when the app falls back to a supported language. Plural
/// formatting must use this locale so it follows the same language as the resolved strings.
func codexBarLocalizedResourceLocale() -> Locale {
let bundleURL = localizedBundle().bundleURL
guard bundleURL.pathExtension == "lproj" else {
return codexBarLocalizedLocale()
}
return codexBarLocale(forLanguage: bundleURL.deletingPathExtension().lastPathComponent)
}

private func codexBarLocale(forLanguage language: String) -> Locale {
guard !language.isEmpty else { return .current }
let normalized = language.lowercased()
if normalized == "ar" || normalized.hasPrefix("ar-") {
Expand Down
16 changes: 9 additions & 7 deletions Sources/CodexBar/UsagePaceText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,21 @@ enum UsagePaceText {

static func sessionEquivalentDetail(forecast: SessionEquivalentForecast) -> SessionEquivalentDetail {
let displayedEstimate = Self.boundedFullWindowCount(forecast.estimatedWindowsToExhaustWeekly)
let numberText = String.localizedStringWithFormat(
L("≈%d full 5h windows of weekly left · %d windows until reset"),
displayedEstimate,
forecast.windowsUntilReset)
let formattingLocale = codexBarLocalizedResourceLocale()
let numberText = String(
format: L("≈%d full 5h windows of weekly left · %d windows until reset"),
locale: formattingLocale,
arguments: [displayedEstimate, forecast.windowsUntilReset])
Comment thread
Yuxin-Qiao marked this conversation as resolved.
let verdictText: String
if forecast.estimatedWindowsToExhaustWeekly >= forecast.availableWindowsUntilReset {
verdictText = L("Weekly cannot run out before reset at this pace")
} else {
let windowsEarly = Self.boundedWindowCount(
forecast.availableWindowsUntilReset - forecast.estimatedWindowsToExhaustWeekly)
verdictText = String.localizedStringWithFormat(
L("Weekly can run out ≈%d windows early"),
max(1, windowsEarly))
verdictText = String(
format: L("Weekly can run out ≈%d windows early"),
locale: formattingLocale,
arguments: [max(1, windowsEarly)])
}
return SessionEquivalentDetail(
verdictText: verdictText,
Expand Down
25 changes: 25 additions & 0 deletions Tests/CodexBarTests/LocalizationBundleCacheTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,31 @@ struct LocalizationBundleCacheTests {
#expect(bundle.bundleURL.lastPathComponent == "en.lproj")
}

@Test
func `format locale follows the resolved resource bundle`() {
let english = CodexBarLocalizationOverride.$appLanguage.withValue("en") {
codexBarLocalizedResourceLocale()
}
#expect(english.language.languageCode?.identifier == "en")

let fallback = CodexBarLocalizationOverride.$appLanguage.withValue("zz-unknown") {
codexBarLocalizedResourceLocale()
}
#expect(fallback.language.languageCode?.identifier == "en")
}

@Test
func `resource locale expands English stringsdict singular forms`() {
let rendered = CodexBarLocalizationOverride.$appLanguage.withValue("en") {
String(
format: L("≈%d full 5h windows of weekly left · %d windows until reset"),
locale: codexBarLocalizedResourceLocale(),
arguments: [1, 1])
}

#expect(rendered == "≈1 full 5h window of weekly left · 1 window until reset")
}

@Test
func `resolution survives an explicit cache reset`() {
let first = CodexBarLocalizationOverride.$appLanguage.withValue("uk") {
Expand Down
59 changes: 37 additions & 22 deletions Tests/CodexBarTests/SpendDashboardControllerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Testing
@testable import CodexBar

@MainActor
@Suite(.serialized)
struct SpendDashboardControllerTests {
@Test
func `empty codex history loads as successful inactive source`() async {
Expand Down Expand Up @@ -288,7 +289,7 @@ struct SpendDashboardControllerTests {
let snapshot = Self.input(id: "claude", provider: .claude, cost: 3).snapshot
store._setTokenSnapshotForTesting(snapshot, provider: .claude)
store._test_tokenUsageRefreshOverride = { _, _ in }
let controller = SpendDashboardController(requestBuilder: { mode in
let controller = SpendDashboardController(userDefaults: settings.userDefaults, requestBuilder: { mode in
await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode)
})

Expand Down Expand Up @@ -408,9 +409,11 @@ struct SpendDashboardControllerTests {
environmentBase: [:])
store._setTokenSnapshotForTesting(Self.input(provider: .claude, cost: 3).snapshot, provider: .claude)
store._test_tokenUsageRefreshOverride = { _, _ in }
let controller = SpendDashboardController(requestBuilder: { mode in
await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode)
})
let controller = SpendDashboardController(
userDefaults: settings.userDefaults,
requestBuilder: { mode in
await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode)
})

let firstConfiguration = SpendDashboardSource.configuration(settings: settings, store: store)
controller.update(configuration: firstConfiguration)
Expand All @@ -431,9 +434,11 @@ struct SpendDashboardControllerTests {
#expect(controller.failedSourceCount == 1)
#expect(store.tokenSnapshot(for: .claude)?.last30DaysCostUSD == 3)

let reopenedController = SpendDashboardController(requestBuilder: { mode in
await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode)
})
let reopenedController = SpendDashboardController(
userDefaults: settings.userDefaults,
requestBuilder: { mode in
await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode)
})
reopenedController.update(configuration: replacementConfiguration)
await Self.waitUntil { !reopenedController.isRefreshing }
#expect(reopenedController.model.groups.isEmpty)
Expand Down Expand Up @@ -487,9 +492,11 @@ struct SpendDashboardControllerTests {

store._setTokenSnapshotForTesting(Self.input(provider: .mistral, cost: 3).snapshot, provider: .mistral)
store._test_providerRefreshOverride = { _ in }
let controller = SpendDashboardController(requestBuilder: { mode in
await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode)
})
let controller = SpendDashboardController(
userDefaults: settings.userDefaults,
requestBuilder: { mode in
await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode)
})
controller.update(configuration: selectedBackupConfiguration)
await Self.waitUntil { !controller.isRefreshing }
#expect(controller.model.groups.first?.totalCost == 3)
Expand Down Expand Up @@ -527,9 +534,11 @@ struct SpendDashboardControllerTests {
environmentBase: [:])
store._setTokenSnapshotForTesting(Self.input(provider: .claude, cost: 4).snapshot, provider: .claude)
store._test_tokenUsageRefreshOverride = { _, _ in }
let controller = SpendDashboardController(requestBuilder: { mode in
await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode)
})
let controller = SpendDashboardController(
userDefaults: settings.userDefaults,
requestBuilder: { mode in
await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode)
})
controller.update(configuration: SpendDashboardSource.configuration(settings: settings, store: store))
await Self.waitUntil { !controller.isRefreshing }
#expect(controller.model.groups.first?.totalCost == 4)
Expand Down Expand Up @@ -557,9 +566,11 @@ struct SpendDashboardControllerTests {
environmentBase: [:])
store._setTokenSnapshotForTesting(Self.input(provider: .claude, cost: 5).snapshot, provider: .claude)
store._test_tokenUsageRefreshOverride = { _, _ in }
let controller = SpendDashboardController(requestBuilder: { mode in
await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode)
})
let controller = SpendDashboardController(
userDefaults: settings.userDefaults,
requestBuilder: { mode in
await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode)
})
let firstConfiguration = SpendDashboardSource.configuration(settings: settings, store: store)
controller.update(configuration: firstConfiguration)
await Self.waitUntil { !controller.isRefreshing }
Expand Down Expand Up @@ -626,9 +637,11 @@ struct SpendDashboardControllerTests {
environmentBase: [:])
store._setTokenSnapshotForTesting(Self.input(provider: .claude, cost: 5).snapshot, provider: .claude)
store._test_tokenUsageRefreshOverride = { _, _ in }
let controller = SpendDashboardController(requestBuilder: { mode in
await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode)
})
let controller = SpendDashboardController(
userDefaults: settings.userDefaults,
requestBuilder: { mode in
await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode)
})
controller.update(configuration: SpendDashboardSource.configuration(settings: settings, store: store))
await Self.waitUntil { !controller.isRefreshing }
#expect(controller.model.groups.first?.totalCost == 5)
Expand All @@ -644,9 +657,11 @@ struct SpendDashboardControllerTests {
#expect(controller.model.groups.isEmpty)
#expect(controller.failedSourceCount == 1)

let reopenedController = SpendDashboardController(requestBuilder: { mode in
await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode)
})
let reopenedController = SpendDashboardController(
userDefaults: settings.userDefaults,
requestBuilder: { mode in
await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode)
})
reopenedController.update(configuration: reenabledConfiguration)
await Self.waitUntil { !reopenedController.isRefreshing }
#expect(reopenedController.model.groups.isEmpty)
Expand Down
24 changes: 15 additions & 9 deletions Tests/CodexBarTests/SpendDashboardTokenProvenanceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,11 @@ struct SpendDashboardTokenProvenanceTests {
store.activateCachedTokenAccountSnapshot(provider: .mistral, accountID: account.id)
#expect(store.tokenSnapshotPublicationRevision(for: .mistral) == baselineRevision)
store._test_providerRefreshOverride = { _ in }
let controller = SpendDashboardController(requestBuilder: { mode in
await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode)
})
let controller = SpendDashboardController(
userDefaults: settings.userDefaults,
requestBuilder: { mode in
await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode)
})
controller.update(configuration: SpendDashboardSource.configuration(settings: settings, store: store))
await Self.waitUntil { !controller.isRefreshing }
#expect(controller.model.groups.first?.totalCost == 3)
Expand All @@ -148,9 +150,11 @@ struct SpendDashboardTokenProvenanceTests {
return loadCount == 1 ? Self.tokenSnapshot(cost: 4) : Self.emptyTokenSnapshot()
}
await store.refreshTokenUsageNow(for: .bedrock, force: true)
let controller = SpendDashboardController(requestBuilder: { mode in
await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode)
})
let controller = SpendDashboardController(
userDefaults: settings.userDefaults,
requestBuilder: { mode in
await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode)
})
controller.update(configuration: SpendDashboardSource.configuration(settings: settings, store: store))
await Self.waitUntil { !controller.isRefreshing }
#expect(controller.model.groups.first?.totalCost == 4)
Expand All @@ -177,9 +181,11 @@ struct SpendDashboardTokenProvenanceTests {
}
await store.refreshTokenUsageNow(for: .bedrock, force: true)
let publicationRevision = store.tokenSnapshotPublicationRevision(for: .bedrock)
let controller = SpendDashboardController(requestBuilder: { mode in
await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode)
})
let controller = SpendDashboardController(
userDefaults: settings.userDefaults,
requestBuilder: { mode in
await SpendDashboardSource.makeRequest(settings: settings, store: store, mode: mode)
})

controller.update(configuration: SpendDashboardSource.configuration(settings: settings, store: store))
await Self.waitUntil { !controller.isRefreshing }
Expand Down
Loading