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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 0.29.2 — Unreleased

### Fixed
- Localization: improve Traditional Chinese wording and localize notification copy (#1158). Thanks @jack24254029!
- Localization: improve Simplified Chinese visible menu, dashboard, and usage labels (#1145). Thanks @Yuxin-Qiao!

## 0.29.1 — 2026-05-26
Expand Down
6 changes: 5 additions & 1 deletion Sources/CodexBar/Localization.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import CodexBarCore
import Foundation

enum CodexBarLocalizationOverride {
@TaskLocal static var appLanguage: String?
}

private func appLanguageDefaults() -> UserDefaults {
if Bundle.main.bundleIdentifier != nil {
return .standard
Expand Down Expand Up @@ -37,7 +41,7 @@ func codexBarLocalizationResourceBundle(

private func localizedBundle() -> Bundle {
let resourceBundle = codexBarLocalizationResourceBundle()
let language = appLanguageDefaults().string(forKey: "appLanguage") ?? ""
let language = CodexBarLocalizationOverride.appLanguage ?? appLanguageDefaults().string(forKey: "appLanguage") ?? ""
if !language.isEmpty {
if let bundle = lprojBundle(named: language, in: resourceBundle) {
return bundle
Expand Down
9 changes: 9 additions & 0 deletions Sources/CodexBar/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,13 @@
"quota_warning_session_capitalized" = "Session";
"quota_warning_weekly" = "weekly";
"quota_warning_weekly_capitalized" = "Weekly";
"quota_warning_notification_title" = "%1$@ %2$@ quota low";
"quota_warning_notification_body" = "%1$@ left. Reached your %2$d%% %3$@ warning threshold.";
"quota_warning_notification_body_with_account" = "Account %1$@. %2$@ left. Reached your %3$d%% %4$@ warning threshold.";
"session_depleted_notification_title" = "%@ session depleted";
"session_depleted_notification_body" = "0% left. Will notify when it's available again.";
"session_restored_notification_title" = "%@ session restored";
"session_restored_notification_body" = "Session quota is available again.";
"quota_warning_warn_at" = "Warn at";
"quota_warning_global_threshold_subtitle" = "Remaining percentages for session and weekly windows unless a provider overrides them.";
"quota_warning_sound" = "Play notification sound";
Expand Down Expand Up @@ -455,6 +462,8 @@
"managed_login_already_running" = "A managed Codex login is already running. Wait for it to finish before adding or re-authenticating another account.";
"managed_login_failed" = "Managed Codex login did not complete. Verify that `codex --version` works in Terminal. If macOS blocked or moved `codex` to Trash, remove stale duplicate installs, run `npm install -g --include=optional @openai/codex@latest`, then try again.";
"managed_login_missing_email" = "Codex login completed, but no account email was available. Try again after confirming the account is fully signed in.";
"login_success_notification_title" = "%@ login successful";
"login_success_notification_body" = "You can return to the app; authentication finished.";
"workspace_selection_cancelled" = "CodexBar found multiple workspaces, but no workspace was selected.";
"unsafe_managed_home" = "CodexBar refused to modify an unexpected managed home path: %@";
"menu_bar_metric_title" = "Menu bar metric";
Expand Down
519 changes: 264 additions & 255 deletions Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings

Large diffs are not rendered by default.

61 changes: 47 additions & 14 deletions Sources/CodexBar/SessionQuotaNotifications.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,24 @@ enum SessionQuotaNotificationLogic {
if wasDepleted, !isDepleted { return .restored }
return .none
}

static func notificationCopy(
transition: SessionQuotaTransition,
providerName: String) -> (title: String, body: String)
{
switch transition {
case .none:
("", "")
case .depleted:
(
L("session_depleted_notification_title", providerName),
L("session_depleted_notification_body"))
case .restored:
(
L("session_restored_notification_title", providerName),
L("session_restored_notification_body"))
}
}
}

enum QuotaWarningNotificationLogic {
Expand All @@ -57,13 +75,24 @@ enum QuotaWarningNotificationLogic {
currentRemaining: Double,
accountDisplayName: String? = nil) -> (title: String, body: String)
{
let windowLabel = window.displayName
let windowLabel = window.localizedNotificationDisplayName
let remainingText = Self.percentText(currentRemaining)
let accountPrefix = accountDisplayName
.map { "Account \($0). " } ?? ""
return (
"\(providerName) \(windowLabel) quota low",
"\(accountPrefix)\(remainingText) left. Reached your \(threshold)% \(windowLabel) warning threshold.")
let title = L("quota_warning_notification_title", providerName, windowLabel)
Comment on lines +78 to +80
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 Avoid mixed-language fallback in quota warning copy

windowLabel is localized separately before formatting the notification strings, but quota_warning_notification_* keys were only added in en.lproj and zh-Hant.lproj. In locales like ca, es, pt-BR, and zh-Hans, L("quota_warning_notification_title", ...) falls back to the English template while still injecting a localized window label, producing mixed text (for example, "Codex sesión quota low"). Please derive the label from the same fallback language as the template (or add these keys to all shipped locales) so notifications stay linguistically consistent.

Useful? React with 👍 / 👎.

let body = if let accountDisplayName {
L(
"quota_warning_notification_body_with_account",
accountDisplayName,
remainingText,
threshold,
windowLabel)
} else {
L(
"quota_warning_notification_body",
remainingText,
threshold,
windowLabel)
}
return (title, body)
}

static func crossedThreshold(
Expand Down Expand Up @@ -116,14 +145,9 @@ final class SessionQuotaNotifier: SessionQuotaNotifying {

let providerName = ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName

let (title, body) = switch transition {
case .none:
("", "")
case .depleted:
("\(providerName) session depleted", "0% left. Will notify when it's available again.")
case .restored:
("\(providerName) session restored", "Session quota is available again.")
}
let (title, body) = SessionQuotaNotificationLogic.notificationCopy(
transition: transition,
providerName: providerName)

let providerText = provider.rawValue
let transitionText = String(describing: transition)
Expand Down Expand Up @@ -156,3 +180,12 @@ final class SessionQuotaNotifier: SessionQuotaNotifying {
AppNotifications.shared.post(idPrefix: idPrefix, title: copy.title, body: copy.body, soundEnabled: false)
}
}

extension QuotaWarningWindow {
fileprivate var localizedNotificationDisplayName: String {
switch self {
case .session: L("quota_warning_session")
case .weekly: L("quota_warning_weekly")
}
}
}
11 changes: 9 additions & 2 deletions Sources/CodexBar/StatusItemController+Actions.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import AppKit
import CodexBarCore

enum LoginNotificationLogic {
static func notificationCopy(providerName: String) -> (title: String, body: String) {
(
L("login_success_notification_title", providerName),
L("login_success_notification_body"))
}
}

extension StatusItemController: StatusItemMenuPersistentActionDelegate {
// MARK: - Actions reachable from menus

Expand Down Expand Up @@ -528,8 +536,7 @@ extension StatusItemController: StatusItemMenuPersistentActionDelegate {

func postLoginNotification(for provider: UsageProvider) {
let name = ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName
let title = "\(name) login successful"
let body = "You can return to the app; authentication finished."
let (title, body) = LoginNotificationLogic.notificationCopy(providerName: name)
AppNotifications.shared.post(idPrefix: "login-\(provider.rawValue)", title: title, body: body)
}

Expand Down
15 changes: 8 additions & 7 deletions Tests/CodexBarTests/CodexManagedOpenAIWebRefreshTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,14 @@ struct CodexManagedOpenAIWebRefreshTests {
await completion.markCompleted()
}

let completed = await completion.waitUntilCompleted(timeout: .seconds(30))
let didStart = await blocker.waitUntilStartedWithin(count: 1, timeout: .seconds(60))
#expect(didStart == true)
if !didStart {
refreshTask.cancel()
return
}

let completed = await completion.waitUntilCompleted(timeout: .seconds(2))
#expect(completed == true)
if !completed {
refreshTask.cancel()
Expand All @@ -73,12 +80,6 @@ struct CodexManagedOpenAIWebRefreshTests {
await refreshTask.value

let backgroundTask = try #require(store.openAIDashboardBackgroundRefreshTask)
let didStart = await blocker.waitUntilStartedWithin(count: 1, timeout: .seconds(30))
#expect(didStart == true)
if !didStart {
backgroundTask.cancel()
return
}
#expect(await blocker.startedCount() == 1)

await blocker.resumeNext(with: .success(OpenAIDashboardSnapshot(
Expand Down
19 changes: 19 additions & 0 deletions Tests/CodexBarTests/LoginNotificationLogicTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Testing
@testable import CodexBar

@Suite(.serialized)
struct LoginNotificationLogicTests {
@Test
func `login success notification copy follows Traditional Chinese app language`() {
Self.withAppLanguage("zh-Hant") {
let copy = LoginNotificationLogic.notificationCopy(providerName: "Codex")

#expect(copy.title == "Codex 登入成功")
#expect(copy.body == "你可以回到 App;認證已完成。")
}
}

private static func withAppLanguage(_ language: String, perform body: () -> Void) {
CodexBarLocalizationOverride.$appLanguage.withValue(language, operation: body)
}
}
8 changes: 5 additions & 3 deletions Tests/CodexBarTests/MiMoProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -378,10 +378,12 @@ struct MiMoProviderTests {
@Test
@MainActor
func `provider detail plan row formats mimo as balance`() {
let row = ProviderDetailView<Text>.planRow(provider: .mimo, planText: "Balance: $25.51")
CodexBarLocalizationOverride.$appLanguage.withValue("en") {
let row = ProviderDetailView<Text>.planRow(provider: .mimo, planText: "Balance: $25.51")

#expect(row?.label == "Balance")
#expect(row?.value == "$25.51")
#expect(row?.label == "Balance")
#expect(row?.value == "$25.51")
}
}

@Test(arguments: [UsageProvider.openrouter, .mimo])
Expand Down
Loading