From 0858d2407a3b69109734397fafc5761fad9fffd7 Mon Sep 17 00:00:00 2001 From: waffensam Date: Sun, 22 Mar 2026 01:03:43 +0800 Subject: [PATCH] Localize settings and menu copy --- Package.swift | 4 + Sources/CodexBar/About.swift | 5 +- .../CodexBar/KeychainPromptCoordinator.swift | 92 ++--- Sources/CodexBar/MenuDescriptor.swift | 34 +- Sources/CodexBar/PreferencesAboutPane.swift | 15 +- .../CodexBar/PreferencesAdvancedPane.swift | 48 ++- Sources/CodexBar/PreferencesDebugPane.swift | 142 ++++---- Sources/CodexBar/PreferencesDisplayPane.swift | 56 +-- Sources/CodexBar/PreferencesGeneralPane.swift | 48 ++- Sources/CodexBar/PreferencesView.swift | 13 +- ...babaCodingPlanProviderImplementation.swift | 26 +- .../Amp/AmpProviderImplementation.swift | 12 +- .../AugmentProviderImplementation.swift | 16 +- .../Claude/ClaudeProviderImplementation.swift | 37 +- .../Codex/CodexProviderImplementation.swift | 24 +- .../CopilotProviderImplementation.swift | 8 +- .../Cursor/CursorProviderImplementation.swift | 12 +- .../FactoryProviderImplementation.swift | 12 +- .../JetBrainsProviderImplementation.swift | 10 +- .../Kilo/KiloProviderImplementation.swift | 9 +- .../Kimi/KimiProviderImplementation.swift | 12 +- .../KimiK2/KimiK2ProviderImplementation.swift | 6 +- .../MiniMaxProviderImplementation.swift | 26 +- .../Ollama/OllamaProviderImplementation.swift | 12 +- .../OpenCodeProviderImplementation.swift | 16 +- .../OpenRouterProviderImplementation.swift | 6 +- .../SyntheticProviderImplementation.swift | 4 +- .../Warp/WarpProviderImplementation.swift | 7 +- .../Zai/ZaiProviderImplementation.swift | 4 +- .../StatusItemController+Actions.swift | 34 +- Sources/CodexBarCore/Localization/L10n.swift | 9 + .../AlibabaCodingPlanProviderDescriptor.swift | 8 +- .../Providers/Amp/AmpProviderDescriptor.swift | 6 +- .../AntigravityProviderDescriptor.swift | 2 +- .../Augment/AugmentProviderDescriptor.swift | 6 +- .../Claude/ClaudeProviderDescriptor.swift | 8 +- .../Codex/CodexProviderDescriptor.swift | 6 +- .../Copilot/CopilotProviderDescriptor.swift | 6 +- .../Cursor/CursorProviderDescriptor.swift | 6 +- .../Factory/FactoryProviderDescriptor.swift | 4 +- .../Gemini/GeminiProviderDescriptor.swift | 4 +- .../JetBrainsProviderDescriptor.swift | 4 +- .../Kilo/KiloProviderDescriptor.swift | 4 +- .../Kimi/KimiProviderDescriptor.swift | 6 +- .../KimiK2/KimiK2ProviderDescriptor.swift | 6 +- .../Kiro/KiroProviderDescriptor.swift | 6 +- .../MiniMax/MiniMaxProviderDescriptor.swift | 6 +- .../Ollama/OllamaProviderDescriptor.swift | 6 +- .../OpenCode/OpenCodeProviderDescriptor.swift | 6 +- .../OpenRouterProviderDescriptor.swift | 6 +- .../SyntheticProviderDescriptor.swift | 6 +- .../VertexAI/VertexAIProviderDescriptor.swift | 6 +- .../Warp/WarpProviderDescriptor.swift | 6 +- .../Providers/Zai/ZaiProviderDescriptor.swift | 4 +- .../Resources/en.lproj/Localizable.strings | 342 ++++++++++++++++++ .../zh-Hans.lproj/Localizable.strings | 342 ++++++++++++++++++ .../CodexBarWidget/CodexBarWidgetViews.swift | 78 ++-- 57 files changed, 1148 insertions(+), 501 deletions(-) create mode 100644 Sources/CodexBarCore/Localization/L10n.swift create mode 100644 Sources/CodexBarCore/Resources/en.lproj/Localizable.strings create mode 100644 Sources/CodexBarCore/Resources/zh-Hans.lproj/Localizable.strings diff --git a/Package.swift b/Package.swift index ed0af2639..e9f390251 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), ], @@ -33,6 +34,9 @@ let package = Package( .product(name: "Logging", package: "swift-log"), .product(name: "SweetCookieKit", package: "SweetCookieKit"), ], + resources: [ + .process("Resources"), + ], swiftSettings: [ .enableUpcomingFeature("StrictConcurrency"), ]), diff --git a/Sources/CodexBar/About.swift b/Sources/CodexBar/About.swift index 677ea6e5a..60eb39605 100644 --- a/Sources/CodexBar/About.swift +++ b/Sources/CodexBar/About.swift @@ -1,4 +1,5 @@ import AppKit +import CodexBarCore @MainActor func showAbout() { @@ -21,7 +22,7 @@ func showAbout() { ]) } - let credits = NSMutableAttributedString(string: "Peter Steinberger — MIT License\n") + let credits = NSMutableAttributedString(string: L10n.tr("about.credits", default: "Peter Steinberger — MIT License\n")) credits.append(makeLink("GitHub", urlString: "https://github.com/steipete/CodexBar")) credits.append(separator) credits.append(makeLink("Website", urlString: "https://codexbar.app")) @@ -30,7 +31,7 @@ func showAbout() { credits.append(separator) credits.append(makeLink("Email", urlString: "mailto:peter@steipete.me")) if let buildTimestamp, let formatted = formattedBuildTimestamp(buildTimestamp) { - var builtLine = "Built \(formatted)" + var builtLine = L10n.tr("about.built", default: "Built %@", formatted) if let gitCommit, !gitCommit.isEmpty, gitCommit != "unknown" { builtLine += " (\(gitCommit)" #if DEBUG diff --git a/Sources/CodexBar/KeychainPromptCoordinator.swift b/Sources/CodexBar/KeychainPromptCoordinator.swift index a6add39ab..88fcf0487 100644 --- a/Sources/CodexBar/KeychainPromptCoordinator.swift +++ b/Sources/CodexBar/KeychainPromptCoordinator.swift @@ -22,94 +22,50 @@ enum KeychainPromptCoordinator { } private static func presentBrowserCookiePrompt(_ context: BrowserCookieKeychainPromptContext) { - let title = "Keychain Access Required" - let message = [ - "CodexBar will ask macOS Keychain for “\(context.label)” so it can decrypt browser cookies", - "and authenticate your account. Click OK to continue.", - ].joined(separator: " ") + let title = L10n.tr("keychain.title", default: "Keychain Access Required") + let message = L10n.tr( + "keychain.browserCookie.message", + default: "CodexBar will ask macOS Keychain for “%@” so it can decrypt browser cookies and authenticate your account. Click OK to continue.", + context.label) self.log.info("Browser cookie keychain prompt requested", metadata: ["label": context.label]) self.presentAlert(title: title, message: message) } private static func keychainCopy(for context: KeychainPromptContext) -> (title: String, message: String) { - let title = "Keychain Access Required" - switch context.kind { + let title = L10n.tr("keychain.title", default: "Keychain Access Required") + let message: String = switch context.kind { case .claudeOAuth: - return (title, [ - "CodexBar will ask macOS Keychain for the Claude Code OAuth token", - "so it can fetch your Claude usage. Click OK to continue.", - ].joined(separator: " ")) + L10n.tr("keychain.claudeOAuth.message", default: "CodexBar will ask macOS Keychain for the Claude Code OAuth token so it can fetch your Claude usage. Click OK to continue.") case .codexCookie: - return (title, [ - "CodexBar will ask macOS Keychain for your OpenAI cookie header", - "so it can fetch Codex dashboard extras. Click OK to continue.", - ].joined(separator: " ")) + L10n.tr("keychain.codexCookie.message", default: "CodexBar will ask macOS Keychain for your OpenAI cookie header so it can fetch Codex dashboard extras. Click OK to continue.") case .claudeCookie: - return (title, [ - "CodexBar will ask macOS Keychain for your Claude cookie header", - "so it can fetch Claude web usage. Click OK to continue.", - ].joined(separator: " ")) + L10n.tr("keychain.claudeCookie.message", default: "CodexBar will ask macOS Keychain for your Claude cookie header so it can fetch Claude web usage. Click OK to continue.") case .cursorCookie: - return (title, [ - "CodexBar will ask macOS Keychain for your Cursor cookie header", - "so it can fetch usage. Click OK to continue.", - ].joined(separator: " ")) + L10n.tr("keychain.cursorCookie.message", default: "CodexBar will ask macOS Keychain for your Cursor cookie header so it can fetch usage. Click OK to continue.") case .opencodeCookie: - return (title, [ - "CodexBar will ask macOS Keychain for your OpenCode cookie header", - "so it can fetch usage. Click OK to continue.", - ].joined(separator: " ")) + L10n.tr("keychain.opencodeCookie.message", default: "CodexBar will ask macOS Keychain for your OpenCode cookie header so it can fetch usage. Click OK to continue.") case .factoryCookie: - return (title, [ - "CodexBar will ask macOS Keychain for your Factory cookie header", - "so it can fetch usage. Click OK to continue.", - ].joined(separator: " ")) + L10n.tr("keychain.factoryCookie.message", default: "CodexBar will ask macOS Keychain for your Factory cookie header so it can fetch usage. Click OK to continue.") case .zaiToken: - return (title, [ - "CodexBar will ask macOS Keychain for your z.ai API token", - "so it can fetch usage. Click OK to continue.", - ].joined(separator: " ")) + L10n.tr("keychain.zaiToken.message", default: "CodexBar will ask macOS Keychain for your z.ai API token so it can fetch usage. Click OK to continue.") case .syntheticToken: - return (title, [ - "CodexBar will ask macOS Keychain for your Synthetic API key", - "so it can fetch usage. Click OK to continue.", - ].joined(separator: " ")) + L10n.tr("keychain.syntheticToken.message", default: "CodexBar will ask macOS Keychain for your Synthetic API key so it can fetch usage. Click OK to continue.") case .copilotToken: - return (title, [ - "CodexBar will ask macOS Keychain for your GitHub Copilot token", - "so it can fetch usage. Click OK to continue.", - ].joined(separator: " ")) + L10n.tr("keychain.copilotToken.message", default: "CodexBar will ask macOS Keychain for your GitHub Copilot token so it can fetch usage. Click OK to continue.") case .kimiToken: - return (title, [ - "CodexBar will ask macOS Keychain for your Kimi auth token", - "so it can fetch usage. Click OK to continue.", - ].joined(separator: " ")) + L10n.tr("keychain.kimiToken.message", default: "CodexBar will ask macOS Keychain for your Kimi auth token so it can fetch usage. Click OK to continue.") case .kimiK2Token: - return (title, [ - "CodexBar will ask macOS Keychain for your Kimi K2 API key", - "so it can fetch usage. Click OK to continue.", - ].joined(separator: " ")) + L10n.tr("keychain.kimik2Token.message", default: "CodexBar will ask macOS Keychain for your Kimi K2 API key so it can fetch usage. Click OK to continue.") case .minimaxCookie: - return (title, [ - "CodexBar will ask macOS Keychain for your MiniMax cookie header", - "so it can fetch usage. Click OK to continue.", - ].joined(separator: " ")) + L10n.tr("keychain.minimaxCookie.message", default: "CodexBar will ask macOS Keychain for your MiniMax cookie header so it can fetch usage. Click OK to continue.") case .minimaxToken: - return (title, [ - "CodexBar will ask macOS Keychain for your MiniMax API token", - "so it can fetch usage. Click OK to continue.", - ].joined(separator: " ")) + L10n.tr("keychain.minimaxToken.message", default: "CodexBar will ask macOS Keychain for your MiniMax API token so it can fetch usage. Click OK to continue.") case .augmentCookie: - return (title, [ - "CodexBar will ask macOS Keychain for your Augment cookie header", - "so it can fetch usage. Click OK to continue.", - ].joined(separator: " ")) + L10n.tr("keychain.augmentCookie.message", default: "CodexBar will ask macOS Keychain for your Augment cookie header so it can fetch usage. Click OK to continue.") case .ampCookie: - return (title, [ - "CodexBar will ask macOS Keychain for your Amp cookie header", - "so it can fetch usage. Click OK to continue.", - ].joined(separator: " ")) + L10n.tr("keychain.ampCookie.message", default: "CodexBar will ask macOS Keychain for your Amp cookie header so it can fetch usage. Click OK to continue.") } + return (title, message) } private static func presentAlert(title: String, message: String) { @@ -134,7 +90,7 @@ enum KeychainPromptCoordinator { let alert = NSAlert() alert.messageText = title alert.informativeText = message - alert.addButton(withTitle: "OK") + alert.addButton(withTitle: L10n.tr("common.ok", default: "OK")) _ = alert.runModal() } } diff --git a/Sources/CodexBar/MenuDescriptor.swift b/Sources/CodexBar/MenuDescriptor.swift index 05aa55fff..cd429d603 100644 --- a/Sources/CodexBar/MenuDescriptor.swift +++ b/Sources/CodexBar/MenuDescriptor.swift @@ -87,7 +87,7 @@ struct MenuDescriptor { sections.append(accountSection) } } else { - sections.append(Section(entries: [.text("No usage configured.", .secondary)])) + sections.append(Section(entries: [ .text(L10n.tr("menu.noUsageConfigured", default: "No usage configured."), .secondary)])) } } @@ -174,7 +174,7 @@ struct MenuDescriptor { if meta.supportsOpus, let opus = snap.tertiary { Self.appendRateWindow( entries: &entries, - title: meta.opusLabel ?? "Sonnet", + title: meta.opusLabel ?? L10n.tr("provider.label.sonnet", default: "Sonnet"), window: opus, resetStyle: resetStyle, showUsed: settings.usageBarsShowUsed) @@ -184,11 +184,11 @@ struct MenuDescriptor { if cost.currencyCode == "Quota" { let used = String(format: "%.0f", cost.used) let limit = String(format: "%.0f", cost.limit) - entries.append(.text("Quota: \(used) / \(limit)", .primary)) + entries.append(.text(L10n.tr("menu.quota", default: "Quota: %@ / %@", used, limit), .primary)) } } } else { - entries.append(.text("No usage yet", .secondary)) + entries.append(.text(L10n.tr("menu.noUsageYet", default: "No usage yet"), .secondary)) } let usageContext = ProviderMenuUsageContext( @@ -236,27 +236,27 @@ struct MenuDescriptor { let redactedEmail = PersonalInfoRedactor.redactEmail(emailText, isEnabled: hidePersonalInfo) if let emailText, !emailText.isEmpty { - entries.append(.text("Account: \(redactedEmail)", .secondary)) + entries.append(.text(L10n.tr("menu.account", default: "Account: %@", redactedEmail), .secondary)) } if provider == .kilo { let kiloLogin = self.kiloLoginParts(loginMethod: loginMethodText) if let pass = kiloLogin.pass { - entries.append(.text("Plan: \(AccountFormatter.plan(pass))", .secondary)) + entries.append(.text(L10n.tr("menu.plan", default: "Plan: %@", AccountFormatter.plan(pass)), .secondary)) } for detail in kiloLogin.details { - entries.append(.text("Activity: \(detail)", .secondary)) + entries.append(.text(L10n.tr("menu.activity", default: "Activity: %@", detail), .secondary)) } } else if let loginMethodText, !loginMethodText.isEmpty { - entries.append(.text("Plan: \(AccountFormatter.plan(loginMethodText))", .secondary)) + entries.append(.text(L10n.tr("menu.plan", default: "Plan: %@", AccountFormatter.plan(loginMethodText)), .secondary)) } if metadata.usesAccountFallback { if emailText?.isEmpty ?? true, let fallbackEmail = fallback.email, !fallbackEmail.isEmpty { let redacted = PersonalInfoRedactor.redactEmail(fallbackEmail, isEnabled: hidePersonalInfo) - entries.append(.text("Account: \(redacted)", .secondary)) + entries.append(.text(L10n.tr("menu.account", default: "Account: %@", redacted), .secondary)) } if loginMethodText?.isEmpty ?? true, let fallbackPlan = fallback.plan, !fallbackPlan.isEmpty { - entries.append(.text("Plan: \(AccountFormatter.plan(fallbackPlan))", .secondary)) + entries.append(.text(L10n.tr("menu.plan", default: "Plan: %@", AccountFormatter.plan(fallbackPlan)), .secondary)) } } @@ -327,7 +327,7 @@ struct MenuDescriptor { } else { let loginAction = self.switchAccountTarget(for: provider, store: store) let hasAccount = self.hasAccount(for: provider, store: store, account: account) - let accountLabel = hasAccount ? "Switch Account..." : "Add Account..." + let accountLabel = hasAccount ? L10n.tr("menu.switchAccount", default: "Switch Account...") : L10n.tr("menu.addAccount", default: "Add Account...") entries.append(.action(accountLabel, loginAction)) } } @@ -343,10 +343,10 @@ struct MenuDescriptor { } if metadata?.dashboardURL != nil { - entries.append(.action("Usage Dashboard", .dashboard)) + entries.append(.action(L10n.tr("menu.dashboard", default: "Usage Dashboard"), .dashboard)) } if metadata?.statusPageURL != nil || metadata?.statusLinkURL != nil { - entries.append(.action("Status Page", .statusPage)) + entries.append(.action(L10n.tr("menu.statusPage", default: "Status Page"), .statusPage)) } if let statusLine = self.statusLine(for: provider, store: store) { @@ -359,12 +359,12 @@ struct MenuDescriptor { private static func metaSection(updateReady: Bool) -> Section { var entries: [Entry] = [] if updateReady { - entries.append(.action("Update ready, restart now?", .installUpdate)) + entries.append(.action(L10n.tr("menu.updateReady", default: "Update ready, restart now?"), .installUpdate)) } entries.append(contentsOf: [ - .action("Settings...", .settings), - .action("About CodexBar", .about), - .action("Quit", .quit), + .action(L10n.tr("menu.settings", default: "Settings..."), .settings), + .action(L10n.tr("menu.about", default: "About CodexBar"), .about), + .action(L10n.tr("menu.quit", default: "Quit"), .quit), ]) return Section(entries: entries) } diff --git a/Sources/CodexBar/PreferencesAboutPane.swift b/Sources/CodexBar/PreferencesAboutPane.swift index 16e27189e..c8e135fb4 100644 --- a/Sources/CodexBar/PreferencesAboutPane.swift +++ b/Sources/CodexBar/PreferencesAboutPane.swift @@ -1,4 +1,5 @@ import AppKit +import CodexBarCore import SwiftUI @MainActor @@ -51,14 +52,14 @@ struct AboutPane: View { VStack(spacing: 2) { Text("CodexBar") .font(.title3).bold() - Text("Version \(self.versionString)") + Text(L10n.tr("preferences.about.version", default: "Version %@", self.versionString)) .foregroundStyle(.secondary) if let buildTimestamp { - Text("Built \(buildTimestamp)") + Text(L10n.tr("preferences.about.built", default: "Built %@", buildTimestamp)) .font(.footnote) .foregroundStyle(.secondary) } - Text("May your tokens never run out—keep agent limits in view.") + Text(L10n.tr("preferences.about.tagline", default: "May your tokens never run out—keep agent limits in view.")) .font(.footnote) .foregroundStyle(.secondary) } @@ -80,12 +81,12 @@ struct AboutPane: View { if self.updater.isAvailable { VStack(spacing: 10) { - Toggle("Check for updates automatically", isOn: self.$autoUpdateEnabled) + Toggle(L10n.tr("preferences.about.autoUpdates", default: "Check for updates automatically"), isOn: self.$autoUpdateEnabled) .toggleStyle(.checkbox) .frame(maxWidth: .infinity, alignment: .center) VStack(spacing: 6) { HStack(spacing: 12) { - Text("Update Channel") + Text(L10n.tr("preferences.about.updateChannel", default: "Update Channel")) Spacer() Picker("", selection: self.updateChannelBinding) { ForEach(UpdateChannel.allCases) { channel in @@ -102,10 +103,10 @@ struct AboutPane: View { .multilineTextAlignment(.center) .frame(maxWidth: 280) } - Button("Check for Updates…") { self.updater.checkForUpdates(nil) } + Button(L10n.tr("preferences.about.checkForUpdates", default: "Check for Updates…")) { self.updater.checkForUpdates(nil) } } } else { - Text(self.updater.unavailableReason ?? "Updates unavailable in this build.") + Text(self.updater.unavailableReason ?? L10n.tr("preferences.about.updatesUnavailable", default: "Updates unavailable in this build.")) .foregroundStyle(.secondary) } diff --git a/Sources/CodexBar/PreferencesAdvancedPane.swift b/Sources/CodexBar/PreferencesAdvancedPane.swift index 1db4897f2..e09a043e8 100644 --- a/Sources/CodexBar/PreferencesAdvancedPane.swift +++ b/Sources/CodexBar/PreferencesAdvancedPane.swift @@ -1,3 +1,4 @@ +import CodexBarCore import KeyboardShortcuts import SwiftUI @@ -11,17 +12,17 @@ struct AdvancedPane: View { ScrollView(.vertical, showsIndicators: true) { VStack(alignment: .leading, spacing: 16) { SettingsSection(contentSpacing: 8) { - Text("Keyboard shortcut") + Text(L10n.tr("preferences.advanced.section.shortcut", default: "Keyboard shortcut")) .font(.caption) .foregroundStyle(.secondary) .textCase(.uppercase) HStack(alignment: .center, spacing: 12) { - Text("Open menu") + Text(L10n.tr("preferences.advanced.openMenu.title", default: "Open menu")) .font(.body) Spacer() KeyboardShortcuts.Recorder(for: .openMenu) } - Text("Trigger the menu bar menu from anywhere.") + Text(L10n.tr("preferences.advanced.openMenu.subtitle", default: "Trigger the menu bar menu from anywhere.")) .font(.footnote) .foregroundStyle(.tertiary) } @@ -36,7 +37,7 @@ struct AdvancedPane: View { if self.isInstallingCLI { ProgressView().controlSize(.small) } else { - Text("Install CLI") + Text(L10n.tr("preferences.advanced.installCLI.button", default: "Install CLI")) } } .disabled(self.isInstallingCLI) @@ -48,7 +49,7 @@ struct AdvancedPane: View { .lineLimit(2) } } - Text("Symlink CodexBarCLI to /usr/local/bin and /opt/homebrew/bin as codexbar.") + Text(L10n.tr("preferences.advanced.installCLI.subtitle", default: "Symlink CodexBarCLI to /usr/local/bin and /opt/homebrew/bin as codexbar.")) .font(.footnote) .foregroundStyle(.tertiary) } @@ -57,12 +58,12 @@ struct AdvancedPane: View { SettingsSection(contentSpacing: 10) { PreferenceToggleRow( - title: "Show Debug Settings", - subtitle: "Expose troubleshooting tools in the Debug tab.", + title: L10n.tr("preferences.advanced.showDebug.title", default: "Show Debug Settings"), + subtitle: L10n.tr("preferences.advanced.showDebug.subtitle", default: "Expose troubleshooting tools in the Debug tab."), binding: self.$settings.debugMenuEnabled) PreferenceToggleRow( - title: "Surprise me", - subtitle: "Check if you like your agents having some fun up there.", + title: L10n.tr("preferences.advanced.surprise.title", default: "Surprise me"), + subtitle: L10n.tr("preferences.advanced.surprise.subtitle", default: "Check if you like your agents having some fun up there."), binding: self.$settings.randomBlinkEnabled) } @@ -70,22 +71,19 @@ struct AdvancedPane: View { SettingsSection(contentSpacing: 10) { PreferenceToggleRow( - title: "Hide personal information", - subtitle: "Obscure email addresses in the menu bar and menu UI.", + title: L10n.tr("preferences.advanced.hidePII.title", default: "Hide personal information"), + subtitle: L10n.tr("preferences.advanced.hidePII.subtitle", default: "Obscure email addresses in the menu bar and menu UI."), 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: L10n.tr("preferences.advanced.keychain.title", default: "Keychain access"), + caption: L10n.tr("preferences.advanced.keychain.caption", default: "Disable all Keychain reads and writes. Browser cookie import is unavailable; paste Cookie headers manually in Providers.")) { PreferenceToggleRow( - title: "Disable Keychain access", - subtitle: "Prevents any Keychain access while enabled.", + title: L10n.tr("preferences.advanced.disableKeychain.title", default: "Disable Keychain access"), + subtitle: L10n.tr("preferences.advanced.disableKeychain.subtitle", default: "Prevents any Keychain access while enabled."), binding: self.$settings.debugDisableKeychainAccess) } } @@ -105,7 +103,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 = L10n.tr("preferences.advanced.installCLI.missing", default: "CodexBarCLI not found in app bundle.") return } @@ -119,29 +117,29 @@ extension AdvancedPane { let dir = (dest as NSString).deletingLastPathComponent guard fm.fileExists(atPath: dir) else { continue } guard fm.isWritableFile(atPath: dir) else { - results.append("No write access: \(dir)") + results.append(L10n.tr("preferences.advanced.installCLI.noWriteAccess", default: "No write access: %@", dir)) continue } if fm.fileExists(atPath: dest) { if Self.isLink(atPath: dest, pointingTo: helperURL.path) { - results.append("Installed: \(dir)") + results.append(L10n.tr("preferences.advanced.installCLI.installed", default: "Installed: %@", dir)) } else { - results.append("Exists: \(dir)") + results.append(L10n.tr("preferences.advanced.installCLI.exists", default: "Exists: %@", dir)) } continue } do { try fm.createSymbolicLink(atPath: dest, withDestinationPath: helperURL.path) - results.append("Installed: \(dir)") + results.append(L10n.tr("preferences.advanced.installCLI.installed", default: "Installed: %@", dir)) } catch { - results.append("Failed: \(dir)") + results.append(L10n.tr("preferences.advanced.installCLI.failed", default: "Failed: %@", dir)) } } self.cliStatus = results.isEmpty - ? "No writable bin dirs found." + ? L10n.tr("preferences.advanced.installCLI.noDirs", default: "No writable bin dirs found.") : results.joined(separator: " · ") } diff --git a/Sources/CodexBar/PreferencesDebugPane.swift b/Sources/CodexBar/PreferencesDebugPane.swift index a86a55434..84387e5ec 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: L10n.tr("preferences.debug.logging.title", default: "Logging")) { PreferenceToggleRow( - title: "Enable file logging", - subtitle: "Write logs to \(self.fileLogPath) for debugging.", + title: L10n.tr("preferences.debug.logging.enable.title", default: "Enable file logging"), + subtitle: L10n.tr("preferences.debug.logging.enable.subtitle", default: "Write logs to %@ for debugging.", self.fileLogPath), binding: self.$debugFileLoggingEnabled) .onChange(of: self.debugFileLoggingEnabled) { _, newValue in if self.settings.debugFileLoggingEnabled != newValue { @@ -39,14 +39,14 @@ struct DebugPane: View { HStack(alignment: .center, spacing: 12) { VStack(alignment: .leading, spacing: 4) { - Text("Verbosity") + Text(L10n.tr("preferences.debug.logging.verbosity", default: "Verbosity")) .font(.body) - Text("Controls how much detail is logged.") + Text(L10n.tr("preferences.debug.logging.verbosity.subtitle", default: "Controls how much detail is logged.")) .font(.footnote) .foregroundStyle(.tertiary) } Spacer() - Picker("Verbosity", selection: self.$settings.debugLogLevel) { + Picker(L10n.tr("preferences.debug.logging.verbosity", default: "Verbosity"), selection: self.$settings.debugLogLevel) { ForEach(CodexBarLog.Level.allCases) { level in Text(level.displayName).tag(level) } @@ -59,31 +59,31 @@ struct DebugPane: View { Button { NSWorkspace.shared.open(CodexBarLog.fileLogURL) } label: { - Label("Open log file", systemImage: "doc.text.magnifyingglass") + Label(L10n.tr("preferences.debug.logging.openFile", default: "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: L10n.tr("preferences.debug.forceAnimation.title", default: "Force animation on next refresh"), + subtitle: L10n.tr("preferences.debug.forceAnimation.subtitle", default: "Temporarily shows the loading animation after the next refresh."), 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: L10n.tr("preferences.debug.animations.title", default: "Loading animations"), + caption: L10n.tr("preferences.debug.animations.caption", default: "Pick a pattern and replay it in the menu bar. \"Random\" keeps the existing behavior.")) { - Picker("Animation pattern", selection: self.animationPatternBinding) { - Text("Random (default)").tag(nil as LoadingPattern?) + Picker(L10n.tr("preferences.debug.animations.pattern", default: "Animation pattern"), selection: self.animationPatternBinding) { + Text(L10n.tr("preferences.debug.animations.random", default: "Random (default)")).tag(nil as LoadingPattern?) ForEach(LoadingPattern.allCases) { pattern in Text(pattern.displayName).tag(Optional(pattern)) } } .pickerStyle(.radioGroup) - Button("Replay selected animation") { + Button(L10n.tr("preferences.debug.animations.replay", default: "Replay selected animation")) { self.replaySelectedAnimation() } .keyboardShortcut(.defaultAction) @@ -91,16 +91,16 @@ struct DebugPane: View { Button { NotificationCenter.default.post(name: .codexbarDebugBlinkNow, object: nil) } label: { - Label("Blink now", systemImage: "eyes") + Label(L10n.tr("preferences.debug.animations.blink", default: "Blink now"), systemImage: "eyes") } .controlSize(.small) } SettingsSection( - title: "Probe logs", - caption: "Fetch the latest probe output for debugging; Copy keeps the full text.") + title: L10n.tr("preferences.debug.probeLogs.title", default: "Probe logs"), + caption: L10n.tr("preferences.debug.probeLogs.caption", default: "Fetch the latest probe output for debugging; Copy keeps the full text.")) { - Picker("Provider", selection: self.$currentLogProvider) { + Picker(L10n.tr("preferences.debug.provider", default: "Provider"), selection: self.$currentLogProvider) { Text("Codex").tag(UsageProvider.codex) Text("Claude").tag(UsageProvider.claude) Text("Cursor").tag(UsageProvider.cursor) @@ -113,23 +113,23 @@ struct DebugPane: View { HStack(spacing: 12) { Button { self.loadLog(self.currentLogProvider) } label: { - Label("Fetch log", systemImage: "arrow.clockwise") + Label(L10n.tr("preferences.debug.probeLogs.fetch", default: "Fetch log"), systemImage: "arrow.clockwise") } .disabled(self.isLoadingLog) Button { self.copyToPasteboard(self.logText) } label: { - Label("Copy", systemImage: "doc.on.doc") + Label(L10n.tr("common.copy", default: "Copy"), systemImage: "doc.on.doc") } .disabled(self.logText.isEmpty) Button { self.saveLog(self.currentLogProvider) } label: { - Label("Save to file", systemImage: "externaldrive.badge.plus") + Label(L10n.tr("preferences.debug.probeLogs.save", default: "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(L10n.tr("preferences.debug.probeLogs.parseDump", default: "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(L10n.tr("preferences.debug.probeLogs.redetect", default: "Re-run provider autodetect"), systemImage: "dot.radiowaves.left.and.right") } .controlSize(.small) @@ -165,10 +165,10 @@ struct DebugPane: View { } SettingsSection( - title: "Fetch strategy attempts", - caption: "Last fetch pipeline decisions and errors for a provider.") + title: L10n.tr("preferences.debug.fetchAttempts.title", default: "Fetch strategy attempts"), + caption: L10n.tr("preferences.debug.fetchAttempts.caption", default: "Last fetch pipeline decisions and errors for a provider.")) { - Picker("Provider", selection: self.$currentFetchProvider) { + Picker(L10n.tr("preferences.debug.provider", default: "Provider"), selection: self.$currentFetchProvider) { ForEach(UsageProvider.allCases, id: \.self) { provider in Text(provider.rawValue.capitalized).tag(provider) } @@ -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: L10n.tr("preferences.debug.openAICookies.title", default: "OpenAI cookies"), + caption: L10n.tr("preferences.debug.openAICookies.caption", default: "Cookie import + WebKit scrape logs from the last OpenAI cookies attempt.")) { HStack(spacing: 12) { Button { self.copyToPasteboard(self.store.openAIDashboardCookieImportDebugLog ?? "") } label: { - Label("Copy", systemImage: "doc.on.doc") + Label(L10n.tr("common.copy", default: "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.") + : L10n.tr("preferences.debug.openAICookies.empty", default: "No log yet. Update OpenAI cookies in Providers → Codex to run an import.")) .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: L10n.tr("preferences.debug.caches.title", default: "Caches"), + caption: L10n.tr("preferences.debug.caches.caption", default: "Clear cached cost scan results.")) { 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(L10n.tr("preferences.debug.caches.clear", default: "Clear cost cache"), systemImage: "trash") } .disabled(self.isClearingCostCache || isTokenRefreshActive) @@ -242,10 +242,10 @@ struct DebugPane: View { } SettingsSection( - title: "Notifications", - caption: "Trigger test notifications for the 5-hour session window (depleted/restored).") + title: L10n.tr("preferences.debug.notifications.title", default: "Notifications"), + caption: L10n.tr("preferences.debug.notifications.caption", default: "Trigger test notifications for the 5-hour session window (depleted/restored).")) { - Picker("Provider", selection: self.$currentLogProvider) { + Picker(L10n.tr("preferences.debug.provider", default: "Provider"), selection: self.$currentLogProvider) { Text("Codex").tag(UsageProvider.codex) Text("Claude").tag(UsageProvider.claude) } @@ -256,26 +256,26 @@ struct DebugPane: View { Button { self.postSessionNotification(.depleted, provider: self.currentLogProvider) } label: { - Label("Post depleted", systemImage: "bell.badge") + Label(L10n.tr("preferences.debug.notifications.depleted", default: "Post depleted"), systemImage: "bell.badge") } .controlSize(.small) Button { self.postSessionNotification(.restored, provider: self.currentLogProvider) } label: { - Label("Post restored", systemImage: "bell") + Label(L10n.tr("preferences.debug.notifications.restored", default: "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: L10n.tr("preferences.debug.cliSessions.title", default: "CLI sessions"), + caption: L10n.tr("preferences.debug.cliSessions.caption", default: "Keep Codex/Claude CLI sessions alive after a probe. Default exits once data is captured.")) { PreferenceToggleRow( - title: "Keep CLI sessions alive", - subtitle: "Skip teardown between probes (debug-only).", + title: L10n.tr("preferences.debug.cliSessions.keepAlive.title", default: "Keep CLI sessions alive"), + subtitle: L10n.tr("preferences.debug.cliSessions.keepAlive.subtitle", default: "Skip teardown between probes (debug-only)."), binding: self.$settings.debugKeepCLISessionsAlive) Button { @@ -283,17 +283,17 @@ struct DebugPane: View { await CLIProbeSessionResetter.resetAll() } } label: { - Label("Reset CLI sessions", systemImage: "arrow.counterclockwise") + Label(L10n.tr("preferences.debug.cliSessions.reset", default: "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: L10n.tr("preferences.debug.errorSimulation.title", default: "Error simulation"), + caption: L10n.tr("preferences.debug.errorSimulation.caption", default: "Inject a fake error message into the menu card for layout testing.")) { - Picker("Provider", selection: self.$currentErrorProvider) { + Picker(L10n.tr("preferences.debug.provider", default: "Provider"), selection: self.$currentErrorProvider) { Text("Codex").tag(UsageProvider.codex) Text("Claude").tag(UsageProvider.claude) Text("Gemini").tag(UsageProvider.gemini) @@ -305,7 +305,7 @@ struct DebugPane: View { .pickerStyle(.segmented) .frame(width: 360) - TextField("Simulated error text", text: self.$simulatedErrorText, axis: .vertical) + TextField(L10n.tr("preferences.debug.errorSimulation.text", default: "Simulated error text"), text: self.$simulatedErrorText, axis: .vertical) .lineLimit(4) HStack(spacing: 12) { @@ -314,14 +314,14 @@ struct DebugPane: View { self.simulatedErrorText, provider: self.currentErrorProvider) } label: { - Label("Set menu error", systemImage: "exclamationmark.triangle") + Label(L10n.tr("preferences.debug.errorSimulation.setMenu", default: "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(L10n.tr("preferences.debug.errorSimulation.clearMenu", default: "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(L10n.tr("preferences.debug.errorSimulation.setCost", default: "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(L10n.tr("preferences.debug.errorSimulation.clearCost", default: "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: L10n.tr("preferences.debug.cliPaths.title", default: "CLI paths"), + caption: L10n.tr("preferences.debug.cliPaths.caption", default: "Resolved Codex binary and PATH layers; startup login PATH capture (short timeout).")) { - self.binaryRow(title: "Codex binary", value: self.store.pathDebugInfo.codexBinary) - self.binaryRow(title: "Claude binary", value: self.store.pathDebugInfo.claudeBinary) + self.binaryRow(title: L10n.tr("preferences.debug.cliPaths.codexBinary", default: "Codex binary"), value: self.store.pathDebugInfo.codexBinary) + self.binaryRow(title: L10n.tr("preferences.debug.cliPaths.claudeBinary", default: "Claude binary"), value: self.store.pathDebugInfo.claudeBinary) VStack(alignment: .leading, spacing: 6) { - Text("Effective PATH") + Text(L10n.tr("preferences.debug.cliPaths.effectivePath", default: "Effective PATH")) .font(.callout.weight(.semibold)) ScrollView { Text( self.store.pathDebugInfo.effectivePATH.isEmpty - ? "Unavailable" + ? L10n.tr("preferences.debug.cliPaths.unavailable", default: "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(L10n.tr("preferences.debug.cliPaths.loginPath", default: "Login shell PATH (startup capture)")) .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 ? L10n.tr("preferences.debug.loading", default: "Loading…") : L10n.tr("preferences.debug.emptyLog", default: "No log yet. Fetch to load.") } 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 ?? L10n.tr("preferences.debug.cliPaths.notFound", default: "Not found")) .font(.system(.footnote, design: .monospaced)) .foregroundStyle(value == nil ? .secondary : .primary) } @@ -500,22 +500,22 @@ struct DebugPane: View { defer { self.isClearingCostCache = false } if let error = await self.store.clearCostUsageCache() { - self.costCacheStatus = "Failed: \(error)" + self.costCacheStatus = L10n.tr("preferences.debug.caches.failed", default: "Failed: %@", error) return } - self.costCacheStatus = "Cleared." + self.costCacheStatus = L10n.tr("preferences.debug.caches.cleared", default: "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 L10n.tr("preferences.debug.fetchAttempts.empty", default: "No fetch attempts yet.") } return attempts.map { attempt in let kind = Self.fetchKindLabel(attempt.kind) var line = "\(attempt.strategyID) (\(kind))" - line += attempt.wasAvailable ? " available" : " unavailable" + line += attempt.wasAvailable ? L10n.tr("preferences.debug.fetchAttempts.available", default: " available") : L10n.tr("preferences.debug.fetchAttempts.unavailable", default: " unavailable") if let error = attempt.errorDescription, !error.isEmpty { - line += " error=\(error)" + line += L10n.tr("preferences.debug.fetchAttempts.error", default: " error=%@", error) } return line }.joined(separator: "\n") @@ -523,12 +523,12 @@ struct DebugPane: View { private static func fetchKindLabel(_ kind: ProviderFetchKind) -> String { switch kind { - case .cli: "cli" - case .web: "web" - case .oauth: "oauth" - case .apiToken: "api" - case .localProbe: "local" - case .webDashboard: "web" + case .cli: L10n.tr("preferences.debug.fetchKind.cli", default: "cli") + case .web: L10n.tr("preferences.debug.fetchKind.web", default: "web") + case .oauth: L10n.tr("preferences.debug.fetchKind.oauth", default: "oauth") + case .apiToken: L10n.tr("preferences.debug.fetchKind.api", default: "api") + case .localProbe: L10n.tr("preferences.debug.fetchKind.local", default: "local") + case .webDashboard: L10n.tr("preferences.debug.fetchKind.web", default: "web") } } } diff --git a/Sources/CodexBar/PreferencesDisplayPane.swift b/Sources/CodexBar/PreferencesDisplayPane.swift index 04050b3bb..dbbc16834 100644 --- a/Sources/CodexBar/PreferencesDisplayPane.swift +++ b/Sources/CodexBar/PreferencesDisplayPane.swift @@ -13,40 +13,40 @@ struct DisplayPane: View { ScrollView(.vertical, showsIndicators: true) { VStack(alignment: .leading, spacing: 16) { SettingsSection(contentSpacing: 12) { - Text("Menu bar") + Text(L10n.tr("preferences.display.section.menuBar", default: "Menu bar")) .font(.caption) .foregroundStyle(.secondary) .textCase(.uppercase) PreferenceToggleRow( - title: "Merge Icons", - subtitle: "Use a single menu bar icon with a provider switcher.", + title: L10n.tr("preferences.display.mergeIcons.title", default: "Merge Icons"), + subtitle: L10n.tr("preferences.display.mergeIcons.subtitle", default: "Use a single menu bar icon with a provider switcher."), binding: self.$settings.mergeIcons) PreferenceToggleRow( - title: "Switcher shows icons", - subtitle: "Show provider icons in the switcher (otherwise show a weekly progress line).", + title: L10n.tr("preferences.display.switcherShowsIcons.title", default: "Switcher shows icons"), + subtitle: L10n.tr("preferences.display.switcherShowsIcons.subtitle", default: "Show provider icons in the switcher (otherwise show a weekly progress line)."), 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: L10n.tr("preferences.display.showMostUsed.title", default: "Show most-used provider"), + subtitle: L10n.tr("preferences.display.showMostUsed.subtitle", default: "Menu bar auto-shows the provider closest to its rate limit."), 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: L10n.tr("preferences.display.showsPercent.title", default: "Menu bar shows percent"), + subtitle: L10n.tr("preferences.display.showsPercent.subtitle", default: "Replace critter bars with provider branding icons and a percentage."), binding: self.$settings.menuBarShowsBrandIconWithPercent) HStack(alignment: .top, spacing: 12) { VStack(alignment: .leading, spacing: 4) { - Text("Display mode") + Text(L10n.tr("preferences.display.mode.title", default: "Display mode")) .font(.body) - Text("Choose what to show in the menu bar (Pace shows usage vs. expected).") + Text(L10n.tr("preferences.display.mode.subtitle", default: "Choose what to show in the menu bar (Pace shows usage vs. expected).")) .font(.footnote) .foregroundStyle(.tertiary) } Spacer() - Picker("Display mode", selection: self.$settings.menuBarDisplayMode) { + Picker(L10n.tr("preferences.display.mode.picker", default: "Display mode"), selection: self.$settings.menuBarDisplayMode) { ForEach(MenuBarDisplayMode.allCases) { mode in Text(mode.label).tag(mode) } @@ -62,25 +62,25 @@ struct DisplayPane: View { Divider() SettingsSection(contentSpacing: 12) { - Text("Menu content") + Text(L10n.tr("preferences.display.section.menuContent", default: "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: L10n.tr("preferences.display.showUsageAsUsed.title", default: "Show usage as used"), + subtitle: L10n.tr("preferences.display.showUsageAsUsed.subtitle", default: "Progress bars fill as you consume quota (instead of showing remaining)."), binding: self.$settings.usageBarsShowUsed) PreferenceToggleRow( - title: "Show reset time as clock", - subtitle: "Display reset times as absolute clock values instead of countdowns.", + title: L10n.tr("preferences.display.resetAsClock.title", default: "Show reset time as clock"), + subtitle: L10n.tr("preferences.display.resetAsClock.subtitle", default: "Display reset times as absolute clock values instead of countdowns."), binding: self.$settings.resetTimesShowAbsolute) PreferenceToggleRow( - title: "Show credits + extra usage", - subtitle: "Show Codex Credits and Claude Extra usage sections in the menu.", + title: L10n.tr("preferences.display.showCredits.title", default: "Show credits + extra usage"), + subtitle: L10n.tr("preferences.display.showCredits.subtitle", default: "Show Codex Credits and Claude Extra usage sections in the menu."), binding: self.$settings.showOptionalCreditsAndExtraUsage) PreferenceToggleRow( - title: "Show all token accounts", - subtitle: "Stack token accounts in the menu (otherwise show an account switcher bar).", + title: L10n.tr("preferences.display.showAllAccounts.title", default: "Show all token accounts"), + subtitle: L10n.tr("preferences.display.showAllAccounts.subtitle", default: "Stack token accounts in the menu (otherwise show an account switcher bar)."), 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(L10n.tr("preferences.display.overviewProviders.title", default: "Overview tab providers")) .font(.body) Spacer(minLength: 0) if self.showsOverviewConfigureButton { - Button("Configure…") { + Button(L10n.tr("preferences.display.overviewProviders.configure", default: "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(L10n.tr("preferences.display.overviewProviders.mergeRequired", default: "Enable Merge Icons to configure Overview tab providers.")) .font(.footnote) .foregroundStyle(.tertiary) } else if self.activeProvidersInOrder.isEmpty { - Text("No enabled providers available for Overview.") + Text(L10n.tr("preferences.display.overviewProviders.noneAvailable", default: "No enabled providers available for Overview.")) .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(L10n.tr("preferences.display.overviewProviders.choose", default: "Choose up to %@ providers", String(Self.maxOverviewProviders))) .font(.headline) - Text("Overview rows always follow provider order.") + Text(L10n.tr("preferences.display.overviewProviders.orderHint", default: "Overview rows always follow provider 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 L10n.tr("preferences.display.overviewProviders.noneSelected", default: "No providers selected") } return selectedNames.joined(separator: ", ") } diff --git a/Sources/CodexBar/PreferencesGeneralPane.swift b/Sources/CodexBar/PreferencesGeneralPane.swift index 39a95a55f..ccf5faed3 100644 --- a/Sources/CodexBar/PreferencesGeneralPane.swift +++ b/Sources/CodexBar/PreferencesGeneralPane.swift @@ -11,20 +11,20 @@ struct GeneralPane: View { ScrollView(.vertical, showsIndicators: true) { VStack(alignment: .leading, spacing: 16) { SettingsSection(contentSpacing: 12) { - Text("System") + Text(L10n.tr("preferences.general.section.system", default: "System")) .font(.caption) .foregroundStyle(.secondary) .textCase(.uppercase) PreferenceToggleRow( - title: "Start at Login", - subtitle: "Automatically opens CodexBar when you start your Mac.", + title: L10n.tr("preferences.general.startAtLogin.title", default: "Start at Login"), + subtitle: L10n.tr("preferences.general.startAtLogin.subtitle", default: "Automatically opens CodexBar when you start your Mac."), binding: self.$settings.launchAtLogin) } Divider() SettingsSection(contentSpacing: 12) { - Text("Usage") + Text(L10n.tr("preferences.general.section.usage", default: "Usage")) .font(.caption) .foregroundStyle(.secondary) .textCase(.uppercase) @@ -32,18 +32,18 @@ struct GeneralPane: View { VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 4) { Toggle(isOn: self.$settings.costUsageEnabled) { - Text("Show cost summary") + Text(L10n.tr("preferences.general.costSummary.title", default: "Show cost summary")) .font(.body) } .toggleStyle(.checkbox) - Text("Reads local usage logs. Shows today + last 30 days cost in the menu.") + Text(L10n.tr("preferences.general.costSummary.subtitle", default: "Reads local usage logs. Shows today + last 30 days cost in the menu.")) .font(.footnote) .foregroundStyle(.tertiary) .fixedSize(horizontal: false, vertical: true) if self.settings.costUsageEnabled { - Text("Auto-refresh: hourly · Timeout: 10m") + Text(L10n.tr("preferences.general.costSummary.status", default: "Auto-refresh: hourly · Timeout: 10m")) .font(.footnote) .foregroundStyle(.tertiary) @@ -57,21 +57,21 @@ struct GeneralPane: View { Divider() SettingsSection(contentSpacing: 12) { - Text("Automation") + Text(L10n.tr("preferences.general.section.automation", default: "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(L10n.tr("preferences.general.refreshCadence.title", default: "Refresh cadence")) .font(.body) - Text("How often CodexBar polls providers in the background.") + Text(L10n.tr("preferences.general.refreshCadence.subtitle", default: "How often CodexBar polls providers in the background.")) .font(.footnote) .foregroundStyle(.tertiary) } Spacer() - Picker("Refresh cadence", selection: self.$settings.refreshFrequency) { + Picker(L10n.tr("preferences.general.refreshCadence.picker", default: "Refresh cadence"), selection: self.$settings.refreshFrequency) { ForEach(RefreshFrequency.allCases) { option in Text(option.label).tag(option) } @@ -81,20 +81,18 @@ struct GeneralPane: View { .frame(maxWidth: 200) } if self.settings.refreshFrequency == .manual { - Text("Auto-refresh is off; use the menu's Refresh command.") + Text(L10n.tr("preferences.general.refreshCadence.manualHint", default: "Auto-refresh is off; use the menu's Refresh command.")) .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: L10n.tr("preferences.general.statusChecks.title", default: "Check provider status"), + subtitle: L10n.tr("preferences.general.statusChecks.subtitle", default: "Polls OpenAI/Claude status pages and Google Workspace for Gemini/Antigravity, surfacing incidents in the icon and menu."), 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: L10n.tr("preferences.general.notifications.title", default: "Session quota notifications"), + subtitle: L10n.tr("preferences.general.notifications.subtitle", default: "Notifies when the 5-hour session quota hits 0% and when it becomes available again."), binding: self.$settings.sessionQuotaNotificationsEnabled) } @@ -103,7 +101,7 @@ struct GeneralPane: View { SettingsSection(contentSpacing: 12) { HStack { Spacer() - Button("Quit CodexBar") { NSApp.terminate(nil) } + Button(L10n.tr("preferences.general.quit", default: "Quit CodexBar")) { NSApp.terminate(nil) } .buttonStyle(.borderedProminent) .controlSize(.large) } @@ -119,7 +117,7 @@ struct GeneralPane: View { let name = ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName guard provider == .claude || provider == .codex else { - return Text("\(name): unsupported") + return Text(L10n.tr("preferences.general.costStatus.unsupported", default: "%@: unsupported", name)) .font(.footnote) .foregroundStyle(.tertiary) } @@ -133,20 +131,20 @@ struct GeneralPane: View { formatter.unitsStyle = .abbreviated return formatter.string(from: seconds).map { " (\($0))" } ?? "" }() - return Text("\(name): fetching…\(elapsed)") + return Text(L10n.tr("preferences.general.costStatus.fetching", default: "%@: 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(L10n.tr("preferences.general.costStatus.snapshot", default: "%@: %@ · 30d %@", 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(L10n.tr("preferences.general.costStatus.error", default: "%@: %@", name, truncated)) .font(.footnote) .foregroundStyle(.tertiary) } @@ -154,11 +152,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(L10n.tr("preferences.general.costStatus.lastAttempt", default: "%@: last attempt %@", name, when)) .font(.footnote) .foregroundStyle(.tertiary) } - return Text("\(name): no data yet") + return Text(L10n.tr("preferences.general.costStatus.noData", default: "%@: no data yet", name)) .font(.footnote) .foregroundStyle(.tertiary) } diff --git a/Sources/CodexBar/PreferencesView.swift b/Sources/CodexBar/PreferencesView.swift index a6f893950..0cfdb5fbb 100644 --- a/Sources/CodexBar/PreferencesView.swift +++ b/Sources/CodexBar/PreferencesView.swift @@ -1,4 +1,5 @@ import AppKit +import CodexBarCore import SwiftUI enum PreferencesTab: String, Hashable { @@ -34,28 +35,28 @@ 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(L10n.tr("preferences.tab.general", default: "General"), systemImage: "gearshape") } .tag(PreferencesTab.general) ProvidersPane(settings: self.settings, store: self.store) - .tabItem { Label("Providers", systemImage: "square.grid.2x2") } + .tabItem { Label(L10n.tr("preferences.tab.providers", default: "Providers"), systemImage: "square.grid.2x2") } .tag(PreferencesTab.providers) DisplayPane(settings: self.settings, store: self.store) - .tabItem { Label("Display", systemImage: "eye") } + .tabItem { Label(L10n.tr("preferences.tab.display", default: "Display"), systemImage: "eye") } .tag(PreferencesTab.display) AdvancedPane(settings: self.settings) - .tabItem { Label("Advanced", systemImage: "slider.horizontal.3") } + .tabItem { Label(L10n.tr("preferences.tab.advanced", default: "Advanced"), systemImage: "slider.horizontal.3") } .tag(PreferencesTab.advanced) AboutPane(updater: self.updater) - .tabItem { Label("About", systemImage: "info.circle") } + .tabItem { Label(L10n.tr("preferences.tab.about", default: "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(L10n.tr("preferences.tab.debug", default: "Debug"), systemImage: "ladybug") } .tag(PreferencesTab.debug) } } diff --git a/Sources/CodexBar/Providers/Alibaba/AlibabaCodingPlanProviderImplementation.swift b/Sources/CodexBar/Providers/Alibaba/AlibabaCodingPlanProviderImplementation.swift index eb198478e..eaabbb738 100644 --- a/Sources/CodexBar/Providers/Alibaba/AlibabaCodingPlanProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Alibaba/AlibabaCodingPlanProviderImplementation.swift @@ -53,16 +53,16 @@ struct AlibabaCodingPlanProviderImplementation: ProviderImplementation { ProviderCookieSourceUI.subtitle( source: context.settings.alibabaCodingPlanCookieSource, keychainDisabled: context.settings.debugDisableKeychainAccess, - auto: "Automatic imports browser cookies from Model Studio/Bailian.", - manual: "Paste a Cookie header from modelstudio.console.alibabacloud.com.", - off: "Alibaba cookies are disabled.") + auto: L10n.tr("provider.settings.cookieSource.alibabaAuto", default: "Automatic imports browser cookies from Model Studio/Bailian."), + manual: L10n.tr("provider.settings.alibaba.manualCookie", default: "Paste a Cookie header from modelstudio.console.alibabacloud.com."), + off: L10n.tr("provider.settings.alibaba.cookiesDisabled", default: "Alibaba cookies are disabled.")) } return [ ProviderSettingsPickerDescriptor( id: "alibaba-coding-plan-cookie-source", - title: "Cookie source", - subtitle: "Automatic imports browser cookies from Model Studio/Bailian.", + title: L10n.tr("provider.settings.cookieSource.title", default: "Cookie source"), + subtitle: L10n.tr("provider.settings.cookieSource.alibabaAuto", default: "Automatic imports browser cookies from Model Studio/Bailian."), dynamicSubtitle: cookieSubtitle, binding: cookieBinding, options: cookieOptions, @@ -71,12 +71,12 @@ struct AlibabaCodingPlanProviderImplementation: ProviderImplementation { trailingText: { guard let entry = CookieHeaderCache.load(provider: .alibaba) else { return nil } let when = entry.storedAt.relativeDescription() - return "Cached: \(entry.sourceLabel) • \(when)" + return L10n.tr("provider.settings.cached", default: "Cached: %@ • %@", entry.sourceLabel, when) }), ProviderSettingsPickerDescriptor( id: "alibaba-coding-plan-region", - title: "Gateway region", - subtitle: "Use international or China mainland console gateways for quota fetches.", + title: L10n.tr("provider.settings.gatewayRegion.title", default: "Gateway region"), + subtitle: L10n.tr("provider.settings.alibabaRegion.subtitle", default: "Use international or China mainland console gateways for quota fetches."), binding: binding, options: options, isVisible: nil, @@ -89,15 +89,15 @@ struct AlibabaCodingPlanProviderImplementation: ProviderImplementation { [ ProviderSettingsFieldDescriptor( id: "alibaba-coding-plan-api-key", - title: "API key", - subtitle: "Stored in ~/.codexbar/config.json. Paste your Coding Plan API key from Model Studio.", + title: L10n.tr("provider.settings.apiKey.title", default: "API key"), + subtitle: L10n.tr("provider.settings.alibabaAPIKey.subtitle", default: "Stored in ~/.codexbar/config.json. Paste your Coding Plan API key from Model Studio."), kind: .secure, placeholder: "cpk-...", binding: context.stringBinding(\.alibabaCodingPlanAPIToken), actions: [ ProviderSettingsActionDescriptor( id: "alibaba-coding-plan-open-dashboard", - title: "Open Coding Plan", + title: L10n.tr("provider.settings.openCodingPlan", default: "Open Coding Plan"), style: .link, isVisible: nil, perform: { @@ -108,7 +108,7 @@ struct AlibabaCodingPlanProviderImplementation: ProviderImplementation { onActivate: { context.settings.ensureAlibabaCodingPlanAPITokenLoaded() }), ProviderSettingsFieldDescriptor( id: "alibaba-coding-plan-cookie", - title: "Cookie header", + title: L10n.tr("provider.settings.cookieHeader.title", default: "Cookie header"), subtitle: "", kind: .secure, placeholder: "Cookie: ...", @@ -116,7 +116,7 @@ struct AlibabaCodingPlanProviderImplementation: ProviderImplementation { actions: [ ProviderSettingsActionDescriptor( id: "alibaba-coding-plan-open-dashboard-cookie", - title: "Open Coding Plan", + title: L10n.tr("provider.settings.openCodingPlan", default: "Open Coding Plan"), style: .link, isVisible: nil, perform: { diff --git a/Sources/CodexBar/Providers/Amp/AmpProviderImplementation.swift b/Sources/CodexBar/Providers/Amp/AmpProviderImplementation.swift index 25ca6c932..b8c1a0fba 100644 --- a/Sources/CodexBar/Providers/Amp/AmpProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Amp/AmpProviderImplementation.swift @@ -34,16 +34,16 @@ struct AmpProviderImplementation: ProviderImplementation { ProviderCookieSourceUI.subtitle( source: context.settings.ampCookieSource, keychainDisabled: context.settings.debugDisableKeychainAccess, - auto: "Automatic imports browser cookies.", - manual: "Paste a Cookie header or cURL capture from Amp settings.", - off: "Amp cookies are disabled.") + auto: L10n.tr("provider.settings.cookieSource.autoBrowserCookies", default: "Automatic imports browser cookies."), + manual: L10n.tr("provider.settings.amp.manualCookie", default: "Paste a Cookie header or cURL capture from Amp settings."), + off: L10n.tr("provider.settings.amp.cookiesDisabled", default: "Amp cookies are disabled.")) } return [ ProviderSettingsPickerDescriptor( id: "amp-cookie-source", - title: "Cookie source", - subtitle: "Automatic imports browser cookies.", + title: L10n.tr("provider.settings.cookieSource.title", default: "Cookie source"), + subtitle: L10n.tr("provider.settings.cookieSource.autoBrowserCookies", default: "Automatic imports browser cookies."), dynamicSubtitle: cookieSubtitle, binding: cookieBinding, options: cookieOptions, @@ -65,7 +65,7 @@ struct AmpProviderImplementation: ProviderImplementation { actions: [ ProviderSettingsActionDescriptor( id: "amp-open-settings", - title: "Open Amp Settings", + title: L10n.tr("provider.settings.openAmpSettings", default: "Open Amp Settings"), style: .link, isVisible: nil, perform: { diff --git a/Sources/CodexBar/Providers/Augment/AugmentProviderImplementation.swift b/Sources/CodexBar/Providers/Augment/AugmentProviderImplementation.swift index c1529bd58..b4dd601f5 100644 --- a/Sources/CodexBar/Providers/Augment/AugmentProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Augment/AugmentProviderImplementation.swift @@ -52,16 +52,16 @@ struct AugmentProviderImplementation: ProviderImplementation { ProviderCookieSourceUI.subtitle( source: context.settings.augmentCookieSource, keychainDisabled: context.settings.debugDisableKeychainAccess, - auto: "Automatic imports browser cookies.", - manual: "Paste a Cookie header or cURL capture from the Augment dashboard.", - off: "Augment cookies are disabled.") + auto: L10n.tr("provider.settings.cookieSource.autoBrowserCookies", default: "Automatic imports browser cookies."), + manual: L10n.tr("provider.settings.augment.manualCookie", default: "Paste a Cookie header or cURL capture from the Augment dashboard."), + off: L10n.tr("provider.settings.augment.cookiesDisabled", default: "Augment cookies are disabled.")) } return [ ProviderSettingsPickerDescriptor( id: "augment-cookie-source", - title: "Cookie source", - subtitle: "Automatic imports browser cookies.", + title: L10n.tr("provider.settings.cookieSource.title", default: "Cookie source"), + subtitle: L10n.tr("provider.settings.cookieSource.autoBrowserCookies", default: "Automatic imports browser cookies."), dynamicSubtitle: cookieSubtitle, binding: cookieBinding, options: cookieOptions, @@ -70,7 +70,7 @@ struct AugmentProviderImplementation: ProviderImplementation { trailingText: { guard let entry = CookieHeaderCache.load(provider: .augment) else { return nil } let when = entry.storedAt.relativeDescription() - return "Cached: \(entry.sourceLabel) • \(when)" + return L10n.tr("provider.settings.cached", default: "Cached: %@ • %@", entry.sourceLabel, when) }), ] } @@ -83,14 +83,14 @@ struct AugmentProviderImplementation: ProviderImplementation { @MainActor func appendActionMenuEntries(context: ProviderMenuActionContext, entries: inout [ProviderMenuEntry]) { - entries.append(.action("Refresh Session", .refreshAugmentSession)) + entries.append(.action(L10n.tr("provider.menu.refreshSession", default: "Refresh Session"), .refreshAugmentSession)) if let error = context.store.error(for: .augment) { if error.contains("session has expired") || error.contains("No Augment session cookie found") { entries.append(.action( - "Open Augment (Log Out & Back In)", + L10n.tr("provider.menu.openAugmentRelogin", default: "Open Augment (Log Out & Back In)"), .loginToProvider(url: "https://app.augmentcode.com"))) } } diff --git a/Sources/CodexBar/Providers/Claude/ClaudeProviderImplementation.swift b/Sources/CodexBar/Providers/Claude/ClaudeProviderImplementation.swift index de1bdffc7..8a2b11577 100644 --- a/Sources/CodexBar/Providers/Claude/ClaudeProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Claude/ClaudeProviderImplementation.swift @@ -65,9 +65,9 @@ struct ClaudeProviderImplementation: ProviderImplementation { @MainActor func settingsToggles(context: ProviderSettingsContext) -> [ProviderSettingsToggleDescriptor] { let subtitle = if context.settings.debugDisableKeychainAccess { - "Inactive while \"Disable Keychain access\" is enabled in Advanced." + L10n.tr("provider.settings.avoidKeychainPrompts.inactive", default: "Inactive while \"Disable Keychain access\" is enabled in Advanced.") } else { - "Use /usr/bin/security to read Claude credentials and avoid CodexBar keychain prompts." + L10n.tr("provider.settings.avoidKeychainPrompts.subtitle", default: "Use /usr/bin/security to read Claude credentials and avoid CodexBar keychain prompts.") } let promptFreeBinding = Binding( @@ -80,7 +80,7 @@ struct ClaudeProviderImplementation: ProviderImplementation { return [ ProviderSettingsToggleDescriptor( id: "claude-oauth-prompt-free-credentials", - title: "Avoid Keychain prompts (experimental)", + title: L10n.tr("provider.settings.avoidKeychainPrompts.title", default: "Avoid Keychain prompts (experimental)"), subtitle: subtitle, binding: promptFreeBinding, statusText: nil, @@ -120,35 +120,34 @@ struct ClaudeProviderImplementation: ProviderImplementation { let keychainPromptPolicyOptions: [ProviderSettingsPickerOption] = [ ProviderSettingsPickerOption( id: ClaudeOAuthKeychainPromptMode.never.rawValue, - title: "Never prompt"), + title: L10n.tr("provider.settings.neverPrompt", default: "Never prompt")), ProviderSettingsPickerOption( id: ClaudeOAuthKeychainPromptMode.onlyOnUserAction.rawValue, - title: "Only on user action"), + title: L10n.tr("provider.settings.onlyOnUserAction", default: "Only on user action")), ProviderSettingsPickerOption( id: ClaudeOAuthKeychainPromptMode.always.rawValue, - title: "Always allow prompts"), + title: L10n.tr("provider.settings.alwaysAllowPrompts", default: "Always allow prompts")), ] let cookieSubtitle: () -> String? = { ProviderCookieSourceUI.subtitle( source: context.settings.claudeCookieSource, keychainDisabled: context.settings.debugDisableKeychainAccess, - auto: "Automatic imports browser cookies for the web API.", - manual: "Paste a Cookie header from a claude.ai request.", - off: "Claude cookies are disabled.") + auto: L10n.tr("provider.settings.claudeCookies.subtitle", default: "Automatic imports browser cookies for the web API."), + manual: L10n.tr("provider.settings.claude.manualCookie", default: "Paste a Cookie header from a claude.ai request."), + off: L10n.tr("provider.settings.claude.cookiesDisabled", default: "Claude cookies are disabled.")) } let keychainPromptPolicySubtitle: () -> String? = { if context.settings.debugDisableKeychainAccess { - return "Global Keychain access is disabled in Advanced, so this setting is currently inactive." + return L10n.tr("provider.settings.keychainPromptPolicy.disabled", default: "Global Keychain access is disabled in Advanced, so this setting is currently inactive.") } - return "Controls Claude OAuth Keychain prompts when experimental reader mode is off. Choosing " + - "\"Never prompt\" can make OAuth unavailable; use Web/CLI when needed." + return L10n.tr("provider.settings.keychainPromptPolicy.dynamic", default: "Controls Claude OAuth Keychain prompts when experimental reader mode is off. Choosing \"Never prompt\" can make OAuth unavailable; use Web/CLI when needed.") } return [ ProviderSettingsPickerDescriptor( id: "claude-usage-source", - title: "Usage source", - subtitle: "Auto falls back to the next source if the preferred one fails.", + title: L10n.tr("provider.settings.usageSource.title", default: "Usage source"), + subtitle: L10n.tr("provider.settings.usageSource.autoFallback", default: "Auto falls back to the next source if the preferred one fails."), binding: usageBinding, options: usageOptions, isVisible: nil, @@ -160,8 +159,8 @@ struct ClaudeProviderImplementation: ProviderImplementation { }), ProviderSettingsPickerDescriptor( id: "claude-keychain-prompt-policy", - title: "Keychain prompt policy", - subtitle: "Applies only to the Security.framework OAuth keychain reader.", + title: L10n.tr("provider.settings.keychainPromptPolicy.title", default: "Keychain prompt policy"), + subtitle: L10n.tr("provider.settings.keychainPromptPolicy.subtitle", default: "Applies only to the Security.framework OAuth keychain reader."), dynamicSubtitle: keychainPromptPolicySubtitle, binding: keychainPromptPolicyBinding, options: keychainPromptPolicyOptions, @@ -170,8 +169,8 @@ struct ClaudeProviderImplementation: ProviderImplementation { onChange: nil), ProviderSettingsPickerDescriptor( id: "claude-cookie-source", - title: "Claude cookies", - subtitle: "Automatic imports browser cookies for the web API.", + title: L10n.tr("provider.settings.claudeCookies.title", default: "Claude cookies"), + subtitle: L10n.tr("provider.settings.claudeCookies.subtitle", default: "Automatic imports browser cookies for the web API."), dynamicSubtitle: cookieSubtitle, binding: cookieBinding, options: cookieOptions, @@ -180,7 +179,7 @@ struct ClaudeProviderImplementation: ProviderImplementation { trailingText: { guard let entry = CookieHeaderCache.load(provider: .claude) else { return nil } let when = entry.storedAt.relativeDescription() - return "Cached: \(entry.sourceLabel) • \(when)" + return L10n.tr("provider.settings.cached", default: "Cached: %@ • %@", entry.sourceLabel, when) }), ] } diff --git a/Sources/CodexBar/Providers/Codex/CodexProviderImplementation.swift b/Sources/CodexBar/Providers/Codex/CodexProviderImplementation.swift index 61aa3a501..3caaccaae 100644 --- a/Sources/CodexBar/Providers/Codex/CodexProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Codex/CodexProviderImplementation.swift @@ -73,8 +73,8 @@ struct CodexProviderImplementation: ProviderImplementation { return [ ProviderSettingsToggleDescriptor( id: "codex-historical-tracking", - title: "Historical tracking", - subtitle: "Stores local Codex usage history (8 weeks) to personalize Pace predictions.", + title: L10n.tr("provider.settings.historicalTracking.title", default: "Historical tracking"), + subtitle: L10n.tr("provider.settings.historicalTracking.subtitle", default: "Stores local Codex usage history (8 weeks) to personalize Pace predictions."), binding: context.boolBinding(\.historicalTrackingEnabled), statusText: nil, actions: [], @@ -84,8 +84,8 @@ struct CodexProviderImplementation: ProviderImplementation { onAppearWhenEnabled: nil), ProviderSettingsToggleDescriptor( id: "codex-openai-web-extras", - title: "OpenAI web extras", - subtitle: "Show usage breakdown, credits history, and code review via chatgpt.com.", + title: L10n.tr("provider.settings.openAIWebExtras.title", default: "OpenAI web extras"), + subtitle: L10n.tr("provider.settings.openAIWebExtras.subtitle", default: "Show usage breakdown, credits history, and code review via chatgpt.com."), binding: extrasBinding, statusText: nil, actions: [], @@ -120,16 +120,16 @@ struct CodexProviderImplementation: ProviderImplementation { ProviderCookieSourceUI.subtitle( source: context.settings.codexCookieSource, keychainDisabled: context.settings.debugDisableKeychainAccess, - auto: "Automatic imports browser cookies for dashboard extras.", - manual: "Paste a Cookie header from a chatgpt.com request.", - off: "Disable OpenAI dashboard cookie usage.") + auto: L10n.tr("provider.settings.openAICookies.subtitle", default: "Automatic imports browser cookies for dashboard extras."), + manual: L10n.tr("provider.settings.codex.manualCookie", default: "Paste a Cookie header from a chatgpt.com request."), + off: L10n.tr("provider.settings.codex.cookiesDisabled", default: "Disable OpenAI dashboard cookie usage.")) } return [ ProviderSettingsPickerDescriptor( id: "codex-usage-source", - title: "Usage source", - subtitle: "Auto falls back to the next source if the preferred one fails.", + title: L10n.tr("provider.settings.usageSource.title", default: "Usage source"), + subtitle: L10n.tr("provider.settings.usageSource.autoFallback", default: "Auto falls back to the next source if the preferred one fails."), binding: usageBinding, options: usageOptions, isVisible: nil, @@ -141,8 +141,8 @@ struct CodexProviderImplementation: ProviderImplementation { }), ProviderSettingsPickerDescriptor( id: "codex-cookie-source", - title: "OpenAI cookies", - subtitle: "Automatic imports browser cookies for dashboard extras.", + title: L10n.tr("provider.settings.openAICookies.title", default: "OpenAI cookies"), + subtitle: L10n.tr("provider.settings.openAICookies.subtitle", default: "Automatic imports browser cookies for dashboard extras."), dynamicSubtitle: cookieSubtitle, binding: cookieBinding, options: cookieOptions, @@ -151,7 +151,7 @@ struct CodexProviderImplementation: ProviderImplementation { trailingText: { guard let entry = CookieHeaderCache.load(provider: .codex) else { return nil } let when = entry.storedAt.relativeDescription() - return "Cached: \(entry.sourceLabel) • \(when)" + return L10n.tr("provider.settings.cached", default: "Cached: %@ • %@", entry.sourceLabel, when) }), ] } diff --git a/Sources/CodexBar/Providers/Copilot/CopilotProviderImplementation.swift b/Sources/CodexBar/Providers/Copilot/CopilotProviderImplementation.swift index 986d81f2f..e955a2533 100644 --- a/Sources/CodexBar/Providers/Copilot/CopilotProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Copilot/CopilotProviderImplementation.swift @@ -29,15 +29,15 @@ struct CopilotProviderImplementation: ProviderImplementation { [ ProviderSettingsFieldDescriptor( id: "copilot-api-token", - title: "GitHub Login", - subtitle: "Requires authentication via GitHub Device Flow.", + title: L10n.tr("provider.settings.githubLogin.title", default: "GitHub Login"), + subtitle: L10n.tr("provider.settings.githubLogin.subtitle", default: "Requires authentication via GitHub Device Flow."), kind: .secure, placeholder: "Sign in via button below", binding: context.stringBinding(\.copilotAPIToken), actions: [ ProviderSettingsActionDescriptor( id: "copilot-login", - title: "Sign in with GitHub", + title: L10n.tr("provider.settings.signInWithGitHub", default: "Sign in with GitHub"), style: .bordered, isVisible: { context.settings.copilotAPIToken.isEmpty }, perform: { @@ -45,7 +45,7 @@ struct CopilotProviderImplementation: ProviderImplementation { }), ProviderSettingsActionDescriptor( id: "copilot-relogin", - title: "Sign in again", + title: L10n.tr("provider.settings.signInAgain", default: "Sign in again"), style: .link, isVisible: { !context.settings.copilotAPIToken.isEmpty }, perform: { diff --git a/Sources/CodexBar/Providers/Cursor/CursorProviderImplementation.swift b/Sources/CodexBar/Providers/Cursor/CursorProviderImplementation.swift index 48db614f9..31d9ec2e9 100644 --- a/Sources/CodexBar/Providers/Cursor/CursorProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Cursor/CursorProviderImplementation.swift @@ -53,16 +53,16 @@ struct CursorProviderImplementation: ProviderImplementation { ProviderCookieSourceUI.subtitle( source: context.settings.cursorCookieSource, keychainDisabled: context.settings.debugDisableKeychainAccess, - auto: "Automatic imports browser cookies or stored sessions.", - manual: "Paste a Cookie header from a cursor.com request.", - off: "Cursor cookies are disabled.") + auto: L10n.tr("provider.settings.cookieSource.cursorAuto", default: "Automatic imports browser cookies or stored sessions."), + manual: L10n.tr("provider.settings.cursor.manualCookie", default: "Paste a Cookie header from a cursor.com request."), + off: L10n.tr("provider.settings.cursor.cookiesDisabled", default: "Cursor cookies are disabled.")) } return [ ProviderSettingsPickerDescriptor( id: "cursor-cookie-source", - title: "Cookie source", - subtitle: "Automatic imports browser cookies or stored sessions.", + title: L10n.tr("provider.settings.cookieSource.title", default: "Cookie source"), + subtitle: L10n.tr("provider.settings.cookieSource.cursorAuto", default: "Automatic imports browser cookies or stored sessions."), dynamicSubtitle: cookieSubtitle, binding: cookieBinding, options: cookieOptions, @@ -71,7 +71,7 @@ struct CursorProviderImplementation: ProviderImplementation { trailingText: { guard let entry = CookieHeaderCache.load(provider: .cursor) else { return nil } let when = entry.storedAt.relativeDescription() - return "Cached: \(entry.sourceLabel) • \(when)" + return L10n.tr("provider.settings.cached", default: "Cached: %@ • %@", entry.sourceLabel, when) }), ] } diff --git a/Sources/CodexBar/Providers/Factory/FactoryProviderImplementation.swift b/Sources/CodexBar/Providers/Factory/FactoryProviderImplementation.swift index d8d2d2024..91052f772 100644 --- a/Sources/CodexBar/Providers/Factory/FactoryProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Factory/FactoryProviderImplementation.swift @@ -48,16 +48,16 @@ struct FactoryProviderImplementation: ProviderImplementation { ProviderCookieSourceUI.subtitle( source: context.settings.factoryCookieSource, keychainDisabled: context.settings.debugDisableKeychainAccess, - auto: "Automatic imports browser cookies and WorkOS tokens.", - manual: "Paste a Cookie header from app.factory.ai.", - off: "Factory cookies are disabled.") + auto: L10n.tr("provider.settings.cookieSource.factoryAuto", default: "Automatic imports browser cookies and WorkOS tokens."), + manual: L10n.tr("provider.settings.factory.manualCookie", default: "Paste a Cookie header from app.factory.ai."), + off: L10n.tr("provider.settings.factory.cookiesDisabled", default: "Factory cookies are disabled.")) } return [ ProviderSettingsPickerDescriptor( id: "factory-cookie-source", - title: "Cookie source", - subtitle: "Automatic imports browser cookies and WorkOS tokens.", + title: L10n.tr("provider.settings.cookieSource.title", default: "Cookie source"), + subtitle: L10n.tr("provider.settings.cookieSource.factoryAuto", default: "Automatic imports browser cookies and WorkOS tokens."), dynamicSubtitle: cookieSubtitle, binding: cookieBinding, options: cookieOptions, @@ -66,7 +66,7 @@ struct FactoryProviderImplementation: ProviderImplementation { trailingText: { guard let entry = CookieHeaderCache.load(provider: .factory) else { return nil } let when = entry.storedAt.relativeDescription() - return "Cached: \(entry.sourceLabel) • \(when)" + return L10n.tr("provider.settings.cached", default: "Cached: %@ • %@", entry.sourceLabel, when) }), ] } diff --git a/Sources/CodexBar/Providers/JetBrains/JetBrainsProviderImplementation.swift b/Sources/CodexBar/Providers/JetBrains/JetBrainsProviderImplementation.swift index 7e7096f50..11b611d54 100644 --- a/Sources/CodexBar/Providers/JetBrains/JetBrainsProviderImplementation.swift +++ b/Sources/CodexBar/Providers/JetBrains/JetBrainsProviderImplementation.swift @@ -19,7 +19,7 @@ struct JetBrainsProviderImplementation: ProviderImplementation { guard !detectedIDEs.isEmpty else { return [] } var options: [ProviderSettingsPickerOption] = [ - ProviderSettingsPickerOption(id: "", title: "Auto-detect"), + ProviderSettingsPickerOption(id: "", title: L10n.tr("provider.settings.autoDetect", default: "Auto-detect")), ] for ide in detectedIDEs { options.append(ProviderSettingsPickerOption(id: ide.basePath, title: ide.displayName)) @@ -28,8 +28,8 @@ struct JetBrainsProviderImplementation: ProviderImplementation { return [ ProviderSettingsPickerDescriptor( id: "jetbrains.ide", - title: "JetBrains IDE", - subtitle: "Select the IDE to monitor", + title: L10n.tr("provider.settings.jetbrainsIDE.title", default: "JetBrains IDE"), + subtitle: L10n.tr("provider.settings.jetbrainsIDE.subtitle", default: "Select the IDE to monitor"), binding: context.stringBinding(\.jetbrainsIDEBasePath), options: options, isVisible: nil, @@ -50,8 +50,8 @@ struct JetBrainsProviderImplementation: ProviderImplementation { [ ProviderSettingsFieldDescriptor( id: "jetbrains.customPath", - title: "Custom Path", - subtitle: "Override auto-detection with a custom IDE base path", + title: L10n.tr("provider.settings.customPath.title", default: "Custom Path"), + subtitle: L10n.tr("provider.settings.customPath.subtitle", default: "Override auto-detection with a custom IDE base path"), kind: .plain, placeholder: "~/Library/Application Support/JetBrains/IntelliJIdea2024.3", binding: context.stringBinding(\.jetbrainsIDEBasePath), diff --git a/Sources/CodexBar/Providers/Kilo/KiloProviderImplementation.swift b/Sources/CodexBar/Providers/Kilo/KiloProviderImplementation.swift index e2bdb3cfa..7406842bd 100644 --- a/Sources/CodexBar/Providers/Kilo/KiloProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Kilo/KiloProviderImplementation.swift @@ -54,8 +54,8 @@ struct KiloProviderImplementation: ProviderImplementation { return [ ProviderSettingsPickerDescriptor( id: "kilo-usage-source", - title: "Usage source", - subtitle: "Auto uses API first, then falls back to CLI on auth failures.", + title: L10n.tr("provider.settings.usageSource.title", default: "Usage source"), + subtitle: L10n.tr("provider.settings.usageSource.kilo.subtitle", default: "Auto uses API first, then falls back to CLI on auth failures."), binding: usageBinding, options: usageOptions, isVisible: nil, @@ -73,9 +73,8 @@ struct KiloProviderImplementation: ProviderImplementation { [ ProviderSettingsFieldDescriptor( id: "kilo-api-key", - title: "API key", - subtitle: "Stored in ~/.codexbar/config.json. You can also provide KILO_API_KEY or " - + "~/.local/share/kilo/auth.json (kilo.access).", + title: L10n.tr("provider.settings.apiKey.title", default: "API key"), + subtitle: L10n.tr("provider.settings.kiloAPIKey.subtitle", default: "Stored in ~/.codexbar/config.json. You can also provide KILO_API_KEY or ~/.local/share/kilo/auth.json (kilo.access)."), kind: .secure, placeholder: "kilo_...", binding: context.stringBinding(\.kiloAPIToken), diff --git a/Sources/CodexBar/Providers/Kimi/KimiProviderImplementation.swift b/Sources/CodexBar/Providers/Kimi/KimiProviderImplementation.swift index d48511963..45b898688 100644 --- a/Sources/CodexBar/Providers/Kimi/KimiProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Kimi/KimiProviderImplementation.swift @@ -39,16 +39,16 @@ struct KimiProviderImplementation: ProviderImplementation { ProviderCookieSourceUI.subtitle( source: context.settings.kimiCookieSource, keychainDisabled: context.settings.debugDisableKeychainAccess, - auto: "Automatic imports browser cookies.", - manual: "Paste a cookie header or the kimi-auth token value.", - off: "Kimi cookies are disabled.") + auto: L10n.tr("provider.settings.cookieSource.autoBrowserCookies", default: "Automatic imports browser cookies."), + manual: L10n.tr("provider.settings.kimi.manualCookie", default: "Paste a cookie header or the kimi-auth token value."), + off: L10n.tr("provider.settings.kimi.cookiesDisabled", default: "Kimi cookies are disabled.")) } return [ ProviderSettingsPickerDescriptor( id: "kimi-cookie-source", - title: "Cookie source", - subtitle: "Automatic imports browser cookies.", + title: L10n.tr("provider.settings.cookieSource.title", default: "Cookie source"), + subtitle: L10n.tr("provider.settings.cookieSource.autoBrowserCookies", default: "Automatic imports browser cookies."), dynamicSubtitle: subtitle, binding: cookieBinding, options: options, @@ -70,7 +70,7 @@ struct KimiProviderImplementation: ProviderImplementation { actions: [ ProviderSettingsActionDescriptor( id: "kimi-open-console", - title: "Open Console", + title: L10n.tr("provider.settings.openConsole", default: "Open Console"), style: .link, isVisible: nil, perform: { diff --git a/Sources/CodexBar/Providers/KimiK2/KimiK2ProviderImplementation.swift b/Sources/CodexBar/Providers/KimiK2/KimiK2ProviderImplementation.swift index 9209bba71..404e69d6a 100644 --- a/Sources/CodexBar/Providers/KimiK2/KimiK2ProviderImplementation.swift +++ b/Sources/CodexBar/Providers/KimiK2/KimiK2ProviderImplementation.swift @@ -17,15 +17,15 @@ struct KimiK2ProviderImplementation: ProviderImplementation { [ ProviderSettingsFieldDescriptor( id: "kimi-k2-api-token", - title: "API key", - subtitle: "Stored in ~/.codexbar/config.json. Generate one at kimi-k2.ai.", + title: L10n.tr("provider.settings.apiKey.title", default: "API key"), + subtitle: L10n.tr("provider.settings.kimik2APIKey.subtitle", default: "Stored in ~/.codexbar/config.json. Generate one at kimi-k2.ai."), kind: .secure, placeholder: "Paste API key…", binding: context.stringBinding(\.kimiK2APIToken), actions: [ ProviderSettingsActionDescriptor( id: "kimi-k2-open-api-keys", - title: "Open API Keys", + title: L10n.tr("provider.settings.openAPIKeys", default: "Open API Keys"), style: .link, isVisible: nil, perform: { diff --git a/Sources/CodexBar/Providers/MiniMax/MiniMaxProviderImplementation.swift b/Sources/CodexBar/Providers/MiniMax/MiniMaxProviderImplementation.swift index 6b7432edc..c7628f352 100644 --- a/Sources/CodexBar/Providers/MiniMax/MiniMaxProviderImplementation.swift +++ b/Sources/CodexBar/Providers/MiniMax/MiniMaxProviderImplementation.swift @@ -64,9 +64,9 @@ struct MiniMaxProviderImplementation: ProviderImplementation { ProviderCookieSourceUI.subtitle( source: context.settings.minimaxCookieSource, keychainDisabled: context.settings.debugDisableKeychainAccess, - auto: "Automatic imports browser cookies and local storage tokens.", - manual: "Paste a Cookie header or cURL capture from the Coding Plan page.", - off: "MiniMax cookies are disabled.") + auto: L10n.tr("provider.settings.cookieSource.minimaxAuto", default: "Automatic imports browser cookies and local storage tokens."), + manual: L10n.tr("provider.settings.minimax.manualCookie", default: "Paste a Cookie header or cURL capture from the Coding Plan page."), + off: L10n.tr("provider.settings.minimax.cookiesDisabled", default: "MiniMax cookies are disabled.")) } let regionBinding = Binding( @@ -81,8 +81,8 @@ struct MiniMaxProviderImplementation: ProviderImplementation { return [ ProviderSettingsPickerDescriptor( id: "minimax-cookie-source", - title: "Cookie source", - subtitle: "Automatic imports browser cookies and local storage tokens.", + title: L10n.tr("provider.settings.cookieSource.title", default: "Cookie source"), + subtitle: L10n.tr("provider.settings.cookieSource.minimaxAuto", default: "Automatic imports browser cookies and local storage tokens."), dynamicSubtitle: cookieSubtitle, binding: cookieBinding, options: cookieOptions, @@ -91,12 +91,12 @@ struct MiniMaxProviderImplementation: ProviderImplementation { trailingText: { guard let entry = CookieHeaderCache.load(provider: .minimax) else { return nil } let when = entry.storedAt.relativeDescription() - return "Cached: \(entry.sourceLabel) • \(when)" + return L10n.tr("provider.settings.cached", default: "Cached: %@ • %@", entry.sourceLabel, when) }), ProviderSettingsPickerDescriptor( id: "minimax-region", - title: "API region", - subtitle: "Choose the MiniMax host (global .io or China mainland .com).", + title: L10n.tr("provider.settings.apiRegion.title", default: "API region"), + subtitle: L10n.tr("provider.settings.minimaxRegion.subtitle", default: "Choose the MiniMax host (global .io or China mainland .com)."), binding: regionBinding, options: regionOptions, isVisible: nil, @@ -114,15 +114,15 @@ struct MiniMaxProviderImplementation: ProviderImplementation { return [ ProviderSettingsFieldDescriptor( id: "minimax-api-token", - title: "API token", - subtitle: "Stored in ~/.codexbar/config.json. Paste your MiniMax API key.", + title: L10n.tr("provider.settings.apiToken.title", default: "API token"), + subtitle: L10n.tr("provider.settings.minimaxAPIToken.subtitle", default: "Stored in ~/.codexbar/config.json. Paste your MiniMax API key."), kind: .secure, placeholder: "Paste API token…", binding: context.stringBinding(\.minimaxAPIToken), actions: [ ProviderSettingsActionDescriptor( id: "minimax-open-dashboard", - title: "Open Coding Plan", + title: L10n.tr("provider.settings.openCodingPlan", default: "Open Coding Plan"), style: .link, isVisible: nil, perform: { @@ -133,7 +133,7 @@ struct MiniMaxProviderImplementation: ProviderImplementation { onActivate: { context.settings.ensureMiniMaxAPITokenLoaded() }), ProviderSettingsFieldDescriptor( id: "minimax-cookie", - title: "Cookie header", + title: L10n.tr("provider.settings.cookieHeader.title", default: "Cookie header"), subtitle: "", kind: .secure, placeholder: "Cookie: …", @@ -141,7 +141,7 @@ struct MiniMaxProviderImplementation: ProviderImplementation { actions: [ ProviderSettingsActionDescriptor( id: "minimax-open-dashboard-cookie", - title: "Open Coding Plan", + title: L10n.tr("provider.settings.openCodingPlan", default: "Open Coding Plan"), style: .link, isVisible: nil, perform: { diff --git a/Sources/CodexBar/Providers/Ollama/OllamaProviderImplementation.swift b/Sources/CodexBar/Providers/Ollama/OllamaProviderImplementation.swift index 99d8582f1..f1362de7e 100644 --- a/Sources/CodexBar/Providers/Ollama/OllamaProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Ollama/OllamaProviderImplementation.swift @@ -48,16 +48,16 @@ struct OllamaProviderImplementation: ProviderImplementation { ProviderCookieSourceUI.subtitle( source: context.settings.ollamaCookieSource, keychainDisabled: context.settings.debugDisableKeychainAccess, - auto: "Automatic imports browser cookies.", - manual: "Paste a Cookie header or cURL capture from Ollama settings.", - off: "Ollama cookies are disabled.") + auto: L10n.tr("provider.settings.cookieSource.autoBrowserCookies", default: "Automatic imports browser cookies."), + manual: L10n.tr("provider.settings.ollama.manualCookie", default: "Paste a Cookie header or cURL capture from Ollama settings."), + off: L10n.tr("provider.settings.ollama.cookiesDisabled", default: "Ollama cookies are disabled.")) } return [ ProviderSettingsPickerDescriptor( id: "ollama-cookie-source", - title: "Cookie source", - subtitle: "Automatic imports browser cookies.", + title: L10n.tr("provider.settings.cookieSource.title", default: "Cookie source"), + subtitle: L10n.tr("provider.settings.cookieSource.autoBrowserCookies", default: "Automatic imports browser cookies."), dynamicSubtitle: cookieSubtitle, binding: cookieBinding, options: cookieOptions, @@ -79,7 +79,7 @@ struct OllamaProviderImplementation: ProviderImplementation { actions: [ ProviderSettingsActionDescriptor( id: "ollama-open-settings", - title: "Open Ollama Settings", + title: L10n.tr("provider.settings.openOllamaSettings", default: "Open Ollama Settings"), style: .link, isVisible: nil, perform: { diff --git a/Sources/CodexBar/Providers/OpenCode/OpenCodeProviderImplementation.swift b/Sources/CodexBar/Providers/OpenCode/OpenCodeProviderImplementation.swift index 5e069f0a1..295452065 100644 --- a/Sources/CodexBar/Providers/OpenCode/OpenCodeProviderImplementation.swift +++ b/Sources/CodexBar/Providers/OpenCode/OpenCodeProviderImplementation.swift @@ -54,16 +54,16 @@ struct OpenCodeProviderImplementation: ProviderImplementation { ProviderCookieSourceUI.subtitle( source: context.settings.opencodeCookieSource, keychainDisabled: context.settings.debugDisableKeychainAccess, - auto: "Automatic imports browser cookies from opencode.ai.", - manual: "Paste a Cookie header captured from the billing page.", - off: "OpenCode cookies are disabled.") + auto: L10n.tr("provider.settings.cookieSource.opencodeAuto", default: "Automatic imports browser cookies from opencode.ai."), + manual: L10n.tr("provider.settings.opencode.manualCookie", default: "Paste a Cookie header captured from the billing page."), + off: L10n.tr("provider.settings.opencode.cookiesDisabled", default: "OpenCode cookies are disabled.")) } return [ ProviderSettingsPickerDescriptor( id: "opencode-cookie-source", - title: "Cookie source", - subtitle: "Automatic imports browser cookies from opencode.ai.", + title: L10n.tr("provider.settings.cookieSource.title", default: "Cookie source"), + subtitle: L10n.tr("provider.settings.cookieSource.opencodeAuto", default: "Automatic imports browser cookies from opencode.ai."), dynamicSubtitle: cookieSubtitle, binding: cookieBinding, options: cookieOptions, @@ -72,7 +72,7 @@ struct OpenCodeProviderImplementation: ProviderImplementation { trailingText: { guard let entry = CookieHeaderCache.load(provider: .opencode) else { return nil } let when = entry.storedAt.relativeDescription() - return "Cached: \(entry.sourceLabel) • \(when)" + return L10n.tr("provider.settings.cached", default: "Cached: %@ • %@", entry.sourceLabel, when) }), ] } @@ -82,8 +82,8 @@ struct OpenCodeProviderImplementation: ProviderImplementation { [ ProviderSettingsFieldDescriptor( id: "opencode-workspace-id", - title: "Workspace ID", - subtitle: "Optional override if workspace lookup fails.", + title: L10n.tr("provider.settings.workspaceID.title", default: "Workspace ID"), + subtitle: L10n.tr("provider.settings.workspaceID.subtitle", default: "Optional override if workspace lookup fails."), kind: .plain, placeholder: "wrk_…", binding: context.stringBinding(\.opencodeWorkspaceID), diff --git a/Sources/CodexBar/Providers/OpenRouter/OpenRouterProviderImplementation.swift b/Sources/CodexBar/Providers/OpenRouter/OpenRouterProviderImplementation.swift index d584a2430..6a231c4ba 100644 --- a/Sources/CodexBar/Providers/OpenRouter/OpenRouterProviderImplementation.swift +++ b/Sources/CodexBar/Providers/OpenRouter/OpenRouterProviderImplementation.swift @@ -42,10 +42,8 @@ struct OpenRouterProviderImplementation: ProviderImplementation { [ ProviderSettingsFieldDescriptor( id: "openrouter-api-key", - title: "API key", - subtitle: "Stored in ~/.codexbar/config.json. " - + "Get your key from openrouter.ai/settings/keys and set a key spending limit " - + "there to enable API key quota tracking.", + title: L10n.tr("provider.settings.apiKey.title", default: "API key"), + subtitle: L10n.tr("provider.settings.openRouterAPIKey.subtitle", default: "Stored in ~/.codexbar/config.json. Get your key from openrouter.ai/settings/keys and set a key spending limit there to enable API key quota tracking."), kind: .secure, placeholder: "sk-or-v1-...", binding: context.stringBinding(\.openRouterAPIToken), diff --git a/Sources/CodexBar/Providers/Synthetic/SyntheticProviderImplementation.swift b/Sources/CodexBar/Providers/Synthetic/SyntheticProviderImplementation.swift index dcef3eb67..6900141f2 100644 --- a/Sources/CodexBar/Providers/Synthetic/SyntheticProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Synthetic/SyntheticProviderImplementation.swift @@ -31,8 +31,8 @@ struct SyntheticProviderImplementation: ProviderImplementation { [ ProviderSettingsFieldDescriptor( id: "synthetic-api-key", - title: "API key", - subtitle: "Stored in ~/.codexbar/config.json. Paste the key from the Synthetic dashboard.", + title: L10n.tr("provider.settings.apiKey.title", default: "API key"), + subtitle: L10n.tr("provider.settings.syntheticAPIKey.subtitle", default: "Stored in ~/.codexbar/config.json. Paste the key from the Synthetic dashboard."), kind: .secure, placeholder: "Paste key…", binding: context.stringBinding(\.syntheticAPIToken), diff --git a/Sources/CodexBar/Providers/Warp/WarpProviderImplementation.swift b/Sources/CodexBar/Providers/Warp/WarpProviderImplementation.swift index e9cb82de9..dc2e05023 100644 --- a/Sources/CodexBar/Providers/Warp/WarpProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Warp/WarpProviderImplementation.swift @@ -17,16 +17,15 @@ struct WarpProviderImplementation: ProviderImplementation { [ ProviderSettingsFieldDescriptor( id: "warp-api-token", - title: "API key", - subtitle: "Stored in ~/.codexbar/config.json. In Warp, open Settings > Platform > API Keys, " - + "then create one.", + title: L10n.tr("provider.settings.apiKey.title", default: "API key"), + subtitle: L10n.tr("provider.settings.warpAPIKey.subtitle", default: "Stored in ~/.codexbar/config.json. In Warp, open Settings > Platform > API Keys, then create one."), kind: .secure, placeholder: "wk-...", binding: context.stringBinding(\.warpAPIToken), actions: [ ProviderSettingsActionDescriptor( id: "warp-open-api-keys", - title: "Open Warp API Key Guide", + title: L10n.tr("provider.settings.openWarpGuide", default: "Open Warp API Key Guide"), style: .link, isVisible: nil, perform: { diff --git a/Sources/CodexBar/Providers/Zai/ZaiProviderImplementation.swift b/Sources/CodexBar/Providers/Zai/ZaiProviderImplementation.swift index d4bc64a9f..cb8344c6a 100644 --- a/Sources/CodexBar/Providers/Zai/ZaiProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Zai/ZaiProviderImplementation.swift @@ -48,8 +48,8 @@ struct ZaiProviderImplementation: ProviderImplementation { return [ ProviderSettingsPickerDescriptor( id: "zai-api-region", - title: "API region", - subtitle: "Use BigModel for the China mainland endpoints (open.bigmodel.cn).", + title: L10n.tr("provider.settings.apiRegion.title", default: "API region"), + subtitle: L10n.tr("provider.settings.zaiRegion.subtitle", default: "Use BigModel for the China mainland endpoints (open.bigmodel.cn)."), binding: binding, options: options, isVisible: nil, diff --git a/Sources/CodexBar/StatusItemController+Actions.swift b/Sources/CodexBar/StatusItemController+Actions.swift index ad65b0879..a2d1b4e17 100644 --- a/Sources/CodexBar/StatusItemController+Actions.swift +++ b/Sources/CodexBar/StatusItemController+Actions.swift @@ -215,18 +215,18 @@ extension StatusItemController { return case .missingBinary: self.presentLoginAlert( - title: "Codex CLI not found", - message: "Install the Codex CLI (npm i -g @openai/codex) and try again.") + title: L10n.tr("login.codex.missingBinary.title", default: "Codex CLI not found"), + message: L10n.tr("login.codex.missingBinary.message", default: "Install the Codex CLI (npm i -g @openai/codex) and try again.")) case let .launchFailed(message): - self.presentLoginAlert(title: "Could not start codex login", message: message) + self.presentLoginAlert(title: L10n.tr("login.codex.launchFailed.title", default: "Could not start codex login"), message: message) case .timedOut: self.presentLoginAlert( - title: "Codex login timed out", + title: L10n.tr("login.codex.timedOut.title", default: "Codex login timed out"), message: self.trimmedLoginOutput(result.output)) case let .failed(status): - let statusLine = "codex login exited with status \(status)." + let statusLine = L10n.tr("login.codex.failed.status", default: "codex login exited with status %@.", String(status)) let message = self.trimmedLoginOutput(result.output.isEmpty ? statusLine : result.output) - self.presentLoginAlert(title: "Codex login failed", message: message) + self.presentLoginAlert(title: L10n.tr("login.codex.failed.title", default: "Codex login failed"), message: message) } } @@ -236,18 +236,18 @@ extension StatusItemController { return case .missingBinary: self.presentLoginAlert( - title: "Claude CLI not found", - message: "Install the Claude CLI (npm i -g @anthropic-ai/claude-code) and try again.") + title: L10n.tr("login.claude.missingBinary.title", default: "Claude CLI not found"), + message: L10n.tr("login.claude.missingBinary.message", default: "Install the Claude CLI (npm i -g @anthropic-ai/claude-code) and try again.")) case let .launchFailed(message): - self.presentLoginAlert(title: "Could not start claude /login", message: message) + self.presentLoginAlert(title: L10n.tr("login.claude.launchFailed.title", default: "Could not start claude /login"), message: message) case .timedOut: self.presentLoginAlert( - title: "Claude login timed out", + title: L10n.tr("login.claude.timedOut.title", default: "Claude login timed out"), message: self.trimmedLoginOutput(result.output)) case let .failed(status): - let statusLine = "claude /login exited with status \(status)." + let statusLine = L10n.tr("login.claude.failed.status", default: "claude /login exited with status %@.", String(status)) let message = self.trimmedLoginOutput(result.output.isEmpty ? statusLine : result.output) - self.presentLoginAlert(title: "Claude login failed", message: message) + self.presentLoginAlert(title: L10n.tr("login.claude.failed.title", default: "Claude login failed"), message: message) } } @@ -295,10 +295,10 @@ extension StatusItemController { nil case .missingBinary: LoginAlertInfo( - title: "Gemini CLI not found", - message: "Install the Gemini CLI (npm i -g @google/gemini-cli) and try again.") + title: L10n.tr("login.gemini.missingBinary.title", default: "Gemini CLI not found"), + message: L10n.tr("login.gemini.missingBinary.message", default: "Install the Gemini CLI (npm i -g @google/gemini-cli) and try again.")) case let .launchFailed(message): - LoginAlertInfo(title: "Could not open Terminal for Gemini", message: message) + LoginAlertInfo(title: L10n.tr("login.gemini.launchFailed.title", default: "Could not open Terminal for Gemini"), message: message) } } @@ -313,7 +313,7 @@ extension StatusItemController { private func trimmedLoginOutput(_ text: String) -> String { let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) let limit = 600 - if trimmed.isEmpty { return "No output captured." } + if trimmed.isEmpty { return L10n.tr("login.noOutput", default: "No output captured.") } if trimmed.count <= limit { return trimmed } let idx = trimmed.index(trimmed.startIndex, offsetBy: limit) return "\(trimmed[.. String { + let format = Bundle.module.localizedString(forKey: key, value: defaultValue, table: nil) + guard !arguments.isEmpty else { return format } + return String(format: format, locale: Locale.current, arguments: arguments) + } +} diff --git a/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanProviderDescriptor.swift index b15a40d54..0fd9ac562 100644 --- a/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Alibaba/AlibabaCodingPlanProviderDescriptor.swift @@ -28,13 +28,13 @@ public enum AlibabaCodingPlanProviderDescriptor { metadata: ProviderMetadata( id: .alibaba, displayName: "Alibaba", - sessionLabel: "5-hour", - weeklyLabel: "Weekly", - opusLabel: "Monthly", + sessionLabel: L10n.tr("provider.label.fiveHour", default: "5-hour"), + weeklyLabel: L10n.tr("provider.label.weekly", default: "Weekly"), + opusLabel: L10n.tr("provider.label.monthly", default: "Monthly"), supportsOpus: true, supportsCredits: false, creditsHint: "", - toggleTitle: "Show Alibaba usage", + toggleTitle: L10n.tr("provider.toggle.alibaba", default: "Show Alibaba usage"), cliName: "alibaba-coding-plan", defaultEnabled: false, isPrimaryProvider: false, diff --git a/Sources/CodexBarCore/Providers/Amp/AmpProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Amp/AmpProviderDescriptor.swift index bfe1bee0e..a659bce79 100644 --- a/Sources/CodexBarCore/Providers/Amp/AmpProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Amp/AmpProviderDescriptor.swift @@ -10,13 +10,13 @@ public enum AmpProviderDescriptor { metadata: ProviderMetadata( id: .amp, displayName: "Amp", - sessionLabel: "Amp Free", - weeklyLabel: "Balance", + sessionLabel: L10n.tr("provider.label.ampFree", default: "Amp Free"), + weeklyLabel: L10n.tr("provider.label.balance", default: "Balance"), opusLabel: nil, supportsOpus: false, supportsCredits: false, creditsHint: "", - toggleTitle: "Show Amp usage", + toggleTitle: L10n.tr("provider.toggle.amp", default: "Show Amp usage"), cliName: "amp", defaultEnabled: false, isPrimaryProvider: false, diff --git a/Sources/CodexBarCore/Providers/Antigravity/AntigravityProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Antigravity/AntigravityProviderDescriptor.swift index 1e59964b0..5046b211a 100644 --- a/Sources/CodexBarCore/Providers/Antigravity/AntigravityProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Antigravity/AntigravityProviderDescriptor.swift @@ -16,7 +16,7 @@ public enum AntigravityProviderDescriptor { supportsOpus: true, supportsCredits: false, creditsHint: "", - toggleTitle: "Show Antigravity usage (experimental)", + toggleTitle: L10n.tr("provider.toggle.antigravity", default: "Show Antigravity usage (experimental)"), cliName: "antigravity", defaultEnabled: false, isPrimaryProvider: false, diff --git a/Sources/CodexBarCore/Providers/Augment/AugmentProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Augment/AugmentProviderDescriptor.swift index 4ccb0987f..c39915f5d 100644 --- a/Sources/CodexBarCore/Providers/Augment/AugmentProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Augment/AugmentProviderDescriptor.swift @@ -34,13 +34,13 @@ public enum AugmentProviderDescriptor { metadata: ProviderMetadata( id: .augment, displayName: "Augment", - sessionLabel: "Credits", - weeklyLabel: "Usage", + sessionLabel: L10n.tr("provider.label.credits", default: "Credits"), + weeklyLabel: L10n.tr("provider.label.usage", default: "Usage"), opusLabel: nil, supportsOpus: false, supportsCredits: true, creditsHint: "Augment Code credits for AI-powered coding assistance.", - toggleTitle: "Show Augment usage", + toggleTitle: L10n.tr("provider.toggle.augment", default: "Show Augment usage"), cliName: "augment", defaultEnabled: false, isPrimaryProvider: false, diff --git a/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift index 3918ef04f..7c54c7b74 100644 --- a/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift @@ -10,13 +10,13 @@ public enum ClaudeProviderDescriptor { metadata: ProviderMetadata( id: .claude, displayName: "Claude", - sessionLabel: "Session", - weeklyLabel: "Weekly", - opusLabel: "Sonnet", + sessionLabel: L10n.tr("provider.label.session", default: "Session"), + weeklyLabel: L10n.tr("provider.label.weekly", default: "Weekly"), + opusLabel: L10n.tr("provider.label.sonnet", default: "Sonnet"), supportsOpus: true, supportsCredits: false, creditsHint: "", - toggleTitle: "Show Claude Code usage", + toggleTitle: L10n.tr("provider.toggle.claude", default: "Show Claude Code usage"), cliName: "claude", defaultEnabled: false, isPrimaryProvider: true, diff --git a/Sources/CodexBarCore/Providers/Codex/CodexProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Codex/CodexProviderDescriptor.swift index 12b68cd22..1df2238c8 100644 --- a/Sources/CodexBarCore/Providers/Codex/CodexProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Codex/CodexProviderDescriptor.swift @@ -10,13 +10,13 @@ public enum CodexProviderDescriptor { metadata: ProviderMetadata( id: .codex, displayName: "Codex", - sessionLabel: "Session", - weeklyLabel: "Weekly", + sessionLabel: L10n.tr("provider.label.session", default: "Session"), + weeklyLabel: L10n.tr("provider.label.weekly", default: "Weekly"), opusLabel: nil, supportsOpus: false, supportsCredits: true, creditsHint: "Credits unavailable; keep Codex running to refresh.", - toggleTitle: "Show Codex usage", + toggleTitle: L10n.tr("provider.toggle.codex", default: "Show Codex usage"), cliName: "codex", defaultEnabled: true, isPrimaryProvider: true, diff --git a/Sources/CodexBarCore/Providers/Copilot/CopilotProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Copilot/CopilotProviderDescriptor.swift index 9e2b063cc..2f571a7f3 100644 --- a/Sources/CodexBarCore/Providers/Copilot/CopilotProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Copilot/CopilotProviderDescriptor.swift @@ -10,13 +10,13 @@ public enum CopilotProviderDescriptor { metadata: ProviderMetadata( id: .copilot, displayName: "Copilot", - sessionLabel: "Premium", - weeklyLabel: "Chat", + sessionLabel: L10n.tr("provider.label.premium", default: "Premium"), + weeklyLabel: L10n.tr("provider.label.chat", default: "Chat"), opusLabel: nil, supportsOpus: false, supportsCredits: false, creditsHint: "", - toggleTitle: "Show Copilot usage", + toggleTitle: L10n.tr("provider.toggle.copilot", default: "Show Copilot usage"), cliName: "copilot", defaultEnabled: false, isPrimaryProvider: false, diff --git a/Sources/CodexBarCore/Providers/Cursor/CursorProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Cursor/CursorProviderDescriptor.swift index e694e2e77..f16370f24 100644 --- a/Sources/CodexBarCore/Providers/Cursor/CursorProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Cursor/CursorProviderDescriptor.swift @@ -10,13 +10,13 @@ public enum CursorProviderDescriptor { metadata: ProviderMetadata( id: .cursor, displayName: "Cursor", - sessionLabel: "Plan", - weeklyLabel: "On-Demand", + sessionLabel: L10n.tr("provider.label.plan", default: "Plan"), + weeklyLabel: L10n.tr("provider.label.onDemand", default: "On-Demand"), opusLabel: nil, supportsOpus: false, supportsCredits: true, creditsHint: "On-demand usage beyond included plan limits.", - toggleTitle: "Show Cursor usage", + toggleTitle: L10n.tr("provider.toggle.cursor", default: "Show Cursor usage"), cliName: "cursor", defaultEnabled: false, isPrimaryProvider: false, diff --git a/Sources/CodexBarCore/Providers/Factory/FactoryProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Factory/FactoryProviderDescriptor.swift index dbbe1509d..3c118085f 100644 --- a/Sources/CodexBarCore/Providers/Factory/FactoryProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Factory/FactoryProviderDescriptor.swift @@ -10,8 +10,8 @@ public enum FactoryProviderDescriptor { metadata: ProviderMetadata( id: .factory, displayName: "Droid", - sessionLabel: "Standard", - weeklyLabel: "Premium", + sessionLabel: L10n.tr("provider.label.standard", default: "Standard"), + weeklyLabel: L10n.tr("provider.label.premium", default: "Premium"), opusLabel: nil, supportsOpus: false, supportsCredits: false, diff --git a/Sources/CodexBarCore/Providers/Gemini/GeminiProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Gemini/GeminiProviderDescriptor.swift index c71f5ca04..51107ed6b 100644 --- a/Sources/CodexBarCore/Providers/Gemini/GeminiProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Gemini/GeminiProviderDescriptor.swift @@ -10,13 +10,13 @@ public enum GeminiProviderDescriptor { metadata: ProviderMetadata( id: .gemini, displayName: "Gemini", - sessionLabel: "Pro", + sessionLabel: L10n.tr("provider.label.pro", default: "Pro"), weeklyLabel: "Flash", opusLabel: "Flash Lite", supportsOpus: true, supportsCredits: false, creditsHint: "", - toggleTitle: "Show Gemini usage", + toggleTitle: L10n.tr("provider.toggle.gemini", default: "Show Gemini usage"), cliName: "gemini", defaultEnabled: false, isPrimaryProvider: false, diff --git a/Sources/CodexBarCore/Providers/JetBrains/JetBrainsProviderDescriptor.swift b/Sources/CodexBarCore/Providers/JetBrains/JetBrainsProviderDescriptor.swift index 7b079e883..3642962e0 100644 --- a/Sources/CodexBarCore/Providers/JetBrains/JetBrainsProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/JetBrains/JetBrainsProviderDescriptor.swift @@ -10,8 +10,8 @@ public enum JetBrainsProviderDescriptor { metadata: ProviderMetadata( id: .jetbrains, displayName: "JetBrains AI", - sessionLabel: "Current", - weeklyLabel: "Refill", + sessionLabel: L10n.tr("provider.label.current", default: "Current"), + weeklyLabel: L10n.tr("provider.label.refill", default: "Refill"), opusLabel: nil, supportsOpus: false, supportsCredits: false, diff --git a/Sources/CodexBarCore/Providers/Kilo/KiloProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Kilo/KiloProviderDescriptor.swift index 8e3d8fad3..57d9f91d9 100644 --- a/Sources/CodexBarCore/Providers/Kilo/KiloProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Kilo/KiloProviderDescriptor.swift @@ -10,13 +10,13 @@ public enum KiloProviderDescriptor { metadata: ProviderMetadata( id: .kilo, displayName: "Kilo", - sessionLabel: "Credits", + sessionLabel: L10n.tr("provider.label.credits", default: "Credits"), weeklyLabel: "Kilo Pass", opusLabel: nil, supportsOpus: false, supportsCredits: false, creditsHint: "", - toggleTitle: "Show Kilo usage", + toggleTitle: L10n.tr("provider.toggle.kilo", default: "Show Kilo usage"), cliName: "kilo", defaultEnabled: false, isPrimaryProvider: false, diff --git a/Sources/CodexBarCore/Providers/Kimi/KimiProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Kimi/KimiProviderDescriptor.swift index 711c20bc8..a63f150ee 100644 --- a/Sources/CodexBarCore/Providers/Kimi/KimiProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Kimi/KimiProviderDescriptor.swift @@ -10,13 +10,13 @@ public enum KimiProviderDescriptor { metadata: ProviderMetadata( id: .kimi, displayName: "Kimi", - sessionLabel: "Weekly", - weeklyLabel: "Rate Limit", + sessionLabel: L10n.tr("provider.label.weekly", default: "Weekly"), + weeklyLabel: L10n.tr("provider.label.rateLimit", default: "Rate Limit"), opusLabel: nil, supportsOpus: false, supportsCredits: false, creditsHint: "", - toggleTitle: "Show Kimi usage", + toggleTitle: L10n.tr("provider.toggle.kimi", default: "Show Kimi usage"), cliName: "kimi", defaultEnabled: false, isPrimaryProvider: false, diff --git a/Sources/CodexBarCore/Providers/KimiK2/KimiK2ProviderDescriptor.swift b/Sources/CodexBarCore/Providers/KimiK2/KimiK2ProviderDescriptor.swift index cc986b6f0..9c1356bf7 100644 --- a/Sources/CodexBarCore/Providers/KimiK2/KimiK2ProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/KimiK2/KimiK2ProviderDescriptor.swift @@ -10,13 +10,13 @@ public enum KimiK2ProviderDescriptor { metadata: ProviderMetadata( id: .kimik2, displayName: "Kimi K2", - sessionLabel: "Credits", - weeklyLabel: "Credits", + sessionLabel: L10n.tr("provider.label.credits", default: "Credits"), + weeklyLabel: L10n.tr("provider.label.credits", default: "Credits"), opusLabel: nil, supportsOpus: false, supportsCredits: false, creditsHint: "", - toggleTitle: "Show Kimi K2 usage", + toggleTitle: L10n.tr("provider.toggle.kimik2", default: "Show Kimi K2 usage"), cliName: "kimik2", defaultEnabled: false, isPrimaryProvider: false, diff --git a/Sources/CodexBarCore/Providers/Kiro/KiroProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Kiro/KiroProviderDescriptor.swift index ad006df0d..561ba41fd 100644 --- a/Sources/CodexBarCore/Providers/Kiro/KiroProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Kiro/KiroProviderDescriptor.swift @@ -10,13 +10,13 @@ public enum KiroProviderDescriptor { metadata: ProviderMetadata( id: .kiro, displayName: "Kiro", - sessionLabel: "Credits", - weeklyLabel: "Bonus", + sessionLabel: L10n.tr("provider.label.credits", default: "Credits"), + weeklyLabel: L10n.tr("provider.label.bonus", default: "Bonus"), opusLabel: nil, supportsOpus: false, supportsCredits: false, creditsHint: "", - toggleTitle: "Show Kiro usage", + toggleTitle: L10n.tr("provider.toggle.kiro", default: "Show Kiro usage"), cliName: "kiro", defaultEnabled: false, isPrimaryProvider: false, diff --git a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift index 762177efe..f367e96ea 100644 --- a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift @@ -10,13 +10,13 @@ public enum MiniMaxProviderDescriptor { metadata: ProviderMetadata( id: .minimax, displayName: "MiniMax", - sessionLabel: "Prompts", - weeklyLabel: "Window", + sessionLabel: L10n.tr("provider.label.prompts", default: "Prompts"), + weeklyLabel: L10n.tr("provider.label.window", default: "Window"), opusLabel: nil, supportsOpus: false, supportsCredits: false, creditsHint: "", - toggleTitle: "Show MiniMax usage", + toggleTitle: L10n.tr("provider.toggle.minimax", default: "Show MiniMax usage"), cliName: "minimax", defaultEnabled: false, isPrimaryProvider: false, diff --git a/Sources/CodexBarCore/Providers/Ollama/OllamaProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Ollama/OllamaProviderDescriptor.swift index 4702609fa..08ee21649 100644 --- a/Sources/CodexBarCore/Providers/Ollama/OllamaProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Ollama/OllamaProviderDescriptor.swift @@ -10,13 +10,13 @@ public enum OllamaProviderDescriptor { metadata: ProviderMetadata( id: .ollama, displayName: "Ollama", - sessionLabel: "Session", - weeklyLabel: "Weekly", + sessionLabel: L10n.tr("provider.label.session", default: "Session"), + weeklyLabel: L10n.tr("provider.label.weekly", default: "Weekly"), opusLabel: nil, supportsOpus: false, supportsCredits: false, creditsHint: "", - toggleTitle: "Show Ollama usage", + toggleTitle: L10n.tr("provider.toggle.ollama", default: "Show Ollama usage"), cliName: "ollama", defaultEnabled: false, isPrimaryProvider: false, diff --git a/Sources/CodexBarCore/Providers/OpenCode/OpenCodeProviderDescriptor.swift b/Sources/CodexBarCore/Providers/OpenCode/OpenCodeProviderDescriptor.swift index 18803a46c..a232d6ada 100644 --- a/Sources/CodexBarCore/Providers/OpenCode/OpenCodeProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/OpenCode/OpenCodeProviderDescriptor.swift @@ -10,13 +10,13 @@ public enum OpenCodeProviderDescriptor { metadata: ProviderMetadata( id: .opencode, displayName: "OpenCode", - sessionLabel: "5-hour", - weeklyLabel: "Weekly", + sessionLabel: L10n.tr("provider.label.fiveHour", default: "5-hour"), + weeklyLabel: L10n.tr("provider.label.weekly", default: "Weekly"), opusLabel: nil, supportsOpus: false, supportsCredits: false, creditsHint: "", - toggleTitle: "Show OpenCode usage", + toggleTitle: L10n.tr("provider.toggle.opencode", default: "Show OpenCode usage"), cliName: "opencode", defaultEnabled: false, isPrimaryProvider: false, diff --git a/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift b/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift index 711954198..de8928e3e 100644 --- a/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/OpenRouter/OpenRouterProviderDescriptor.swift @@ -10,13 +10,13 @@ public enum OpenRouterProviderDescriptor { metadata: ProviderMetadata( id: .openrouter, displayName: "OpenRouter", - sessionLabel: "Credits", - weeklyLabel: "Usage", + sessionLabel: L10n.tr("provider.label.credits", default: "Credits"), + weeklyLabel: L10n.tr("provider.label.usage", default: "Usage"), opusLabel: nil, supportsOpus: false, supportsCredits: true, creditsHint: "Credit balance from OpenRouter API", - toggleTitle: "Show OpenRouter usage", + toggleTitle: L10n.tr("provider.toggle.openrouter", default: "Show OpenRouter usage"), cliName: "openrouter", defaultEnabled: false, isPrimaryProvider: false, diff --git a/Sources/CodexBarCore/Providers/Synthetic/SyntheticProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Synthetic/SyntheticProviderDescriptor.swift index 550ab9190..da1d98560 100644 --- a/Sources/CodexBarCore/Providers/Synthetic/SyntheticProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Synthetic/SyntheticProviderDescriptor.swift @@ -10,13 +10,13 @@ public enum SyntheticProviderDescriptor { metadata: ProviderMetadata( id: .synthetic, displayName: "Synthetic", - sessionLabel: "Quota", - weeklyLabel: "Usage", + sessionLabel: L10n.tr("provider.label.quota", default: "Quota"), + weeklyLabel: L10n.tr("provider.label.usage", default: "Usage"), opusLabel: nil, supportsOpus: false, supportsCredits: false, creditsHint: "", - toggleTitle: "Show Synthetic usage", + toggleTitle: L10n.tr("provider.toggle.synthetic", default: "Show Synthetic usage"), cliName: "synthetic", defaultEnabled: false, isPrimaryProvider: false, diff --git a/Sources/CodexBarCore/Providers/VertexAI/VertexAIProviderDescriptor.swift b/Sources/CodexBarCore/Providers/VertexAI/VertexAIProviderDescriptor.swift index f81e0d1f2..c00bcf5ee 100644 --- a/Sources/CodexBarCore/Providers/VertexAI/VertexAIProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/VertexAI/VertexAIProviderDescriptor.swift @@ -10,13 +10,13 @@ public enum VertexAIProviderDescriptor { metadata: ProviderMetadata( id: .vertexai, displayName: "Vertex AI", - sessionLabel: "Requests", - weeklyLabel: "Tokens", + sessionLabel: L10n.tr("provider.label.requests", default: "Requests"), + weeklyLabel: L10n.tr("provider.label.tokens", default: "Tokens"), opusLabel: nil, supportsOpus: false, supportsCredits: false, creditsHint: "", - toggleTitle: "Show Vertex AI usage", + toggleTitle: L10n.tr("provider.toggle.vertexai", default: "Show Vertex AI usage"), cliName: "vertexai", defaultEnabled: false, isPrimaryProvider: false, diff --git a/Sources/CodexBarCore/Providers/Warp/WarpProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Warp/WarpProviderDescriptor.swift index 29506321c..dea8fe76b 100644 --- a/Sources/CodexBarCore/Providers/Warp/WarpProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Warp/WarpProviderDescriptor.swift @@ -10,13 +10,13 @@ public enum WarpProviderDescriptor { metadata: ProviderMetadata( id: .warp, displayName: "Warp", - sessionLabel: "Credits", - weeklyLabel: "Add-on credits", + sessionLabel: L10n.tr("provider.label.credits", default: "Credits"), + weeklyLabel: L10n.tr("provider.label.addOnCredits", default: "Add-on credits"), opusLabel: nil, supportsOpus: false, supportsCredits: false, creditsHint: "", - toggleTitle: "Show Warp usage", + toggleTitle: L10n.tr("provider.toggle.warp", default: "Show Warp usage"), cliName: "warp", defaultEnabled: false, isPrimaryProvider: false, diff --git a/Sources/CodexBarCore/Providers/Zai/ZaiProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Zai/ZaiProviderDescriptor.swift index 430066a10..06aef421b 100644 --- a/Sources/CodexBarCore/Providers/Zai/ZaiProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Zai/ZaiProviderDescriptor.swift @@ -10,13 +10,13 @@ public enum ZaiProviderDescriptor { metadata: ProviderMetadata( id: .zai, displayName: "z.ai", - sessionLabel: "Tokens", + sessionLabel: L10n.tr("provider.label.tokens", default: "Tokens"), weeklyLabel: "MCP", opusLabel: nil, supportsOpus: false, supportsCredits: false, creditsHint: "", - toggleTitle: "Show z.ai usage", + toggleTitle: L10n.tr("provider.toggle.zai", default: "Show z.ai usage"), cliName: "zai", defaultEnabled: false, isPrimaryProvider: false, diff --git a/Sources/CodexBarCore/Resources/en.lproj/Localizable.strings b/Sources/CodexBarCore/Resources/en.lproj/Localizable.strings new file mode 100644 index 000000000..fe7cef67f --- /dev/null +++ b/Sources/CodexBarCore/Resources/en.lproj/Localizable.strings @@ -0,0 +1,342 @@ +"about.built" = "Built %@"; +"about.credits" = "Peter Steinberger — MIT License\n"; +"common.copy" = "Copy"; +"common.ok" = "OK"; +"keychain.ampCookie.message" = "CodexBar will ask macOS Keychain for your Amp cookie header so it can fetch usage. Click OK to continue."; +"keychain.augmentCookie.message" = "CodexBar will ask macOS Keychain for your Augment cookie header so it can fetch usage. Click OK to continue."; +"keychain.claudeCookie.message" = "CodexBar will ask macOS Keychain for your Claude cookie header so it can fetch Claude web usage. Click OK to continue."; +"keychain.claudeOAuth.message" = "CodexBar will ask macOS Keychain for the Claude Code OAuth token so it can fetch your Claude usage. Click OK to continue."; +"keychain.codexCookie.message" = "CodexBar will ask macOS Keychain for your OpenAI cookie header so it can fetch Codex dashboard extras. Click OK to continue."; +"keychain.copilotToken.message" = "CodexBar will ask macOS Keychain for your GitHub Copilot token so it can fetch usage. Click OK to continue."; +"keychain.cursorCookie.message" = "CodexBar will ask macOS Keychain for your Cursor cookie header so it can fetch usage. Click OK to continue."; +"keychain.factoryCookie.message" = "CodexBar will ask macOS Keychain for your Factory cookie header so it can fetch usage. Click OK to continue."; +"keychain.kimiToken.message" = "CodexBar will ask macOS Keychain for your Kimi auth token so it can fetch usage. Click OK to continue."; +"keychain.kimik2Token.message" = "CodexBar will ask macOS Keychain for your Kimi K2 API key so it can fetch usage. Click OK to continue."; +"keychain.minimaxCookie.message" = "CodexBar will ask macOS Keychain for your MiniMax cookie header so it can fetch usage. Click OK to continue."; +"keychain.minimaxToken.message" = "CodexBar will ask macOS Keychain for your MiniMax API token so it can fetch usage. Click OK to continue."; +"keychain.opencodeCookie.message" = "CodexBar will ask macOS Keychain for your OpenCode cookie header so it can fetch usage. Click OK to continue."; +"keychain.syntheticToken.message" = "CodexBar will ask macOS Keychain for your Synthetic API key so it can fetch usage. Click OK to continue."; +"keychain.title" = "Keychain Access Required"; +"keychain.zaiToken.message" = "CodexBar will ask macOS Keychain for your z.ai API token so it can fetch usage. Click OK to continue."; +"login.claude.failed.status" = "claude /login exited with status %@."; +"login.claude.failed.title" = "Claude login failed"; +"login.claude.launchFailed.title" = "Could not start claude /login"; +"login.claude.missingBinary.message" = "Install the Claude CLI (npm i -g @anthropic-ai/claude-code) and try again."; +"login.claude.missingBinary.title" = "Claude CLI not found"; +"login.claude.timedOut.title" = "Claude login timed out"; +"login.codex.failed.status" = "codex login exited with status %@."; +"login.codex.failed.title" = "Codex login failed"; +"login.codex.launchFailed.title" = "Could not start codex login"; +"login.codex.missingBinary.message" = "Install the Codex CLI (npm i -g @openai/codex) and try again."; +"login.codex.missingBinary.title" = "Codex CLI not found"; +"login.codex.timedOut.title" = "Codex login timed out"; +"login.cursor.failed.title" = "Cursor login failed"; +"login.gemini.launchFailed.title" = "Could not open Terminal for Gemini"; +"login.gemini.missingBinary.message" = "Install the Gemini CLI (npm i -g @google/gemini-cli) and try again."; +"login.gemini.missingBinary.title" = "Gemini CLI not found"; +"login.noOutput" = "No output captured."; +"menu.about" = "About CodexBar"; +"menu.account" = "Account: %@"; +"menu.activity" = "Activity: %@"; +"menu.addAccount" = "Add Account..."; +"menu.dashboard" = "Usage Dashboard"; +"menu.noUsageConfigured" = "No usage configured."; +"menu.noUsageYet" = "No usage yet"; +"menu.plan" = "Plan: %@"; +"menu.quit" = "Quit"; +"menu.quota" = "Quota: %@ / %@"; +"menu.settings" = "Settings..."; +"menu.statusPage" = "Status Page"; +"menu.switchAccount" = "Switch Account..."; +"menu.updateReady" = "Update ready, restart now?"; +"preferences.about.autoUpdates" = "Check for updates automatically"; +"preferences.about.built" = "Built %@"; +"preferences.about.checkForUpdates" = "Check for Updates…"; +"preferences.about.tagline" = "May your tokens never run out—keep agent limits in view."; +"preferences.about.updateChannel" = "Update Channel"; +"preferences.about.updatesUnavailable" = "Updates unavailable in this build."; +"preferences.about.version" = "Version %@"; +"preferences.advanced.disableKeychain.subtitle" = "Prevents any Keychain access while enabled."; +"preferences.advanced.disableKeychain.title" = "Disable Keychain access"; +"preferences.advanced.hidePII.subtitle" = "Obscure email addresses in the menu bar and menu UI."; +"preferences.advanced.hidePII.title" = "Hide personal information"; +"preferences.advanced.installCLI.button" = "Install CLI"; +"preferences.advanced.installCLI.exists" = "Exists: %@"; +"preferences.advanced.installCLI.failed" = "Failed: %@"; +"preferences.advanced.installCLI.installed" = "Installed: %@"; +"preferences.advanced.installCLI.missing" = "CodexBarCLI not found in app bundle."; +"preferences.advanced.installCLI.noDirs" = "No writable bin dirs found."; +"preferences.advanced.installCLI.noWriteAccess" = "No write access: %@"; +"preferences.advanced.installCLI.subtitle" = "Symlink CodexBarCLI to /usr/local/bin and /opt/homebrew/bin as codexbar."; +"preferences.advanced.keychain.caption" = "Disable all Keychain reads and writes. Browser cookie import is unavailable; paste Cookie headers manually in Providers."; +"preferences.advanced.keychain.title" = "Keychain access"; +"preferences.advanced.openMenu.subtitle" = "Trigger the menu bar menu from anywhere."; +"preferences.advanced.openMenu.title" = "Open menu"; +"preferences.advanced.section.shortcut" = "Keyboard shortcut"; +"preferences.advanced.showDebug.subtitle" = "Expose troubleshooting tools in the Debug tab."; +"preferences.advanced.showDebug.title" = "Show Debug Settings"; +"preferences.advanced.surprise.subtitle" = "Check if you like your agents having some fun up there."; +"preferences.advanced.surprise.title" = "Surprise me"; +"preferences.debug.animations.blink" = "Blink now"; +"preferences.debug.animations.pattern" = "Animation pattern"; +"preferences.debug.animations.random" = "Random (default)"; +"preferences.debug.animations.replay" = "Replay selected animation"; +"preferences.debug.animations.title" = "Loading animations"; +"preferences.debug.caches.caption" = "Clear cached cost scan results."; +"preferences.debug.caches.clear" = "Clear cost cache"; +"preferences.debug.caches.cleared" = "Cleared."; +"preferences.debug.caches.failed" = "Failed: %@"; +"preferences.debug.caches.title" = "Caches"; +"preferences.debug.cliPaths.caption" = "Resolved Codex binary and PATH layers; startup login PATH capture (short timeout)."; +"preferences.debug.cliPaths.claudeBinary" = "Claude binary"; +"preferences.debug.cliPaths.codexBinary" = "Codex binary"; +"preferences.debug.cliPaths.effectivePath" = "Effective PATH"; +"preferences.debug.cliPaths.loginPath" = "Login shell PATH (startup capture)"; +"preferences.debug.cliPaths.notFound" = "Not found"; +"preferences.debug.cliPaths.title" = "CLI paths"; +"preferences.debug.cliPaths.unavailable" = "Unavailable"; +"preferences.debug.cliSessions.caption" = "Keep Codex/Claude CLI sessions alive after a probe. Default exits once data is captured."; +"preferences.debug.cliSessions.keepAlive.subtitle" = "Skip teardown between probes (debug-only)."; +"preferences.debug.cliSessions.keepAlive.title" = "Keep CLI sessions alive"; +"preferences.debug.cliSessions.reset" = "Reset CLI sessions"; +"preferences.debug.cliSessions.title" = "CLI sessions"; +"preferences.debug.emptyLog" = "No log yet. Fetch to load."; +"preferences.debug.errorSimulation.caption" = "Inject a fake error message into the menu card for layout testing."; +"preferences.debug.errorSimulation.clearCost" = "Clear cost error"; +"preferences.debug.errorSimulation.clearMenu" = "Clear menu error"; +"preferences.debug.errorSimulation.setCost" = "Set cost error"; +"preferences.debug.errorSimulation.setMenu" = "Set menu error"; +"preferences.debug.errorSimulation.text" = "Simulated error text"; +"preferences.debug.errorSimulation.title" = "Error simulation"; +"preferences.debug.fetchAttempts.available" = " available"; +"preferences.debug.fetchAttempts.caption" = "Last fetch pipeline decisions and errors for a provider."; +"preferences.debug.fetchAttempts.empty" = "No fetch attempts yet."; +"preferences.debug.fetchAttempts.error" = " error=%@"; +"preferences.debug.fetchAttempts.title" = "Fetch strategy attempts"; +"preferences.debug.fetchAttempts.unavailable" = " unavailable"; +"preferences.debug.fetchKind.api" = "api"; +"preferences.debug.fetchKind.cli" = "cli"; +"preferences.debug.fetchKind.local" = "local"; +"preferences.debug.fetchKind.oauth" = "oauth"; +"preferences.debug.fetchKind.web" = "web"; +"preferences.debug.forceAnimation.subtitle" = "Temporarily shows the loading animation after the next refresh."; +"preferences.debug.forceAnimation.title" = "Force animation on next refresh"; +"preferences.debug.loading" = "Loading…"; +"preferences.debug.logging.enable.subtitle" = "Write logs to %@ for debugging."; +"preferences.debug.logging.enable.title" = "Enable file logging"; +"preferences.debug.logging.openFile" = "Open log file"; +"preferences.debug.logging.title" = "Logging"; +"preferences.debug.logging.verbosity" = "Verbosity"; +"preferences.debug.logging.verbosity.subtitle" = "Controls how much detail is logged."; +"preferences.debug.notifications.caption" = "Trigger test notifications for the 5-hour session window (depleted/restored)."; +"preferences.debug.notifications.depleted" = "Post depleted"; +"preferences.debug.notifications.restored" = "Post restored"; +"preferences.debug.notifications.title" = "Notifications"; +"preferences.debug.openAICookies.caption" = "Cookie import + WebKit scrape logs from the last OpenAI cookies attempt."; +"preferences.debug.openAICookies.empty" = "No log yet. Update OpenAI cookies in Providers → Codex to run an import."; +"preferences.debug.openAICookies.title" = "OpenAI cookies"; +"preferences.debug.probeLogs.caption" = "Fetch the latest probe output for debugging; Copy keeps the full text."; +"preferences.debug.probeLogs.fetch" = "Fetch log"; +"preferences.debug.probeLogs.parseDump" = "Load parse dump"; +"preferences.debug.probeLogs.redetect" = "Re-run provider autodetect"; +"preferences.debug.probeLogs.save" = "Save to file"; +"preferences.debug.probeLogs.title" = "Probe logs"; +"preferences.debug.provider" = "Provider"; +"preferences.display.mergeIcons.subtitle" = "Use a single menu bar icon with a provider switcher."; +"preferences.display.mergeIcons.title" = "Merge Icons"; +"preferences.display.mode.picker" = "Display mode"; +"preferences.display.mode.subtitle" = "Choose what to show in the menu bar (Pace shows usage vs. expected)."; +"preferences.display.mode.title" = "Display mode"; +"preferences.display.overviewProviders.choose" = "Choose up to %@ providers"; +"preferences.display.overviewProviders.configure" = "Configure…"; +"preferences.display.overviewProviders.mergeRequired" = "Enable Merge Icons to configure Overview tab providers."; +"preferences.display.overviewProviders.noneAvailable" = "No enabled providers available for Overview."; +"preferences.display.overviewProviders.noneSelected" = "No providers selected"; +"preferences.display.overviewProviders.orderHint" = "Overview rows always follow provider order."; +"preferences.display.overviewProviders.title" = "Overview tab providers"; +"preferences.display.resetAsClock.subtitle" = "Display reset times as absolute clock values instead of countdowns."; +"preferences.display.resetAsClock.title" = "Show reset time as clock"; +"preferences.display.section.menuBar" = "Menu bar"; +"preferences.display.section.menuContent" = "Menu content"; +"preferences.display.showAllAccounts.subtitle" = "Stack token accounts in the menu (otherwise show an account switcher bar)."; +"preferences.display.showAllAccounts.title" = "Show all token accounts"; +"preferences.display.showCredits.subtitle" = "Show Codex Credits and Claude Extra usage sections in the menu."; +"preferences.display.showCredits.title" = "Show credits + extra usage"; +"preferences.display.showMostUsed.subtitle" = "Menu bar auto-shows the provider closest to its rate limit."; +"preferences.display.showMostUsed.title" = "Show most-used provider"; +"preferences.display.showUsageAsUsed.subtitle" = "Progress bars fill as you consume quota (instead of showing remaining)."; +"preferences.display.showUsageAsUsed.title" = "Show usage as used"; +"preferences.display.showsPercent.subtitle" = "Replace critter bars with provider branding icons and a percentage."; +"preferences.display.showsPercent.title" = "Menu bar shows percent"; +"preferences.display.switcherShowsIcons.subtitle" = "Show provider icons in the switcher (otherwise show a weekly progress line)."; +"preferences.display.switcherShowsIcons.title" = "Switcher shows icons"; +"preferences.general.costStatus.error" = "%@: %@"; +"preferences.general.costStatus.fetching" = "%@: fetching…%@"; +"preferences.general.costStatus.lastAttempt" = "%@: last attempt %@"; +"preferences.general.costStatus.noData" = "%@: no data yet"; +"preferences.general.costStatus.snapshot" = "%@: %@ · 30d %@"; +"preferences.general.costStatus.unsupported" = "%@: unsupported"; +"preferences.general.costSummary.status" = "Auto-refresh: hourly · Timeout: 10m"; +"preferences.general.costSummary.subtitle" = "Reads local usage logs. Shows today + last 30 days cost in the menu."; +"preferences.general.costSummary.title" = "Show cost summary"; +"preferences.general.notifications.subtitle" = "Notifies when the 5-hour session quota hits 0% and when it becomes available again."; +"preferences.general.notifications.title" = "Session quota notifications"; +"preferences.general.quit" = "Quit CodexBar"; +"preferences.general.refreshCadence.manualHint" = "Auto-refresh is off; use the menu's Refresh command."; +"preferences.general.refreshCadence.picker" = "Refresh cadence"; +"preferences.general.refreshCadence.subtitle" = "How often CodexBar polls providers in the background."; +"preferences.general.refreshCadence.title" = "Refresh cadence"; +"preferences.general.section.automation" = "Automation"; +"preferences.general.section.system" = "System"; +"preferences.general.section.usage" = "Usage"; +"preferences.general.startAtLogin.subtitle" = "Automatically opens CodexBar when you start your Mac."; +"preferences.general.startAtLogin.title" = "Start at Login"; +"preferences.general.statusChecks.subtitle" = "Polls OpenAI/Claude status pages and Google Workspace for Gemini/Antigravity, surfacing incidents in the icon and menu."; +"preferences.general.statusChecks.title" = "Check provider status"; +"preferences.tab.about" = "About"; +"preferences.tab.advanced" = "Advanced"; +"preferences.tab.debug" = "Debug"; +"preferences.tab.display" = "Display"; +"preferences.tab.general" = "General"; +"preferences.tab.providers" = "Providers"; +"provider.label.addOnCredits" = "Add-on credits"; +"provider.label.ampFree" = "Amp Free"; +"provider.label.balance" = "Balance"; +"provider.label.bonus" = "Bonus"; +"provider.label.chat" = "Chat"; +"provider.label.credits" = "Credits"; +"provider.label.current" = "Current"; +"provider.label.fiveHour" = "5-hour"; +"provider.label.monthly" = "Monthly"; +"provider.label.onDemand" = "On-Demand"; +"provider.label.plan" = "Plan"; +"provider.label.premium" = "Premium"; +"provider.label.pro" = "Pro"; +"provider.label.prompts" = "Prompts"; +"provider.label.quota" = "Quota"; +"provider.label.rateLimit" = "Rate Limit"; +"provider.label.refill" = "Refill"; +"provider.label.requests" = "Requests"; +"provider.label.session" = "Session"; +"provider.label.sonnet" = "Sonnet"; +"provider.label.standard" = "Standard"; +"provider.label.tokens" = "Tokens"; +"provider.label.usage" = "Usage"; +"provider.label.weekly" = "Weekly"; +"provider.label.window" = "Window"; +"provider.menu.openAugmentRelogin" = "Open Augment (Log Out & Back In)"; +"provider.menu.refreshSession" = "Refresh Session"; +"provider.settings.alibaba.cookiesDisabled" = "Alibaba cookies are disabled."; +"provider.settings.alibaba.manualCookie" = "Paste a Cookie header from modelstudio.console.alibabacloud.com."; +"provider.settings.alibabaAPIKey.subtitle" = "Stored in ~/.codexbar/config.json. Paste your Coding Plan API key from Model Studio."; +"provider.settings.alibabaRegion.subtitle" = "Use international or China mainland console gateways for quota fetches."; +"provider.settings.alwaysAllowPrompts" = "Always allow prompts"; +"provider.settings.amp.cookiesDisabled" = "Amp cookies are disabled."; +"provider.settings.amp.manualCookie" = "Paste a Cookie header or cURL capture from Amp settings."; +"provider.settings.apiKey.title" = "API key"; +"provider.settings.apiRegion.title" = "API region"; +"provider.settings.apiToken.title" = "API token"; +"provider.settings.augment.cookiesDisabled" = "Augment cookies are disabled."; +"provider.settings.augment.manualCookie" = "Paste a Cookie header or cURL capture from the Augment dashboard."; +"provider.settings.autoDetect" = "Auto-detect"; +"provider.settings.avoidKeychainPrompts.subtitle" = "Use /usr/bin/security to read Claude credentials and avoid CodexBar keychain prompts."; +"provider.settings.avoidKeychainPrompts.title" = "Avoid Keychain prompts (experimental)"; +"provider.settings.claude.cookiesDisabled" = "Claude cookies are disabled."; +"provider.settings.claude.manualCookie" = "Paste a Cookie header from a claude.ai request."; +"provider.settings.claudeCookies.subtitle" = "Automatic imports browser cookies for the web API."; +"provider.settings.claudeCookies.title" = "Claude cookies"; +"provider.settings.codex.cookiesDisabled" = "Disable OpenAI dashboard cookie usage."; +"provider.settings.codex.manualCookie" = "Paste a Cookie header from a chatgpt.com request."; +"provider.settings.cookieHeader.title" = "Cookie header"; +"provider.settings.cookieSource.alibabaAuto" = "Automatic imports browser cookies from Model Studio/Bailian."; +"provider.settings.cookieSource.autoBrowserCookies" = "Automatic imports browser cookies."; +"provider.settings.cookieSource.cursorAuto" = "Automatic imports browser cookies or stored sessions."; +"provider.settings.cookieSource.factoryAuto" = "Automatic imports browser cookies and WorkOS tokens."; +"provider.settings.cookieSource.minimaxAuto" = "Automatic imports browser cookies and local storage tokens."; +"provider.settings.cookieSource.opencodeAuto" = "Automatic imports browser cookies from opencode.ai."; +"provider.settings.cookieSource.title" = "Cookie source"; +"provider.settings.cursor.cookiesDisabled" = "Cursor cookies are disabled."; +"provider.settings.cursor.manualCookie" = "Paste a Cookie header from a cursor.com request."; +"provider.settings.customPath.subtitle" = "Override auto-detection with a custom IDE base path"; +"provider.settings.customPath.title" = "Custom Path"; +"provider.settings.factory.cookiesDisabled" = "Factory cookies are disabled."; +"provider.settings.factory.manualCookie" = "Paste a Cookie header from app.factory.ai."; +"provider.settings.gatewayRegion.title" = "Gateway region"; +"provider.settings.githubLogin.subtitle" = "Requires authentication via GitHub Device Flow."; +"provider.settings.githubLogin.title" = "GitHub Login"; +"provider.settings.historicalTracking.subtitle" = "Stores local Codex usage history (8 weeks) to personalize Pace predictions."; +"provider.settings.historicalTracking.title" = "Historical tracking"; +"provider.settings.jetbrainsIDE.subtitle" = "Select the IDE to monitor"; +"provider.settings.jetbrainsIDE.title" = "JetBrains IDE"; +"provider.settings.keychainPromptPolicy.disabled" = "Global Keychain access is disabled in Advanced, so this setting is currently inactive."; +"provider.settings.keychainPromptPolicy.subtitle" = "Applies only to the Security.framework OAuth keychain reader."; +"provider.settings.keychainPromptPolicy.title" = "Keychain prompt policy"; +"provider.settings.kiloAPIKey.subtitle" = "Stored in ~/.codexbar/config.json. You can also provide KILO_API_KEY or ~/.local/share/kilo/auth.json (kilo.access)."; +"provider.settings.kimi.cookiesDisabled" = "Kimi cookies are disabled."; +"provider.settings.kimi.manualCookie" = "Paste a cookie header or the kimi-auth token value."; +"provider.settings.kimik2APIKey.subtitle" = "Stored in ~/.codexbar/config.json. Generate one at kimi-k2.ai."; +"provider.settings.minimax.cookiesDisabled" = "MiniMax cookies are disabled."; +"provider.settings.minimax.manualCookie" = "Paste a Cookie header or cURL capture from the Coding Plan page."; +"provider.settings.minimaxAPIToken.subtitle" = "Stored in ~/.codexbar/config.json. Paste your MiniMax API key."; +"provider.settings.minimaxRegion.subtitle" = "Choose the MiniMax host (global .io or China mainland .com)."; +"provider.settings.neverPrompt" = "Never prompt"; +"provider.settings.ollama.cookiesDisabled" = "Ollama cookies are disabled."; +"provider.settings.ollama.manualCookie" = "Paste a Cookie header or cURL capture from Ollama settings."; +"provider.settings.onlyOnUserAction" = "Only on user action"; +"provider.settings.openAICookies.subtitle" = "Automatic imports browser cookies for dashboard extras."; +"provider.settings.openAICookies.title" = "OpenAI cookies"; +"provider.settings.openAIWebExtras.subtitle" = "Show usage breakdown, credits history, and code review via chatgpt.com."; +"provider.settings.openAIWebExtras.title" = "OpenAI web extras"; +"provider.settings.openAPIKeys" = "Open API Keys"; +"provider.settings.openAmpSettings" = "Open Amp Settings"; +"provider.settings.openCodingPlan" = "Open Coding Plan"; +"provider.settings.openConsole" = "Open Console"; +"provider.settings.openOllamaSettings" = "Open Ollama Settings"; +"provider.settings.openRouterAPIKey.subtitle" = "Stored in ~/.codexbar/config.json. Get your key from openrouter.ai/settings/keys and set a key spending limit there to enable API key quota tracking."; +"provider.settings.openWarpGuide" = "Open Warp API Key Guide"; +"provider.settings.opencode.cookiesDisabled" = "OpenCode cookies are disabled."; +"provider.settings.opencode.manualCookie" = "Paste a Cookie header captured from the billing page."; +"provider.settings.signInAgain" = "Sign in again"; +"provider.settings.signInWithGitHub" = "Sign in with GitHub"; +"provider.settings.syntheticAPIKey.subtitle" = "Stored in ~/.codexbar/config.json. Paste the key from the Synthetic dashboard."; +"provider.settings.usageSource.autoFallback" = "Auto falls back to the next source if the preferred one fails."; +"provider.settings.usageSource.kilo.subtitle" = "Auto uses API first, then falls back to CLI on auth failures."; +"provider.settings.usageSource.title" = "Usage source"; +"provider.settings.warpAPIKey.subtitle" = "Stored in ~/.codexbar/config.json. In Warp, open Settings > Platform > API Keys, then create one."; +"provider.settings.workspaceID.subtitle" = "Optional override if workspace lookup fails."; +"provider.settings.workspaceID.title" = "Workspace ID"; +"provider.settings.zaiRegion.subtitle" = "Use BigModel for the China mainland endpoints (open.bigmodel.cn)."; +"provider.toggle.alibaba" = "Show Alibaba usage"; +"provider.toggle.amp" = "Show Amp usage"; +"provider.toggle.antigravity" = "Show Antigravity usage (experimental)"; +"provider.toggle.augment" = "Show Augment usage"; +"provider.toggle.claude" = "Show Claude Code usage"; +"provider.toggle.codex" = "Show Codex usage"; +"provider.toggle.copilot" = "Show Copilot usage"; +"provider.toggle.cursor" = "Show Cursor usage"; +"provider.toggle.gemini" = "Show Gemini usage"; +"provider.toggle.kilo" = "Show Kilo usage"; +"provider.toggle.kimi" = "Show Kimi usage"; +"provider.toggle.kimik2" = "Show Kimi K2 usage"; +"provider.toggle.kiro" = "Show Kiro usage"; +"provider.toggle.minimax" = "Show MiniMax usage"; +"provider.toggle.ollama" = "Show Ollama usage"; +"provider.toggle.opencode" = "Show OpenCode usage"; +"provider.toggle.openrouter" = "Show OpenRouter usage"; +"provider.toggle.synthetic" = "Show Synthetic usage"; +"provider.toggle.vertexai" = "Show Vertex AI usage"; +"provider.toggle.warp" = "Show Warp usage"; +"provider.toggle.zai" = "Show z.ai usage"; +"widget.empty.historyRefresh" = "Usage history will appear after a refresh."; +"widget.empty.switcherRefresh" = "Usage data appears after a refresh."; +"widget.empty.usageRefresh" = "Usage data will appear once the app refreshes."; +"widget.metric.30dCost" = "30d cost"; +"widget.metric.codeReview" = "Code review"; +"widget.metric.creditsLeft" = "Credits left"; +"widget.metric.todayCost" = "Today cost"; +"widget.openCodexBar" = "Open CodexBar"; +"provider.settings.cached" = "Cached: %@ • %@"; +"widget.metric.credits" = "Credits"; +"widget.metric.today" = "Today"; +"widget.metric.30d" = "30d"; +"preferences.debug.animations.caption" = "Pick a pattern and replay it in the menu bar. \"Random\" keeps the existing behavior."; diff --git a/Sources/CodexBarCore/Resources/zh-Hans.lproj/Localizable.strings b/Sources/CodexBarCore/Resources/zh-Hans.lproj/Localizable.strings new file mode 100644 index 000000000..c70ddb838 --- /dev/null +++ b/Sources/CodexBarCore/Resources/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,342 @@ +"about.built" = "构建于 %@"; +"about.credits" = "Peter Steinberger — MIT License\n"; +"common.copy" = "复制"; +"common.ok" = "好的"; +"keychain.ampCookie.message" = "CodexBar 将向 macOS 钥匙串请求您的 Amp Cookie 标头,以便获取用量。点击“好的”继续。"; +"keychain.augmentCookie.message" = "CodexBar 将向 macOS 钥匙串请求您的 Augment Cookie 标头,以便获取用量。点击“好的”继续。"; +"keychain.claudeCookie.message" = "CodexBar 将向 macOS 钥匙串请求您的 Claude Cookie 标头,以便获取 Claude 网页用量。点击“好的”继续。"; +"keychain.claudeOAuth.message" = "CodexBar 将向 macOS 钥匙串请求 Claude Code OAuth 令牌,以便获取您的 Claude 用量。点击“好的”继续。"; +"keychain.codexCookie.message" = "CodexBar 将向 macOS 钥匙串请求您的 OpenAI Cookie 标头,以便获取 Codex 仪表板附加数据。点击“好的”继续。"; +"keychain.copilotToken.message" = "CodexBar 将向 macOS 钥匙串请求您的 GitHub Copilot 令牌,以便获取用量。点击“好的”继续。"; +"keychain.cursorCookie.message" = "CodexBar 将向 macOS 钥匙串请求您的 Cursor Cookie 标头,以便获取用量。点击“好的”继续。"; +"keychain.factoryCookie.message" = "CodexBar 将向 macOS 钥匙串请求您的 Factory Cookie 标头,以便获取用量。点击“好的”继续。"; +"keychain.kimiToken.message" = "CodexBar 将向 macOS 钥匙串请求您的 Kimi 认证令牌,以便获取用量。点击“好的”继续。"; +"keychain.kimik2Token.message" = "CodexBar 将向 macOS 钥匙串请求您的 Kimi K2 API 密钥,以便获取用量。点击“好的”继续。"; +"keychain.minimaxCookie.message" = "CodexBar 将向 macOS 钥匙串请求您的 MiniMax Cookie 标头,以便获取用量。点击“好的”继续。"; +"keychain.minimaxToken.message" = "CodexBar 将向 macOS 钥匙串请求您的 MiniMax API 令牌,以便获取用量。点击“好的”继续。"; +"keychain.opencodeCookie.message" = "CodexBar 将向 macOS 钥匙串请求您的 OpenCode Cookie 标头,以便获取用量。点击“好的”继续。"; +"keychain.syntheticToken.message" = "CodexBar 将向 macOS 钥匙串请求您的 Synthetic API 密钥,以便获取用量。点击“好的”继续。"; +"keychain.title" = "需要访问钥匙串"; +"keychain.zaiToken.message" = "CodexBar 将向 macOS 钥匙串请求您的 z.ai API 令牌,以便获取用量。点击“好的”继续。"; +"login.claude.failed.status" = "claude /login 以状态 %@ 退出。"; +"login.claude.failed.title" = "Claude 登录失败"; +"login.claude.launchFailed.title" = "无法启动 claude /login"; +"login.claude.missingBinary.message" = "请安装 Claude CLI(npm i -g @anthropic-ai/claude-code)后重试。"; +"login.claude.missingBinary.title" = "未找到 Claude CLI"; +"login.claude.timedOut.title" = "Claude 登录超时"; +"login.codex.failed.status" = "codex login 以状态 %@ 退出。"; +"login.codex.failed.title" = "Codex 登录失败"; +"login.codex.launchFailed.title" = "无法启动 codex 登录"; +"login.codex.missingBinary.message" = "请安装 Codex CLI(npm i -g @openai/codex)后重试。"; +"login.codex.missingBinary.title" = "未找到 Codex CLI"; +"login.codex.timedOut.title" = "Codex 登录超时"; +"login.cursor.failed.title" = "Cursor 登录失败"; +"login.gemini.launchFailed.title" = "无法为 Gemini 打开终端"; +"login.gemini.missingBinary.message" = "请安装 Gemini CLI(npm i -g @google/gemini-cli)后重试。"; +"login.gemini.missingBinary.title" = "未找到 Gemini CLI"; +"login.noOutput" = "未捕获到输出。"; +"menu.about" = "关于 CodexBar"; +"menu.account" = "账户:%@"; +"menu.activity" = "活动:%@"; +"menu.addAccount" = "添加账户…"; +"menu.dashboard" = "用量仪表板"; +"menu.noUsageConfigured" = "尚未配置用量。"; +"menu.noUsageYet" = "暂无用量"; +"menu.plan" = "套餐:%@"; +"menu.quit" = "退出"; +"menu.quota" = "配额:%@ / %@"; +"menu.settings" = "设置…"; +"menu.statusPage" = "状态页面"; +"menu.switchAccount" = "切换账户…"; +"menu.updateReady" = "更新已就绪,立即重启?"; +"preferences.about.autoUpdates" = "自动检查更新"; +"preferences.about.built" = "构建于 %@"; +"preferences.about.checkForUpdates" = "检查更新…"; +"preferences.about.tagline" = "愿你的 tokens 永不耗尽——始终掌握代理限额。"; +"preferences.about.updateChannel" = "更新通道"; +"preferences.about.updatesUnavailable" = "此构建不支持更新。"; +"preferences.about.version" = "版本 %@"; +"preferences.advanced.disableKeychain.subtitle" = "启用后会阻止任何钥匙串访问。"; +"preferences.advanced.disableKeychain.title" = "禁用钥匙串访问"; +"preferences.advanced.hidePII.subtitle" = "在菜单栏和菜单界面中隐藏邮箱地址。"; +"preferences.advanced.hidePII.title" = "隐藏个人信息"; +"preferences.advanced.installCLI.button" = "安装 CLI"; +"preferences.advanced.installCLI.exists" = "已存在:%@"; +"preferences.advanced.installCLI.failed" = "失败:%@"; +"preferences.advanced.installCLI.installed" = "已安装:%@"; +"preferences.advanced.installCLI.missing" = "在 app bundle 中找不到 CodexBarCLI。"; +"preferences.advanced.installCLI.noDirs" = "未找到可写的 bin 目录。"; +"preferences.advanced.installCLI.noWriteAccess" = "没有写权限:%@"; +"preferences.advanced.installCLI.subtitle" = "将 CodexBarCLI 以 codexbar 的名称软链接到 /usr/local/bin 和 /opt/homebrew/bin。"; +"preferences.advanced.keychain.caption" = "禁用所有钥匙串读写。浏览器 Cookie 导入将不可用;请在 Providers 中手动粘贴 Cookie 标头。"; +"preferences.advanced.keychain.title" = "钥匙串访问"; +"preferences.advanced.openMenu.subtitle" = "从任何位置触发菜单栏菜单。"; +"preferences.advanced.openMenu.title" = "打开菜单"; +"preferences.advanced.section.shortcut" = "键盘快捷键"; +"preferences.advanced.showDebug.subtitle" = "在 Debug 标签页中显示故障排查工具。"; +"preferences.advanced.showDebug.title" = "显示调试设置"; +"preferences.advanced.surprise.subtitle" = "看看你是否喜欢让 agents 在上面玩点花样。"; +"preferences.advanced.surprise.title" = "给我点惊喜"; +"preferences.debug.animations.blink" = "立即闪烁"; +"preferences.debug.animations.pattern" = "动画模式"; +"preferences.debug.animations.random" = "随机(默认)"; +"preferences.debug.animations.replay" = "重播所选动画"; +"preferences.debug.animations.title" = "加载动画"; +"preferences.debug.caches.caption" = "清除缓存的费用扫描结果。"; +"preferences.debug.caches.clear" = "清除费用缓存"; +"preferences.debug.caches.cleared" = "已清除。"; +"preferences.debug.caches.failed" = "失败:%@"; +"preferences.debug.caches.title" = "缓存"; +"preferences.debug.cliPaths.caption" = "解析后的 Codex 二进制与 PATH 层级;启动登录 PATH 捕获(短超时)。"; +"preferences.debug.cliPaths.claudeBinary" = "Claude 二进制"; +"preferences.debug.cliPaths.codexBinary" = "Codex 二进制"; +"preferences.debug.cliPaths.effectivePath" = "生效中的 PATH"; +"preferences.debug.cliPaths.loginPath" = "登录 shell PATH(启动捕获)"; +"preferences.debug.cliPaths.notFound" = "未找到"; +"preferences.debug.cliPaths.title" = "CLI 路径"; +"preferences.debug.cliPaths.unavailable" = "不可用"; +"preferences.debug.cliSessions.caption" = "在探测后保持 Codex/Claude CLI 会话存活。默认会在捕获数据后退出。"; +"preferences.debug.cliSessions.keepAlive.subtitle" = "在探测之间跳过清理(仅调试)。"; +"preferences.debug.cliSessions.keepAlive.title" = "保持 CLI 会话存活"; +"preferences.debug.cliSessions.reset" = "重置 CLI 会话"; +"preferences.debug.cliSessions.title" = "CLI 会话"; +"preferences.debug.emptyLog" = "暂无日志。请点击获取。"; +"preferences.debug.errorSimulation.caption" = "向菜单卡片注入一条假的错误消息以测试布局。"; +"preferences.debug.errorSimulation.clearCost" = "清除费用错误"; +"preferences.debug.errorSimulation.clearMenu" = "清除菜单错误"; +"preferences.debug.errorSimulation.setCost" = "设置费用错误"; +"preferences.debug.errorSimulation.setMenu" = "设置菜单错误"; +"preferences.debug.errorSimulation.text" = "模拟错误文本"; +"preferences.debug.errorSimulation.title" = "错误模拟"; +"preferences.debug.fetchAttempts.available" = " 可用"; +"preferences.debug.fetchAttempts.caption" = "某个 provider 最近一次获取流水线的决策与错误。"; +"preferences.debug.fetchAttempts.empty" = "尚无获取尝试。"; +"preferences.debug.fetchAttempts.error" = " error=%@"; +"preferences.debug.fetchAttempts.title" = "获取策略尝试"; +"preferences.debug.fetchAttempts.unavailable" = " 不可用"; +"preferences.debug.fetchKind.api" = "api"; +"preferences.debug.fetchKind.cli" = "cli"; +"preferences.debug.fetchKind.local" = "local"; +"preferences.debug.fetchKind.oauth" = "oauth"; +"preferences.debug.fetchKind.web" = "web"; +"preferences.debug.forceAnimation.subtitle" = "在下一次刷新后临时显示加载动画。"; +"preferences.debug.forceAnimation.title" = "下次刷新时强制动画"; +"preferences.debug.loading" = "加载中…"; +"preferences.debug.logging.enable.subtitle" = "将日志写入 %@ 以供调试。"; +"preferences.debug.logging.enable.title" = "启用文件日志"; +"preferences.debug.logging.openFile" = "打开日志文件"; +"preferences.debug.logging.title" = "日志"; +"preferences.debug.logging.verbosity" = "详细程度"; +"preferences.debug.logging.verbosity.subtitle" = "控制记录的详细级别。"; +"preferences.debug.notifications.caption" = "为 5 小时会话窗口触发测试通知(耗尽/恢复)。"; +"preferences.debug.notifications.depleted" = "发送耗尽通知"; +"preferences.debug.notifications.restored" = "发送恢复通知"; +"preferences.debug.notifications.title" = "通知"; +"preferences.debug.openAICookies.caption" = "最近一次 OpenAI Cookies 尝试的 Cookie 导入和 WebKit 抓取日志。"; +"preferences.debug.openAICookies.empty" = "暂无日志。请在 Providers → Codex 中更新 OpenAI Cookies 以运行导入。"; +"preferences.debug.openAICookies.title" = "OpenAI Cookies"; +"preferences.debug.probeLogs.caption" = "获取最新的探测输出以供调试;“复制”会保留完整文本。"; +"preferences.debug.probeLogs.fetch" = "获取日志"; +"preferences.debug.probeLogs.parseDump" = "加载解析转储"; +"preferences.debug.probeLogs.redetect" = "重新运行 provider 自动检测"; +"preferences.debug.probeLogs.save" = "保存到文件"; +"preferences.debug.probeLogs.title" = "探测日志"; +"preferences.debug.provider" = "Provider"; +"preferences.display.mergeIcons.subtitle" = "使用单个菜单栏图标并提供 provider 切换器。"; +"preferences.display.mergeIcons.title" = "合并图标"; +"preferences.display.mode.picker" = "显示模式"; +"preferences.display.mode.subtitle" = "选择菜单栏中显示的内容(Pace 表示实际用量与预期用量的对比)。"; +"preferences.display.mode.title" = "显示模式"; +"preferences.display.overviewProviders.choose" = "最多选择 %@ 个 provider"; +"preferences.display.overviewProviders.configure" = "配置…"; +"preferences.display.overviewProviders.mergeRequired" = "启用“合并图标”后才能配置总览标签页 provider。"; +"preferences.display.overviewProviders.noneAvailable" = "没有可用于总览的已启用 provider。"; +"preferences.display.overviewProviders.noneSelected" = "未选择 provider"; +"preferences.display.overviewProviders.orderHint" = "总览行始终遵循 provider 顺序。"; +"preferences.display.overviewProviders.title" = "总览标签页 provider"; +"preferences.display.resetAsClock.subtitle" = "将重置时间显示为绝对时刻,而不是倒计时。"; +"preferences.display.resetAsClock.title" = "以时钟显示重置时间"; +"preferences.display.section.menuBar" = "菜单栏"; +"preferences.display.section.menuContent" = "菜单内容"; +"preferences.display.showAllAccounts.subtitle" = "在菜单中堆叠显示 token 账户(否则显示账户切换条)。"; +"preferences.display.showAllAccounts.title" = "显示所有 token 账户"; +"preferences.display.showCredits.subtitle" = "在菜单中显示 Codex Credits 和 Claude Extra usage 部分。"; +"preferences.display.showCredits.title" = "显示 credits 和额外用量"; +"preferences.display.showMostUsed.subtitle" = "菜单栏会自动显示最接近限额的 provider。"; +"preferences.display.showMostUsed.title" = "显示最常用 provider"; +"preferences.display.showUsageAsUsed.subtitle" = "进度条会随着配额消耗而填充(而不是显示剩余量)。"; +"preferences.display.showUsageAsUsed.title" = "按已用量显示"; +"preferences.display.showsPercent.subtitle" = "用 provider 品牌图标和百分比替代 critter 条形图。"; +"preferences.display.showsPercent.title" = "菜单栏显示百分比"; +"preferences.display.switcherShowsIcons.subtitle" = "在切换器中显示 provider 图标(否则显示每周进度线)。"; +"preferences.display.switcherShowsIcons.title" = "切换器显示图标"; +"preferences.general.costStatus.error" = "%@: %@"; +"preferences.general.costStatus.fetching" = "%@: 获取中…%@"; +"preferences.general.costStatus.lastAttempt" = "%@: 上次尝试 %@"; +"preferences.general.costStatus.noData" = "%@: 暂无数据"; +"preferences.general.costStatus.snapshot" = "%@: %@ · 30 天 %@"; +"preferences.general.costStatus.unsupported" = "%@: 不支持"; +"preferences.general.costSummary.status" = "自动刷新:每小时 · 超时:10 分钟"; +"preferences.general.costSummary.subtitle" = "读取本地用量日志,并在菜单中显示今天与最近 30 天的费用。"; +"preferences.general.costSummary.title" = "显示费用摘要"; +"preferences.general.notifications.subtitle" = "当 5 小时会话配额降至 0% 以及再次可用时发出通知。"; +"preferences.general.notifications.title" = "会话配额通知"; +"preferences.general.quit" = "退出 CodexBar"; +"preferences.general.refreshCadence.manualHint" = "自动刷新已关闭;请使用菜单中的“刷新”命令。"; +"preferences.general.refreshCadence.picker" = "刷新频率"; +"preferences.general.refreshCadence.subtitle" = "CodexBar 在后台轮询 provider 的频率。"; +"preferences.general.refreshCadence.title" = "刷新频率"; +"preferences.general.section.automation" = "自动化"; +"preferences.general.section.system" = "系统"; +"preferences.general.section.usage" = "用量"; +"preferences.general.startAtLogin.subtitle" = "在 Mac 启动时自动打开 CodexBar。"; +"preferences.general.startAtLogin.title" = "登录时启动"; +"preferences.general.statusChecks.subtitle" = "轮询 OpenAI/Claude 状态页以及 Gemini/Antigravity 的 Google Workspace 状态,并在图标和菜单中显示故障。"; +"preferences.general.statusChecks.title" = "检查 provider 状态"; +"preferences.tab.about" = "关于"; +"preferences.tab.advanced" = "高级"; +"preferences.tab.debug" = "调试"; +"preferences.tab.display" = "显示"; +"preferences.tab.general" = "通用"; +"preferences.tab.providers" = "Providers"; +"provider.label.addOnCredits" = "附加 Credits"; +"provider.label.ampFree" = "Amp 免费版"; +"provider.label.balance" = "余额"; +"provider.label.bonus" = "奖励"; +"provider.label.chat" = "聊天"; +"provider.label.credits" = "Credits"; +"provider.label.current" = "当前"; +"provider.label.fiveHour" = "5 小时"; +"provider.label.monthly" = "每月"; +"provider.label.onDemand" = "按需"; +"provider.label.plan" = "套餐"; +"provider.label.premium" = "高级"; +"provider.label.pro" = "Pro"; +"provider.label.prompts" = "提示数"; +"provider.label.quota" = "配额"; +"provider.label.rateLimit" = "速率限制"; +"provider.label.refill" = "补充"; +"provider.label.requests" = "请求数"; +"provider.label.session" = "会话"; +"provider.label.sonnet" = "Sonnet"; +"provider.label.standard" = "标准"; +"provider.label.tokens" = "Tokens"; +"provider.label.usage" = "用量"; +"provider.label.weekly" = "每周"; +"provider.label.window" = "窗口"; +"provider.menu.openAugmentRelogin" = "打开 Augment(退出并重新登录)"; +"provider.menu.refreshSession" = "刷新会话"; +"provider.settings.alibaba.cookiesDisabled" = "Alibaba Cookies 已禁用。"; +"provider.settings.alibaba.manualCookie" = "粘贴来自 modelstudio.console.alibabacloud.com 的 Cookie 标头。"; +"provider.settings.alibabaAPIKey.subtitle" = "存储在 ~/.codexbar/config.json 中。请粘贴来自 Model Studio 的 Coding Plan API 密钥。"; +"provider.settings.alibabaRegion.subtitle" = "为配额获取选择国际版或中国大陆版控制台网关。"; +"provider.settings.alwaysAllowPrompts" = "始终允许提示"; +"provider.settings.amp.cookiesDisabled" = "Amp Cookies 已禁用。"; +"provider.settings.amp.manualCookie" = "粘贴来自 Amp 设置页的 Cookie 标头或 cURL 抓取内容。"; +"provider.settings.apiKey.title" = "API 密钥"; +"provider.settings.apiRegion.title" = "API 区域"; +"provider.settings.apiToken.title" = "API 令牌"; +"provider.settings.augment.cookiesDisabled" = "Augment Cookies 已禁用。"; +"provider.settings.augment.manualCookie" = "粘贴来自 Augment 仪表板的 Cookie 标头或 cURL 抓取内容。"; +"provider.settings.autoDetect" = "自动检测"; +"provider.settings.avoidKeychainPrompts.subtitle" = "使用 /usr/bin/security 读取 Claude 凭证,以避免 CodexBar 的钥匙串提示。"; +"provider.settings.avoidKeychainPrompts.title" = "避免钥匙串提示(实验性)"; +"provider.settings.claude.cookiesDisabled" = "Claude Cookies 已禁用。"; +"provider.settings.claude.manualCookie" = "粘贴来自 claude.ai 请求的 Cookie 标头。"; +"provider.settings.claudeCookies.subtitle" = "自动导入用于网页 API 的浏览器 Cookies。"; +"provider.settings.claudeCookies.title" = "Claude Cookies"; +"provider.settings.codex.cookiesDisabled" = "禁用 OpenAI 仪表板 Cookie 用法。"; +"provider.settings.codex.manualCookie" = "粘贴来自 chatgpt.com 请求的 Cookie 标头。"; +"provider.settings.cookieHeader.title" = "Cookie 标头"; +"provider.settings.cookieSource.alibabaAuto" = "自动导入来自 Model Studio/Bailian 的浏览器 Cookies。"; +"provider.settings.cookieSource.autoBrowserCookies" = "自动导入浏览器 Cookies。"; +"provider.settings.cookieSource.cursorAuto" = "自动导入浏览器 Cookies 或已存储会话。"; +"provider.settings.cookieSource.factoryAuto" = "自动导入浏览器 Cookies 和 WorkOS 令牌。"; +"provider.settings.cookieSource.minimaxAuto" = "自动导入浏览器 Cookies 和本地存储令牌。"; +"provider.settings.cookieSource.opencodeAuto" = "自动导入来自 opencode.ai 的浏览器 Cookies。"; +"provider.settings.cookieSource.title" = "Cookie 来源"; +"provider.settings.cursor.cookiesDisabled" = "Cursor Cookies 已禁用。"; +"provider.settings.cursor.manualCookie" = "粘贴来自 cursor.com 请求的 Cookie 标头。"; +"provider.settings.customPath.subtitle" = "使用自定义 IDE 基础路径覆盖自动检测"; +"provider.settings.customPath.title" = "自定义路径"; +"provider.settings.factory.cookiesDisabled" = "Factory Cookies 已禁用。"; +"provider.settings.factory.manualCookie" = "粘贴来自 app.factory.ai 的 Cookie 标头。"; +"provider.settings.gatewayRegion.title" = "网关区域"; +"provider.settings.githubLogin.subtitle" = "需要通过 GitHub Device Flow 进行认证。"; +"provider.settings.githubLogin.title" = "GitHub 登录"; +"provider.settings.historicalTracking.subtitle" = "存储本地 Codex 用量历史(8 周),用于个性化 Pace 预测。"; +"provider.settings.historicalTracking.title" = "历史跟踪"; +"provider.settings.jetbrainsIDE.subtitle" = "选择要监控的 IDE"; +"provider.settings.jetbrainsIDE.title" = "JetBrains IDE"; +"provider.settings.keychainPromptPolicy.disabled" = "高级设置中已全局禁用钥匙串访问,因此此设置当前不生效。"; +"provider.settings.keychainPromptPolicy.subtitle" = "仅适用于 Security.framework OAuth 钥匙串读取器。"; +"provider.settings.keychainPromptPolicy.title" = "钥匙串提示策略"; +"provider.settings.kiloAPIKey.subtitle" = "存储在 ~/.codexbar/config.json 中。你也可以提供 KILO_API_KEY 或 ~/.local/share/kilo/auth.json(kilo.access)。"; +"provider.settings.kimi.cookiesDisabled" = "Kimi Cookies 已禁用。"; +"provider.settings.kimi.manualCookie" = "粘贴 cookie 标头或 kimi-auth 令牌值。"; +"provider.settings.kimik2APIKey.subtitle" = "存储在 ~/.codexbar/config.json 中。可在 kimi-k2.ai 生成。"; +"provider.settings.minimax.cookiesDisabled" = "MiniMax Cookies 已禁用。"; +"provider.settings.minimax.manualCookie" = "粘贴来自 Coding Plan 页面的 Cookie 标头或 cURL 抓取内容。"; +"provider.settings.minimaxAPIToken.subtitle" = "存储在 ~/.codexbar/config.json 中。请粘贴您的 MiniMax API 密钥。"; +"provider.settings.minimaxRegion.subtitle" = "选择 MiniMax 主机(全球 .io 或中国大陆 .com)。"; +"provider.settings.neverPrompt" = "从不提示"; +"provider.settings.ollama.cookiesDisabled" = "Ollama Cookies 已禁用。"; +"provider.settings.ollama.manualCookie" = "粘贴来自 Ollama 设置页的 Cookie 标头或 cURL 抓取内容。"; +"provider.settings.onlyOnUserAction" = "仅在用户操作时提示"; +"provider.settings.openAICookies.subtitle" = "自动导入用于仪表板附加数据的浏览器 Cookies。"; +"provider.settings.openAICookies.title" = "OpenAI Cookies"; +"provider.settings.openAIWebExtras.subtitle" = "通过 chatgpt.com 显示用量明细、credits 历史和代码审查。"; +"provider.settings.openAIWebExtras.title" = "OpenAI 网页附加信息"; +"provider.settings.openAPIKeys" = "打开 API Keys"; +"provider.settings.openAmpSettings" = "打开 Amp 设置"; +"provider.settings.openCodingPlan" = "打开 Coding Plan"; +"provider.settings.openConsole" = "打开控制台"; +"provider.settings.openOllamaSettings" = "打开 Ollama 设置"; +"provider.settings.openRouterAPIKey.subtitle" = "存储在 ~/.codexbar/config.json 中。请从 openrouter.ai/settings/keys 获取密钥,并在那里设置密钥消费上限以启用 API 密钥配额跟踪。"; +"provider.settings.openWarpGuide" = "打开 Warp API Key 指南"; +"provider.settings.opencode.cookiesDisabled" = "OpenCode Cookies 已禁用。"; +"provider.settings.opencode.manualCookie" = "粘贴从计费页面捕获的 Cookie 标头。"; +"provider.settings.signInAgain" = "重新登录"; +"provider.settings.signInWithGitHub" = "使用 GitHub 登录"; +"provider.settings.syntheticAPIKey.subtitle" = "存储在 ~/.codexbar/config.json 中。请粘贴来自 Synthetic 仪表板的密钥。"; +"provider.settings.usageSource.autoFallback" = "自动模式会在首选来源失败时回退到下一个来源。"; +"provider.settings.usageSource.kilo.subtitle" = "自动模式会优先使用 API,并在认证失败时回退到 CLI。"; +"provider.settings.usageSource.title" = "用量来源"; +"provider.settings.warpAPIKey.subtitle" = "存储在 ~/.codexbar/config.json 中。在 Warp 中打开 Settings > Platform > API Keys,然后创建一个。"; +"provider.settings.workspaceID.subtitle" = "当工作区查找失败时使用的可选覆盖值。"; +"provider.settings.workspaceID.title" = "工作区 ID"; +"provider.settings.zaiRegion.subtitle" = "中国大陆端点(open.bigmodel.cn)请使用 BigModel。"; +"provider.toggle.alibaba" = "显示 Alibaba 用量"; +"provider.toggle.amp" = "显示 Amp 用量"; +"provider.toggle.antigravity" = "显示 Antigravity 用量 (experimental)"; +"provider.toggle.augment" = "显示 Augment 用量"; +"provider.toggle.claude" = "显示 Claude Code 用量"; +"provider.toggle.codex" = "显示 Codex 用量"; +"provider.toggle.copilot" = "显示 Copilot 用量"; +"provider.toggle.cursor" = "显示 Cursor 用量"; +"provider.toggle.gemini" = "显示 Gemini 用量"; +"provider.toggle.kilo" = "显示 Kilo 用量"; +"provider.toggle.kimi" = "显示 Kimi 用量"; +"provider.toggle.kimik2" = "显示 Kimi K2 用量"; +"provider.toggle.kiro" = "显示 Kiro 用量"; +"provider.toggle.minimax" = "显示 MiniMax 用量"; +"provider.toggle.ollama" = "显示 Ollama 用量"; +"provider.toggle.opencode" = "显示 OpenCode 用量"; +"provider.toggle.openrouter" = "显示 OpenRouter 用量"; +"provider.toggle.synthetic" = "显示 Synthetic 用量"; +"provider.toggle.vertexai" = "显示 Vertex AI 用量"; +"provider.toggle.warp" = "显示 Warp 用量"; +"provider.toggle.zai" = "显示 z.ai 用量"; +"widget.empty.historyRefresh" = "刷新后将显示用量历史。"; +"widget.empty.switcherRefresh" = "刷新后将显示用量数据。"; +"widget.empty.usageRefresh" = "应用刷新后将显示用量数据。"; +"widget.metric.30dCost" = "30 天费用"; +"widget.metric.codeReview" = "代码审查"; +"widget.metric.creditsLeft" = "剩余 Credits"; +"widget.metric.todayCost" = "今日费用"; +"widget.openCodexBar" = "打开 CodexBar"; +"provider.settings.cached" = "已缓存:%@ • %@"; +"widget.metric.credits" = "Credits"; +"widget.metric.today" = "今天"; +"widget.metric.30d" = "30 天"; +"preferences.debug.animations.caption" = "选择一种模式并在菜单栏中重播。\"随机\"会保留现有行为。"; diff --git a/Sources/CodexBarWidget/CodexBarWidgetViews.swift b/Sources/CodexBarWidget/CodexBarWidgetViews.swift index 3b0dd2d27..8ca80ad1b 100644 --- a/Sources/CodexBarWidget/CodexBarWidgetViews.swift +++ b/Sources/CodexBarWidget/CodexBarWidgetViews.swift @@ -33,10 +33,10 @@ struct CodexBarUsageWidgetView: View { private var emptyState: some View { VStack(alignment: .leading, spacing: 6) { - Text("Open CodexBar") + Text(L10n.tr("widget.openCodexBar", default: "Open CodexBar")) .font(.body) .fontWeight(.semibold) - Text("Usage data will appear once the app refreshes.") + Text(L10n.tr("widget.empty.usageRefresh", default: "Usage data will appear once the app refreshes.")) .font(.caption) .foregroundStyle(.secondary) } @@ -63,10 +63,10 @@ struct CodexBarHistoryWidgetView: View { private var emptyState: some View { VStack(alignment: .leading, spacing: 6) { - Text("Open CodexBar") + Text(L10n.tr("widget.openCodexBar", default: "Open CodexBar")) .font(.body) .fontWeight(.semibold) - Text("Usage history will appear after a refresh.") + Text(L10n.tr("widget.empty.historyRefresh", default: "Usage history will appear after a refresh.")) .font(.caption) .foregroundStyle(.secondary) } @@ -92,10 +92,10 @@ struct CodexBarCompactWidgetView: View { private var emptyState: some View { VStack(alignment: .leading, spacing: 6) { - Text("Open CodexBar") + Text(L10n.tr("widget.openCodexBar", default: "Open CodexBar")) .font(.body) .fontWeight(.semibold) - Text("Usage data will appear once the app refreshes.") + Text(L10n.tr("widget.empty.usageRefresh", default: "Usage data will appear once the app refreshes.")) .font(.caption) .foregroundStyle(.secondary) } @@ -143,10 +143,10 @@ struct CodexBarSwitcherWidgetView: View { private var emptyState: some View { VStack(alignment: .leading, spacing: 6) { - Text("Open CodexBar") + Text(L10n.tr("widget.openCodexBar", default: "Open CodexBar")) .font(.caption) .foregroundStyle(.secondary) - Text("Usage data appears after a refresh.") + Text(L10n.tr("widget.empty.switcherRefresh", default: "Usage data appears after a refresh.")) .font(.caption2) .foregroundStyle(.secondary) } @@ -182,15 +182,15 @@ private struct CompactMetricView: View { switch self.metric { case .credits: let value = self.entry.creditsRemaining.map(WidgetFormat.credits) ?? "—" - return (value, "Credits left", nil) + return (value, L10n.tr("widget.metric.creditsLeft", default: "Credits left"), nil) case .todayCost: let value = self.entry.tokenUsage?.sessionCostUSD.map(WidgetFormat.usd) ?? "—" let detail = self.entry.tokenUsage?.sessionTokens.map(WidgetFormat.tokenCount) - return (value, "Today cost", detail) + return (value, L10n.tr("widget.metric.todayCost", default: "Today cost"), detail) case .last30DaysCost: let value = self.entry.tokenUsage?.last30DaysCostUSD.map(WidgetFormat.usd) ?? "—" let detail = self.entry.tokenUsage?.last30DaysTokens.map(WidgetFormat.tokenCount) - return (value, "30d cost", detail) + return (value, L10n.tr("widget.metric.30dCost", default: "30d cost"), detail) } } } @@ -290,16 +290,16 @@ private struct SwitcherSmallUsageView: View { var body: some View { VStack(alignment: .leading, spacing: 8) { UsageBarRow( - title: ProviderDefaults.metadata[self.entry.provider]?.sessionLabel ?? "Session", + title: ProviderDefaults.metadata[self.entry.provider]?.sessionLabel ?? L10n.tr("provider.label.session", default: "Session"), percentLeft: self.entry.primary?.remainingPercent, color: WidgetColors.color(for: self.entry.provider)) UsageBarRow( - title: ProviderDefaults.metadata[self.entry.provider]?.weeklyLabel ?? "Weekly", + title: ProviderDefaults.metadata[self.entry.provider]?.weeklyLabel ?? L10n.tr("provider.label.weekly", default: "Weekly"), percentLeft: self.entry.secondary?.remainingPercent, color: WidgetColors.color(for: self.entry.provider)) if let codeReview = entry.codeReviewRemainingPercent { UsageBarRow( - title: "Code review", + title: L10n.tr("widget.metric.codeReview", default: "Code review"), percentLeft: codeReview, color: WidgetColors.color(for: self.entry.provider)) } @@ -313,19 +313,19 @@ private struct SwitcherMediumUsageView: View { var body: some View { VStack(alignment: .leading, spacing: 10) { UsageBarRow( - title: ProviderDefaults.metadata[self.entry.provider]?.sessionLabel ?? "Session", + title: ProviderDefaults.metadata[self.entry.provider]?.sessionLabel ?? L10n.tr("provider.label.session", default: "Session"), percentLeft: self.entry.primary?.remainingPercent, color: WidgetColors.color(for: self.entry.provider)) UsageBarRow( - title: ProviderDefaults.metadata[self.entry.provider]?.weeklyLabel ?? "Weekly", + title: ProviderDefaults.metadata[self.entry.provider]?.weeklyLabel ?? L10n.tr("provider.label.weekly", default: "Weekly"), percentLeft: self.entry.secondary?.remainingPercent, color: WidgetColors.color(for: self.entry.provider)) if let credits = entry.creditsRemaining { - ValueLine(title: "Credits", value: WidgetFormat.credits(credits)) + ValueLine(title: L10n.tr("widget.metric.credits", default: "Credits"), value: WidgetFormat.credits(credits)) } if let token = entry.tokenUsage { ValueLine( - title: "Today", + title: L10n.tr("widget.metric.today", default: "Today"), value: WidgetFormat.costAndTokens(cost: token.sessionCostUSD, tokens: token.sessionTokens)) } } @@ -338,29 +338,29 @@ private struct SwitcherLargeUsageView: View { var body: some View { VStack(alignment: .leading, spacing: 12) { UsageBarRow( - title: ProviderDefaults.metadata[self.entry.provider]?.sessionLabel ?? "Session", + title: ProviderDefaults.metadata[self.entry.provider]?.sessionLabel ?? L10n.tr("provider.label.session", default: "Session"), percentLeft: self.entry.primary?.remainingPercent, color: WidgetColors.color(for: self.entry.provider)) UsageBarRow( - title: ProviderDefaults.metadata[self.entry.provider]?.weeklyLabel ?? "Weekly", + title: ProviderDefaults.metadata[self.entry.provider]?.weeklyLabel ?? L10n.tr("provider.label.weekly", default: "Weekly"), percentLeft: self.entry.secondary?.remainingPercent, color: WidgetColors.color(for: self.entry.provider)) if let codeReview = entry.codeReviewRemainingPercent { UsageBarRow( - title: "Code review", + title: L10n.tr("widget.metric.codeReview", default: "Code review"), percentLeft: codeReview, color: WidgetColors.color(for: self.entry.provider)) } if let credits = entry.creditsRemaining { - ValueLine(title: "Credits", value: WidgetFormat.credits(credits)) + ValueLine(title: L10n.tr("widget.metric.credits", default: "Credits"), value: WidgetFormat.credits(credits)) } if let token = entry.tokenUsage { VStack(alignment: .leading, spacing: 4) { ValueLine( - title: "Today", + title: L10n.tr("widget.metric.today", default: "Today"), value: WidgetFormat.costAndTokens(cost: token.sessionCostUSD, tokens: token.sessionTokens)) ValueLine( - title: "30d", + title: L10n.tr("widget.metric.30d", default: "30d"), value: WidgetFormat.costAndTokens( cost: token.last30DaysCostUSD, tokens: token.last30DaysTokens)) @@ -379,16 +379,16 @@ private struct SmallUsageView: View { VStack(alignment: .leading, spacing: 8) { HeaderView(provider: self.entry.provider, updatedAt: self.entry.updatedAt) UsageBarRow( - title: ProviderDefaults.metadata[self.entry.provider]?.sessionLabel ?? "Session", + title: ProviderDefaults.metadata[self.entry.provider]?.sessionLabel ?? L10n.tr("provider.label.session", default: "Session"), percentLeft: self.entry.primary?.remainingPercent, color: WidgetColors.color(for: self.entry.provider)) UsageBarRow( - title: ProviderDefaults.metadata[self.entry.provider]?.weeklyLabel ?? "Weekly", + title: ProviderDefaults.metadata[self.entry.provider]?.weeklyLabel ?? L10n.tr("provider.label.weekly", default: "Weekly"), percentLeft: self.entry.secondary?.remainingPercent, color: WidgetColors.color(for: self.entry.provider)) if let codeReview = entry.codeReviewRemainingPercent { UsageBarRow( - title: "Code review", + title: L10n.tr("widget.metric.codeReview", default: "Code review"), percentLeft: codeReview, color: WidgetColors.color(for: self.entry.provider)) } @@ -404,19 +404,19 @@ private struct MediumUsageView: View { VStack(alignment: .leading, spacing: 10) { HeaderView(provider: self.entry.provider, updatedAt: self.entry.updatedAt) UsageBarRow( - title: ProviderDefaults.metadata[self.entry.provider]?.sessionLabel ?? "Session", + title: ProviderDefaults.metadata[self.entry.provider]?.sessionLabel ?? L10n.tr("provider.label.session", default: "Session"), percentLeft: self.entry.primary?.remainingPercent, color: WidgetColors.color(for: self.entry.provider)) UsageBarRow( - title: ProviderDefaults.metadata[self.entry.provider]?.weeklyLabel ?? "Weekly", + title: ProviderDefaults.metadata[self.entry.provider]?.weeklyLabel ?? L10n.tr("provider.label.weekly", default: "Weekly"), percentLeft: self.entry.secondary?.remainingPercent, color: WidgetColors.color(for: self.entry.provider)) if let credits = entry.creditsRemaining { - ValueLine(title: "Credits", value: WidgetFormat.credits(credits)) + ValueLine(title: L10n.tr("widget.metric.credits", default: "Credits"), value: WidgetFormat.credits(credits)) } if let token = entry.tokenUsage { ValueLine( - title: "Today", + title: L10n.tr("widget.metric.today", default: "Today"), value: WidgetFormat.costAndTokens(cost: token.sessionCostUSD, tokens: token.sessionTokens)) } } @@ -431,29 +431,29 @@ private struct LargeUsageView: View { VStack(alignment: .leading, spacing: 12) { HeaderView(provider: self.entry.provider, updatedAt: self.entry.updatedAt) UsageBarRow( - title: ProviderDefaults.metadata[self.entry.provider]?.sessionLabel ?? "Session", + title: ProviderDefaults.metadata[self.entry.provider]?.sessionLabel ?? L10n.tr("provider.label.session", default: "Session"), percentLeft: self.entry.primary?.remainingPercent, color: WidgetColors.color(for: self.entry.provider)) UsageBarRow( - title: ProviderDefaults.metadata[self.entry.provider]?.weeklyLabel ?? "Weekly", + title: ProviderDefaults.metadata[self.entry.provider]?.weeklyLabel ?? L10n.tr("provider.label.weekly", default: "Weekly"), percentLeft: self.entry.secondary?.remainingPercent, color: WidgetColors.color(for: self.entry.provider)) if let codeReview = entry.codeReviewRemainingPercent { UsageBarRow( - title: "Code review", + title: L10n.tr("widget.metric.codeReview", default: "Code review"), percentLeft: codeReview, color: WidgetColors.color(for: self.entry.provider)) } if let credits = entry.creditsRemaining { - ValueLine(title: "Credits", value: WidgetFormat.credits(credits)) + ValueLine(title: L10n.tr("widget.metric.credits", default: "Credits"), value: WidgetFormat.credits(credits)) } if let token = entry.tokenUsage { VStack(alignment: .leading, spacing: 4) { ValueLine( - title: "Today", + title: L10n.tr("widget.metric.today", default: "Today"), value: WidgetFormat.costAndTokens(cost: token.sessionCostUSD, tokens: token.sessionTokens)) ValueLine( - title: "30d", + title: L10n.tr("widget.metric.30d", default: "30d"), value: WidgetFormat.costAndTokens( cost: token.last30DaysCostUSD, tokens: token.last30DaysTokens)) @@ -477,10 +477,10 @@ private struct HistoryView: View { .frame(height: self.isLarge ? 90 : 60) if let token = entry.tokenUsage { ValueLine( - title: "Today", + title: L10n.tr("widget.metric.today", default: "Today"), value: WidgetFormat.costAndTokens(cost: token.sessionCostUSD, tokens: token.sessionTokens)) ValueLine( - title: "30d", + title: L10n.tr("widget.metric.30d", default: "30d"), value: WidgetFormat.costAndTokens(cost: token.last30DaysCostUSD, tokens: token.last30DaysTokens)) } }