From a06c740e99237cae1d8b3374bf2b8d3b02865046 Mon Sep 17 00:00:00 2001 From: Z <1492162933@qq.com> Date: Thu, 30 Apr 2026 21:49:54 +0800 Subject: [PATCH 1/2] Add Chinese (Simplified) localization - Add Localization.swift with L() helper for string lookup - Add en.lproj/Localizable.strings (base English translations) - Add zh-Hans.lproj/Localizable.strings (Simplified Chinese translations) - Replace hardcoded strings with L() calls in 25 source files - Add defaultLocalization to Package.swift - Add new localized setting keys in SettingsStore+Defaults.swift This enables full Chinese UI for users with zh-Hans locale preference. --- Package.swift | 1 + Sources/CodexBar/CodexbarApp.swift | 10 + .../CodexBar/KeychainPromptCoordinator.swift | 2 +- Sources/CodexBar/Localization.swift | 32 + .../CodexBar/ManagedCodexAccountService.swift | 8 +- Sources/CodexBar/MenuBarDisplayMode.swift | 12 +- Sources/CodexBar/PreferencesAboutPane.swift | 24 +- .../CodexBar/PreferencesAdvancedPane.swift | 41 +- Sources/CodexBar/PreferencesDebugPane.swift | 110 ++-- Sources/CodexBar/PreferencesDisplayPane.swift | 54 +- Sources/CodexBar/PreferencesGeneralPane.swift | 85 ++- .../PreferencesProviderDetailView.swift | 2 +- .../PreferencesProviderErrorView.swift | 2 +- .../CodexBar/PreferencesProvidersPane.swift | 51 +- Sources/CodexBar/PreferencesView.swift | 24 +- .../Claude/ClaudeProviderImplementation.swift | 4 +- .../Codex/CodexProviderImplementation.swift | 4 +- .../Providers/Copilot/CopilotLoginFlow.swift | 21 +- .../Cursor/CursorProviderImplementation.swift | 4 +- .../JetBrains/JetBrainsLoginFlow.swift | 7 +- .../VertexAI/VertexAILoginFlow.swift | 17 +- .../Resources/en.lproj/Localizable.strings | 592 +++++++++++++++++ .../zh-Hans.lproj/Localizable.strings | 602 ++++++++++++++++++ Sources/CodexBar/SettingsStore+Defaults.swift | 15 + Sources/CodexBar/SettingsStore.swift | 28 +- Sources/CodexBar/SettingsStoreState.swift | 1 + .../CodexBar/StatusItemController+Menu.swift | 6 +- Sources/CodexBar/UsageStoreSupport.swift | 12 +- 28 files changed, 1521 insertions(+), 250 deletions(-) create mode 100644 Sources/CodexBar/Localization.swift create mode 100644 Sources/CodexBar/Resources/en.lproj/Localizable.strings create mode 100644 Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings diff --git a/Package.swift b/Package.swift index e8e769f33..6dea40135 100644 --- a/Package.swift +++ b/Package.swift @@ -13,6 +13,7 @@ let sweetCookieKitDependency: Package.Dependency = let package = Package( name: "CodexBar", + defaultLocalization: "en", platforms: [ .macOS(.v14), ], diff --git a/Sources/CodexBar/CodexbarApp.swift b/Sources/CodexBar/CodexbarApp.swift index ccaad6969..686231822 100644 --- a/Sources/CodexBar/CodexbarApp.swift +++ b/Sources/CodexBar/CodexbarApp.swift @@ -43,6 +43,7 @@ struct CodexBarApp: App { let preferencesSelection = PreferencesSelection() let settings = SettingsStore() + Self.applyLanguagePreference(from: settings) let managedCodexAccountCoordinator = ManagedCodexAccountCoordinator() managedCodexAccountCoordinator.onManagedAccountsDidChange = { _ = settings.persistResolvedCodexActiveSourceCorrectionIfNeeded() @@ -100,6 +101,15 @@ struct CodexBarApp: App { NSApp.activate(ignoringOtherApps: true) _ = NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil) } + + private static func applyLanguagePreference(from settings: SettingsStore) { + let language = settings.appLanguage + if language.isEmpty { + UserDefaults.standard.removeObject(forKey: "AppleLanguages") + } else { + UserDefaults.standard.set([language], forKey: "AppleLanguages") + } + } } // MARK: - Updater abstraction diff --git a/Sources/CodexBar/KeychainPromptCoordinator.swift b/Sources/CodexBar/KeychainPromptCoordinator.swift index a6add39ab..abbeb0caa 100644 --- a/Sources/CodexBar/KeychainPromptCoordinator.swift +++ b/Sources/CodexBar/KeychainPromptCoordinator.swift @@ -134,7 +134,7 @@ enum KeychainPromptCoordinator { let alert = NSAlert() alert.messageText = title alert.informativeText = message - alert.addButton(withTitle: "OK") + alert.addButton(withTitle: L("OK")) _ = alert.runModal() } } diff --git a/Sources/CodexBar/Localization.swift b/Sources/CodexBar/Localization.swift new file mode 100644 index 000000000..13e3e7171 --- /dev/null +++ b/Sources/CodexBar/Localization.swift @@ -0,0 +1,32 @@ +import Foundation + +private func appLanguageDefaults() -> UserDefaults { + if Bundle.main.bundleIdentifier != nil { + return .standard + } + // Fallback for running outside a .app bundle (swift run / debug builds) + return UserDefaults(suiteName: "CodexBar") ?? .standard +} + +private func localizedBundle() -> Bundle { + let language = appLanguageDefaults().string(forKey: "appLanguage") ?? "" + let target = language.isEmpty ? "en" : language.lowercased() + if let path = Bundle.module.path(forResource: target, ofType: "lproj"), + let bundle = Bundle(path: path) { + return bundle + } + // Fallback to en.lproj to avoid following system language + if let path = Bundle.module.path(forResource: "en", ofType: "lproj"), + let bundle = Bundle(path: path) { + return bundle + } + return Bundle.module +} + +func L(_ key: String) -> String { + localizedBundle().localizedString(forKey: key, value: nil, table: nil) +} + +func L(_ key: String, _ arguments: CVarArg...) -> String { + String(format: localizedBundle().localizedString(forKey: key, value: nil, table: nil), arguments: arguments) +} diff --git a/Sources/CodexBar/ManagedCodexAccountService.swift b/Sources/CodexBar/ManagedCodexAccountService.swift index 11b0c2b78..a3cdd9aea 100644 --- a/Sources/CodexBar/ManagedCodexAccountService.swift +++ b/Sources/CodexBar/ManagedCodexAccountService.swift @@ -161,12 +161,12 @@ struct CodexWorkspaceAlertSelector: ManagedCodexWorkspaceSelecting { } let alert = NSAlert() - alert.messageText = "Choose Codex workspace" - alert.informativeText = "CodexBar found multiple workspaces for \(email). Choose the one to add." + alert.messageText = L("Choose Codex workspace") + alert.informativeText = String(format: L("multiple_workspaces_found"), email) alert.alertStyle = .informational alert.accessoryView = popup - alert.addButton(withTitle: "Add Workspace") - alert.addButton(withTitle: "Cancel") + alert.addButton(withTitle: L("Add Workspace")) + alert.addButton(withTitle: L("Cancel")) guard alert.runModal() == .alertFirstButtonReturn else { return nil diff --git a/Sources/CodexBar/MenuBarDisplayMode.swift b/Sources/CodexBar/MenuBarDisplayMode.swift index 8daa30ccf..484d20969 100644 --- a/Sources/CodexBar/MenuBarDisplayMode.swift +++ b/Sources/CodexBar/MenuBarDisplayMode.swift @@ -12,17 +12,17 @@ enum MenuBarDisplayMode: String, CaseIterable, Identifiable { var label: String { switch self { - case .percent: "Percent" - case .pace: "Pace" - case .both: "Both" + case .percent: L("display_mode_percent") + case .pace: L("display_mode_pace") + case .both: L("display_mode_both") } } var description: String { switch self { - case .percent: "Show remaining/used percentage (e.g. 45%)" - case .pace: "Show pace indicator (e.g. +5%)" - case .both: "Show both percentage and pace (e.g. 45% · +5%)" + case .percent: L("display_mode_percent_desc") + case .pace: L("display_mode_pace_desc") + case .both: L("display_mode_both_desc") } } } diff --git a/Sources/CodexBar/PreferencesAboutPane.swift b/Sources/CodexBar/PreferencesAboutPane.swift index 94e478e59..31c3bbe43 100644 --- a/Sources/CodexBar/PreferencesAboutPane.swift +++ b/Sources/CodexBar/PreferencesAboutPane.swift @@ -51,14 +51,14 @@ struct AboutPane: View { VStack(spacing: 2) { Text("CodexBar") .font(.title3).bold() - Text("Version \(self.versionString)") + Text(String(format: L("version_format"), self.versionString)) .foregroundStyle(.secondary) if let buildTimestamp { - Text("Built \(buildTimestamp)") + Text(String(format: L("built_format"), buildTimestamp)) .font(.footnote) .foregroundStyle(.secondary) } - Text("May your tokens never run out—keep agent limits in view.") + Text(L("about_tagline")) .font(.footnote) .foregroundStyle(.secondary) } @@ -66,11 +66,11 @@ struct AboutPane: View { VStack(alignment: .center, spacing: 10) { AboutLinkRow( icon: "chevron.left.slash.chevron.right", - title: "GitHub", + title: L("link_github"), url: "https://github.com/steipete/CodexBar") - AboutLinkRow(icon: "globe", title: "Website", url: "https://steipete.me") - AboutLinkRow(icon: "bird", title: "Twitter", url: "https://twitter.com/steipete") - AboutLinkRow(icon: "envelope", title: "Email", url: "mailto:peter@steipete.me") + AboutLinkRow(icon: "globe", title: L("link_website"), url: "https://steipete.me") + AboutLinkRow(icon: "bird", title: L("link_twitter"), url: "https://twitter.com/steipete") + AboutLinkRow(icon: "envelope", title: L("link_email"), url: "mailto:peter@steipete.me") } .padding(.top, 8) .frame(maxWidth: .infinity) @@ -80,12 +80,12 @@ struct AboutPane: View { if self.updater.isAvailable { VStack(spacing: 10) { - Toggle("Check for updates automatically", isOn: self.$autoUpdateEnabled) + Toggle(L("check_updates_auto"), isOn: self.$autoUpdateEnabled) .toggleStyle(.checkbox) .frame(maxWidth: .infinity, alignment: .center) VStack(spacing: 6) { HStack(spacing: 12) { - Text("Update Channel") + Text(L("update_channel")) Spacer() Picker("", selection: self.updateChannelBinding) { ForEach(UpdateChannel.allCases) { channel in @@ -102,14 +102,14 @@ struct AboutPane: View { .multilineTextAlignment(.center) .frame(maxWidth: 280) } - Button("Check for Updates…") { self.updater.checkForUpdates(nil) } + Button(L("check_for_updates")) { self.updater.checkForUpdates(nil) } } } else { - Text(self.updater.unavailableReason ?? "Updates unavailable in this build.") + Text(self.updater.unavailableReason ?? L("updates_unavailable")) .foregroundStyle(.secondary) } - Text("© 2026 Peter Steinberger. MIT License.") + Text(L("copyright")) .font(.footnote) .foregroundStyle(.secondary) .padding(.top, 4) diff --git a/Sources/CodexBar/PreferencesAdvancedPane.swift b/Sources/CodexBar/PreferencesAdvancedPane.swift index 9b901c9d8..87556768d 100644 --- a/Sources/CodexBar/PreferencesAdvancedPane.swift +++ b/Sources/CodexBar/PreferencesAdvancedPane.swift @@ -11,17 +11,17 @@ struct AdvancedPane: View { ScrollView(.vertical, showsIndicators: true) { VStack(alignment: .leading, spacing: 16) { SettingsSection(contentSpacing: 8) { - Text("Keyboard shortcut") + Text(L("section_keyboard_shortcut")) .font(.caption) .foregroundStyle(.secondary) .textCase(.uppercase) HStack(alignment: .center, spacing: 12) { - Text("Open menu") + Text(L("open_menu_shortcut_title")) .font(.body) Spacer() KeyboardShortcuts.Recorder(for: .openMenu) } - Text("Trigger the menu bar menu from anywhere.") + Text(L("open_menu_shortcut_subtitle")) .font(.footnote) .foregroundStyle(.tertiary) } @@ -36,7 +36,7 @@ struct AdvancedPane: View { if self.isInstallingCLI { ProgressView().controlSize(.small) } else { - Text("Install CLI") + Text(L("install_cli")) } } .disabled(self.isInstallingCLI) @@ -48,7 +48,7 @@ struct AdvancedPane: View { .lineLimit(2) } } - Text("Symlink CodexBarCLI to /usr/local/bin and /opt/homebrew/bin as codexbar.") + Text(L("install_cli_subtitle")) .font(.footnote) .foregroundStyle(.tertiary) } @@ -57,16 +57,16 @@ struct AdvancedPane: View { SettingsSection(contentSpacing: 10) { PreferenceToggleRow( - title: "Show Debug Settings", - subtitle: "Expose troubleshooting tools in the Debug tab.", + title: L("show_debug_settings_title"), + subtitle: L("show_debug_settings_subtitle"), binding: self.$settings.debugMenuEnabled) PreferenceToggleRow( - title: "Surprise me", - subtitle: "Check if you like your agents having some fun up there.", + title: L("surprise_me_title"), + subtitle: L("surprise_me_subtitle"), binding: self.$settings.randomBlinkEnabled) PreferenceToggleRow( - title: "Weekly limit confetti", - subtitle: "Play full-screen confetti when weekly usage resets.", + title: L("weekly_limit_confetti_title"), + subtitle: L("weekly_limit_confetti_subtitle"), binding: self.$settings.confettiOnWeeklyLimitResetsEnabled) } @@ -74,22 +74,19 @@ struct AdvancedPane: View { SettingsSection(contentSpacing: 10) { PreferenceToggleRow( - title: "Hide personal information", - subtitle: "Obscure email addresses in the menu bar and menu UI.", + title: L("hide_personal_info_title"), + subtitle: L("hide_personal_info_subtitle"), binding: self.$settings.hidePersonalInfo) } Divider() SettingsSection( - title: "Keychain access", - caption: """ - Disable all Keychain reads and writes. Browser cookie import is unavailable; paste Cookie \ - headers manually in Providers. - """) { + title: L("section_keychain_access"), + caption: L("keychain_access_caption")) { PreferenceToggleRow( - title: "Disable Keychain access", - subtitle: "Prevents any Keychain access while enabled.", + title: L("disable_keychain_access_title"), + subtitle: L("disable_keychain_access_subtitle"), binding: self.$settings.debugDisableKeychainAccess) } } @@ -109,7 +106,7 @@ extension AdvancedPane { let helperURL = Bundle.main.bundleURL.appendingPathComponent("Contents/Helpers/CodexBarCLI") let fm = FileManager.default guard fm.fileExists(atPath: helperURL.path) else { - self.cliStatus = "CodexBarCLI not found in app bundle." + self.cliStatus = L("cli_not_found") return } @@ -145,7 +142,7 @@ extension AdvancedPane { } self.cliStatus = results.isEmpty - ? "No writable bin dirs found." + ? L("no_writable_bin_dirs") : results.joined(separator: " · ") } diff --git a/Sources/CodexBar/PreferencesDebugPane.swift b/Sources/CodexBar/PreferencesDebugPane.swift index a86a55434..7a239f844 100644 --- a/Sources/CodexBar/PreferencesDebugPane.swift +++ b/Sources/CodexBar/PreferencesDebugPane.swift @@ -26,10 +26,10 @@ struct DebugPane: View { var body: some View { ScrollView(.vertical, showsIndicators: true) { VStack(alignment: .leading, spacing: 20) { - SettingsSection(title: "Logging") { + SettingsSection(title: L("section_logging")) { PreferenceToggleRow( - title: "Enable file logging", - subtitle: "Write logs to \(self.fileLogPath) for debugging.", + title: L("enable_file_logging"), + subtitle: String(format: L("enable_file_logging_subtitle"), self.fileLogPath), binding: self.$debugFileLoggingEnabled) .onChange(of: self.debugFileLoggingEnabled) { _, newValue in if self.settings.debugFileLoggingEnabled != newValue { @@ -39,9 +39,9 @@ struct DebugPane: View { HStack(alignment: .center, spacing: 12) { VStack(alignment: .leading, spacing: 4) { - Text("Verbosity") + Text(L("verbosity_title")) .font(.body) - Text("Controls how much detail is logged.") + Text(L("verbosity_subtitle")) .font(.footnote) .foregroundStyle(.tertiary) } @@ -59,31 +59,31 @@ struct DebugPane: View { Button { NSWorkspace.shared.open(CodexBarLog.fileLogURL) } label: { - Label("Open log file", systemImage: "doc.text.magnifyingglass") + Label(L("open_log_file"), systemImage: "doc.text.magnifyingglass") } .controlSize(.small) } SettingsSection { PreferenceToggleRow( - title: "Force animation on next refresh", - subtitle: "Temporarily shows the loading animation after the next refresh.", + title: L("force_animation_next_refresh"), + subtitle: L("force_animation_next_refresh_subtitle"), binding: self.$store.debugForceAnimation) } SettingsSection( - title: "Loading animations", - caption: "Pick a pattern and replay it in the menu bar. \"Random\" keeps the existing behavior.") + title: L("section_loading_animations"), + caption: L("loading_animations_caption")) { Picker("Animation pattern", selection: self.animationPatternBinding) { - Text("Random (default)").tag(nil as LoadingPattern?) + Text(L("animation_random_default")).tag(nil as LoadingPattern?) ForEach(LoadingPattern.allCases) { pattern in Text(pattern.displayName).tag(Optional(pattern)) } } .pickerStyle(.radioGroup) - Button("Replay selected animation") { + Button(L("replay_selected_animation")) { self.replaySelectedAnimation() } .keyboardShortcut(.defaultAction) @@ -91,14 +91,14 @@ struct DebugPane: View { Button { NotificationCenter.default.post(name: .codexbarDebugBlinkNow, object: nil) } label: { - Label("Blink now", systemImage: "eyes") + Label(L("blink_now"), systemImage: "eyes") } .controlSize(.small) } SettingsSection( - title: "Probe logs", - caption: "Fetch the latest probe output for debugging; Copy keeps the full text.") + title: L("section_probe_logs"), + caption: L("probe_logs_caption")) { Picker("Provider", selection: self.$currentLogProvider) { Text("Codex").tag(UsageProvider.codex) @@ -113,23 +113,23 @@ struct DebugPane: View { HStack(spacing: 12) { Button { self.loadLog(self.currentLogProvider) } label: { - Label("Fetch log", systemImage: "arrow.clockwise") + Label(L("fetch_log"), systemImage: "arrow.clockwise") } .disabled(self.isLoadingLog) Button { self.copyToPasteboard(self.logText) } label: { - Label("Copy", systemImage: "doc.on.doc") + Label(L("copy"), systemImage: "doc.on.doc") } .disabled(self.logText.isEmpty) Button { self.saveLog(self.currentLogProvider) } label: { - Label("Save to file", systemImage: "externaldrive.badge.plus") + Label(L("save_to_file"), systemImage: "externaldrive.badge.plus") } .disabled(self.isLoadingLog && self.logText.isEmpty) if self.currentLogProvider == .claude { Button { self.loadClaudeDump() } label: { - Label("Load parse dump", systemImage: "doc.text.magnifyingglass") + Label(L("load_parse_dump"), systemImage: "doc.text.magnifyingglass") } .disabled(self.isLoadingLog) } @@ -139,7 +139,7 @@ struct DebugPane: View { self.settings.rerunProviderDetection() self.loadLog(self.currentLogProvider) } label: { - Label("Re-run provider autodetect", systemImage: "dot.radiowaves.left.and.right") + Label(L("rerun_provider_autodetect"), systemImage: "dot.radiowaves.left.and.right") } .controlSize(.small) @@ -165,8 +165,8 @@ struct DebugPane: View { } SettingsSection( - title: "Fetch strategy attempts", - caption: "Last fetch pipeline decisions and errors for a provider.") + title: L("section_fetch_strategy"), + caption: L("fetch_strategy_caption")) { Picker("Provider", selection: self.$currentFetchProvider) { ForEach(UsageProvider.allCases, id: \.self) { provider in @@ -190,14 +190,14 @@ struct DebugPane: View { if !self.settings.debugDisableKeychainAccess { SettingsSection( - title: "OpenAI cookies", - caption: "Cookie import + WebKit scrape logs from the last OpenAI cookies attempt.") + title: L("section_openai_cookies"), + caption: L("openai_cookies_caption")) { HStack(spacing: 12) { Button { self.copyToPasteboard(self.store.openAIDashboardCookieImportDebugLog ?? "") } label: { - Label("Copy", systemImage: "doc.on.doc") + Label(L("copy"), systemImage: "doc.on.doc") } .disabled((self.store.openAIDashboardCookieImportDebugLog ?? "").isEmpty) } @@ -206,7 +206,7 @@ struct DebugPane: View { Text( self.store.openAIDashboardCookieImportDebugLog?.isEmpty == false ? (self.store.openAIDashboardCookieImportDebugLog ?? "") - : "No log yet. Update OpenAI cookies in Providers → Codex to run an import.") + : L("no_log_yet")) .font(.system(.footnote, design: .monospaced)) .textSelection(.enabled) .frame(maxWidth: .infinity, alignment: .leading) @@ -219,8 +219,8 @@ struct DebugPane: View { } SettingsSection( - title: "Caches", - caption: "Clear cached cost scan results.") + title: L("section_caches"), + caption: L("caches_caption")) { let isTokenRefreshActive = self.store.isTokenRefreshInFlight(for: .codex) || self.store.isTokenRefreshInFlight(for: .claude) @@ -229,7 +229,7 @@ struct DebugPane: View { Button { Task { await self.clearCostCache() } } label: { - Label("Clear cost cache", systemImage: "trash") + Label(L("clear_cost_cache"), systemImage: "trash") } .disabled(self.isClearingCostCache || isTokenRefreshActive) @@ -242,8 +242,8 @@ struct DebugPane: View { } SettingsSection( - title: "Notifications", - caption: "Trigger test notifications for the 5-hour session window (depleted/restored).") + title: L("section_notifications"), + caption: L("notifications_caption")) { Picker("Provider", selection: self.$currentLogProvider) { Text("Codex").tag(UsageProvider.codex) @@ -256,26 +256,26 @@ struct DebugPane: View { Button { self.postSessionNotification(.depleted, provider: self.currentLogProvider) } label: { - Label("Post depleted", systemImage: "bell.badge") + Label(L("post_depleted"), systemImage: "bell.badge") } .controlSize(.small) Button { self.postSessionNotification(.restored, provider: self.currentLogProvider) } label: { - Label("Post restored", systemImage: "bell") + Label(L("post_restored"), systemImage: "bell") } .controlSize(.small) } } SettingsSection( - title: "CLI sessions", - caption: "Keep Codex/Claude CLI sessions alive after a probe. Default exits once data is captured.") + title: L("section_cli_sessions"), + caption: L("cli_sessions_caption")) { PreferenceToggleRow( - title: "Keep CLI sessions alive", - subtitle: "Skip teardown between probes (debug-only).", + title: L("keep_cli_sessions_alive"), + subtitle: L("keep_cli_sessions_alive_subtitle"), binding: self.$settings.debugKeepCLISessionsAlive) Button { @@ -283,15 +283,15 @@ struct DebugPane: View { await CLIProbeSessionResetter.resetAll() } } label: { - Label("Reset CLI sessions", systemImage: "arrow.counterclockwise") + Label(L("reset_cli_sessions"), systemImage: "arrow.counterclockwise") } .controlSize(.small) } #if DEBUG SettingsSection( - title: "Error simulation", - caption: "Inject a fake error message into the menu card for layout testing.") + title: L("section_error_simulation"), + caption: L("error_simulation_caption")) { Picker("Provider", selection: self.$currentErrorProvider) { Text("Codex").tag(UsageProvider.codex) @@ -314,14 +314,14 @@ struct DebugPane: View { self.simulatedErrorText, provider: self.currentErrorProvider) } label: { - Label("Set menu error", systemImage: "exclamationmark.triangle") + Label(L("set_menu_error"), systemImage: "exclamationmark.triangle") } .controlSize(.small) Button { self.store._setErrorForTesting(nil, provider: self.currentErrorProvider) } label: { - Label("Clear menu error", systemImage: "xmark.circle") + Label(L("clear_menu_error"), systemImage: "xmark.circle") } .controlSize(.small) } @@ -333,7 +333,7 @@ struct DebugPane: View { self.simulatedErrorText, provider: self.currentErrorProvider) } label: { - Label("Set cost error", systemImage: "banknote") + Label(L("set_cost_error"), systemImage: "banknote") } .controlSize(.small) .disabled(!supportsTokenError) @@ -341,7 +341,7 @@ struct DebugPane: View { Button { self.store._setTokenErrorForTesting(nil, provider: self.currentErrorProvider) } label: { - Label("Clear cost error", systemImage: "xmark.circle") + Label(L("clear_cost_error"), systemImage: "xmark.circle") } .controlSize(.small) .disabled(!supportsTokenError) @@ -350,19 +350,19 @@ struct DebugPane: View { #endif SettingsSection( - title: "CLI paths", - caption: "Resolved Codex binary and PATH layers; startup login PATH capture (short timeout).") + title: L("section_cli_paths"), + caption: L("cli_paths_caption")) { - self.binaryRow(title: "Codex binary", value: self.store.pathDebugInfo.codexBinary) - self.binaryRow(title: "Claude binary", value: self.store.pathDebugInfo.claudeBinary) + self.binaryRow(title: L("codex_binary"), value: self.store.pathDebugInfo.codexBinary) + self.binaryRow(title: L("claude_binary"), value: self.store.pathDebugInfo.claudeBinary) VStack(alignment: .leading, spacing: 6) { - Text("Effective PATH") + Text(L("effective_path")) .font(.callout.weight(.semibold)) ScrollView { Text( self.store.pathDebugInfo.effectivePATH.isEmpty - ? "Unavailable" + ? L("unavailable") : self.store.pathDebugInfo.effectivePATH) .font(.system(.footnote, design: .monospaced)) .textSelection(.enabled) @@ -376,7 +376,7 @@ struct DebugPane: View { if let loginPATH = self.store.pathDebugInfo.loginShellPATH { VStack(alignment: .leading, spacing: 6) { - Text("Login shell PATH (startup capture)") + Text(L("login_shell_path")) .font(.callout.weight(.semibold)) ScrollView { Text(loginPATH) @@ -422,7 +422,7 @@ struct DebugPane: View { private var displayedLog: String { if self.logText.isEmpty { - return self.isLoadingLog ? "Loading…" : "No log yet. Fetch to load." + return self.isLoadingLog ? L("loading") : L("no_log_yet_fetch") } return self.logText } @@ -472,7 +472,7 @@ struct DebugPane: View { VStack(alignment: .leading, spacing: 6) { Text(title) .font(.callout.weight(.semibold)) - Text(value ?? "Not found") + Text(value ?? L("not_found")) .font(.system(.footnote, design: .monospaced)) .foregroundStyle(value == nil ? .secondary : .primary) } @@ -504,12 +504,12 @@ struct DebugPane: View { return } - self.costCacheStatus = "Cleared." + self.costCacheStatus = L("cleared") } private func fetchAttemptsText(for provider: UsageProvider) -> String { let attempts = self.store.fetchAttempts(for: provider) - guard !attempts.isEmpty else { return "No fetch attempts yet." } + guard !attempts.isEmpty else { return L("no_fetch_attempts") } return attempts.map { attempt in let kind = Self.fetchKindLabel(attempt.kind) var line = "\(attempt.strategyID) (\(kind))" diff --git a/Sources/CodexBar/PreferencesDisplayPane.swift b/Sources/CodexBar/PreferencesDisplayPane.swift index 04050b3bb..788abcce4 100644 --- a/Sources/CodexBar/PreferencesDisplayPane.swift +++ b/Sources/CodexBar/PreferencesDisplayPane.swift @@ -13,35 +13,35 @@ struct DisplayPane: View { ScrollView(.vertical, showsIndicators: true) { VStack(alignment: .leading, spacing: 16) { SettingsSection(contentSpacing: 12) { - Text("Menu bar") + Text(L("section_menu_bar")) .font(.caption) .foregroundStyle(.secondary) .textCase(.uppercase) PreferenceToggleRow( - title: "Merge Icons", - subtitle: "Use a single menu bar icon with a provider switcher.", + title: L("merge_icons_title"), + subtitle: L("merge_icons_subtitle"), binding: self.$settings.mergeIcons) PreferenceToggleRow( - title: "Switcher shows icons", - subtitle: "Show provider icons in the switcher (otherwise show a weekly progress line).", + title: L("switcher_shows_icons_title"), + subtitle: L("switcher_shows_icons_subtitle"), binding: self.$settings.switcherShowsIcons) .disabled(!self.settings.mergeIcons) .opacity(self.settings.mergeIcons ? 1 : 0.5) PreferenceToggleRow( - title: "Show most-used provider", - subtitle: "Menu bar auto-shows the provider closest to its rate limit.", + title: L("show_most_used_provider_title"), + subtitle: L("show_most_used_provider_subtitle"), binding: self.$settings.menuBarShowsHighestUsage) .disabled(!self.settings.mergeIcons) .opacity(self.settings.mergeIcons ? 1 : 0.5) PreferenceToggleRow( - title: "Menu bar shows percent", - subtitle: "Replace critter bars with provider branding icons and a percentage.", + title: L("menu_bar_shows_percent_title"), + subtitle: L("menu_bar_shows_percent_subtitle"), binding: self.$settings.menuBarShowsBrandIconWithPercent) HStack(alignment: .top, spacing: 12) { VStack(alignment: .leading, spacing: 4) { - Text("Display mode") + Text(L("display_mode_title")) .font(.body) - Text("Choose what to show in the menu bar (Pace shows usage vs. expected).") + Text(L("display_mode_subtitle")) .font(.footnote) .foregroundStyle(.tertiary) } @@ -62,25 +62,25 @@ struct DisplayPane: View { Divider() SettingsSection(contentSpacing: 12) { - Text("Menu content") + Text(L("section_menu_content")) .font(.caption) .foregroundStyle(.secondary) .textCase(.uppercase) PreferenceToggleRow( - title: "Show usage as used", - subtitle: "Progress bars fill as you consume quota (instead of showing remaining).", + title: L("show_usage_as_used_title"), + subtitle: L("show_usage_as_used_subtitle"), binding: self.$settings.usageBarsShowUsed) PreferenceToggleRow( - title: "Show reset time as clock", - subtitle: "Display reset times as absolute clock values instead of countdowns.", + title: L("show_reset_time_as_clock_title"), + subtitle: L("show_reset_time_as_clock_subtitle"), binding: self.$settings.resetTimesShowAbsolute) PreferenceToggleRow( - title: "Show credits + extra usage", - subtitle: "Show Codex Credits and Claude Extra usage sections in the menu.", + title: L("show_credits_extra_usage_title"), + subtitle: L("show_credits_extra_usage_subtitle"), binding: self.$settings.showOptionalCreditsAndExtraUsage) PreferenceToggleRow( - title: "Show all token accounts", - subtitle: "Stack token accounts in the menu (otherwise show an account switcher bar).", + title: L("show_all_token_accounts_title"), + subtitle: L("show_all_token_accounts_subtitle"), binding: self.$settings.showAllTokenAccountsInMenu) self.overviewProviderSelector } @@ -110,11 +110,11 @@ struct DisplayPane: View { private var overviewProviderSelector: some View { VStack(alignment: .leading, spacing: 6) { HStack(alignment: .center, spacing: 12) { - Text("Overview tab providers") + Text(L("overview_tab_providers_title")) .font(.body) Spacer(minLength: 0) if self.showsOverviewConfigureButton { - Button("Configure…") { + Button(L("configure")) { self.isOverviewProviderPopoverPresented = true } .offset(y: 1) @@ -125,11 +125,11 @@ struct DisplayPane: View { } if !self.settings.mergeIcons { - Text("Enable Merge Icons to configure Overview tab providers.") + Text(L("overview_enable_merge_icons_hint")) .font(.footnote) .foregroundStyle(.tertiary) } else if self.activeProvidersInOrder.isEmpty { - Text("No enabled providers available for Overview.") + Text(L("overview_no_providers_hint")) .font(.footnote) .foregroundStyle(.tertiary) } else { @@ -144,9 +144,9 @@ struct DisplayPane: View { private var overviewProviderPopover: some View { VStack(alignment: .leading, spacing: 10) { - Text("Choose up to \(Self.maxOverviewProviders) providers") + Text(String(format: L("overview_choose_providers"), Self.maxOverviewProviders)) .font(.headline) - Text("Overview rows always follow provider order.") + Text(L("overview_rows_follow_order")) .font(.footnote) .foregroundStyle(.tertiary) @@ -191,7 +191,7 @@ struct DisplayPane: View { private var overviewProviderSelectionSummary: String { let selectedNames = self.overviewSelectedProviders.map(self.providerDisplayName) - guard !selectedNames.isEmpty else { return "No providers selected" } + guard !selectedNames.isEmpty else { return L("overview_no_providers_selected") } return selectedNames.joined(separator: ", ") } diff --git a/Sources/CodexBar/PreferencesGeneralPane.swift b/Sources/CodexBar/PreferencesGeneralPane.swift index 39a95a55f..a61109333 100644 --- a/Sources/CodexBar/PreferencesGeneralPane.swift +++ b/Sources/CodexBar/PreferencesGeneralPane.swift @@ -2,6 +2,22 @@ import AppKit import CodexBarCore import SwiftUI +enum AppLanguage: String, CaseIterable, Identifiable { + case system = "" + case english = "en" + case chineseSimplified = "zh-Hans" + + var id: String { self.rawValue } + + var label: String { + switch self { + case .system: return L("language_system") + case .english: return L("language_english") + case .chineseSimplified: return L("language_chinese_simplified") + } + } +} + @MainActor struct GeneralPane: View { @Bindable var settings: SettingsStore @@ -11,20 +27,43 @@ struct GeneralPane: View { ScrollView(.vertical, showsIndicators: true) { VStack(alignment: .leading, spacing: 16) { SettingsSection(contentSpacing: 12) { - Text("System") + Text(L("section_system")) .font(.caption) .foregroundStyle(.secondary) .textCase(.uppercase) + + VStack(alignment: .leading, spacing: 6) { + HStack(alignment: .top, spacing: 12) { + VStack(alignment: .leading, spacing: 4) { + Text(L("language_title")) + .font(.body) + Text(L("language_subtitle")) + .font(.footnote) + .foregroundStyle(.tertiary) + .fixedSize(horizontal: false, vertical: true) + } + Spacer() + Picker("Language", selection: self.$settings.appLanguage) { + ForEach(AppLanguage.allCases) { option in + Text(option.label).tag(option.rawValue) + } + } + .labelsHidden() + .pickerStyle(.menu) + .frame(maxWidth: 200) + } + } + PreferenceToggleRow( - title: "Start at Login", - subtitle: "Automatically opens CodexBar when you start your Mac.", + title: L("start_at_login_title"), + subtitle: L("start_at_login_subtitle"), binding: self.$settings.launchAtLogin) } Divider() SettingsSection(contentSpacing: 12) { - Text("Usage") + Text(L("section_usage")) .font(.caption) .foregroundStyle(.secondary) .textCase(.uppercase) @@ -32,18 +71,18 @@ struct GeneralPane: View { VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 4) { Toggle(isOn: self.$settings.costUsageEnabled) { - Text("Show cost summary") + Text(L("show_cost_summary")) .font(.body) } .toggleStyle(.checkbox) - Text("Reads local usage logs. Shows today + last 30 days cost in the menu.") + Text(L("show_cost_summary_subtitle")) .font(.footnote) .foregroundStyle(.tertiary) .fixedSize(horizontal: false, vertical: true) if self.settings.costUsageEnabled { - Text("Auto-refresh: hourly · Timeout: 10m") + Text(L("cost_auto_refresh_info")) .font(.footnote) .foregroundStyle(.tertiary) @@ -57,16 +96,16 @@ struct GeneralPane: View { Divider() SettingsSection(contentSpacing: 12) { - Text("Automation") + Text(L("section_automation")) .font(.caption) .foregroundStyle(.secondary) .textCase(.uppercase) VStack(alignment: .leading, spacing: 6) { HStack(alignment: .top, spacing: 12) { VStack(alignment: .leading, spacing: 4) { - Text("Refresh cadence") + Text(L("refresh_cadence_title")) .font(.body) - Text("How often CodexBar polls providers in the background.") + Text(L("refresh_cadence_subtitle")) .font(.footnote) .foregroundStyle(.tertiary) } @@ -81,20 +120,18 @@ struct GeneralPane: View { .frame(maxWidth: 200) } if self.settings.refreshFrequency == .manual { - Text("Auto-refresh is off; use the menu's Refresh command.") + Text(L("manual_refresh_hint")) .font(.footnote) .foregroundStyle(.secondary) } } PreferenceToggleRow( - title: "Check provider status", - subtitle: "Polls OpenAI/Claude status pages and Google Workspace for " + - "Gemini/Antigravity, surfacing incidents in the icon and menu.", + title: L("check_provider_status_title"), + subtitle: L("check_provider_status_subtitle"), binding: self.$settings.statusChecksEnabled) PreferenceToggleRow( - title: "Session quota notifications", - subtitle: "Notifies when the 5-hour session quota hits 0% and when it becomes " + - "available again.", + title: L("session_quota_notifications_title"), + subtitle: L("session_quota_notifications_subtitle"), binding: self.$settings.sessionQuotaNotificationsEnabled) } @@ -103,7 +140,7 @@ struct GeneralPane: View { SettingsSection(contentSpacing: 12) { HStack { Spacer() - Button("Quit CodexBar") { NSApp.terminate(nil) } + Button(L("quit_app")) { NSApp.terminate(nil) } .buttonStyle(.borderedProminent) .controlSize(.large) } @@ -119,7 +156,7 @@ struct GeneralPane: View { let name = ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName guard provider == .claude || provider == .codex else { - return Text("\(name): unsupported") + return Text(String(format: L("cost_status_unsupported"), name)) .font(.footnote) .foregroundStyle(.tertiary) } @@ -133,20 +170,20 @@ struct GeneralPane: View { formatter.unitsStyle = .abbreviated return formatter.string(from: seconds).map { " (\($0))" } ?? "" }() - return Text("\(name): fetching…\(elapsed)") + return Text(String(format: L("cost_status_fetching"), name, elapsed)) .font(.footnote) .foregroundStyle(.tertiary) } if let snapshot = self.store.tokenSnapshot(for: provider) { let updated = UsageFormatter.updatedString(from: snapshot.updatedAt) let cost = snapshot.last30DaysCostUSD.map { UsageFormatter.usdString($0) } ?? "—" - return Text("\(name): \(updated) · 30d \(cost)") + return Text(String(format: L("cost_status_snapshot"), name, updated, cost)) .font(.footnote) .foregroundStyle(.tertiary) } if let error = self.store.tokenError(for: provider), !error.isEmpty { let truncated = UsageFormatter.truncatedSingleLine(error, max: 120) - return Text("\(name): \(truncated)") + return Text(String(format: L("cost_status_error"), name, truncated)) .font(.footnote) .foregroundStyle(.tertiary) } @@ -154,11 +191,11 @@ struct GeneralPane: View { let rel = RelativeDateTimeFormatter() rel.unitsStyle = .abbreviated let when = rel.localizedString(for: lastAttempt, relativeTo: Date()) - return Text("\(name): last attempt \(when)") + return Text(String(format: L("cost_status_last_attempt"), name, when)) .font(.footnote) .foregroundStyle(.tertiary) } - return Text("\(name): no data yet") + return Text(String(format: L("cost_status_no_data"), name)) .font(.footnote) .foregroundStyle(.tertiary) } diff --git a/Sources/CodexBar/PreferencesProviderDetailView.swift b/Sources/CodexBar/PreferencesProviderDetailView.swift index 5ecff079d..8daf9d554 100644 --- a/Sources/CodexBar/PreferencesProviderDetailView.swift +++ b/Sources/CodexBar/PreferencesProviderDetailView.swift @@ -99,7 +99,7 @@ struct ProviderDetailView: View { if let errorDisplay { ProviderErrorView( - title: "Last \(self.store.metadata(for: self.provider).displayName) fetch failed:", + title: String(format: L("last_fetch_failed"), self.store.metadata(for: self.provider).displayName), display: errorDisplay, isExpanded: self.$isErrorExpanded, onCopy: { self.onCopyError(errorDisplay.full) }) diff --git a/Sources/CodexBar/PreferencesProviderErrorView.swift b/Sources/CodexBar/PreferencesProviderErrorView.swift index 0fa246d88..4b5c96b78 100644 --- a/Sources/CodexBar/PreferencesProviderErrorView.swift +++ b/Sources/CodexBar/PreferencesProviderErrorView.swift @@ -36,7 +36,7 @@ struct ProviderErrorView: View { .fixedSize(horizontal: false, vertical: true) if self.display.preview != self.display.full { - Button(self.isExpanded ? "Hide details" : "Show details") { self.isExpanded.toggle() } + Button(self.isExpanded ? L("Hide details") : L("Show details")) { self.isExpanded.toggle() } .buttonStyle(.link) .font(.footnote) } diff --git a/Sources/CodexBar/PreferencesProvidersPane.swift b/Sources/CodexBar/PreferencesProvidersPane.swift index 9d916465f..d15e1fecc 100644 --- a/Sources/CodexBar/PreferencesProvidersPane.swift +++ b/Sources/CodexBar/PreferencesProvidersPane.swift @@ -99,7 +99,7 @@ struct ProvidersPane: View { } }) } else { - Text("Select a provider") + Text(L("select_a_provider")) .foregroundStyle(.secondary) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) } @@ -127,7 +127,7 @@ struct ProvidersPane: View { active.onConfirm() self.activeConfirmation = nil } - Button("Cancel", role: .cancel) { self.activeConfirmation = nil } + Button(L("cancel"), role: .cancel) { self.activeConfirmation = nil } } }, message: { @@ -176,9 +176,9 @@ struct ProvidersPane: View { let relative = snapshot.updatedAt.relativeDescription() usageText = relative } else if self.store.isStale(provider: provider) { - usageText = "last fetch failed" + usageText = L("last_fetch_failed") } else { - usageText = "usage not fetched yet" + usageText = L("usage_not_fetched_yet") } let presentationContext = ProviderPresentationContext( @@ -199,8 +199,7 @@ struct ProvidersPane: View { let projection = self.settings.codexVisibleAccountProjection let degradedNotice: CodexAccountsSectionNotice? = if projection.hasUnreadableAddedAccountStore { CodexAccountsSectionNotice( - text: "Managed account storage is unreadable. Live account access is still available, " - + "but managed add, re-auth, and remove actions are disabled until the store is recoverable.", + text: L("managed_account_storage_unreadable"), tone: .warning) } else { nil @@ -303,9 +302,9 @@ struct ProvidersPane: View { func requestManagedCodexAccountRemoval(_ account: CodexVisibleAccount) { guard let accountID = account.storedAccountID else { return } self.activeConfirmation = ProviderSettingsConfirmationState( - title: "Remove Codex account?", - message: "Remove \(account.email) from CodexBar? Its managed Codex home will be deleted.", - confirmTitle: "Remove", + title: L("remove_codex_account_title"), + message: String(format: L("remove_account_message"), account.email), + confirmTitle: L("remove"), onConfirm: { Task { @MainActor in await self.removeManagedCodexAccount(id: accountID) @@ -454,18 +453,18 @@ struct ProvidersPane: View { let options: [ProviderSettingsPickerOption] if provider == .openrouter { options = [ - ProviderSettingsPickerOption(id: MenuBarMetricPreference.automatic.rawValue, title: "Automatic"), + ProviderSettingsPickerOption(id: MenuBarMetricPreference.automatic.rawValue, title: L("automatic")), ProviderSettingsPickerOption( id: MenuBarMetricPreference.primary.rawValue, - title: "Primary (API key limit)"), + title: L("primary_api_key_limit")), ] } else if provider == .abacus { let metadata = self.store.metadata(for: provider) options = [ - ProviderSettingsPickerOption(id: MenuBarMetricPreference.automatic.rawValue, title: "Automatic"), + ProviderSettingsPickerOption(id: MenuBarMetricPreference.automatic.rawValue, title: L("automatic")), ProviderSettingsPickerOption( id: MenuBarMetricPreference.primary.rawValue, - title: "Primary (\(metadata.sessionLabel))"), + title: String(format: L("metric_primary"), metadata.sessionLabel)), ] } else { let metadata = self.store.metadata(for: provider) @@ -474,19 +473,19 @@ struct ProvidersPane: View { let supportsTertiary = self.settings.menuBarMetricSupportsTertiary(for: provider, snapshot: snapshot) let supportsExtraUsage = self.settings.menuBarMetricSupportsExtraUsage(for: provider, snapshot: snapshot) var metricOptions: [ProviderSettingsPickerOption] = [ - ProviderSettingsPickerOption(id: MenuBarMetricPreference.automatic.rawValue, title: "Automatic"), + ProviderSettingsPickerOption(id: MenuBarMetricPreference.automatic.rawValue, title: L("automatic")), ProviderSettingsPickerOption( id: MenuBarMetricPreference.primary.rawValue, - title: "Primary (\(metadata.sessionLabel))"), + title: String(format: L("metric_primary"), metadata.sessionLabel)), ProviderSettingsPickerOption( id: MenuBarMetricPreference.secondary.rawValue, - title: "Secondary (\(metadata.weeklyLabel))"), + title: String(format: L("metric_secondary"), metadata.weeklyLabel)), ] if supportsTertiary { let tertiaryTitle = metadata.opusLabel ?? MenuBarMetricPreference.tertiary.label metricOptions.append(ProviderSettingsPickerOption( id: MenuBarMetricPreference.tertiary.rawValue, - title: "Tertiary (\(tertiaryTitle))")) + title: String(format: L("metric_tertiary"), tertiaryTitle))) } if supportsExtraUsage { metricOptions.append(ProviderSettingsPickerOption( @@ -496,14 +495,14 @@ struct ProvidersPane: View { if supportsAverage { metricOptions.append(ProviderSettingsPickerOption( id: MenuBarMetricPreference.average.rawValue, - title: "Average (\(metadata.sessionLabel) + \(metadata.weeklyLabel))")) + title: String(format: L("metric_average"), metadata.sessionLabel, metadata.weeklyLabel))) } options = metricOptions } return ProviderSettingsPickerDescriptor( id: "menuBarMetric", - title: "Menu bar metric", - subtitle: "Choose which window drives the menu bar percent.", + title: L("menu_bar_metric_title"), + subtitle: L("menu_bar_metric_subtitle"), binding: Binding( get: { self.settings @@ -607,22 +606,20 @@ struct ProvidersPane: View { error == .authenticationInProgress { return CodexAccountsSectionNotice( - text: "A managed Codex login is already running. Wait for it to finish before adding " - + "or re-authenticating another account.", + text: L("managed_login_already_running"), tone: .warning) } if let error = error as? ManagedCodexAccountServiceError { let message = switch error { case .loginFailed: - "Managed Codex login did not complete. Try again after finishing the browser login flow." + L("managed_login_failed") case .missingEmail: - "Codex login completed, but no account email was available. Try again after confirming " - + "the account is fully signed in." + L("managed_login_missing_email") case .workspaceSelectionCancelled: - "CodexBar found multiple workspaces, but no workspace was selected." + L("workspace_selection_cancelled") case let .unsafeManagedHome(path): - "CodexBar refused to modify an unexpected managed home path: \(path)" + String(format: L("unsafe_managed_home"), path) } return CodexAccountsSectionNotice(text: message, tone: .warning) } diff --git a/Sources/CodexBar/PreferencesView.swift b/Sources/CodexBar/PreferencesView.swift index 408a83d6d..a5163eef0 100644 --- a/Sources/CodexBar/PreferencesView.swift +++ b/Sources/CodexBar/PreferencesView.swift @@ -15,12 +15,12 @@ enum PreferencesTab: String, CaseIterable, Hashable { var title: String { switch self { - case .general: "General" - case .providers: "Providers" - case .display: "Display" - case .advanced: "Advanced" - case .about: "About" - case .debug: "Debug" + case .general: L("tab_general") + case .providers: L("tab_providers") + case .display: L("tab_display") + case .advanced: L("tab_advanced") + case .about: L("tab_about") + case .debug: L("tab_debug") } } @@ -67,7 +67,7 @@ struct PreferencesView: View { var body: some View { TabView(selection: self.$selection.tab) { GeneralPane(settings: self.settings, store: self.store) - .tabItem { Label("General", systemImage: "gearshape") } + .tabItem { Label(L("tab_general"), systemImage: "gearshape") } .tag(PreferencesTab.general) ProvidersPane( @@ -75,24 +75,24 @@ struct PreferencesView: View { store: self.store, managedCodexAccountCoordinator: self.managedCodexAccountCoordinator, codexAccountPromotionCoordinator: self.codexAccountPromotionCoordinator) - .tabItem { Label("Providers", systemImage: "square.grid.2x2") } + .tabItem { Label(L("tab_providers"), systemImage: "square.grid.2x2") } .tag(PreferencesTab.providers) DisplayPane(settings: self.settings, store: self.store) - .tabItem { Label("Display", systemImage: "eye") } + .tabItem { Label(L("tab_display"), systemImage: "eye") } .tag(PreferencesTab.display) AdvancedPane(settings: self.settings) - .tabItem { Label("Advanced", systemImage: "slider.horizontal.3") } + .tabItem { Label(L("tab_advanced"), systemImage: "slider.horizontal.3") } .tag(PreferencesTab.advanced) AboutPane(updater: self.updater) - .tabItem { Label("About", systemImage: "info.circle") } + .tabItem { Label(L("tab_about"), systemImage: "info.circle") } .tag(PreferencesTab.about) if self.settings.debugMenuEnabled { DebugPane(settings: self.settings, store: self.store) - .tabItem { Label("Debug", systemImage: "ladybug") } + .tabItem { Label(L("tab_debug"), systemImage: "ladybug") } .tag(PreferencesTab.debug) } } diff --git a/Sources/CodexBar/Providers/Claude/ClaudeProviderImplementation.swift b/Sources/CodexBar/Providers/Claude/ClaudeProviderImplementation.swift index da82c1734..2c7f56c16 100644 --- a/Sources/CodexBar/Providers/Claude/ClaudeProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Claude/ClaudeProviderImplementation.swift @@ -216,7 +216,7 @@ struct ClaudeProviderImplementation: ProviderImplementation { @MainActor func appendUsageMenuEntries(context: ProviderMenuUsageContext, entries: inout [ProviderMenuEntry]) { if context.snapshot?.secondary == nil { - entries.append(.text("Weekly usage unavailable for this account.", .secondary)) + entries.append(.text(L("Weekly usage unavailable for this account."), .secondary)) } if let cost = context.snapshot?.providerCost, @@ -225,7 +225,7 @@ struct ClaudeProviderImplementation: ProviderImplementation { { let used = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode) let limit = UsageFormatter.currencyString(cost.limit, currencyCode: cost.currencyCode) - entries.append(.text("Extra usage: \(used) / \(limit)", .primary)) + entries.append(.text(String(format: L("extra_usage_format"), used, limit), .primary)) } } diff --git a/Sources/CodexBar/Providers/Codex/CodexProviderImplementation.swift b/Sources/CodexBar/Providers/Codex/CodexProviderImplementation.swift index 9a39c3af1..899b8236f 100644 --- a/Sources/CodexBar/Providers/Codex/CodexProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Codex/CodexProviderImplementation.swift @@ -199,9 +199,9 @@ struct CodexProviderImplementation: ProviderImplementation { else { return } if let credits = context.store.credits { - entries.append(.text("Credits: \(UsageFormatter.creditsString(from: credits.remaining))", .primary)) + entries.append(.text(String(format: L("credits_remaining"), UsageFormatter.creditsString(from: credits.remaining)), .primary)) if let latest = credits.events.first { - entries.append(.text("Last spend: \(UsageFormatter.creditEventSummary(latest))", .secondary)) + entries.append(.text(String(format: L("last_spend"), UsageFormatter.creditEventSummary(latest)), .secondary)) } } else { let hint = context.store.userFacingLastCreditsError ?? context.metadata.creditsHint diff --git a/Sources/CodexBar/Providers/Copilot/CopilotLoginFlow.swift b/Sources/CodexBar/Providers/Copilot/CopilotLoginFlow.swift index b314afb75..85ffa39c4 100644 --- a/Sources/CodexBar/Providers/Copilot/CopilotLoginFlow.swift +++ b/Sources/CodexBar/Providers/Copilot/CopilotLoginFlow.swift @@ -16,14 +16,10 @@ struct CopilotLoginFlow { pb.setString(code.userCode, forType: .string) let alert = NSAlert() - alert.messageText = "GitHub Copilot Login" - alert.informativeText = """ - A device code has been copied to your clipboard: \(code.userCode) - - Please verify it at: \(code.verificationUri) - """ - alert.addButton(withTitle: "Open Browser") - alert.addButton(withTitle: "Cancel") + alert.messageText = L("GitHub Copilot Login") + alert.informativeText = String(format: L("copilot_device_code"), code.userCode, code.verificationUri) + alert.addButton(withTitle: L("Open Browser")) + alert.addButton(withTitle: L("Cancel")) let response = alert.runModal() if response == .alertSecondButtonReturn { @@ -43,12 +39,9 @@ struct CopilotLoginFlow { // Let's show a "Waiting" alert that can be cancelled. let waitingAlert = NSAlert() - waitingAlert.messageText = "Waiting for Authentication..." - waitingAlert.informativeText = """ - Please complete the login in your browser. - This window will close automatically when finished. - """ - waitingAlert.addButton(withTitle: "Cancel") + waitingAlert.messageText = L("Waiting for Authentication...") + waitingAlert.informativeText = L("copilot_waiting_text") + waitingAlert.addButton(withTitle: L("Cancel")) let parentWindow = Self.resolveWaitingParentWindow() let hostWindow = parentWindow ?? Self.makeWaitingHostWindow() let shouldCloseHostWindow = parentWindow == nil diff --git a/Sources/CodexBar/Providers/Cursor/CursorProviderImplementation.swift b/Sources/CodexBar/Providers/Cursor/CursorProviderImplementation.swift index 48db614f9..a8638af89 100644 --- a/Sources/CodexBar/Providers/Cursor/CursorProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Cursor/CursorProviderImplementation.swift @@ -94,9 +94,9 @@ struct CursorProviderImplementation: ProviderImplementation { let used = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode) if cost.limit > 0 { let limitStr = UsageFormatter.currencyString(cost.limit, currencyCode: cost.currencyCode) - entries.append(.text("On-Demand: \(used) / \(limitStr)", .primary)) + entries.append(.text(String(format: L("cursor_on_demand_with_limit"), used, limitStr), .primary)) } else { - entries.append(.text("On-Demand: \(used)", .primary)) + entries.append(.text(String(format: L("cursor_on_demand"), used), .primary)) } } } diff --git a/Sources/CodexBar/Providers/JetBrains/JetBrainsLoginFlow.swift b/Sources/CodexBar/Providers/JetBrains/JetBrainsLoginFlow.swift index bfb92438e..e188c04bb 100644 --- a/Sources/CodexBar/Providers/JetBrains/JetBrainsLoginFlow.swift +++ b/Sources/CodexBar/Providers/JetBrains/JetBrainsLoginFlow.swift @@ -1,4 +1,5 @@ import CodexBarCore +import Foundation @MainActor extension StatusItemController { @@ -17,10 +18,10 @@ extension StatusItemController { let ideNames = detectedIDEs.prefix(3).map(\.displayName).joined(separator: ", ") let hasQuotaFile = !JetBrainsIDEDetector.detectInstalledIDEs().isEmpty let message = hasQuotaFile - ? "Detected: \(ideNames). Select your preferred IDE in Settings, then refresh CodexBar." - : "Detected: \(ideNames). Use AI Assistant once to generate quota data, then refresh CodexBar." + ? String(format: L("jetbrains_detected_select"), ideNames) + : String(format: L("jetbrains_detected_generate"), ideNames) self.presentLoginAlert( - title: "JetBrains AI is ready", + title: L("JetBrains AI is ready"), message: message) } } diff --git a/Sources/CodexBar/Providers/VertexAI/VertexAILoginFlow.swift b/Sources/CodexBar/Providers/VertexAI/VertexAILoginFlow.swift index 1f8fe5418..2741f091d 100644 --- a/Sources/CodexBar/Providers/VertexAI/VertexAILoginFlow.swift +++ b/Sources/CodexBar/Providers/VertexAI/VertexAILoginFlow.swift @@ -7,20 +7,11 @@ extension StatusItemController { func runVertexAILoginFlow() async { // Show alert with instructions let alert = NSAlert() - alert.messageText = "Vertex AI Login" - alert.informativeText = """ - To use Vertex AI tracking, you need to authenticate with Google Cloud. - - 1. Open Terminal - 2. Run: gcloud auth application-default login - 3. Follow the browser prompts to sign in - 4. Set your project: gcloud config set project PROJECT_ID - - Would you like to open Terminal now? - """ + alert.messageText = L("Vertex AI Login") + alert.informativeText = L("vertex_ai_login_instructions") alert.alertStyle = .informational - alert.addButton(withTitle: "Open Terminal") - alert.addButton(withTitle: "Cancel") + alert.addButton(withTitle: L("Open Terminal")) + alert.addButton(withTitle: L("Cancel")) let response = alert.runModal() diff --git a/Sources/CodexBar/Resources/en.lproj/Localizable.strings b/Sources/CodexBar/Resources/en.lproj/Localizable.strings new file mode 100644 index 000000000..327b9e359 --- /dev/null +++ b/Sources/CodexBar/Resources/en.lproj/Localizable.strings @@ -0,0 +1,592 @@ +/* English localization for CodexBar (base/fallback) */ + +" providers" = " providers"; +"(System)" = "(System)"; +"30d" = "30d"; +"A managed Codex login is already running. Wait for it to finish before adding " = "A managed Codex login is already running. Wait for it to finish before adding "; +"API key" = "API key"; +"API region" = "API region"; +"API token" = "API token"; +"API tokens" = "API tokens"; +"About" = "About"; +"Account" = "Account"; +"Accounts" = "Accounts"; +"Accounts subtitle" = "Accounts subtitle"; +"Active" = "Active"; +"Add" = "Add"; +"Add Workspace" = "Add Workspace"; +"Advanced" = "Advanced"; +"All" = "All"; +"Always allow prompts" = "Always allow prompts"; +"Animation pattern" = "Animation pattern"; +"Antigravity login is managed in the app" = "Antigravity login is managed in the app"; +"Applies only to the Security.framework OAuth keychain reader." = "Applies only to the Security.framework OAuth keychain reader."; +"Auto falls back to the next source if the preferred one fails." = "Auto falls back to the next source if the preferred one fails."; +"Auto uses API first, then falls back to CLI on auth failures." = "Auto uses API first, then falls back to CLI on auth failures."; +"Auto-detect" = "Auto-detect"; +"Auto-refresh is off; use the menu's Refresh command." = "Auto-refresh is off; use the menu's Refresh command."; +"Auto-refresh: hourly · Timeout: 10m" = "Auto-refresh: hourly · Timeout: 10m"; +"Automatic" = "Automatic"; +"Automatic imports browser cookies and WorkOS tokens." = "Automatic imports browser cookies and WorkOS tokens."; +"Automatic imports browser cookies and local storage tokens." = "Automatic imports browser cookies and local storage tokens."; +"Automatic imports browser cookies for dashboard extras." = "Automatic imports browser cookies for dashboard extras."; +"Automatic imports browser cookies for the web API." = "Automatic imports browser cookies for the web API."; +"Automatic imports browser cookies from Model Studio/Bailian." = "Automatic imports browser cookies from Model Studio/Bailian."; +"Automatic imports browser cookies from admin.mistral.ai." = "Automatic imports browser cookies from admin.mistral.ai."; +"Automatic imports browser cookies from opencode.ai." = "Automatic imports browser cookies from opencode.ai."; +"Automatic imports browser cookies or stored sessions." = "Automatic imports browser cookies or stored sessions."; +"Automatic imports browser cookies." = "Automatic imports browser cookies."; +"Automatically imports browser session cookie." = "Automatically imports browser session cookie."; +"Automatically opens CodexBar when you start your Mac." = "Automatically opens CodexBar when you start your Mac."; +"Automation" = "Automation"; +"Average (\\(label1) + \\(label2))" = "Average (\\(label1) + \\(label2))"; +"Average (\\(metadata.sessionLabel) + \\(metadata.weeklyLabel))" = "Average (\\(metadata.sessionLabel) + \\(metadata.weeklyLabel))"; +"Avoid Keychain prompts" = "Avoid Keychain prompts"; +"Balance" = "Balance"; +"Battery Saver" = "Battery Saver"; +"Bordered" = "Bordered"; +"Build" = "Build"; +"Built \\(buildTimestamp)" = "Built \\(buildTimestamp)"; +"Buy Credits..." = "Buy Credits..."; +"Buy Credits…" = "Buy Credits…"; +"CLI paths" = "CLI paths"; +"CLI sessions" = "CLI sessions"; +"Caches" = "Caches"; +"Cancel" = "Cancel"; +"Check for Updates…" = "Check for Updates…"; +"Check for updates automatically" = "Check for updates automatically"; +"Check if you like your agents having some fun up there." = "Check if you like your agents having some fun up there."; +"Check provider status" = "Check provider status"; +"Choose Codex workspace" = "Choose Codex workspace"; +"Choose the MiniMax host (global .io or China mainland .com)." = "Choose the MiniMax host (global .io or China mainland .com)."; +"Choose up to " = "Choose up to "; +"Choose up to \\(Self.maxOverviewProviders) providers" = "Choose up to \\(Self.maxOverviewProviders) providers"; +"Choose up to \\(count) providers" = "Choose up to \\(count) providers"; +"Choose what to show in the menu bar (Pace shows usage vs. expected)." = "Choose what to show in the menu bar (Pace shows usage vs. expected)."; +"Choose which Codex account CodexBar should follow." = "Choose which Codex account CodexBar should follow."; +"Choose which window drives the menu bar percent." = "Choose which window drives the menu bar percent."; +"Chrome" = "Chrome"; +"Claude CLI not found" = "Claude CLI not found"; +"Claude binary" = "Claude binary"; +"Claude cookies" = "Claude cookies"; +"Claude login failed" = "Claude login failed"; +"Claude login timed out" = "Claude login timed out"; +"Close" = "Close"; +"Code review" = "Code review"; +"Codex CLI not found" = "Codex CLI not found"; +"Codex account login already running" = "Codex account login already running"; +"Codex binary" = "Codex binary"; +"Codex login failed" = "Codex login failed"; +"Codex login timed out" = "Codex login timed out"; +"CodexBar Lifecycle Keepalive" = "CodexBar Lifecycle Keepalive"; +"CodexBar could not read managed account storage. " = "CodexBar could not read managed account storage. "; +"Configure…" = "Configure…"; +"Connected" = "Connected"; +"Controls how much detail is logged." = "Controls how much detail is logged."; +"Cookie header" = "Cookie header"; +"Cookie source" = "Cookie source"; +"Cookie: ..." = "Cookie: ..."; +"Cookie: \\u{2026}\\\n\\\nor paste a cURL capture from the Abacus AI dashboard" = "Cookie: \\u{2026}\\\n\\\nor paste a cURL capture from the Abacus AI dashboard"; +"Cookie: \\u{2026}\\\n\\\nor paste the __Secure-next-auth.session-token value" = "Cookie: \\u{2026}\\\n\\\nor paste the __Secure-next-auth.session-token value"; +"Cookie: \\u{2026}\\\n\\\nor paste the kimi-auth token value" = "Cookie: \\u{2026}\\\n\\\nor paste the kimi-auth token value"; +"Cookie: …" = "Cookie: …"; +"CopilotDeviceFlow" = "CopilotDeviceFlow"; +"Cost" = "Cost"; +"Could not add Codex account" = "Could not add Codex account"; +"Could not open Terminal for Gemini" = "Could not open Terminal for Gemini"; +"Could not start claude /login" = "Could not start claude /login"; +"Could not start codex login" = "Could not start codex login"; +"Could not switch system account" = "Could not switch system account"; +"Credits" = "Credits"; +"Credits history" = "Credits history"; +"Cursor login failed" = "Cursor login failed"; +"Custom" = "Custom"; +"Custom Path" = "Custom Path"; +"Daily Routines" = "Daily Routines"; +"Debug" = "Debug"; +"Default" = "Default"; +"Designs" = "Designs"; +"Disable Keychain access" = "Disable Keychain access"; +"Disabled" = "Disabled"; +"Disconnected" = "Disconnected"; +"Display" = "Display"; +"Display mode" = "Display mode"; +"Display reset times as absolute clock values instead of countdowns." = "Display reset times as absolute clock values instead of countdowns."; +"Done" = "Done"; +"Effective PATH" = "Effective PATH"; +"Email" = "Email"; +"Enable Merge Icons to configure Overview tab providers." = "Enable Merge Icons to configure Overview tab providers."; +"Enable file logging" = "Enable file logging"; +"Enabled" = "Enabled"; +"Error" = "Error"; +"Error simulation" = "Error simulation"; +"Expose troubleshooting tools in the Debug tab." = "Expose troubleshooting tools in the Debug tab."; +"Failed" = "Failed"; +"False" = "False"; +"Fetch strategy attempts" = "Fetch strategy attempts"; +"Fetching" = "Fetching"; +"Field" = "Field"; +"Field subtitle" = "Field subtitle"; +"Finish the current managed account change before switching the system account." = "Finish the current managed account change before switching the system account."; +"Force animation on next refresh" = "Force animation on next refresh"; +"Gateway region" = "Gateway region"; +"Gemini CLI not found" = "Gemini CLI not found"; +"Gemini/Antigravity, surfacing incidents in the icon and menu." = "Gemini/Antigravity, surfacing incidents in the icon and menu."; +"General" = "General"; +"GitHub" = "GitHub"; +"GitHub Copilot Login" = "GitHub Copilot Login"; +"GitHub Login" = "GitHub Login"; +"Hide details" = "Hide details"; +"Hide personal information" = "Hide personal information"; +"Historical tracking" = "Historical tracking"; +"How often CodexBar polls providers in the background." = "How often CodexBar polls providers in the background."; +"Inactive" = "Inactive"; +"Install CLI" = "Install CLI"; +"Install the Claude CLI (npm i -g @anthropic-ai/claude-code) and try again." = "Install the Claude CLI (npm i -g @anthropic-ai/claude-code) and try again."; +"Install the Codex CLI (npm i -g @openai/codex) and try again." = "Install the Codex CLI (npm i -g @openai/codex) and try again."; +"Install the Gemini CLI (npm i -g @google/gemini-cli) and try again." = "Install the Gemini CLI (npm i -g @google/gemini-cli) and try again."; +"JetBrains AI is ready" = "JetBrains AI is ready"; +"JetBrains IDE" = "JetBrains IDE"; +"Keep CLI sessions alive" = "Keep CLI sessions alive"; +"Keyboard shortcut" = "Keyboard shortcut"; +"Keychain access" = "Keychain access"; +"Keychain prompt policy" = "Keychain prompt policy"; +"Last \\(name) fetch failed:" = "Last \\(name) fetch failed:"; +"Last \\(self.store.metadata(for: self.provider).displayName) fetch failed:" = "Last \\(self.store.metadata(for: self.provider).displayName) fetch failed:"; +"Last attempt" = "Last attempt"; +"Link" = "Link"; +"Loading animations" = "Loading animations"; +"Loading…" = "Loading…"; +"Local" = "Local"; +"Logging" = "Logging"; +"Login failed" = "Login failed"; +"Login shell PATH (startup capture)" = "Login shell PATH (startup capture)"; +"Login timed out" = "Login timed out"; +"MCP details" = "MCP details"; +"Managed Codex accounts unavailable" = "Managed Codex accounts unavailable"; +"Managed account storage is unreadable. Live account access is still available, " = "Managed account storage is unreadable. Live account access is still available, "; +"Manual" = "Manual"; +"May your tokens never run out—keep agent limits in view." = "May your tokens never run out—keep agent limits in view."; +"Menu bar" = "Menu bar"; +"Menu bar auto-shows the provider closest to its rate limit." = "Menu bar auto-shows the provider closest to its rate limit."; +"Menu bar metric" = "Menu bar metric"; +"Menu bar shows percent" = "Menu bar shows percent"; +"Menu content" = "Menu content"; +"Merge Icons" = "Merge Icons"; +"Never prompt" = "Never prompt"; +"No" = "No"; +"No Codex accounts detected yet." = "No Codex accounts detected yet."; +"No JetBrains IDE detected" = "No JetBrains IDE detected"; +"No cost history data." = "No cost history data."; +"No credits history data." = "No credits history data."; +"No data available" = "No data available"; +"No data yet" = "No data yet"; +"No enabled providers available for Overview." = "No enabled providers available for Overview."; +"No providers selected" = "No providers selected"; +"No token accounts yet." = "No token accounts yet."; +"No usage breakdown data." = "No usage breakdown data."; +"None" = "None"; +"Notifications" = "Notifications"; +"Notifies when the 5-hour session quota hits 0% and when it becomes " = "Notifies when the 5-hour session quota hits 0% and when it becomes "; +"OK" = "OK"; +"Obscure email addresses in the menu bar and menu UI." = "Obscure email addresses in the menu bar and menu UI."; +"Off" = "Off"; +"Off-peak" = "Off-peak"; +"Off-peak · peak in \\(self.formatDuration(minutes: minutes))" = "Off-peak · peak in \\(self.formatDuration(minutes: minutes))"; +"Offline" = "Offline"; +"On" = "On"; +"Online" = "Online"; +"Only on user action" = "Only on user action"; +"Open" = "Open"; +"Open API Keys" = "Open API Keys"; +"Open Amp Settings" = "Open Amp Settings"; +"Open Antigravity to sign in, then refresh CodexBar." = "Open Antigravity to sign in, then refresh CodexBar."; +"Open Browser" = "Open Browser"; +"Open Coding Plan" = "Open Coding Plan"; +"Open Console" = "Open Console"; +"Open Dashboard" = "Open Dashboard"; +"Open Mistral Admin" = "Open Mistral Admin"; +"Open Ollama Settings" = "Open Ollama Settings"; +"Open Terminal" = "Open Terminal"; +"Open Usage Page" = "Open Usage Page"; +"Open Warp API Key Guide" = "Open Warp API Key Guide"; +"Open menu" = "Open menu"; +"Open token file" = "Open token file"; +"OpenAI cookies" = "OpenAI cookies"; +"OpenAI web extras" = "OpenAI web extras"; +"Option A" = "Option A"; +"Option B" = "Option B"; +"Optional override if workspace lookup fails." = "Optional override if workspace lookup fails."; +"Options" = "Options"; +"Override auto-detection with a custom IDE base path" = "Override auto-detection with a custom IDE base path"; +"Overview" = "Overview"; +"Overview rows always follow provider order." = "Overview rows always follow provider order."; +"Overview tab providers" = "Overview tab providers"; +"Paste API key…" = "Paste API key…"; +"Paste API token…" = "Paste API token…"; +"Paste key…" = "Paste key…"; +"Paste sessionKey or OAuth token…" = "Paste sessionKey or OAuth token…"; +"Paste the Cookie header from a request to admin.mistral.ai. " = "Paste the Cookie header from a request to admin.mistral.ai. "; +"Paste token…" = "Paste token…"; +"Peak · ends in \\(self.formatDuration(minutes: remaining))" = "Peak · ends in \\(self.formatDuration(minutes: remaining))"; +"Personal" = "Personal"; +"Picker" = "Picker"; +"Picker subtitle" = "Picker subtitle"; +"Placeholder" = "Placeholder"; +"Plan" = "Plan"; +"Play full-screen confetti when weekly usage resets." = "Play full-screen confetti when weekly usage resets."; +"Polls OpenAI/Claude status pages and Google Workspace for " = "Polls OpenAI/Claude status pages and Google Workspace for "; +"Prevents any Keychain access while enabled." = "Prevents any Keychain access while enabled."; +"Primary (API key limit)" = "Primary (API key limit)"; +"Primary (\\(label))" = "Primary (\\(label))"; +"Primary (\\(metadata.sessionLabel))" = "Primary (\\(metadata.sessionLabel))"; +"Probe logs" = "Probe logs"; +"Progress bars fill as you consume quota (instead of showing remaining)." = "Progress bars fill as you consume quota (instead of showing remaining)."; +"Provider" = "Provider"; +"Providers" = "Providers"; +"Quit CodexBar" = "Quit CodexBar"; +"Random (default)" = "Random (default)"; +"Reads local usage logs. Shows today + last 30 days cost in the menu." = "Reads local usage logs. Shows today + last 30 days cost in the menu."; +"Refresh" = "Refresh"; +"Refresh cadence" = "Refresh cadence"; +"Remote" = "Remote"; +"Remove" = "Remove"; +"Remove Codex account?" = "Remove Codex account?"; +"Remove \\(account.email) from CodexBar? Its managed Codex home will be deleted." = "Remove \\(account.email) from CodexBar? Its managed Codex home will be deleted."; +"Remove \\(email) from CodexBar? Its managed Codex home will be deleted." = "Remove \\(email) from CodexBar? Its managed Codex home will be deleted."; +"Remove selected account" = "Remove selected account"; +"Replace critter bars with provider branding icons and a percentage." = "Replace critter bars with provider branding icons and a percentage."; +"Replay selected animation" = "Replay selected animation"; +"Requires authentication via GitHub Device Flow." = "Requires authentication via GitHub Device Flow."; +"Resets: \\(reset)" = "Resets: \\(reset)"; +"Rolling five-hour limit" = "Rolling five-hour limit"; +"Search hourly" = "Search hourly"; +"Secondary (\\(label))" = "Secondary (\\(label))"; +"Secondary (\\(metadata.weeklyLabel))" = "Secondary (\\(metadata.weeklyLabel))"; +"Select a provider" = "Select a provider"; +"Select the IDE to monitor" = "Select the IDE to monitor"; +"Session quota notifications" = "Session quota notifications"; +"Session tokens" = "Session tokens"; +"Settings" = "Settings"; +"Show Codex Credits and Claude Extra usage sections in the menu." = "Show Codex Credits and Claude Extra usage sections in the menu."; +"Show Debug Settings" = "Show Debug Settings"; +"Show all token accounts" = "Show all token accounts"; +"Show cost summary" = "Show cost summary"; +"Show credits + extra usage" = "Show credits + extra usage"; +"Show details" = "Show details"; +"Show most-used provider" = "Show most-used provider"; +"Show peak hours indicator" = "Show peak hours indicator"; +"Show provider icons in the switcher (otherwise show a weekly progress line)." = "Show provider icons in the switcher (otherwise show a weekly progress line)."; +"Show reset time as clock" = "Show reset time as clock"; +"Show usage as used" = "Show usage as used"; +"Show whether Claude is in peak usage hours." = "Show whether Claude is in peak usage hours."; +"Sign in via button below" = "Sign in via button below"; +"Skip teardown between probes (debug-only)." = "Skip teardown between probes (debug-only)."; +"Stack token accounts in the menu (otherwise show an account switcher bar)." = "Stack token accounts in the menu (otherwise show an account switcher bar)."; +"Start at Login" = "Start at Login"; +"Status" = "Status"; +"Store Claude sessionKey cookies or OAuth access tokens." = "Store Claude sessionKey cookies or OAuth access tokens."; +"Store multiple Abacus AI Cookie headers." = "Store multiple Abacus AI Cookie headers."; +"Store multiple Augment Cookie headers." = "Store multiple Augment Cookie headers."; +"Store multiple Cursor Cookie headers." = "Store multiple Cursor Cookie headers."; +"Store multiple Factory Cookie headers." = "Store multiple Factory Cookie headers."; +"Store multiple MiniMax Cookie headers." = "Store multiple MiniMax Cookie headers."; +"Store multiple Mistral Cookie headers." = "Store multiple Mistral Cookie headers."; +"Store multiple Ollama Cookie headers." = "Store multiple Ollama Cookie headers."; +"Store multiple OpenCode Cookie headers." = "Store multiple OpenCode Cookie headers."; +"Store multiple OpenCode Go Cookie headers." = "Store multiple OpenCode Go Cookie headers."; +"Stored in the CodexBar config file." = "Stored in the CodexBar config file."; +"Stored in ~/.codexbar/config.json. " = "Stored in ~/.codexbar/config.json. "; +"Stored in ~/.codexbar/config.json. Generate one at kimi-k2.ai." = "Stored in ~/.codexbar/config.json. Generate one at kimi-k2.ai."; +"Stored in ~/.codexbar/config.json. Paste the key from the Synthetic dashboard." = "Stored in ~/.codexbar/config.json. Paste the key from the Synthetic dashboard."; +"Stored in ~/.codexbar/config.json. Paste your Coding Plan API key from Model Studio." = "Stored in ~/.codexbar/config.json. Paste your Coding Plan API key from Model Studio."; +"Stored in ~/.codexbar/config.json. Paste your MiniMax API key." = "Stored in ~/.codexbar/config.json. Paste your MiniMax API key."; +"Stored in ~/.codexbar/config.json. You can also provide KILO_API_KEY or " = "Stored in ~/.codexbar/config.json. You can also provide KILO_API_KEY or "; +"Stores local Codex usage history (8 weeks) to personalize Pace predictions." = "Stores local Codex usage history (8 weeks) to personalize Pace predictions."; +"Subscription Utilization" = "Subscription Utilization"; +"Surprise me" = "Surprise me"; +"Switcher shows icons" = "Switcher shows icons"; +"Symlink CodexBarCLI to /usr/local/bin and /opt/homebrew/bin as codexbar." = "Symlink CodexBarCLI to /usr/local/bin and /opt/homebrew/bin as codexbar."; +"System" = "System"; +"Temporarily shows the loading animation after the next refresh." = "Temporarily shows the loading animation after the next refresh."; +"Tertiary (\\(label))" = "Tertiary (\\(label))"; +"Tertiary (\\(tertiaryTitle))" = "Tertiary (\\(tertiaryTitle))"; +"The default Codex account on this Mac." = "The default Codex account on this Mac."; +"Toggle" = "Toggle"; +"Toggle subtitle" = "Toggle subtitle"; +"Token" = "Token"; +"Trigger the menu bar menu from anywhere." = "Trigger the menu bar menu from anywhere."; +"True" = "True"; +"Twitter" = "Twitter"; +"Unsupported" = "Unsupported"; +"Update Channel" = "Update Channel"; +"Updated" = "Updated"; +"Updates unavailable in this build." = "Updates unavailable in this build."; +"Usage" = "Usage"; +"Usage breakdown" = "Usage breakdown"; +"Usage history (30 days)" = "Usage history (30 days)"; +"Usage source" = "Usage source"; +"Use BigModel for the China mainland endpoints (open.bigmodel.cn)." = "Use BigModel for the China mainland endpoints (open.bigmodel.cn)."; +"Use a single menu bar icon with a provider switcher." = "Use a single menu bar icon with a provider switcher."; +"Use international or China mainland console gateways for quota fetches." = "Use international or China mainland console gateways for quota fetches."; +"Version" = "Version"; +"Version \\(self.versionString)" = "Version \\(self.versionString)"; +"Version \\(version)" = "Version \\(version)"; +"Version \\(versionString)" = "Version \\(versionString)"; +"Vertex AI Login" = "Vertex AI Login"; +"Wait for the current managed Codex login to finish before adding another account." = "Wait for the current managed Codex login to finish before adding another account."; +"Waiting for Authentication..." = "Waiting for Authentication..."; +"Website" = "Website"; +"Weekly limit confetti" = "Weekly limit confetti"; +"Weekly token limit" = "Weekly token limit"; +"Weekly usage" = "Weekly usage"; +"Weekly usage unavailable for this account." = "Weekly usage unavailable for this account."; +"Window: \\(window)" = "Window: \\(window)"; +"Write logs to \\(self.fileLogPath) for debugging." = "Write logs to \\(self.fileLogPath) for debugging."; +"Yes" = "Yes"; +"\\(detail.modelCode): \\(usage)" = "\\(detail.modelCode): \\(usage)"; +"\\(name): \\(truncated)" = "\\(name): \\(truncated)"; +"\\(name): \\(updated) · 30d \\(cost)" = "\\(name): \\(updated) · 30d \\(cost)"; +"\\(name): fetching…\\(elapsed)" = "\\(name): fetching…\\(elapsed)"; +"\\(name): last attempt \\(when)" = "\\(name): last attempt \\(when)"; +"\\(name): no data yet" = "\\(name): no data yet"; +"\\(name): unsupported" = "\\(name): unsupported"; +"all browsers" = "all browsers"; +"available again." = "available again."; +"built_format" = "built_format"; +"copilot_complete_in_browser" = "copilot_complete_in_browser"; +"copilot_device_code" = "copilot_device_code"; +"copilot_device_code_copied" = "copilot_device_code_copied"; +"copilot_verify_at" = "copilot_verify_at"; +"copilot_waiting_text" = "copilot_waiting_text"; +"copilot_window_closes_auto" = "copilot_window_closes_auto"; +"cost_status_error" = "cost_status_error"; +"cost_status_fetching" = "cost_status_fetching"; +"cost_status_last_attempt" = "cost_status_last_attempt"; +"cost_status_no_data" = "cost_status_no_data"; +"cost_status_snapshot" = "cost_status_snapshot"; +"cost_status_unsupported" = "cost_status_unsupported"; +"credits_remaining" = "credits_remaining"; +"cursor_on_demand" = "cursor_on_demand"; +"cursor_on_demand_with_limit" = "cursor_on_demand_with_limit"; +"extra_usage_format" = "extra_usage_format"; +"jetbrains_detected_generate" = "jetbrains_detected_generate"; +"jetbrains_detected_select" = "jetbrains_detected_select"; +"last_fetch_failed" = "last_fetch_failed"; +"last_spend" = "last_spend"; +"mcp_model_usage" = "mcp_model_usage"; +"mcp_resets" = "mcp_resets"; +"mcp_window" = "mcp_window"; +"metric_average" = "metric_average"; +"metric_primary" = "metric_primary"; +"metric_secondary" = "metric_secondary"; +"metric_tertiary" = "metric_tertiary"; +"multiple_workspaces_found" = "multiple_workspaces_found"; +"off_peak" = "off_peak"; +"off_peak_peak_in" = "off_peak_peak_in"; +"ory_session_…=…; csrftoken=…" = "ory_session_…=…; csrftoken=…"; +"overview_choose_providers" = "overview_choose_providers"; +"peak_ends_in" = "peak_ends_in"; +"remove_account_message" = "remove_account_message"; +"version_format" = "version_format"; +"vertex_ai_login_instructions" = "vertex_ai_login_instructions"; +"workspaceID is set but only opencode and opencodego support workspaceID." = "workspaceID is set but only opencode and opencodego support workspaceID."; +"© 2026 Peter Steinberger. MIT License." = "© 2026 Peter Steinberger. MIT License."; + +/* General Pane */ +"section_system" = "System"; +"section_usage" = "Usage"; +"section_automation" = "Automation"; +"language_title" = "Language"; +"language_subtitle" = "Change the display language. Requires app restart to take full effect."; +"language_system" = "System"; +"language_english" = "English"; +"language_chinese_simplified" = "简体中文"; +"start_at_login_title" = "Start at Login"; +"start_at_login_subtitle" = "Automatically opens CodexBar when you start your Mac."; +"show_cost_summary" = "Show cost summary"; +"show_cost_summary_subtitle" = "Reads local usage logs. Shows today + last 30 days cost in the menu."; +"cost_auto_refresh_info" = "Auto-refresh: hourly · Timeout: 10m"; +"refresh_cadence_title" = "Refresh cadence"; +"refresh_cadence_subtitle" = "How often CodexBar polls providers in the background."; +"manual_refresh_hint" = "Auto-refresh is off; use the menu's Refresh command."; +"check_provider_status_title" = "Check provider status"; +"check_provider_status_subtitle" = "Polls OpenAI/Claude status pages and Google Workspace for Gemini/Antigravity, surfacing incidents in the icon and menu."; +"session_quota_notifications_title" = "Session quota notifications"; +"session_quota_notifications_subtitle" = "Notifies when the 5-hour session quota hits 0% and when it becomes available again."; +"quit_app" = "Quit CodexBar"; + +/* Tab titles */ +"tab_general" = "General"; +"tab_providers" = "Providers"; +"tab_display" = "Display"; +"tab_advanced" = "Advanced"; +"tab_about" = "About"; +"tab_debug" = "Debug"; + +/* Providers Pane */ +"select_a_provider" = "Select a provider"; +"cancel" = "Cancel"; +"last_fetch_failed" = "last fetch failed"; +"usage_not_fetched_yet" = "usage not fetched yet"; +"managed_account_storage_unreadable" = "Managed account storage is unreadable. Live account access is still available, but managed add, re-auth, and remove actions are disabled until the store is recoverable."; +"remove_codex_account_title" = "Remove Codex account?"; +"remove" = "Remove"; +"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. Try again after finishing the browser login flow."; +"managed_login_missing_email" = "Codex login completed, but no account email was available. Try again after confirming the account is fully signed in."; +"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"; +"menu_bar_metric_subtitle" = "Choose which window drives the menu bar percent."; +"automatic" = "Automatic"; +"primary_api_key_limit" = "Primary (API key limit)"; + +/* Display Pane */ +"section_menu_bar" = "Menu bar"; +"merge_icons_title" = "Merge Icons"; +"merge_icons_subtitle" = "Use a single menu bar icon with a provider switcher."; +"switcher_shows_icons_title" = "Switcher shows icons"; +"switcher_shows_icons_subtitle" = "Show provider icons in the switcher (otherwise show a weekly progress line)."; +"show_most_used_provider_title" = "Show most-used provider"; +"show_most_used_provider_subtitle" = "Menu bar auto-shows the provider closest to its rate limit."; +"menu_bar_shows_percent_title" = "Menu bar shows percent"; +"menu_bar_shows_percent_subtitle" = "Replace critter bars with provider branding icons and a percentage."; +"display_mode_title" = "Display mode"; +"display_mode_subtitle" = "Choose what to show in the menu bar (Pace shows usage vs. expected)."; +"section_menu_content" = "Menu content"; +"show_usage_as_used_title" = "Show usage as used"; +"show_usage_as_used_subtitle" = "Progress bars fill as you consume quota (instead of showing remaining)."; +"show_reset_time_as_clock_title" = "Show reset time as clock"; +"show_reset_time_as_clock_subtitle" = "Display reset times as absolute clock values instead of countdowns."; +"show_credits_extra_usage_title" = "Show credits + extra usage"; +"show_credits_extra_usage_subtitle" = "Show Codex Credits and Claude Extra usage sections in the menu."; +"show_all_token_accounts_title" = "Show all token accounts"; +"show_all_token_accounts_subtitle" = "Stack token accounts in the menu (otherwise show an account switcher bar)."; +"overview_tab_providers_title" = "Overview tab providers"; +"configure" = "Configure…"; +"overview_enable_merge_icons_hint" = "Enable Merge Icons to configure Overview tab providers."; +"overview_no_providers_hint" = "No enabled providers available for Overview."; +"overview_rows_follow_order" = "Overview rows always follow provider order."; +"overview_no_providers_selected" = "No providers selected"; + +/* Advanced Pane */ +"section_keyboard_shortcut" = "Keyboard shortcut"; +"open_menu_shortcut_title" = "Open menu"; +"open_menu_shortcut_subtitle" = "Trigger the menu bar menu from anywhere."; +"install_cli" = "Install CLI"; +"install_cli_subtitle" = "Symlink CodexBarCLI to /usr/local/bin and /opt/homebrew/bin as codexbar."; +"cli_not_found" = "CodexBarCLI not found in app bundle."; +"no_writable_bin_dirs" = "No writable bin dirs found."; +"show_debug_settings_title" = "Show Debug Settings"; +"show_debug_settings_subtitle" = "Expose troubleshooting tools in the Debug tab."; +"surprise_me_title" = "Surprise me"; +"surprise_me_subtitle" = "Check if you like your agents having some fun up there."; +"weekly_limit_confetti_title" = "Weekly limit confetti"; +"weekly_limit_confetti_subtitle" = "Play full-screen confetti when weekly usage resets."; +"hide_personal_info_title" = "Hide personal information"; +"hide_personal_info_subtitle" = "Obscure email addresses in the menu bar and menu UI."; +"section_keychain_access" = "Keychain access"; +"keychain_access_caption" = "Disable all Keychain reads and writes. Browser cookie import is unavailable; paste Cookie headers manually in Providers."; +"disable_keychain_access_title" = "Disable Keychain access"; +"disable_keychain_access_subtitle" = "Prevents any Keychain access while enabled."; + +/* About Pane */ +"about_tagline" = "May your tokens never run out—keep agent limits in view."; +"link_github" = "GitHub"; +"link_website" = "Website"; +"link_twitter" = "Twitter"; +"link_email" = "Email"; +"check_updates_auto" = "Check for updates automatically"; +"update_channel" = "Update Channel"; +"check_for_updates" = "Check for Updates…"; +"updates_unavailable" = "Updates unavailable in this build."; +"copyright" = "© 2026 Peter Steinberger. MIT License."; + +/* Debug Pane */ +"section_logging" = "Logging"; +"enable_file_logging" = "Enable file logging"; +"enable_file_logging_subtitle" = "Write logs to %@ for debugging."; +"verbosity_title" = "Verbosity"; +"verbosity_subtitle" = "Controls how much detail is logged."; +"open_log_file" = "Open log file"; +"force_animation_next_refresh" = "Force animation on next refresh"; +"force_animation_next_refresh_subtitle" = "Temporarily shows the loading animation after the next refresh."; +"section_loading_animations" = "Loading animations"; +"loading_animations_caption" = "Pick a pattern and replay it in the menu bar. \"Random\" keeps the existing behavior."; +"animation_random_default" = "Random (default)"; +"replay_selected_animation" = "Replay selected animation"; +"blink_now" = "Blink now"; +"section_probe_logs" = "Probe logs"; +"probe_logs_caption" = "Fetch the latest probe output for debugging; Copy keeps the full text."; +"fetch_log" = "Fetch log"; +"copy" = "Copy"; +"save_to_file" = "Save to file"; +"load_parse_dump" = "Load parse dump"; +"rerun_provider_autodetect" = "Re-run provider autodetect"; +"loading" = "Loading…"; +"no_log_yet_fetch" = "No log yet. Fetch to load."; +"section_fetch_strategy" = "Fetch strategy attempts"; +"fetch_strategy_caption" = "Last fetch pipeline decisions and errors for a provider."; +"section_openai_cookies" = "OpenAI cookies"; +"openai_cookies_caption" = "Cookie import + WebKit scrape logs from the last OpenAI cookies attempt."; +"no_log_yet" = "No log yet. Update OpenAI cookies in Providers → Codex to run an import."; +"section_caches" = "Caches"; +"caches_caption" = "Clear cached cost scan results."; +"clear_cost_cache" = "Clear cost cache"; +"section_notifications" = "Notifications"; +"notifications_caption" = "Trigger test notifications for the 5-hour session window (depleted/restored)."; +"post_depleted" = "Post depleted"; +"post_restored" = "Post restored"; +"section_cli_sessions" = "CLI sessions"; +"cli_sessions_caption" = "Keep Codex/Claude CLI sessions alive after a probe. Default exits once data is captured."; +"keep_cli_sessions_alive" = "Keep CLI sessions alive"; +"keep_cli_sessions_alive_subtitle" = "Skip teardown between probes (debug-only)."; +"reset_cli_sessions" = "Reset CLI sessions"; +"section_error_simulation" = "Error simulation"; +"error_simulation_caption" = "Inject a fake error message into the menu card for layout testing."; +"set_menu_error" = "Set menu error"; +"clear_menu_error" = "Clear menu error"; +"set_cost_error" = "Set cost error"; +"clear_cost_error" = "Clear cost error"; +"section_cli_paths" = "CLI paths"; +"cli_paths_caption" = "Resolved Codex binary and PATH layers; startup login PATH capture (short timeout)."; +"codex_binary" = "Codex binary"; +"claude_binary" = "Claude binary"; +"effective_path" = "Effective PATH"; +"unavailable" = "Unavailable"; +"login_shell_path" = "Login shell PATH (startup capture)"; +"cleared" = "Cleared."; +"no_fetch_attempts" = "No fetch attempts yet."; + +/* Metric preferences */ +"metric_pref_automatic" = "Automatic"; +"metric_pref_primary" = "Primary"; +"metric_pref_secondary" = "Secondary"; +"metric_pref_tertiary" = "Tertiary"; +"metric_pref_extra_usage" = "Extra usage"; +"metric_pref_average" = "Average"; + +/* Display modes */ +"display_mode_percent" = "Percent"; +"display_mode_pace" = "Pace"; +"display_mode_both" = "Both"; +"display_mode_percent_desc" = "Show remaining/used percentage (e.g. 45%)"; +"display_mode_pace_desc" = "Show pace indicator (e.g. +5%)"; +"display_mode_both_desc" = "Show both percentage and pace (e.g. 45% · +5%)"; + +/* Provider status */ +"status_operational" = "Operational"; +"status_partial_outage" = "Partial outage"; +"status_major_outage" = "Major outage"; +"status_critical_issue" = "Critical issue"; +"status_maintenance" = "Maintenance"; +"status_unknown" = "Status unknown"; + +/* Refresh frequency */ +"refresh_manual" = "Manual"; +"refresh_1min" = "1 min"; +"refresh_2min" = "2 min"; +"refresh_5min" = "5 min"; +"refresh_15min" = "15 min"; +"refresh_30min" = "30 min"; diff --git a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings new file mode 100644 index 000000000..dbf06308b --- /dev/null +++ b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,602 @@ +/* Chinese (Simplified) localization for CodexBar */ + +" providers" = ""; +"(System)" = ""; +"30d" = ""; +"A managed Codex login is already running. Wait for it to finish before adding " = ""; +"API key" = ""; +"API region" = ""; +"API token" = ""; +"API tokens" = ""; +"About" = ""; +"Account" = ""; +"Accounts" = ""; +"Accounts subtitle" = ""; +"Active" = ""; +"Add" = ""; +"Add Workspace" = "添加工作区"; +"Advanced" = ""; +"All" = ""; +"Always allow prompts" = ""; +"Animation pattern" = ""; +"Antigravity login is managed in the app" = ""; +"Applies only to the Security.framework OAuth keychain reader." = ""; +"Auto falls back to the next source if the preferred one fails." = ""; +"Auto uses API first, then falls back to CLI on auth failures." = ""; +"Auto-detect" = ""; +"Auto-refresh is off; use the menu's Refresh command." = ""; +"Auto-refresh: hourly · Timeout: 10m" = ""; +"Automatic" = ""; +"Automatic imports browser cookies and WorkOS tokens." = ""; +"Automatic imports browser cookies and local storage tokens." = ""; +"Automatic imports browser cookies for dashboard extras." = ""; +"Automatic imports browser cookies for the web API." = ""; +"Automatic imports browser cookies from Model Studio/Bailian." = ""; +"Automatic imports browser cookies from admin.mistral.ai." = ""; +"Automatic imports browser cookies from opencode.ai." = ""; +"Automatic imports browser cookies or stored sessions." = ""; +"Automatic imports browser cookies." = ""; +"Automatically imports browser session cookie." = ""; +"Automatically opens CodexBar when you start your Mac." = ""; +"Automation" = ""; +"Average (\\(label1) + \\(label2))" = ""; +"Average (\\(metadata.sessionLabel) + \\(metadata.weeklyLabel))" = ""; +"Avoid Keychain prompts" = ""; +"Balance" = ""; +"Battery Saver" = ""; +"Bordered" = ""; +"Build" = ""; +"Built \\(buildTimestamp)" = ""; +"Buy Credits..." = ""; +"Buy Credits…" = ""; +"CLI paths" = ""; +"CLI sessions" = ""; +"Caches" = ""; +"Cancel" = "取消"; +"Check for Updates…" = ""; +"Check for updates automatically" = ""; +"Check if you like your agents having some fun up there." = ""; +"Check provider status" = ""; +"Choose Codex workspace" = "选择 Codex 工作区"; +"Choose the MiniMax host (global .io or China mainland .com)." = ""; +"Choose up to " = ""; +"Choose up to \\(Self.maxOverviewProviders) providers" = ""; +"Choose up to \\(count) providers" = ""; +"Choose what to show in the menu bar (Pace shows usage vs. expected)." = ""; +"Choose which Codex account CodexBar should follow." = ""; +"Choose which window drives the menu bar percent." = ""; +"Chrome" = ""; +"Claude CLI not found" = ""; +"Claude binary" = ""; +"Claude cookies" = ""; +"Claude login failed" = ""; +"Claude login timed out" = ""; +"Close" = ""; +"Code review" = ""; +"Codex CLI not found" = ""; +"Codex account login already running" = ""; +"Codex binary" = ""; +"Codex login failed" = ""; +"Codex login timed out" = ""; +"CodexBar Lifecycle Keepalive" = ""; +"CodexBar could not read managed account storage. " = ""; +"Configure…" = ""; +"Connected" = ""; +"Controls how much detail is logged." = ""; +"Cookie header" = ""; +"Cookie source" = ""; +"Cookie: ..." = ""; +"Cookie: \\u{2026}\\\n\\\nor paste a cURL capture from the Abacus AI dashboard" = ""; +"Cookie: \\u{2026}\\\n\\\nor paste the __Secure-next-auth.session-token value" = ""; +"Cookie: \\u{2026}\\\n\\\nor paste the kimi-auth token value" = ""; +"Cookie: …" = ""; +"CopilotDeviceFlow" = ""; +"Cost" = ""; +"Could not add Codex account" = ""; +"Could not open Terminal for Gemini" = ""; +"Could not start claude /login" = ""; +"Could not start codex login" = ""; +"Could not switch system account" = ""; +"Credits" = ""; +"Credits history" = ""; +"Cursor login failed" = ""; +"Custom" = ""; +"Custom Path" = ""; +"Daily Routines" = ""; +"Debug" = ""; +"Default" = ""; +"Designs" = ""; +"Disable Keychain access" = ""; +"Disabled" = ""; +"Disconnected" = ""; +"Display" = ""; +"Display mode" = ""; +"Display reset times as absolute clock values instead of countdowns." = ""; +"Done" = ""; +"Effective PATH" = ""; +"Email" = ""; +"Enable Merge Icons to configure Overview tab providers." = ""; +"Enable file logging" = ""; +"Enabled" = ""; +"Error" = ""; +"Error simulation" = ""; +"Expose troubleshooting tools in the Debug tab." = ""; +"Failed" = ""; +"False" = ""; +"Fetch strategy attempts" = ""; +"Fetching" = ""; +"Field" = ""; +"Field subtitle" = ""; +"Finish the current managed account change before switching the system account." = ""; +"Force animation on next refresh" = ""; +"Gateway region" = ""; +"Gemini CLI not found" = ""; +"Gemini/Antigravity, surfacing incidents in the icon and menu." = ""; +"General" = ""; +"GitHub" = ""; +"GitHub Copilot Login" = "GitHub Copilot 登录"; +"GitHub Login" = ""; +"Hide details" = "隐藏详情"; +"Hide personal information" = ""; +"Historical tracking" = ""; +"How often CodexBar polls providers in the background." = ""; +"Inactive" = ""; +"Install CLI" = ""; +"Install the Claude CLI (npm i -g @anthropic-ai/claude-code) and try again." = ""; +"Install the Codex CLI (npm i -g @openai/codex) and try again." = ""; +"Install the Gemini CLI (npm i -g @google/gemini-cli) and try again." = ""; +"JetBrains AI is ready" = "JetBrains AI 已就绪"; +"JetBrains IDE" = ""; +"Keep CLI sessions alive" = ""; +"Keyboard shortcut" = ""; +"Keychain access" = ""; +"Keychain prompt policy" = ""; +"Last \\(name) fetch failed:" = ""; +"Last \\(self.store.metadata(for: self.provider).displayName) fetch failed:" = ""; +"Last attempt" = ""; +"Link" = ""; +"Loading animations" = ""; +"Loading…" = ""; +"Local" = ""; +"Logging" = ""; +"Login failed" = ""; +"Login shell PATH (startup capture)" = ""; +"Login timed out" = ""; +"MCP details" = ""; +"Managed Codex accounts unavailable" = ""; +"Managed account storage is unreadable. Live account access is still available, " = ""; +"Manual" = ""; +"May your tokens never run out—keep agent limits in view." = ""; +"Menu bar" = ""; +"Menu bar auto-shows the provider closest to its rate limit." = ""; +"Menu bar metric" = ""; +"Menu bar shows percent" = ""; +"Menu content" = ""; +"Merge Icons" = ""; +"Never prompt" = ""; +"No" = ""; +"No Codex accounts detected yet." = ""; +"No JetBrains IDE detected" = ""; +"No cost history data." = ""; +"No credits history data." = ""; +"No data available" = ""; +"No data yet" = ""; +"No enabled providers available for Overview." = ""; +"No providers selected" = ""; +"No token accounts yet." = ""; +"No usage breakdown data." = ""; +"None" = ""; +"Notifications" = ""; +"Notifies when the 5-hour session quota hits 0% and when it becomes " = ""; +"OK" = "确定"; +"Obscure email addresses in the menu bar and menu UI." = ""; +"Off" = ""; +"Off-peak" = ""; +"Off-peak · peak in \\(self.formatDuration(minutes: minutes))" = ""; +"Offline" = ""; +"On" = ""; +"Online" = ""; +"Only on user action" = ""; +"Open" = ""; +"Open API Keys" = ""; +"Open Amp Settings" = ""; +"Open Antigravity to sign in, then refresh CodexBar." = ""; +"Open Browser" = "打开浏览器"; +"Open Coding Plan" = ""; +"Open Console" = ""; +"Open Dashboard" = ""; +"Open Mistral Admin" = ""; +"Open Ollama Settings" = ""; +"Open Terminal" = "打开终端"; +"Open Usage Page" = ""; +"Open Warp API Key Guide" = ""; +"Open menu" = ""; +"Open token file" = ""; +"OpenAI cookies" = ""; +"OpenAI web extras" = ""; +"Option A" = ""; +"Option B" = ""; +"Optional override if workspace lookup fails." = ""; +"Options" = ""; +"Override auto-detection with a custom IDE base path" = ""; +"Overview" = ""; +"Overview rows always follow provider order." = ""; +"Overview tab providers" = ""; +"Paste API key…" = ""; +"Paste API token…" = ""; +"Paste key…" = ""; +"Paste sessionKey or OAuth token…" = ""; +"Paste the Cookie header from a request to admin.mistral.ai. " = ""; +"Paste token…" = ""; +"Peak · ends in \\(self.formatDuration(minutes: remaining))" = ""; +"Personal" = ""; +"Picker" = ""; +"Picker subtitle" = ""; +"Placeholder" = ""; +"Plan" = ""; +"Play full-screen confetti when weekly usage resets." = ""; +"Polls OpenAI/Claude status pages and Google Workspace for " = ""; +"Prevents any Keychain access while enabled." = ""; +"Primary (API key limit)" = ""; +"Primary (\\(label))" = ""; +"Primary (\\(metadata.sessionLabel))" = ""; +"Probe logs" = ""; +"Progress bars fill as you consume quota (instead of showing remaining)." = ""; +"Provider" = ""; +"Providers" = ""; +"Quit CodexBar" = ""; +"Random (default)" = ""; +"Reads local usage logs. Shows today + last 30 days cost in the menu." = ""; +"Refresh" = ""; +"Refresh cadence" = ""; +"Remote" = ""; +"Remove" = ""; +"Remove Codex account?" = ""; +"Remove \\(account.email) from CodexBar? Its managed Codex home will be deleted." = ""; +"Remove \\(email) from CodexBar? Its managed Codex home will be deleted." = ""; +"Remove selected account" = ""; +"Replace critter bars with provider branding icons and a percentage." = ""; +"Replay selected animation" = ""; +"Requires authentication via GitHub Device Flow." = ""; +"Resets: \\(reset)" = ""; +"Rolling five-hour limit" = ""; +"Search hourly" = ""; +"Secondary (\\(label))" = ""; +"Secondary (\\(metadata.weeklyLabel))" = ""; +"Select a provider" = ""; +"Select the IDE to monitor" = ""; +"Session quota notifications" = ""; +"Session tokens" = ""; +"Settings" = ""; +"Show Codex Credits and Claude Extra usage sections in the menu." = ""; +"Show Debug Settings" = ""; +"Show all token accounts" = ""; +"Show cost summary" = ""; +"Show credits + extra usage" = ""; +"Show details" = "显示详情"; +"Show most-used provider" = ""; +"Show peak hours indicator" = ""; +"Show provider icons in the switcher (otherwise show a weekly progress line)." = ""; +"Show reset time as clock" = ""; +"Show usage as used" = ""; +"Show whether Claude is in peak usage hours." = ""; +"Sign in via button below" = ""; +"Skip teardown between probes (debug-only)." = ""; +"Stack token accounts in the menu (otherwise show an account switcher bar)." = ""; +"Start at Login" = ""; +"Status" = ""; +"Store Claude sessionKey cookies or OAuth access tokens." = ""; +"Store multiple Abacus AI Cookie headers." = ""; +"Store multiple Augment Cookie headers." = ""; +"Store multiple Cursor Cookie headers." = ""; +"Store multiple Factory Cookie headers." = ""; +"Store multiple MiniMax Cookie headers." = ""; +"Store multiple Mistral Cookie headers." = ""; +"Store multiple Ollama Cookie headers." = ""; +"Store multiple OpenCode Cookie headers." = ""; +"Store multiple OpenCode Go Cookie headers." = ""; +"Stored in the CodexBar config file." = ""; +"Stored in ~/.codexbar/config.json. " = ""; +"Stored in ~/.codexbar/config.json. Generate one at kimi-k2.ai." = ""; +"Stored in ~/.codexbar/config.json. Paste the key from the Synthetic dashboard." = ""; +"Stored in ~/.codexbar/config.json. Paste your Coding Plan API key from Model Studio." = ""; +"Stored in ~/.codexbar/config.json. Paste your MiniMax API key." = ""; +"Stored in ~/.codexbar/config.json. You can also provide KILO_API_KEY or " = ""; +"Stores local Codex usage history (8 weeks) to personalize Pace predictions." = ""; +"Subscription Utilization" = ""; +"Surprise me" = "给我惊喜"; +"Switcher shows icons" = ""; +"Symlink CodexBarCLI to /usr/local/bin and /opt/homebrew/bin as codexbar." = ""; +"System" = ""; +"Temporarily shows the loading animation after the next refresh." = ""; +"Tertiary (\\(label))" = ""; +"Tertiary (\\(tertiaryTitle))" = ""; +"The default Codex account on this Mac." = ""; +"Toggle" = ""; +"Toggle subtitle" = ""; +"Token" = ""; +"Trigger the menu bar menu from anywhere." = ""; +"True" = ""; +"Twitter" = ""; +"Unsupported" = ""; +"Update Channel" = ""; +"Updated" = ""; +"Updates unavailable in this build." = ""; +"Usage" = ""; +"Usage breakdown" = ""; +"Usage history (30 days)" = ""; +"Usage source" = ""; +"Use BigModel for the China mainland endpoints (open.bigmodel.cn)." = ""; +"Use a single menu bar icon with a provider switcher." = ""; +"Use international or China mainland console gateways for quota fetches." = ""; +"Version" = ""; +"Version \\(self.versionString)" = ""; +"Version \\(version)" = ""; +"Version \\(versionString)" = ""; +"Vertex AI Login" = "Vertex AI 登录"; +"Wait for the current managed Codex login to finish before adding another account." = ""; +"Waiting for Authentication..." = "等待认证…"; +"Website" = ""; +"Weekly limit confetti" = ""; +"Weekly token limit" = ""; +"Weekly usage" = ""; +"Weekly usage unavailable for this account." = "此账户的每周用量不可用。"; +"Window: \\(window)" = ""; +"Write logs to \\(self.fileLogPath) for debugging." = "将日志写入 \\(self.fileLogPath) 用于调试。"; +"Yes" = ""; +"\\(detail.modelCode): \\(usage)" = ""; +"\\(name): \\(truncated)" = ""; +"\\(name): \\(updated) · 30d \\(cost)" = ""; +"\\(name): fetching…\\(elapsed)" = ""; +"\\(name): last attempt \\(when)" = ""; +"\\(name): no data yet" = ""; +"\\(name): unsupported" = ""; +"all browsers" = ""; +"available again." = ""; +"built_format" = "构建于 %@"; +"copilot_complete_in_browser" = ""; +"copilot_device_code" = "设备代码已复制到剪贴板:%1$@ + +请在以下地址验证:%2$@"; +"copilot_device_code_copied" = ""; +"copilot_verify_at" = ""; +"copilot_waiting_text" = "请在浏览器中完成登录。 +完成后此窗口将自动关闭。"; +"copilot_window_closes_auto" = ""; +"cost_status_error" = "%@:%@"; +"cost_status_fetching" = "%1$@:获取中…%2$@"; +"cost_status_last_attempt" = "%1$@:上次尝试 %2$@"; +"cost_status_no_data" = "%@:暂无数据"; +"cost_status_snapshot" = "%1$@:%2$@ · 30天 %3$@"; +"cost_status_unsupported" = "%@:不支持"; +"credits_remaining" = "积分:%@"; +"cursor_on_demand" = "按需计费:%@"; +"cursor_on_demand_with_limit" = "按需计费:%1$@ / %2$@"; +"extra_usage_format" = "额外用量:%1$@ / %2$@"; +"jetbrains_detected_generate" = "检测到:%@。使用一次 AI 助手以生成配额数据,然后刷新 CodexBar。"; +"jetbrains_detected_select" = "检测到:%@。在设置中选择您偏好的 IDE,然后刷新 CodexBar。"; +"last_fetch_failed" = "上次获取 %1$@ 失败:"; +"last_spend" = "上次消耗:%@"; +"mcp_model_usage" = "%1$@:%2$@"; +"mcp_resets" = "重置:%@"; +"mcp_window" = "窗口:%@"; +"metric_average" = "平均(%1$@ + %2$@)"; +"metric_primary" = "主要(%@)"; +"metric_secondary" = "次要(%@)"; +"metric_tertiary" = "第三(%@)"; +"multiple_workspaces_found" = "CodexBar 发现了 %1$@ 的多个工作区。请选择要添加的工作区。"; +"ory_session_…=…; csrftoken=…" = ""; +"overview_choose_providers" = "选择最多 %1$@ 个提供商"; +"remove_account_message" = "从 CodexBar 中移除 %@?其托管的 Codex 主目录将被删除。"; +"version_format" = "版本 %@"; +"vertex_ai_login_instructions" = "要使用 Vertex AI 跟踪,您需要通过 Google Cloud 进行认证。 + +1. 打开终端 +2. 运行:gcloud auth application-default login +3. 按照浏览器提示登录 +4. 设置项目:gcloud config set project PROJECT_ID + +是否现在打开终端?"; +"workspaceID is set but only opencode and opencodego support workspaceID." = ""; +"© 2026 Peter Steinberger. MIT License." = ""; + +/* General Pane */ +"section_system" = "系统"; +"section_usage" = "用量"; +"section_automation" = "自动化"; +"language_title" = "语言"; +"language_subtitle" = "更改显示语言。需要重启应用才能完全生效。"; +"language_system" = "跟随系统"; +"language_english" = "English"; +"language_chinese_simplified" = "简体中文"; +"start_at_login_title" = "开机启动"; +"start_at_login_subtitle" = "启动 Mac 时自动打开 CodexBar。"; +"show_cost_summary" = "显示费用摘要"; +"show_cost_summary_subtitle" = "读取本地使用日志。在菜单中显示今天及最近30天的费用。"; +"cost_auto_refresh_info" = "自动刷新:每小时 · 超时:10分钟"; +"refresh_cadence_title" = "刷新频率"; +"refresh_cadence_subtitle" = "CodexBar 在后台轮询提供商的频率。"; +"manual_refresh_hint" = "自动刷新已关闭;请使用菜单中的刷新命令。"; +"check_provider_status_title" = "检查提供商状态"; +"check_provider_status_subtitle" = "轮询 OpenAI/Claude 状态页面和 Google Workspace 的 Gemini/Antigravity,在图标和菜单中显示故障信息。"; +"session_quota_notifications_title" = "会话配额通知"; +"session_quota_notifications_subtitle" = "当5小时会话配额用完及恢复时发送通知。"; +"quit_app" = "退出 CodexBar"; + +/* Tab titles */ +"tab_general" = "通用"; +"tab_providers" = "提供商"; +"tab_display" = "显示"; +"tab_advanced" = "高级"; +"tab_about" = "关于"; +"tab_debug" = "调试"; + +/* Providers Pane */ +"select_a_provider" = "选择一个提供商"; +"cancel" = "取消"; +"last_fetch_failed" = "上次获取失败"; +"usage_not_fetched_yet" = "尚未获取用量"; +"managed_account_storage_unreadable" = "托管账户存储不可读。实时账户访问仍可用,但托管的添加、重新认证和移除操作已被禁用,直到存储恢复。"; +"remove_codex_account_title" = "移除 Codex 账户?"; +"remove" = "移除"; +"managed_login_already_running" = "托管 Codex 登录已在运行。请等待完成后再添加或重新认证其他账户。"; +"managed_login_failed" = "托管 Codex 登录未完成。请在完成浏览器登录流程后重试。"; +"managed_login_missing_email" = "Codex 登录已完成,但无法获取账户邮箱。请在确认账户已完全登录后重试。"; +"workspace_selection_cancelled" = "CodexBar 发现多个工作区,但未选择任何工作区。"; +"unsafe_managed_home" = "CodexBar 拒绝修改意外的托管主目录路径:%@"; +"menu_bar_metric_title" = "菜单栏指标"; +"menu_bar_metric_subtitle" = "选择哪个窗口驱动菜单栏百分比。"; +"automatic" = "自动"; +"primary_api_key_limit" = "主要(API 密钥限制)"; + +/* Display Pane */ +"section_menu_bar" = "菜单栏"; +"merge_icons_title" = "合并图标"; +"merge_icons_subtitle" = "使用单个菜单栏图标并带提供商切换器。"; +"switcher_shows_icons_title" = "切换器显示图标"; +"switcher_shows_icons_subtitle" = "在切换器中显示提供商图标(否则显示每周进度线)。"; +"show_most_used_provider_title" = "显示用量最高的提供商"; +"show_most_used_provider_subtitle" = "菜单栏自动显示最接近速率限制的提供商。"; +"menu_bar_shows_percent_title" = "菜单栏显示百分比"; +"menu_bar_shows_percent_subtitle" = "将动态条替换为提供商品牌图标和百分比。"; +"display_mode_title" = "显示模式"; +"display_mode_subtitle" = "选择在菜单栏中显示的内容(节奏显示用量与预期的对比)。"; +"section_menu_content" = "菜单内容"; +"show_usage_as_used_title" = "显示已使用用量"; +"show_usage_as_used_subtitle" = "进度条随用量消耗而填充(而非显示剩余量)。"; +"show_reset_time_as_clock_title" = "将重置时间显示为时钟"; +"show_reset_time_as_clock_subtitle" = "将重置时间显示为绝对时钟值而非倒计时。"; +"show_credits_extra_usage_title" = "显示积分 + 额外用量"; +"show_credits_extra_usage_subtitle" = "在菜单中显示 Codex 积分和 Claude 额外用量部分。"; +"show_all_token_accounts_title" = "显示所有令牌账户"; +"show_all_token_accounts_subtitle" = "在菜单中堆叠令牌账户(否则显示账户切换栏)。"; +"overview_tab_providers_title" = "概览标签提供商"; +"configure" = "配置…"; +"overview_enable_merge_icons_hint" = "启用合并图标以配置概览标签提供商。"; +"overview_no_providers_hint" = "概览没有可用的已启用提供商。"; +"overview_rows_follow_order" = "概览行始终遵循提供商顺序。"; +"overview_no_providers_selected" = "未选择提供商"; + +/* Advanced Pane */ +"section_keyboard_shortcut" = "快捷键"; +"open_menu_shortcut_title" = "打开菜单"; +"open_menu_shortcut_subtitle" = "从任意位置触发菜单栏菜单。"; +"install_cli" = "安装 CLI"; +"install_cli_subtitle" = "将 CodexBarCLI 符号链接到 /usr/local/bin 和 /opt/homebrew/bin 作为 codexbar。"; +"cli_not_found" = "在应用包中未找到 CodexBarCLI。"; +"no_writable_bin_dirs" = "未找到可写的 bin 目录。"; +"show_debug_settings_title" = "显示调试设置"; +"show_debug_settings_subtitle" = "在调试标签中显示故障排除工具。"; +"surprise_me_title" = "给我惊喜"; +"surprise_me_subtitle" = "看看你是否喜欢你的智能体在上面找点乐子。"; +"weekly_limit_confetti_title" = "每周限制彩纸"; +"weekly_limit_confetti_subtitle" = "当每周用量重置时播放全屏彩纸。"; +"hide_personal_info_title" = "隐藏个人信息"; +"hide_personal_info_subtitle" = "在菜单栏和菜单界面中隐藏电子邮件地址。"; +"section_keychain_access" = "钥匙串访问"; +"keychain_access_caption" = "禁用所有钥匙串读写。浏览器 cookie 导入不可用;在提供商中手动粘贴 Cookie 标头。"; +"disable_keychain_access_title" = "禁用钥匙串访问"; +"disable_keychain_access_subtitle" = "启用时阻止任何钥匙串访问。"; + +/* About Pane */ +"about_tagline" = "愿你的令牌永不耗尽——时刻关注智能体限制。"; +"link_github" = "GitHub"; +"link_website" = "网站"; +"link_twitter" = "Twitter"; +"link_email" = "电子邮件"; +"check_updates_auto" = "自动检查更新"; +"update_channel" = "更新频道"; +"check_for_updates" = "检查更新…"; +"updates_unavailable" = "此构建中更新不可用。"; +"copyright" = "© 2026 Peter Steinberger. MIT License."; + +/* Debug Pane */ +"section_logging" = "日志"; +"enable_file_logging" = "启用文件日志"; +"enable_file_logging_subtitle" = "将日志写入 %@ 以进行调试。"; +"verbosity_title" = "详细程度"; +"verbosity_subtitle" = "控制记录多少详细信息。"; +"open_log_file" = "打开日志文件"; +"force_animation_next_refresh" = "下次刷新时强制动画"; +"force_animation_next_refresh_subtitle" = "下次刷新后临时显示加载动画。"; +"section_loading_animations" = "加载动画"; +"loading_animations_caption" = "选择一个模式并在菜单栏中重放。\"随机\"保持现有行为。"; +"animation_random_default" = "随机(默认)"; +"replay_selected_animation" = "重放选中的动画"; +"blink_now" = "立即闪烁"; +"section_probe_logs" = "探测日志"; +"probe_logs_caption" = "获取最新的探测输出以进行调试;复制保留完整文本。"; +"fetch_log" = "获取日志"; +"copy" = "复制"; +"save_to_file" = "保存到文件"; +"load_parse_dump" = "加载解析转储"; +"rerun_provider_autodetect" = "重新运行提供商自动检测"; +"loading" = "加载中…"; +"no_log_yet_fetch" = "尚无日志。获取以加载。"; +"section_fetch_strategy" = "获取策略尝试"; +"fetch_strategy_caption" = "提供商的上次获取流水线决策和错误。"; +"section_openai_cookies" = "OpenAI Cookie"; +"openai_cookies_caption" = "上次 OpenAI Cookie 尝试的 Cookie 导入 + WebKit 抓取日志。"; +"no_log_yet" = "尚无日志。在提供商 → Codex 中更新 OpenAI Cookie 以运行导入。"; +"section_caches" = "缓存"; +"caches_caption" = "清除缓存的费用扫描结果。"; +"clear_cost_cache" = "清除费用缓存"; +"section_notifications" = "通知"; +"notifications_caption" = "触发 5 小时会话窗口的测试通知(耗尽/恢复)。"; +"post_depleted" = "发布耗尽通知"; +"post_restored" = "发布恢复通知"; +"section_cli_sessions" = "CLI 会话"; +"cli_sessions_caption" = "探测后保持 Codex/Claude CLI 会话存活。默认在捕获数据后退出。"; +"keep_cli_sessions_alive" = "保持 CLI 会话存活"; +"keep_cli_sessions_alive_subtitle" = "探测之间跳过拆卸(仅限调试)。"; +"reset_cli_sessions" = "重置 CLI 会话"; +"section_error_simulation" = "错误模拟"; +"error_simulation_caption" = "将假错误消息注入菜单卡片以进行布局测试。"; +"set_menu_error" = "设置菜单错误"; +"clear_menu_error" = "清除菜单错误"; +"set_cost_error" = "设置费用错误"; +"clear_cost_error" = "清除费用错误"; +"section_cli_paths" = "CLI 路径"; +"cli_paths_caption" = "解析的 Codex 二进制文件和 PATH 层;启动时登录 PATH 捕获(短超时)。"; +"codex_binary" = "Codex 二进制文件"; +"claude_binary" = "Claude 二进制文件"; +"effective_path" = "有效 PATH"; +"unavailable" = "不可用"; +"login_shell_path" = "登录 shell PATH(启动捕获)"; +"cleared" = "已清除。"; +"no_fetch_attempts" = "尚无获取尝试。"; + +/* Metric preferences */ +"metric_pref_automatic" = "自动"; +"metric_pref_primary" = "主要"; +"metric_pref_secondary" = "次要"; +"metric_pref_tertiary" = "第三"; +"metric_pref_extra_usage" = "额外用量"; +"metric_pref_average" = "平均"; + +/* Display modes */ +"display_mode_percent" = "百分比"; +"display_mode_pace" = "节奏"; +"display_mode_both" = "两者"; +"display_mode_percent_desc" = "显示剩余/已使用百分比(例如 45%)"; +"display_mode_pace_desc" = "显示节奏指示器(例如 +5%)"; +"display_mode_both_desc" = "同时显示百分比和节奏(例如 45% · +5%)"; + +/* Provider status */ +"status_operational" = "正常运行"; +"status_partial_outage" = "部分中断"; +"status_major_outage" = "重大中断"; +"status_critical_issue" = "严重问题"; +"status_maintenance" = "维护中"; +"status_unknown" = "状态未知"; + +/* Refresh frequency */ +"refresh_manual" = "手动"; +"refresh_1min" = "1 分钟"; +"refresh_2min" = "2 分钟"; +"refresh_5min" = "5 分钟"; +"refresh_15min" = "15 分钟"; +"refresh_30min" = "30 分钟"; + +/* Additional keys */ +"not_found" = "未找到"; diff --git a/Sources/CodexBar/SettingsStore+Defaults.swift b/Sources/CodexBar/SettingsStore+Defaults.swift index c4705bcc6..d199f5285 100644 --- a/Sources/CodexBar/SettingsStore+Defaults.swift +++ b/Sources/CodexBar/SettingsStore+Defaults.swift @@ -505,6 +505,21 @@ extension SettingsStore { } } + var appLanguage: String { + get { self.defaultsState.appLanguageRaw ?? "" } + set { + let stored = newValue.isEmpty ? nil : newValue + self.defaultsState.appLanguageRaw = stored + if let stored { + self.userDefaults.set(stored, forKey: "appLanguage") + UserDefaults.standard.set([stored], forKey: "AppleLanguages") + } else { + self.userDefaults.removeObject(forKey: "appLanguage") + UserDefaults.standard.removeObject(forKey: "AppleLanguages") + } + } + } + var debugLoadingPattern: LoadingPattern? { get { self.debugLoadingPatternRaw.flatMap(LoadingPattern.init(rawValue:)) } set { self.debugLoadingPatternRaw = newValue?.rawValue } diff --git a/Sources/CodexBar/SettingsStore.swift b/Sources/CodexBar/SettingsStore.swift index 6d3e76e4f..80bfdfc4c 100644 --- a/Sources/CodexBar/SettingsStore.swift +++ b/Sources/CodexBar/SettingsStore.swift @@ -28,12 +28,12 @@ enum RefreshFrequency: String, CaseIterable, Identifiable { var label: String { switch self { - case .manual: "Manual" - case .oneMinute: "1 min" - case .twoMinutes: "2 min" - case .fiveMinutes: "5 min" - case .fifteenMinutes: "15 min" - case .thirtyMinutes: "30 min" + case .manual: L("refresh_manual") + case .oneMinute: L("refresh_1min") + case .twoMinutes: L("refresh_2min") + case .fiveMinutes: L("refresh_5min") + case .fifteenMinutes: L("refresh_15min") + case .thirtyMinutes: L("refresh_30min") } } } @@ -52,12 +52,12 @@ enum MenuBarMetricPreference: String, CaseIterable, Identifiable { var label: String { switch self { - case .automatic: "Automatic" - case .primary: "Primary" - case .secondary: "Secondary" - case .tertiary: "Tertiary" - case .extraUsage: "Extra usage" - case .average: "Average" + case .automatic: L("metric_pref_automatic") + case .primary: L("metric_pref_primary") + case .secondary: L("metric_pref_secondary") + case .tertiary: L("metric_pref_tertiary") + case .extraUsage: L("metric_pref_extra_usage") + case .average: L("metric_pref_average") } } } @@ -282,6 +282,7 @@ extension SettingsStore { forKey: "mergedOverviewSelectedProviders") as? [String] ?? [] let selectedMenuProviderRaw = userDefaults.string(forKey: "selectedMenuProvider") let providerDetectionCompleted = userDefaults.object(forKey: "providerDetectionCompleted") as? Bool ?? false + let appLanguageRaw = userDefaults.string(forKey: "appLanguage") return SettingsDefaultsState( refreshFrequency: refreshFrequency, @@ -319,7 +320,8 @@ extension SettingsStore { mergedMenuLastSelectedWasOverview: mergedMenuLastSelectedWasOverview, mergedOverviewSelectedProvidersRaw: mergedOverviewSelectedProvidersRaw, selectedMenuProviderRaw: selectedMenuProviderRaw, - providerDetectionCompleted: providerDetectionCompleted) + providerDetectionCompleted: providerDetectionCompleted, + appLanguageRaw: appLanguageRaw) } } diff --git a/Sources/CodexBar/SettingsStoreState.swift b/Sources/CodexBar/SettingsStoreState.swift index c28de8f63..7611d8a3e 100644 --- a/Sources/CodexBar/SettingsStoreState.swift +++ b/Sources/CodexBar/SettingsStoreState.swift @@ -37,4 +37,5 @@ struct SettingsDefaultsState { var mergedOverviewSelectedProvidersRaw: [String] var selectedMenuProviderRaw: String? var providerDetectionCompleted: Bool + var appLanguageRaw: String? } diff --git a/Sources/CodexBar/StatusItemController+Menu.swift b/Sources/CodexBar/StatusItemController+Menu.swift index 9d03f140b..97d94b49d 100644 --- a/Sources/CodexBar/StatusItemController+Menu.swift +++ b/Sources/CodexBar/StatusItemController+Menu.swift @@ -1276,7 +1276,7 @@ extension StatusItemController { submenu.addItem(titleItem) if let window = timeLimit.windowLabel { - let item = NSMenuItem(title: "Window: \(window)", action: nil, keyEquivalent: "") + let item = NSMenuItem(title: String(format: L("mcp_window"), window), action: nil, keyEquivalent: "") item.isEnabled = false submenu.addItem(item) } @@ -1284,7 +1284,7 @@ extension StatusItemController { let reset = self.settings.resetTimeDisplayStyle == .absolute ? UsageFormatter.resetDescription(from: resetTime) : UsageFormatter.resetCountdownDescription(from: resetTime) - let item = NSMenuItem(title: "Resets: \(reset)", action: nil, keyEquivalent: "") + let item = NSMenuItem(title: String(format: L("mcp_resets"), reset), action: nil, keyEquivalent: "") item.isEnabled = false submenu.addItem(item) } @@ -1295,7 +1295,7 @@ extension StatusItemController { } for detail in sortedDetails { let usage = UsageFormatter.tokenCountString(detail.usage) - let item = NSMenuItem(title: "\(detail.modelCode): \(usage)", action: nil, keyEquivalent: "") + let item = NSMenuItem(title: String(format: L("mcp_model_usage"), detail.modelCode, usage), action: nil, keyEquivalent: "") submenu.addItem(item) } return submenu diff --git a/Sources/CodexBar/UsageStoreSupport.swift b/Sources/CodexBar/UsageStoreSupport.swift index 522416898..3ac92fc59 100644 --- a/Sources/CodexBar/UsageStoreSupport.swift +++ b/Sources/CodexBar/UsageStoreSupport.swift @@ -18,12 +18,12 @@ enum ProviderStatusIndicator: String { var label: String { switch self { - case .none: "Operational" - case .minor: "Partial outage" - case .major: "Major outage" - case .critical: "Critical issue" - case .maintenance: "Maintenance" - case .unknown: "Status unknown" + case .none: L("status_operational") + case .minor: L("status_partial_outage") + case .major: L("status_major_outage") + case .critical: L("status_critical_issue") + case .maintenance: L("status_maintenance") + case .unknown: L("status_unknown") } } } From b248bad704017f4daf69030f4a5c1bded0b86ac0 Mon Sep 17 00:00:00 2001 From: Z <1492162933@qq.com> Date: Fri, 1 May 2026 00:26:20 +0800 Subject: [PATCH 2/2] fix: populate English format strings and resolve duplicate localization keys - Replace 35 placeholder format-string entries in en.lproj with real English values (keys like version_format, cost_status_*, mcp_*, etc. were self-mapped causing raw key names to render in English UI) - Split duplicate last_fetch_failed into plain and format-string variants (last_fetch_failed + last_fetch_failed_with_provider) in both en and zh-Hans, preventing the plain variant from silently overriding the formatted one and dropping the provider name - Fix Localization.swift to follow system locale when appLanguage is empty ("System" mode), instead of always forcing English - Remove lowercased() from lproj path lookup to avoid mismatch on case-sensitive filesystems - Localize the Picker accessibility label in General preferences (was hard-coded "Language", now uses language_title) --- Sources/CodexBar/Localization.swift | 18 +++-- Sources/CodexBar/PreferencesGeneralPane.swift | 2 +- .../PreferencesProviderDetailView.swift | 2 +- .../Resources/en.lproj/Localizable.strings | 72 +++++++++---------- .../zh-Hans.lproj/Localizable.strings | 2 +- 5 files changed, 52 insertions(+), 44 deletions(-) diff --git a/Sources/CodexBar/Localization.swift b/Sources/CodexBar/Localization.swift index 13e3e7171..03e241d53 100644 --- a/Sources/CodexBar/Localization.swift +++ b/Sources/CodexBar/Localization.swift @@ -10,12 +10,20 @@ private func appLanguageDefaults() -> UserDefaults { private func localizedBundle() -> Bundle { let language = appLanguageDefaults().string(forKey: "appLanguage") ?? "" - let target = language.isEmpty ? "en" : language.lowercased() - if let path = Bundle.module.path(forResource: target, ofType: "lproj"), - let bundle = Bundle(path: path) { - return bundle + if !language.isEmpty { + if let path = Bundle.module.path(forResource: language, ofType: "lproj"), + let bundle = Bundle(path: path) { + return bundle + } + } else { + // System mode: follow macOS language preferences + if let preferred = Bundle.module.preferredLocalizations.first, + let path = Bundle.module.path(forResource: preferred, ofType: "lproj"), + let bundle = Bundle(path: path) { + return bundle + } } - // Fallback to en.lproj to avoid following system language + // Fallback to en.lproj if let path = Bundle.module.path(forResource: "en", ofType: "lproj"), let bundle = Bundle(path: path) { return bundle diff --git a/Sources/CodexBar/PreferencesGeneralPane.swift b/Sources/CodexBar/PreferencesGeneralPane.swift index a61109333..20327a54b 100644 --- a/Sources/CodexBar/PreferencesGeneralPane.swift +++ b/Sources/CodexBar/PreferencesGeneralPane.swift @@ -43,7 +43,7 @@ struct GeneralPane: View { .fixedSize(horizontal: false, vertical: true) } Spacer() - Picker("Language", selection: self.$settings.appLanguage) { + Picker(L("language_title"), selection: self.$settings.appLanguage) { ForEach(AppLanguage.allCases) { option in Text(option.label).tag(option.rawValue) } diff --git a/Sources/CodexBar/PreferencesProviderDetailView.swift b/Sources/CodexBar/PreferencesProviderDetailView.swift index 8daf9d554..61d80d263 100644 --- a/Sources/CodexBar/PreferencesProviderDetailView.swift +++ b/Sources/CodexBar/PreferencesProviderDetailView.swift @@ -99,7 +99,7 @@ struct ProviderDetailView: View { if let errorDisplay { ProviderErrorView( - title: String(format: L("last_fetch_failed"), self.store.metadata(for: self.provider).displayName), + title: String(format: L("last_fetch_failed_with_provider"), self.store.metadata(for: self.provider).displayName), display: errorDisplay, isExpanded: self.$isErrorExpanded, onCopy: { self.onCopyError(errorDisplay.full) }) diff --git a/Sources/CodexBar/Resources/en.lproj/Localizable.strings b/Sources/CodexBar/Resources/en.lproj/Localizable.strings index 327b9e359..3ab194587 100644 --- a/Sources/CodexBar/Resources/en.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/en.lproj/Localizable.strings @@ -353,43 +353,43 @@ "\\(name): unsupported" = "\\(name): unsupported"; "all browsers" = "all browsers"; "available again." = "available again."; -"built_format" = "built_format"; -"copilot_complete_in_browser" = "copilot_complete_in_browser"; -"copilot_device_code" = "copilot_device_code"; -"copilot_device_code_copied" = "copilot_device_code_copied"; -"copilot_verify_at" = "copilot_verify_at"; -"copilot_waiting_text" = "copilot_waiting_text"; -"copilot_window_closes_auto" = "copilot_window_closes_auto"; -"cost_status_error" = "cost_status_error"; -"cost_status_fetching" = "cost_status_fetching"; -"cost_status_last_attempt" = "cost_status_last_attempt"; -"cost_status_no_data" = "cost_status_no_data"; -"cost_status_snapshot" = "cost_status_snapshot"; -"cost_status_unsupported" = "cost_status_unsupported"; -"credits_remaining" = "credits_remaining"; -"cursor_on_demand" = "cursor_on_demand"; -"cursor_on_demand_with_limit" = "cursor_on_demand_with_limit"; -"extra_usage_format" = "extra_usage_format"; -"jetbrains_detected_generate" = "jetbrains_detected_generate"; -"jetbrains_detected_select" = "jetbrains_detected_select"; -"last_fetch_failed" = "last_fetch_failed"; -"last_spend" = "last_spend"; -"mcp_model_usage" = "mcp_model_usage"; -"mcp_resets" = "mcp_resets"; -"mcp_window" = "mcp_window"; -"metric_average" = "metric_average"; -"metric_primary" = "metric_primary"; -"metric_secondary" = "metric_secondary"; -"metric_tertiary" = "metric_tertiary"; -"multiple_workspaces_found" = "multiple_workspaces_found"; -"off_peak" = "off_peak"; -"off_peak_peak_in" = "off_peak_peak_in"; +"built_format" = "Built %@"; +"copilot_complete_in_browser" = "Complete sign in in your browser."; +"copilot_device_code" = "Device code copied to clipboard: %1$@\n\nVerify at: %2$@"; +"copilot_device_code_copied" = "Device code copied."; +"copilot_verify_at" = "Verify at %@"; +"copilot_waiting_text" = "Complete sign in in your browser.\nThis window closes automatically when sign-in completes."; +"copilot_window_closes_auto" = "This window closes automatically when sign-in completes."; +"cost_status_error" = "%1$@: %2$@"; +"cost_status_fetching" = "%1$@: fetching… %2$@"; +"cost_status_last_attempt" = "%1$@: last attempt %2$@"; +"cost_status_no_data" = "%@: no data yet"; +"cost_status_snapshot" = "%1$@: %2$@ · 30d %3$@"; +"cost_status_unsupported" = "%@: unsupported"; +"credits_remaining" = "Credits: %@"; +"cursor_on_demand" = "On-demand: %@"; +"cursor_on_demand_with_limit" = "On-demand: %1$@ / %2$@"; +"extra_usage_format" = "Extra usage: %1$@ / %2$@"; +"jetbrains_detected_generate" = "Detected: %@. Use the AI assistant once to generate quota data, then refresh CodexBar."; +"jetbrains_detected_select" = "Detected: %@. Select your preferred IDE in Settings, then refresh CodexBar."; +"last_fetch_failed_with_provider" = "Last %@ fetch failed:"; +"last_spend" = "Last spend: %@"; +"mcp_model_usage" = "%1$@: %2$@"; +"mcp_resets" = "Resets: %@"; +"mcp_window" = "Window: %@"; +"metric_average" = "Average (%1$@ + %2$@)"; +"metric_primary" = "Primary (%@)"; +"metric_secondary" = "Secondary (%@)"; +"metric_tertiary" = "Tertiary (%@)"; +"multiple_workspaces_found" = "CodexBar found multiple workspaces for %@. Please choose the workspace to add."; +"off_peak" = "Off-peak"; +"off_peak_peak_in" = "Off-peak · peak in %@"; "ory_session_…=…; csrftoken=…" = "ory_session_…=…; csrftoken=…"; -"overview_choose_providers" = "overview_choose_providers"; -"peak_ends_in" = "peak_ends_in"; -"remove_account_message" = "remove_account_message"; -"version_format" = "version_format"; -"vertex_ai_login_instructions" = "vertex_ai_login_instructions"; +"overview_choose_providers" = "Choose up to %@ providers"; +"peak_ends_in" = "Peak ends in %@"; +"remove_account_message" = "Remove %@ from CodexBar? Its managed Codex home will be deleted."; +"version_format" = "Version %@"; +"vertex_ai_login_instructions" = "To track Vertex AI usage, authenticate with Google Cloud.\n\n1. Open Terminal\n2. Run: gcloud auth application-default login\n3. Follow the browser prompts to sign in\n4. Set your project: gcloud config set project PROJECT_ID\n\nOpen Terminal now?"; "workspaceID is set but only opencode and opencodego support workspaceID." = "workspaceID is set but only opencode and opencodego support workspaceID."; "© 2026 Peter Steinberger. MIT License." = "© 2026 Peter Steinberger. MIT License."; diff --git a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings index dbf06308b..5cea87e17 100644 --- a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings @@ -375,7 +375,7 @@ "extra_usage_format" = "额外用量:%1$@ / %2$@"; "jetbrains_detected_generate" = "检测到:%@。使用一次 AI 助手以生成配额数据,然后刷新 CodexBar。"; "jetbrains_detected_select" = "检测到:%@。在设置中选择您偏好的 IDE,然后刷新 CodexBar。"; -"last_fetch_failed" = "上次获取 %1$@ 失败:"; +"last_fetch_failed_with_provider" = "上次获取 %1$@ 失败:"; "last_spend" = "上次消耗:%@"; "mcp_model_usage" = "%1$@:%2$@"; "mcp_resets" = "重置:%@";