diff --git a/Sources/CodexBar/CostHistoryChartMenuView.swift b/Sources/CodexBar/CostHistoryChartMenuView.swift index c7f81ad61..c9162a204 100644 --- a/Sources/CodexBar/CostHistoryChartMenuView.swift +++ b/Sources/CodexBar/CostHistoryChartMenuView.swift @@ -49,10 +49,10 @@ struct CostHistoryChartMenuView: View { let model = Self.makeModel(provider: self.provider, daily: self.daily) VStack(alignment: .leading, spacing: 10) { if model.points.isEmpty { - Text("No cost history data.") + Text(L("No cost history data.")) .font(.footnote) .foregroundStyle(.secondary) - .accessibilityLabel("No cost history data available.") + .accessibilityLabel(L("No cost history data available.")) } else { Chart { ForEach(model.points) { point in @@ -82,8 +82,10 @@ struct CostHistoryChartMenuView: View { } .chartLegend(.hidden) .frame(height: 130) - .accessibilityLabel("Cost history chart") - .accessibilityValue(model.points.isEmpty ? "No data" : "\(model.points.count) days of cost data") + .accessibilityLabel(L("Cost history chart")) + .accessibilityValue(model.points.isEmpty ? L("No data") : String( + format: L("%@ days of cost data"), + "\(model.points.count)")) .chartOverlay { proxy in GeometryReader { geo in ZStack(alignment: .topLeading) { @@ -148,7 +150,7 @@ struct CostHistoryChartMenuView: View { } if let total = self.totalCostUSD { - Text("Est. total (30d): \(UsageFormatter.usdString(total))") + Text(String(format: L("Est. total (30d): %@"), UsageFormatter.usdString(total))) .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) @@ -354,13 +356,13 @@ struct CostHistoryChartMenuView: View { let point = model.pointsByDateKey[key], let date = Self.dateFromDayKey(key) else { - return DetailContent(primary: "Hover a bar for details", rows: []) + return DetailContent(primary: L("Hover a bar for details"), rows: []) } let dayLabel = date.formatted(.dateTime.month(.abbreviated).day()) let cost = UsageFormatter.usdString(point.costUSD) let primary = if let tokens = point.totalTokens { - "\(dayLabel): \(cost) · \(UsageFormatter.tokenCountString(tokens)) tokens" + String(format: L("%1$@: %2$@ · %3$@ tokens"), dayLabel, cost, UsageFormatter.tokenCountString(tokens)) } else { "\(dayLabel): \(cost)" } diff --git a/Sources/CodexBar/CreditsHistoryChartMenuView.swift b/Sources/CodexBar/CreditsHistoryChartMenuView.swift index a746251bb..304d58dd4 100644 --- a/Sources/CodexBar/CreditsHistoryChartMenuView.swift +++ b/Sources/CodexBar/CreditsHistoryChartMenuView.swift @@ -29,10 +29,10 @@ struct CreditsHistoryChartMenuView: View { let model = Self.makeModel(from: self.breakdown) VStack(alignment: .leading, spacing: 10) { if model.points.isEmpty { - Text("No credits history data.") + Text(L("No credits history data.")) .font(.footnote) .foregroundStyle(.secondary) - .accessibilityLabel("No credits history data available.") + .accessibilityLabel(L("No credits history data available.")) } else { Chart { ForEach(model.points) { point in @@ -62,8 +62,10 @@ struct CreditsHistoryChartMenuView: View { } .chartLegend(.hidden) .frame(height: 130) - .accessibilityLabel("Credits history chart") - .accessibilityValue(model.points.isEmpty ? "No data" : "\(model.points.count) days of credits data") + .accessibilityLabel(L("Credits history chart")) + .accessibilityValue(model.points.isEmpty ? L("No data") : String( + format: L("%@ days of credits data"), + "\(model.points.count)")) .chartOverlay { proxy in GeometryReader { geo in ZStack(alignment: .topLeading) { @@ -101,7 +103,9 @@ struct CreditsHistoryChartMenuView: View { } if let total = model.totalCreditsUsed { - Text("Total (30d): \(total.formatted(.number.precision(.fractionLength(0...2)))) credits") + Text(String( + format: L("Total (30d): %@ credits"), + total.formatted(.number.precision(.fractionLength(0...2))))) .font(.caption) .foregroundStyle(.secondary) } @@ -302,17 +306,17 @@ struct CreditsHistoryChartMenuView: View { let day = model.breakdownByDayKey[key], let date = Self.dateFromDayKey(key) else { - return ("Hover a bar for details", nil) + return (L("Hover a bar for details"), nil) } let dayLabel = date.formatted(.dateTime.month(.abbreviated).day()) let total = day.totalCreditsUsed.formatted(.number.precision(.fractionLength(0...2))) if day.services.isEmpty { - return ("\(dayLabel): \(total) credits", nil) + return (String(format: L("%1$@: %2$@ credits"), dayLabel, total), nil) } if day.services.count <= 1, let first = day.services.first { let used = first.creditsUsed.formatted(.number.precision(.fractionLength(0...2))) - return ("\(dayLabel): \(used) credits", first.service) + return (String(format: L("%1$@: %2$@ credits"), dayLabel, used), first.service) } let services = day.services @@ -324,6 +328,6 @@ struct CreditsHistoryChartMenuView: View { .map { "\($0.service) \($0.creditsUsed.formatted(.number.precision(.fractionLength(0...2))))" } .joined(separator: " · ") - return ("\(dayLabel): \(total) credits", services) + return (String(format: L("%1$@: %2$@ credits"), dayLabel, total), services) } } diff --git a/Sources/CodexBar/CursorLoginRunner.swift b/Sources/CodexBar/CursorLoginRunner.swift index d7e667002..4e5dbe9f0 100644 --- a/Sources/CodexBar/CursorLoginRunner.swift +++ b/Sources/CodexBar/CursorLoginRunner.swift @@ -66,7 +66,7 @@ final class CursorLoginRunner { await self.resetSessionCache() guard self.openURL(Self.authURL) else { - let message = "Could not open Cursor login in your browser." + let message = L("Could not open Cursor login in your browser.") onPhaseChange(.failed(message)) self.logger.error("Cursor login browser launch failed") return Result(outcome: .failed(message), email: nil) @@ -103,10 +103,13 @@ final class CursorLoginRunner { } private static func timeoutMessage(lastError: Error?) -> String { - let hint = "Sign in to cursor.com in your browser, then refresh Cursor in CodexBar." + let hint = L("Sign in to cursor.com in your browser, then refresh Cursor in CodexBar.") guard let lastError else { - return "Timed out waiting for Cursor login. \(hint)" + return String(format: L("Timed out waiting for Cursor login. %@"), hint) } - return "Timed out waiting for Cursor login. \(hint) Last error: \(lastError.localizedDescription)" + return String( + format: L("Timed out waiting for Cursor login. %@ Last error: %@"), + hint, + lastError.localizedDescription) } } diff --git a/Sources/CodexBar/Date+RelativeDescription.swift b/Sources/CodexBar/Date+RelativeDescription.swift index cb3c4f59e..de5b07ea7 100644 --- a/Sources/CodexBar/Date+RelativeDescription.swift +++ b/Sources/CodexBar/Date+RelativeDescription.swift @@ -1,22 +1,16 @@ import Foundation -enum RelativeTimeFormatters { - @MainActor - static let full: RelativeDateTimeFormatter = { - let formatter = RelativeDateTimeFormatter() - formatter.locale = Locale(identifier: "en_US") - formatter.unitsStyle = .full - return formatter - }() -} - extension Date { @MainActor func relativeDescription(now: Date = .now) -> String { let seconds = abs(now.timeIntervalSince(self)) if seconds < 15 { - return "just now" + return L("just now") } - return RelativeTimeFormatters.full.localizedString(for: self, relativeTo: now) + + let formatter = RelativeDateTimeFormatter() + formatter.locale = codexBarLocalizationLocale() + formatter.unitsStyle = .full + return formatter.localizedString(for: self, relativeTo: now) } } diff --git a/Sources/CodexBar/KeychainPromptCoordinator.swift b/Sources/CodexBar/KeychainPromptCoordinator.swift index abbeb0caa..65c2e67d3 100644 --- a/Sources/CodexBar/KeychainPromptCoordinator.swift +++ b/Sources/CodexBar/KeychainPromptCoordinator.swift @@ -22,96 +22,62 @@ 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 = L("Keychain Access Required") + let messageTemplate = L( + "CodexBar will ask macOS Keychain for “%@” so it can decrypt browser cookies " + + "and authenticate your account. Click OK to continue.") + let message = String( + format: messageTemplate, + 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" + let title = L("Keychain Access Required") 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: " ")) + return (title, self.keychainFetchMessage(item: "the Claude Code OAuth token", purpose: "your Claude usage")) 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: " ")) + return ( + title, + self.keychainFetchMessage(item: "your OpenAI cookie header", purpose: "Codex dashboard extras")) 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: " ")) + return (title, self.keychainFetchMessage(item: "your Claude cookie header", purpose: "Claude web usage")) 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: " ")) + return (title, self.keychainFetchMessage(item: "your Cursor cookie header")) 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: " ")) + return (title, self.keychainFetchMessage(item: "your OpenCode cookie header")) 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: " ")) + return (title, self.keychainFetchMessage(item: "your Factory cookie header")) 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: " ")) + return (title, self.keychainFetchMessage(item: "your z.ai API token")) 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: " ")) + return (title, self.keychainFetchMessage(item: "your Synthetic API key")) 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: " ")) + return (title, self.keychainFetchMessage(item: "your GitHub Copilot token")) 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: " ")) + return (title, self.keychainFetchMessage(item: "your Kimi auth token")) 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: " ")) + return (title, self.keychainFetchMessage(item: "your Kimi K2 API key")) 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: " ")) + return (title, self.keychainFetchMessage(item: "your MiniMax cookie header")) 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: " ")) + return (title, self.keychainFetchMessage(item: "your MiniMax API token")) 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: " ")) + return (title, self.keychainFetchMessage(item: "your Augment cookie header")) 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: " ")) + return (title, self.keychainFetchMessage(item: "your Amp cookie header")) } } + private static func keychainFetchMessage(item: String, purpose: String = "usage") -> String { + String( + format: L("CodexBar will ask macOS Keychain for %@ so it can fetch %@. Click OK to continue."), + L(item), + L(purpose)) + } + private static func presentAlert(title: String, message: String) { self.promptLock.lock() defer { self.promptLock.unlock() } diff --git a/Sources/CodexBar/Localization.swift b/Sources/CodexBar/Localization.swift index fddd82b72..eac9b8f47 100644 --- a/Sources/CodexBar/Localization.swift +++ b/Sources/CodexBar/Localization.swift @@ -58,6 +58,14 @@ private func localizedBundle() -> Bundle { return resourceBundle } +func codexBarLocalizationLocale() -> Locale { + let language = appLanguageDefaults().string(forKey: "appLanguage") ?? "" + if !language.isEmpty { + return Locale(identifier: language) + } + return .autoupdatingCurrent +} + private func lprojBundle(named language: String, in resourceBundle: Bundle) -> Bundle? { let candidates = [language, language.lowercased()] for candidate in candidates where !candidate.isEmpty { @@ -70,10 +78,26 @@ private func lprojBundle(named language: String, in resourceBundle: Bundle) -> B return nil } +private func localizedString(for key: String) -> String { + let bundle = localizedBundle() + let value = bundle.localizedString(forKey: key, value: nil, table: nil) + guard value == key else { return value } + + let resourceBundle = codexBarLocalizationResourceBundle() + guard bundle.bundleURL.lastPathComponent != "en.lproj", + let englishBundle = lprojBundle(named: "en", in: resourceBundle) + else { + return value + } + + let fallback = englishBundle.localizedString(forKey: key, value: nil, table: nil) + return fallback == key ? value : fallback +} + func L(_ key: String) -> String { - localizedBundle().localizedString(forKey: key, value: nil, table: nil) + localizedString(for: key) } func L(_ key: String, _ arguments: CVarArg...) -> String { - String(format: localizedBundle().localizedString(forKey: key, value: nil, table: nil), arguments: arguments) + String(format: localizedString(for: key), arguments: arguments) } diff --git a/Sources/CodexBar/LocalizedProviderText.swift b/Sources/CodexBar/LocalizedProviderText.swift new file mode 100644 index 000000000..f1d491484 --- /dev/null +++ b/Sources/CodexBar/LocalizedProviderText.swift @@ -0,0 +1,62 @@ +import Foundation + +enum LocalizedProviderText { + static func sourceLabel(_ raw: String) -> String { + raw.split(separator: "+", omittingEmptySubsequences: false) + .map { self.sourceComponent(String($0)) } + .joined(separator: " + ") + } + + static func statusText(_ status: ProviderStatus) -> String { + if status.indicator == .none { + return status.indicator.label + } + + guard let description = status.description?.trimmingCharacters(in: .whitespacesAndNewlines), + !description.isEmpty + else { + return status.indicator.label + } + return L(description) + } + + private static func sourceComponent(_ raw: String) -> String { + let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) + switch trimmed.lowercased() { + case "api": + return L("source_api") + case "auto": + return L("source_auto") + case "cached": + return L("source_cached") + case "claude": + return L("source_claude_cli") + case "cli": + return L("source_cli") + case "codex-cli": + return L("source_codex_cli") + case "local": + return L("source_local") + case "login": + return L("source_login") + case "manual": + return L("source_manual") + case "manual cookie header": + return L("source_manual_cookie_header") + case "oauth": + return L("source_oauth") + case "oauth-api": + return L("source_oauth_api") + case "off": + return L("source_off") + case "openai-web": + return L("source_openai_web") + case "web": + return L("source_web") + case "windsurf-web": + return L("source_windsurf_web") + default: + return L(trimmed) + } + } +} diff --git a/Sources/CodexBar/LocalizedUsageText.swift b/Sources/CodexBar/LocalizedUsageText.swift new file mode 100644 index 000000000..be7ae20d7 --- /dev/null +++ b/Sources/CodexBar/LocalizedUsageText.swift @@ -0,0 +1,126 @@ +import CodexBarCore +import Foundation + +enum LocalizedUsageText { + static func usageLine(remaining: Double, used: Double, showUsed: Bool) -> String { + let percent = showUsed ? used : remaining + let clamped = min(100, max(0, percent)) + let suffix = showUsed ? L("used") : L("left") + return String(format: "%.0f%% %@", clamped, suffix) + } + + static func resetLine( + for window: RateWindow, + style: ResetTimeDisplayStyle, + now: Date = .init()) -> String? + { + if let date = window.resetsAt { + let text = style == .countdown + ? self.resetCountdownDescription(from: date, now: now) + : self.resetDescription(from: date, now: now) + return String(format: L("Resets %@"), text) + } + + if let desc = window.resetDescription { + let trimmed = desc.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return nil } + if trimmed.lowercased().hasPrefix("resets") { + let suffix = trimmed.dropFirst("resets".count).trimmingCharacters(in: .whitespacesAndNewlines) + guard !suffix.isEmpty else { return L("Resets") } + return String(format: L("Resets %@"), suffix) + } + return String(format: L("Resets %@"), trimmed) + } + return nil + } + + static func updatedString(from date: Date, now: Date = .init()) -> String { + let delta = now.timeIntervalSince(date) + if abs(delta) < 60 { + return L("Updated just now") + } + + if let hours = Calendar.current.dateComponents([.hour], from: date, to: now).hour, hours < 24 { + let rel = RelativeDateTimeFormatter() + rel.locale = codexBarLocalizationLocale() + rel.unitsStyle = .abbreviated + return String(format: L("Updated %@"), rel.localizedString(for: date, relativeTo: now)) + } + + return String(format: L("Updated %@"), self.formattedTime(date)) + } + + static func creditsString(from value: Double) -> String { + let number = NumberFormatter() + number.numberStyle = .decimal + number.maximumFractionDigits = 2 + number.locale = codexBarLocalizationLocale() + let formatted = number.string(from: NSNumber(value: value)) ?? String(format: "%.2f", value) + return String(format: L("%@ left"), formatted) + } + + static func creditEventSummary(_ event: CreditEvent) -> String { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.locale = codexBarLocalizationLocale() + + let number = NumberFormatter() + number.numberStyle = .decimal + number.maximumFractionDigits = 2 + number.locale = codexBarLocalizationLocale() + + let credits = number.string(from: NSNumber(value: event.creditsUsed)) ?? "0" + return String( + format: L("%1$@ · %2$@ · %3$@ credits"), + formatter.string(from: event.date), + event.service, + credits) + } + + static func resetCountdownDescription(from date: Date, now: Date = .init()) -> String { + let seconds = max(0, date.timeIntervalSince(now)) + if seconds < 1 { return L("now") } + + let totalMinutes = max(1, Int(ceil(seconds / 60.0))) + let days = totalMinutes / (24 * 60) + let hours = (totalMinutes / 60) % 24 + let minutes = totalMinutes % 60 + + if days > 0 { + if hours > 0 { return String(format: L("in %1$d d %2$d h"), days, hours) } + return String(format: L("in %d d"), days) + } + if hours > 0 { + if minutes > 0 { return String(format: L("in %1$d h %2$d m"), hours, minutes) } + return String(format: L("in %d h"), hours) + } + return String(format: L("in %d m"), totalMinutes) + } + + static func resetDescription(from date: Date, now: Date = .init()) -> String { + let calendar = Calendar.current + if calendar.isDate(date, inSameDayAs: now) { + return self.formattedTime(date) + } + if let tomorrow = calendar.date(byAdding: .day, value: 1, to: now), + calendar.isDate(date, inSameDayAs: tomorrow) + { + return String(format: L("tomorrow, %@"), self.formattedTime(date)) + } + return date.formatted( + Date.FormatStyle() + .month(.abbreviated) + .day() + .hour() + .minute() + .locale(codexBarLocalizationLocale())) + } + + private static func formattedTime(_ date: Date) -> String { + date.formatted( + Date.FormatStyle() + .hour() + .minute() + .locale(codexBarLocalizationLocale())) + } +} diff --git a/Sources/CodexBar/MenuCardView+Costs.swift b/Sources/CodexBar/MenuCardView+Costs.swift index a139f2c2a..9340aa6f3 100644 --- a/Sources/CodexBar/MenuCardView+Costs.swift +++ b/Sources/CodexBar/MenuCardView+Costs.swift @@ -9,7 +9,7 @@ extension UsageMenuCardView.Model { { guard metadata.supportsCredits else { return nil } if let credits { - return UsageFormatter.creditsString(from: credits.remaining) + return LocalizedUsageText.creditsString(from: credits.remaining) } if let error, !error.isEmpty { return error.trimmingCharacters(in: .whitespacesAndNewlines) @@ -35,14 +35,14 @@ extension UsageMenuCardView.Model { if provider == .bedrock { let label = Self.bedrockLatestBillingDayLabel(from: snapshot) if let sessionTokens { - return "\(label): \(sessionCost) · \(sessionTokens) tokens" + return String(format: L("%1$@: %2$@ · %3$@ tokens"), label, sessionCost, sessionTokens) } - return "\(label): \(sessionCost)" + return String(format: L("%1$@: %2$@"), label, sessionCost) } if let sessionTokens { - return "Today: \(sessionCost) · \(sessionTokens) tokens" + return String(format: L("Today: %1$@ · %2$@ tokens"), sessionCost, sessionTokens) } - return "Today: \(sessionCost)" + return String(format: L("Today: %@"), sessionCost) }() let monthCost = snapshot.last30DaysCostUSD.map { UsageFormatter.usdString($0) } ?? "—" @@ -51,9 +51,9 @@ extension UsageMenuCardView.Model { let monthTokens = monthTokensValue.map { UsageFormatter.tokenCountString($0) } let monthLine: String = { if let monthTokens { - return "Last 30 days: \(monthCost) · \(monthTokens) tokens" + return String(format: L("Last 30 days: %1$@ · %2$@ tokens"), monthCost, monthTokens) } - return "Last 30 days: \(monthCost)" + return String(format: L("Last 30 days: %@"), monthCost) }() let err = (error?.isEmpty ?? true) ? nil : error return TokenUsageSection( @@ -67,13 +67,13 @@ extension UsageMenuCardView.Model { static func tokenUsageHint(provider: UsageProvider) -> String? { switch provider { case .codex: - "Estimated from local Codex logs for the selected account." + L("Estimated from local Codex logs for the selected account.") case .claude: - UsageFormatter.costEstimateHint(provider: provider) + L(UsageFormatter.costEstimateHint(provider: provider)) case .vertexai: - UsageFormatter.costEstimateHint + L(UsageFormatter.costEstimateHint) case .bedrock: - "Reported by AWS Cost Explorer; daily billing data can lag." + L("Reported by AWS Cost Explorer; daily billing data can lag.") default: nil } @@ -82,8 +82,8 @@ extension UsageMenuCardView.Model { private static func bedrockLatestBillingDayLabel(from snapshot: CostUsageTokenSnapshot) -> String { guard let entry = bedrockLatestBillingDay(from: snapshot.daily), let displayDate = bedrockDisplayDate(from: entry.date) - else { return "Latest billing day" } - return "Latest billing day (\(displayDate))" + else { return L("Latest billing day") } + return String(format: L("Latest billing day (%@)"), displayDate) } private static func bedrockLatestBillingDay(from entries: [CostUsageDailyReport.Entry]) @@ -135,7 +135,7 @@ extension UsageMenuCardView.Model { return ProviderCostSection( title: "Extra usage", percentUsed: nil, - spendLine: "Balance: \(balance)", + spendLine: String(format: L("Balance: %@"), balance), percentLine: nil) } @@ -156,13 +156,13 @@ extension UsageMenuCardView.Model { } let percentUsed = Self.clamped((cost.used / cost.limit) * 100) - let periodLabel = cost.period ?? "This month" + let periodLabel = cost.period ?? L("This month") return ProviderCostSection( title: title, percentUsed: percentUsed, - spendLine: "\(periodLabel): \(used) / \(limit)", - percentLine: String(format: "%.0f%% used", min(100, max(0, percentUsed)))) + spendLine: String(format: L("%1$@: %2$@ / %3$@"), L(periodLabel), used, limit), + percentLine: String(format: L("%.0f%% used"), min(100, max(0, percentUsed)))) } static func clamped(_ value: Double) -> Double { diff --git a/Sources/CodexBar/MenuCardView+ModelHelpers.swift b/Sources/CodexBar/MenuCardView+ModelHelpers.swift index 24eb598ca..0e78f1620 100644 --- a/Sources/CodexBar/MenuCardView+ModelHelpers.swift +++ b/Sources/CodexBar/MenuCardView+ModelHelpers.swift @@ -29,7 +29,7 @@ extension UsageMenuCardView.Model { style: ResetTimeDisplayStyle, now: Date) -> String? { - UsageFormatter.resetLine(for: window, style: style, now: now) + LocalizedUsageText.resetLine(for: window, style: style, now: now) } static func placeholder(input: Input) -> String? { diff --git a/Sources/CodexBar/MenuCardView.swift b/Sources/CodexBar/MenuCardView.swift index 42e4be0f0..331406f9e 100644 --- a/Sources/CodexBar/MenuCardView.swift +++ b/Sources/CodexBar/MenuCardView.swift @@ -11,8 +11,8 @@ struct UsageMenuCardView: View { var labelSuffix: String { switch self { - case .left: "left" - case .used: "used" + case .left: L("left") + case .used: L("used") } } @@ -181,7 +181,7 @@ struct UsageMenuCardView: View { } if let tokenUsage = self.model.tokenUsage { VStack(alignment: .leading, spacing: 6) { - Text("cost_header_estimated") + Text(L("cost_header_estimated")) .font(.body) .fontWeight(.medium) Text(tokenUsage.sessionLine) @@ -241,7 +241,7 @@ private struct UsageMenuCardHeaderView: View { } let subtitleAlignment: VerticalAlignment = self.model.subtitleStyle == .error ? .top : .firstTextBaseline HStack(alignment: subtitleAlignment) { - Text(self.model.subtitleText) + Text(L(self.model.subtitleText)) .font(.footnote) .foregroundStyle(self.subtitleColor) .lineLimit(self.model.subtitleStyle == .error ? 4 : 1) @@ -314,7 +314,7 @@ private struct CopyIconButton: View { .frame(width: 18, height: 18) } .buttonStyle(CopyIconButtonStyle(isHighlighted: self.isHighlighted)) - .accessibilityLabel(self.didCopy ? "Copied" : "Copy error") + .accessibilityLabel(self.didCopy ? L("Copied") : L("Copy error")) } private func copyToPasteboard() { @@ -331,14 +331,14 @@ private struct ProviderCostContent: View { var body: some View { VStack(alignment: .leading, spacing: 6) { - Text(self.section.title) + Text(L(self.section.title)) .font(.body) .fontWeight(.medium) if let percentUsed = self.section.percentUsed { UsageProgressBar( percent: percentUsed, tint: self.progressColor, - accessibilityLabel: "Extra usage spent") + accessibilityLabel: L("Extra usage spent")) } HStack(alignment: .firstTextBaseline) { Text(self.section.spendLine) @@ -362,11 +362,11 @@ private struct MetricRow: View { var body: some View { VStack(alignment: .leading, spacing: 6) { - Text(self.title) + Text(L(self.title)) .font(.body) .fontWeight(.medium) if let statusText = self.metric.statusText { - Text(statusText) + Text(L(statusText)) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(1) @@ -374,7 +374,7 @@ private struct MetricRow: View { UsageProgressBar( percent: self.metric.percent, tint: self.progressColor, - accessibilityLabel: self.metric.percentStyle.accessibilityLabel, + accessibilityLabel: L(self.metric.percentStyle.accessibilityLabel), pacePercent: self.metric.pacePercent, paceOnTop: self.metric.paceOnTop, warningMarkerPercents: self.metric.warningMarkerPercents) @@ -385,7 +385,7 @@ private struct MetricRow: View { .lineLimit(1) Spacer() if let rightLabel = self.metric.resetText { - Text(rightLabel) + Text(L(rightLabel)) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(1) @@ -394,14 +394,14 @@ private struct MetricRow: View { if self.metric.detailLeftText != nil || self.metric.detailRightText != nil { HStack(alignment: .firstTextBaseline) { if let detailLeft = self.metric.detailLeftText { - Text(detailLeft) + Text(L(detailLeft)) .font(.footnote) .foregroundStyle(MenuHighlightStyle.primary(self.isHighlighted)) .lineLimit(1) } Spacer() if let detailRight = self.metric.detailRightText { - Text(detailRight) + Text(L(detailRight)) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(1) @@ -411,7 +411,7 @@ private struct MetricRow: View { } .frame(maxWidth: .infinity, alignment: .leading) if let detail = self.metric.detailText { - Text(detail) + Text(L(detail)) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(1) @@ -429,7 +429,7 @@ private struct UsageNotesContent: View { var body: some View { VStack(alignment: .leading, spacing: 4) { ForEach(Array(self.notes.enumerated()), id: \.offset) { _, note in - Text(note) + Text(L(note)) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(2) @@ -550,14 +550,14 @@ private struct CreditsBarContent: View { var body: some View { VStack(alignment: .leading, spacing: 6) { - Text("Credits") + Text(L("Credits")) .font(.body) .fontWeight(.medium) if let percentLeft { UsageProgressBar( percent: percentLeft, tint: self.progressColor, - accessibilityLabel: "Credits remaining") + accessibilityLabel: L("Credits remaining")) HStack(alignment: .firstTextBaseline) { Text(self.creditsText) .font(.caption) @@ -598,7 +598,7 @@ struct UsageMenuCardCostSectionView: View { VStack(alignment: .leading, spacing: 10) { if let tokenUsage = self.model.tokenUsage { VStack(alignment: .leading, spacing: 6) { - Text("cost_header_estimated") + Text(L("cost_header_estimated")) .font(.body) .fontWeight(.medium) Text(tokenUsage.sessionLine) @@ -950,14 +950,14 @@ extension UsageMenuCardView.Model { } if isRefreshing, snapshot == nil { - return ("Refreshing...", .loading) + return (L("Refreshing"), .loading) } if let updated = snapshot?.updatedAt { - return (UsageFormatter.updatedString(from: updated, now: now), .info) + return (LocalizedUsageText.updatedString(from: updated, now: now), .info) } - return ("Not fetched yet", .info) + return (L("Not fetched yet"), .info) } private struct RedactedText { @@ -1500,7 +1500,7 @@ extension UsageMenuCardView.Model { let remaining = UsageFormatter.usdString(keyRemaining) let limit = UsageFormatter.usdString(keyLimit) - return "\(remaining)/\(limit) left" + return String(format: L("%1$@/%2$@ left"), remaining, limit) } private static func syntheticRegenDetail( @@ -1516,21 +1516,21 @@ extension UsageMenuCardView.Model { let resetsAt = weekly.resetsAt else { return nil } - let countdown = UsageFormatter.resetCountdownDescription(from: resetsAt, now: now) - let resetText = "Regenerates \(countdown)" + let countdown = LocalizedUsageText.resetCountdownDescription(from: resetsAt, now: now) + let resetText = String(format: L("Regenerates %@"), countdown) let nextRegenPercent = (nextRegenAmount / cost.limit) * 100 let afterNextRegenRemaining = min(100, weekly.remainingPercent + nextRegenPercent) let afterNextRegen = showUsed ? max(0, 100 - afterNextRegenRemaining) : afterNextRegenRemaining - let suffix = showUsed ? "used after next regen" : "after next regen" + let suffix = showUsed ? L("used after next regen") : L("after next regen") let ticksToFull = max(0, cost.used) / nextRegenAmount let left = String(format: "%.0f%% %@", afterNextRegen, suffix) let right = if ticksToFull <= 0.1 { - "Near full" + L("Near full") } else if ticksToFull < 1.5 { - "Full in ~1 regen" + L("Full in ~1 regen") } else { - String(format: "Full in ~%.0f regens", ceil(ticksToFull)) + String(format: L("Full in ~%.0f regens"), ceil(ticksToFull)) } return (resetText, PaceDetail(leftLabel: left, rightLabel: right, pacePercent: nil, paceOnTop: true)) } @@ -1545,22 +1545,22 @@ extension UsageMenuCardView.Model { nextRegenPercent > 0 else { return nil } - let countdown = UsageFormatter.resetCountdownDescription(from: resetsAt, now: now) - let resetText = "Regenerates \(countdown)" + let countdown = LocalizedUsageText.resetCountdownDescription(from: resetsAt, now: now) + let resetText = String(format: L("Regenerates %@"), countdown) let afterNextRegenRemaining = min(100, window.remainingPercent + nextRegenPercent) let afterNextRegen = showUsed ? max(0, 100 - afterNextRegenRemaining) : afterNextRegenRemaining - let suffix = showUsed ? "used after next regen" : "after next regen" + let suffix = showUsed ? L("used after next regen") : L("after next regen") let left = String(format: "%.0f%% %@", afterNextRegen, suffix) let missingPercent = max(0, window.usedPercent) let ticksToFull = missingPercent / nextRegenPercent let right = if ticksToFull <= 0.1 { - "Near full" + L("Near full") } else if ticksToFull < 1.5 { - "Full in ~1 regen" + L("Full in ~1 regen") } else { - String(format: "Full in ~%.0f regens", ceil(ticksToFull)) + String(format: L("Full in ~%.0f regens"), ceil(ticksToFull)) } return (resetText, PaceDetail(leftLabel: left, rightLabel: right, pacePercent: nil, paceOnTop: true)) diff --git a/Sources/CodexBar/MenuDescriptor.swift b/Sources/CodexBar/MenuDescriptor.swift index 9b7811006..79cef14f0 100644 --- a/Sources/CodexBar/MenuDescriptor.swift +++ b/Sources/CodexBar/MenuDescriptor.swift @@ -113,7 +113,7 @@ struct MenuDescriptor { sections.append(accountSection) } } else { - sections.append(Section(entries: [.text("No usage configured.", .secondary)])) + sections.append(Section(entries: [.text(L("No usage configured."), .secondary)])) } } @@ -242,11 +242,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(String(format: L("Quota: %@ / %@"), used, limit), .primary)) } } } else { - entries.append(.text("No usage yet", .secondary)) + entries.append(.text(L("No usage yet"), .secondary)) } let usageContext = ProviderMenuUsageContext( @@ -294,7 +294,7 @@ struct MenuDescriptor { let redactedEmail = PersonalInfoRedactor.redactEmail(emailText, isEnabled: hidePersonalInfo) if let emailText, !emailText.isEmpty { - entries.append(.text("Account: \(redactedEmail)", .secondary)) + entries.append(.text(String(format: L("Account: %@"), redactedEmail), .secondary)) } if provider == .kiro { if let plan = snapshot?.kiroUsage?.displayPlanName, @@ -313,10 +313,12 @@ struct MenuDescriptor { } else if provider == .kilo { let kiloLogin = self.kiloLoginParts(loginMethod: loginMethodText) if let pass = kiloLogin.pass { - entries.append(.text("Plan: \(AccountFormatter.plan(pass, provider: provider))", .secondary)) + entries.append(.text( + String(format: L("Plan: %@"), AccountFormatter.plan(pass, provider: provider)), + .secondary)) } for detail in kiloLogin.details { - entries.append(.text("Activity: \(detail)", .secondary)) + entries.append(.text(String(format: L("Activity: %@"), detail), .secondary)) } } else if let loginMethodText, !loginMethodText.isEmpty { if provider == .openrouter || provider == .mimo, @@ -329,19 +331,25 @@ struct MenuDescriptor { options: [.regularExpression]) .trimmingCharacters(in: .whitespacesAndNewlines) let value = balanceValue.isEmpty ? loginMethodText : balanceValue - entries.append(.text("Balance: \(AccountFormatter.plan(value, provider: provider))", .secondary)) + entries.append(.text( + String(format: L("Balance: %@"), AccountFormatter.plan(value, provider: provider)), + .secondary)) } else { - entries.append(.text("Plan: \(AccountFormatter.plan(loginMethodText, provider: provider))", .secondary)) + entries.append(.text( + String(format: L("Plan: %@"), AccountFormatter.plan(loginMethodText, provider: provider)), + .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(String(format: L("Account: %@"), redacted), .secondary)) } if loginMethodText?.isEmpty ?? true, let fallbackPlan = fallback.plan, !fallbackPlan.isEmpty { - entries.append(.text("Plan: \(AccountFormatter.plan(fallbackPlan, provider: provider))", .secondary)) + entries.append(.text( + String(format: L("Plan: %@"), AccountFormatter.plan(fallbackPlan, provider: provider)), + .secondary)) } } @@ -469,10 +477,9 @@ struct MenuDescriptor { let status = store.status(for: target), status.indicator != .none else { return nil } - let description = status.description?.trimmingCharacters(in: .whitespacesAndNewlines) - let label = description?.isEmpty == false ? description! : status.indicator.label + let label = LocalizedProviderText.statusText(status) if let updated = status.updatedAt { - let freshness = UsageFormatter.updatedString(from: updated) + let freshness = LocalizedUsageText.updatedString(from: updated) return "\(label) — \(freshness)" } return label @@ -524,12 +531,12 @@ struct MenuDescriptor { showUsed: Bool, resetOverride: String? = nil) { - let line = UsageFormatter + let line = LocalizedUsageText .usageLine(remaining: window.remainingPercent, used: window.usedPercent, showUsed: showUsed) - entries.append(.text("\(title): \(line)", .primary)) + entries.append(.text("\(L(title)): \(line)", .primary)) if let resetOverride { entries.append(.text(resetOverride, .secondary)) - } else if let reset = UsageFormatter.resetLine(for: window, style: resetStyle) { + } else if let reset = LocalizedUsageText.resetLine(for: window, style: resetStyle) { entries.append(.text(reset, .secondary)) } } diff --git a/Sources/CodexBar/OpenAICreditsPurchaseWindowController.swift b/Sources/CodexBar/OpenAICreditsPurchaseWindowController.swift index 99ec8eef6..1e95ffea0 100644 --- a/Sources/CodexBar/OpenAICreditsPurchaseWindowController.swift +++ b/Sources/CodexBar/OpenAICreditsPurchaseWindowController.swift @@ -420,7 +420,7 @@ final class OpenAICreditsPurchaseWindowController: NSWindowController, WKNavigat styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: false) - window.title = "Buy Credits" + window.title = L("Buy Credits") window.isReleasedWhenClosed = false window.collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary] window.contentView = container diff --git a/Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift b/Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift index 072c10cc1..1374afe9e 100644 --- a/Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift +++ b/Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift @@ -115,7 +115,7 @@ struct PlanUtilizationHistoryChartMenuView: View { if model.points.isEmpty { ZStack { - Text(Self.emptyStateText(title: effectiveSelectedSeries?.title)) + Text(Self.localizedEmptyStateText(title: effectiveSelectedSeries?.title)) .font(.footnote) .foregroundStyle(.secondary) } @@ -146,8 +146,11 @@ struct PlanUtilizationHistoryChartMenuView: View { } .chartLegend(.hidden) .frame(height: Layout.chartHeight) - .accessibilityLabel("Plan utilization chart") - .accessibilityValue(model.points.isEmpty ? "No data" : "\(model.points.count) utilization samples") + .accessibilityLabel(L("Plan utilization chart")) + .accessibilityValue( + model.points.isEmpty + ? L("No data") + : String(format: L("%@ utilization samples"), "\(model.points.count)")) .chartOverlay { proxy in GeometryReader { geo in MouseLocationReader { location in @@ -158,7 +161,9 @@ struct PlanUtilizationHistoryChartMenuView: View { } } - Text(self.detailLine(model: model, windowMinutes: effectiveSelectedSeries?.history.windowMinutes ?? 0)) + Text(self.localizedDetailLine( + model: model, + windowMinutes: effectiveSelectedSeries?.history.windowMinutes ?? 0)) .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) @@ -621,6 +626,13 @@ struct PlanUtilizationHistoryChartMenuView: View { return "No utilization data yet." } + private static func localizedEmptyStateText(title: String?) -> String { + if let title { + return String(format: L("No %@ utilization data yet."), L(title).lowercased()) + } + return L("No utilization data yet.") + } + #if DEBUG struct ModelSnapshot: Equatable { let pointCount: Int @@ -733,6 +745,11 @@ struct PlanUtilizationHistoryChartMenuView: View { return Self.detailLine(point: activePoint, windowMinutes: windowMinutes) } + private func localizedDetailLine(model: Model, windowMinutes: Int) -> String { + let activePoint = self.selectedPoint(model: model) ?? model.points.last + return Self.localizedDetailLine(point: activePoint, windowMinutes: windowMinutes) + } + private func updateSelection( location: CGPoint?, model: Model, @@ -788,6 +805,20 @@ extension PlanUtilizationHistoryChartMenuView { return "\(dateLabel): \(usedText)% used" } + private static func localizedDetailLine(point: Point?, windowMinutes: Int) -> String { + guard let point else { + return "-" + } + + let dateLabel = self.detailDateLabel(for: point.date, windowMinutes: windowMinutes) + let used = max(0, min(100, point.usedPercent)) + if !point.isObserved { + return String(format: L("%1$@: %2$@"), dateLabel, "-") + } + let usedText = used.formatted(.number.precision(.fractionLength(0...1))) + return String(format: L("%1$@: %2$@%% used"), dateLabel, usedText) + } + private nonisolated static func detailDateLabel(for date: Date, windowMinutes: Int) -> String { let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") diff --git a/Sources/CodexBar/PreferencesAboutPane.swift b/Sources/CodexBar/PreferencesAboutPane.swift index 31c3bbe43..0b751d2a9 100644 --- a/Sources/CodexBar/PreferencesAboutPane.swift +++ b/Sources/CodexBar/PreferencesAboutPane.swift @@ -89,14 +89,14 @@ struct AboutPane: View { Spacer() Picker("", selection: self.updateChannelBinding) { ForEach(UpdateChannel.allCases) { channel in - Text(channel.displayName).tag(channel) + Text(L(channel.displayName)).tag(channel) } } .pickerStyle(.menu) .labelsHidden() } .frame(maxWidth: 280) - Text(self.updateChannel.description) + Text(L(self.updateChannel.description)) .font(.footnote) .foregroundStyle(.secondary) .multilineTextAlignment(.center) diff --git a/Sources/CodexBar/PreferencesAdvancedPane.swift b/Sources/CodexBar/PreferencesAdvancedPane.swift index 8dc82da59..54f2d49ec 100644 --- a/Sources/CodexBar/PreferencesAdvancedPane.swift +++ b/Sources/CodexBar/PreferencesAdvancedPane.swift @@ -78,8 +78,9 @@ struct AdvancedPane: View { subtitle: L("hide_personal_info_subtitle"), binding: self.$settings.hidePersonalInfo) PreferenceToggleRow( - title: "Show provider storage usage", - subtitle: "Show local disk usage in menus. Scans known provider-owned paths in the background.", + title: L("Show provider storage usage"), + subtitle: L( + "Show local disk usage in menus. Scans known provider-owned paths in the background."), binding: self.$settings.providerStorageFootprintsEnabled) } diff --git a/Sources/CodexBar/PreferencesCodexAccountsSection.swift b/Sources/CodexBar/PreferencesCodexAccountsSection.swift index 4b09898c2..20b02fe2a 100644 --- a/Sources/CodexBar/PreferencesCodexAccountsSection.swift +++ b/Sources/CodexBar/PreferencesCodexAccountsSection.swift @@ -136,7 +136,7 @@ struct CodexAccountsSectionView: View { if let selection = self.activeSelectionBinding { VStack(alignment: .leading, spacing: 6) { HStack(alignment: .firstTextBaseline, spacing: 10) { - Text("Active") + Text(L("Active")) .font(.subheadline.weight(.semibold)) .frame(width: ProviderSettingsMetrics.pickerLabelWidth, alignment: .leading) @@ -152,7 +152,7 @@ struct CodexAccountsSectionView: View { Spacer(minLength: 0) } - Text("Choose which Codex account CodexBar should follow.") + Text(L("Choose which Codex account CodexBar should follow.")) .font(.footnote) .foregroundStyle(.secondary) @@ -166,7 +166,7 @@ struct CodexAccountsSectionView: View { } else if let account = self.state.singleVisibleAccount { VStack(alignment: .leading, spacing: 6) { HStack(alignment: .firstTextBaseline, spacing: 10) { - Text("Account") + Text(L("Account")) .font(.subheadline.weight(.semibold)) .frame(width: ProviderSettingsMetrics.pickerLabelWidth, alignment: .leading) @@ -181,7 +181,7 @@ struct CodexAccountsSectionView: View { } if self.state.visibleAccounts.isEmpty { - Text("No Codex accounts detected yet.") + Text(L("No Codex accounts detected yet.")) .font(.footnote) .foregroundStyle(.secondary) } else { @@ -200,13 +200,13 @@ struct CodexAccountsSectionView: View { } if let notice = self.state.notice { - Text(notice.text) + Text(L(notice.text)) .font(.footnote) .foregroundStyle(notice.tone == .warning ? .red : .secondary) .fixedSize(horizontal: false, vertical: true) } - Button(self.state.addAccountTitle) { + Button(L(self.state.addAccountTitle)) { self.addAccount() } .buttonStyle(.bordered) @@ -235,7 +235,7 @@ struct CodexAccountsSectionView: View { @ViewBuilder private func systemRow(selection: Binding?) -> some View { HStack(alignment: .firstTextBaseline, spacing: 10) { - Text("System") + Text(L("System")) .font(.subheadline.weight(.semibold)) .frame(width: ProviderSettingsMetrics.pickerLabelWidth, alignment: .leading) @@ -259,13 +259,13 @@ struct CodexAccountsSectionView: View { .disabled(self.state.canPromoteToSystem(account) == false) } } label: { - Text(self.state.systemDisplayName) + Text(self.localizedSystemDisplayName) .font(.subheadline) .foregroundStyle(.secondary) } .disabled(self.state.isSystemSelectionDisabled) } else { - Text(self.state.systemDisplayName) + Text(self.localizedSystemDisplayName) .font(.subheadline) .foregroundStyle(self.state.systemVisibleAccount == nil ? .secondary : .primary) } @@ -273,10 +273,16 @@ struct CodexAccountsSectionView: View { Spacer(minLength: 0) } - Text("The default Codex account on this Mac.") + Text(L("The default Codex account on this Mac.")) .font(.footnote) .foregroundStyle(.secondary) } + + private var localizedSystemDisplayName: String { + self.state.systemDisplayName == "No system account" + ? L("No system account") + : self.state.systemDisplayName + } } private struct CodexAccountsSectionRowView: View { @@ -294,7 +300,7 @@ private struct CodexAccountsSectionRowView: View { Text(self.account.displayName) .font(.subheadline.weight(.semibold)) if self.showsSystemBadge { - Text("(System)") + Text(L("(System)")) .font(.caption.weight(.semibold)) .foregroundStyle(.secondary) } @@ -303,7 +309,7 @@ private struct CodexAccountsSectionRowView: View { Spacer(minLength: 8) if self.account.canReauthenticate { - Button(self.reauthenticateTitle) { + Button(L(self.reauthenticateTitle)) { self.onReauthenticate() } .buttonStyle(.bordered) @@ -312,7 +318,7 @@ private struct CodexAccountsSectionRowView: View { } if self.account.canRemove { - Button("Remove") { + Button(L("Remove")) { self.onRemove() } .buttonStyle(.bordered) diff --git a/Sources/CodexBar/PreferencesDebugPane.swift b/Sources/CodexBar/PreferencesDebugPane.swift index 08e9d2de0..c98ae9c10 100644 --- a/Sources/CodexBar/PreferencesDebugPane.swift +++ b/Sources/CodexBar/PreferencesDebugPane.swift @@ -47,9 +47,9 @@ struct DebugPane: View { .foregroundStyle(.tertiary) } Spacer() - Picker("Verbosity", selection: self.$settings.debugLogLevel) { + Picker(L("verbosity_title"), selection: self.$settings.debugLogLevel) { ForEach(CodexBarLog.Level.allCases) { level in - Text(level.displayName).tag(level) + Text(L(level.displayName)).tag(level) } } .labelsHidden() @@ -76,10 +76,10 @@ struct DebugPane: View { title: L("section_loading_animations"), caption: L("loading_animations_caption")) { - Picker("Animation pattern", selection: self.animationPatternBinding) { + Picker(L("Animation pattern"), selection: self.animationPatternBinding) { Text(L("animation_random_default")).tag(nil as LoadingPattern?) ForEach(LoadingPattern.allCases) { pattern in - Text(pattern.displayName).tag(Optional(pattern)) + Text(L(pattern.displayName)).tag(Optional(pattern)) } } .pickerStyle(.radioGroup) @@ -101,7 +101,7 @@ struct DebugPane: View { title: L("section_probe_logs"), caption: L("probe_logs_caption")) { - Picker("Provider", selection: self.$currentLogProvider) { + Picker(L("Provider"), selection: self.$currentLogProvider) { Text("Codex").tag(UsageProvider.codex) Text("Claude").tag(UsageProvider.claude) Text("Cursor").tag(UsageProvider.cursor) @@ -169,7 +169,7 @@ struct DebugPane: View { title: L("section_fetch_strategy"), caption: L("fetch_strategy_caption")) { - Picker("Provider", selection: self.$currentFetchProvider) { + Picker(L("Provider"), selection: self.$currentFetchProvider) { ForEach(UsageProvider.allCases, id: \.self) { provider in Text(provider.rawValue.capitalized).tag(provider) } @@ -260,7 +260,7 @@ struct DebugPane: View { title: L("section_notifications"), caption: L("notifications_caption")) { - Picker("Provider", selection: self.$currentLogProvider) { + Picker(L("Provider"), selection: self.$currentLogProvider) { Text("Codex").tag(UsageProvider.codex) Text("Claude").tag(UsageProvider.claude) } @@ -308,7 +308,7 @@ struct DebugPane: View { title: L("section_error_simulation"), caption: L("error_simulation_caption")) { - Picker("Provider", selection: self.$currentErrorProvider) { + Picker(L("Provider"), selection: self.$currentErrorProvider) { Text("Codex").tag(UsageProvider.codex) Text("Claude").tag(UsageProvider.claude) Text("Gemini").tag(UsageProvider.gemini) @@ -320,7 +320,7 @@ struct DebugPane: View { .pickerStyle(.segmented) .frame(width: 360) - TextField("Simulated error text", text: self.$simulatedErrorText, axis: .vertical) + TextField(L("Simulated error text"), text: self.$simulatedErrorText, axis: .vertical) .lineLimit(4) HStack(spacing: 12) { diff --git a/Sources/CodexBar/PreferencesDisplayPane.swift b/Sources/CodexBar/PreferencesDisplayPane.swift index 4628a649a..037ea19e9 100644 --- a/Sources/CodexBar/PreferencesDisplayPane.swift +++ b/Sources/CodexBar/PreferencesDisplayPane.swift @@ -50,7 +50,7 @@ struct DisplayPane: View { .foregroundStyle(.tertiary) } Spacer() - Picker("Display mode", selection: self.$settings.menuBarDisplayMode) { + Picker(L("display_mode_title"), selection: self.$settings.menuBarDisplayMode) { ForEach(MenuBarDisplayMode.allCases) { mode in Text(mode.label).tag(mode) } diff --git a/Sources/CodexBar/PreferencesGeneralPane.swift b/Sources/CodexBar/PreferencesGeneralPane.swift index edf0a2a00..2a6af21bf 100644 --- a/Sources/CodexBar/PreferencesGeneralPane.swift +++ b/Sources/CodexBar/PreferencesGeneralPane.swift @@ -114,7 +114,7 @@ struct GeneralPane: View { .foregroundStyle(.tertiary) } Spacer() - Picker("Refresh cadence", selection: self.$settings.refreshFrequency) { + Picker(L("refresh_cadence_title"), selection: self.$settings.refreshFrequency) { ForEach(RefreshFrequency.allCases) { option in Text(option.label).tag(option) } @@ -186,7 +186,7 @@ struct GeneralPane: View { .foregroundStyle(.tertiary) } if let snapshot = self.store.tokenSnapshot(for: provider) { - let updated = UsageFormatter.updatedString(from: snapshot.updatedAt) + let updated = LocalizedUsageText.updatedString(from: snapshot.updatedAt) let cost = snapshot.last30DaysCostUSD.map { UsageFormatter.usdString($0) } ?? "—" return Text(String(format: L("cost_status_snapshot"), name, updated, cost)) .font(.footnote) diff --git a/Sources/CodexBar/PreferencesProviderDetailView.swift b/Sources/CodexBar/PreferencesProviderDetailView.swift index e0e184158..88b014e5d 100644 --- a/Sources/CodexBar/PreferencesProviderDetailView.swift +++ b/Sources/CodexBar/PreferencesProviderDetailView.swift @@ -237,7 +237,7 @@ private struct ProviderDetailHeaderView: View { } .buttonStyle(.bordered) .controlSize(.small) - .help("Refresh") + .help(L("Refresh")) Toggle("", isOn: self.$isEnabled) .labelsHidden() @@ -296,11 +296,11 @@ private struct ProviderDetailInfoGrid: View { var body: some View { let status = self.store.status(for: self.provider) - let source = self.store.sourceLabel(for: self.provider) - let version = self.store.version(for: self.provider) ?? "not detected" + let source = LocalizedProviderText.sourceLabel(self.store.sourceLabel(for: self.provider)) + let version = self.store.version(for: self.provider) ?? L("not detected") let updated = self.updatedText let email = self.model.email - let enabledText = self.isEnabled ? "Enabled" : "Disabled" + let enabledText = self.isEnabled ? L("Enabled") : L("Disabled") Grid(alignment: .leading, horizontalSpacing: 12, verticalSpacing: 6) { ProviderDetailInfoRow(label: "State", value: enabledText, labelWidth: self.labelWidth) @@ -311,7 +311,7 @@ private struct ProviderDetailInfoGrid: View { if let status { ProviderDetailInfoRow( label: "Status", - value: status.description ?? status.indicator.label, + value: LocalizedProviderText.statusText(status), labelWidth: self.labelWidth) } @@ -339,15 +339,15 @@ private struct ProviderDetailInfoGrid: View { private var updatedText: String { if let updated = self.store.snapshot(for: self.provider)?.updatedAt { - return UsageFormatter.updatedString(from: updated) + return LocalizedUsageText.updatedString(from: updated) } if self.store.refreshingProviders.contains(self.provider) { - return "Refreshing" + return L("Refreshing") } if self.store.unavailableMessage(for: self.provider) != nil { - return "Unavailable" + return L("Unavailable") } - return "Not fetched yet" + return L("Not fetched yet") } } @@ -358,7 +358,7 @@ private struct ProviderDetailInfoRow: View { var body: some View { GridRow { - Text(self.label) + Text(L(self.label)) .frame(width: self.labelWidth, alignment: .leading) Text(self.value) .lineLimit(2) @@ -435,9 +435,9 @@ struct ProviderMetricsInlineView: View { private var placeholderText: String { if !self.isEnabled { - return "Disabled — no recent data" + return L("Disabled — no recent data") } - return self.model.placeholder ?? "No usage yet" + return self.model.placeholder.map { L($0) } ?? L("No usage yet") } } @@ -449,7 +449,7 @@ private struct ProviderMetricInlineRow: View { var body: some View { HStack(alignment: .top, spacing: 10) { - Text(self.title) + Text(L(self.title)) .font(.subheadline.weight(.semibold)) .lineLimit(1) .frame(width: self.labelWidth, alignment: .leading) @@ -458,7 +458,7 @@ private struct ProviderMetricInlineRow: View { UsageProgressBar( percent: self.metric.percent, tint: self.progressColor, - accessibilityLabel: self.metric.percentStyle.accessibilityLabel, + accessibilityLabel: L(self.metric.percentStyle.accessibilityLabel), pacePercent: self.metric.pacePercent, paceOnTop: self.metric.paceOnTop, warningMarkerPercents: self.metric.warningMarkerPercents) @@ -525,7 +525,7 @@ private struct ProviderUsageNotesInlineView: View { } VStack(alignment: .leading, spacing: 4) { ForEach(Array(self.notes.enumerated()), id: \.offset) { _, note in - Text(note) + Text(L(note)) .font(.footnote) .foregroundStyle(.secondary) .lineLimit(2) @@ -545,7 +545,7 @@ private struct ProviderMetricInlineTextRow: View { var body: some View { HStack(alignment: .firstTextBaseline, spacing: 12) { - Text(self.title) + Text(L(self.title)) .font(.subheadline.weight(.semibold)) .frame(width: self.labelWidth, alignment: .leading) @@ -566,7 +566,7 @@ private struct ProviderMetricInlineCostRow: View { var body: some View { HStack(alignment: .top, spacing: 10) { - Text(self.section.title) + Text(L(self.section.title)) .font(.subheadline.weight(.semibold)) .frame(width: self.labelWidth, alignment: .leading) @@ -575,7 +575,7 @@ private struct ProviderMetricInlineCostRow: View { UsageProgressBar( percent: percentUsed, tint: self.progressColor, - accessibilityLabel: "Usage used") + accessibilityLabel: L("Usage used")) .frame(minWidth: ProviderSettingsMetrics.metricBarWidth, maxWidth: .infinity) } diff --git a/Sources/CodexBar/PreferencesProviderErrorView.swift b/Sources/CodexBar/PreferencesProviderErrorView.swift index 4b5c96b78..156dc6271 100644 --- a/Sources/CodexBar/PreferencesProviderErrorView.swift +++ b/Sources/CodexBar/PreferencesProviderErrorView.swift @@ -26,7 +26,7 @@ struct ProviderErrorView: View { } .buttonStyle(.plain) .foregroundStyle(.secondary) - .help("Copy error") + .help(L("Copy error")) } Text(self.display.preview) diff --git a/Sources/CodexBar/PreferencesProviderSettingsRows.swift b/Sources/CodexBar/PreferencesProviderSettingsRows.swift index c4f4cb817..bdc31257b 100644 --- a/Sources/CodexBar/PreferencesProviderSettingsRows.swift +++ b/Sources/CodexBar/PreferencesProviderSettingsRows.swift @@ -23,7 +23,7 @@ struct ProviderSettingsSection: View { var body: some View { VStack(alignment: .leading, spacing: self.spacing) { - Text(self.title) + Text(L(self.title)) .font(.headline) self.content() } @@ -41,9 +41,9 @@ struct ProviderSettingsToggleRowView: View { VStack(alignment: .leading, spacing: 8) { HStack(alignment: .firstTextBaseline, spacing: 12) { VStack(alignment: .leading, spacing: 4) { - Text(self.toggle.title) + Text(L(self.toggle.title)) .font(.subheadline.weight(.semibold)) - Text(self.toggle.subtitle) + Text(L(self.toggle.subtitle)) .font(.footnote) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) @@ -67,7 +67,7 @@ struct ProviderSettingsToggleRowView: View { if !actions.isEmpty { HStack(spacing: 10) { ForEach(actions) { action in - Button(action.title) { + Button(L(action.title)) { Task { @MainActor in await action.perform() } @@ -101,13 +101,13 @@ struct ProviderSettingsPickerRowView: View { let isEnabled = self.picker.isEnabled?() ?? true VStack(alignment: .leading, spacing: 6) { HStack(alignment: .firstTextBaseline, spacing: 10) { - Text(self.picker.title) + Text(L(self.picker.title)) .font(.subheadline.weight(.semibold)) .frame(width: ProviderSettingsMetrics.pickerLabelWidth, alignment: .leading) Picker("", selection: self.picker.binding) { ForEach(self.picker.options) { option in - Text(option.title).tag(option.id) + Text(L(option.title)).tag(option.id) } } .labelsHidden() @@ -128,7 +128,7 @@ struct ProviderSettingsPickerRowView: View { let subtitle = self.picker.dynamicSubtitle?() ?? self.picker.subtitle if !subtitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - Text(subtitle) + Text(L(subtitle)) .font(.footnote) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) @@ -157,11 +157,11 @@ struct ProviderSettingsFieldRowView: View { if hasHeader { VStack(alignment: .leading, spacing: 4) { if !trimmedTitle.isEmpty { - Text(trimmedTitle) + Text(L(trimmedTitle)) .font(.subheadline.weight(.semibold)) } if !trimmedSubtitle.isEmpty { - Text(trimmedSubtitle) + Text(L(trimmedSubtitle)) .font(.footnote) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) @@ -171,12 +171,12 @@ struct ProviderSettingsFieldRowView: View { switch self.field.kind { case .plain: - TextField(self.field.placeholder ?? "", text: self.field.binding) + TextField(self.field.placeholder.map { L($0) } ?? "", text: self.field.binding) .textFieldStyle(.roundedBorder) .font(.footnote) .onTapGesture { self.field.onActivate?() } case .secure: - SecureField(self.field.placeholder ?? "", text: self.field.binding) + SecureField(self.field.placeholder.map { L($0) } ?? "", text: self.field.binding) .textFieldStyle(.roundedBorder) .font(.footnote) .onTapGesture { self.field.onActivate?() } @@ -186,7 +186,7 @@ struct ProviderSettingsFieldRowView: View { if !actions.isEmpty { HStack(spacing: 10) { ForEach(actions) { action in - Button(action.title) { + Button(L(action.title)) { Task { @MainActor in await action.perform() } @@ -198,7 +198,7 @@ struct ProviderSettingsFieldRowView: View { } if let footer = self.field.footerText, !footer.isEmpty { - Text(footer) + Text(L(footer)) .font(.footnote) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) @@ -213,11 +213,11 @@ struct ProviderSettingsActionsRowView: View { var body: some View { VStack(alignment: .leading, spacing: 8) { - Text(self.descriptor.title) + Text(L(self.descriptor.title)) .font(.subheadline.weight(.semibold)) if !self.descriptor.subtitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - Text(self.descriptor.subtitle) + Text(L(self.descriptor.subtitle)) .font(.footnote) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) @@ -227,7 +227,7 @@ struct ProviderSettingsActionsRowView: View { if !actions.isEmpty { HStack(spacing: 10) { ForEach(actions) { action in - Button(action.title) { + Button(L(action.title)) { Task { @MainActor in await action.perform() } @@ -251,13 +251,13 @@ struct ProviderSettingsTokenAccountsRowView: View { var body: some View { VStack(alignment: .leading, spacing: 10) { HStack(alignment: .center, spacing: 12) { - Text(self.descriptor.title) + Text(L(self.descriptor.title)) .font(.subheadline.weight(.semibold)) Spacer(minLength: 8) if let title = self.descriptor.primaryAddActionTitle, let action = self.descriptor.primaryAddAction { - Button(title) { + Button(L(title)) { Task { @MainActor in await action() } @@ -268,7 +268,7 @@ struct ProviderSettingsTokenAccountsRowView: View { } if !self.descriptor.subtitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - Text(self.descriptor.subtitle) + Text(L(self.descriptor.subtitle)) .font(.footnote) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) @@ -276,7 +276,7 @@ struct ProviderSettingsTokenAccountsRowView: View { let accounts = self.descriptor.accounts() if accounts.isEmpty { - Text("No token accounts yet.") + Text(L("No token accounts yet.")) .font(.footnote) .foregroundStyle(.secondary) } else { @@ -304,7 +304,7 @@ struct ProviderSettingsTokenAccountsRowView: View { } .buttonStyle(.plain) - Button("Remove") { + Button(L("Remove")) { self.descriptor.removeAccount(account.id) } .buttonStyle(.bordered) @@ -320,13 +320,13 @@ struct ProviderSettingsTokenAccountsRowView: View { if self.descriptor.primaryAddAction == nil { VStack(alignment: .leading, spacing: 6) { HStack(spacing: 8) { - TextField("Label", text: self.$newLabel) + TextField(L("Label"), text: self.$newLabel) .textFieldStyle(.roundedBorder) .font(.footnote) - SecureField(self.descriptor.placeholder, text: self.$newToken) + SecureField(L(self.descriptor.placeholder), text: self.$newToken) .textFieldStyle(.roundedBorder) .font(.footnote) - Button("Add") { + Button(L("Add")) { let label = self.newLabel.trimmingCharacters(in: .whitespacesAndNewlines) let token = self.newToken.trimmingCharacters(in: .whitespacesAndNewlines) guard !label.isEmpty, !token.isEmpty else { return } @@ -344,21 +344,22 @@ struct ProviderSettingsTokenAccountsRowView: View { self.newToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) } if self.descriptor.showsOrganizationField { - TextField("Org ID (optional)", text: self.$newOrgID) + TextField(L("Org ID (optional)"), text: self.$newOrgID) .textFieldStyle(.roundedBorder) .font(.footnote) - .help("Optional organization ID for accounts linked to multiple Anthropic organizations.") + .help( + L("Optional organization ID for accounts linked to multiple Anthropic organizations.")) } } } HStack(spacing: 10) { - Button("Open token file") { + Button(L("Open token file")) { self.descriptor.openConfigFile() } .buttonStyle(.link) .controlSize(.small) - Button("Reload") { + Button(L("Reload")) { self.descriptor.reloadFromDisk() } .buttonStyle(.link) diff --git a/Sources/CodexBar/PreferencesProviderSidebarView.swift b/Sources/CodexBar/PreferencesProviderSidebarView.swift index 559c5329e..1df0d147c 100644 --- a/Sources/CodexBar/PreferencesProviderSidebarView.swift +++ b/Sources/CodexBar/PreferencesProviderSidebarView.swift @@ -72,7 +72,7 @@ private struct ProviderSidebarRowView: View { .contentShape(Rectangle()) .padding(.vertical, 4) .padding(.horizontal, 2) - .help("Drag to reorder") + .help(L("Drag to reorder")) .onDrag { self.draggingProvider = self.provider return NSItemProvider(object: self.provider.rawValue as NSString) @@ -115,13 +115,14 @@ private struct ProviderSidebarRowView: View { private var statusText: String { guard !self.isEnabled else { return self.subtitle } + let disabled = L("Disabled") let lines = self.subtitle.split(separator: "\n", omittingEmptySubsequences: false) if lines.count >= 2 { let first = lines[0] let rest = lines.dropFirst().joined(separator: "\n") - return "Disabled — \(first)\n\(rest)" + return "\(disabled) — \(first)\n\(rest)" } - return "Disabled — \(self.subtitle)" + return "\(disabled) — \(self.subtitle)" } } @@ -145,7 +146,7 @@ private struct ProviderSidebarReorderHandle: View { width: ProviderSettingsMetrics.reorderHandleSize, height: ProviderSettingsMetrics.reorderHandleSize) .foregroundStyle(.tertiary) - .accessibilityLabel("Reorder") + .accessibilityLabel(L("Reorder")) } } diff --git a/Sources/CodexBar/PreferencesProvidersPane.swift b/Sources/CodexBar/PreferencesProvidersPane.swift index 38d167188..be6de3355 100644 --- a/Sources/CodexBar/PreferencesProvidersPane.swift +++ b/Sources/CodexBar/PreferencesProvidersPane.swift @@ -488,7 +488,7 @@ struct ProvidersPane: View { ] } else if SettingsStore.isBalanceOnlyProvider(provider) { options = [ - ProviderSettingsPickerOption(id: MenuBarMetricPreference.automatic.rawValue, title: "Automatic"), + ProviderSettingsPickerOption(id: MenuBarMetricPreference.automatic.rawValue, title: L("automatic")), ] } else if provider == .abacus { let metadata = self.store.metadata(for: provider) diff --git a/Sources/CodexBar/Providers/Abacus/AbacusProviderImplementation.swift b/Sources/CodexBar/Providers/Abacus/AbacusProviderImplementation.swift index 1e0cdeb9f..d1d1f01fc 100644 --- a/Sources/CodexBar/Providers/Abacus/AbacusProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Abacus/AbacusProviderImplementation.swift @@ -63,11 +63,7 @@ struct AbacusProviderImplementation: ProviderImplementation { options: cookieOptions, isVisible: nil, onChange: nil, - trailingText: { - guard let entry = CookieHeaderCache.load(provider: .abacus) else { return nil } - let when = entry.storedAt.relativeDescription() - return "Cached: \(entry.sourceLabel) • \(when)" - }), + trailingText: { ProviderCookieSourceUI.cachedTrailingText(provider: .abacus) }), ] } diff --git a/Sources/CodexBar/Providers/Alibaba/AlibabaCodingPlanProviderImplementation.swift b/Sources/CodexBar/Providers/Alibaba/AlibabaCodingPlanProviderImplementation.swift index eb198478e..a3aa243ae 100644 --- a/Sources/CodexBar/Providers/Alibaba/AlibabaCodingPlanProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Alibaba/AlibabaCodingPlanProviderImplementation.swift @@ -11,7 +11,7 @@ struct AlibabaCodingPlanProviderImplementation: ProviderImplementation { @MainActor func presentation(context _: ProviderPresentationContext) -> ProviderPresentation { ProviderPresentation { context in - context.store.sourceLabel(for: context.provider) + LocalizedProviderText.sourceLabel(context.store.sourceLabel(for: context.provider)) } } @@ -68,11 +68,7 @@ struct AlibabaCodingPlanProviderImplementation: ProviderImplementation { options: cookieOptions, isVisible: nil, onChange: nil, - trailingText: { - guard let entry = CookieHeaderCache.load(provider: .alibaba) else { return nil } - let when = entry.storedAt.relativeDescription() - return "Cached: \(entry.sourceLabel) • \(when)" - }), + trailingText: { ProviderCookieSourceUI.cachedTrailingText(provider: .alibaba) }), ProviderSettingsPickerDescriptor( id: "alibaba-coding-plan-region", title: "Gateway region", diff --git a/Sources/CodexBar/Providers/Antigravity/AntigravityProviderImplementation.swift b/Sources/CodexBar/Providers/Antigravity/AntigravityProviderImplementation.swift index bb3b01070..201f41d9d 100644 --- a/Sources/CodexBar/Providers/Antigravity/AntigravityProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Antigravity/AntigravityProviderImplementation.swift @@ -50,7 +50,7 @@ struct AntigravityProviderImplementation: ProviderImplementation { trailingText: { guard context.settings.antigravityUsageDataSource == .auto else { return nil } let label = context.store.sourceLabel(for: .antigravity) - return label == "auto" ? nil : label + return label == "auto" ? nil : LocalizedProviderText.sourceLabel(label) }), ] } diff --git a/Sources/CodexBar/Providers/Augment/AugmentProviderImplementation.swift b/Sources/CodexBar/Providers/Augment/AugmentProviderImplementation.swift index c1529bd58..ac1c4a32d 100644 --- a/Sources/CodexBar/Providers/Augment/AugmentProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Augment/AugmentProviderImplementation.swift @@ -67,11 +67,7 @@ struct AugmentProviderImplementation: ProviderImplementation { options: cookieOptions, isVisible: nil, onChange: nil, - trailingText: { - guard let entry = CookieHeaderCache.load(provider: .augment) else { return nil } - let when = entry.storedAt.relativeDescription() - return "Cached: \(entry.sourceLabel) • \(when)" - }), + trailingText: { ProviderCookieSourceUI.cachedTrailingText(provider: .augment) }), ] } diff --git a/Sources/CodexBar/Providers/Claude/ClaudeProviderImplementation.swift b/Sources/CodexBar/Providers/Claude/ClaudeProviderImplementation.swift index 3bd49fbe1..5098e885f 100644 --- a/Sources/CodexBar/Providers/Claude/ClaudeProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Claude/ClaudeProviderImplementation.swift @@ -10,7 +10,7 @@ struct ClaudeProviderImplementation: ProviderImplementation { @MainActor func presentation(context _: ProviderPresentationContext) -> ProviderPresentation { ProviderPresentation { context in - var versionText = context.store.version(for: context.provider) ?? "not detected" + var versionText = context.store.version(for: context.provider) ?? L("not detected") if let parenRange = versionText.range(of: "(") { versionText = versionText[.. ProviderPresentation { ProviderPresentation { context in - context.store.version(for: context.provider) ?? "not detected" + context.store.version(for: context.provider) ?? L("not detected") } } @@ -157,7 +157,7 @@ struct CodexProviderImplementation: ProviderImplementation { trailingText: { guard context.settings.codexUsageDataSource == .auto else { return nil } let label = context.store.sourceLabel(for: .codex) - return label == "auto" ? nil : label + return label == "auto" ? nil : LocalizedProviderText.sourceLabel(label) }), ProviderSettingsPickerDescriptor( id: "codex-cookie-source", @@ -168,11 +168,7 @@ struct CodexProviderImplementation: ProviderImplementation { options: cookieOptions, isVisible: { context.settings.openAIWebAccessEnabled }, onChange: nil, - trailingText: { - guard let entry = CookieHeaderCache.load(provider: .codex) else { return nil } - let when = entry.storedAt.relativeDescription() - return "Cached: \(entry.sourceLabel) • \(when)" - }), + trailingText: { ProviderCookieSourceUI.cachedTrailingText(provider: .codex) }), ] } @@ -202,11 +198,11 @@ struct CodexProviderImplementation: ProviderImplementation { if let credits = context.store.credits { entries.append(.text( - String(format: L("credits_remaining"), UsageFormatter.creditsString(from: credits.remaining)), + String(format: L("credits_remaining"), LocalizedUsageText.creditsString(from: credits.remaining)), .primary)) if let latest = credits.events.first { entries.append(.text( - String(format: L("last_spend"), UsageFormatter.creditEventSummary(latest)), + String(format: L("last_spend"), LocalizedUsageText.creditEventSummary(latest)), .secondary)) } } else { diff --git a/Sources/CodexBar/Providers/Copilot/CopilotLoginFlow.swift b/Sources/CodexBar/Providers/Copilot/CopilotLoginFlow.swift index 14374c5f2..339978287 100644 --- a/Sources/CodexBar/Providers/Copilot/CopilotLoginFlow.swift +++ b/Sources/CodexBar/Providers/Copilot/CopilotLoginFlow.swift @@ -99,9 +99,11 @@ struct CopilotLoginFlow { } catch { guard existingAccounts.isEmpty else { let err = NSAlert() - err.messageText = "Could Not Identify GitHub Account" - err.informativeText = "GitHub login succeeded, but CodexBar could not verify which " + - "account it belongs to. Please try again." + err.messageText = L("Could Not Identify GitHub Account") + // swiftlint:disable line_length + err.informativeText = L( + "GitHub login succeeded, but CodexBar could not verify which account it belongs to. Please try again.") + // swiftlint:enable line_length err.runModal() return } @@ -138,20 +140,20 @@ struct CopilotLoginFlow { enabled: true) let success = NSAlert() - success.messageText = wasRefresh ? "Token Refreshed" : "Account Added" + success.messageText = wasRefresh ? L("Token Refreshed") : L("Account Added") success.informativeText = label success.runModal() case let .failure(error): guard !(error is CancellationError) else { return } let err = NSAlert() - err.messageText = "Login Failed" + err.messageText = L("Login Failed") err.informativeText = error.localizedDescription err.runModal() } } catch { let err = NSAlert() - err.messageText = "Login Failed" + err.messageText = L("Login Failed") err.informativeText = error.localizedDescription err.runModal() } diff --git a/Sources/CodexBar/Providers/Cursor/CursorProviderImplementation.swift b/Sources/CodexBar/Providers/Cursor/CursorProviderImplementation.swift index a8638af89..4d7b6b48b 100644 --- a/Sources/CodexBar/Providers/Cursor/CursorProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Cursor/CursorProviderImplementation.swift @@ -68,11 +68,7 @@ struct CursorProviderImplementation: ProviderImplementation { options: cookieOptions, isVisible: nil, onChange: nil, - trailingText: { - guard let entry = CookieHeaderCache.load(provider: .cursor) else { return nil } - let when = entry.storedAt.relativeDescription() - return "Cached: \(entry.sourceLabel) • \(when)" - }), + trailingText: { ProviderCookieSourceUI.cachedTrailingText(provider: .cursor) }), ] } diff --git a/Sources/CodexBar/Providers/Factory/FactoryProviderImplementation.swift b/Sources/CodexBar/Providers/Factory/FactoryProviderImplementation.swift index c9a759cba..ff20cbd12 100644 --- a/Sources/CodexBar/Providers/Factory/FactoryProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Factory/FactoryProviderImplementation.swift @@ -63,11 +63,7 @@ struct FactoryProviderImplementation: ProviderImplementation { options: cookieOptions, isVisible: nil, onChange: nil, - trailingText: { - guard let entry = CookieHeaderCache.load(provider: .factory) else { return nil } - let when = entry.storedAt.relativeDescription() - return "Cached: \(entry.sourceLabel) • \(when)" - }), + trailingText: { ProviderCookieSourceUI.cachedTrailingText(provider: .factory) }), ] } @@ -98,6 +94,6 @@ struct FactoryProviderImplementation: ProviderImplementation { else { return } let balance = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode) - entries.append(.text("Extra usage balance: \(balance)", .primary)) + entries.append(.text(String(format: L("Extra usage balance: %@"), balance), .primary)) } } diff --git a/Sources/CodexBar/Providers/Kilo/KiloProviderImplementation.swift b/Sources/CodexBar/Providers/Kilo/KiloProviderImplementation.swift index 8038ddb81..f08ae7257 100644 --- a/Sources/CodexBar/Providers/Kilo/KiloProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Kilo/KiloProviderImplementation.swift @@ -63,7 +63,7 @@ struct KiloProviderImplementation: ProviderImplementation { trailingText: { guard context.settings.kiloUsageDataSource == .auto else { return nil } let label = context.store.sourceLabel(for: .kilo) - return label == "auto" ? nil : label + return label == "auto" ? nil : LocalizedProviderText.sourceLabel(label) }), ] } diff --git a/Sources/CodexBar/Providers/MiMo/MiMoProviderImplementation.swift b/Sources/CodexBar/Providers/MiMo/MiMoProviderImplementation.swift index bcb9b68cf..33036a7a1 100644 --- a/Sources/CodexBar/Providers/MiMo/MiMoProviderImplementation.swift +++ b/Sources/CodexBar/Providers/MiMo/MiMoProviderImplementation.swift @@ -54,11 +54,7 @@ struct MiMoProviderImplementation: ProviderImplementation { options: cookieOptions, isVisible: nil, onChange: nil, - trailingText: { - guard let entry = CookieHeaderCache.load(provider: .mimo) else { return nil } - let when = entry.storedAt.relativeDescription() - return "Cached: \(entry.sourceLabel) • \(when)" - }), + trailingText: { ProviderCookieSourceUI.cachedTrailingText(provider: .mimo) }), ] } diff --git a/Sources/CodexBar/Providers/MiniMax/MiniMaxProviderImplementation.swift b/Sources/CodexBar/Providers/MiniMax/MiniMaxProviderImplementation.swift index 661441e99..34ec77f31 100644 --- a/Sources/CodexBar/Providers/MiniMax/MiniMaxProviderImplementation.swift +++ b/Sources/CodexBar/Providers/MiniMax/MiniMaxProviderImplementation.swift @@ -11,7 +11,7 @@ struct MiniMaxProviderImplementation: ProviderImplementation { @MainActor func presentation(context _: ProviderPresentationContext) -> ProviderPresentation { ProviderPresentation { context in - context.store.sourceLabel(for: context.provider) + LocalizedProviderText.sourceLabel(context.store.sourceLabel(for: context.provider)) } } @@ -88,11 +88,7 @@ struct MiniMaxProviderImplementation: ProviderImplementation { options: cookieOptions, isVisible: { authMode().allowsCookies }, onChange: nil, - trailingText: { - guard let entry = CookieHeaderCache.load(provider: .minimax) else { return nil } - let when = entry.storedAt.relativeDescription() - return "Cached: \(entry.sourceLabel) • \(when)" - }), + trailingText: { ProviderCookieSourceUI.cachedTrailingText(provider: .minimax) }), ProviderSettingsPickerDescriptor( id: "minimax-region", title: "API region", diff --git a/Sources/CodexBar/Providers/Mistral/MistralProviderImplementation.swift b/Sources/CodexBar/Providers/Mistral/MistralProviderImplementation.swift index 1949d2ed0..64af97d1f 100644 --- a/Sources/CodexBar/Providers/Mistral/MistralProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Mistral/MistralProviderImplementation.swift @@ -68,11 +68,7 @@ struct MistralProviderImplementation: ProviderImplementation { options: cookieOptions, isVisible: nil, onChange: nil, - trailingText: { - guard let entry = CookieHeaderCache.load(provider: .mistral) else { return nil } - let when = entry.storedAt.relativeDescription() - return "Cached: \(entry.sourceLabel) • \(when)" - }), + trailingText: { ProviderCookieSourceUI.cachedTrailingText(provider: .mistral) }), ] } diff --git a/Sources/CodexBar/Providers/OpenCode/OpenCodeProviderUI.swift b/Sources/CodexBar/Providers/OpenCode/OpenCodeProviderUI.swift index e6646e6c2..103d31c18 100644 --- a/Sources/CodexBar/Providers/OpenCode/OpenCodeProviderUI.swift +++ b/Sources/CodexBar/Providers/OpenCode/OpenCodeProviderUI.swift @@ -5,8 +5,6 @@ enum OpenCodeProviderUI { @MainActor static func cachedCookieTrailingText(provider: UsageProvider, cookieSource: ProviderCookieSource) -> String? { guard cookieSource != .manual else { return nil } - guard let entry = CookieHeaderCache.load(provider: provider) else { return nil } - let when = entry.storedAt.relativeDescription() - return "Cached: \(entry.sourceLabel) • \(when)" + return ProviderCookieSourceUI.cachedTrailingText(provider: provider) } } diff --git a/Sources/CodexBar/Providers/Shared/ProviderCookieSourceUI.swift b/Sources/CodexBar/Providers/Shared/ProviderCookieSourceUI.swift index 4964f4df4..afba30a3d 100644 --- a/Sources/CodexBar/Providers/Shared/ProviderCookieSourceUI.swift +++ b/Sources/CodexBar/Providers/Shared/ProviderCookieSourceUI.swift @@ -30,7 +30,11 @@ enum ProviderCookieSourceUI { off: String) -> String { if keychainDisabled { - return source == .off ? off : "\(self.keychainDisabledPrefix) \(manual)" + return source == .off + ? off + : String( + format: L("Keychain access is disabled in Advanced, so browser cookie import is unavailable. %@"), + L(manual)) } switch source { case .auto: @@ -41,4 +45,12 @@ enum ProviderCookieSourceUI { return off } } + + @MainActor + static func cachedTrailingText(provider: UsageProvider) -> String? { + guard let entry = CookieHeaderCache.load(provider: provider) else { return nil } + let when = entry.storedAt.relativeDescription() + let source = LocalizedProviderText.sourceLabel(entry.sourceLabel) + return String(format: L("Cached: %@ • %@"), source, when) + } } diff --git a/Sources/CodexBar/Providers/Shared/ProviderPresentation.swift b/Sources/CodexBar/Providers/Shared/ProviderPresentation.swift index 70fe3504f..ab54ca346 100644 --- a/Sources/CodexBar/Providers/Shared/ProviderPresentation.swift +++ b/Sources/CodexBar/Providers/Shared/ProviderPresentation.swift @@ -6,7 +6,7 @@ struct ProviderPresentation { @MainActor static func standardDetailLine(context: ProviderPresentationContext) -> String { - let versionText = context.store.version(for: context.provider) ?? "not detected" + let versionText = context.store.version(for: context.provider) ?? L("not detected") return "\(context.metadata.cliName) \(versionText)" } } diff --git a/Sources/CodexBar/Providers/StepFun/StepFunProviderImplementation.swift b/Sources/CodexBar/Providers/StepFun/StepFunProviderImplementation.swift index dec72a8c8..e9c28e1b1 100644 --- a/Sources/CodexBar/Providers/StepFun/StepFunProviderImplementation.swift +++ b/Sources/CodexBar/Providers/StepFun/StepFunProviderImplementation.swift @@ -95,11 +95,7 @@ struct StepFunProviderImplementation: ProviderImplementation { options: cookieOptions, isVisible: nil, onChange: nil, - trailingText: { - guard let entry = CookieHeaderCache.load(provider: .stepfun) else { return nil } - let when = entry.storedAt.relativeDescription() - return "Cached: \(entry.sourceLabel) • \(when)" - }), + trailingText: { ProviderCookieSourceUI.cachedTrailingText(provider: .stepfun) }), ] } diff --git a/Sources/CodexBar/Providers/Windsurf/WindsurfProviderImplementation.swift b/Sources/CodexBar/Providers/Windsurf/WindsurfProviderImplementation.swift index 38d290663..755af3300 100644 --- a/Sources/CodexBar/Providers/Windsurf/WindsurfProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Windsurf/WindsurfProviderImplementation.swift @@ -71,7 +71,7 @@ struct WindsurfProviderImplementation: ProviderImplementation { trailingText: { guard context.settings.windsurfUsageDataSource == .auto else { return nil } let label = context.store.sourceLabel(for: .windsurf) - return label == "auto" ? nil : label + return label == "auto" ? nil : LocalizedProviderText.sourceLabel(label) }), ProviderSettingsPickerDescriptor( id: "windsurf-cookie-source", diff --git a/Sources/CodexBar/Resources/en.lproj/Localizable.strings b/Sources/CodexBar/Resources/en.lproj/Localizable.strings index e22ef072c..9336c303a 100644 --- a/Sources/CodexBar/Resources/en.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/en.lproj/Localizable.strings @@ -17,6 +17,7 @@ "Add Workspace" = "Add Workspace"; "Advanced" = "Advanced"; "All" = "All"; +"All Systems Operational" = "All Systems Operational"; "Always allow prompts" = "Always allow prompts"; "Animation pattern" = "Animation pattern"; "Antigravity login is managed in the app" = "Antigravity login is managed in the app"; @@ -71,6 +72,7 @@ "Claude cookies" = "Claude cookies"; "Claude login failed" = "Claude login failed"; "Claude login timed out" = "Claude login timed out"; +"Claude web usage" = "Claude web usage"; "Close" = "Close"; "Code review" = "Code review"; "Codex CLI not found" = "Codex CLI not found"; @@ -219,6 +221,7 @@ "OpenAI web extras" = "OpenAI web extras"; "Option A" = "Option A"; "Option B" = "Option B"; +"Optional. Turn this on to show code review, usage breakdown, and credits history via chatgpt.com." = "Optional. Turn this on to show code review, usage breakdown, and credits history via chatgpt.com."; "Optional override if workspace lookup fails." = "Optional override if workspace lookup fails."; "Options" = "Options"; "Override auto-detection with a custom IDE base path" = "Override auto-detection with a custom IDE base path"; @@ -247,6 +250,7 @@ "Progress bars fill as you consume quota (instead of showing remaining)." = "Progress bars fill as you consume quota (instead of showing remaining)."; "Provider" = "Provider"; "Providers" = "Providers"; +"Quota usage" = "Quota usage"; "Quit CodexBar" = "Quit CodexBar"; "Random (default)" = "Random (default)"; "Reads local usage logs. Shows today + last 30 days cost in the menu." = "Reads local usage logs. Shows today + last 30 days cost in the menu."; @@ -288,6 +292,7 @@ "Stack token accounts in the menu (otherwise show an account switcher bar)." = "Stack token accounts in the menu (otherwise show an account switcher bar)."; "Start at Login" = "Start at Login"; "Status" = "Status"; +"State" = "State"; "Store Claude sessionKey cookies or OAuth access tokens." = "Store Claude sessionKey cookies or OAuth access tokens."; "Store multiple Abacus AI Cookie headers." = "Store multiple Abacus AI Cookie headers."; "Store multiple Augment Cookie headers." = "Store multiple Augment Cookie headers."; @@ -629,6 +634,297 @@ "refresh_15min" = "15 min"; "refresh_30min" = "30 min"; +/* Runtime menu and provider details */ +"%.0f%% used" = "%.0f%% used"; +"%1$@ days of usage data across %2$@ services" = "%1$@ days of usage data across %2$@ services"; +"%1$@: %2$@" = "%1$@: %2$@"; +"%1$@: %2$@ / %3$@" = "%1$@: %2$@ / %3$@"; +"%1$@: %2$@ credits" = "%1$@: %2$@ credits"; +"%1$@: %2$@ · %3$@ tokens" = "%1$@: %2$@ · %3$@ tokens"; +"%1$@: %2$@%% used" = "%1$@: %2$@%% used"; +"%@ days of cost data" = "%@ days of cost data"; +"%@ days of credits data" = "%@ days of credits data"; +"%@ login successful" = "%@ login successful"; +"%@ more items" = "%@ more items"; +"%@ percent" = "%@ percent"; +"%@ unreadable item(s) skipped" = "%@ unreadable item(s) skipped"; +"%@ utilization samples" = "%@ utilization samples"; +"Account Added" = "Account Added"; +"Account: %@" = "Account: %@"; +"Activity: %@" = "Activity: %@"; +"Add Account" = "Add Account"; +"Add accounts via GitHub OAuth Device Flow on the selected host." = "Add accounts via GitHub OAuth Device Flow on the selected host."; +"Antigravity login failed" = "Antigravity login failed"; +"Antigravity login timed out" = "Antigravity login timed out"; +"Auth source" = "Auth source"; +"Auto uses the local IDE API first, then Google OAuth when the IDE is closed." = "Auto uses the local IDE API first, then Google OAuth when the IDE is closed."; +"Automatic imports Chrome browser cookies from Xiaomi MiMo." = "Automatic imports Chrome browser cookies from Xiaomi MiMo."; +"Automatic imports Windsurf session data from Chromium browser localStorage." = "Automatic imports Windsurf session data from Chromium browser localStorage."; +"Automatically imports browser session cookies." = "Automatically imports browser session cookies."; +"Balance: %@" = "Balance: %@"; +"Buy Credits" = "Buy Credits"; +"Choose the Moonshot/Kimi API host for international or China mainland accounts." = "Choose the Moonshot/Kimi API host for international or China mainland accounts."; +"Cleanup ideas" = "Cleanup ideas"; +"Cookie: …\n\nor paste a cURL capture from the Abacus AI dashboard" = "Cookie: …\n\nor paste a cURL capture from the Abacus AI dashboard"; +"Cookie: …\n\nor paste the __Secure-next-auth.session-token value" = "Cookie: …\n\nor paste the __Secure-next-auth.session-token value"; +"Cookie: …\n\nor paste the kimi-auth token value" = "Cookie: …\n\nor paste the kimi-auth token value"; +"Copied" = "Copied"; +"Copy error" = "Copy error"; +"Copy path" = "Copy path"; +"Cost history chart" = "Cost history chart"; +"Could Not Identify GitHub Account" = "Could Not Identify GitHub Account"; +"CodexBar will ask macOS Keychain for %@ so it can fetch %@. Click OK to continue." = "CodexBar will ask macOS Keychain for %@ so it can fetch %@. Click OK to continue."; +"CodexBar will ask macOS Keychain for “%@” so it can decrypt browser cookies and authenticate your account. Click OK to continue." = "CodexBar will ask macOS Keychain for “%@” so it can decrypt browser cookies and authenticate your account. Click OK to continue."; +"Codex dashboard extras" = "Codex dashboard extras"; +"Could not open browser for Antigravity" = "Could not open browser for Antigravity"; +"Credits history chart" = "Credits history chart"; +"Credits remaining" = "Credits remaining"; +"Cursor Login" = "Cursor Login"; +"Disabled — no recent data" = "Disabled — no recent data"; +"Drag to reorder" = "Drag to reorder"; +"Enterprise host" = "Enterprise host"; +"Extra usage" = "Extra usage"; +"Extra usage spent" = "Extra usage spent"; +"Full in ~%.0f regens" = "Full in ~%.0f regens"; +"Full in ~1 regen" = "Full in ~1 regen"; +"GitHub login succeeded, but CodexBar could not verify which account it belongs to. Please try again." = "GitHub login succeeded, but CodexBar could not verify which account it belongs to. Please try again."; +"Google OAuth" = "Google OAuth"; +"Hover a bar for details" = "Hover a bar for details"; +"Keychain Access Required" = "Keychain Access Required"; +"Label" = "Label"; +"Last 30 days: %1$@ · %2$@ tokens" = "Last 30 days: %1$@ · %2$@ tokens"; +"Last 30 days: %@" = "Last 30 days: %@"; +"Login Failed" = "Login Failed"; +"Near full" = "Near full"; +"No %@ utilization data yet." = "No %@ utilization data yet."; +"No cost history data available." = "No cost history data available."; +"No credits history data available." = "No credits history data available."; +"No data" = "No data"; +"No local data found" = "No local data found"; +"No system account" = "No system account"; +"No usage breakdown data available." = "No usage breakdown data available."; +"No usage configured." = "No usage configured."; +"No usage yet" = "No usage yet"; +"No utilization data yet." = "No utilization data yet."; +"Not fetched yet" = "Not fetched yet"; +"Oasis-Token" = "Oasis-Token"; +"Oasis-Token=…" = "Oasis-Token=…"; +"Open Codebuff Dashboard" = "Open Codebuff Dashboard"; +"Open Command Code Settings" = "Open Command Code Settings"; +"Open Crof dashboard" = "Open Crof dashboard"; +"Open Manus" = "Open Manus"; +"Open MiMo Balance" = "Open MiMo Balance"; +"Open Moonshot Console" = "Open Moonshot Console"; +"Open StepFun Platform" = "Open StepFun Platform"; +"Open Token Plan" = "Open Token Plan"; +"Open Volcengine Ark Console" = "Open Volcengine Ark Console"; +"Open billing" = "Open billing"; +"Optional. Enter your GitHub Enterprise host, for example octocorp.ghe.com. Leave blank for github.com." = "Optional. Enter your GitHub Enterprise host, for example octocorp.ghe.com. Leave blank for github.com."; +"Paste the Oasis-Token from a logged-in browser session on platform.stepfun.com." = "Paste the Oasis-Token from a logged-in browser session on platform.stepfun.com."; +"Paste the Cookie header from a request to admin.mistral.ai. Must contain an ory_session_* cookie." = "Paste the Cookie header from a request to admin.mistral.ai. Must contain an ory_session_* cookie."; +"Plan utilization chart" = "Plan utilization chart"; +"Plan: %@" = "Plan: %@"; +"Quota: %@ / %@" = "Quota: %@ / %@"; +"Refreshing" = "Refreshing"; +"Regenerates %@" = "Regenerates %@"; +"Reload" = "Reload"; +"Reorder" = "Reorder"; +"Show local disk usage in menus. Scans known provider-owned paths in the background." = "Show local disk usage in menus. Scans known provider-owned paths in the background."; +"Show provider storage usage" = "Show provider storage usage"; +"Simulated error text" = "Simulated error text"; +"Storage" = "Storage"; +"Storage:" = "Storage:"; +"Source" = "Source"; +"Limits background chatgpt.com refreshes to reduce battery and network usage. Dashboard extras may stay stale until you refresh them manually." = "Limits background chatgpt.com refreshes to reduce battery and network usage. Dashboard extras may stay stale until you refresh them manually."; +"This month" = "This month"; +"Today: %1$@ · %2$@ tokens" = "Today: %1$@ · %2$@ tokens"; +"Today: %@" = "Today: %@"; +"Token Refreshed" = "Token Refreshed"; +"Total (30d): %@ credits" = "Total (30d): %@ credits"; +"Total (30d): %@" = "Total (30d): %@"; +"Total: %@" = "Total: %@"; +"Unavailable" = "Unavailable"; +"Usage breakdown chart" = "Usage breakdown chart"; +"Usage used" = "Usage used"; +"Uses username + password to login and obtain an Oasis-Token automatically." = "Uses username + password to login and obtain an Oasis-Token automatically."; +"Windsurf session JSON bundle" = "Windsurf session JSON bundle"; +"Workspace ID" = "Workspace ID"; +"You can return to the app; authentication finished." = "You can return to the app; authentication finished."; +"Your StepFun platform password. Used to login and obtain a session token." = "Your StepFun platform password. Used to login and obtain a session token."; +"after next regen" = "after next regen"; +"ark-..." = "ark-..."; +"cb_..." = "cb_..."; +"cpk-..." = "cpk-..."; +"crof_..." = "crof_..."; +"github.com" = "github.com"; +"kilo_..." = "kilo_..."; +"left" = "left"; +"just now" = "just now"; +"not detected" = "not detected"; +"not_found" = "Not found"; +"session_id=...\n\nor paste just the session_id value" = "session_id=...\n\nor paste just the session_id value"; +"sk-..." = "sk-..."; +"sk-or-v1-..." = "sk-or-v1-..."; +"used" = "used"; +"used after next regen" = "used after next regen"; +"usage" = "usage"; +"user@example.com" = "user@example.com"; +"the Claude Code OAuth token" = "the Claude Code OAuth token"; +"your Amp cookie header" = "your Amp cookie header"; +"your Augment cookie header" = "your Augment cookie header"; +"your Claude cookie header" = "your Claude cookie header"; +"your Claude usage" = "your Claude usage"; +"your Cursor cookie header" = "your Cursor cookie header"; +"your Factory cookie header" = "your Factory cookie header"; +"your GitHub Copilot token" = "your GitHub Copilot token"; +"your Kimi K2 API key" = "your Kimi K2 API key"; +"your Kimi auth token" = "your Kimi auth token"; +"your MiniMax API token" = "your MiniMax API token"; +"your MiniMax cookie header" = "your MiniMax cookie header"; +"your OpenAI cookie header" = "your OpenAI cookie header"; +"your OpenCode cookie header" = "your OpenCode cookie header"; +"your Synthetic API key" = "your Synthetic API key"; +"your z.ai API token" = "your z.ai API token"; +"wk-..." = "wk-..."; +"wrk_…" = "wrk_…"; +"~/Library/Application Support/JetBrains/IntelliJIdea2024.3" = "~/Library/Application Support/JetBrains/IntelliJIdea2024.3"; +"StepFun platform account (phone number or email)." = "StepFun platform account (phone number or email)."; +"Stored in ~/.codexbar/config.json." = "Stored in ~/.codexbar/config.json."; +"Stored in ~/.codexbar/config.json. Get your API key from the Volcengine Ark console." = "Stored in ~/.codexbar/config.json. Get your API key from the Volcengine Ark console."; +"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." = "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."; +"Stored in ~/.codexbar/config.json. In Warp, open Settings > Platform > API Keys, then create one." = "Stored in ~/.codexbar/config.json. In Warp, open Settings > Platform > API Keys, then create one."; +"Stored in ~/.codexbar/config.json. You can also provide CODEBUFF_API_KEY or let CodexBar read ~/.config/manicode/credentials.json (created by `codebuff login`)." = "Stored in ~/.codexbar/config.json. You can also provide CODEBUFF_API_KEY or let CodexBar read ~/.config/manicode/credentials.json (created by `codebuff login`)."; +"Stored in ~/.codexbar/config.json. You can also provide CROF_API_KEY." = "Stored in ~/.codexbar/config.json. You can also provide CROF_API_KEY."; +"Stored in ~/.codexbar/config.json. You can also provide KILO_API_KEY or ~/.local/share/kilo/auth.json (kilo.access)." = "Stored in ~/.codexbar/config.json. You can also provide KILO_API_KEY or ~/.local/share/kilo/auth.json (kilo.access)."; +"Stored in ~/.codexbar/config.json. You can also provide OPENAI_API_KEY." = "Stored in ~/.codexbar/config.json. You can also provide OPENAI_API_KEY."; +"The browser login did not complete in time. Try Antigravity login again." = "The browser login did not complete in time. Try Antigravity login again."; +"Username" = "Username"; +"Password" = "Password"; +"Secure" = "Secure"; +"Secure subtitle" = "Secure subtitle"; +"Subtitle" = "Subtitle"; +"Managed Codex login did not complete. Try again after finishing the browser login flow." = "Managed Codex login did not complete. Try again after finishing the browser login flow."; +"Codex login completed, but no account email was available. Try again after confirming the account is fully signed in." = "Codex login completed, but no account email was available. Try again after confirming the account is fully signed in."; +"CodexBar found multiple workspaces, but no workspace was selected." = "CodexBar found multiple workspaces, but no workspace was selected."; +"CodexBar could not read managed account storage. Recover the store before adding another account." = "CodexBar could not read managed account storage. Recover the store before adding another account."; +"No output captured." = "No output captured."; +"That account is no longer available in CodexBar. Refresh the account list and try again." = "That account is no longer available in CodexBar. Refresh the account list and try again."; +"CodexBar could not find saved auth for that account. Re-authenticate it and try again." = "CodexBar could not find saved auth for that account. Re-authenticate it and try again."; +"CodexBar could not read saved auth for that account. Re-authenticate it and try again." = "CodexBar could not read saved auth for that account. Re-authenticate it and try again."; +"CodexBar could not read the current system account on this Mac." = "CodexBar could not read the current system account on this Mac."; +"CodexBar could not safely preserve the current system account before switching." = "CodexBar could not safely preserve the current system account before switching."; +"CodexBar can't replace a system account that is signed in with an API key only setup." = "CodexBar can't replace a system account that is signed in with an API key only setup."; +"CodexBar found another managed account that already uses the current system account. Resolve the duplicate account before switching." = "CodexBar found another managed account that already uses the current system account. Resolve the duplicate account before switching."; +"CodexBar could not save the current system account before switching." = "CodexBar could not save the current system account before switching."; +"CodexBar could not update managed account storage." = "CodexBar could not update managed account storage."; +"CodexBar could not replace the live Codex auth on this Mac." = "CodexBar could not replace the live Codex auth on this Mac."; +"%1$@ · %2$@ · %3$@ credits" = "%1$@ · %2$@ · %3$@ credits"; +"%1$@/%2$@ left" = "%1$@/%2$@ left"; +"%@ left" = "%@ left"; +"%@ · %@" = "%@ · %@"; +"%d%% in deficit" = "%d%% in deficit"; +"%d%% in reserve" = "%d%% in reserve"; +"5-hour" = "5-hour"; +"API" = "API"; +"About CodexBar" = "About CodexBar"; +"Add Account..." = "Add Account..."; +"Auto" = "Auto"; +"Beta" = "Beta"; +"BigModel CN (open.bigmodel.cn)" = "BigModel CN (open.bigmodel.cn)"; +"CLI" = "CLI"; +"CLI (PTY)" = "CLI (PTY)"; +"CLI (RPC/PTY)" = "CLI (RPC/PTY)"; +"Cached: %@ • %@" = "Cached: %@ • %@"; +"Changelog" = "Changelog"; +"China (api.moonshot.cn)" = "China (api.moonshot.cn)"; +"China mainland (bailian.console.aliyun.com)" = "China mainland (bailian.console.aliyun.com)"; +"China mainland (platform.minimaxi.com)" = "China mainland (platform.minimaxi.com)"; +"Critical" = "Critical"; +"Cylon" = "Cylon"; +"Extra usage balance: %@" = "Extra usage balance: %@"; +"Global (api.z.ai)" = "Global (api.z.ai)"; +"Global (platform.minimax.io)" = "Global (platform.minimax.io)"; +"Info" = "Info"; +"International (api.moonshot.ai)" = "International (api.moonshot.ai)"; +"International (modelstudio.console.alibabacloud.com)" = "International (modelstudio.console.alibabacloud.com)"; +"Keychain access is disabled in Advanced, so browser cookie import is unavailable. %@" = "Keychain access is disabled in Advanced, so browser cookie import is unavailable. %@"; +"Knight Rider" = "Knight Rider"; +"Lasts until reset" = "Lasts until reset"; +"Local (SQLite cache)" = "Local (SQLite cache)"; +"Local IDE API" = "Local IDE API"; +"Monthly" = "Monthly"; +"OAuth API" = "OAuth API"; +"On pace" = "On pace"; +"Open Droid in Browser..." = "Open Droid in Browser..."; +"Outside-In" = "Outside-In"; +"Pace: %@" = "Pace: %@"; +"Pace: %@ · %@" = "Pace: %@ · %@"; +"Pulse" = "Pulse"; +"Quit" = "Quit"; +"Race" = "Race"; +"Receive only stable, production-ready releases." = "Receive only stable, production-ready releases."; +"Receive stable releases plus beta previews." = "Receive stable releases plus beta previews."; +"Refresh Session" = "Refresh Session"; +"Resets" = "Resets"; +"Resets %@" = "Resets %@"; +"Runs out in %@" = "Runs out in %@"; +"Runs out now" = "Runs out now"; +"Session" = "Session"; +"Settings..." = "Settings..."; +"Sonnet" = "Sonnet"; +"Stable" = "Stable"; +"Status Page" = "Status Page"; +"Switch Account..." = "Switch Account..."; +"System Account" = "System Account"; +"Trace" = "Trace"; +"Unbraid (logo → bars)" = "Unbraid (logo → bars)"; +"Updated %@" = "Updated %@"; +"Updated just now" = "Updated just now"; +"Update ready, restart now?" = "Update ready, restart now?"; +"Usage Dashboard" = "Usage Dashboard"; +"Verbose" = "Verbose"; +"Warning" = "Warning"; +"Web API (IndexedDB)" = "Web API (IndexedDB)"; +"Web API (cookies)" = "Web API (cookies)"; +"Weekly" = "Weekly"; +"in %1$d d %2$d h" = "in %1$dd %2$dh"; +"in %1$d h %2$d m" = "in %1$dh %2$dm"; +"in %d d" = "in %dd"; +"in %d h" = "in %dh"; +"in %d m" = "in %dm"; +"now" = "now"; +"source_api" = "API"; +"source_auto" = "Auto"; +"source_cached" = "Cached"; +"source_claude_cli" = "Claude CLI"; +"source_cli" = "CLI"; +"source_codex_cli" = "Codex CLI"; +"source_local" = "Local"; +"source_login" = "Login"; +"source_manual" = "Manual"; +"source_manual_cookie_header" = "Manual cookie header"; +"source_oauth" = "OAuth"; +"source_oauth_api" = "OAuth API"; +"source_off" = "Off"; +"source_openai_web" = "OpenAI Web"; +"source_web" = "Web"; +"source_windsurf_web" = "Windsurf Web"; +"tomorrow, %@" = "tomorrow, %@"; +"≈ %d%% run-out risk" = "≈ %d%% run-out risk"; + /* Cost estimation */ "cost_header_estimated" = "Cost (estimated)"; "cost_estimate_hint" = "Estimated from local logs · may differ from your bill"; +"Est. total (30d): %@" = "Est. total (30d): %@"; +"Estimated from local Codex logs for the selected account." = "Estimated from local Codex logs for the selected account."; +"Estimated from local Claude logs at API rates; token totals include cache read/write tokens and may differ from Claude Code /status." = "Estimated from local Claude logs at API rates; token totals include cache read/write tokens and may differ from Claude Code /status."; +"Estimated from local logs · may differ from your bill" = "Estimated from local logs · may differ from your bill"; +"Latest billing day" = "Latest billing day"; +"Latest billing day (%@)" = "Latest billing day (%@)"; +"Reported by AWS Cost Explorer; daily billing data can lag." = "Reported by AWS Cost Explorer; daily billing data can lag."; +"Could not open Cursor login in your browser." = "Could not open Cursor login in your browser."; +"Sign in to cursor.com in your browser, then refresh Cursor in CodexBar." = "Sign in to cursor.com in your browser, then refresh Cursor in CodexBar."; +"Timed out waiting for Cursor login. %@" = "Timed out waiting for Cursor login. %@"; +"Timed out waiting for Cursor login. %@ Last error: %@" = "Timed out waiting for Cursor login. %@ Last error: %@"; +"Org ID (optional)" = "Org ID (optional)"; +"Optional organization ID for accounts linked to multiple Anthropic organizations." = "Optional organization ID for accounts linked to multiple Anthropic organizations."; diff --git a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings index af4775eb6..a508d3c09 100644 --- a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings @@ -1,369 +1,374 @@ /* Chinese (Simplified) localization for CodexBar */ -" providers" = ""; -"(System)" = ""; -"30d" = ""; -"A managed Codex login is already running. Wait for it to finish before adding " = ""; -"API key" = ""; -"API region" = ""; -"API token" = ""; -"API tokens" = ""; -"About" = ""; -"Account" = ""; -"Accounts" = ""; -"Accounts subtitle" = ""; -"Active" = ""; -"Add" = ""; +" providers" = " 个提供商"; +"(System)" = "(系统)"; +"30d" = "30天"; +"A managed Codex login is already running. Wait for it to finish before adding " = "托管 Codex 登录已在运行。请等待完成后再添加 "; +"API key" = "API 密钥"; +"API region" = "API 区域"; +"API token" = "API token"; +"API tokens" = "API tokens"; +"About" = "关于"; +"Account" = "账户"; +"Accounts" = "账户"; +"Accounts subtitle" = "账户副标题"; +"Active" = "活跃"; +"Add" = "添加"; "Add Workspace" = "添加工作区"; -"Advanced" = ""; -"All" = ""; -"Always allow prompts" = ""; -"Animation pattern" = ""; -"Antigravity login is managed in the app" = ""; -"Applies only to the Security.framework OAuth keychain reader." = ""; -"Auto falls back to the next source if the preferred one fails." = ""; -"Auto uses API first, then falls back to CLI on auth failures." = ""; -"Auto-detect" = ""; -"Auto-refresh is off; use the menu's Refresh command." = ""; -"Auto-refresh: hourly · Timeout: 10m" = ""; -"Automatic" = ""; -"Automatic imports browser cookies and WorkOS tokens." = ""; -"Automatic imports browser cookies and local storage tokens." = ""; -"Automatic imports browser cookies for dashboard extras." = ""; -"Automatic imports browser cookies for the web API." = ""; -"Automatic imports browser cookies from Model Studio/Bailian." = ""; -"Automatic imports browser cookies from admin.mistral.ai." = ""; -"Automatic imports browser cookies from opencode.ai." = ""; -"Automatic imports browser cookies or stored sessions." = ""; -"Automatic imports browser cookies." = ""; -"Automatically imports browser session cookie." = ""; -"Automatically opens CodexBar when you start your Mac." = ""; -"Automation" = ""; -"Average (\\(label1) + \\(label2))" = ""; -"Average (\\(metadata.sessionLabel) + \\(metadata.weeklyLabel))" = ""; -"Avoid Keychain prompts" = ""; -"Balance" = ""; -"Battery Saver" = ""; -"Bordered" = ""; -"Build" = ""; -"Built \\(buildTimestamp)" = ""; -"Buy Credits..." = ""; -"Buy Credits…" = ""; -"CLI paths" = ""; -"CLI sessions" = ""; -"Caches" = ""; +"Advanced" = "高级"; +"All" = "全部"; +"All Systems Operational" = "所有系统正常运行"; +"Always allow prompts" = "始终允许提示"; +"Animation pattern" = "动画模式"; +"Antigravity login is managed in the app" = "Antigravity 登录由应用管理"; +"Applies only to the Security.framework OAuth keychain reader." = "仅适用于 Security.framework OAuth 钥匙串读取器。"; +"Auto falls back to the next source if the preferred one fails." = "自动模式会在首选来源失败时回退到下一个来源。"; +"Auto uses API first, then falls back to CLI on auth failures." = "自动模式优先使用 API,认证失败时回退到 CLI。"; +"Auto-detect" = "自动检测"; +"Auto-refresh is off; use the menu's Refresh command." = "自动刷新已关闭;请使用菜单中的“刷新”命令。"; +"Auto-refresh: hourly · Timeout: 10m" = "自动刷新:每小时 · 超时:10分钟"; +"Automatic" = "自动"; +"Automatic imports browser cookies and WorkOS tokens." = "自动导入浏览器 Cookie 和 WorkOS tokens。"; +"Automatic imports browser cookies and local storage tokens." = "自动导入浏览器 Cookie 和本地存储 tokens。"; +"Automatic imports browser cookies for dashboard extras." = "自动导入用于仪表板扩展信息的浏览器 Cookie。"; +"Automatic imports browser cookies for the web API." = "自动导入 Web API 的浏览器 Cookie。"; +"Automatic imports browser cookies from Model Studio/Bailian." = "自动从 Model Studio/Bailian 导入浏览器 Cookie。"; +"Automatic imports browser cookies from admin.mistral.ai." = "自动从 admin.mistral.ai 导入浏览器 Cookie。"; +"Automatic imports browser cookies from opencode.ai." = "自动从 opencode.ai 导入浏览器 Cookie。"; +"Automatic imports browser cookies or stored sessions." = "自动导入浏览器 Cookie 或已存储会话。"; +"Automatic imports browser cookies." = "自动导入浏览器 Cookie。"; +"Automatically imports browser session cookie." = "自动导入浏览器会话 Cookie。"; +"Automatically opens CodexBar when you start your Mac." = "启动 Mac 时自动打开 CodexBar。"; +"Automation" = "自动化"; +"Average (\\(label1) + \\(label2))" = "平均(\\(label1) + \\(label2))"; +"Average (\\(metadata.sessionLabel) + \\(metadata.weeklyLabel))" = "平均(\\(metadata.sessionLabel) + \\(metadata.weeklyLabel))"; +"Avoid Keychain prompts" = "避免钥匙串提示"; +"Balance" = "余额"; +"Battery Saver" = "省电模式"; +"Bordered" = "带边框"; +"Build" = "构建"; +"Built \\(buildTimestamp)" = "构建于 \\(buildTimestamp)"; +"Buy Credits..." = "购买积分..."; +"Buy Credits…" = "购买积分…"; +"CLI paths" = "CLI 路径"; +"CLI sessions" = "CLI 会话"; +"Caches" = "缓存"; "Cancel" = "取消"; -"Check for Updates…" = ""; -"Check for updates automatically" = ""; -"Check if you like your agents having some fun up there." = ""; -"Check provider status" = ""; +"Check for Updates…" = "检查更新…"; +"Check for updates automatically" = "自动检查更新"; +"Check if you like your agents having some fun up there." = "看看你是否喜欢你的智能体在上面找点乐子。"; +"Check provider status" = "检查提供商状态"; "Choose Codex workspace" = "选择 Codex 工作区"; -"Choose the MiniMax host (global .io or China mainland .com)." = ""; -"Choose up to " = ""; -"Choose up to \\(Self.maxOverviewProviders) providers" = ""; -"Choose up to \\(count) providers" = ""; -"Choose what to show in the menu bar (Pace shows usage vs. expected)." = ""; -"Choose which Codex account CodexBar should follow." = ""; -"Choose which window drives the menu bar percent." = ""; -"Chrome" = ""; -"Claude CLI not found" = ""; -"Claude binary" = ""; -"Claude cookies" = ""; -"Claude login failed" = ""; -"Claude login timed out" = ""; -"Close" = ""; -"Code review" = ""; -"Codex CLI not found" = ""; -"Codex account login already running" = ""; -"Codex binary" = ""; -"Codex login failed" = ""; -"Codex login timed out" = ""; -"CodexBar Lifecycle Keepalive" = ""; -"CodexBar could not read managed account storage. " = ""; -"Configure…" = ""; -"Connected" = ""; -"Controls how much detail is logged." = ""; -"Cookie header" = ""; -"Cookie source" = ""; -"Cookie: ..." = ""; -"Cookie: \\u{2026}\\\n\\\nor paste a cURL capture from the Abacus AI dashboard" = ""; -"Cookie: \\u{2026}\\\n\\\nor paste the __Secure-next-auth.session-token value" = ""; -"Cookie: \\u{2026}\\\n\\\nor paste the kimi-auth token value" = ""; -"Cookie: …" = ""; -"CopilotDeviceFlow" = ""; -"Cost" = ""; -"Could not add Codex account" = ""; -"Could not open Terminal for Gemini" = ""; -"Could not start claude /login" = ""; -"Could not start codex login" = ""; -"Could not switch system account" = ""; -"Credits" = ""; -"Credits history" = ""; -"Cursor login failed" = ""; -"Custom" = ""; -"Custom Path" = ""; -"Daily Routines" = ""; -"Debug" = ""; -"Default" = ""; -"Designs" = ""; -"Disable Keychain access" = ""; -"Disabled" = ""; -"Disconnected" = ""; -"Display" = ""; -"Display mode" = ""; -"Display reset times as absolute clock values instead of countdowns." = ""; -"Done" = ""; -"Effective PATH" = ""; -"Email" = ""; -"Enable Merge Icons to configure Overview tab providers." = ""; -"Enable file logging" = ""; -"Enabled" = ""; -"Error" = ""; -"Error simulation" = ""; -"Expose troubleshooting tools in the Debug tab." = ""; -"Failed" = ""; -"False" = ""; -"Fetch strategy attempts" = ""; -"Fetching" = ""; -"Field" = ""; -"Field subtitle" = ""; -"Finish the current managed account change before switching the system account." = ""; -"Force animation on next refresh" = ""; -"Gateway region" = ""; -"Gemini CLI not found" = ""; -"Gemini/Antigravity, surfacing incidents in the icon and menu." = ""; -"General" = ""; -"GitHub" = ""; +"Choose the MiniMax host (global .io or China mainland .com)." = "选择 MiniMax 主机(全球 .io 或中国大陆 .com)。"; +"Choose up to " = "最多选择 "; +"Choose up to \\(Self.maxOverviewProviders) providers" = "最多选择 \\(Self.maxOverviewProviders) 个提供商"; +"Choose up to \\(count) providers" = "最多选择 \\(count) 个提供商"; +"Choose what to show in the menu bar (Pace shows usage vs. expected)." = "选择菜单栏显示内容(节奏显示用量与预期的对比)。"; +"Choose which Codex account CodexBar should follow." = "选择 CodexBar 要跟随的 Codex 账户。"; +"Choose which window drives the menu bar percent." = "选择哪个窗口驱动菜单栏百分比。"; +"Chrome" = "Chrome"; +"Claude CLI not found" = "未找到 Claude CLI"; +"Claude binary" = "Claude 二进制文件"; +"Claude cookies" = "Claude Cookie"; +"Claude login failed" = "Claude 登录失败"; +"Claude login timed out" = "Claude 登录超时"; +"Claude web usage" = "Claude Web 用量"; +"Close" = "关闭"; +"Code review" = "代码审查"; +"Codex CLI not found" = "未找到 Codex CLI"; +"Codex account login already running" = "Codex 账户登录已在运行"; +"Codex binary" = "Codex 二进制文件"; +"Codex login failed" = "Codex 登录失败"; +"Codex login timed out" = "Codex 登录超时"; +"CodexBar Lifecycle Keepalive" = "CodexBar 生命周期保活"; +"CodexBar could not read managed account storage. " = "CodexBar 无法读取托管账户存储。 "; +"Configure…" = "配置…"; +"Connected" = "已连接"; +"Controls how much detail is logged." = "控制记录多少详细信息。"; +"Cookie header" = "Cookie 标头"; +"Cookie source" = "Cookie 来源"; +"Cookie: ..." = "Cookie: ..."; +"Cookie: \\u{2026}\\\n\\\nor paste a cURL capture from the Abacus AI dashboard" = "Cookie: \\u{2026}\\\n\\\n或粘贴来自 Abacus AI 仪表板的 cURL 捕获"; +"Cookie: \\u{2026}\\\n\\\nor paste the __Secure-next-auth.session-token value" = "Cookie: \\u{2026}\\\n\\\n或粘贴 __Secure-next-auth.session-token 的值"; +"Cookie: \\u{2026}\\\n\\\nor paste the kimi-auth token value" = "Cookie: \\u{2026}\\\n\\\n或粘贴 kimi-auth token 值"; +"Cookie: …" = "Cookie: …"; +"CopilotDeviceFlow" = "CopilotDeviceFlow"; +"Cost" = "费用"; +"Could not add Codex account" = "无法添加 Codex 账户"; +"Could not open Terminal for Gemini" = "无法为 Gemini 打开终端"; +"Could not start claude /login" = "无法启动 claude /login"; +"Could not start codex login" = "无法启动 codex login"; +"Could not switch system account" = "无法切换系统账户"; +"Credits" = "积分"; +"Credits history" = "积分历史"; +"Cursor login failed" = "Cursor 登录失败"; +"Custom" = "自定义"; +"Custom Path" = "自定义路径"; +"Daily Routines" = "每日例程"; +"Debug" = "调试"; +"Default" = "默认"; +"Designs" = "设计"; +"Disable Keychain access" = "禁用钥匙串访问"; +"Disabled" = "已禁用"; +"Disconnected" = "未连接"; +"Display" = "显示"; +"Display mode" = "显示模式"; +"Display reset times as absolute clock values instead of countdowns." = "将重置时间显示为绝对时钟值而不是倒计时。"; +"Done" = "完成"; +"Effective PATH" = "有效 PATH"; +"Email" = "电子邮件"; +"Enable Merge Icons to configure Overview tab providers." = "启用合并图标以配置概览标签提供商。"; +"Enable file logging" = "启用文件日志"; +"Enabled" = "已启用"; +"Error" = "错误"; +"Error simulation" = "错误模拟"; +"Expose troubleshooting tools in the Debug tab." = "在调试标签中显示故障排除工具。"; +"Failed" = "失败"; +"False" = "假"; +"Fetch strategy attempts" = "获取策略尝试"; +"Fetching" = "获取中"; +"Field" = "字段"; +"Field subtitle" = "字段副标题"; +"Finish the current managed account change before switching the system account." = "请先完成当前托管账户变更,再切换系统账户。"; +"Force animation on next refresh" = "下次刷新时强制动画"; +"Gateway region" = "网关区域"; +"Gemini CLI not found" = "未找到 Gemini CLI"; +"Gemini/Antigravity, surfacing incidents in the icon and menu." = "Gemini/Antigravity,并在图标和菜单中显示故障信息。"; +"General" = "通用"; +"GitHub" = "GitHub"; "GitHub Copilot Login" = "GitHub Copilot 登录"; -"GitHub Login" = ""; +"GitHub Login" = "GitHub 登录"; "Hide details" = "隐藏详情"; -"Hide personal information" = ""; -"Historical tracking" = ""; -"How often CodexBar polls providers in the background." = ""; -"Inactive" = ""; -"Install CLI" = ""; -"Install the Claude CLI (npm i -g @anthropic-ai/claude-code) and try again." = ""; -"Install the Codex CLI (npm i -g @openai/codex) and try again." = ""; -"Install the Gemini CLI (npm i -g @google/gemini-cli) and try again." = ""; +"Hide personal information" = "隐藏个人信息"; +"Historical tracking" = "历史跟踪"; +"How often CodexBar polls providers in the background." = "CodexBar 在后台轮询提供商的频率。"; +"Inactive" = "未活跃"; +"Install CLI" = "安装 CLI"; +"Install the Claude CLI (npm i -g @anthropic-ai/claude-code) and try again." = "请安装 Claude CLI(npm i -g @anthropic-ai/claude-code)后重试。"; +"Install the Codex CLI (npm i -g @openai/codex) and try again." = "请安装 Codex CLI(npm i -g @openai/codex)后重试。"; +"Install the Gemini CLI (npm i -g @google/gemini-cli) and try again." = "请安装 Gemini CLI(npm i -g @google/gemini-cli)后重试。"; "JetBrains AI is ready" = "JetBrains AI 已就绪"; -"JetBrains IDE" = ""; -"Keep CLI sessions alive" = ""; -"Keyboard shortcut" = ""; -"Keychain access" = ""; -"Keychain prompt policy" = ""; -"Last \\(name) fetch failed:" = ""; -"Last \\(self.store.metadata(for: self.provider).displayName) fetch failed:" = ""; -"Last attempt" = ""; -"Link" = ""; -"Loading animations" = ""; -"Loading…" = ""; -"Local" = ""; -"Logging" = ""; -"Login failed" = ""; -"Login shell PATH (startup capture)" = ""; -"Login timed out" = ""; -"MCP details" = ""; -"Managed Codex accounts unavailable" = ""; -"Managed account storage is unreadable. Live account access is still available, " = ""; -"Manual" = ""; -"May your tokens never run out—keep agent limits in view." = ""; -"Menu bar" = ""; -"Menu bar auto-shows the provider closest to its rate limit." = ""; -"Menu bar metric" = ""; -"Menu bar shows percent" = ""; -"Menu content" = ""; -"Merge Icons" = ""; -"Never prompt" = ""; -"No" = ""; -"No Codex accounts detected yet." = ""; -"No JetBrains IDE detected" = ""; -"No cost history data." = ""; -"No credits history data." = ""; -"No data available" = ""; -"No data yet" = ""; -"No enabled providers available for Overview." = ""; -"No providers selected" = ""; -"No token accounts yet." = ""; -"No usage breakdown data." = ""; -"None" = ""; -"Notifications" = ""; -"Notifies when the 5-hour session quota hits 0% and when it becomes " = ""; +"JetBrains IDE" = "JetBrains IDE"; +"Keep CLI sessions alive" = "保持 CLI 会话存活"; +"Keyboard shortcut" = "快捷键"; +"Keychain access" = "钥匙串访问"; +"Keychain prompt policy" = "钥匙串提示策略"; +"Last \\(name) fetch failed:" = "上次获取 \\(name) 失败:"; +"Last \\(self.store.metadata(for: self.provider).displayName) fetch failed:" = "上次获取 \\(self.store.metadata(for: self.provider).displayName) 失败:"; +"Last attempt" = "上次尝试"; +"Link" = "链接"; +"Loading animations" = "加载动画"; +"Loading…" = "加载中…"; +"Local" = "本地"; +"Logging" = "日志"; +"Login failed" = "登录失败"; +"Login shell PATH (startup capture)" = "登录 shell PATH(启动捕获)"; +"Login timed out" = "登录超时"; +"MCP details" = "MCP 详情"; +"Managed Codex accounts unavailable" = "托管 Codex 账户不可用"; +"Managed account storage is unreadable. Live account access is still available, " = "托管账户存储不可读。实时账户访问仍可用,"; +"Manual" = "手动"; +"May your tokens never run out—keep agent limits in view." = "愿你的 tokens 永不耗尽——时刻关注智能体用量限制。"; +"Menu bar" = "菜单栏"; +"Menu bar auto-shows the provider closest to its rate limit." = "菜单栏自动显示最接近速率限制的提供商。"; +"Menu bar metric" = "菜单栏指标"; +"Menu bar shows percent" = "菜单栏显示百分比"; +"Menu content" = "菜单内容"; +"Merge Icons" = "合并图标"; +"Never prompt" = "从不提示"; +"No" = "否"; +"No Codex accounts detected yet." = "尚未检测到 Codex 账户。"; +"No JetBrains IDE detected" = "未检测到 JetBrains IDE"; +"No cost history data." = "暂无费用历史数据。"; +"No credits history data." = "暂无积分历史数据。"; +"No data available" = "暂无可用数据"; +"No data yet" = "暂无数据"; +"No enabled providers available for Overview." = "概览没有可用的已启用提供商。"; +"No providers selected" = "未选择提供商"; +"No token accounts yet." = "尚无 token 账户。"; +"No usage breakdown data." = "暂无用量明细数据。"; +"None" = "无"; +"Notifications" = "通知"; +"Notifies when the 5-hour session quota hits 0% and when it becomes " = "当 5 小时会话配额降至 0% 以及恢复可用时发送通知。"; "OK" = "确定"; -"Obscure email addresses in the menu bar and menu UI." = ""; -"Off" = ""; -"Off-peak" = ""; -"Off-peak · peak in \\(self.formatDuration(minutes: minutes))" = ""; -"Offline" = ""; -"On" = ""; -"Online" = ""; -"Only on user action" = ""; -"Open" = ""; -"Open API Keys" = ""; -"Open Amp Settings" = ""; -"Open Antigravity to sign in, then refresh CodexBar." = ""; +"Obscure email addresses in the menu bar and menu UI." = "在菜单栏和菜单界面中隐藏电子邮件地址。"; +"Off" = "关闭"; +"Off-peak" = "非高峰"; +"Off-peak · peak in \\(self.formatDuration(minutes: minutes))" = "非高峰 · 高峰将在 \\(self.formatDuration(minutes: minutes)) 后开始"; +"Offline" = "离线"; +"On" = "开启"; +"Online" = "在线"; +"Only on user action" = "仅在用户操作时"; +"Open" = "打开"; +"Open API Keys" = "打开 API 密钥"; +"Open Amp Settings" = "打开 Amp 设置"; +"Open Antigravity to sign in, then refresh CodexBar." = "打开 Antigravity 登录,然后刷新 CodexBar。"; "Open Browser" = "打开浏览器"; -"Open Coding Plan" = ""; -"Open Console" = ""; -"Open Dashboard" = ""; -"Open Mistral Admin" = ""; -"Open Ollama Settings" = ""; +"Open Coding Plan" = "打开 Coding Plan"; +"Open Console" = "打开控制台"; +"Open Dashboard" = "打开仪表板"; +"Open Mistral Admin" = "打开 Mistral 管理后台"; +"Open Ollama Settings" = "打开 Ollama 设置"; "Open Terminal" = "打开终端"; -"Open Usage Page" = ""; -"Open Warp API Key Guide" = ""; -"Open menu" = ""; -"Open token file" = ""; -"OpenAI cookies" = ""; -"OpenAI web extras" = ""; -"Option A" = ""; -"Option B" = ""; -"Optional override if workspace lookup fails." = ""; -"Options" = ""; -"Override auto-detection with a custom IDE base path" = ""; -"Overview" = ""; -"Overview rows always follow provider order." = ""; -"Overview tab providers" = ""; -"Paste API key…" = ""; -"Paste API token…" = ""; -"Paste key…" = ""; -"Paste sessionKey or OAuth token…" = ""; -"Paste the Cookie header from a request to admin.mistral.ai. " = ""; -"Paste token…" = ""; -"Peak · ends in \\(self.formatDuration(minutes: remaining))" = ""; -"Personal" = ""; -"Picker" = ""; -"Picker subtitle" = ""; -"Placeholder" = ""; -"Plan" = ""; -"Play full-screen confetti when weekly usage resets." = ""; -"Polls OpenAI/Claude status pages and Google Workspace for " = ""; -"Prevents any Keychain access while enabled." = ""; -"Primary (API key limit)" = ""; -"Primary (\\(label))" = ""; -"Primary (\\(metadata.sessionLabel))" = ""; -"Probe logs" = ""; -"Progress bars fill as you consume quota (instead of showing remaining)." = ""; -"Provider" = ""; -"Providers" = ""; -"Quit CodexBar" = ""; -"Random (default)" = ""; -"Reads local usage logs. Shows today + last 30 days cost in the menu." = ""; -"Refresh" = ""; -"Refresh cadence" = ""; -"Remote" = ""; -"Remove" = ""; -"Remove Codex account?" = ""; -"Remove \\(account.email) from CodexBar? Its managed Codex home will be deleted." = ""; -"Remove \\(email) from CodexBar? Its managed Codex home will be deleted." = ""; -"Remove selected account" = ""; -"Replace critter bars with provider branding icons and a percentage." = ""; -"Replay selected animation" = ""; -"Requires authentication via GitHub Device Flow." = ""; -"Resets: \\(reset)" = ""; -"Rolling five-hour limit" = ""; -"Search hourly" = ""; -"Secondary (\\(label))" = ""; -"Secondary (\\(metadata.weeklyLabel))" = ""; -"Select a provider" = ""; -"Select the IDE to monitor" = ""; -"Session quota notifications" = ""; -"Session tokens" = ""; -"Settings" = ""; -"Show Codex Credits and Claude Extra usage sections in the menu." = ""; -"Show Debug Settings" = ""; -"Show all token accounts" = ""; -"Show cost summary" = ""; -"Show credits + extra usage" = ""; +"Open Usage Page" = "打开用量页面"; +"Open Warp API Key Guide" = "打开 Warp API 密钥指南"; +"Open menu" = "打开菜单"; +"Open token file" = "打开 token 文件"; +"OpenAI cookies" = "OpenAI Cookie"; +"OpenAI web extras" = "OpenAI Web 扩展信息"; +"Option A" = "选项 A"; +"Option B" = "选项 B"; +"Optional. Turn this on to show code review, usage breakdown, and credits history via chatgpt.com." = "可选。开启后可通过 chatgpt.com 显示代码审查、用量明细和积分历史。"; +"Optional override if workspace lookup fails." = "工作区查找失败时可选覆盖。"; +"Options" = "选项"; +"Override auto-detection with a custom IDE base path" = "使用自定义 IDE 基础路径覆盖自动检测"; +"Overview" = "概览"; +"Overview rows always follow provider order." = "概览行始终遵循提供商顺序。"; +"Overview tab providers" = "概览标签提供商"; +"Paste API key…" = "粘贴 API 密钥…"; +"Paste API token…" = "粘贴 API token…"; +"Paste key…" = "粘贴密钥…"; +"Paste sessionKey or OAuth token…" = "粘贴 sessionKey 或 OAuth token…"; +"Paste the Cookie header from a request to admin.mistral.ai. " = "粘贴来自 admin.mistral.ai 请求的 Cookie 标头。 "; +"Paste token…" = "粘贴 token…"; +"Peak · ends in \\(self.formatDuration(minutes: remaining))" = "高峰 · 将在 \\(self.formatDuration(minutes: remaining)) 后结束"; +"Personal" = "个人"; +"Picker" = "选择器"; +"Picker subtitle" = "选择器副标题"; +"Placeholder" = "占位符"; +"Plan" = "计划"; +"Play full-screen confetti when weekly usage resets." = "当每周用量重置时播放全屏彩纸。"; +"Polls OpenAI/Claude status pages and Google Workspace for " = "轮询 OpenAI/Claude 状态页面和 Google Workspace 的 "; +"Prevents any Keychain access while enabled." = "启用时阻止任何钥匙串访问。"; +"Primary (API key limit)" = "主要(API 密钥限制)"; +"Primary (\\(label))" = "主要(\\(label))"; +"Primary (\\(metadata.sessionLabel))" = "主要(\\(metadata.sessionLabel))"; +"Probe logs" = "探测日志"; +"Progress bars fill as you consume quota (instead of showing remaining)." = "进度条随用量消耗而填充(而非显示剩余量)。"; +"Provider" = "提供商"; +"Providers" = "提供商"; +"Quota usage" = "配额用量"; +"Quit CodexBar" = "退出 CodexBar"; +"Random (default)" = "随机(默认)"; +"Reads local usage logs. Shows today + last 30 days cost in the menu." = "读取本地使用日志。在菜单中显示今天及最近30天的费用。"; +"Refresh" = "刷新"; +"Refresh cadence" = "刷新频率"; +"Remote" = "远程"; +"Remove" = "移除"; +"Remove Codex account?" = "移除 Codex 账户?"; +"Remove \\(account.email) from CodexBar? Its managed Codex home will be deleted." = "从 CodexBar 中移除 \\(account.email)?其托管的 Codex 主目录将被删除。"; +"Remove \\(email) from CodexBar? Its managed Codex home will be deleted." = "从 CodexBar 中移除 \\(email)?其托管的 Codex 主目录将被删除。"; +"Remove selected account" = "移除所选账户"; +"Replace critter bars with provider branding icons and a percentage." = "将动态条替换为提供商品牌图标和百分比。"; +"Replay selected animation" = "重放选中的动画"; +"Requires authentication via GitHub Device Flow." = "需要通过 GitHub 设备流程认证。"; +"Resets: \\(reset)" = "重置:\\(reset)"; +"Rolling five-hour limit" = "滚动 5 小时限制"; +"Search hourly" = "每小时搜索"; +"Secondary (\\(label))" = "次要(\\(label))"; +"Secondary (\\(metadata.weeklyLabel))" = "次要(\\(metadata.weeklyLabel))"; +"Select a provider" = "选择一个提供商"; +"Select the IDE to monitor" = "选择要监控的 IDE"; +"Session quota notifications" = "会话配额通知"; +"Session tokens" = "会话 tokens"; +"Settings" = "设置"; +"Show Codex Credits and Claude Extra usage sections in the menu." = "在菜单中显示 Codex 积分和 Claude 额外用量部分。"; +"Show Debug Settings" = "显示调试设置"; +"Show all token accounts" = "显示所有 token 账户"; +"Show cost summary" = "显示费用摘要"; +"Show credits + extra usage" = "显示积分 + 额外用量"; "Show details" = "显示详情"; -"Show most-used provider" = ""; -"Show peak hours indicator" = ""; -"Show provider icons in the switcher (otherwise show a weekly progress line)." = ""; -"Show reset time as clock" = ""; -"Show usage as used" = ""; -"Show whether Claude is in peak usage hours." = ""; -"Sign in via button below" = ""; -"Skip teardown between probes (debug-only)." = ""; -"Stack token accounts in the menu (otherwise show an account switcher bar)." = ""; -"Start at Login" = ""; -"Status" = ""; -"Store Claude sessionKey cookies or OAuth access tokens." = ""; -"Store multiple Abacus AI Cookie headers." = ""; -"Store multiple Augment Cookie headers." = ""; -"Store multiple Cursor Cookie headers." = ""; -"Store multiple Factory Cookie headers." = ""; -"Store multiple MiniMax Cookie headers." = ""; -"Store multiple Mistral Cookie headers." = ""; -"Store multiple Ollama Cookie headers." = ""; -"Store multiple OpenCode Cookie headers." = ""; -"Store multiple OpenCode Go Cookie headers." = ""; -"Stored in the CodexBar config file." = ""; -"Stored in ~/.codexbar/config.json. " = ""; -"Stored in ~/.codexbar/config.json. Generate one at kimi-k2.ai." = ""; -"Stored in ~/.codexbar/config.json. Paste the key from the Synthetic dashboard." = ""; -"Stored in ~/.codexbar/config.json. Paste your Coding Plan API key from Model Studio." = ""; -"Stored in ~/.codexbar/config.json. Paste your MiniMax API key." = ""; -"Stored in ~/.codexbar/config.json. You can also provide KILO_API_KEY or " = ""; -"Stores local Codex usage history (8 weeks) to personalize Pace predictions." = ""; -"Subscription Utilization" = ""; +"Show most-used provider" = "显示用量最高的提供商"; +"Show peak hours indicator" = "显示高峰时段指示器"; +"Show provider icons in the switcher (otherwise show a weekly progress line)." = "在切换器中显示提供商图标(否则显示每周进度线)。"; +"Show reset time as clock" = "将重置时间显示为时钟"; +"Show usage as used" = "显示已使用用量"; +"Show whether Claude is in peak usage hours." = "显示 Claude 是否处于高峰用量时段。"; +"Sign in via button below" = "通过下方按钮登录"; +"Skip teardown between probes (debug-only)." = "探测之间跳过拆卸(仅限调试)。"; +"Stack token accounts in the menu (otherwise show an account switcher bar)." = "在菜单中堆叠 token 账户(否则显示账户切换栏)。"; +"Start at Login" = "开机启动"; +"Status" = "状态"; +"State" = "状态"; +"Store Claude sessionKey cookies or OAuth access tokens." = "存储 Claude sessionKey Cookie 或 OAuth access tokens。"; +"Store multiple Abacus AI Cookie headers." = "存储多个 Abacus AI Cookie 标头。"; +"Store multiple Augment Cookie headers." = "存储多个 Augment Cookie 标头。"; +"Store multiple Cursor Cookie headers." = "存储多个 Cursor Cookie 标头。"; +"Store multiple Factory Cookie headers." = "存储多个 Factory Cookie 标头。"; +"Store multiple MiniMax Cookie headers." = "存储多个 MiniMax Cookie 标头。"; +"Store multiple Mistral Cookie headers." = "存储多个 Mistral Cookie 标头。"; +"Store multiple Ollama Cookie headers." = "存储多个 Ollama Cookie 标头。"; +"Store multiple OpenCode Cookie headers." = "存储多个 OpenCode Cookie 标头。"; +"Store multiple OpenCode Go Cookie headers." = "存储多个 OpenCode Go Cookie 标头。"; +"Stored in the CodexBar config file." = "存储在 CodexBar 配置文件中。"; +"Stored in ~/.codexbar/config.json. " = "存储在 ~/.codexbar/config.json。 "; +"Stored in ~/.codexbar/config.json. Generate one at kimi-k2.ai." = "存储在 ~/.codexbar/config.json。请在 kimi-k2.ai 生成。"; +"Stored in ~/.codexbar/config.json. Paste the key from the Synthetic dashboard." = "存储在 ~/.codexbar/config.json。粘贴 Synthetic 仪表板中的密钥。"; +"Stored in ~/.codexbar/config.json. Paste your Coding Plan API key from Model Studio." = "存储在 ~/.codexbar/config.json。粘贴来自 Model Studio 的 Coding Plan API 密钥。"; +"Stored in ~/.codexbar/config.json. Paste your MiniMax API key." = "存储在 ~/.codexbar/config.json。粘贴你的 MiniMax API 密钥。"; +"Stored in ~/.codexbar/config.json. You can also provide KILO_API_KEY or " = "存储在 ~/.codexbar/config.json。也可以提供 KILO_API_KEY 或 "; +"Stores local Codex usage history (8 weeks) to personalize Pace predictions." = "存储本地 Codex 用量历史(8 周)以个性化节奏预测。"; +"Subscription Utilization" = "订阅使用率"; "Surprise me" = "给我惊喜"; -"Switcher shows icons" = ""; -"Symlink CodexBarCLI to /usr/local/bin and /opt/homebrew/bin as codexbar." = ""; -"System" = ""; -"Temporarily shows the loading animation after the next refresh." = ""; -"Tertiary (\\(label))" = ""; -"Tertiary (\\(tertiaryTitle))" = ""; -"The default Codex account on this Mac." = ""; -"Toggle" = ""; -"Toggle subtitle" = ""; -"Token" = ""; -"Trigger the menu bar menu from anywhere." = ""; -"True" = ""; -"Twitter" = ""; -"Unsupported" = ""; -"Update Channel" = ""; -"Updated" = ""; -"Updates unavailable in this build." = ""; -"Usage" = ""; -"Usage breakdown" = ""; -"Usage history (30 days)" = ""; -"Usage source" = ""; -"Use BigModel for the China mainland endpoints (open.bigmodel.cn)." = ""; -"Use a single menu bar icon with a provider switcher." = ""; -"Use international or China mainland console gateways for quota fetches." = ""; -"Version" = ""; -"Version \\(self.versionString)" = ""; -"Version \\(version)" = ""; -"Version \\(versionString)" = ""; +"Switcher shows icons" = "切换器显示图标"; +"Symlink CodexBarCLI to /usr/local/bin and /opt/homebrew/bin as codexbar." = "将 CodexBarCLI 符号链接到 /usr/local/bin 和 /opt/homebrew/bin 作为 codexbar。"; +"System" = "系统"; +"Temporarily shows the loading animation after the next refresh." = "下次刷新后临时显示加载动画。"; +"Tertiary (\\(label))" = "第三(\\(label))"; +"Tertiary (\\(tertiaryTitle))" = "第三(\\(tertiaryTitle))"; +"The default Codex account on this Mac." = "此 Mac 上的默认 Codex 账户。"; +"Toggle" = "切换"; +"Toggle subtitle" = "切换副标题"; +"Token" = "Token"; +"Trigger the menu bar menu from anywhere." = "从任意位置触发菜单栏菜单。"; +"True" = "真"; +"Twitter" = "Twitter"; +"Unsupported" = "不支持"; +"Update Channel" = "更新频道"; +"Updated" = "已更新"; +"Updates unavailable in this build." = "此构建中更新不可用。"; +"Usage" = "用量"; +"Usage breakdown" = "用量明细"; +"Usage history (30 days)" = "用量历史(30 天)"; +"Usage source" = "用量来源"; +"Use BigModel for the China mainland endpoints (open.bigmodel.cn)." = "对中国大陆端点使用 BigModel(open.bigmodel.cn)。"; +"Use a single menu bar icon with a provider switcher." = "使用单个菜单栏图标并带提供商切换器。"; +"Use international or China mainland console gateways for quota fetches." = "使用国际或中国大陆控制台网关获取配额。"; +"Version" = "版本"; +"Version \\(self.versionString)" = "版本 \\(self.versionString)"; +"Version \\(version)" = "版本 \\(version)"; +"Version \\(versionString)" = "版本 \\(versionString)"; "Vertex AI Login" = "Vertex AI 登录"; -"Wait for the current managed Codex login to finish before adding another account." = ""; +"Wait for the current managed Codex login to finish before adding another account." = "请等待当前托管 Codex 登录完成后再添加其他账户。"; "Waiting for Authentication..." = "等待认证…"; -"Website" = ""; -"Weekly limit confetti" = ""; -"Weekly token limit" = ""; -"Weekly usage" = ""; +"Website" = "网站"; +"Weekly limit confetti" = "每周限制彩纸"; +"Weekly token limit" = "每周 token 限制"; +"Weekly usage" = "每周用量"; "Weekly usage unavailable for this account." = "此账户的每周用量不可用。"; -"Window: \\(window)" = ""; +"Window: \\(window)" = "窗口:\\(window)"; "Write logs to \\(self.fileLogPath) for debugging." = "将日志写入 \\(self.fileLogPath) 用于调试。"; -"Yes" = ""; -"\\(detail.modelCode): \\(usage)" = ""; -"\\(name): \\(truncated)" = ""; -"\\(name): \\(updated) · 30d \\(cost)" = ""; -"\\(name): fetching…\\(elapsed)" = ""; -"\\(name): last attempt \\(when)" = ""; -"\\(name): no data yet" = ""; -"\\(name): unsupported" = ""; -"all browsers" = ""; -"available again." = ""; +"Yes" = "是"; +"\\(detail.modelCode): \\(usage)" = "\\(detail.modelCode):\\(usage)"; +"\\(name): \\(truncated)" = "\\(name):\\(truncated)"; +"\\(name): \\(updated) · 30d \\(cost)" = "\\(name):\\(updated) · 30天 \\(cost)"; +"\\(name): fetching…\\(elapsed)" = "\\(name):获取中…\\(elapsed)"; +"\\(name): last attempt \\(when)" = "\\(name):上次尝试 \\(when)"; +"\\(name): no data yet" = "\\(name):暂无数据"; +"\\(name): unsupported" = "\\(name):不支持"; +"all browsers" = "所有浏览器"; +"available again." = "再次可用。"; "built_format" = "构建于 %@"; -"copilot_complete_in_browser" = ""; +"copilot_complete_in_browser" = "请在浏览器中完成登录。"; "copilot_device_code" = "设备代码已复制到剪贴板:%1$@ 请在以下地址验证:%2$@"; -"copilot_device_code_copied" = ""; -"copilot_verify_at" = ""; +"copilot_device_code_copied" = "设备代码已复制。"; +"copilot_verify_at" = "请在 %@ 验证"; "copilot_waiting_text" = "请在浏览器中完成登录。 完成后此窗口将自动关闭。"; -"copilot_window_closes_auto" = ""; -"cost_status_error" = "%@:%@"; +"copilot_window_closes_auto" = "登录完成后此窗口将自动关闭。"; +"cost_status_error" = "%1$@:%2$@"; "cost_status_fetching" = "%1$@:获取中…%2$@"; "cost_status_last_attempt" = "%1$@:上次尝试 %2$@"; "cost_status_no_data" = "%@:暂无数据"; @@ -375,7 +380,7 @@ "extra_usage_format" = "额外用量:%1$@ / %2$@"; "jetbrains_detected_generate" = "检测到:%@。使用一次 AI 助手以生成配额数据,然后刷新 CodexBar。"; "jetbrains_detected_select" = "检测到:%@。在设置中选择您偏好的 IDE,然后刷新 CodexBar。"; -"last_fetch_failed_with_provider" = "上次获取 %1$@ 失败:"; +"last_fetch_failed_with_provider" = "上次获取 %@ 失败:"; "last_spend" = "上次消耗:%@"; "mcp_model_usage" = "%1$@:%2$@"; "mcp_resets" = "重置:%@"; @@ -387,7 +392,7 @@ "multiple_workspaces_found" = "CodexBar 发现了 %1$@ 的多个工作区。请选择要添加的工作区。"; "off_peak" = "非高峰"; "off_peak_peak_in" = "非高峰 · %@ 后进入高峰"; -"ory_session_…=…; csrftoken=…" = ""; +"ory_session_…=…; csrftoken=…" = "ory_session_…=…; csrftoken=…"; "overview_choose_providers" = "选择最多 %1$@ 个提供商"; "peak_ends_in" = "高峰 · %@ 后结束"; "remove_account_message" = "从 CodexBar 中移除 %@?其托管的 Codex 主目录将被删除。"; @@ -400,8 +405,8 @@ 4. 设置项目:gcloud config set project PROJECT_ID 是否现在打开终端?"; -"workspaceID is set but only opencode and opencodego support workspaceID." = ""; -"© 2026 Peter Steinberger. MIT License." = ""; +"workspaceID is set but only opencode and opencodego support workspaceID." = "已设置 workspaceID,但只有 opencode 和 opencodego 支持 workspaceID。"; +"© 2026 Peter Steinberger. MIT License." = "© 2026 Peter Steinberger. MIT 许可证。"; /* General Pane */ "section_system" = "系统"; @@ -410,9 +415,9 @@ "language_title" = "语言"; "language_subtitle" = "更改显示语言。需要重启应用才能完全生效。"; "language_system" = "跟随系统"; -"language_english" = "English"; +"language_english" = "英语"; "language_chinese_simplified" = "简体中文"; -"language_portuguese_brazilian" = "Português (Brasil)"; +"language_portuguese_brazilian" = "葡萄牙语(巴西)"; "start_at_login_title" = "开机启动"; "start_at_login_subtitle" = "启动 Mac 时自动打开 CodexBar。"; "show_cost_summary" = "显示费用摘要"; @@ -500,8 +505,8 @@ "show_provider_changelog_links_subtitle" = "在菜单中为支持的 CLI 提供商添加发布说明链接。"; "show_credits_extra_usage_title" = "显示积分 + 额外用量"; "show_credits_extra_usage_subtitle" = "在菜单中显示 Codex 积分和 Claude 额外用量部分。"; -"show_all_token_accounts_title" = "显示所有令牌账户"; -"show_all_token_accounts_subtitle" = "在菜单中堆叠令牌账户(否则显示账户切换栏)。"; +"show_all_token_accounts_title" = "显示所有 token 账户"; +"show_all_token_accounts_subtitle" = "在菜单中堆叠 token 账户(否则显示账户切换栏)。"; "multi_account_layout_title" = "多账户布局"; "multi_account_layout_subtitle" = "选择分段账户切换或堆叠账户卡片。"; "multi_account_layout_segmented" = "分段"; @@ -535,7 +540,7 @@ "disable_keychain_access_subtitle" = "启用时阻止任何钥匙串访问。"; /* About Pane */ -"about_tagline" = "愿你的令牌永不耗尽——时刻关注智能体限制。"; +"about_tagline" = "愿你的 tokens 永不耗尽——时刻关注智能体用量限制。"; "link_github" = "GitHub"; "link_website" = "网站"; "link_twitter" = "Twitter"; @@ -544,7 +549,7 @@ "update_channel" = "更新频道"; "check_for_updates" = "检查更新…"; "updates_unavailable" = "此构建中更新不可用。"; -"copyright" = "© 2026 Peter Steinberger. MIT License."; +"copyright" = "© 2026 Peter Steinberger. MIT 许可证。"; /* Debug Pane */ "section_logging" = "日志"; @@ -638,6 +643,296 @@ /* Additional keys */ "not_found" = "未找到"; +/* Runtime menu and provider details */ +"%.0f%% used" = "已用 %.0f%%"; +"%1$@ days of usage data across %2$@ services" = "%1$@ 天用量数据,覆盖 %2$@ 项服务"; +"%1$@: %2$@" = "%1$@:%2$@"; +"%1$@: %2$@ / %3$@" = "%1$@:%2$@ / %3$@"; +"%1$@: %2$@ credits" = "%1$@:%2$@ 积分"; +"%1$@: %2$@ · %3$@ tokens" = "%1$@:%2$@ · %3$@ tokens"; +"%1$@: %2$@%% used" = "%1$@:已用 %2$@%%"; +"%@ days of cost data" = "%@ 天费用数据"; +"%@ days of credits data" = "%@ 天积分数据"; +"%@ login successful" = "%@ 登录成功"; +"%@ more items" = "还有 %@ 个项目"; +"%@ percent" = "%@%%"; +"%@ unreadable item(s) skipped" = "已跳过 %@ 个不可读项目"; +"%@ utilization samples" = "%@ 个使用率样本"; +"Account Added" = "账户已添加"; +"Account: %@" = "账户:%@"; +"Activity: %@" = "活动:%@"; +"Add Account" = "添加账户"; +"Add accounts via GitHub OAuth Device Flow on the selected host." = "通过所选主机上的 GitHub OAuth 设备流程添加账户。"; +"Antigravity login failed" = "Antigravity 登录失败"; +"Antigravity login timed out" = "Antigravity 登录超时"; +"Auth source" = "认证来源"; +"Auto uses the local IDE API first, then Google OAuth when the IDE is closed." = "自动模式优先使用本地 IDE API;IDE 关闭时使用 Google OAuth。"; +"Automatic imports Chrome browser cookies from Xiaomi MiMo." = "自动从 Chrome 导入小米 MiMo 的浏览器 Cookie。"; +"Automatic imports Windsurf session data from Chromium browser localStorage." = "自动从 Chromium 浏览器 localStorage 导入 Windsurf 会话数据。"; +"Automatically imports browser session cookies." = "自动导入浏览器会话 Cookie。"; +"Balance: %@" = "余额:%@"; +"Buy Credits" = "购买积分"; +"Choose the Moonshot/Kimi API host for international or China mainland accounts." = "选择 Moonshot/Kimi API 主机,用于国际或中国大陆账户。"; +"Cleanup ideas" = "清理建议"; +"Cookie: …\n\nor paste a cURL capture from the Abacus AI dashboard" = "Cookie: …\n\n或粘贴来自 Abacus AI 仪表板的 cURL 捕获"; +"Cookie: …\n\nor paste the __Secure-next-auth.session-token value" = "Cookie: …\n\n或粘贴 __Secure-next-auth.session-token 的值"; +"Cookie: …\n\nor paste the kimi-auth token value" = "Cookie: …\n\n或粘贴 kimi-auth token 值"; +"Copied" = "已复制"; +"Copy error" = "复制错误"; +"Copy path" = "复制路径"; +"Cost history chart" = "费用历史图表"; +"Could Not Identify GitHub Account" = "无法识别 GitHub 账户"; +"CodexBar will ask macOS Keychain for %@ so it can fetch %@. Click OK to continue." = "CodexBar 会向 macOS 钥匙串请求%@,以便获取%@。点击“确定”继续。"; +"CodexBar will ask macOS Keychain for “%@” so it can decrypt browser cookies and authenticate your account. Click OK to continue." = "CodexBar 会向 macOS 钥匙串请求“%@”,以便解密浏览器 Cookie 并认证你的账户。点击“确定”继续。"; +"Codex dashboard extras" = "Codex 仪表板扩展信息"; +"Could not open browser for Antigravity" = "无法为 Antigravity 打开浏览器"; +"Credits history chart" = "积分历史图表"; +"Credits remaining" = "剩余积分"; +"Cursor Login" = "Cursor 登录"; +"Disabled — no recent data" = "已禁用 - 无近期数据"; +"Drag to reorder" = "拖动以重新排序"; +"Enterprise host" = "企业主机"; +"Extra usage" = "额外用量"; +"Extra usage spent" = "额外用量消耗"; +"Full in ~%.0f regens" = "约 %.0f 次恢复后满额"; +"Full in ~1 regen" = "约 1 次恢复后满额"; +"GitHub login succeeded, but CodexBar could not verify which account it belongs to. Please try again." = "GitHub 登录已成功,但 CodexBar 无法确认它属于哪个账户。请重试。"; +"Google OAuth" = "Google OAuth"; +"Hover a bar for details" = "悬停在柱形图上查看详情"; +"Keychain Access Required" = "需要访问钥匙串"; +"Label" = "标签"; +"Last 30 days: %1$@ · %2$@ tokens" = "最近 30 天:%1$@ · %2$@ tokens"; +"Last 30 days: %@" = "最近 30 天:%@"; +"Login Failed" = "登录失败"; +"Near full" = "接近满额"; +"No %@ utilization data yet." = "尚无%@使用率数据。"; +"No cost history data available." = "暂无可用的费用历史数据。"; +"No credits history data available." = "暂无可用的积分历史数据。"; +"No data" = "无数据"; +"No local data found" = "未找到本地数据"; +"No system account" = "无系统账户"; +"No usage breakdown data available." = "暂无可用的用量明细数据。"; +"No usage configured." = "未配置用量。"; +"No usage yet" = "尚无用量"; +"No utilization data yet." = "尚无使用率数据。"; +"Not fetched yet" = "尚未获取"; +"Oasis-Token" = "Oasis-Token"; +"Oasis-Token=…" = "Oasis-Token=…"; +"Open Codebuff Dashboard" = "打开 Codebuff 仪表板"; +"Open Command Code Settings" = "打开 Command Code 设置"; +"Open Crof dashboard" = "打开 Crof 仪表板"; +"Open Manus" = "打开 Manus"; +"Open MiMo Balance" = "打开 MiMo 余额"; +"Open Moonshot Console" = "打开 Moonshot 控制台"; +"Open StepFun Platform" = "打开 StepFun 平台"; +"Open Token Plan" = "打开 Token 套餐"; +"Open Volcengine Ark Console" = "打开火山引擎 Ark 控制台"; +"Open billing" = "打开账单"; +"Optional. Enter your GitHub Enterprise host, for example octocorp.ghe.com. Leave blank for github.com." = "可选。输入你的 GitHub Enterprise 主机,例如 octocorp.ghe.com。留空则使用 github.com。"; +"Paste the Oasis-Token from a logged-in browser session on platform.stepfun.com." = "粘贴 platform.stepfun.com 已登录浏览器会话中的 Oasis-Token。"; +"Paste the Cookie header from a request to admin.mistral.ai. Must contain an ory_session_* cookie." = "粘贴来自 admin.mistral.ai 请求的 Cookie 标头。必须包含 ory_session_* Cookie。"; +"Plan utilization chart" = "计划使用率图表"; +"Plan: %@" = "计划:%@"; +"Quota: %@ / %@" = "配额:%@ / %@"; +"Refreshing" = "刷新中"; +"Regenerates %@" = "%@ 后恢复"; +"Reload" = "重新加载"; +"Reorder" = "重新排序"; +"Show local disk usage in menus. Scans known provider-owned paths in the background." = "在菜单中显示本地磁盘用量。后台扫描已知的提供商数据路径。"; +"Show provider storage usage" = "显示提供商存储用量"; +"Simulated error text" = "模拟错误文本"; +"Storage" = "存储"; +"Storage:" = "存储:"; +"Source" = "来源"; +"Limits background chatgpt.com refreshes to reduce battery and network usage. Dashboard extras may stay stale until you refresh them manually." = "限制后台 chatgpt.com 刷新以降低电池和网络消耗。仪表板扩展信息可能会保持旧数据,直到你手动刷新。"; +"This month" = "本月"; +"Today: %1$@ · %2$@ tokens" = "今天:%1$@ · %2$@ tokens"; +"Today: %@" = "今天:%@"; +"Token Refreshed" = "Token 已刷新"; +"Total (30d): %@ credits" = "总计(30 天):%@ 积分"; +"Total (30d): %@" = "总计(30 天):%@"; +"Total: %@" = "总计:%@"; +"Unavailable" = "不可用"; +"Usage breakdown chart" = "用量明细图表"; +"Usage used" = "已用用量"; +"Uses username + password to login and obtain an Oasis-Token automatically." = "使用用户名和密码登录并自动获取 Oasis-Token。"; +"Windsurf session JSON bundle" = "Windsurf 会话 JSON 包"; +"Workspace ID" = "工作区 ID"; +"You can return to the app; authentication finished." = "认证已完成,可以返回应用。"; +"Your StepFun platform password. Used to login and obtain a session token." = "你的 StepFun 平台密码。用于登录并获取会话 token。"; +"after next regen" = "下次恢复后"; +"ark-..." = "ark-..."; +"cb_..." = "cb_..."; +"cpk-..." = "cpk-..."; +"crof_..." = "crof_..."; +"github.com" = "github.com"; +"kilo_..." = "kilo_..."; +"left" = "剩余"; +"just now" = "刚刚"; +"not detected" = "未检测到"; +"session_id=...\n\nor paste just the session_id value" = "session_id=...\n\n或仅粘贴 session_id 值"; +"sk-..." = "sk-..."; +"sk-or-v1-..." = "sk-or-v1-..."; +"used" = "已用"; +"used after next regen" = "下次恢复后已用"; +"usage" = "用量"; +"user@example.com" = "user@example.com"; +"the Claude Code OAuth token" = "Claude Code OAuth token"; +"your Amp cookie header" = "你的 Amp Cookie 标头"; +"your Augment cookie header" = "你的 Augment Cookie 标头"; +"your Claude cookie header" = "你的 Claude Cookie 标头"; +"your Claude usage" = "你的 Claude 用量"; +"your Cursor cookie header" = "你的 Cursor Cookie 标头"; +"your Factory cookie header" = "你的 Factory Cookie 标头"; +"your GitHub Copilot token" = "你的 GitHub Copilot token"; +"your Kimi K2 API key" = "你的 Kimi K2 API 密钥"; +"your Kimi auth token" = "你的 Kimi auth token"; +"your MiniMax API token" = "你的 MiniMax API token"; +"your MiniMax cookie header" = "你的 MiniMax Cookie 标头"; +"your OpenAI cookie header" = "你的 OpenAI Cookie 标头"; +"your OpenCode cookie header" = "你的 OpenCode Cookie 标头"; +"your Synthetic API key" = "你的 Synthetic API 密钥"; +"your z.ai API token" = "你的 z.ai API token"; +"wk-..." = "wk-..."; +"wrk_…" = "wrk_…"; +"~/Library/Application Support/JetBrains/IntelliJIdea2024.3" = "~/Library/Application Support/JetBrains/IntelliJIdea2024.3"; +"StepFun platform account (phone number or email)." = "StepFun 平台账户(手机号或电子邮件)。"; +"Stored in ~/.codexbar/config.json." = "存储在 ~/.codexbar/config.json。"; +"Stored in ~/.codexbar/config.json. Get your API key from the Volcengine Ark console." = "存储在 ~/.codexbar/config.json。请从火山引擎 Ark 控制台获取 API 密钥。"; +"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." = "存储在 ~/.codexbar/config.json。请从 openrouter.ai/settings/keys 获取密钥,并在那里设置密钥消费上限以启用 API 密钥配额跟踪。"; +"Stored in ~/.codexbar/config.json. In Warp, open Settings > Platform > API Keys, then create one." = "存储在 ~/.codexbar/config.json。在 Warp 中打开 Settings > Platform > API Keys,然后创建一个密钥。"; +"Stored in ~/.codexbar/config.json. You can also provide CODEBUFF_API_KEY or let CodexBar read ~/.config/manicode/credentials.json (created by `codebuff login`)." = "存储在 ~/.codexbar/config.json。也可以提供 CODEBUFF_API_KEY,或让 CodexBar 读取 ~/.config/manicode/credentials.json(由 `codebuff login` 创建)。"; +"Stored in ~/.codexbar/config.json. You can also provide CROF_API_KEY." = "存储在 ~/.codexbar/config.json。也可以提供 CROF_API_KEY。"; +"Stored in ~/.codexbar/config.json. You can also provide KILO_API_KEY or ~/.local/share/kilo/auth.json (kilo.access)." = "存储在 ~/.codexbar/config.json。也可以提供 KILO_API_KEY 或 ~/.local/share/kilo/auth.json(kilo.access)。"; +"Stored in ~/.codexbar/config.json. You can also provide OPENAI_API_KEY." = "存储在 ~/.codexbar/config.json。也可以提供 OPENAI_API_KEY。"; +"The browser login did not complete in time. Try Antigravity login again." = "浏览器登录未及时完成。请重新尝试 Antigravity 登录。"; +"Username" = "用户名"; +"Password" = "密码"; +"Secure" = "安全"; +"Secure subtitle" = "安全副标题"; +"Subtitle" = "副标题"; +"Managed Codex login did not complete. Try again after finishing the browser login flow." = "托管 Codex 登录未完成。请完成浏览器登录流程后重试。"; +"Codex login completed, but no account email was available. Try again after confirming the account is fully signed in." = "Codex 登录已完成,但没有可用的账户电子邮件。确认账户已完全登录后请重试。"; +"CodexBar found multiple workspaces, but no workspace was selected." = "CodexBar 找到了多个工作区,但未选择任何工作区。"; +"CodexBar could not read managed account storage. Recover the store before adding another account." = "CodexBar 无法读取托管账户存储。请先恢复该存储,再添加其他账户。"; +"No output captured." = "未捕获到输出。"; +"That account is no longer available in CodexBar. Refresh the account list and try again." = "该账户在 CodexBar 中不再可用。请刷新账户列表后重试。"; +"CodexBar could not find saved auth for that account. Re-authenticate it and try again." = "CodexBar 找不到该账户保存的认证信息。请重新认证后重试。"; +"CodexBar could not read saved auth for that account. Re-authenticate it and try again." = "CodexBar 无法读取该账户保存的认证信息。请重新认证后重试。"; +"CodexBar could not read the current system account on this Mac." = "CodexBar 无法读取此 Mac 上的当前系统账户。"; +"CodexBar could not safely preserve the current system account before switching." = "CodexBar 在切换前无法安全保留当前系统账户。"; +"CodexBar can't replace a system account that is signed in with an API key only setup." = "CodexBar 无法替换仅使用 API 密钥登录的系统账户。"; +"CodexBar found another managed account that already uses the current system account. Resolve the duplicate account before switching." = "CodexBar 发现另一个托管账户已使用当前系统账户。请先解决重复账户再切换。"; +"CodexBar could not save the current system account before switching." = "CodexBar 在切换前无法保存当前系统账户。"; +"CodexBar could not update managed account storage." = "CodexBar 无法更新托管账户存储。"; +"CodexBar could not replace the live Codex auth on this Mac." = "CodexBar 无法替换此 Mac 上的实时 Codex 认证。"; +"%1$@ · %2$@ · %3$@ credits" = "%1$@ · %2$@ · %3$@ 点数"; +"%1$@/%2$@ left" = "剩余 %1$@/%2$@"; +"%@ left" = "剩余 %@"; +"%@ · %@" = "%@ · %@"; +"%d%% in deficit" = "超额 %d%%"; +"%d%% in reserve" = "余量 %d%%"; +"5-hour" = "5 小时"; +"API" = "API"; +"About CodexBar" = "关于 CodexBar"; +"Add Account..." = "添加账户..."; +"Auto" = "自动"; +"Beta" = "Beta"; +"BigModel CN (open.bigmodel.cn)" = "BigModel CN (open.bigmodel.cn)"; +"CLI" = "CLI"; +"CLI (PTY)" = "CLI (PTY)"; +"CLI (RPC/PTY)" = "CLI (RPC/PTY)"; +"Cached: %@ • %@" = "已缓存:%@ • %@"; +"Changelog" = "更新日志"; +"China (api.moonshot.cn)" = "中国(api.moonshot.cn)"; +"China mainland (bailian.console.aliyun.com)" = "中国大陆(bailian.console.aliyun.com)"; +"China mainland (platform.minimaxi.com)" = "中国大陆(platform.minimaxi.com)"; +"Critical" = "严重"; +"Cylon" = "赛昂"; +"Extra usage balance: %@" = "额外用量余额:%@"; +"Global (api.z.ai)" = "全球(api.z.ai)"; +"Global (platform.minimax.io)" = "全球(platform.minimax.io)"; +"Info" = "信息"; +"International (api.moonshot.ai)" = "国际(api.moonshot.ai)"; +"International (modelstudio.console.alibabacloud.com)" = "国际(modelstudio.console.alibabacloud.com)"; +"Keychain access is disabled in Advanced, so browser cookie import is unavailable. %@" = "高级设置中已禁用钥匙串访问,因此无法导入浏览器 Cookie。%@"; +"Knight Rider" = "霹雳游侠"; +"Lasts until reset" = "可持续到重置"; +"Local (SQLite cache)" = "本地(SQLite 缓存)"; +"Local IDE API" = "本地 IDE API"; +"Monthly" = "每月"; +"OAuth API" = "OAuth API"; +"On pace" = "节奏正常"; +"Open Droid in Browser..." = "在浏览器中打开 Droid..."; +"Outside-In" = "由外向内"; +"Pace: %@" = "节奏:%@"; +"Pace: %@ · %@" = "节奏:%@ · %@"; +"Pulse" = "脉冲"; +"Quit" = "退出"; +"Race" = "竞速"; +"Receive only stable, production-ready releases." = "仅接收稳定的生产就绪版本。"; +"Receive stable releases plus beta previews." = "接收稳定版本以及 Beta 预览版。"; +"Refresh Session" = "刷新会话"; +"Resets" = "重置"; +"Resets %@" = "重置:%@"; +"Runs out in %@" = "将在 %@ 用尽"; +"Runs out now" = "现在用尽"; +"Session" = "会话"; +"Settings..." = "设置..."; +"Sonnet" = "Sonnet"; +"Stable" = "稳定版"; +"Status Page" = "状态页"; +"Switch Account..." = "切换账户..."; +"System Account" = "系统账户"; +"Trace" = "跟踪"; +"Unbraid (logo → bars)" = "拆分(Logo → 条形)"; +"Updated %@" = "更新于 %@"; +"Updated just now" = "刚刚更新"; +"Update ready, restart now?" = "更新已就绪,要现在重启吗?"; +"Usage Dashboard" = "用量仪表板"; +"Verbose" = "详细"; +"Warning" = "警告"; +"Web API (IndexedDB)" = "Web API(IndexedDB)"; +"Web API (cookies)" = "Web API(Cookie)"; +"Weekly" = "每周"; +"in %1$d d %2$d h" = "%1$d天%2$d小时后"; +"in %1$d h %2$d m" = "%1$d小时%2$d分钟后"; +"in %d d" = "%d天后"; +"in %d h" = "%d小时后"; +"in %d m" = "%d分钟后"; +"now" = "现在"; +"source_api" = "API"; +"source_auto" = "自动"; +"source_cached" = "缓存"; +"source_claude_cli" = "Claude CLI"; +"source_cli" = "CLI"; +"source_codex_cli" = "Codex CLI"; +"source_local" = "本地"; +"source_login" = "登录"; +"source_manual" = "手动"; +"source_manual_cookie_header" = "手动 Cookie 标头"; +"source_oauth" = "OAuth"; +"source_oauth_api" = "OAuth API"; +"source_off" = "关闭"; +"source_openai_web" = "OpenAI Web"; +"source_web" = "Web"; +"source_windsurf_web" = "Windsurf Web"; +"tomorrow, %@" = "明天 %@"; +"≈ %d%% run-out risk" = "约 %d%% 用尽风险"; + /* Cost estimation */ "cost_header_estimated" = "费用(估算)"; "cost_estimate_hint" = "根据本地日志估算 · 可能与账单不同"; +"Est. total (30d): %@" = "估算总计(30 天):%@"; +"Estimated from local Codex logs for the selected account." = "根据所选账户的本地 Codex 日志估算。"; +"Estimated from local Claude logs at API rates; token totals include cache read/write tokens and may differ from Claude Code /status." = "根据本地 Claude 日志按 API 费率估算;token 总数包含缓存读写 token,可能与 Claude Code /status 不同。"; +"Estimated from local logs · may differ from your bill" = "根据本地日志估算 · 可能与账单不同"; +"Latest billing day" = "最近账单日"; +"Latest billing day (%@)" = "最近账单日(%@)"; +"Reported by AWS Cost Explorer; daily billing data can lag." = "由 AWS Cost Explorer 报告;每日账单数据可能有延迟。"; +"Could not open Cursor login in your browser." = "无法在浏览器中打开 Cursor 登录。"; +"Sign in to cursor.com in your browser, then refresh Cursor in CodexBar." = "请在浏览器中登录 cursor.com,然后在 CodexBar 中刷新 Cursor。"; +"Timed out waiting for Cursor login. %@" = "等待 Cursor 登录超时。%@"; +"Timed out waiting for Cursor login. %@ Last error: %@" = "等待 Cursor 登录超时。%@ 上次错误:%@"; +"Org ID (optional)" = "组织 ID(可选)"; +"Optional organization ID for accounts linked to multiple Anthropic organizations." = "适用于关联多个 Anthropic 组织的账户,可填写可选组织 ID。"; diff --git a/Sources/CodexBar/StatusItemController+Actions.swift b/Sources/CodexBar/StatusItemController+Actions.swift index 07a151210..06485a079 100644 --- a/Sources/CodexBar/StatusItemController+Actions.swift +++ b/Sources/CodexBar/StatusItemController+Actions.swift @@ -495,8 +495,8 @@ extension StatusItemController: StatusItemMenuPersistentActionDelegate { func presentLoginAlert(title: String, message: String) { let alert = NSAlert() - alert.messageText = title - alert.informativeText = message + alert.messageText = L(title) + alert.informativeText = L(message) alert.alertStyle = .warning alert.runModal() } @@ -512,8 +512,8 @@ extension StatusItemController: StatusItemMenuPersistentActionDelegate { func postLoginNotification(for provider: UsageProvider) { let name = ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName - let title = "\(name) login successful" - let body = "You can return to the app; authentication finished." + let title = String(format: L("%@ login successful"), name) + let body = L("You can return to the app; authentication finished.") AppNotifications.shared.post(idPrefix: "login-\(provider.rawValue)", title: title, body: body) } diff --git a/Sources/CodexBar/StatusItemController+HostedSubmenus.swift b/Sources/CodexBar/StatusItemController+HostedSubmenus.swift index ed2f770ce..ea10412e1 100644 --- a/Sources/CodexBar/StatusItemController+HostedSubmenus.swift +++ b/Sources/CodexBar/StatusItemController+HostedSubmenus.swift @@ -92,7 +92,7 @@ extension StatusItemController { guard !didHydrate else { return } - let unavailableItem = NSMenuItem(title: "No data available", action: nil, keyEquivalent: "") + let unavailableItem = NSMenuItem(title: L("No data available"), action: nil, keyEquivalent: "") unavailableItem.isEnabled = false unavailableItem.representedObject = chartID unavailableItem.toolTip = placeholder.toolTip diff --git a/Sources/CodexBar/StatusItemController+Menu.swift b/Sources/CodexBar/StatusItemController+Menu.swift index acddb311e..eb24afcee 100644 --- a/Sources/CodexBar/StatusItemController+Menu.swift +++ b/Sources/CodexBar/StatusItemController+Menu.swift @@ -720,30 +720,35 @@ extension StatusItemController { for entry in section.entries { switch entry { case let .text(text, style): + let localizedText = L(text) if style == .secondary { - menu.addItem(self.makeWrappedSecondaryTextItem(text: text, width: width)) + menu.addItem(self.makeWrappedSecondaryTextItem(text: localizedText, width: width)) continue } - let item = NSMenuItem(title: text, action: nil, keyEquivalent: "") + let item = NSMenuItem(title: localizedText, action: nil, keyEquivalent: "") item.isEnabled = false if style == .headline { let font = NSFont.systemFont(ofSize: NSFont.systemFontSize, weight: .semibold) - item.attributedTitle = NSAttributedString(string: text, attributes: [.font: font]) + item.attributedTitle = NSAttributedString(string: localizedText, attributes: [.font: font]) } else if style == .secondary { let font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize) item.attributedTitle = NSAttributedString( - string: text, + string: localizedText, attributes: [.font: font, .foregroundColor: NSColor.secondaryLabelColor]) } menu.addItem(item) case let .action(title, action): + let localizedTitle = L(title) if case .refresh = action { - menu.addItem(self.makePersistentMenuActionItem(title: title, action: action, width: width)) + menu.addItem(self.makePersistentMenuActionItem( + title: localizedTitle, + action: action, + width: width)) continue } let (selector, represented) = self.selector(for: action) - let item = NSMenuItem(title: title, action: selector, keyEquivalent: "") + let item = NSMenuItem(title: localizedTitle, action: selector, keyEquivalent: "") item.target = self item.representedObject = represented if let shortcut = self.shortcut(for: action) { @@ -761,16 +766,17 @@ extension StatusItemController { let subtitle = self.switchAccountSubtitle(for: targetProvider) { item.isEnabled = false - self.applySubtitle(subtitle, to: item, title: title) + self.applySubtitle(subtitle, to: item, title: localizedTitle) } else if case .addCodexAccount = action, let subtitle = self.codexAddAccountSubtitle() { item.isEnabled = false - self.applySubtitle(subtitle, to: item, title: title) + self.applySubtitle(subtitle, to: item, title: localizedTitle) } menu.addItem(item) case let .submenu(title, systemImageName, submenuItems): - let item = NSMenuItem(title: title, action: nil, keyEquivalent: "") + let localizedTitle = L(title) + let item = NSMenuItem(title: localizedTitle, action: nil, keyEquivalent: "") if let systemImageName, let image = NSImage(systemSymbolName: systemImageName, accessibilityDescription: nil) { @@ -778,10 +784,10 @@ extension StatusItemController { image.size = NSSize(width: 16, height: 16) item.image = image } - let submenu = NSMenu(title: title) + let submenu = NSMenu(title: localizedTitle) submenu.autoenablesItems = false for submenuItem in submenuItems { - let child = NSMenuItem(title: submenuItem.title, action: nil, keyEquivalent: "") + let child = NSMenuItem(title: L(submenuItem.title), action: nil, keyEquivalent: "") child.state = submenuItem.isChecked ? .on : .off child.isEnabled = submenuItem.isEnabled if let action = submenuItem.action { @@ -1422,7 +1428,10 @@ extension StatusItemController { } private func makeBuyCreditsItem() -> NSMenuItem { - let item = NSMenuItem(title: "Buy Credits...", action: #selector(self.openCreditsPurchase), keyEquivalent: "") + let item = NSMenuItem( + title: L("Buy Credits..."), + action: #selector(self.openCreditsPurchase), + keyEquivalent: "") item.target = self if let image = NSImage(systemSymbolName: "plus.circle", accessibilityDescription: nil) { image.isTemplate = true @@ -1436,7 +1445,7 @@ extension StatusItemController { private func addCreditsHistorySubmenu(to menu: NSMenu) -> Bool { guard let submenu = self.makeCreditsHistorySubmenu(width: self.renderedMenuWidth(for: menu)) else { return false } - let item = NSMenuItem(title: "Credits history", action: nil, keyEquivalent: "") + let item = NSMenuItem(title: L("Credits history"), action: nil, keyEquivalent: "") item.isEnabled = true item.submenu = submenu menu.addItem(item) @@ -1447,7 +1456,7 @@ extension StatusItemController { private func addUsageBreakdownSubmenu(to menu: NSMenu) -> Bool { guard let submenu = self.makeUsageBreakdownSubmenu(width: self.renderedMenuWidth(for: menu)) else { return false } - let item = NSMenuItem(title: "Usage breakdown", action: nil, keyEquivalent: "") + let item = NSMenuItem(title: L("Usage breakdown"), action: nil, keyEquivalent: "") item.isEnabled = true item.submenu = submenu menu.addItem(item) @@ -1458,7 +1467,7 @@ extension StatusItemController { private func addCostHistorySubmenu(to menu: NSMenu, provider: UsageProvider) -> Bool { guard let submenu = self.makeCostHistorySubmenu(provider: provider, width: self.renderedMenuWidth(for: menu)) else { return false } - let item = NSMenuItem(title: "Usage history (30 days)", action: nil, keyEquivalent: "") + let item = NSMenuItem(title: L("Usage history (30 days)"), action: nil, keyEquivalent: "") item.isEnabled = true item.submenu = submenu menu.addItem(item) @@ -1486,7 +1495,7 @@ extension StatusItemController { let submenu = NSMenu() submenu.delegate = self - let titleItem = NSMenuItem(title: "MCP details", action: nil, keyEquivalent: "") + let titleItem = NSMenuItem(title: L("MCP details"), action: nil, keyEquivalent: "") titleItem.isEnabled = false submenu.addItem(titleItem) @@ -1497,8 +1506,8 @@ extension StatusItemController { } if let resetTime = timeLimit.nextResetTime { let reset = self.settings.resetTimeDisplayStyle == .absolute - ? UsageFormatter.resetDescription(from: resetTime) - : UsageFormatter.resetCountdownDescription(from: resetTime) + ? LocalizedUsageText.resetDescription(from: resetTime) + : LocalizedUsageText.resetCountdownDescription(from: resetTime) let item = NSMenuItem(title: String(format: L("mcp_resets"), reset), action: nil, keyEquivalent: "") item.isEnabled = false submenu.addItem(item) @@ -1589,10 +1598,7 @@ extension StatusItemController { @objc private func selectOverviewProvider(_ sender: NSMenuItem) { guard let represented = sender.representedObject as? String, - represented.hasPrefix(Self.overviewRowIdentifierPrefix) - else { - return - } + represented.hasPrefix(Self.overviewRowIdentifierPrefix) else { return } let rawProvider = String(represented.dropFirst(Self.overviewRowIdentifierPrefix.count)) guard let provider = UsageProvider(rawValue: rawProvider), let menu = sender.menu diff --git a/Sources/CodexBar/StatusItemController+MenuTypes.swift b/Sources/CodexBar/StatusItemController+MenuTypes.swift index 88e5fa305..a8cbc2ba4 100644 --- a/Sources/CodexBar/StatusItemController+MenuTypes.swift +++ b/Sources/CodexBar/StatusItemController+MenuTypes.swift @@ -34,7 +34,7 @@ struct OverviewMenuCardRowView: View { } if let storageText { HStack(alignment: .firstTextBaseline, spacing: 4) { - Text("Storage:") + Text(L("Storage:")) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) Text(storageText) diff --git a/Sources/CodexBar/StatusItemController+SwitcherViews.swift b/Sources/CodexBar/StatusItemController+SwitcherViews.swift index b24a932d4..a3d27f807 100644 --- a/Sources/CodexBar/StatusItemController+SwitcherViews.swift +++ b/Sources/CodexBar/StatusItemController+SwitcherViews.swift @@ -70,7 +70,7 @@ final class ProviderSwitcherView: NSView { Segment( selection: .overview, image: overviewIcon, - title: "Overview"), + title: L("Overview")), at: 0) } self.segments = segments diff --git a/Sources/CodexBar/StatusItemController+UsageHistoryMenu.swift b/Sources/CodexBar/StatusItemController+UsageHistoryMenu.swift index 0259f26ab..f3d1ea45a 100644 --- a/Sources/CodexBar/StatusItemController+UsageHistoryMenu.swift +++ b/Sources/CodexBar/StatusItemController+UsageHistoryMenu.swift @@ -14,7 +14,7 @@ extension StatusItemController { guard let submenu = self.makeUsageHistorySubmenu(provider: provider, width: width) else { return false } let item = self.makeMenuCardItem( HStack(spacing: 0) { - Text("Subscription Utilization") + Text(L("Subscription Utilization")) .font(.system(size: NSFont.menuFont(ofSize: 0).pointSize)) .lineLimit(1) .frame(maxWidth: .infinity, alignment: .leading) diff --git a/Sources/CodexBar/StorageBreakdownMenuView.swift b/Sources/CodexBar/StorageBreakdownMenuView.swift index dbdbfda72..e9e628daa 100644 --- a/Sources/CodexBar/StorageBreakdownMenuView.swift +++ b/Sources/CodexBar/StorageBreakdownMenuView.swift @@ -10,7 +10,7 @@ struct StorageMenuCardSectionView: View { var body: some View { VStack(alignment: .leading, spacing: 6) { - Text("Storage") + Text(L("Storage")) .font(.body) .fontWeight(.medium) Text(self.storageText) @@ -67,16 +67,16 @@ struct StorageBreakdownMenuView: View { private var content: some View { VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 3) { - Text("Storage") + Text(L("Storage")) .font(.body) .fontWeight(.medium) - Text("Total: \(UsageFormatter.byteCountString(self.footprint.totalBytes))") + Text(String(format: L("Total: %@"), UsageFormatter.byteCountString(self.footprint.totalBytes))) .font(.caption) .foregroundStyle(.secondary) } if self.visibleComponents.isEmpty { - Text("No local data found") + Text(L("No local data found")) .font(.footnote) .foregroundStyle(.secondary) } else { @@ -88,7 +88,9 @@ struct StorageBreakdownMenuView: View { } if self.footprint.components.count > self.visibleComponents.count { - Text("\(self.footprint.components.count - self.visibleComponents.count) more items") + Text(String( + format: L("%@ more items"), + "\(self.footprint.components.count - self.visibleComponents.count)")) .font(.caption) .foregroundStyle(.secondary) } @@ -96,7 +98,7 @@ struct StorageBreakdownMenuView: View { Divider() .padding(.vertical, 2) VStack(alignment: .leading, spacing: 8) { - Text("Cleanup ideas") + Text(L("Cleanup ideas")) .font(.body) .fontWeight(.medium) ForEach(self.cleanupRecommendations) { recommendation in @@ -105,7 +107,9 @@ struct StorageBreakdownMenuView: View { } } if !self.footprint.unreadablePaths.isEmpty { - Text("\(self.footprint.unreadablePaths.count) unreadable item(s) skipped") + Text(String( + format: L("%@ unreadable item(s) skipped"), + "\(self.footprint.unreadablePaths.count)")) .font(.caption) .foregroundStyle(.secondary) } @@ -210,8 +214,8 @@ struct StoragePathCopyButton: View { .contentShape(Rectangle()) } .buttonStyle(.plain) - .help(self.didCopy ? "Copied" : "Copy path") - .accessibilityLabel(self.didCopy ? "Copied" : "Copy path") + .help(self.didCopy ? L("Copied") : L("Copy path")) + .accessibilityLabel(self.didCopy ? L("Copied") : L("Copy path")) } static func copyToPasteboard(_ path: String) { diff --git a/Sources/CodexBar/UsageBreakdownChartMenuView.swift b/Sources/CodexBar/UsageBreakdownChartMenuView.swift index ad53befa3..ab12ede0d 100644 --- a/Sources/CodexBar/UsageBreakdownChartMenuView.swift +++ b/Sources/CodexBar/UsageBreakdownChartMenuView.swift @@ -31,10 +31,10 @@ struct UsageBreakdownChartMenuView: View { let model = Self.makeModel(from: self.breakdown) VStack(alignment: .leading, spacing: 10) { if model.points.isEmpty { - Text("No usage breakdown data.") + Text(L("No usage breakdown data.")) .font(.footnote) .foregroundStyle(.secondary) - .accessibilityLabel("No usage breakdown data available.") + .accessibilityLabel(L("No usage breakdown data available.")) } else { Chart { ForEach(model.points) { point in @@ -65,11 +65,14 @@ struct UsageBreakdownChartMenuView: View { } .chartLegend(.hidden) .frame(height: 130) - .accessibilityLabel("Usage breakdown chart") + .accessibilityLabel(L("Usage breakdown chart")) .accessibilityValue( model.points.isEmpty - ? "No data" - : "\(model.points.count) days of usage data across \(model.services.count) services") + ? L("No data") + : String( + format: L("%1$@ days of usage data across %2$@ services"), + "\(model.points.count)", + "\(model.services.count)")) .chartOverlay { proxy in GeometryReader { geo in ZStack(alignment: .topLeading) { @@ -368,17 +371,17 @@ struct UsageBreakdownChartMenuView: View { let day = model.breakdownByDayKey[key], let date = Self.dateFromDayKey(key) else { - return ("Hover a bar for details", nil) + return (L("Hover a bar for details"), nil) } let dayLabel = date.formatted(.dateTime.month(.abbreviated).day()) let total = day.totalCreditsUsed.formatted(.number.precision(.fractionLength(0...2))) if day.services.isEmpty { - return ("\(dayLabel): \(total)", nil) + return (String(format: L("%1$@: %2$@"), dayLabel, total), nil) } if day.services.count <= 1, let first = day.services.first { let used = first.creditsUsed.formatted(.number.precision(.fractionLength(0...2))) - return ("\(dayLabel): \(used)", first.service) + return (String(format: L("%1$@: %2$@"), dayLabel, used), first.service) } let services = day.services @@ -390,6 +393,6 @@ struct UsageBreakdownChartMenuView: View { .map { "\($0.service) \($0.creditsUsed.formatted(.number.precision(.fractionLength(0...2))))" } .joined(separator: " · ") - return ("\(dayLabel): \(total)", services) + return (String(format: L("%1$@: %2$@"), dayLabel, total), services) } } diff --git a/Sources/CodexBar/UsagePaceText.swift b/Sources/CodexBar/UsagePaceText.swift index f24f1d029..53b7fbf6f 100644 --- a/Sources/CodexBar/UsagePaceText.swift +++ b/Sources/CodexBar/UsagePaceText.swift @@ -12,9 +12,9 @@ enum UsagePaceText { static func weeklySummary(pace: UsagePace, now: Date = .init()) -> String { let detail = self.weeklyDetail(pace: pace, now: now) if let rightLabel = detail.rightLabel { - return "Pace: \(detail.leftLabel) · \(rightLabel)" + return String(format: L("Pace: %@ · %@"), detail.leftLabel, rightLabel) } - return "Pace: \(detail.leftLabel)" + return String(format: L("Pace: %@"), detail.leftLabel) } static func weeklyDetail(pace: UsagePace, now: Date = .init()) -> WeeklyDetail { @@ -29,38 +29,38 @@ enum UsagePaceText { let deltaValue = Int(abs(pace.deltaPercent).rounded()) switch pace.stage { case .onTrack: - return "On pace" + return L("On pace") case .slightlyAhead, .ahead, .farAhead: - return "\(deltaValue)% in deficit" + return String(format: L("%d%% in deficit"), deltaValue) case .slightlyBehind, .behind, .farBehind: - return "\(deltaValue)% in reserve" + return String(format: L("%d%% in reserve"), deltaValue) } } private static func detailRightLabel(for pace: UsagePace, now: Date) -> String? { let etaLabel: String? if pace.willLastToReset { - etaLabel = "Lasts until reset" + etaLabel = L("Lasts until reset") } else if let etaSeconds = pace.etaSeconds { let etaText = Self.durationText(seconds: etaSeconds, now: now) - etaLabel = etaText == "now" ? "Runs out now" : "Runs out in \(etaText)" + etaLabel = etaText == L("now") ? L("Runs out now") : String(format: L("Runs out in %@"), etaText) } else { etaLabel = nil } guard let runOutProbability = pace.runOutProbability else { return etaLabel } let roundedRisk = self.roundedRiskPercent(runOutProbability) - let riskLabel = "≈ \(roundedRisk)% run-out risk" + let riskLabel = String(format: L("≈ %d%% run-out risk"), roundedRisk) if let etaLabel { - return "\(etaLabel) · \(riskLabel)" + return String(format: L("%@ · %@"), etaLabel, riskLabel) } return riskLabel } private static func durationText(seconds: TimeInterval, now: Date) -> String { let date = now.addingTimeInterval(seconds) - let countdown = UsageFormatter.resetCountdownDescription(from: date, now: now) - if countdown == "now" { return "now" } + let countdown = LocalizedUsageText.resetCountdownDescription(from: date, now: now) + if countdown == L("now") { return L("now") } if countdown.hasPrefix("in ") { return String(countdown.dropFirst(3)) } return countdown } @@ -91,8 +91,8 @@ enum UsagePaceText { static func sessionSummary(provider: UsageProvider, window: RateWindow, now: Date = .init()) -> String? { guard let detail = sessionDetail(provider: provider, window: window, now: now) else { return nil } if let rightLabel = detail.rightLabel { - return "Pace: \(detail.leftLabel) · \(rightLabel)" + return String(format: L("Pace: %@ · %@"), detail.leftLabel, rightLabel) } - return "Pace: \(detail.leftLabel)" + return String(format: L("Pace: %@"), detail.leftLabel) } } diff --git a/Sources/CodexBar/UsageProgressBar.swift b/Sources/CodexBar/UsageProgressBar.swift index fbb344e32..a2b379bcf 100644 --- a/Sources/CodexBar/UsageProgressBar.swift +++ b/Sources/CodexBar/UsageProgressBar.swift @@ -118,7 +118,7 @@ struct UsageProgressBar: View { } .frame(height: 6) .accessibilityLabel(self.accessibilityLabel) - .accessibilityValue("\(Int(self.clamped)) percent") + .accessibilityValue(String(format: L("%@ percent"), "\(Int(self.clamped))")) } private static func paceStripePaths(size: CGSize, scale: CGFloat) -> (punched: Path, center: Path) { diff --git a/Tests/CodexBarTests/LocalizationBundleTests.swift b/Tests/CodexBarTests/LocalizationBundleTests.swift index ed15b66ab..81342d895 100644 --- a/Tests/CodexBarTests/LocalizationBundleTests.swift +++ b/Tests/CodexBarTests/LocalizationBundleTests.swift @@ -24,6 +24,25 @@ struct LocalizationBundleTests { #expect(bundle.bundleURL == fixture.appBundle.bundleURL) } + @Test + func `bundled localization files do not contain empty values`() throws { + let bundle = codexBarLocalizationResourceBundle() + let lprojURLs = try FileManager.default.contentsOfDirectory( + at: bundle.bundleURL, + includingPropertiesForKeys: nil) + .filter { $0.pathExtension == "lproj" } + + for lprojURL in lprojURLs { + let stringsURL = lprojURL.appendingPathComponent("Localizable.strings") + let data = try Data(contentsOf: stringsURL) + let plist = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) + let strings = try #require(plist as? [String: String]) + let emptyKeys = strings.keys.filter { strings[$0]?.isEmpty == true } + + #expect(emptyKeys.isEmpty) + } + } + @Test func `packaged app resolves raw copied localization resources from main bundle`() throws { let fixture = try Self.makeAppBundleFixture( diff --git a/Tests/CodexBarTests/MenuCardSubtitleTests.swift b/Tests/CodexBarTests/MenuCardSubtitleTests.swift index 8407c1c10..94c19f9bf 100644 --- a/Tests/CodexBarTests/MenuCardSubtitleTests.swift +++ b/Tests/CodexBarTests/MenuCardSubtitleTests.swift @@ -44,6 +44,6 @@ struct MenuCardSubtitleTests { hidePersonalInfo: false, now: now)) - #expect(model.subtitleText == UsageFormatter.updatedString(from: updatedAt, now: now)) + #expect(model.subtitleText == LocalizedUsageText.updatedString(from: updatedAt, now: now)) } } diff --git a/Tests/CodexBarTests/ProviderSettingsDescriptorTests.swift b/Tests/CodexBarTests/ProviderSettingsDescriptorTests.swift index 514bde3df..ebbc45f24 100644 --- a/Tests/CodexBarTests/ProviderSettingsDescriptorTests.swift +++ b/Tests/CodexBarTests/ProviderSettingsDescriptorTests.swift @@ -406,6 +406,6 @@ struct ProviderSettingsDescriptorTests { .presentation(context: context) .detailLine(context) - #expect(detailLine == store.sourceLabel(for: .alibaba)) + #expect(detailLine == LocalizedProviderText.sourceLabel(store.sourceLabel(for: .alibaba))) } } diff --git a/Tests/CodexBarTests/StatusMenuSwitcherRefreshTests.swift b/Tests/CodexBarTests/StatusMenuSwitcherRefreshTests.swift index b6c722563..5d1a32918 100644 --- a/Tests/CodexBarTests/StatusMenuSwitcherRefreshTests.swift +++ b/Tests/CodexBarTests/StatusMenuSwitcherRefreshTests.swift @@ -76,11 +76,13 @@ struct StatusMenuSwitcherRefreshTests { let suite = "StatusMenuSwitcherRefreshTests-\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! defaults.removePersistentDomain(forName: suite) - return SettingsStore( + let settings = SettingsStore( userDefaults: defaults, configStore: testConfigStore(suiteName: suite), zaiTokenStore: NoopZaiTokenStore(), syntheticTokenStore: NoopSyntheticTokenStore()) + settings.providerDetectionCompleted = true + return settings } private static func enableCodexAndClaude(_ settings: SettingsStore) {