From caca846d1ae7532281cc541755086cb7efa3a5cb Mon Sep 17 00:00:00 2001 From: Shun Min Chang Date: Wed, 27 May 2026 09:21:05 +0800 Subject: [PATCH 1/6] feat(localization): localize popup panels Localize popup menu labels, dashboard notes, chart accessibility, and storage copy helpers across supported languages. Add popup localization regression tests and isolate test language resolution from the user's persisted app language. Verification: make check; plutil -lint Sources/CodexBar/Resources/*/Localizable.strings; git diff --check; duplicate-key scan; swift test. --- .../CodexBar/CostHistoryChartMenuView.swift | 7 +- .../CreditsHistoryChartMenuView.swift | 11 +- .../InlineUsageDashboardContent.swift | 128 ++++++++++-------- Sources/CodexBar/Localization.swift | 25 +++- Sources/CodexBar/MenuCardView+Costs.swift | 16 +-- Sources/CodexBar/MenuCardView+Kiro.swift | 9 +- .../CodexBar/MenuCardView+ModelHelpers.swift | 4 +- Sources/CodexBar/MenuCardView.swift | 56 ++++---- Sources/CodexBar/MenuContent.swift | 6 +- Sources/CodexBar/MenuDescriptor.swift | 61 +++++---- .../PlanUtilizationHistoryChartMenuView.swift | 7 +- .../Resources/ca.lproj/Localizable.strings | 109 ++++++++++++++- .../Resources/en.lproj/Localizable.strings | 72 +++++++++- .../Resources/es.lproj/Localizable.strings | 109 ++++++++++++++- .../Resources/pt-BR.lproj/Localizable.strings | 109 ++++++++++++++- .../zh-Hans.lproj/Localizable.strings | 70 ++++++++++ .../zh-Hant.lproj/Localizable.strings | 121 +++++++++++++++++ .../CodexBar/StatusItemController+Menu.swift | 2 +- ...tusItemController+ZaiHourlyChartMenu.swift | 2 +- .../CodexBar/StorageBreakdownMenuView.swift | 4 +- .../UsageBreakdownChartMenuView.swift | 9 +- .../PopupLocalizationTests.swift | 113 ++++++++++++++++ 22 files changed, 897 insertions(+), 153 deletions(-) create mode 100644 Tests/CodexBarTests/PopupLocalizationTests.swift diff --git a/Sources/CodexBar/CostHistoryChartMenuView.swift b/Sources/CodexBar/CostHistoryChartMenuView.swift index 5ff7bbc284..0ca5f6d6e3 100644 --- a/Sources/CodexBar/CostHistoryChartMenuView.swift +++ b/Sources/CodexBar/CostHistoryChartMenuView.swift @@ -99,8 +99,11 @@ 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("%d days of cost data"), model.points.count)) .chartOverlay { proxy in GeometryReader { geo in ZStack(alignment: .topLeading) { diff --git a/Sources/CodexBar/CreditsHistoryChartMenuView.swift b/Sources/CodexBar/CreditsHistoryChartMenuView.swift index b3ae60e897..a73e255055 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,11 @@ 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("%d days of credits data"), model.points.count)) .chartOverlay { proxy in GeometryReader { geo in ZStack(alignment: .topLeading) { diff --git a/Sources/CodexBar/InlineUsageDashboardContent.swift b/Sources/CodexBar/InlineUsageDashboardContent.swift index e1ec45057d..0437288b50 100644 --- a/Sources/CodexBar/InlineUsageDashboardContent.swift +++ b/Sources/CodexBar/InlineUsageDashboardContent.swift @@ -47,8 +47,10 @@ extension UsageMenuCardView.Model { let billing = input.snapshot?.minimaxUsage?.billingSummary { return [ - "Today: \(UsageFormatter.tokenCountString(billing.todayTokens)) tokens", - "Last 30 days: \(UsageFormatter.tokenCountString(billing.last30DaysTokens)) tokens", + String(format: L("Today: %@ tokens"), UsageFormatter.tokenCountString(billing.todayTokens)), + String( + format: L("Last 30 days: %@ tokens"), + UsageFormatter.tokenCountString(billing.last30DaysTokens)), ] } @@ -59,15 +61,18 @@ extension UsageMenuCardView.Model { let symbol = usage.currency == "CNY" ? "¥" : "$" let todayCostStr = usage.todayCost.map { "\(symbol)\(String(format: "%.4f", max(0, $0)))" } ?? "—" return [ - "Today: \(todayCostStr) · \(UsageFormatter.tokenCountString(usage.todayTokens)) tokens", - "This month: \(UsageFormatter.tokenCountString(usage.currentMonthTokens)) tokens", + String( + format: L("Today: %@ · %@ tokens"), + todayCostStr, + UsageFormatter.tokenCountString(usage.todayTokens)), + String(format: L("This month: %@ tokens"), UsageFormatter.tokenCountString(usage.currentMonthTokens)), ] } if input.provider == .ollama, input.snapshot?.identity?.loginMethod == "API key" { - return ["API key verified. Ollama does not expose Cloud quota limits through the API."] + return [L("API key verified. Ollama does not expose Cloud quota limits through the API.")] } return nil @@ -78,19 +83,22 @@ extension UsageMenuCardView.Model { let seven = usage.last7Days let thirty = usage.last30Days let historyLabel = usage.historyWindowLabel - let todayNote = "Today: \(UsageFormatter.usdString(today.costUSD)) · " + - "\(UsageFormatter.tokenCountString(today.totalTokens)) tokens" + let todayNote = String( + format: L("Today: %@ · %@ tokens"), + UsageFormatter.usdString(today.costUSD), + UsageFormatter.tokenCountString(today.totalTokens)) let sevenDayNote = "7d: \(UsageFormatter.usdString(seven.costUSD)) · " + - "\(UsageFormatter.tokenCountString(seven.requests)) requests" - let thirtyDayNote = "\(historyLabel): \(UsageFormatter.tokenCountString(thirty.totalTokens)) tokens · " + - "\(UsageFormatter.tokenCountString(thirty.requests)) requests" + "\(UsageFormatter.tokenCountString(seven.requests)) \(L("requests"))" + let thirtyDayNote = + "\(historyLabel): \(UsageFormatter.tokenCountString(thirty.totalTokens)) \(L("tokens")) · " + + "\(UsageFormatter.tokenCountString(thirty.requests)) \(L("requests"))" var notes: [String] = [ todayNote, sevenDayNote, thirtyDayNote, ] if let topModel = usage.topModels.first { - notes.append("Top model: \(topModel.name)") + notes.append("\(L("Top model")): \(topModel.name)") } return notes } @@ -201,7 +209,7 @@ extension UsageMenuCardView.Model { details.append("\(L("Top model")): \(Self.shortModelName(topModel))") } if let requestCount = snapshot.last30DaysRequests { - details.append("\(requestHistoryTitle): \(UsageFormatter.tokenCountString(requestCount)) requests") + details.append("\(requestHistoryTitle): \(UsageFormatter.tokenCountString(requestCount)) \(L("requests"))") } if let hint = Self.tokenUsageHint(provider: provider) { details.append(hint) @@ -266,24 +274,24 @@ extension UsageMenuCardView.Model { accessibilityValue: "\($0.day): \(UsageFormatter.usdString($0.costUSD))") } var details = [ - "30d: \(UsageFormatter.tokenCountString(last30.totalTokens)) tokens", - "Cache read: \(UsageFormatter.tokenCountString(last30.cacheReadInputTokens)) tokens", + "30d: \(UsageFormatter.tokenCountString(last30.totalTokens)) \(L("tokens"))", + "\(L("Cache read")): \(UsageFormatter.tokenCountString(last30.cacheReadInputTokens)) \(L("tokens"))", ] if let topModel = usage.topModels.first { - details.append("Top model: \(Self.shortModelName(topModel.name))") + details.append("\(L("Top model")): \(Self.shortModelName(topModel.name))") } return InlineUsageDashboardModel( - accessibilityLabel: "Claude Admin API 30 day spend trend", + accessibilityLabel: L("Claude Admin API 30 day spend trend"), valueStyle: .currencyUSD, kpis: [ - .init(title: "Today", value: UsageFormatter.usdString(today.costUSD), emphasis: true), - .init(title: "7d spend", value: UsageFormatter.usdString(last7.costUSD), emphasis: false), + .init(title: L("Today"), value: UsageFormatter.usdString(today.costUSD), emphasis: true), + .init(title: L("7d spend"), value: UsageFormatter.usdString(last7.costUSD), emphasis: false), .init( - title: "30d spend", + title: L("30d spend"), value: UsageFormatter.usdString(last30.costUSD), emphasis: false), .init( - title: "Today tokens", + title: L("Today tokens"), value: UsageFormatter.tokenCountString(today.totalTokens), emphasis: false), ], @@ -293,9 +301,9 @@ extension UsageMenuCardView.Model { private static func openRouterInlineDashboard(_ usage: OpenRouterUsageSnapshot) -> InlineUsageDashboardModel? { let periodValues: [(String, String, Double?)] = [ - ("day", "Today", usage.keyUsageDaily), - ("week", "Week", usage.keyUsageWeekly), - ("month", "Month", usage.keyUsageMonthly), + ("day", L("Today"), usage.keyUsageDaily), + ("week", L("Week"), usage.keyUsageWeekly), + ("month", L("Month"), usage.keyUsageMonthly), ] let points = periodValues.compactMap { id, label, value -> InlineUsageDashboardModel.Point? in guard let value else { return nil } @@ -308,33 +316,33 @@ extension UsageMenuCardView.Model { guard !points.isEmpty else { return nil } var details: [String] = [] if let rate = usage.rateLimit { - details.append("Rate limit: \(rate.requests) / \(rate.interval)") + details.append(String(format: L("Rate limit: %d / %@"), rate.requests, rate.interval)) } switch usage.keyQuotaStatus { case .available: if let remaining = usage.keyRemaining { - details.append("Key remaining: \(Self.openRouterCurrencyString(remaining))") + details.append("\(L("Key remaining")): \(Self.openRouterCurrencyString(remaining))") } case .noLimitConfigured: - details.append("No limit set for the API key") + details.append(L("No limit set for the API key")) case .unavailable: - details.append("API key limit unavailable right now") + details.append(L("API key limit unavailable right now")) } return InlineUsageDashboardModel( - accessibilityLabel: "OpenRouter API key spend trend", + accessibilityLabel: L("OpenRouter API key spend trend"), valueStyle: .currencyUSD, kpis: [ - .init(title: "Balance", value: Self.openRouterCurrencyString(usage.balance), emphasis: true), + .init(title: L("Balance"), value: Self.openRouterCurrencyString(usage.balance), emphasis: true), .init( - title: "Today", + title: L("Today"), value: usage.keyUsageDaily.map(Self.openRouterCurrencyString) ?? "—", emphasis: false), .init( - title: "Week", + title: L("Week"), value: usage.keyUsageWeekly.map(Self.openRouterCurrencyString) ?? "—", emphasis: false), .init( - title: "Month", + title: L("Month"), value: usage.keyUsageMonthly.map(Self.openRouterCurrencyString) ?? "—", emphasis: false), ], @@ -353,26 +361,26 @@ extension UsageMenuCardView.Model { id: "\(index)-\(bar.label)", label: bar.label, value: Double(bar.totalTokens), - accessibilityValue: "\(bar.label): \(UsageFormatter.tokenCountString(bar.totalTokens)) tokens") + accessibilityValue: "\(bar.label): \(UsageFormatter.tokenCountString(bar.totalTokens)) \(L("tokens"))") } let topModel = Self.topZaiModel(from: bars) return InlineUsageDashboardModel( - accessibilityLabel: "z.ai hourly token trend", + accessibilityLabel: L("z.ai hourly token trend"), valueStyle: .tokens, kpis: [ - .init(title: "24h tokens", value: UsageFormatter.tokenCountString(total), emphasis: true), + .init(title: L("24h tokens"), value: UsageFormatter.tokenCountString(total), emphasis: true), .init( - title: "Latest hour", + title: L("Latest hour"), value: latest.map { UsageFormatter.tokenCountString($0.totalTokens) } ?? "—", emphasis: false), .init( - title: "Peak hour", + title: L("Peak hour"), value: peak.map { UsageFormatter.tokenCountString($0.totalTokens) } ?? "—", emphasis: false), - .init(title: "Models", value: "\(modelUsage.modelNames.count)", emphasis: false), + .init(title: L("Models"), value: "\(modelUsage.modelNames.count)", emphasis: false), ], points: points, - detailLines: topModel.map { ["Top model: \(Self.shortModelName($0))"] } ?? []) + detailLines: topModel.map { ["\(L("Top model")): \(Self.shortModelName($0))"] } ?? []) } private static func minimaxInlineDashboard(_ billing: MiniMaxBillingSummary) -> InlineUsageDashboardModel { @@ -381,36 +389,36 @@ extension UsageMenuCardView.Model { id: $0.day, label: Self.shortDayLabel($0.day), value: Double($0.tokens), - accessibilityValue: "\($0.day): \(UsageFormatter.tokenCountString($0.tokens)) tokens") + accessibilityValue: "\($0.day): \(UsageFormatter.tokenCountString($0.tokens)) \(L("tokens"))") } - var details = ["30d billing history from MiniMax web session"] + var details = [L("30d billing history from MiniMax web session")] if let topModel = billing.topModels.first { - details.append("Top model: \(Self.shortModelName(topModel.name))") + details.append("\(L("Top model")): \(Self.shortModelName(topModel.name))") } if let topMethod = billing.topMethods.first { - details.append("Top method: \(Self.shortModelName(topMethod.name))") + details.append("\(L("Top method")): \(Self.shortModelName(topMethod.name))") } if let cash = billing.last30DaysCash { - details.append("30d cash: \(Self.minimaxCashString(cash))") + details.append("\(L("30d cash")): \(Self.minimaxCashString(cash))") } return InlineUsageDashboardModel( - accessibilityLabel: "MiniMax 30 day token usage trend", + accessibilityLabel: L("MiniMax 30 day token usage trend"), valueStyle: .tokens, kpis: [ .init( - title: "Today", + title: L("Today"), value: UsageFormatter.tokenCountString(billing.todayTokens), emphasis: true), .init( - title: "30d tokens", + title: L("30d tokens"), value: UsageFormatter.tokenCountString(billing.last30DaysTokens), emphasis: false), .init( - title: "Today cash", + title: L("Today cash"), value: billing.todayCash.map(Self.minimaxCashString) ?? "—", emphasis: false), .init( - title: "Models", + title: L("Models"), value: "\(billing.topModels.count)", emphasis: false), ], @@ -425,45 +433,45 @@ extension UsageMenuCardView.Model { id: $0.date, label: Self.shortDayLabel($0.date), value: Double($0.totalTokens), - accessibilityValue: "\($0.date): \(UsageFormatter.tokenCountString($0.totalTokens)) tokens") + accessibilityValue: "\($0.date): \(UsageFormatter.tokenCountString($0.totalTokens)) \(L("tokens"))") } var details: [String] = [] if let topModel = usage.topModel { - details.append("Top model: \(Self.shortModelName(topModel))") + details.append("\(L("Top model")): \(Self.shortModelName(topModel))") } if let cacheHit = usage.categoryBreakdown.first(where: { $0.category == .promptCacheHitToken }) { - details.append("cache-hit input: \(UsageFormatter.tokenCountString(cacheHit.tokens))") + details.append("\(L("cache-hit input")): \(UsageFormatter.tokenCountString(cacheHit.tokens))") } if let cacheMiss = usage.categoryBreakdown.first(where: { $0.category == .promptCacheMissToken }) { - details.append("cache-miss input: \(UsageFormatter.tokenCountString(cacheMiss.tokens))") + details.append("\(L("cache-miss input")): \(UsageFormatter.tokenCountString(cacheMiss.tokens))") } if let output = usage.categoryBreakdown.first(where: { $0.category == .responseToken }) { - details.append("output: \(UsageFormatter.tokenCountString(output.tokens))") + details.append("\(L("output")): \(UsageFormatter.tokenCountString(output.tokens))") } - details.append("requests: \(usage.currentMonthRequestCount)") + details.append("\(L("requests")): \(usage.currentMonthRequestCount)") let todayCostStr = usage.todayCost.map { "\(symbol)\(String(format: "%.4f", max(0, $0)))" } ?? "—" let monthCostStr = usage.currentMonthCost.map { "\(symbol)\(String(format: "%.4f", max(0, $0)))" } ?? "—" let monthTokensStr = UsageFormatter.tokenCountString(usage.currentMonthTokens) return InlineUsageDashboardModel( - accessibilityLabel: "DeepSeek 30 day token usage trend", + accessibilityLabel: L("DeepSeek 30 day token usage trend"), valueStyle: .tokens, kpis: [ .init( - title: "Today", + title: L("Today"), value: "\(todayCostStr) · \(UsageFormatter.tokenCountString(usage.todayTokens))", emphasis: true), .init( - title: "This month", + title: L("This month"), value: "\(monthCostStr) · \(monthTokensStr)", emphasis: false), .init( - title: "Models", + title: L("Models"), value: usage.topModel.map { Self.shortModelName($0) } ?? "—", emphasis: false), .init( - title: "Requests", + title: L("Requests"), value: "\(usage.currentMonthRequestCount)", emphasis: false), ], diff --git a/Sources/CodexBar/Localization.swift b/Sources/CodexBar/Localization.swift index e7b36cddc7..a4c38c8ade 100644 --- a/Sources/CodexBar/Localization.swift +++ b/Sources/CodexBar/Localization.swift @@ -16,6 +16,27 @@ private func appLanguageDefaults() -> UserDefaults { return UserDefaults(suiteName: "CodexBar") ?? .standard } +private func isRunningTestsProcess() -> Bool { + let env = ProcessInfo.processInfo.environment + if env["XCTestConfigurationFilePath"] != nil { return true } + if env["TESTING_LIBRARY_VERSION"] != nil { return true } + if env["SWIFT_TESTING"] != nil { return true } + return NSClassFromString("XCTestCase") != nil +} + +private let standardAppLanguageAtProcessStart = UserDefaults.standard.string(forKey: "appLanguage") + +private func resolvedAppLanguage() -> String { + if let override = CodexBarLocalizationOverride.appLanguage { + return override + } + if isRunningTestsProcess() { + let current = UserDefaults.standard.string(forKey: "appLanguage") + return current == standardAppLanguageAtProcessStart ? "en" : current ?? "" + } + return appLanguageDefaults().string(forKey: "appLanguage") ?? "" +} + func codexBarLocalizationResourceBundle( mainBundle: Bundle = .main, bundleName: String = "CodexBar_CodexBar") -> Bundle @@ -41,7 +62,7 @@ func codexBarLocalizationResourceBundle( private func localizedBundle() -> Bundle { let resourceBundle = codexBarLocalizationResourceBundle() - let language = CodexBarLocalizationOverride.appLanguage ?? appLanguageDefaults().string(forKey: "appLanguage") ?? "" + let language = resolvedAppLanguage() if !language.isEmpty { if let bundle = lprojBundle(named: language, in: resourceBundle) { return bundle @@ -85,7 +106,7 @@ func L(_ key: String, _ arguments: CVarArg...) -> String { } func codexBarLocalizedLocale() -> Locale { - let language = appLanguageDefaults().string(forKey: "appLanguage") ?? "" + let language = resolvedAppLanguage() guard !language.isEmpty else { return .current } switch language.lowercased() { case "zh-hans": diff --git a/Sources/CodexBar/MenuCardView+Costs.swift b/Sources/CodexBar/MenuCardView+Costs.swift index 86f907267a..37973a99ab 100644 --- a/Sources/CodexBar/MenuCardView+Costs.swift +++ b/Sources/CodexBar/MenuCardView+Costs.swift @@ -151,18 +151,18 @@ extension UsageMenuCardView.Model { if provider == .factory, cost.period == "Extra usage balance" { let balance = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode) return ProviderCostSection( - title: "Extra usage", + title: L("Extra usage"), percentUsed: nil, - spendLine: "Balance: \(balance)", + spendLine: "\(L("Balance")): \(balance)", percentLine: nil) } if provider == .opencodego, cost.period == "Zen balance" { let balance = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode) return ProviderCostSection( - title: "Zen balance", + title: L("Zen balance"), percentUsed: nil, - spendLine: "Balance: \(balance)", + spendLine: "\(L("Balance")): \(balance)", percentLine: nil) } @@ -170,7 +170,7 @@ extension UsageMenuCardView.Model { let spend = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode) let periodLabel = Self.localizedPeriodLabel(cost.period ?? "Last 30 days") return ProviderCostSection( - title: "API spend", + title: L("API spend"), percentUsed: nil, spendLine: "\(periodLabel): \(spend)", percentLine: nil) @@ -183,11 +183,11 @@ extension UsageMenuCardView.Model { let title: String if cost.currencyCode == "Quota" { - title = "Quota usage" + title = L("Quota usage") used = String(format: "%.0f", cost.used) limit = String(format: "%.0f", cost.limit) } else { - title = "Extra usage" + title = L("Extra usage") used = UsageFormatter.currencyString(cost.used, currencyCode: cost.currencyCode) limit = UsageFormatter.currencyString(cost.limit, currencyCode: cost.currencyCode) } @@ -199,7 +199,7 @@ extension UsageMenuCardView.Model { title: title, percentUsed: percentUsed, spendLine: "\(periodLabel): \(used) / \(limit)", - percentLine: String(format: "%.0f%% used", min(100, max(0, percentUsed)))) + percentLine: String(format: L("%.0f%% used"), min(100, max(0, percentUsed)))) } private static func localizedPeriodLabel(_ label: String) -> String { diff --git a/Sources/CodexBar/MenuCardView+Kiro.swift b/Sources/CodexBar/MenuCardView+Kiro.swift index f7bea33821..f9f61a9c03 100644 --- a/Sources/CodexBar/MenuCardView+Kiro.swift +++ b/Sources/CodexBar/MenuCardView+Kiro.swift @@ -8,13 +8,13 @@ extension UsageMenuCardView.Model { .trimmingCharacters(in: .whitespacesAndNewlines), !authMethod.isEmpty { - notes.append("Auth: \(authMethod)") + notes.append("\(L("Auth")): \(authMethod)") } if let overages = input.snapshot?.kiroUsage?.overagesStatus? .trimmingCharacters(in: .whitespacesAndNewlines), !overages.isEmpty { - notes.append("Overages: \(overages)") + notes.append("\(L("Overages")): \(overages)") } let overagesEnabled = input.snapshot?.kiroUsage?.overagesStatus? .trimmingCharacters(in: .whitespacesAndNewlines) @@ -23,12 +23,13 @@ extension UsageMenuCardView.Model { if overagesEnabled, let overageCreditsUsed = input.snapshot?.kiroUsage?.overageCreditsUsed { - notes.append("Overage usage: \(UsageFormatter.kiroCreditNumber(overageCreditsUsed)) credits") + notes.append( + "\(L("Overage usage")): \(UsageFormatter.kiroCreditNumber(overageCreditsUsed)) \(L("credits"))") } if overagesEnabled, let estimatedOverageCostUSD = input.snapshot?.kiroUsage?.estimatedOverageCostUSD { - notes.append("Overage cost: \(UsageFormatter.usdString(estimatedOverageCostUSD))") + notes.append("\(L("Overage cost")): \(UsageFormatter.usdString(estimatedOverageCostUSD))") } return notes } diff --git a/Sources/CodexBar/MenuCardView+ModelHelpers.swift b/Sources/CodexBar/MenuCardView+ModelHelpers.swift index b4ff75f78d..4b97974ab4 100644 --- a/Sources/CodexBar/MenuCardView+ModelHelpers.swift +++ b/Sources/CodexBar/MenuCardView+ModelHelpers.swift @@ -48,11 +48,11 @@ extension UsageMenuCardView.Model { static func placeholder(input: Input) -> String? { if self.shouldShowRateLimitsUnavailablePlaceholder(input: input) { - return "Limits not available" + return L("Limits not available") } if input.snapshot == nil, !input.isRefreshing, input.lastError == nil { - return "No usage yet" + return L("No usage yet") } return nil diff --git a/Sources/CodexBar/MenuCardView.swift b/Sources/CodexBar/MenuCardView.swift index 84fad20305..b9d8c21fee 100644 --- a/Sources/CodexBar/MenuCardView.swift +++ b/Sources/CodexBar/MenuCardView.swift @@ -18,8 +18,8 @@ struct UsageMenuCardView: View { var accessibilityLabel: String { switch self { - case .left: "Usage remaining" - case .used: "Usage used" + case .left: L("Usage remaining") + case .used: L("Usage used") } } } @@ -121,7 +121,7 @@ struct UsageMenuCardView: View { static func popupMetricTitle(provider: UsageProvider, metric: Model.Metric) -> String { if provider == .openrouter, metric.id == "primary" { - return "API key limit" + return L("API key limit") } return metric.title } @@ -323,7 +323,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() { @@ -347,7 +347,7 @@ private struct ProviderCostContent: View { UsageProgressBar( percent: percentUsed, tint: self.progressColor, - accessibilityLabel: "Extra usage spent") + accessibilityLabel: L("Extra usage spent")) } HStack(alignment: .firstTextBaseline) { Text(self.section.spendLine) @@ -561,19 +561,19 @@ private struct CreditsBarContent: View { private var scaleText: String { let scale = UsageFormatter.tokenCountString(Int(Self.fullScaleTokens)) - return "\(scale) tokens" + return "\(scale) \(L("tokens"))" } 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) @@ -832,15 +832,15 @@ extension UsageMenuCardView.Model { resolvedSource == "cli", !notes.contains(where: { $0.caseInsensitiveCompare("Using CLI fallback") == .orderedSame }) { - notes.append("Using CLI fallback") + notes.append(L("Using CLI fallback")) } return notes } if input.provider == .mimo, input.snapshot != nil { return [ - "Balance updates in near-real time (up to 5 min lag)", - "Daily billing data finalizes at 07:00 UTC", + L("Balance updates in near-real time (up to 5 min lag)"), + L("Daily billing data finalizes at 07:00 UTC"), ] } @@ -983,7 +983,7 @@ extension UsageMenuCardView.Model { return (UsageFormatter.updatedString(from: updated, now: now), .info) } - return ("Not fetched yet", .info) + return (L("Not fetched yet"), .info) } private struct RedactedText { @@ -1140,7 +1140,7 @@ extension UsageMenuCardView.Model { } metrics.append(Metric( id: "code-review", - title: "Code review", + title: L("Code review"), percent: Self.clamped(percent), percentStyle: percentStyle, resetText: resetText, @@ -1158,7 +1158,7 @@ extension UsageMenuCardView.Model { snapshot: UsageSnapshot) -> (primary: String, secondary: String, tertiary: String, showsTertiary: Bool) { if input.provider == .factory, snapshot.tertiary != nil { - return ("5-hour", L("Weekly"), "Monthly", true) + return ("5-hour", L("Weekly"), L("Monthly"), true) } let primaryLabel = input.provider == .grok ? GrokProviderDescriptor.primaryLabel(window: snapshot.primary) ?? input.metadata.sessionLabel @@ -1166,7 +1166,7 @@ extension UsageMenuCardView.Model { return ( L(primaryLabel), L(input.metadata.weeklyLabel), - input.metadata.opusLabel ?? "Sonnet", + input.metadata.opusLabel.map(L) ?? L("Sonnet"), input.metadata.supportsOpus) } @@ -1211,7 +1211,7 @@ extension UsageMenuCardView.Model { { let remaining = UsageFormatter.kiroCreditNumber(kiroUsage.creditsRemaining) let total = UsageFormatter.kiroCreditNumber(kiroUsage.creditsTotal) - primaryDetailLeft = "\(remaining) of \(total) credits left" + primaryDetailLeft = String(format: L("%@ of %@ credits left"), remaining, total) } if input.provider == .alibaba || input.provider == .alibabatokenplan || input.provider == .mistral || input .provider == .manus, @@ -1333,7 +1333,7 @@ extension UsageMenuCardView.Model { let remainingText = UsageFormatter.kiroCreditNumber(remaining) let totalText = UsageFormatter.kiroCreditNumber(total) paceDetail = PaceDetail( - leftLabel: "\(remainingText) of \(totalText) bonus credits left", + leftLabel: String(format: L("%@ of %@ bonus credits left"), remainingText, totalText), rightLabel: nil, pacePercent: nil, paceOnTop: true) @@ -1460,7 +1460,7 @@ extension UsageMenuCardView.Model { percentStyle: percentStyle), Self.antigravityMetric( id: "tertiary", - title: input.metadata.opusLabel ?? "Gemini Flash", + title: input.metadata.opusLabel.map(L) ?? L("Gemini Flash"), window: snapshot.tertiary, input: input, percentStyle: percentStyle), @@ -1548,20 +1548,20 @@ extension UsageMenuCardView.Model { else { return nil } let countdown = UsageFormatter.resetCountdownDescription(from: resetsAt, now: now) - let resetText = "Regenerates \(countdown)" + 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)) } @@ -1577,21 +1577,21 @@ extension UsageMenuCardView.Model { else { return nil } let countdown = UsageFormatter.resetCountdownDescription(from: resetsAt, now: now) - let resetText = "Regenerates \(countdown)" + 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/MenuContent.swift b/Sources/CodexBar/MenuContent.swift index b9204b1fb1..264eeb1e39 100644 --- a/Sources/CodexBar/MenuContent.swift +++ b/Sources/CodexBar/MenuContent.swift @@ -183,15 +183,15 @@ struct StatusIconView: View { private var accessibilityValue: String { let snapshot = self.store.snapshot(for: self.provider) guard let snap = snapshot else { - return "No data" + return L("No data") } let remaining = IconRemainingResolver.resolvedRemaining( snapshot: snap, style: self.store.style(for: self.provider)) let primary = remaining.primary - let percent = primary.map { "\(Int($0 * 100)) percent remaining" } ?? "Unknown" + let percent = primary.map { String(format: L("%d percent remaining"), Int($0 * 100)) } ?? L("Unknown") let stale = self.store.isStale(provider: self.provider) - return stale ? "\(percent), stale data" : percent + return stale ? "\(percent), \(L("stale data"))" : percent } private var icon: NSImage { diff --git a/Sources/CodexBar/MenuDescriptor.swift b/Sources/CodexBar/MenuDescriptor.swift index 5543ad792d..20788c2cb8 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)])) } } @@ -263,7 +263,7 @@ 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("\(L("Quota")): \(used) / \(limit)", .primary)) } } if let openAIAPIUsage = snapshot.openAIAPIUsage { @@ -291,15 +291,15 @@ struct MenuDescriptor { entries.append(.text( "\(L("Today")): \(UsageFormatter.usdString(today.costUSD)) · " + - "\(UsageFormatter.tokenCountString(today.totalTokens)) tokens", + "\(UsageFormatter.tokenCountString(today.totalTokens)) \(L("tokens"))", .secondary)) entries.append(.text( "7d: \(UsageFormatter.usdString(last7.costUSD)) · " + - "\(UsageFormatter.tokenCountString(last7.requests)) requests", + "\(UsageFormatter.tokenCountString(last7.requests)) \(L("requests"))", .secondary)) entries.append(.text( "\(historyLabel): \(UsageFormatter.usdString(last30.costUSD)) · " + - "\(UsageFormatter.tokenCountString(last30.requests)) requests", + "\(UsageFormatter.tokenCountString(last30.requests)) \(L("requests"))", .secondary)) if let topModel = usage.topModels.first?.name { entries.append(.text("\(L("Top model")): \(topModel)", .secondary)) @@ -316,15 +316,15 @@ struct MenuDescriptor { entries.append(.text( "\(L("Today")): \(UsageFormatter.usdString(today.costUSD)) · " + - "\(UsageFormatter.tokenCountString(today.totalTokens)) tokens", + "\(UsageFormatter.tokenCountString(today.totalTokens)) \(L("tokens"))", .secondary)) entries.append(.text( "7d: \(UsageFormatter.usdString(last7.costUSD)) · " + - "\(UsageFormatter.tokenCountString(last7.totalTokens)) tokens", + "\(UsageFormatter.tokenCountString(last7.totalTokens)) \(L("tokens"))", .secondary)) entries.append(.text( "30d: \(UsageFormatter.usdString(last30.costUSD)) · " + - "\(UsageFormatter.tokenCountString(last30.totalTokens)) tokens", + "\(UsageFormatter.tokenCountString(last30.totalTokens)) \(L("tokens"))", .secondary)) if let topModel = usage.topModels.first?.name { entries.append(.text("\(L("Top model")): \(topModel)", .secondary)) @@ -339,10 +339,10 @@ struct MenuDescriptor { entries.append(.text("\(L("Today")): \(UsageFormatter.usdString(daily))", .secondary)) } if let weekly = usage.keyUsageWeekly { - entries.append(.text("Week: \(UsageFormatter.usdString(weekly))", .secondary)) + entries.append(.text("\(L("Week")): \(UsageFormatter.usdString(weekly))", .secondary)) } if let monthly = usage.keyUsageMonthly { - entries.append(.text("Month: \(UsageFormatter.usdString(monthly))", .secondary)) + entries.append(.text("\(L("Month")): \(UsageFormatter.usdString(monthly))", .secondary)) } } @@ -353,14 +353,14 @@ struct MenuDescriptor { let latest = usage.daily.last if let latest { entries.append(.text( - "Latest: \(usage.currencySymbol)\(String(format: "%.4f", max(0, latest.cost))) · " + - "\(UsageFormatter.tokenCountString(latest.totalTokens)) tokens", + "\(L("Latest")): \(usage.currencySymbol)\(String(format: "%.4f", max(0, latest.cost))) · " + + "\(UsageFormatter.tokenCountString(latest.totalTokens)) \(L("tokens"))", .secondary)) } let totalTokens = usage.totalInputTokens + usage.totalCachedTokens + usage.totalOutputTokens entries.append(.text( - "Month: \(usage.currencySymbol)\(String(format: "%.4f", max(0, usage.totalCost))) · " + - "\(UsageFormatter.tokenCountString(totalTokens)) tokens", + "\(L("Month")): \(usage.currencySymbol)\(String(format: "%.4f", max(0, usage.totalCost))) · " + + "\(UsageFormatter.tokenCountString(totalTokens)) \(L("tokens"))", .secondary)) if let top = Self.topMistralModel(from: usage.daily) { entries.append(.text("\(L("Top model")): \(top)", .secondary)) @@ -413,29 +413,29 @@ struct MenuDescriptor { let redactedEmail = PersonalInfoRedactor.redactEmail(emailText, isEnabled: hidePersonalInfo) if let emailText, !emailText.isEmpty { - entries.append(.text("Account: \(redactedEmail)", .secondary)) + entries.append(.text("\(L("Account")): \(redactedEmail)", .secondary)) } if provider == .kiro { if let plan = snapshot?.kiroUsage?.displayPlanName, !plan.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - entries.append(.text("Plan: \(plan)", .secondary)) + entries.append(.text("\(L("Plan")): \(plan)", .secondary)) } if let loginMethodText, !loginMethodText.isEmpty { - entries.append(.text("Auth: \(loginMethodText)", .secondary)) + entries.append(.text("\(L("Auth")): \(loginMethodText)", .secondary)) } if let overages = snapshot?.kiroUsage?.overagesStatus, !overages.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - entries.append(.text("Overages: \(overages)", .secondary)) + entries.append(.text("\(L("Overages")): \(overages)", .secondary)) } } 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("\(L("Plan")): \(AccountFormatter.plan(pass, provider: provider))", .secondary)) } for detail in kiloLogin.details { - entries.append(.text("Activity: \(detail)", .secondary)) + entries.append(.text("\(L("Activity")): \(detail)", .secondary)) } } else if let loginMethodText, !loginMethodText.isEmpty { if provider == .openrouter || provider == .mimo, @@ -448,19 +448,26 @@ 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("\(L("Balance")): \(AccountFormatter.plan(value, provider: provider))", .secondary)) } else { - entries.append(.text("Plan: \(AccountFormatter.plan(loginMethodText, provider: provider))", .secondary)) + entries.append( + .text( + "\(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("\(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( + "\(L("Plan")): \(AccountFormatter.plan(fallbackPlan, provider: provider))", + .secondary)) } } @@ -558,7 +565,7 @@ struct MenuDescriptor { entries.append(.action(L("Status Page"), .statusPage)) } if store.settings.providerChangelogLinksEnabled, metadata?.changelogURL != nil { - entries.append(.action("Changelog", .changelog)) + entries.append(.action(L("Changelog"), .changelog)) } if let statusLine = self.statusLine(for: provider, store: store) { @@ -626,7 +633,7 @@ struct MenuDescriptor { snapshot: UsageSnapshot) -> (primary: String, secondary: String, tertiary: String, showsTertiary: Bool) { if provider == .factory, snapshot.tertiary != nil { - return ("5-hour", L("Weekly"), "Monthly", true) + return ("5-hour", L("Weekly"), L("Monthly"), true) } let primaryLabel = provider == .grok ? GrokProviderDescriptor.primaryLabel(window: snapshot.primary) ?? metadata.sessionLabel @@ -634,7 +641,7 @@ struct MenuDescriptor { return ( L(primaryLabel), L(metadata.weeklyLabel), - metadata.opusLabel ?? "Sonnet", + metadata.opusLabel.map(L) ?? L("Sonnet"), metadata.supportsOpus) } diff --git a/Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift b/Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift index 169da498b2..7b5321d842 100644 --- a/Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift +++ b/Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift @@ -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("%d utilization samples"), model.points.count)) .chartOverlay { proxy in GeometryReader { geo in MouseLocationReader { location in diff --git a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings index 970c7764f7..b5f9f70d1f 100644 --- a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings @@ -72,7 +72,6 @@ "Claude login failed" = "L'inici de sessió de Claude ha fallat"; "Claude login timed out" = "L'inici de sessió de Claude ha esgotat el temps d'espera"; "Close" = "Tanca"; -"Code review" = "Revisió de codi"; "Codex CLI not found" = "No s'ha trobat la CLI de Codex"; "Codex account login already running" = "Ja hi ha un inici de sessió de compte de Codex en curs"; "Codex binary" = "Binari de Codex"; @@ -633,3 +632,111 @@ /* Cost estimation */ "cost_header_estimated" = "Cost (estimat)"; "cost_estimate_hint" = "Estimat a partir de registres locals · pot diferir de la teva factura"; + +/* Popup panels */ +"No usage configured." = "No hi ha cap ús configurat."; +"Quota" = "Quota"; +"tokens" = "tokens"; +"requests" = "sol·licituds"; +"Latest" = "Més recent"; +"Monthly" = "Mensual"; +"Sonnet" = "Sonnet"; +"Auth" = "Autenticació"; +"Overages" = "Excedents"; +"Activity" = "Activitat"; +"Copied" = "Copiat"; +"Copy error" = "Error en copiar"; +"Copy path" = "Copia el camí"; +"Extra usage spent" = "Despesa d'ús addicional"; +"Credits remaining" = "Crèdits restants"; +"Using CLI fallback" = "S'està utilitzant l'alternativa de la CLI"; +"Balance updates in near-real time (up to 5 min lag)" = "El saldo s'actualitza gairebé en temps real (fins a 5 min de retard)"; +"Daily billing data finalizes at 07:00 UTC" = "Les dades diàries de facturació es tanquen a les 07:00 UTC"; +"%@ of %@ credits left" = "Queden %@ de %@ crèdits"; +"%@ of %@ bonus credits left" = "Queden %@ de %@ crèdits de bonificació"; +"Gemini Flash" = "Gemini Flash"; +"Regenerates %@" = "Es regenera %@"; +"used after next regen" = "usat després de la propera regeneració"; +"after next regen" = "després de la propera regeneració"; +"Near full" = "Gairebé ple"; +"Full in ~1 regen" = "Ple en ~1 regeneració"; +"Full in ~%.0f regens" = "Ple en ~%.0f regeneracions"; +"Overage usage" = "Ús excedent"; +"Overage cost" = "Cost excedent"; +"credits" = "crèdits"; +"Zen balance" = "Saldo Zen"; +"API spend" = "Despesa d'API"; +"Extra usage" = "Ús addicional"; +"Quota usage" = "Ús de quota"; +"%.0f%% used" = "%.0f%% usat"; +"Usage history (today)" = "Historial d'ús (avui)"; +"Usage history (%d days)" = "Historial d'ús (%d dies)"; +"%d percent remaining" = "%d%% restant"; +"Unknown" = "Desconegut"; +"stale data" = "dades obsoletes"; +"No credits history data available." = "No hi ha dades d'historial de crèdits disponibles."; +"Credits history chart" = "Gràfic d'historial de crèdits"; +"%d days of credits data" = "%d dies de dades de crèdits"; +"Usage breakdown chart" = "Gràfic de desglossament d'ús"; +"%d days of usage data across %d services" = "%d dies de dades d'ús en %d serveis"; +"Cost history chart" = "Gràfic d'historial de costos"; +"%d days of cost data" = "%d dies de dades de costos"; +"Plan utilization chart" = "Gràfic d'utilització del pla"; +"%d utilization samples" = "%d mostres d'utilització"; +"Hourly Usage" = "Ús per hora"; +"Usage remaining" = "Ús restant"; +"Usage used" = "Ús utilitzat"; +"API key verified. Ollama does not expose Cloud quota limits through the API." = "Clau d'API verificada. Ollama no exposa els límits de quota de Cloud a través de l'API."; +"Last 30 days: %@ tokens" = "Últims 30 dies: %@ tokens"; +"7d spend" = "Despesa 7 d"; +"30d spend" = "Despesa 30 d"; +"Cache read" = "Lectura de memòria cau"; +"Claude Admin API 30 day spend trend" = "Tendència de despesa de 30 dies de Claude Admin API"; +"OpenRouter API key spend trend" = "Tendència de despesa de la clau API d'OpenRouter"; +"z.ai hourly token trend" = "Tendència horària de tokens de z.ai"; +"MiniMax 30 day token usage trend" = "Tendència d'ús de tokens de 30 dies de MiniMax"; +"Today cash" = "Efectiu d'avui"; +"DeepSeek 30 day token usage trend" = "Tendència d'ús de tokens de 30 dies de DeepSeek"; +"cache-hit input" = "entrada amb encert de memòria cau"; +"cache-miss input" = "entrada sense encert de memòria cau"; +"output" = "sortida"; +"Requests" = "Sol·licituds"; +"Reported by OpenAI Admin API organization usage." = "Informat per l'ús de l'organització a OpenAI Admin API."; +"Reported by Mistral billing usage." = "Informat per l'ús de facturació de Mistral."; +"Today" = "Avui"; +"Today tokens" = "Tokens d'avui"; +"30d cost" = "Cost 30 d"; +"30d tokens" = "Tokens 30 d"; +"Latest tokens" = "Tokens recents"; +"Top model" = "Model principal"; +"Storage" = "Emmagatzematge"; +"No data" = "Sense dades"; +"Last %d days" = "Últims %d dies"; +"%@ tokens" = "%@ tokens"; +"Latest billing day" = "Últim dia de facturació"; +"Latest billing day (%@)" = "Últim dia de facturació (%@)"; +"This week" = "Aquesta setmana"; +"This month" = "Aquest mes"; +"Week" = "Setmana"; +"Month" = "Mes"; +"Models" = "Models"; +"24h tokens" = "Tokens 24 h"; +"Latest hour" = "Última hora"; +"Peak hour" = "Hora punta"; +"Top method" = "Mètode principal"; +"30d cash" = "Efectiu 30 d"; +"30d billing history from MiniMax web session" = "Historial de facturació de 30 dies de la sessió web de MiniMax"; +"AWS Cost Explorer billing can lag." = "La facturació d'AWS Cost Explorer pot endarrerir-se."; +"Rate limit: %d / %@" = "Límit de taxa: %d / %@"; +"Key remaining" = "Restant de la clau"; +"No limit set for the API key" = "No hi ha cap límit configurat per a la clau API"; +"API key limit unavailable right now" = "El límit de la clau API no està disponible ara mateix"; +"Today: %@ · %@ tokens" = "Avui: %@ · %@ tokens"; +"Today: %@" = "Avui: %@"; +"Today: %@ tokens" = "Avui: %@ tokens"; +"This month: %@ tokens" = "Aquest mes: %@ tokens"; +"API key limit" = "Límit de la clau API"; +"Limits not available" = "Límits no disponibles"; +"No usage yet" = "Encara no hi ha ús"; +"Not fetched yet" = "Encara no obtingut"; +"Code review" = "Revisió de codi"; diff --git a/Sources/CodexBar/Resources/en.lproj/Localizable.strings b/Sources/CodexBar/Resources/en.lproj/Localizable.strings index ac3729881a..d5af3e735e 100644 --- a/Sources/CodexBar/Resources/en.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/en.lproj/Localizable.strings @@ -180,7 +180,6 @@ "No Codex accounts detected yet." = "No Codex accounts detected yet."; "No JetBrains IDE detected" = "No JetBrains IDE detected"; "No cost history data." = "No cost history data."; -"No credits history data." = "No credits history data."; "No data available" = "No data available"; "No data yet" = "No data yet"; "No enabled providers available for Overview." = "No enabled providers available for Overview."; @@ -753,6 +752,77 @@ "No overview data available." = "No overview data available."; "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."; "Login with Google" = "Login with Google"; + +/* Popup panels */ +"No usage configured." = "No usage configured."; +"Quota" = "Quota"; +"tokens" = "tokens"; +"requests" = "requests"; +"Latest" = "Latest"; +"Monthly" = "Monthly"; +"Sonnet" = "Sonnet"; +"Overages" = "Overages"; +"Activity" = "Activity"; +"Copied" = "Copied"; +"Copy error" = "Copy error"; +"Copy path" = "Copy path"; +"Extra usage spent" = "Extra usage spent"; +"Credits remaining" = "Credits remaining"; +"Using CLI fallback" = "Using CLI fallback"; +"Balance updates in near-real time (up to 5 min lag)" = "Balance updates in near-real time (up to 5 min lag)"; +"Daily billing data finalizes at 07:00 UTC" = "Daily billing data finalizes at 07:00 UTC"; +"%@ of %@ credits left" = "%@ of %@ credits left"; +"%@ of %@ bonus credits left" = "%@ of %@ bonus credits left"; +"Gemini Flash" = "Gemini Flash"; +"Regenerates %@" = "Regenerates %@"; +"used after next regen" = "used after next regen"; +"after next regen" = "after next regen"; +"Near full" = "Near full"; +"Full in ~1 regen" = "Full in ~1 regen"; +"Full in ~%.0f regens" = "Full in ~%.0f regens"; +"Overage usage" = "Overage usage"; +"Overage cost" = "Overage cost"; +"credits" = "credits"; +"Zen balance" = "Zen balance"; +"API spend" = "API spend"; +"Extra usage" = "Extra usage"; +"Quota usage" = "Quota usage"; +"%.0f%% used" = "%.0f%% used"; +"Usage history (today)" = "Usage history (today)"; +"Usage history (%d days)" = "Usage history (%d days)"; +"%d percent remaining" = "%d percent remaining"; +"Unknown" = "Unknown"; +"stale data" = "stale data"; +"No credits history data." = "No credits history data."; +"No credits history data available." = "No credits history data available."; +"Credits history chart" = "Credits history chart"; +"%d days of credits data" = "%d days of credits data"; +"Usage breakdown chart" = "Usage breakdown chart"; +"%d days of usage data across %d services" = "%d days of usage data across %d services"; +"Cost history chart" = "Cost history chart"; +"%d days of cost data" = "%d days of cost data"; +"Plan utilization chart" = "Plan utilization chart"; +"%d utilization samples" = "%d utilization samples"; +"Hourly Usage" = "Hourly Usage"; +"Usage remaining" = "Usage remaining"; +"Usage used" = "Usage used"; +"API key verified. Ollama does not expose Cloud quota limits through the API." = "API key verified. Ollama does not expose Cloud quota limits through the API."; +"Last 30 days: %@ tokens" = "Last 30 days: %@ tokens"; +"7d spend" = "7d spend"; +"30d spend" = "30d spend"; +"Cache read" = "Cache read"; +"Claude Admin API 30 day spend trend" = "Claude Admin API 30 day spend trend"; +"OpenRouter API key spend trend" = "OpenRouter API key spend trend"; +"z.ai hourly token trend" = "z.ai hourly token trend"; +"MiniMax 30 day token usage trend" = "MiniMax 30 day token usage trend"; +"Today cash" = "Today cash"; +"DeepSeek 30 day token usage trend" = "DeepSeek 30 day token usage trend"; +"cache-hit input" = "cache-hit input"; +"cache-miss input" = "cache-miss input"; +"output" = "output"; +"Requests" = "Requests"; +"Reported by OpenAI Admin API organization usage." = "Reported by OpenAI Admin API organization usage."; +"Reported by Mistral billing usage." = "Reported by Mistral billing usage."; "Google OAuth" = "Google OAuth"; "Add accounts via GitHub OAuth Device Flow on the selected host." = "Add accounts via GitHub OAuth Device Flow on the selected host."; "Stores each signed-in Google account for quick Antigravity switching. Uses Antigravity.app OAuth when available, or ANTIGRAVITY_OAUTH_CLIENT_ID and ANTIGRAVITY_OAUTH_CLIENT_SECRET as an override." = "Stores each signed-in Google account for quick Antigravity switching. Uses Antigravity.app OAuth when available, or ANTIGRAVITY_OAUTH_CLIENT_ID and ANTIGRAVITY_OAUTH_CLIENT_SECRET as an override."; diff --git a/Sources/CodexBar/Resources/es.lproj/Localizable.strings b/Sources/CodexBar/Resources/es.lproj/Localizable.strings index fa3fe614b2..425edb56dd 100644 --- a/Sources/CodexBar/Resources/es.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/es.lproj/Localizable.strings @@ -72,7 +72,6 @@ "Claude login failed" = "El inicio de sesión de Claude falló"; "Claude login timed out" = "El inicio de sesión de Claude agotó el tiempo de espera"; "Close" = "Cerrar"; -"Code review" = "Revisión de código"; "Codex CLI not found" = "No se encontró la CLI de Codex"; "Codex account login already running" = "Ya hay un inicio de sesión de cuenta de Codex en curso"; "Codex binary" = "Binario de Codex"; @@ -633,3 +632,111 @@ /* Cost estimation */ "cost_header_estimated" = "Coste (estimado)"; "cost_estimate_hint" = "Estimado a partir de registros locales · puede diferir de tu factura"; + +/* Popup panels */ +"No usage configured." = "No hay uso configurado."; +"Quota" = "Cuota"; +"tokens" = "tokens"; +"requests" = "solicitudes"; +"Latest" = "Último"; +"Monthly" = "Mensual"; +"Sonnet" = "Sonnet"; +"Auth" = "Autenticación"; +"Overages" = "Excesos"; +"Activity" = "Actividad"; +"Copied" = "Copiado"; +"Copy error" = "Error al copiar"; +"Copy path" = "Copiar ruta"; +"Extra usage spent" = "Gasto de uso adicional"; +"Credits remaining" = "Créditos restantes"; +"Using CLI fallback" = "Usando alternativa de CLI"; +"Balance updates in near-real time (up to 5 min lag)" = "El saldo se actualiza casi en tiempo real (hasta 5 min de retraso)"; +"Daily billing data finalizes at 07:00 UTC" = "Los datos diarios de facturación se cierran a las 07:00 UTC"; +"%@ of %@ credits left" = "Quedan %@ de %@ créditos"; +"%@ of %@ bonus credits left" = "Quedan %@ de %@ créditos de bonificación"; +"Gemini Flash" = "Gemini Flash"; +"Regenerates %@" = "Se regenera %@"; +"used after next regen" = "usado tras la próxima regeneración"; +"after next regen" = "tras la próxima regeneración"; +"Near full" = "Casi lleno"; +"Full in ~1 regen" = "Lleno en ~1 regeneración"; +"Full in ~%.0f regens" = "Lleno en ~%.0f regeneraciones"; +"Overage usage" = "Uso excedente"; +"Overage cost" = "Coste excedente"; +"credits" = "créditos"; +"Zen balance" = "Saldo Zen"; +"API spend" = "Gasto de API"; +"Extra usage" = "Uso adicional"; +"Quota usage" = "Uso de cuota"; +"%.0f%% used" = "%.0f%% usado"; +"Usage history (today)" = "Historial de uso (hoy)"; +"Usage history (%d days)" = "Historial de uso (%d días)"; +"%d percent remaining" = "%d%% restante"; +"Unknown" = "Desconocido"; +"stale data" = "datos obsoletos"; +"No credits history data available." = "No hay datos de historial de créditos disponibles."; +"Credits history chart" = "Gráfico de historial de créditos"; +"%d days of credits data" = "%d días de datos de créditos"; +"Usage breakdown chart" = "Gráfico de desglose de uso"; +"%d days of usage data across %d services" = "%d días de datos de uso en %d servicios"; +"Cost history chart" = "Gráfico de historial de costes"; +"%d days of cost data" = "%d días de datos de costes"; +"Plan utilization chart" = "Gráfico de uso del plan"; +"%d utilization samples" = "%d muestras de uso"; +"Hourly Usage" = "Uso por hora"; +"Usage remaining" = "Uso restante"; +"Usage used" = "Uso utilizado"; +"API key verified. Ollama does not expose Cloud quota limits through the API." = "Clave de API verificada. Ollama no expone los límites de cuota de Cloud mediante la API."; +"Last 30 days: %@ tokens" = "Últimos 30 días: %@ tokens"; +"7d spend" = "Gasto 7 d"; +"30d spend" = "Gasto 30 d"; +"Cache read" = "Lectura de caché"; +"Claude Admin API 30 day spend trend" = "Tendencia de gasto de 30 días de Claude Admin API"; +"OpenRouter API key spend trend" = "Tendencia de gasto de la clave API de OpenRouter"; +"z.ai hourly token trend" = "Tendencia horaria de tokens de z.ai"; +"MiniMax 30 day token usage trend" = "Tendencia de uso de tokens de 30 días de MiniMax"; +"Today cash" = "Efectivo de hoy"; +"DeepSeek 30 day token usage trend" = "Tendencia de uso de tokens de 30 días de DeepSeek"; +"cache-hit input" = "entrada con acierto de caché"; +"cache-miss input" = "entrada sin acierto de caché"; +"output" = "salida"; +"Requests" = "Solicitudes"; +"Reported by OpenAI Admin API organization usage." = "Informado por el uso de la organización en OpenAI Admin API."; +"Reported by Mistral billing usage." = "Informado por el uso de facturación de Mistral."; +"Today" = "Hoy"; +"Today tokens" = "Tokens de hoy"; +"30d cost" = "Coste 30 d"; +"30d tokens" = "Tokens 30 d"; +"Latest tokens" = "Tokens recientes"; +"Top model" = "Modelo principal"; +"Storage" = "Almacenamiento"; +"No data" = "Sin datos"; +"Last %d days" = "Últimos %d días"; +"%@ tokens" = "%@ tokens"; +"Latest billing day" = "Último día de facturación"; +"Latest billing day (%@)" = "Último día de facturación (%@)"; +"This week" = "Esta semana"; +"This month" = "Este mes"; +"Week" = "Semana"; +"Month" = "Mes"; +"Models" = "Modelos"; +"24h tokens" = "Tokens 24 h"; +"Latest hour" = "Última hora"; +"Peak hour" = "Hora pico"; +"Top method" = "Método principal"; +"30d cash" = "Efectivo 30 d"; +"30d billing history from MiniMax web session" = "Historial de facturación de 30 días de la sesión web de MiniMax"; +"AWS Cost Explorer billing can lag." = "La facturación de AWS Cost Explorer puede retrasarse."; +"Rate limit: %d / %@" = "Límite de tasa: %d / %@"; +"Key remaining" = "Clave restante"; +"No limit set for the API key" = "No hay límite configurado para la clave API"; +"API key limit unavailable right now" = "El límite de la clave API no está disponible ahora"; +"Today: %@ · %@ tokens" = "Hoy: %@ · %@ tokens"; +"Today: %@" = "Hoy: %@"; +"Today: %@ tokens" = "Hoy: %@ tokens"; +"This month: %@ tokens" = "Este mes: %@ tokens"; +"API key limit" = "Límite de clave API"; +"Limits not available" = "Límites no disponibles"; +"No usage yet" = "Aún no hay uso"; +"Not fetched yet" = "Aún no obtenido"; +"Code review" = "Revisión de código"; diff --git a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings index 099da41a97..6edcfb0f5c 100644 --- a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings @@ -72,7 +72,6 @@ "Claude login failed" = "Falha no login do Claude"; "Claude login timed out" = "Tempo esgotado no login do Claude"; "Close" = "Fechar"; -"Code review" = "Revisão de código"; "Codex CLI not found" = "CLI do Codex não encontrada"; "Codex account login already running" = "Login de conta Codex já em andamento"; "Codex binary" = "Binário do Codex"; @@ -633,3 +632,111 @@ /* Cost estimation */ "cost_header_estimated" = "Custo (estimado)"; "cost_estimate_hint" = "Estimado a partir de logs locais · pode diferir da sua fatura"; + +/* Popup panels */ +"No usage configured." = "Nenhum uso configurado."; +"Quota" = "Cota"; +"tokens" = "tokens"; +"requests" = "requisições"; +"Latest" = "Mais recente"; +"Monthly" = "Mensal"; +"Sonnet" = "Sonnet"; +"Auth" = "Autenticação"; +"Overages" = "Excedentes"; +"Activity" = "Atividade"; +"Copied" = "Copiado"; +"Copy error" = "Erro ao copiar"; +"Copy path" = "Copiar caminho"; +"Extra usage spent" = "Gasto de uso extra"; +"Credits remaining" = "Créditos restantes"; +"Using CLI fallback" = "Usando fallback da CLI"; +"Balance updates in near-real time (up to 5 min lag)" = "O saldo atualiza quase em tempo real (até 5 min de atraso)"; +"Daily billing data finalizes at 07:00 UTC" = "Os dados diários de cobrança fecham às 07:00 UTC"; +"%@ of %@ credits left" = "Restam %@ de %@ créditos"; +"%@ of %@ bonus credits left" = "Restam %@ de %@ créditos bônus"; +"Gemini Flash" = "Gemini Flash"; +"Regenerates %@" = "Regenera %@"; +"used after next regen" = "usado após a próxima regeneração"; +"after next regen" = "após a próxima regeneração"; +"Near full" = "Quase cheio"; +"Full in ~1 regen" = "Cheio em ~1 regeneração"; +"Full in ~%.0f regens" = "Cheio em ~%.0f regenerações"; +"Overage usage" = "Uso excedente"; +"Overage cost" = "Custo excedente"; +"credits" = "créditos"; +"Zen balance" = "Saldo Zen"; +"API spend" = "Gasto de API"; +"Extra usage" = "Uso extra"; +"Quota usage" = "Uso da cota"; +"%.0f%% used" = "%.0f%% usado"; +"Usage history (today)" = "Histórico de uso (hoje)"; +"Usage history (%d days)" = "Histórico de uso (%d dias)"; +"%d percent remaining" = "%d%% restante"; +"Unknown" = "Desconhecido"; +"stale data" = "dados desatualizados"; +"No credits history data available." = "Nenhum dado de histórico de créditos disponível."; +"Credits history chart" = "Gráfico de histórico de créditos"; +"%d days of credits data" = "%d dias de dados de créditos"; +"Usage breakdown chart" = "Gráfico de detalhamento de uso"; +"%d days of usage data across %d services" = "%d dias de dados de uso em %d serviços"; +"Cost history chart" = "Gráfico de histórico de custos"; +"%d days of cost data" = "%d dias de dados de custos"; +"Plan utilization chart" = "Gráfico de utilização do plano"; +"%d utilization samples" = "%d amostras de utilização"; +"Hourly Usage" = "Uso por hora"; +"Usage remaining" = "Uso restante"; +"Usage used" = "Uso usado"; +"API key verified. Ollama does not expose Cloud quota limits through the API." = "Chave de API verificada. O Ollama não expõe limites de cota do Cloud pela API."; +"Last 30 days: %@ tokens" = "Últimos 30 dias: %@ tokens"; +"7d spend" = "Gasto 7 d"; +"30d spend" = "Gasto 30 d"; +"Cache read" = "Leitura de cache"; +"Claude Admin API 30 day spend trend" = "Tendência de gasto de 30 dias da Claude Admin API"; +"OpenRouter API key spend trend" = "Tendência de gasto da chave API do OpenRouter"; +"z.ai hourly token trend" = "Tendência horária de tokens da z.ai"; +"MiniMax 30 day token usage trend" = "Tendência de uso de tokens de 30 dias da MiniMax"; +"Today cash" = "Dinheiro de hoje"; +"DeepSeek 30 day token usage trend" = "Tendência de uso de tokens de 30 dias da DeepSeek"; +"cache-hit input" = "entrada com acerto de cache"; +"cache-miss input" = "entrada sem acerto de cache"; +"output" = "saída"; +"Requests" = "Requisições"; +"Reported by OpenAI Admin API organization usage." = "Reportado pelo uso da organização na OpenAI Admin API."; +"Reported by Mistral billing usage." = "Reportado pelo uso de cobrança da Mistral."; +"Today" = "Hoje"; +"Today tokens" = "Tokens de hoje"; +"30d cost" = "Custo 30 d"; +"30d tokens" = "Tokens 30 d"; +"Latest tokens" = "Tokens recentes"; +"Top model" = "Modelo principal"; +"Storage" = "Armazenamento"; +"No data" = "Sem dados"; +"Last %d days" = "Últimos %d dias"; +"%@ tokens" = "%@ tokens"; +"Latest billing day" = "Último dia de cobrança"; +"Latest billing day (%@)" = "Último dia de cobrança (%@)"; +"This week" = "Esta semana"; +"This month" = "Este mês"; +"Week" = "Semana"; +"Month" = "Mês"; +"Models" = "Modelos"; +"24h tokens" = "Tokens 24 h"; +"Latest hour" = "Última hora"; +"Peak hour" = "Hora de pico"; +"Top method" = "Método principal"; +"30d cash" = "Dinheiro 30 d"; +"30d billing history from MiniMax web session" = "Histórico de cobrança de 30 dias da sessão web da MiniMax"; +"AWS Cost Explorer billing can lag." = "A cobrança do AWS Cost Explorer pode atrasar."; +"Rate limit: %d / %@" = "Limite de taxa: %d / %@"; +"Key remaining" = "Restante da chave"; +"No limit set for the API key" = "Nenhum limite configurado para a chave API"; +"API key limit unavailable right now" = "O limite da chave API está indisponível no momento"; +"Today: %@ · %@ tokens" = "Hoje: %@ · %@ tokens"; +"Today: %@" = "Hoje: %@"; +"Today: %@ tokens" = "Hoje: %@ tokens"; +"This month: %@ tokens" = "Este mês: %@ tokens"; +"API key limit" = "Limite da chave API"; +"Limits not available" = "Limites indisponíveis"; +"No usage yet" = "Ainda sem uso"; +"Not fetched yet" = "Ainda não buscado"; +"Code review" = "Revisão de código"; diff --git a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings index 5dcbc00854..aa3311a666 100644 --- a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings @@ -733,6 +733,76 @@ "Manual cleanup: past sessions" = "手动清理:历史会话"; "Clearing removes past resume, continue, and rewind history." = "清理后将移除历史恢复、继续和回退记录。"; "Manual cleanup: file checkpoints" = "手动清理:文件检查点"; + +/* Popup panels */ +"No usage configured." = "尚未配置用量。"; +"Quota" = "配额"; +"tokens" = "token"; +"requests" = "请求"; +"Latest" = "最新"; +"Monthly" = "每月"; +"Sonnet" = "Sonnet"; +"Overages" = "超额"; +"Activity" = "活动"; +"Copied" = "已复制"; +"Copy error" = "复制错误"; +"Copy path" = "复制路径"; +"Extra usage spent" = "额外用量支出"; +"Credits remaining" = "剩余额度"; +"Using CLI fallback" = "使用 CLI 回退"; +"Balance updates in near-real time (up to 5 min lag)" = "余额接近实时更新(最多延迟 5 分钟)"; +"Daily billing data finalizes at 07:00 UTC" = "每日账单数据会在 UTC 07:00 完成结算"; +"%@ of %@ credits left" = "剩余 %@ / %@ 点额度"; +"%@ of %@ bonus credits left" = "剩余 %@ / %@ 点奖励额度"; +"Gemini Flash" = "Gemini Flash"; +"Regenerates %@" = "%@后恢复"; +"used after next regen" = "下次恢复后已使用"; +"after next regen" = "下次恢复后"; +"Near full" = "接近全满"; +"Full in ~1 regen" = "约 1 次恢复后全满"; +"Full in ~%.0f regens" = "约 %.0f 次恢复后全满"; +"Overage usage" = "超额用量"; +"Overage cost" = "超额费用"; +"credits" = "额度"; +"Zen balance" = "Zen 余额"; +"API spend" = "API 支出"; +"Extra usage" = "额外用量"; +"Quota usage" = "配额用量"; +"%.0f%% used" = "已使用 %.0f%%"; +"Usage history (today)" = "用量记录(今天)"; +"Usage history (%d days)" = "用量记录(%d 天)"; +"%d percent remaining" = "剩余 %d%%"; +"Unknown" = "未知"; +"stale data" = "数据过旧"; +"No credits history data available." = "暂无可用额度记录数据。"; +"Credits history chart" = "额度记录图表"; +"%d days of credits data" = "%d 天额度数据"; +"Usage breakdown chart" = "用量明细图表"; +"%d days of usage data across %d services" = "%d 天用量数据,涵盖 %d 个服务"; +"Cost history chart" = "费用记录图表"; +"%d days of cost data" = "%d 天费用数据"; +"Plan utilization chart" = "套餐使用率图表"; +"%d utilization samples" = "%d 条使用率样本"; +"Hourly Usage" = "每小时用量"; +"Usage remaining" = "剩余用量"; +"Usage used" = "已使用用量"; +"API key verified. Ollama does not expose Cloud quota limits through the API." = "API 密钥已验证。Ollama 不会通过 API 暴露 Cloud 配额限制。"; +"Last 30 days: %@ tokens" = "近 30 天:%@ token"; +"7d spend" = "7 天支出"; +"30d spend" = "30 天支出"; +"Cache read" = "缓存读取"; +"Claude Admin API 30 day spend trend" = "Claude Admin API 30 天支出趋势"; +"OpenRouter API key spend trend" = "OpenRouter API 密钥支出趋势"; +"z.ai hourly token trend" = "z.ai 每小时 token 趋势"; +"MiniMax 30 day token usage trend" = "MiniMax 30 天 token 用量趋势"; +"Today cash" = "今日现金"; +"DeepSeek 30 day token usage trend" = "DeepSeek 30 天 token 用量趋势"; +"cache-hit input" = "缓存命中输入"; +"cache-miss input" = "缓存未命中输入"; +"output" = "输出"; +"Requests" = "请求"; +"Reported by OpenAI Admin API organization usage." = "由 OpenAI Admin API 组织用量报告。"; +"Reported by Mistral billing usage." = "由 Mistral 账单用量报告。"; "Clearing removes checkpoint restore data for previous edits." = "清理后将移除以往编辑的检查点恢复数据。"; "Manual cleanup: saved plans" = "手动清理:已保存计划"; "Clearing removes old plan-mode files." = "清理后将移除旧的计划模式文件。"; diff --git a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings index 9c12ee79e4..35e30c9fa5 100644 --- a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings @@ -632,3 +632,124 @@ "copilot_device_code" = "裝置代碼已複製到剪貼簿:%1$@\n\n請到以下網址驗證:%2$@"; "copilot_waiting_text" = "請在瀏覽器中完成登入。\n登入完成後,此視窗會自動關閉。"; "vertex_ai_login_instructions" = "要追蹤 Vertex AI 使用量,請透過 Google Cloud 進行認證。\n\n1. 開啟終端\n2. 執行:gcloud auth application-default login\n3. 依照瀏覽器提示登入\n4. 設定你的專案:gcloud config set project PROJECT_ID\n\n要現在開啟終端嗎?"; + +/* Popup panels */ +"No usage configured." = "尚未設定使用量。"; +"Quota" = "配額"; +"tokens" = "token"; +"requests" = "請求"; +"Latest" = "最新"; +"Monthly" = "每月"; +"Sonnet" = "Sonnet"; +"Overages" = "超額"; +"Activity" = "活動"; +"Copied" = "已複製"; +"Copy error" = "複製錯誤"; +"Copy path" = "複製路徑"; +"Extra usage spent" = "額外使用量支出"; +"Credits remaining" = "剩餘額度"; +"Using CLI fallback" = "使用 CLI 備援"; +"Balance updates in near-real time (up to 5 min lag)" = "餘額接近即時更新(最多延遲 5 分鐘)"; +"Daily billing data finalizes at 07:00 UTC" = "每日帳單資料會在 UTC 07:00 完成結算"; +"%@ of %@ credits left" = "剩餘 %@ / %@ 點額度"; +"%@ of %@ bonus credits left" = "剩餘 %@ / %@ 點獎勵額度"; +"Gemini Flash" = "Gemini Flash"; +"Regenerates %@" = "%@後恢復"; +"used after next regen" = "下次恢復後已使用"; +"after next regen" = "下次恢復後"; +"Near full" = "接近全滿"; +"Full in ~1 regen" = "約 1 次恢復後全滿"; +"Full in ~%.0f regens" = "約 %.0f 次恢復後全滿"; +"Overage usage" = "超額使用量"; +"Overage cost" = "超額費用"; +"credits" = "額度"; +"Zen balance" = "Zen 餘額"; +"API spend" = "API 支出"; +"Extra usage" = "額外使用量"; +"Quota usage" = "配額使用量"; +"%.0f%% used" = "已使用 %.0f%%"; +"Usage history (today)" = "使用量記錄(今天)"; +"Usage history (%d days)" = "使用量記錄(%d 天)"; +"%d percent remaining" = "剩餘 %d%%"; +"Unknown" = "未知"; +"stale data" = "資料過舊"; +"No credits history data available." = "尚無可用的額度記錄資料。"; +"Credits history chart" = "額度記錄圖表"; +"%d days of credits data" = "%d 天額度資料"; +"Usage breakdown chart" = "使用量明細圖表"; +"%d days of usage data across %d services" = "%d 天使用量資料,涵蓋 %d 個服務"; +"Cost history chart" = "費用記錄圖表"; +"%d days of cost data" = "%d 天費用資料"; +"Plan utilization chart" = "方案使用率圖表"; +"%d utilization samples" = "%d 筆使用率樣本"; +"Hourly Usage" = "每小時使用量"; +"Usage remaining" = "剩餘使用量"; +"Usage used" = "已使用使用量"; +"API key verified. Ollama does not expose Cloud quota limits through the API." = "API 金鑰已驗證。Ollama 不會透過 API 暴露 Cloud 配額限制。"; +"Last 30 days: %@ tokens" = "近 30 天:%@ token"; +"7d spend" = "7 天支出"; +"30d spend" = "30 天支出"; +"Cache read" = "快取讀取"; +"Claude Admin API 30 day spend trend" = "Claude Admin API 30 天支出趨勢"; +"OpenRouter API key spend trend" = "OpenRouter API 金鑰支出趨勢"; +"z.ai hourly token trend" = "z.ai 每小時 token 趨勢"; +"MiniMax 30 day token usage trend" = "MiniMax 30 天 token 使用量趨勢"; +"Today cash" = "今日現金"; +"DeepSeek 30 day token usage trend" = "DeepSeek 30 天 token 使用量趨勢"; +"cache-hit input" = "快取命中輸入"; +"cache-miss input" = "快取未命中輸入"; +"output" = "輸出"; +"Requests" = "請求"; +"Reported by OpenAI Admin API organization usage." = "由 OpenAI Admin API 組織使用量回報。"; +"Reported by Mistral billing usage." = "由 Mistral 帳單使用量回報。"; +"Today" = "今天"; +"Today tokens" = "今日 token"; +"30d cost" = "近 30 天費用"; +"30d tokens" = "近 30 天 token"; +"Latest tokens" = "最新 token"; +"Top model" = "主要模型"; +"Storage" = "儲存空間"; +"Add Account..." = "新增帳號…"; +"Usage Dashboard" = "使用量儀表板"; +"Status Page" = "狀態頁"; +"Settings..." = "設定…"; +"About CodexBar" = "關於 CodexBar"; +"Quit" = "結束"; +"Last %d day" = "近 %d 天"; +"Last %d days" = "近 %d 天"; +"%@ tokens" = "%@ token"; +"Latest billing day" = "最新帳單日"; +"Latest billing day (%@)" = "最新帳單日(%@)"; +"This week" = "本週"; +"Week" = "週"; +"Month" = "月"; +"Models" = "模型數"; +"24h tokens" = "24 小時 token"; +"Latest hour" = "最新小時"; +"Peak hour" = "尖峰小時"; +"Top method" = "主要方法"; +"30d cash" = "30 天現金"; +"30d billing history from MiniMax web session" = "來自 MiniMax 網頁工作階段的 30 天帳單記錄"; +"AWS Cost Explorer billing can lag." = "AWS Cost Explorer 帳單資料可能延遲。"; +"Rate limit: %d / %@" = "速率限制: %d / %@"; +"Key remaining" = "金鑰剩餘額度"; +"No limit set for the API key" = "此 API 金鑰未設定限制"; +"API key limit unavailable right now" = "目前無法取得 API 金鑰限制"; +"This month: %@ tokens" = "本月:%@ token"; +"Switch Account..." = "切換帳號…"; +"Update ready, restart now?" = "更新已就緒,要立即重新啟動嗎?"; +"Daily" = "每日"; +"Hourly Tokens" = "每小時 token"; +"No data" = "無資料"; +"No usage breakdown data available." = "尚無可用的使用量明細資料。"; +"Today: %@ · %@ tokens" = "今天:%@ · %@ token"; +"Today: %@" = "今天:%@"; +"Today: %@ tokens" = "今天:%@ token"; +"Last 30 days: %@ · %@ tokens" = "近 30 天:%@ · %@ token"; +"Last 30 days: %@" = "近 30 天:%@"; +"Est. total (30d): %@" = "估計總計(30 天):%@"; +"Est. total (%@): %@" = "估計總計(%@):%@"; +"Hover a bar for details" = "停留在長條上查看詳細資料"; +"%@: %@ · %@ tokens" = "%@:%@ · %@ token"; +"No providers selected for Overview." = "概覽尚未選擇提供者。"; +"No overview data available." = "概覽尚無可用資料。"; diff --git a/Sources/CodexBar/StatusItemController+Menu.swift b/Sources/CodexBar/StatusItemController+Menu.swift index 518de2d4ff..02614f5326 100644 --- a/Sources/CodexBar/StatusItemController+Menu.swift +++ b/Sources/CodexBar/StatusItemController+Menu.swift @@ -1464,7 +1464,7 @@ extension StatusItemController { guard let submenu = self.makeCostHistorySubmenu(provider: provider, width: self.renderedMenuWidth(for: menu)) else { return false } let days = self.store.settings.costUsageHistoryDays - let title = days == 1 ? "Usage history (today)" : "Usage history (\(days) days)" + let title = days == 1 ? L("Usage history (today)") : String(format: L("Usage history (%d days)"), days) let item = NSMenuItem(title: title, action: nil, keyEquivalent: "") item.isEnabled = true item.submenu = submenu diff --git a/Sources/CodexBar/StatusItemController+ZaiHourlyChartMenu.swift b/Sources/CodexBar/StatusItemController+ZaiHourlyChartMenu.swift index ae341252ab..3751e1c61f 100644 --- a/Sources/CodexBar/StatusItemController+ZaiHourlyChartMenu.swift +++ b/Sources/CodexBar/StatusItemController+ZaiHourlyChartMenu.swift @@ -14,7 +14,7 @@ extension StatusItemController { let submenu = self.makeHostedSubviewPlaceholderMenu(chartID: Self.zaiHourlyUsageChartID, provider: provider) let item = self.makeMenuCardItem( HStack(spacing: 0) { - Text("Hourly Usage") + Text(L("Hourly Usage")) .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 cd715a410c..276a5365c9 100644 --- a/Sources/CodexBar/StorageBreakdownMenuView.swift +++ b/Sources/CodexBar/StorageBreakdownMenuView.swift @@ -210,8 +210,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 25a97c4bca..358c0955a3 100644 --- a/Sources/CodexBar/UsageBreakdownChartMenuView.swift +++ b/Sources/CodexBar/UsageBreakdownChartMenuView.swift @@ -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("%d days of usage data across %d services"), + model.points.count, + model.services.count)) .chartOverlay { proxy in GeometryReader { geo in ZStack(alignment: .topLeading) { diff --git a/Tests/CodexBarTests/PopupLocalizationTests.swift b/Tests/CodexBarTests/PopupLocalizationTests.swift new file mode 100644 index 0000000000..769812010a --- /dev/null +++ b/Tests/CodexBarTests/PopupLocalizationTests.swift @@ -0,0 +1,113 @@ +import CodexBarCore +import Foundation +import Testing +@testable import CodexBar + +@MainActor +@Suite(.serialized) +struct PopupLocalizationTests { + @Test + func `descriptor account labels use selected localization`() throws { + try CodexBarLocalizationOverride.$appLanguage.withValue("zh-Hant") { + let suite = "PopupLocalizationTests-descriptor" + let settings = try Self.makeSettingsStore(suite: suite) + let store = UsageStore( + fetcher: UsageFetcher(environment: [:]), + browserDetection: BrowserDetection(cacheTTL: 0), + settings: settings, + startupBehavior: .testing) + store._setSnapshotForTesting( + UsageSnapshot( + primary: RateWindow(usedPercent: 12, windowMinutes: 300, resetsAt: nil, resetDescription: nil), + secondary: nil, + updatedAt: Date(), + identity: ProviderIdentitySnapshot( + providerID: .codex, + accountEmail: "codex@example.com", + accountOrganization: nil, + loginMethod: "free")), + provider: .codex) + + let descriptor = MenuDescriptor.build( + provider: .codex, + store: store, + settings: settings, + account: AccountInfo(email: nil, plan: nil), + updateReady: false, + includeContextualActions: false) + + let lines = Self.textLines(from: descriptor) + + #expect(lines.contains("帳號: codex@example.com")) + #expect(lines.contains("方案: Free")) + #expect(!lines.contains("Account: codex@example.com")) + #expect(!lines.contains("Plan: Free")) + } + } + + @Test + func `inline dashboard labels use selected localization`() throws { + try CodexBarLocalizationOverride.$appLanguage.withValue("zh-Hant") { + let now = Date(timeIntervalSince1970: 1_700_179_200) + let metadata = try #require(ProviderDefaults.metadata[.openrouter]) + let usage = OpenRouterUsageSnapshot( + totalCredits: 100, + totalUsage: 40, + balance: 60, + usedPercent: 40, + keyDataFetched: true, + keyLimit: 25, + keyUsage: 10, + keyUsageDaily: 1.25, + keyUsageWeekly: 7.5, + keyUsageMonthly: 18.75, + rateLimit: OpenRouterRateLimit(requests: 100, interval: "10s"), + updatedAt: now) + + let model = UsageMenuCardView.Model.make(.init( + provider: .openrouter, + metadata: metadata, + snapshot: usage.toUsageSnapshot(), + credits: nil, + creditsError: nil, + dashboard: nil, + dashboardError: nil, + tokenSnapshot: nil, + tokenError: nil, + account: AccountInfo(email: nil, plan: nil), + isRefreshing: false, + lastError: nil, + usageBarsShowUsed: false, + resetTimeDisplayStyle: .countdown, + tokenCostUsageEnabled: false, + showOptionalCreditsAndExtraUsage: true, + hidePersonalInfo: false, + now: now)) + + let dashboard = try #require(model.inlineUsageDashboard) + + #expect(dashboard.kpis.map(\.title) == ["餘額", "今天", "週", "月"]) + #expect(dashboard.points.map(\.label) == ["今天", "週", "月"]) + #expect(dashboard.detailLines.contains("速率限制: 100 / 10s")) + } + } + + private static func makeSettingsStore(suite: String) throws -> SettingsStore { + let defaults = try #require(UserDefaults(suiteName: suite)) + defaults.removePersistentDomain(forName: suite) + let settings = SettingsStore( + userDefaults: defaults, + configStore: testConfigStore(suiteName: suite), + zaiTokenStore: NoopZaiTokenStore(), + syntheticTokenStore: NoopSyntheticTokenStore()) + settings.statusChecksEnabled = false + return settings + } + + private static func textLines(from descriptor: MenuDescriptor) -> [String] { + descriptor.sections.flatMap(\.entries).compactMap { entry -> String? in + guard case let .text(text, _) = entry else { return nil } + return text + } + } +} From 38541a6eac42c36beb785d2d8b1cca8ede305edc Mon Sep 17 00:00:00 2001 From: Shun Min Chang Date: Wed, 27 May 2026 13:11:47 +0800 Subject: [PATCH 2/6] feat(i18n): localize popup panels Localize popup cards, provider settings, login alerts, keychain prompts, chart labels, and menu actions across supported locales. Refresh open merged menus when the app language changes so popup tab and cost labels update immediately. Add focused regression and localization coverage tests for popup/menu language behavior. --- .gitignore | 1 + .../CodexAccountPromotionCoordinator.swift | 29 ++-- .../CodexLoginAlertPresentation.swift | 14 +- Sources/CodexBar/CodexLoginRunner.swift | 2 +- .../CodexBar/CostHistoryChartMenuView.swift | 10 +- .../CreditsHistoryChartMenuView.swift | 20 ++- Sources/CodexBar/CursorLoginRunner.swift | 11 +- .../CodexBar/KeychainPromptCoordinator.swift | 142 ++++++++-------- Sources/CodexBar/Localization.swift | 4 + .../PlanUtilizationHistoryChartMenuView.swift | 10 +- .../PreferencesCodexAccountsSection.swift | 30 ++-- Sources/CodexBar/PreferencesDebugPane.swift | 14 +- Sources/CodexBar/PreferencesDisplayPane.swift | 10 +- Sources/CodexBar/PreferencesGeneralPane.swift | 2 +- .../PreferencesProviderDetailView.swift | 4 +- .../PreferencesProviderErrorView.swift | 2 +- .../PreferencesProviderSettingsRows.swift | 69 ++++---- .../PreferencesProviderSidebarView.swift | 8 +- .../CodexBar/PreferencesProvidersPane.swift | 12 +- .../AugmentProviderImplementation.swift | 4 +- .../FactoryProviderImplementation.swift | 2 +- .../Resources/ca.lproj/Localizable.strings | 156 +++++++++++++++++ .../Resources/en.lproj/Localizable.strings | 156 +++++++++++++++++ .../Resources/es.lproj/Localizable.strings | 156 +++++++++++++++++ .../Resources/pt-BR.lproj/Localizable.strings | 156 +++++++++++++++++ .../zh-Hans.lproj/Localizable.strings | 158 +++++++++++++++++- .../zh-Hant.lproj/Localizable.strings | 156 +++++++++++++++++ .../SettingsStore+MenuObservation.swift | 1 + .../StatusItemController+Actions.swift | 58 +++---- .../StatusItemController+CostMenuCard.swift | 4 +- .../CodexBar/StatusItemController+Menu.swift | 16 +- ...tatusItemController+MenuLocalization.swift | 36 ++++ ...tatusItemController+UsageHistoryMenu.swift | 2 +- Sources/CodexBar/StatusItemController.swift | 6 + .../UsageBreakdownChartMenuView.swift | 12 +- Sources/CodexBar/UsageStore+Refresh.swift | 2 +- .../StatusMenuLocalizationRefreshTests.swift | 123 ++++++++++++++ .../UserFacingLocalizationCoverageTests.swift | 112 +++++++++++++ 38 files changed, 1472 insertions(+), 238 deletions(-) create mode 100644 Sources/CodexBar/StatusItemController+MenuLocalization.swift create mode 100644 Tests/CodexBarTests/StatusMenuLocalizationRefreshTests.swift create mode 100644 Tests/CodexBarTests/UserFacingLocalizationCoverageTests.swift diff --git a/.gitignore b/.gitignore index 8bb2ec1f8f..8580a0d819 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ xcuserdata/ .swiftpm/xcode/xcshareddata/ .codexbar/config.json +.codexbar-local/ *.env *.local diff --git a/Sources/CodexBar/CodexAccountPromotionCoordinator.swift b/Sources/CodexBar/CodexAccountPromotionCoordinator.swift index 900ce48bd9..c22699bef0 100644 --- a/Sources/CodexBar/CodexAccountPromotionCoordinator.swift +++ b/Sources/CodexBar/CodexAccountPromotionCoordinator.swift @@ -73,36 +73,37 @@ final class CodexAccountPromotionCoordinator { private static func interactionBlockedError() -> CodexSystemAccountPromotionUserFacingError { CodexSystemAccountPromotionUserFacingError( - title: "Could not switch system account", - message: "Finish the current managed account change before switching the system account.") + title: L("Could not switch system account"), + message: L("Finish the current managed account change before switching the system account.")) } static func mapUserFacingError(_ error: Error) -> CodexSystemAccountPromotionUserFacingError { - let title = "Could not switch system account" + let title = L("Could not switch system account") if let error = error as? CodexAccountPromotionError { let message = switch error { case .targetManagedAccountNotFound: - "That account is no longer available in CodexBar. Refresh the account list and try again." + L("That account is no longer available in CodexBar. Refresh the account list and try again.") case .targetManagedAccountAuthMissing: - "CodexBar could not find saved auth for that account. Re-authenticate it and try again." + L("CodexBar could not find saved auth for that account. Re-authenticate it and try again.") case .targetManagedAccountAuthUnreadable: - "CodexBar could not read saved auth for that account. Re-authenticate it and try again." + L("CodexBar could not read saved auth for that account. Re-authenticate it and try again.") case .liveAccountUnreadable: - "CodexBar could not read the current system account on this Mac." + L("CodexBar could not read the current system account on this Mac.") case .liveAccountMissingIdentityForPreservation: - "CodexBar could not safely preserve the current system account before switching." + L("CodexBar could not safely preserve the current system account before switching.") case .liveAccountAPIKeyOnlyUnsupported: - "CodexBar can't replace a system account that is signed in with an API key only setup." + L("CodexBar can't replace a system account that is signed in with an API key only setup.") case .displacedLiveManagedAccountConflict: - "CodexBar found another managed account that already uses the current system account. " - + "Resolve the duplicate account before switching." + L( + "CodexBar found another managed account that already uses the current system account. " + + "Resolve the duplicate account before switching.") case .displacedLiveImportFailed: - "CodexBar could not save the current system account before switching." + L("CodexBar could not save the current system account before switching.") case .managedStoreCommitFailed: - "CodexBar could not update managed account storage." + L("CodexBar could not update managed account storage.") case .liveAuthSwapFailed: - "CodexBar could not replace the live Codex auth on this Mac." + L("CodexBar could not replace the live Codex auth on this Mac.") } return CodexSystemAccountPromotionUserFacingError(title: title, message: message) diff --git a/Sources/CodexBar/CodexLoginAlertPresentation.swift b/Sources/CodexBar/CodexLoginAlertPresentation.swift index 9f1e79650b..eb14211031 100644 --- a/Sources/CodexBar/CodexLoginAlertPresentation.swift +++ b/Sources/CodexBar/CodexLoginAlertPresentation.swift @@ -12,25 +12,25 @@ enum CodexLoginAlertPresentation { return nil case .missingBinary: return CodexLoginAlertInfo( - title: "Codex CLI not found", - message: "Install the Codex CLI (npm i -g @openai/codex) and try again.") + title: L("Codex CLI not found"), + message: L("Install the Codex CLI (npm i -g @openai/codex) and try again.")) case let .launchFailed(message): - return CodexLoginAlertInfo(title: "Could not start codex login", message: message) + return CodexLoginAlertInfo(title: L("Could not start codex login"), message: message) case .timedOut: return CodexLoginAlertInfo( - title: "Codex login timed out", + title: L("Codex login timed out"), message: self.trimmedOutput(result.output)) case let .failed(status): - let statusLine = "codex login exited with status \(status)." + let statusLine = String(format: L("codex login exited with status %d."), status) let message = self.trimmedOutput(result.output.isEmpty ? statusLine : result.output) - return CodexLoginAlertInfo(title: "Codex login failed", message: message) + return CodexLoginAlertInfo(title: L("Codex login failed"), message: message) } } private static func trimmedOutput(_ text: String) -> String { let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) let limit = 600 - if trimmed.isEmpty { return "No output captured." } + if trimmed.isEmpty { return L("No output captured.") } if trimmed.count <= limit { return trimmed } let idx = trimmed.index(trimmed.startIndex, offsetBy: limit) return "\(trimmed[.. String { diff --git a/Sources/CodexBar/CostHistoryChartMenuView.swift b/Sources/CodexBar/CostHistoryChartMenuView.swift index 0ca5f6d6e3..e1a35e28a4 100644 --- a/Sources/CodexBar/CostHistoryChartMenuView.swift +++ b/Sources/CodexBar/CostHistoryChartMenuView.swift @@ -74,16 +74,16 @@ struct CostHistoryChartMenuView: View { Chart { ForEach(model.points) { point in BarMark( - x: .value("Day", point.date, unit: .day), - y: .value("Cost", point.costUSD)) + x: .value(L("Day"), point.date, unit: .day), + y: .value(L("Cost"), point.costUSD)) .foregroundStyle(model.barColor) } if let peak = Self.peakPoint(model: model) { let capStart = max(peak.costUSD - Self.capHeight(maxValue: model.maxCostUSD), 0) BarMark( - x: .value("Day", peak.date, unit: .day), - yStart: .value("Cap start", capStart), - yEnd: .value("Cap end", peak.costUSD)) + x: .value(L("Day"), peak.date, unit: .day), + yStart: .value(L("Cap start"), capStart), + yEnd: .value(L("Cap end"), peak.costUSD)) .foregroundStyle(Color(nsColor: .systemYellow)) } } diff --git a/Sources/CodexBar/CreditsHistoryChartMenuView.swift b/Sources/CodexBar/CreditsHistoryChartMenuView.swift index a73e255055..e75521eb96 100644 --- a/Sources/CodexBar/CreditsHistoryChartMenuView.swift +++ b/Sources/CodexBar/CreditsHistoryChartMenuView.swift @@ -37,16 +37,16 @@ struct CreditsHistoryChartMenuView: View { Chart { ForEach(model.points) { point in BarMark( - x: .value("Day", point.date, unit: .day), - y: .value("Credits used", point.creditsUsed)) + x: .value(L("Day"), point.date, unit: .day), + y: .value(L("Credits used"), point.creditsUsed)) .foregroundStyle(Self.barColor) } if let peak = Self.peakPoint(model: model) { let capStart = max(peak.creditsUsed - Self.capHeight(maxValue: model.maxCreditsUsed), 0) BarMark( - x: .value("Day", peak.date, unit: .day), - yStart: .value("Cap start", capStart), - yEnd: .value("Cap end", peak.creditsUsed)) + x: .value(L("Day"), peak.date, unit: .day), + yStart: .value(L("Cap start"), capStart), + yEnd: .value(L("Cap end"), peak.creditsUsed)) .foregroundStyle(Color(nsColor: .systemYellow)) } } @@ -104,7 +104,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) } @@ -311,11 +313,11 @@ struct CreditsHistoryChartMenuView: View { 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("%@: %@ 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("%@: %@ credits"), dayLabel, used), first.service) } let services = day.services @@ -327,6 +329,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("%@: %@ credits"), dayLabel, total), services) } } diff --git a/Sources/CodexBar/CursorLoginRunner.swift b/Sources/CodexBar/CursorLoginRunner.swift index d7e6670029..4e5dbe9f01 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/KeychainPromptCoordinator.swift b/Sources/CodexBar/KeychainPromptCoordinator.swift index abbeb0caac..7a4586514d 100644 --- a/Sources/CodexBar/KeychainPromptCoordinator.swift +++ b/Sources/CodexBar/KeychainPromptCoordinator.swift @@ -2,6 +2,58 @@ import AppKit import CodexBarCore import SweetCookieKit +private enum KeychainPromptMessage { + static let browserCookie = + "CodexBar will ask macOS Keychain for “%@” so it can decrypt browser cookies " + + "and authenticate your account. Click OK to continue." + + static let claudeOAuth = + "CodexBar will ask macOS Keychain for the Claude Code OAuth token " + + "so it can fetch your Claude usage. Click OK to continue." + static let codexCookie = + "CodexBar will ask macOS Keychain for your OpenAI cookie header " + + "so it can fetch Codex dashboard extras. Click OK to continue." + static let claudeCookie = + "CodexBar will ask macOS Keychain for your Claude cookie header " + + "so it can fetch Claude web usage. Click OK to continue." + static let cursorCookie = + "CodexBar will ask macOS Keychain for your Cursor cookie header " + + "so it can fetch usage. Click OK to continue." + static let openCodeCookie = + "CodexBar will ask macOS Keychain for your OpenCode cookie header " + + "so it can fetch usage. Click OK to continue." + static let factoryCookie = + "CodexBar will ask macOS Keychain for your Factory cookie header " + + "so it can fetch usage. Click OK to continue." + static let zaiToken = + "CodexBar will ask macOS Keychain for your z.ai API token " + + "so it can fetch usage. Click OK to continue." + static let syntheticToken = + "CodexBar will ask macOS Keychain for your Synthetic API key " + + "so it can fetch usage. Click OK to continue." + static let copilotToken = + "CodexBar will ask macOS Keychain for your GitHub Copilot token " + + "so it can fetch usage. Click OK to continue." + static let kimiToken = + "CodexBar will ask macOS Keychain for your Kimi auth token " + + "so it can fetch usage. Click OK to continue." + static let kimiK2Token = + "CodexBar will ask macOS Keychain for your Kimi K2 API key " + + "so it can fetch usage. Click OK to continue." + static let minimaxCookie = + "CodexBar will ask macOS Keychain for your MiniMax cookie header " + + "so it can fetch usage. Click OK to continue." + static let minimaxToken = + "CodexBar will ask macOS Keychain for your MiniMax API token " + + "so it can fetch usage. Click OK to continue." + static let augmentCookie = + "CodexBar will ask macOS Keychain for your Augment cookie header " + + "so it can fetch usage. Click OK to continue." + static let ampCookie = + "CodexBar will ask macOS Keychain for your Amp cookie header " + + "so it can fetch usage. Click OK to continue." +} + enum KeychainPromptCoordinator { private static let promptLock = NSLock() private static let log = CodexBarLog.logger(LogCategories.keychainPrompt) @@ -22,93 +74,47 @@ 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 message = L( + KeychainPromptMessage.browserCookie, + 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, L(KeychainPromptMessage.claudeOAuth)) 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, L(KeychainPromptMessage.codexCookie)) 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, L(KeychainPromptMessage.claudeCookie)) 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, L(KeychainPromptMessage.cursorCookie)) 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, L(KeychainPromptMessage.openCodeCookie)) 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, L(KeychainPromptMessage.factoryCookie)) 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, L(KeychainPromptMessage.zaiToken)) 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, L(KeychainPromptMessage.syntheticToken)) 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, L(KeychainPromptMessage.copilotToken)) 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, L(KeychainPromptMessage.kimiToken)) 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, L(KeychainPromptMessage.kimiK2Token)) 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, L(KeychainPromptMessage.minimaxCookie)) 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, L(KeychainPromptMessage.minimaxToken)) 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, L(KeychainPromptMessage.augmentCookie)) 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, L(KeychainPromptMessage.ampCookie)) } } @@ -132,8 +138,8 @@ enum KeychainPromptCoordinator { @MainActor private static func showAlert(title: String, message: String) { let alert = NSAlert() - alert.messageText = title - alert.informativeText = message + alert.messageText = L(title) + alert.informativeText = L(message) alert.addButton(withTitle: L("OK")) _ = alert.runModal() } diff --git a/Sources/CodexBar/Localization.swift b/Sources/CodexBar/Localization.swift index a4c38c8ade..8fbdf32170 100644 --- a/Sources/CodexBar/Localization.swift +++ b/Sources/CodexBar/Localization.swift @@ -37,6 +37,10 @@ private func resolvedAppLanguage() -> String { return appLanguageDefaults().string(forKey: "appLanguage") ?? "" } +func codexBarLocalizationSignature() -> String { + resolvedAppLanguage() +} + func codexBarLocalizationResourceBundle( mainBundle: Bundle = .main, bundleName: String = "CodexBar_CodexBar") -> Bundle diff --git a/Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift b/Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift index 7b5321d842..ae995cc1b4 100644 --- a/Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift +++ b/Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift @@ -710,7 +710,7 @@ struct PlanUtilizationHistoryChartMenuView: View { #endif private func xValue(for index: Int) -> PlottableValue { - .value("Series", Double(index)) + .value(L("Series"), Double(index)) } @ViewBuilder @@ -732,14 +732,14 @@ struct PlanUtilizationHistoryChartMenuView: View { ForEach(model.points) { point in BarMark( x: self.xValue(for: point.index), - yStart: .value("Capacity Start", 0), - yEnd: .value("Capacity End", 100), + yStart: .value(L("Capacity Start"), 0), + yEnd: .value(L("Capacity End"), 100), width: .fixed(Layout.barWidth)) .foregroundStyle(model.trackColor) BarMark( x: self.xValue(for: point.index), - yStart: .value("Utilization Start", 0), - yEnd: .value("Utilization End", point.usedPercent), + yStart: .value(L("Utilization Start"), 0), + yEnd: .value(L("Utilization End"), point.usedPercent), width: .fixed(Layout.barWidth)) .foregroundStyle(model.barColor) } diff --git a/Sources/CodexBar/PreferencesCodexAccountsSection.swift b/Sources/CodexBar/PreferencesCodexAccountsSection.swift index c8d541433e..f42be52718 100644 --- a/Sources/CodexBar/PreferencesCodexAccountsSection.swift +++ b/Sources/CodexBar/PreferencesCodexAccountsSection.swift @@ -51,7 +51,7 @@ struct CodexAccountsSectionState: Equatable { } var systemDisplayName: String { - self.systemVisibleAccount?.displayName ?? "No system account" + self.systemVisibleAccount?.displayName ?? L("No system account") } var canAddAccount: Bool { @@ -64,9 +64,9 @@ struct CodexAccountsSectionState: Equatable { var addAccountTitle: String { if self.isAuthenticatingManagedAccount, self.authenticatingManagedAccountID == nil { - return "Adding Account…" + return L("Adding Account…") } - return "Add Account" + return L("Add Account") } func showsLiveBadge(for account: CodexVisibleAccount) -> Bool { @@ -113,12 +113,12 @@ struct CodexAccountsSectionState: Equatable { self.isAuthenticatingManagedAccount, self.authenticatingManagedAccountID == accountID { - return "Re-authenticating…" + return L("Re-authenticating…") } if account.storedAccountID == nil, self.isAuthenticatingLiveAccount { - return "Re-authenticating…" + return L("Re-authenticating…") } - return "Re-auth" + return L("Re-auth") } } @@ -132,11 +132,11 @@ struct CodexAccountsSectionView: View { let addAccount: () -> Void var body: some View { - ProviderSettingsSection(title: "Accounts") { + ProviderSettingsSection(title: L("Accounts")) { 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 { @@ -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) @@ -273,7 +273,7 @@ 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) } @@ -295,7 +295,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) } @@ -319,7 +319,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 e70e330863..e5de4da915 100644 --- a/Sources/CodexBar/PreferencesDebugPane.swift +++ b/Sources/CodexBar/PreferencesDebugPane.swift @@ -47,7 +47,7 @@ struct DebugPane: View { .foregroundStyle(.tertiary) } Spacer() - Picker("Verbosity", selection: self.$settings.debugLogLevel) { + Picker(L("Verbosity"), selection: self.$settings.debugLogLevel) { ForEach(CodexBarLog.Level.allCases) { level in Text(level.displayName).tag(level) } @@ -76,7 +76,7 @@ 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)) @@ -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) @@ -321,7 +321,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 81f456d8ac..63de3853b1 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"), selection: self.$settings.menuBarDisplayMode) { ForEach(MenuBarDisplayMode.allCases) { mode in Text(mode.label).tag(mode) } @@ -88,10 +88,10 @@ struct DisplayPane: View { } Spacer() Picker(L("weekly_progress_work_days_title"), selection: self.$settings.weeklyProgressWorkDays) { - Text("Off").tag(nil as Int?) - Text("4 days").tag(4 as Int?) - Text("5 days").tag(5 as Int?) - Text("7 days").tag(7 as Int?) + Text(L("Off")).tag(nil as Int?) + Text(L("4 days")).tag(4 as Int?) + Text(L("5 days")).tag(5 as Int?) + Text(L("7 days")).tag(7 as Int?) } .labelsHidden() .pickerStyle(.menu) diff --git a/Sources/CodexBar/PreferencesGeneralPane.swift b/Sources/CodexBar/PreferencesGeneralPane.swift index c4ba6a8904..709c4d31ba 100644 --- a/Sources/CodexBar/PreferencesGeneralPane.swift +++ b/Sources/CodexBar/PreferencesGeneralPane.swift @@ -131,7 +131,7 @@ struct GeneralPane: View { .foregroundStyle(.tertiary) } Spacer() - Picker("Refresh cadence", selection: self.$settings.refreshFrequency) { + Picker(L("Refresh cadence"), selection: self.$settings.refreshFrequency) { ForEach(RefreshFrequency.allCases) { option in Text(option.label).tag(option) } diff --git a/Sources/CodexBar/PreferencesProviderDetailView.swift b/Sources/CodexBar/PreferencesProviderDetailView.swift index d6a850c9cf..36dd0ce6dc 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() @@ -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 4b5c96b780..156dc62719 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 c4f4cb8176..154fa5b6fe 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(L(self.field.placeholder ?? ""), text: self.field.binding) .textFieldStyle(.roundedBorder) .font(.footnote) .onTapGesture { self.field.onActivate?() } case .secure: - SecureField(self.field.placeholder ?? "", text: self.field.binding) + SecureField(L(self.field.placeholder ?? ""), 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) @@ -395,7 +396,7 @@ struct ProviderSettingsOrganizationsRowView: 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) } @@ -403,7 +404,7 @@ struct ProviderSettingsOrganizationsRowView: View { if let subtitle = self.descriptor.subtitle, !subtitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - Text(subtitle) + Text(L(subtitle)) .font(.footnote) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) @@ -411,7 +412,7 @@ struct ProviderSettingsOrganizationsRowView: View { let entries = self.descriptor.entries() if entries.allSatisfy(\.isLocked) { - Text("No organizations loaded. Click Refresh after setting your API key.") + Text(L("No organizations loaded. Click Refresh after setting your API key.")) .font(.footnote) .foregroundStyle(.secondary) } else { @@ -423,12 +424,12 @@ struct ProviderSettingsOrganizationsRowView: View { self.descriptor.onToggle(entry.id, newValue) })) { VStack(alignment: .leading, spacing: 1) { - Text(entry.title) + Text(L(entry.title)) .font(.footnote) if let subtitle = entry.subtitle, !subtitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - Text(subtitle) + Text(L(subtitle)) .font(.caption) .foregroundStyle(.secondary) } @@ -441,7 +442,7 @@ struct ProviderSettingsOrganizationsRowView: View { } HStack(spacing: 10) { - Button("Refresh organizations") { + Button(L("Refresh organizations")) { Task { @MainActor in self.isRefreshing = true let result = await self.descriptor.onRefresh() diff --git a/Sources/CodexBar/PreferencesProviderSidebarView.swift b/Sources/CodexBar/PreferencesProviderSidebarView.swift index 559c5329e3..21d22019eb 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) @@ -119,9 +119,9 @@ private struct ProviderSidebarRowView: View { if lines.count >= 2 { let first = lines[0] let rest = lines.dropFirst().joined(separator: "\n") - return "Disabled — \(first)\n\(rest)" + return "\(L("Disabled")) — \(first)\n\(rest)" } - return "Disabled — \(self.subtitle)" + return "\(L("Disabled")) — \(self.subtitle)" } } @@ -145,7 +145,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 c10f9c0e18..e911e8a3a4 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) @@ -688,8 +688,8 @@ struct ProvidersPane: View { private 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() } @@ -754,9 +754,9 @@ struct ProviderSettingsConfirmationState: Identifiable { } init(confirmation: ProviderSettingsConfirmation) { - self.title = confirmation.title - self.message = confirmation.message - self.confirmTitle = confirmation.confirmTitle + self.title = L(confirmation.title) + self.message = L(confirmation.message) + self.confirmTitle = L(confirmation.confirmTitle) self.onConfirm = confirmation.onConfirm } } diff --git a/Sources/CodexBar/Providers/Augment/AugmentProviderImplementation.swift b/Sources/CodexBar/Providers/Augment/AugmentProviderImplementation.swift index c1529bd58b..bad4a58a00 100644 --- a/Sources/CodexBar/Providers/Augment/AugmentProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Augment/AugmentProviderImplementation.swift @@ -83,14 +83,14 @@ struct AugmentProviderImplementation: ProviderImplementation { @MainActor func appendActionMenuEntries(context: ProviderMenuActionContext, entries: inout [ProviderMenuEntry]) { - entries.append(.action("Refresh Session", .refreshAugmentSession)) + entries.append(.action(L("Refresh Session"), .refreshAugmentSession)) if let error = context.store.error(for: .augment) { if error.contains("session has expired") || error.contains("No Augment session cookie found") { entries.append(.action( - "Open Augment (Log Out & Back In)", + L("Open Augment (Log Out & Back In)"), .loginToProvider(url: "https://app.augmentcode.com"))) } } diff --git a/Sources/CodexBar/Providers/Factory/FactoryProviderImplementation.swift b/Sources/CodexBar/Providers/Factory/FactoryProviderImplementation.swift index c9a759cbac..3df11a9418 100644 --- a/Sources/CodexBar/Providers/Factory/FactoryProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Factory/FactoryProviderImplementation.swift @@ -98,6 +98,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(L("Extra usage balance: %@", balance), .primary)) } } diff --git a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings index b5f9f70d1f..eb7832056e 100644 --- a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings @@ -740,3 +740,159 @@ "No usage yet" = "Encara no hi ha ús"; "Not fetched yet" = "Encara no obtingut"; "Code review" = "Revisió de codi"; + +/* Additional provider settings and alerts */ +"%@ is waiting for permission" = "%@ espera permís"; +"%@ requests" = "%@ sol·licituds"; +"%@: %@ credits" = "%@: %@ crèdits"; +"30d requests" = "Sol·licituds de 30 d"; +"4 days" = "4 dies"; +"5 days" = "5 dies"; +"7 days" = "7 dies"; +"API key verifies Ollama Cloud access; cookies still expose quota limits." = "La clau API verifica l'accés a Ollama Cloud; les galetes encara exposen els límits de quota."; +"AWS access key ID. Can also be set with AWS_ACCESS_KEY_ID." = "ID de clau d'accés d'AWS. També es pot definir amb AWS_ACCESS_KEY_ID."; +"AWS region. Can also be set with AWS_REGION." = "Regió d'AWS. També es pot definir amb AWS_REGION."; +"AWS secret access key. Can also be set with AWS_SECRET_ACCESS_KEY." = "Clau secreta d'accés d'AWS. També es pot definir amb AWS_SECRET_ACCESS_KEY."; +"Access key ID" = "ID de clau d'accés"; +"Add Account" = "Afegeix compte"; +"Adding Account…" = "S'està afegint el compte…"; +"Antigravity login failed" = "L'inici de sessió d'Antigravity ha fallat"; +"Antigravity login timed out" = "L'inici de sessió d'Antigravity ha esgotat el temps"; +"Auth source" = "Font d'autenticació"; +"Automatic imports Chrome browser cookies from Xiaomi MiMo." = "Importa automàticament les galetes de Chrome de Xiaomi MiMo."; +"Automatic imports Windsurf session data from Chromium browser localStorage." = "Importa automàticament dades de sessió de Windsurf del localStorage de Chromium."; +"Automatic imports browser cookies from Bailian." = "Importa automàticament galetes del navegador de Bailian."; +"Automatically imports browser cookies." = "Importa automàticament galetes del navegador."; +"Automatically imports browser session cookies." = "Importa automàticament galetes de sessió del navegador."; +"Azure OpenAI deployment name. AZURE_OPENAI_DEPLOYMENT_NAME is also supported." = "Nom del desplegament d'Azure OpenAI. També s'admet AZURE_OPENAI_DEPLOYMENT_NAME."; +"Azure OpenAI key" = "Clau d'Azure OpenAI"; +"Azure OpenAI resource endpoint. AZURE_OPENAI_ENDPOINT is also supported." = "Endpoint del recurs Azure OpenAI. També s'admet AZURE_OPENAI_ENDPOINT."; +"Base URL" = "URL base"; +"Base URL for the LLM-API-Key-Proxy instance." = "URL base de la instància LLM-API-Key-Proxy."; +"Browser cookies" = "Galetes del navegador"; +"Cap end" = "Final del límit"; +"Cap start" = "Inici del límit"; +"Capacity End" = "Final de capacitat"; +"Capacity Start" = "Inici de capacitat"; +"Changelog" = "Registre de canvis"; +"Choose the Moonshot/Kimi API host for international or China mainland accounts." = "Tria el host de l'API Moonshot/Kimi per a comptes internacionals o de la Xina continental."; +"CodexBar can't replace a system account that is signed in with an API key only setup." = "CodexBar no pot substituir un compte del sistema iniciat només amb una clau API."; +"CodexBar could not find saved auth for that account. Re-authenticate it and try again." = "CodexBar no ha trobat autenticació desada per a aquest compte. Torna'l a autenticar i prova-ho de nou."; +"CodexBar could not read managed account storage. Recover the store before adding another account." = "CodexBar no ha pogut llegir l'emmagatzematge de comptes gestionats. Recupera'l abans d'afegir un altre compte."; +"CodexBar could not read saved auth for that account. Re-authenticate it and try again." = "CodexBar no ha pogut llegir l'autenticació desada per a aquest compte. Torna'l a autenticar i prova-ho de nou."; +"CodexBar could not read the current system account on this Mac." = "CodexBar no ha pogut llegir el compte del sistema actual en aquest Mac."; +"CodexBar could not replace the live Codex auth on this Mac." = "CodexBar no ha pogut substituir l'autenticació activa de Codex en aquest Mac."; +"CodexBar could not safely preserve the current system account before switching." = "CodexBar no ha pogut preservar de manera segura el compte del sistema actual abans de canviar."; +"CodexBar could not save the current system account before switching." = "CodexBar no ha pogut desar el compte del sistema actual abans de canviar."; +"CodexBar could not update managed account storage." = "CodexBar no ha pogut actualitzar l'emmagatzematge de comptes gestionats."; +"CodexBar found another managed account that already uses the current system account. Resolve the duplicate account before switching." = "CodexBar ha trobat un altre compte gestionat que ja utilitza el compte del sistema actual. Resol el compte duplicat abans de canviar."; +"CodexBar will ask macOS Keychain for “%@” so it can decrypt browser cookies and authenticate your account. Click OK to continue." = "CodexBar demanarà a Clauers de macOS “%@” per desxifrar galetes del navegador i autenticar el compte. Fes clic a OK per continuar."; +"CodexBar will ask macOS Keychain for the Claude Code OAuth token so it can fetch your Claude usage. Click OK to continue." = "CodexBar demanarà a Clauers de macOS el token OAuth de Claude Code per obtenir l'ús de Claude. Fes clic a OK per continuar."; +"CodexBar will ask macOS Keychain for your Amp cookie header so it can fetch usage. Click OK to continue." = "CodexBar demanarà a Clauers de macOS la capçalera Cookie d'Amp per obtenir l'ús. Fes clic a OK per continuar."; +"CodexBar will ask macOS Keychain for your Augment cookie header so it can fetch usage. Click OK to continue." = "CodexBar demanarà a Clauers de macOS la capçalera Cookie d'Augment per obtenir l'ús. Fes clic a OK per continuar."; +"CodexBar will ask macOS Keychain for your Claude cookie header so it can fetch Claude web usage. Click OK to continue." = "CodexBar demanarà a Clauers de macOS la capçalera Cookie de Claude per obtenir l'ús web de Claude. Fes clic a OK per continuar."; +"CodexBar will ask macOS Keychain for your Cursor cookie header so it can fetch usage. Click OK to continue." = "CodexBar demanarà a Clauers de macOS la capçalera Cookie de Cursor per obtenir l'ús. Fes clic a OK per continuar."; +"CodexBar will ask macOS Keychain for your Factory cookie header so it can fetch usage. Click OK to continue." = "CodexBar demanarà a Clauers de macOS la capçalera Cookie de Factory per obtenir l'ús. Fes clic a OK per continuar."; +"CodexBar will ask macOS Keychain for your GitHub Copilot token so it can fetch usage. Click OK to continue." = "CodexBar demanarà a Clauers de macOS el token de GitHub Copilot per obtenir l'ús. Fes clic a OK per continuar."; +"CodexBar will ask macOS Keychain for your Kimi K2 API key so it can fetch usage. Click OK to continue." = "CodexBar demanarà a Clauers de macOS la clau API de Kimi K2 per obtenir l'ús. Fes clic a OK per continuar."; +"CodexBar will ask macOS Keychain for your Kimi auth token so it can fetch usage. Click OK to continue." = "CodexBar demanarà a Clauers de macOS el token d'autenticació de Kimi per obtenir l'ús. Fes clic a OK per continuar."; +"CodexBar will ask macOS Keychain for your MiniMax API token so it can fetch usage. Click OK to continue." = "CodexBar demanarà a Clauers de macOS el token API de MiniMax per obtenir l'ús. Fes clic a OK per continuar."; +"CodexBar will ask macOS Keychain for your MiniMax cookie header so it can fetch usage. Click OK to continue." = "CodexBar demanarà a Clauers de macOS la capçalera Cookie de MiniMax per obtenir l'ús. Fes clic a OK per continuar."; +"CodexBar will ask macOS Keychain for your OpenAI cookie header so it can fetch Codex dashboard extras. Click OK to continue." = "CodexBar demanarà a Clauers de macOS la capçalera Cookie d'OpenAI per obtenir extres del tauler de Codex. Fes clic a OK per continuar."; +"CodexBar will ask macOS Keychain for your OpenCode cookie header so it can fetch usage. Click OK to continue." = "CodexBar demanarà a Clauers de macOS la capçalera Cookie d'OpenCode per obtenir l'ús. Fes clic a OK per continuar."; +"CodexBar will ask macOS Keychain for your Synthetic API key so it can fetch usage. Click OK to continue." = "CodexBar demanarà a Clauers de macOS la clau API de Synthetic per obtenir l'ús. Fes clic a OK per continuar."; +"CodexBar will ask macOS Keychain for your z.ai API token so it can fetch usage. Click OK to continue." = "CodexBar demanarà a Clauers de macOS el token API de z.ai per obtenir l'ús. Fes clic a OK per continuar."; +"Could not open Cursor login in your browser." = "No s'ha pogut obrir l'inici de sessió de Cursor al navegador."; +"Could not open browser for Antigravity" = "No s'ha pogut obrir el navegador per a Antigravity"; +"Credits used" = "Crèdits usats"; +"Day" = "Dia"; +"Deployment" = "Desplegament"; +"Drag to reorder" = "Arrossega per reordenar"; +"Endpoint" = "Endpoint"; +"Enterprise host" = "Host Enterprise"; +"Extra usage balance: %@" = "Saldo d'ús extra: %@"; +"Keychain Access Required" = "Cal accés a Clauers"; +"Kiro menu bar value" = "Valor de Kiro a la barra de menús"; +"Label" = "Etiqueta"; +"No organizations loaded. Click Refresh after setting your API key." = "No hi ha organitzacions carregades. Fes clic a Actualitza després de configurar la clau API."; +"No output captured." = "No s'ha capturat cap sortida."; +"No system account" = "Sense compte del sistema"; +"Oasis-Token" = "Oasis-Token"; +"Open Augment (Log Out & Back In)" = "Obre Augment (tanca sessió i torna a entrar)"; +"Open Codebuff Dashboard" = "Obre el tauler de Codebuff"; +"Open Command Code Settings" = "Obre la configuració de Command Code"; +"Open Crof dashboard" = "Obre el tauler de Crof"; +"Open Manus" = "Obre Manus"; +"Open MiMo Balance" = "Obre el saldo de MiMo"; +"Open Moonshot Console" = "Obre la consola de Moonshot"; +"Open Ollama API Keys" = "Obre les claus API d'Ollama"; +"Open StepFun Platform" = "Obre la plataforma StepFun"; +"Open T3 Chat Settings" = "Obre la configuració de T3 Chat"; +"Open Volcengine Ark Console" = "Obre la consola Volcengine Ark"; +"Open legacy provider docs" = "Obre la documentació del proveïdor heretat"; +"Open projects" = "Obre projectes"; +"Open this URL manually to continue login:\n\n%@" = "Obre aquesta URL manualment per continuar l'inici de sessió:\n\n%@"; +"Optional organization ID for accounts linked to multiple Anthropic organizations." = "ID d'organització opcional per a comptes vinculats a diverses organitzacions d'Anthropic."; +"Optional. Applies to the configured Admin API key; selected token accounts do not inherit OPENAI_PROJECT_ID." = "Opcional. S'aplica a la clau Admin API configurada; els comptes de token seleccionats no hereten OPENAI_PROJECT_ID."; +"Optional. Enter your GitHub Enterprise host, for example octocorp.ghe.com. Leave blank for github.com." = "Opcional. Introdueix el host de GitHub Enterprise, per exemple octocorp.ghe.com. Deixa-ho en blanc per a github.com."; +"Optional. Leave blank to discover and aggregate projects visible to the API key." = "Opcional. Deixa-ho en blanc per descobrir i agregar projectes visibles per a la clau API."; +"Org ID (optional)" = "ID d'org. (opcional)"; +"Organizations" = "Organitzacions"; +"Password" = "Contrasenya"; +"Paste a Cookie header or full cURL capture from T3 Chat settings." = "Enganxa una capçalera Cookie o una captura cURL completa de la configuració de T3 Chat."; +"Paste the Cookie header from a request to admin.mistral.ai. Must contain an ory_session_* cookie." = "Enganxa la capçalera Cookie d'una sol·licitud a admin.mistral.ai. Ha de contenir una galeta ory_session_*."; +"Paste the Oasis-Token from a logged-in browser session on platform.stepfun.com." = "Enganxa l'Oasis-Token d'una sessió iniciada a platform.stepfun.com."; +"Personal account" = "Compte personal"; +"Project ID" = "ID de projecte"; +"Re-auth" = "Reautentica"; +"Re-authenticating…" = "S'està reautenticant…"; +"Refresh Session" = "Actualitza la sessió"; +"Refresh organizations" = "Actualitza organitzacions"; +"Region" = "Regió"; +"Reload" = "Recarrega"; +"Reorder" = "Reordena"; +"Secret access key" = "Clau secreta d'accés"; +"Series" = "Sèrie"; +"Service" = "Servei"; +"Show or hide Kiro credits, percent, or both next to the menu bar icon." = "Mostra o amaga crèdits de Kiro, percentatge o tots dos al costat de la icona de la barra de menús."; +"Show usage for organizations you belong to. Personal account is always shown." = "Mostra l'ús de les organitzacions a què pertanys. El compte personal sempre es mostra."; +"Sign in to cursor.com in your browser, then refresh Cursor in CodexBar." = "Inicia sessió a cursor.com al navegador i després actualitza Cursor a CodexBar."; +"Simulated error text" = "Text d'error simulat"; +"StepFun platform account (phone number or email)." = "Compte de la plataforma StepFun (telèfon o correu)."; +"Stored in ~/.codexbar/config.json." = "Desat a ~/.codexbar/config.json."; +"Stored in ~/.codexbar/config.json. AZURE_OPENAI_API_KEY is also supported." = "Desat a ~/.codexbar/config.json. També s'admet AZURE_OPENAI_API_KEY."; +"Stored in ~/.codexbar/config.json. For the official Kimi API, use Moonshot / Kimi API." = "Desat a ~/.codexbar/config.json. Per a l'API oficial de Kimi, usa Moonshot / Kimi API."; +"Stored in ~/.codexbar/config.json. Get your API key from the Volcengine Ark console." = "Desat a ~/.codexbar/config.json. Obtén la clau API a la consola Volcengine Ark."; +"Stored in ~/.codexbar/config.json. Get your key from Ollama settings." = "Desat a ~/.codexbar/config.json. Obtén la clau a la configuració d'Ollama."; +"Stored in ~/.codexbar/config.json. Get your key from console.deepgram.com." = "Desat a ~/.codexbar/config.json. Obtén la clau a console.deepgram.com."; +"Stored in ~/.codexbar/config.json. Get your key from elevenlabs.io/app/settings/api-keys." = "Desat a ~/.codexbar/config.json. Obtén la clau a elevenlabs.io/app/settings/api-keys."; +"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." = "Desat a ~/.codexbar/config.json. Obtén la clau a openrouter.ai/settings/keys i defineix-hi un límit de despesa per activar el seguiment de quota."; +"Stored in ~/.codexbar/config.json. In Warp, open Settings > Platform > API Keys, then create one." = "Desat a ~/.codexbar/config.json. A Warp, obre Settings > Platform > API Keys i crea'n una."; +"Stored in ~/.codexbar/config.json. Metrics require Groq Enterprise Prometheus access." = "Desat a ~/.codexbar/config.json. Les mètriques requereixen accés a Groq Enterprise Prometheus."; +"Stored in ~/.codexbar/config.json. OPENAI_ADMIN_KEY is preferred; OPENAI_API_KEY still works." = "Desat a ~/.codexbar/config.json. Es prefereix OPENAI_ADMIN_KEY; OPENAI_API_KEY encara funciona."; +"Stored in ~/.codexbar/config.json. Requires an Anthropic Admin API key." = "Desat a ~/.codexbar/config.json. Requereix una clau Anthropic Admin API."; +"Stored in ~/.codexbar/config.json. Used for /v1/quota-stats." = "Desat a ~/.codexbar/config.json. S'usa per a /v1/quota-stats."; +"Stored in ~/.codexbar/config.json. You can also provide CODEBUFF_API_KEY or let CodexBar read ~/.config/manicode/credentials.json (created by `codebuff login`)." = "Desat a ~/.codexbar/config.json. També pots proporcionar CODEBUFF_API_KEY o deixar que CodexBar llegeixi ~/.config/manicode/credentials.json (creat per `codebuff login`)."; +"Stored in ~/.codexbar/config.json. You can also provide CROF_API_KEY." = "Desat a ~/.codexbar/config.json. També pots proporcionar CROF_API_KEY."; +"Stored in ~/.codexbar/config.json. You can also provide KILO_API_KEY or ~/.local/share/kilo/auth.json (kilo.access)." = "Desat a ~/.codexbar/config.json. També pots proporcionar KILO_API_KEY o ~/.local/share/kilo/auth.json (kilo.access)."; +"T3 Chat cookie" = "Galeta de T3 Chat"; +"That account is no longer available in CodexBar. Refresh the account list and try again." = "Aquest compte ja no està disponible a CodexBar. Actualitza la llista de comptes i torna-ho a provar."; +"The browser login did not complete in time. Try Antigravity login again." = "L'inici de sessió del navegador no s'ha completat a temps. Torna a provar l'inici de sessió d'Antigravity."; +"Timed out waiting for Cursor login. %@" = "S'ha esgotat el temps esperant l'inici de sessió de Cursor. %@"; +"Timed out waiting for Cursor login. %@ Last error: %@" = "S'ha esgotat el temps esperant l'inici de sessió de Cursor. %@ Últim error: %@"; +"Today requests" = "Sol·licituds d'avui"; +"Total (30d): %@ credits" = "Total (30 d): %@ crèdits"; +"Username" = "Nom d'usuari"; +"Uses username + password to login and obtain an Oasis-Token automatically." = "Usa nom d'usuari i contrasenya per iniciar sessió i obtenir un Oasis-Token automàticament."; +"Utilization End" = "Final d'utilització"; +"Utilization Start" = "Inici d'utilització"; +"Verbosity" = "Detall"; +"Windsurf session JSON bundle" = "Paquet JSON de sessió de Windsurf"; +"Workspace ID" = "ID d'espai de treball"; +"Your StepFun platform password. Used to login and obtain a session token." = "La contrasenya de la plataforma StepFun. S'usa per iniciar sessió i obtenir un token de sessió."; +"claude /login exited with status %d." = "claude /login ha sortit amb estat %d."; +"codex login exited with status %d." = "codex login ha sortit amb estat %d."; +"Cookie: …\n\nor paste a cURL capture from the Abacus AI dashboard" = "Cookie: …\n\no enganxa una captura cURL del tauler d'Abacus AI"; +"Cookie: …\n\nor paste the __Secure-next-auth.session-token value" = "Cookie: …\n\no enganxa el valor de __Secure-next-auth.session-token"; +"Cookie: …\n\nor paste the kimi-auth token value" = "Cookie: …\n\no enganxa el valor del token kimi-auth"; +"session_id=...\n\nor paste just the session_id value" = "session_id=...\n\no enganxa només el valor de session_id"; diff --git a/Sources/CodexBar/Resources/en.lproj/Localizable.strings b/Sources/CodexBar/Resources/en.lproj/Localizable.strings index d5af3e735e..a4af4cd115 100644 --- a/Sources/CodexBar/Resources/en.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/en.lproj/Localizable.strings @@ -883,3 +883,159 @@ "minimax_service_lyrics_generation" = "Lyrics generation"; "minimax_service_coding_plan_vlm" = "Coding plan VLM"; "minimax_service_coding_plan_search" = "Coding plan search"; + +/* Additional provider settings and alerts */ +"%@ is waiting for permission" = "%@ is waiting for permission"; +"%@ requests" = "%@ requests"; +"%@: %@ credits" = "%@: %@ credits"; +"30d requests" = "30d requests"; +"4 days" = "4 days"; +"5 days" = "5 days"; +"7 days" = "7 days"; +"API key verifies Ollama Cloud access; cookies still expose quota limits." = "API key verifies Ollama Cloud access; cookies still expose quota limits."; +"AWS access key ID. Can also be set with AWS_ACCESS_KEY_ID." = "AWS access key ID. Can also be set with AWS_ACCESS_KEY_ID."; +"AWS region. Can also be set with AWS_REGION." = "AWS region. Can also be set with AWS_REGION."; +"AWS secret access key. Can also be set with AWS_SECRET_ACCESS_KEY." = "AWS secret access key. Can also be set with AWS_SECRET_ACCESS_KEY."; +"Access key ID" = "Access key ID"; +"Add Account" = "Add Account"; +"Adding Account…" = "Adding Account…"; +"Antigravity login failed" = "Antigravity login failed"; +"Antigravity login timed out" = "Antigravity login timed out"; +"Auth source" = "Auth source"; +"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."; +"Automatic imports browser cookies from Bailian." = "Automatic imports browser cookies from Bailian."; +"Automatically imports browser cookies." = "Automatically imports browser cookies."; +"Automatically imports browser session cookies." = "Automatically imports browser session cookies."; +"Azure OpenAI deployment name. AZURE_OPENAI_DEPLOYMENT_NAME is also supported." = "Azure OpenAI deployment name. AZURE_OPENAI_DEPLOYMENT_NAME is also supported."; +"Azure OpenAI key" = "Azure OpenAI key"; +"Azure OpenAI resource endpoint. AZURE_OPENAI_ENDPOINT is also supported." = "Azure OpenAI resource endpoint. AZURE_OPENAI_ENDPOINT is also supported."; +"Base URL" = "Base URL"; +"Base URL for the LLM-API-Key-Proxy instance." = "Base URL for the LLM-API-Key-Proxy instance."; +"Browser cookies" = "Browser cookies"; +"Cap end" = "Cap end"; +"Cap start" = "Cap start"; +"Capacity End" = "Capacity End"; +"Capacity Start" = "Capacity Start"; +"Changelog" = "Changelog"; +"Choose the Moonshot/Kimi API host for international or China mainland accounts." = "Choose the Moonshot/Kimi API host for international or China mainland accounts."; +"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 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 managed account storage. Recover the store before adding another account." = "CodexBar could not read managed account storage. Recover the store before adding another account."; +"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 replace the live Codex auth on this Mac." = "CodexBar could not replace the live Codex auth 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 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 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 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."; +"CodexBar will ask macOS Keychain for the Claude Code OAuth token so it can fetch your Claude usage. Click OK to continue." = "CodexBar will ask macOS Keychain for the Claude Code OAuth token so it can fetch your Claude usage. Click OK to continue."; +"CodexBar will ask macOS Keychain for your Amp cookie header so it can fetch usage. Click OK to continue." = "CodexBar will ask macOS Keychain for your Amp cookie header so it can fetch usage. Click OK to continue."; +"CodexBar will ask macOS Keychain for your Augment cookie header so it can fetch usage. Click OK to continue." = "CodexBar will ask macOS Keychain for your Augment cookie header so it can fetch usage. Click OK to continue."; +"CodexBar will ask macOS Keychain for your Claude cookie header so it can fetch Claude web usage. Click OK to continue." = "CodexBar will ask macOS Keychain for your Claude cookie header so it can fetch Claude web usage. Click OK to continue."; +"CodexBar will ask macOS Keychain for your Cursor cookie header so it can fetch usage. Click OK to continue." = "CodexBar will ask macOS Keychain for your Cursor cookie header so it can fetch usage. Click OK to continue."; +"CodexBar will ask macOS Keychain for your Factory cookie header so it can fetch usage. Click OK to continue." = "CodexBar will ask macOS Keychain for your Factory cookie header so it can fetch usage. Click OK to continue."; +"CodexBar will ask macOS Keychain for your GitHub Copilot token so it can fetch usage. Click OK to continue." = "CodexBar will ask macOS Keychain for your GitHub Copilot token so it can fetch usage. Click OK to continue."; +"CodexBar will ask macOS Keychain for your Kimi K2 API key so it can fetch usage. Click OK to continue." = "CodexBar will ask macOS Keychain for your Kimi K2 API key so it can fetch usage. Click OK to continue."; +"CodexBar will ask macOS Keychain for your Kimi auth token so it can fetch usage. Click OK to continue." = "CodexBar will ask macOS Keychain for your Kimi auth token so it can fetch usage. Click OK to continue."; +"CodexBar will ask macOS Keychain for your MiniMax API token so it can fetch usage. Click OK to continue." = "CodexBar will ask macOS Keychain for your MiniMax API token so it can fetch usage. Click OK to continue."; +"CodexBar will ask macOS Keychain for your MiniMax cookie header so it can fetch usage. Click OK to continue." = "CodexBar will ask macOS Keychain for your MiniMax cookie header so it can fetch usage. Click OK to continue."; +"CodexBar will ask macOS Keychain for your OpenAI cookie header so it can fetch Codex dashboard extras. Click OK to continue." = "CodexBar will ask macOS Keychain for your OpenAI cookie header so it can fetch Codex dashboard extras. Click OK to continue."; +"CodexBar will ask macOS Keychain for your OpenCode cookie header so it can fetch usage. Click OK to continue." = "CodexBar will ask macOS Keychain for your OpenCode cookie header so it can fetch usage. Click OK to continue."; +"CodexBar will ask macOS Keychain for your Synthetic API key so it can fetch usage. Click OK to continue." = "CodexBar will ask macOS Keychain for your Synthetic API key so it can fetch usage. Click OK to continue."; +"CodexBar will ask macOS Keychain for your z.ai API token so it can fetch usage. Click OK to continue." = "CodexBar will ask macOS Keychain for your z.ai API token so it can fetch usage. Click OK to continue."; +"Could not open Cursor login in your browser." = "Could not open Cursor login in your browser."; +"Could not open browser for Antigravity" = "Could not open browser for Antigravity"; +"Credits used" = "Credits used"; +"Day" = "Day"; +"Deployment" = "Deployment"; +"Drag to reorder" = "Drag to reorder"; +"Endpoint" = "Endpoint"; +"Enterprise host" = "Enterprise host"; +"Extra usage balance: %@" = "Extra usage balance: %@"; +"Keychain Access Required" = "Keychain Access Required"; +"Kiro menu bar value" = "Kiro menu bar value"; +"Label" = "Label"; +"No organizations loaded. Click Refresh after setting your API key." = "No organizations loaded. Click Refresh after setting your API key."; +"No output captured." = "No output captured."; +"No system account" = "No system account"; +"Oasis-Token" = "Oasis-Token"; +"Open Augment (Log Out & Back In)" = "Open Augment (Log Out & Back In)"; +"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 Ollama API Keys" = "Open Ollama API Keys"; +"Open StepFun Platform" = "Open StepFun Platform"; +"Open T3 Chat Settings" = "Open T3 Chat Settings"; +"Open Volcengine Ark Console" = "Open Volcengine Ark Console"; +"Open legacy provider docs" = "Open legacy provider docs"; +"Open projects" = "Open projects"; +"Open this URL manually to continue login:\n\n%@" = "Open this URL manually to continue login:\n\n%@"; +"Optional organization ID for accounts linked to multiple Anthropic organizations." = "Optional organization ID for accounts linked to multiple Anthropic organizations."; +"Optional. Applies to the configured Admin API key; selected token accounts do not inherit OPENAI_PROJECT_ID." = "Optional. Applies to the configured Admin API key; selected token accounts do not inherit OPENAI_PROJECT_ID."; +"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."; +"Optional. Leave blank to discover and aggregate projects visible to the API key." = "Optional. Leave blank to discover and aggregate projects visible to the API key."; +"Org ID (optional)" = "Org ID (optional)"; +"Organizations" = "Organizations"; +"Password" = "Password"; +"Paste a Cookie header or full cURL capture from T3 Chat settings." = "Paste a Cookie header or full cURL capture from T3 Chat settings."; +"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."; +"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."; +"Personal account" = "Personal account"; +"Project ID" = "Project ID"; +"Re-auth" = "Re-auth"; +"Re-authenticating…" = "Re-authenticating…"; +"Refresh Session" = "Refresh Session"; +"Refresh organizations" = "Refresh organizations"; +"Region" = "Region"; +"Reload" = "Reload"; +"Reorder" = "Reorder"; +"Secret access key" = "Secret access key"; +"Series" = "Series"; +"Service" = "Service"; +"Show or hide Kiro credits, percent, or both next to the menu bar icon." = "Show or hide Kiro credits, percent, or both next to the menu bar icon."; +"Show usage for organizations you belong to. Personal account is always shown." = "Show usage for organizations you belong to. Personal account is always shown."; +"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."; +"Simulated error text" = "Simulated error text"; +"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. AZURE_OPENAI_API_KEY is also supported." = "Stored in ~/.codexbar/config.json. AZURE_OPENAI_API_KEY is also supported."; +"Stored in ~/.codexbar/config.json. For the official Kimi API, use Moonshot / Kimi API." = "Stored in ~/.codexbar/config.json. For the official Kimi API, use Moonshot / Kimi API."; +"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 Ollama settings." = "Stored in ~/.codexbar/config.json. Get your key from Ollama settings."; +"Stored in ~/.codexbar/config.json. Get your key from console.deepgram.com." = "Stored in ~/.codexbar/config.json. Get your key from console.deepgram.com."; +"Stored in ~/.codexbar/config.json. Get your key from elevenlabs.io/app/settings/api-keys." = "Stored in ~/.codexbar/config.json. Get your key from elevenlabs.io/app/settings/api-keys."; +"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. Metrics require Groq Enterprise Prometheus access." = "Stored in ~/.codexbar/config.json. Metrics require Groq Enterprise Prometheus access."; +"Stored in ~/.codexbar/config.json. OPENAI_ADMIN_KEY is preferred; OPENAI_API_KEY still works." = "Stored in ~/.codexbar/config.json. OPENAI_ADMIN_KEY is preferred; OPENAI_API_KEY still works."; +"Stored in ~/.codexbar/config.json. Requires an Anthropic Admin API key." = "Stored in ~/.codexbar/config.json. Requires an Anthropic Admin API key."; +"Stored in ~/.codexbar/config.json. Used for /v1/quota-stats." = "Stored in ~/.codexbar/config.json. Used for /v1/quota-stats."; +"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)."; +"T3 Chat cookie" = "T3 Chat cookie"; +"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."; +"The browser login did not complete in time. Try Antigravity login again." = "The browser login did not complete in time. Try Antigravity login again."; +"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: %@"; +"Today requests" = "Today requests"; +"Total (30d): %@ credits" = "Total (30d): %@ credits"; +"Username" = "Username"; +"Uses username + password to login and obtain an Oasis-Token automatically." = "Uses username + password to login and obtain an Oasis-Token automatically."; +"Utilization End" = "Utilization End"; +"Utilization Start" = "Utilization Start"; +"Verbosity" = "Verbosity"; +"Windsurf session JSON bundle" = "Windsurf session JSON bundle"; +"Workspace ID" = "Workspace ID"; +"Your StepFun platform password. Used to login and obtain a session token." = "Your StepFun platform password. Used to login and obtain a session token."; +"claude /login exited with status %d." = "claude /login exited with status %d."; +"codex login exited with status %d." = "codex login exited with status %d."; +"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"; +"session_id=...\n\nor paste just the session_id value" = "session_id=...\n\nor paste just the session_id value"; diff --git a/Sources/CodexBar/Resources/es.lproj/Localizable.strings b/Sources/CodexBar/Resources/es.lproj/Localizable.strings index 425edb56dd..41d2b201a7 100644 --- a/Sources/CodexBar/Resources/es.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/es.lproj/Localizable.strings @@ -740,3 +740,159 @@ "No usage yet" = "Aún no hay uso"; "Not fetched yet" = "Aún no obtenido"; "Code review" = "Revisión de código"; + +/* Additional provider settings and alerts */ +"%@ is waiting for permission" = "%@ espera permiso"; +"%@ requests" = "%@ solicitudes"; +"%@: %@ credits" = "%@: %@ créditos"; +"30d requests" = "Solicitudes de 30 d"; +"4 days" = "4 días"; +"5 days" = "5 días"; +"7 days" = "7 días"; +"API key verifies Ollama Cloud access; cookies still expose quota limits." = "La clave API verifica el acceso a Ollama Cloud; las cookies aún muestran los límites de cuota."; +"AWS access key ID. Can also be set with AWS_ACCESS_KEY_ID." = "ID de clave de acceso de AWS. También puede definirse con AWS_ACCESS_KEY_ID."; +"AWS region. Can also be set with AWS_REGION." = "Región de AWS. También puede definirse con AWS_REGION."; +"AWS secret access key. Can also be set with AWS_SECRET_ACCESS_KEY." = "Clave secreta de AWS. También puede definirse con AWS_SECRET_ACCESS_KEY."; +"Access key ID" = "ID de clave de acceso"; +"Add Account" = "Añadir cuenta"; +"Adding Account…" = "Añadiendo cuenta…"; +"Antigravity login failed" = "Error al iniciar sesión en Antigravity"; +"Antigravity login timed out" = "El inicio de sesión en Antigravity agotó el tiempo"; +"Auth source" = "Fuente de autenticación"; +"Automatic imports Chrome browser cookies from Xiaomi MiMo." = "Importa automáticamente las cookies de Chrome desde Xiaomi MiMo."; +"Automatic imports Windsurf session data from Chromium browser localStorage." = "Importa automáticamente datos de sesión de Windsurf desde localStorage de Chromium."; +"Automatic imports browser cookies from Bailian." = "Importa automáticamente cookies del navegador desde Bailian."; +"Automatically imports browser cookies." = "Importa automáticamente cookies del navegador."; +"Automatically imports browser session cookies." = "Importa automáticamente cookies de sesión del navegador."; +"Azure OpenAI deployment name. AZURE_OPENAI_DEPLOYMENT_NAME is also supported." = "Nombre de despliegue de Azure OpenAI. También se admite AZURE_OPENAI_DEPLOYMENT_NAME."; +"Azure OpenAI key" = "Clave de Azure OpenAI"; +"Azure OpenAI resource endpoint. AZURE_OPENAI_ENDPOINT is also supported." = "Endpoint del recurso de Azure OpenAI. También se admite AZURE_OPENAI_ENDPOINT."; +"Base URL" = "URL base"; +"Base URL for the LLM-API-Key-Proxy instance." = "URL base de la instancia de LLM-API-Key-Proxy."; +"Browser cookies" = "Cookies del navegador"; +"Cap end" = "Fin del límite"; +"Cap start" = "Inicio del límite"; +"Capacity End" = "Fin de capacidad"; +"Capacity Start" = "Inicio de capacidad"; +"Changelog" = "Registro de cambios"; +"Choose the Moonshot/Kimi API host for international or China mainland accounts." = "Elige el host de la API Moonshot/Kimi para cuentas internacionales o de China continental."; +"CodexBar can't replace a system account that is signed in with an API key only setup." = "CodexBar no puede reemplazar una cuenta del sistema iniciada solo con una clave API."; +"CodexBar could not find saved auth for that account. Re-authenticate it and try again." = "CodexBar no encontró autenticación guardada para esa cuenta. Vuelve a autenticarla e inténtalo de nuevo."; +"CodexBar could not read managed account storage. Recover the store before adding another account." = "CodexBar no pudo leer el almacenamiento de cuentas gestionadas. Recupera el almacén antes de añadir otra cuenta."; +"CodexBar could not read saved auth for that account. Re-authenticate it and try again." = "CodexBar no pudo leer la autenticación guardada para esa cuenta. Vuelve a autenticarla e inténtalo de nuevo."; +"CodexBar could not read the current system account on this Mac." = "CodexBar no pudo leer la cuenta del sistema actual en este Mac."; +"CodexBar could not replace the live Codex auth on this Mac." = "CodexBar no pudo reemplazar la autenticación activa de Codex en este Mac."; +"CodexBar could not safely preserve the current system account before switching." = "CodexBar no pudo preservar con seguridad la cuenta del sistema actual antes de cambiar."; +"CodexBar could not save the current system account before switching." = "CodexBar no pudo guardar la cuenta del sistema actual antes de cambiar."; +"CodexBar could not update managed account storage." = "CodexBar no pudo actualizar el almacenamiento de cuentas gestionadas."; +"CodexBar found another managed account that already uses the current system account. Resolve the duplicate account before switching." = "CodexBar encontró otra cuenta gestionada que ya usa la cuenta del sistema actual. Resuelve la cuenta duplicada antes de cambiar."; +"CodexBar will ask macOS Keychain for “%@” so it can decrypt browser cookies and authenticate your account. Click OK to continue." = "CodexBar pedirá a Llaveros de macOS “%@” para descifrar cookies del navegador y autenticar tu cuenta. Haz clic en OK para continuar."; +"CodexBar will ask macOS Keychain for the Claude Code OAuth token so it can fetch your Claude usage. Click OK to continue." = "CodexBar pedirá a Llaveros de macOS el token OAuth de Claude Code para obtener tu uso de Claude. Haz clic en OK para continuar."; +"CodexBar will ask macOS Keychain for your Amp cookie header so it can fetch usage. Click OK to continue." = "CodexBar pedirá a Llaveros de macOS tu cabecera Cookie de Amp para obtener el uso. Haz clic en OK para continuar."; +"CodexBar will ask macOS Keychain for your Augment cookie header so it can fetch usage. Click OK to continue." = "CodexBar pedirá a Llaveros de macOS tu cabecera Cookie de Augment para obtener el uso. Haz clic en OK para continuar."; +"CodexBar will ask macOS Keychain for your Claude cookie header so it can fetch Claude web usage. Click OK to continue." = "CodexBar pedirá a Llaveros de macOS tu cabecera Cookie de Claude para obtener el uso web de Claude. Haz clic en OK para continuar."; +"CodexBar will ask macOS Keychain for your Cursor cookie header so it can fetch usage. Click OK to continue." = "CodexBar pedirá a Llaveros de macOS tu cabecera Cookie de Cursor para obtener el uso. Haz clic en OK para continuar."; +"CodexBar will ask macOS Keychain for your Factory cookie header so it can fetch usage. Click OK to continue." = "CodexBar pedirá a Llaveros de macOS tu cabecera Cookie de Factory para obtener el uso. Haz clic en OK para continuar."; +"CodexBar will ask macOS Keychain for your GitHub Copilot token so it can fetch usage. Click OK to continue." = "CodexBar pedirá a Llaveros de macOS tu token de GitHub Copilot para obtener el uso. Haz clic en OK para continuar."; +"CodexBar will ask macOS Keychain for your Kimi K2 API key so it can fetch usage. Click OK to continue." = "CodexBar pedirá a Llaveros de macOS tu clave API de Kimi K2 para obtener el uso. Haz clic en OK para continuar."; +"CodexBar will ask macOS Keychain for your Kimi auth token so it can fetch usage. Click OK to continue." = "CodexBar pedirá a Llaveros de macOS tu token de autenticación de Kimi para obtener el uso. Haz clic en OK para continuar."; +"CodexBar will ask macOS Keychain for your MiniMax API token so it can fetch usage. Click OK to continue." = "CodexBar pedirá a Llaveros de macOS tu token API de MiniMax para obtener el uso. Haz clic en OK para continuar."; +"CodexBar will ask macOS Keychain for your MiniMax cookie header so it can fetch usage. Click OK to continue." = "CodexBar pedirá a Llaveros de macOS tu cabecera Cookie de MiniMax para obtener el uso. Haz clic en OK para continuar."; +"CodexBar will ask macOS Keychain for your OpenAI cookie header so it can fetch Codex dashboard extras. Click OK to continue." = "CodexBar pedirá a Llaveros de macOS tu cabecera Cookie de OpenAI para obtener extras del panel de Codex. Haz clic en OK para continuar."; +"CodexBar will ask macOS Keychain for your OpenCode cookie header so it can fetch usage. Click OK to continue." = "CodexBar pedirá a Llaveros de macOS tu cabecera Cookie de OpenCode para obtener el uso. Haz clic en OK para continuar."; +"CodexBar will ask macOS Keychain for your Synthetic API key so it can fetch usage. Click OK to continue." = "CodexBar pedirá a Llaveros de macOS tu clave API de Synthetic para obtener el uso. Haz clic en OK para continuar."; +"CodexBar will ask macOS Keychain for your z.ai API token so it can fetch usage. Click OK to continue." = "CodexBar pedirá a Llaveros de macOS tu token API de z.ai para obtener el uso. Haz clic en OK para continuar."; +"Could not open Cursor login in your browser." = "No se pudo abrir el inicio de sesión de Cursor en el navegador."; +"Could not open browser for Antigravity" = "No se pudo abrir el navegador para Antigravity"; +"Credits used" = "Créditos usados"; +"Day" = "Día"; +"Deployment" = "Despliegue"; +"Drag to reorder" = "Arrastra para reordenar"; +"Endpoint" = "Endpoint"; +"Enterprise host" = "Host Enterprise"; +"Extra usage balance: %@" = "Saldo de uso extra: %@"; +"Keychain Access Required" = "Se requiere acceso a Llaveros"; +"Kiro menu bar value" = "Valor de Kiro en la barra de menús"; +"Label" = "Etiqueta"; +"No organizations loaded. Click Refresh after setting your API key." = "No hay organizaciones cargadas. Haz clic en Actualizar después de configurar tu clave API."; +"No output captured." = "No se capturó salida."; +"No system account" = "Sin cuenta del sistema"; +"Oasis-Token" = "Oasis-Token"; +"Open Augment (Log Out & Back In)" = "Abrir Augment (cerrar sesión y volver a entrar)"; +"Open Codebuff Dashboard" = "Abrir panel de Codebuff"; +"Open Command Code Settings" = "Abrir ajustes de Command Code"; +"Open Crof dashboard" = "Abrir panel de Crof"; +"Open Manus" = "Abrir Manus"; +"Open MiMo Balance" = "Abrir saldo de MiMo"; +"Open Moonshot Console" = "Abrir consola de Moonshot"; +"Open Ollama API Keys" = "Abrir claves API de Ollama"; +"Open StepFun Platform" = "Abrir plataforma StepFun"; +"Open T3 Chat Settings" = "Abrir ajustes de T3 Chat"; +"Open Volcengine Ark Console" = "Abrir consola Volcengine Ark"; +"Open legacy provider docs" = "Abrir documentación del proveedor heredado"; +"Open projects" = "Abrir proyectos"; +"Open this URL manually to continue login:\n\n%@" = "Abre esta URL manualmente para continuar el inicio de sesión:\n\n%@"; +"Optional organization ID for accounts linked to multiple Anthropic organizations." = "ID de organización opcional para cuentas vinculadas a varias organizaciones de Anthropic."; +"Optional. Applies to the configured Admin API key; selected token accounts do not inherit OPENAI_PROJECT_ID." = "Opcional. Se aplica a la clave Admin API configurada; las cuentas de token seleccionadas no heredan OPENAI_PROJECT_ID."; +"Optional. Enter your GitHub Enterprise host, for example octocorp.ghe.com. Leave blank for github.com." = "Opcional. Introduce tu host de GitHub Enterprise, por ejemplo octocorp.ghe.com. Déjalo vacío para github.com."; +"Optional. Leave blank to discover and aggregate projects visible to the API key." = "Opcional. Déjalo vacío para descubrir y agregar proyectos visibles para la clave API."; +"Org ID (optional)" = "ID de org. (opcional)"; +"Organizations" = "Organizaciones"; +"Password" = "Contraseña"; +"Paste a Cookie header or full cURL capture from T3 Chat settings." = "Pega una cabecera Cookie o una captura cURL completa desde los ajustes de T3 Chat."; +"Paste the Cookie header from a request to admin.mistral.ai. Must contain an ory_session_* cookie." = "Pega la cabecera Cookie de una solicitud a admin.mistral.ai. Debe contener una cookie ory_session_*."; +"Paste the Oasis-Token from a logged-in browser session on platform.stepfun.com." = "Pega el Oasis-Token de una sesión iniciada en platform.stepfun.com."; +"Personal account" = "Cuenta personal"; +"Project ID" = "ID de proyecto"; +"Re-auth" = "Reautenticar"; +"Re-authenticating…" = "Reautenticando…"; +"Refresh Session" = "Actualizar sesión"; +"Refresh organizations" = "Actualizar organizaciones"; +"Region" = "Región"; +"Reload" = "Recargar"; +"Reorder" = "Reordenar"; +"Secret access key" = "Clave de acceso secreta"; +"Series" = "Serie"; +"Service" = "Servicio"; +"Show or hide Kiro credits, percent, or both next to the menu bar icon." = "Muestra u oculta créditos de Kiro, porcentaje o ambos junto al icono de la barra de menús."; +"Show usage for organizations you belong to. Personal account is always shown." = "Muestra el uso de las organizaciones a las que perteneces. La cuenta personal siempre se muestra."; +"Sign in to cursor.com in your browser, then refresh Cursor in CodexBar." = "Inicia sesión en cursor.com en el navegador y luego actualiza Cursor en CodexBar."; +"Simulated error text" = "Texto de error simulado"; +"StepFun platform account (phone number or email)." = "Cuenta de la plataforma StepFun (teléfono o correo)."; +"Stored in ~/.codexbar/config.json." = "Guardado en ~/.codexbar/config.json."; +"Stored in ~/.codexbar/config.json. AZURE_OPENAI_API_KEY is also supported." = "Guardado en ~/.codexbar/config.json. También se admite AZURE_OPENAI_API_KEY."; +"Stored in ~/.codexbar/config.json. For the official Kimi API, use Moonshot / Kimi API." = "Guardado en ~/.codexbar/config.json. Para la API oficial de Kimi, usa Moonshot / Kimi API."; +"Stored in ~/.codexbar/config.json. Get your API key from the Volcengine Ark console." = "Guardado en ~/.codexbar/config.json. Obtén tu clave API en la consola Volcengine Ark."; +"Stored in ~/.codexbar/config.json. Get your key from Ollama settings." = "Guardado en ~/.codexbar/config.json. Obtén tu clave en los ajustes de Ollama."; +"Stored in ~/.codexbar/config.json. Get your key from console.deepgram.com." = "Guardado en ~/.codexbar/config.json. Obtén tu clave en console.deepgram.com."; +"Stored in ~/.codexbar/config.json. Get your key from elevenlabs.io/app/settings/api-keys." = "Guardado en ~/.codexbar/config.json. Obtén tu clave en elevenlabs.io/app/settings/api-keys."; +"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." = "Guardado en ~/.codexbar/config.json. Obtén tu clave en openrouter.ai/settings/keys y define allí un límite de gasto para activar el seguimiento de cuota."; +"Stored in ~/.codexbar/config.json. In Warp, open Settings > Platform > API Keys, then create one." = "Guardado en ~/.codexbar/config.json. En Warp, abre Settings > Platform > API Keys y crea una."; +"Stored in ~/.codexbar/config.json. Metrics require Groq Enterprise Prometheus access." = "Guardado en ~/.codexbar/config.json. Las métricas requieren acceso a Groq Enterprise Prometheus."; +"Stored in ~/.codexbar/config.json. OPENAI_ADMIN_KEY is preferred; OPENAI_API_KEY still works." = "Guardado en ~/.codexbar/config.json. Se prefiere OPENAI_ADMIN_KEY; OPENAI_API_KEY también funciona."; +"Stored in ~/.codexbar/config.json. Requires an Anthropic Admin API key." = "Guardado en ~/.codexbar/config.json. Requiere una clave Anthropic Admin API."; +"Stored in ~/.codexbar/config.json. Used for /v1/quota-stats." = "Guardado en ~/.codexbar/config.json. Se usa para /v1/quota-stats."; +"Stored in ~/.codexbar/config.json. You can also provide CODEBUFF_API_KEY or let CodexBar read ~/.config/manicode/credentials.json (created by `codebuff login`)." = "Guardado en ~/.codexbar/config.json. También puedes proporcionar CODEBUFF_API_KEY o dejar que CodexBar lea ~/.config/manicode/credentials.json (creado por `codebuff login`)."; +"Stored in ~/.codexbar/config.json. You can also provide CROF_API_KEY." = "Guardado en ~/.codexbar/config.json. También puedes proporcionar CROF_API_KEY."; +"Stored in ~/.codexbar/config.json. You can also provide KILO_API_KEY or ~/.local/share/kilo/auth.json (kilo.access)." = "Guardado en ~/.codexbar/config.json. También puedes proporcionar KILO_API_KEY o ~/.local/share/kilo/auth.json (kilo.access)."; +"T3 Chat cookie" = "Cookie de T3 Chat"; +"That account is no longer available in CodexBar. Refresh the account list and try again." = "Esa cuenta ya no está disponible en CodexBar. Actualiza la lista de cuentas e inténtalo de nuevo."; +"The browser login did not complete in time. Try Antigravity login again." = "El inicio de sesión del navegador no terminó a tiempo. Intenta iniciar sesión en Antigravity de nuevo."; +"Timed out waiting for Cursor login. %@" = "Se agotó el tiempo esperando el inicio de sesión de Cursor. %@"; +"Timed out waiting for Cursor login. %@ Last error: %@" = "Se agotó el tiempo esperando el inicio de sesión de Cursor. %@ Último error: %@"; +"Today requests" = "Solicitudes de hoy"; +"Total (30d): %@ credits" = "Total (30 d): %@ créditos"; +"Username" = "Usuario"; +"Uses username + password to login and obtain an Oasis-Token automatically." = "Usa usuario y contraseña para iniciar sesión y obtener un Oasis-Token automáticamente."; +"Utilization End" = "Fin de utilización"; +"Utilization Start" = "Inicio de utilización"; +"Verbosity" = "Detalle"; +"Windsurf session JSON bundle" = "Paquete JSON de sesión de Windsurf"; +"Workspace ID" = "ID de espacio de trabajo"; +"Your StepFun platform password. Used to login and obtain a session token." = "Tu contraseña de la plataforma StepFun. Se usa para iniciar sesión y obtener un token de sesión."; +"claude /login exited with status %d." = "claude /login salió con estado %d."; +"codex login exited with status %d." = "codex login salió con estado %d."; +"Cookie: …\n\nor paste a cURL capture from the Abacus AI dashboard" = "Cookie: …\n\no pega una captura cURL del panel de Abacus AI"; +"Cookie: …\n\nor paste the __Secure-next-auth.session-token value" = "Cookie: …\n\no pega el valor de __Secure-next-auth.session-token"; +"Cookie: …\n\nor paste the kimi-auth token value" = "Cookie: …\n\no pega el valor del token kimi-auth"; +"session_id=...\n\nor paste just the session_id value" = "session_id=...\n\no pega solo el valor de session_id"; diff --git a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings index 6edcfb0f5c..181301541b 100644 --- a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings @@ -740,3 +740,159 @@ "No usage yet" = "Ainda sem uso"; "Not fetched yet" = "Ainda não buscado"; "Code review" = "Revisão de código"; + +/* Additional provider settings and alerts */ +"%@ is waiting for permission" = "%@ está aguardando permissão"; +"%@ requests" = "%@ solicitações"; +"%@: %@ credits" = "%@: %@ créditos"; +"30d requests" = "Solicitações de 30 dias"; +"4 days" = "4 dias"; +"5 days" = "5 dias"; +"7 days" = "7 dias"; +"API key verifies Ollama Cloud access; cookies still expose quota limits." = "A chave de API verifica o acesso ao Ollama Cloud; os cookies ainda expõem limites de cota."; +"AWS access key ID. Can also be set with AWS_ACCESS_KEY_ID." = "ID da chave de acesso da AWS. Também pode ser definido com AWS_ACCESS_KEY_ID."; +"AWS region. Can also be set with AWS_REGION." = "Região da AWS. Também pode ser definida com AWS_REGION."; +"AWS secret access key. Can also be set with AWS_SECRET_ACCESS_KEY." = "Chave secreta de acesso da AWS. Também pode ser definida com AWS_SECRET_ACCESS_KEY."; +"Access key ID" = "ID da chave de acesso"; +"Add Account" = "Adicionar conta"; +"Adding Account…" = "Adicionando conta…"; +"Antigravity login failed" = "Falha no login do Antigravity"; +"Antigravity login timed out" = "Tempo esgotado no login do Antigravity"; +"Auth source" = "Fonte de autenticação"; +"Automatic imports Chrome browser cookies from Xiaomi MiMo." = "Importa automaticamente cookies do Chrome do Xiaomi MiMo."; +"Automatic imports Windsurf session data from Chromium browser localStorage." = "Importa automaticamente dados de sessão do Windsurf do localStorage do Chromium."; +"Automatic imports browser cookies from Bailian." = "Importa automaticamente cookies do navegador do Bailian."; +"Automatically imports browser cookies." = "Importa automaticamente cookies do navegador."; +"Automatically imports browser session cookies." = "Importa automaticamente cookies de sessão do navegador."; +"Azure OpenAI deployment name. AZURE_OPENAI_DEPLOYMENT_NAME is also supported." = "Nome do deployment do Azure OpenAI. AZURE_OPENAI_DEPLOYMENT_NAME também é aceito."; +"Azure OpenAI key" = "Chave do Azure OpenAI"; +"Azure OpenAI resource endpoint. AZURE_OPENAI_ENDPOINT is also supported." = "Endpoint do recurso Azure OpenAI. AZURE_OPENAI_ENDPOINT também é aceito."; +"Base URL" = "URL base"; +"Base URL for the LLM-API-Key-Proxy instance." = "URL base da instância LLM-API-Key-Proxy."; +"Browser cookies" = "Cookies do navegador"; +"Cap end" = "Fim do limite"; +"Cap start" = "Início do limite"; +"Capacity End" = "Fim da capacidade"; +"Capacity Start" = "Início da capacidade"; +"Changelog" = "Registro de alterações"; +"Choose the Moonshot/Kimi API host for international or China mainland accounts." = "Escolha o host da API Moonshot/Kimi para contas internacionais ou da China continental."; +"CodexBar can't replace a system account that is signed in with an API key only setup." = "O CodexBar não pode substituir uma conta do sistema conectada apenas com chave de API."; +"CodexBar could not find saved auth for that account. Re-authenticate it and try again." = "O CodexBar não encontrou autenticação salva para essa conta. Reautentique e tente novamente."; +"CodexBar could not read managed account storage. Recover the store before adding another account." = "O CodexBar não conseguiu ler o armazenamento de contas gerenciadas. Recupere o armazenamento antes de adicionar outra conta."; +"CodexBar could not read saved auth for that account. Re-authenticate it and try again." = "O CodexBar não conseguiu ler a autenticação salva dessa conta. Reautentique e tente novamente."; +"CodexBar could not read the current system account on this Mac." = "O CodexBar não conseguiu ler a conta do sistema atual neste Mac."; +"CodexBar could not replace the live Codex auth on this Mac." = "O CodexBar não conseguiu substituir a autenticação ativa do Codex neste Mac."; +"CodexBar could not safely preserve the current system account before switching." = "O CodexBar não conseguiu preservar com segurança a conta do sistema atual antes da troca."; +"CodexBar could not save the current system account before switching." = "O CodexBar não conseguiu salvar a conta do sistema atual antes da troca."; +"CodexBar could not update managed account storage." = "O CodexBar não conseguiu atualizar o armazenamento de contas gerenciadas."; +"CodexBar found another managed account that already uses the current system account. Resolve the duplicate account before switching." = "O CodexBar encontrou outra conta gerenciada que já usa a conta do sistema atual. Resolva a conta duplicada antes de trocar."; +"CodexBar will ask macOS Keychain for “%@” so it can decrypt browser cookies and authenticate your account. Click OK to continue." = "O CodexBar pedirá ao Chaves do macOS “%@” para descriptografar cookies do navegador e autenticar sua conta. Clique em OK para continuar."; +"CodexBar will ask macOS Keychain for the Claude Code OAuth token so it can fetch your Claude usage. Click OK to continue." = "O CodexBar pedirá ao Chaves do macOS o token OAuth do Claude Code para buscar seu uso do Claude. Clique em OK para continuar."; +"CodexBar will ask macOS Keychain for your Amp cookie header so it can fetch usage. Click OK to continue." = "O CodexBar pedirá ao Chaves do macOS o cabeçalho Cookie do Amp para buscar uso. Clique em OK para continuar."; +"CodexBar will ask macOS Keychain for your Augment cookie header so it can fetch usage. Click OK to continue." = "O CodexBar pedirá ao Chaves do macOS o cabeçalho Cookie do Augment para buscar uso. Clique em OK para continuar."; +"CodexBar will ask macOS Keychain for your Claude cookie header so it can fetch Claude web usage. Click OK to continue." = "O CodexBar pedirá ao Chaves do macOS o cabeçalho Cookie do Claude para buscar uso web do Claude. Clique em OK para continuar."; +"CodexBar will ask macOS Keychain for your Cursor cookie header so it can fetch usage. Click OK to continue." = "O CodexBar pedirá ao Chaves do macOS o cabeçalho Cookie do Cursor para buscar uso. Clique em OK para continuar."; +"CodexBar will ask macOS Keychain for your Factory cookie header so it can fetch usage. Click OK to continue." = "O CodexBar pedirá ao Chaves do macOS o cabeçalho Cookie do Factory para buscar uso. Clique em OK para continuar."; +"CodexBar will ask macOS Keychain for your GitHub Copilot token so it can fetch usage. Click OK to continue." = "O CodexBar pedirá ao Chaves do macOS o token do GitHub Copilot para buscar uso. Clique em OK para continuar."; +"CodexBar will ask macOS Keychain for your Kimi K2 API key so it can fetch usage. Click OK to continue." = "O CodexBar pedirá ao Chaves do macOS a chave API do Kimi K2 para buscar uso. Clique em OK para continuar."; +"CodexBar will ask macOS Keychain for your Kimi auth token so it can fetch usage. Click OK to continue." = "O CodexBar pedirá ao Chaves do macOS o token de autenticação do Kimi para buscar uso. Clique em OK para continuar."; +"CodexBar will ask macOS Keychain for your MiniMax API token so it can fetch usage. Click OK to continue." = "O CodexBar pedirá ao Chaves do macOS o token API do MiniMax para buscar uso. Clique em OK para continuar."; +"CodexBar will ask macOS Keychain for your MiniMax cookie header so it can fetch usage. Click OK to continue." = "O CodexBar pedirá ao Chaves do macOS o cabeçalho Cookie do MiniMax para buscar uso. Clique em OK para continuar."; +"CodexBar will ask macOS Keychain for your OpenAI cookie header so it can fetch Codex dashboard extras. Click OK to continue." = "O CodexBar pedirá ao Chaves do macOS o cabeçalho Cookie da OpenAI para buscar extras do painel Codex. Clique em OK para continuar."; +"CodexBar will ask macOS Keychain for your OpenCode cookie header so it can fetch usage. Click OK to continue." = "O CodexBar pedirá ao Chaves do macOS o cabeçalho Cookie do OpenCode para buscar uso. Clique em OK para continuar."; +"CodexBar will ask macOS Keychain for your Synthetic API key so it can fetch usage. Click OK to continue." = "O CodexBar pedirá ao Chaves do macOS a chave API do Synthetic para buscar uso. Clique em OK para continuar."; +"CodexBar will ask macOS Keychain for your z.ai API token so it can fetch usage. Click OK to continue." = "O CodexBar pedirá ao Chaves do macOS o token API do z.ai para buscar uso. Clique em OK para continuar."; +"Could not open Cursor login in your browser." = "Não foi possível abrir o login do Cursor no navegador."; +"Could not open browser for Antigravity" = "Não foi possível abrir o navegador para Antigravity"; +"Credits used" = "Créditos usados"; +"Day" = "Dia"; +"Deployment" = "Deployment"; +"Drag to reorder" = "Arraste para reordenar"; +"Endpoint" = "Endpoint"; +"Enterprise host" = "Host Enterprise"; +"Extra usage balance: %@" = "Saldo de uso extra: %@"; +"Keychain Access Required" = "Acesso ao Chaves necessário"; +"Kiro menu bar value" = "Valor do Kiro na barra de menu"; +"Label" = "Rótulo"; +"No organizations loaded. Click Refresh after setting your API key." = "Nenhuma organização carregada. Clique em Atualizar depois de definir sua chave API."; +"No output captured." = "Nenhuma saída capturada."; +"No system account" = "Sem conta do sistema"; +"Oasis-Token" = "Oasis-Token"; +"Open Augment (Log Out & Back In)" = "Abrir Augment (sair e entrar novamente)"; +"Open Codebuff Dashboard" = "Abrir painel do Codebuff"; +"Open Command Code Settings" = "Abrir configurações do Command Code"; +"Open Crof dashboard" = "Abrir painel do Crof"; +"Open Manus" = "Abrir Manus"; +"Open MiMo Balance" = "Abrir saldo do MiMo"; +"Open Moonshot Console" = "Abrir console do Moonshot"; +"Open Ollama API Keys" = "Abrir chaves API do Ollama"; +"Open StepFun Platform" = "Abrir plataforma StepFun"; +"Open T3 Chat Settings" = "Abrir configurações do T3 Chat"; +"Open Volcengine Ark Console" = "Abrir console Volcengine Ark"; +"Open legacy provider docs" = "Abrir docs do provedor legado"; +"Open projects" = "Abrir projetos"; +"Open this URL manually to continue login:\n\n%@" = "Abra esta URL manualmente para continuar o login:\n\n%@"; +"Optional organization ID for accounts linked to multiple Anthropic organizations." = "ID de organização opcional para contas vinculadas a várias organizações Anthropic."; +"Optional. Applies to the configured Admin API key; selected token accounts do not inherit OPENAI_PROJECT_ID." = "Opcional. Aplica-se à chave Admin API configurada; contas de token selecionadas não herdam OPENAI_PROJECT_ID."; +"Optional. Enter your GitHub Enterprise host, for example octocorp.ghe.com. Leave blank for github.com." = "Opcional. Informe seu host GitHub Enterprise, por exemplo octocorp.ghe.com. Deixe em branco para github.com."; +"Optional. Leave blank to discover and aggregate projects visible to the API key." = "Opcional. Deixe em branco para descobrir e agregar projetos visíveis à chave API."; +"Org ID (optional)" = "ID da org. (opcional)"; +"Organizations" = "Organizações"; +"Password" = "Senha"; +"Paste a Cookie header or full cURL capture from T3 Chat settings." = "Cole um cabeçalho Cookie ou captura cURL completa das configurações do T3 Chat."; +"Paste the Cookie header from a request to admin.mistral.ai. Must contain an ory_session_* cookie." = "Cole o cabeçalho Cookie de uma solicitação a admin.mistral.ai. Deve conter um cookie ory_session_*."; +"Paste the Oasis-Token from a logged-in browser session on platform.stepfun.com." = "Cole o Oasis-Token de uma sessão conectada em platform.stepfun.com."; +"Personal account" = "Conta pessoal"; +"Project ID" = "ID do projeto"; +"Re-auth" = "Reautenticar"; +"Re-authenticating…" = "Reautenticando…"; +"Refresh Session" = "Atualizar sessão"; +"Refresh organizations" = "Atualizar organizações"; +"Region" = "Região"; +"Reload" = "Recarregar"; +"Reorder" = "Reordenar"; +"Secret access key" = "Chave secreta de acesso"; +"Series" = "Série"; +"Service" = "Serviço"; +"Show or hide Kiro credits, percent, or both next to the menu bar icon." = "Mostra ou oculta créditos Kiro, porcentagem ou ambos ao lado do ícone da barra de menu."; +"Show usage for organizations you belong to. Personal account is always shown." = "Mostra o uso das organizações às quais você pertence. A conta pessoal sempre é exibida."; +"Sign in to cursor.com in your browser, then refresh Cursor in CodexBar." = "Entre em cursor.com no navegador e atualize Cursor no CodexBar."; +"Simulated error text" = "Texto de erro simulado"; +"StepFun platform account (phone number or email)." = "Conta da plataforma StepFun (telefone ou email)."; +"Stored in ~/.codexbar/config.json." = "Armazenado em ~/.codexbar/config.json."; +"Stored in ~/.codexbar/config.json. AZURE_OPENAI_API_KEY is also supported." = "Armazenado em ~/.codexbar/config.json. AZURE_OPENAI_API_KEY também é aceito."; +"Stored in ~/.codexbar/config.json. For the official Kimi API, use Moonshot / Kimi API." = "Armazenado em ~/.codexbar/config.json. Para a API oficial do Kimi, use Moonshot / Kimi API."; +"Stored in ~/.codexbar/config.json. Get your API key from the Volcengine Ark console." = "Armazenado em ~/.codexbar/config.json. Obtenha sua chave API no console Volcengine Ark."; +"Stored in ~/.codexbar/config.json. Get your key from Ollama settings." = "Armazenado em ~/.codexbar/config.json. Obtenha sua chave nas configurações do Ollama."; +"Stored in ~/.codexbar/config.json. Get your key from console.deepgram.com." = "Armazenado em ~/.codexbar/config.json. Obtenha sua chave em console.deepgram.com."; +"Stored in ~/.codexbar/config.json. Get your key from elevenlabs.io/app/settings/api-keys." = "Armazenado em ~/.codexbar/config.json. Obtenha sua chave em elevenlabs.io/app/settings/api-keys."; +"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." = "Armazenado em ~/.codexbar/config.json. Obtenha sua chave em openrouter.ai/settings/keys e defina um limite de gasto para ativar o rastreamento de cota."; +"Stored in ~/.codexbar/config.json. In Warp, open Settings > Platform > API Keys, then create one." = "Armazenado em ~/.codexbar/config.json. No Warp, abra Settings > Platform > API Keys e crie uma."; +"Stored in ~/.codexbar/config.json. Metrics require Groq Enterprise Prometheus access." = "Armazenado em ~/.codexbar/config.json. As métricas exigem acesso ao Groq Enterprise Prometheus."; +"Stored in ~/.codexbar/config.json. OPENAI_ADMIN_KEY is preferred; OPENAI_API_KEY still works." = "Armazenado em ~/.codexbar/config.json. OPENAI_ADMIN_KEY é preferida; OPENAI_API_KEY ainda funciona."; +"Stored in ~/.codexbar/config.json. Requires an Anthropic Admin API key." = "Armazenado em ~/.codexbar/config.json. Requer uma chave Anthropic Admin API."; +"Stored in ~/.codexbar/config.json. Used for /v1/quota-stats." = "Armazenado em ~/.codexbar/config.json. Usado para /v1/quota-stats."; +"Stored in ~/.codexbar/config.json. You can also provide CODEBUFF_API_KEY or let CodexBar read ~/.config/manicode/credentials.json (created by `codebuff login`)." = "Armazenado em ~/.codexbar/config.json. Você também pode fornecer CODEBUFF_API_KEY ou permitir que o CodexBar leia ~/.config/manicode/credentials.json (criado por `codebuff login`)."; +"Stored in ~/.codexbar/config.json. You can also provide CROF_API_KEY." = "Armazenado em ~/.codexbar/config.json. Você também pode fornecer CROF_API_KEY."; +"Stored in ~/.codexbar/config.json. You can also provide KILO_API_KEY or ~/.local/share/kilo/auth.json (kilo.access)." = "Armazenado em ~/.codexbar/config.json. Você também pode fornecer KILO_API_KEY ou ~/.local/share/kilo/auth.json (kilo.access)."; +"T3 Chat cookie" = "Cookie do T3 Chat"; +"That account is no longer available in CodexBar. Refresh the account list and try again." = "Essa conta não está mais disponível no CodexBar. Atualize a lista de contas e tente novamente."; +"The browser login did not complete in time. Try Antigravity login again." = "O login no navegador não foi concluído a tempo. Tente o login do Antigravity novamente."; +"Timed out waiting for Cursor login. %@" = "Tempo esgotado aguardando o login do Cursor. %@"; +"Timed out waiting for Cursor login. %@ Last error: %@" = "Tempo esgotado aguardando o login do Cursor. %@ Último erro: %@"; +"Today requests" = "Solicitações de hoje"; +"Total (30d): %@ credits" = "Total (30 dias): %@ créditos"; +"Username" = "Nome de usuário"; +"Uses username + password to login and obtain an Oasis-Token automatically." = "Usa nome de usuário e senha para entrar e obter um Oasis-Token automaticamente."; +"Utilization End" = "Fim da utilização"; +"Utilization Start" = "Início da utilização"; +"Verbosity" = "Detalhamento"; +"Windsurf session JSON bundle" = "Pacote JSON de sessão do Windsurf"; +"Workspace ID" = "ID do workspace"; +"Your StepFun platform password. Used to login and obtain a session token." = "Sua senha da plataforma StepFun. Usada para entrar e obter um token de sessão."; +"claude /login exited with status %d." = "claude /login saiu com status %d."; +"codex login exited with status %d." = "codex login saiu com status %d."; +"Cookie: …\n\nor paste a cURL capture from the Abacus AI dashboard" = "Cookie: …\n\nou cole uma captura cURL do painel Abacus AI"; +"Cookie: …\n\nor paste the __Secure-next-auth.session-token value" = "Cookie: …\n\nou cole o valor de __Secure-next-auth.session-token"; +"Cookie: …\n\nor paste the kimi-auth token value" = "Cookie: …\n\nou cole o valor do token kimi-auth"; +"session_id=...\n\nor paste just the session_id value" = "session_id=...\n\nou cole apenas o valor de session_id"; diff --git a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings index aa3311a666..70caa72959 100644 --- a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings @@ -421,7 +421,7 @@ "session_quota_notifications_title" = "会话配额通知"; "session_quota_notifications_subtitle" = "当 5 小时会话配额用完及恢复时发送通知。"; "quota_warning_notifications_title" = "配额预警通知"; -"quota_warning_notifications_subtitle" = "当会话或每周剩余配额低于设定阈值时提醒。"; +"quota_warning_notifications_subtitle" = "当会话或每周剩余配额低于设置的阈值时提醒。"; "quota_warnings_title" = "配额预警"; "quota_warning_session" = "会话"; "quota_warning_session_capitalized" = "会话"; @@ -857,3 +857,159 @@ "quota_warning_notification_title" = "%1$@ 的 %2$@ 额度偏低"; "quota_warning_notification_body" = "剩余 %1$@。已达到 %2$d%% 的 %3$@ 预警阈值。"; "quota_warning_notification_body_with_account" = "账户 %1$@。剩余 %2$@。已达到 %3$d%% 的 %4$@ 预警阈值。"; + +/* Additional provider settings and alerts */ +"%@ is waiting for permission" = "%@ 正在等待权限"; +"%@ requests" = "%@ 个请求"; +"%@: %@ credits" = "%@:%@ 额度"; +"30d requests" = "近 30 天请求"; +"4 days" = "4 天"; +"5 days" = "5 天"; +"7 days" = "7 天"; +"API key verifies Ollama Cloud access; cookies still expose quota limits." = "API 密钥会验证 Ollama Cloud 访问权限;Cookie 仍会提供配额限制。"; +"AWS access key ID. Can also be set with AWS_ACCESS_KEY_ID." = "AWS 访问密钥 ID。也可以用 AWS_ACCESS_KEY_ID 设置。"; +"AWS region. Can also be set with AWS_REGION." = "AWS 区域。也可以用 AWS_REGION 设置。"; +"AWS secret access key. Can also be set with AWS_SECRET_ACCESS_KEY." = "AWS 秘密访问密钥。也可以用 AWS_SECRET_ACCESS_KEY 设置。"; +"Access key ID" = "访问密钥 ID"; +"Add Account" = "添加账号"; +"Adding Account…" = "正在添加账号…"; +"Antigravity login failed" = "Antigravity 登录失败"; +"Antigravity login timed out" = "Antigravity 登录超时"; +"Auth source" = "认证来源"; +"Automatic imports Chrome browser cookies from Xiaomi MiMo." = "自动导入 Xiaomi MiMo 的 Chrome 浏览器 Cookie。"; +"Automatic imports Windsurf session data from Chromium browser localStorage." = "自动从 Chromium 浏览器 localStorage 导入 Windsurf 会话数据。"; +"Automatic imports browser cookies from Bailian." = "自动导入 Bailian 的浏览器 Cookie。"; +"Automatically imports browser cookies." = "自动导入浏览器 Cookie。"; +"Automatically imports browser session cookies." = "自动导入浏览器会话 Cookie。"; +"Azure OpenAI deployment name. AZURE_OPENAI_DEPLOYMENT_NAME is also supported." = "Azure OpenAI 部署名称。也支持 AZURE_OPENAI_DEPLOYMENT_NAME。"; +"Azure OpenAI key" = "Azure OpenAI 密钥"; +"Azure OpenAI resource endpoint. AZURE_OPENAI_ENDPOINT is also supported." = "Azure OpenAI 资源端点。也支持 AZURE_OPENAI_ENDPOINT。"; +"Base URL" = "Base URL"; +"Base URL for the LLM-API-Key-Proxy instance." = "LLM-API-Key-Proxy 实例的 Base URL。"; +"Browser cookies" = "浏览器 Cookie"; +"Cap end" = "上限终点"; +"Cap start" = "上限起点"; +"Capacity End" = "容量终点"; +"Capacity Start" = "容量起点"; +"Changelog" = "变更记录"; +"Choose the Moonshot/Kimi API host for international or China mainland accounts." = "选择国际或中国大陆账号使用的 Moonshot/Kimi API 主机。"; +"CodexBar can't replace a system account that is signed in with an API key only setup." = "CodexBar 无法替换仅使用 API 密钥登录设置的系统账号。"; +"CodexBar could not find saved auth for that account. Re-authenticate it and try again." = "CodexBar 找不到该账号已保存的认证。请重新认证后再试。"; +"CodexBar could not read managed account storage. Recover the store before adding another account." = "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 replace the live Codex auth on this Mac." = "CodexBar 无法替换此 Mac 上当前的 Codex 认证。"; +"CodexBar could not safely preserve the current system account before switching." = "CodexBar 无法在切换前安全保留当前的系统账号。"; +"CodexBar could not save the current system account before switching." = "CodexBar 无法在切换前保存当前的系统账号。"; +"CodexBar could not update managed account storage." = "CodexBar 无法更新托管账号存储区。"; +"CodexBar found another managed account that already uses the current system account. Resolve the duplicate account before switching." = "CodexBar 发现另一个托管账号已使用当前的系统账号。请先解决重复账号,再进行切换。"; +"CodexBar will ask macOS Keychain for “%@” so it can decrypt browser cookies and authenticate your account. Click OK to continue." = "CodexBar 将向 macOS 钥匙串请求“%@”,以解密浏览器 Cookie 并认证你的账号。点击“确定”继续。"; +"CodexBar will ask macOS Keychain for the Claude Code OAuth token so it can fetch your Claude usage. Click OK to continue." = "CodexBar 将向 macOS 钥匙串请求 Claude Code OAuth token,以获取你的 Claude 用量。点击“确定”继续。"; +"CodexBar will ask macOS Keychain for your Amp cookie header so it can fetch usage. Click OK to continue." = "CodexBar 将向 macOS 钥匙串请求你的 Amp Cookie 标头,以获取用量。点击“确定”继续。"; +"CodexBar will ask macOS Keychain for your Augment cookie header so it can fetch usage. Click OK to continue." = "CodexBar 将向 macOS 钥匙串请求你的 Augment Cookie 标头,以获取用量。点击“确定”继续。"; +"CodexBar will ask macOS Keychain for your Claude cookie header so it can fetch Claude web usage. Click OK to continue." = "CodexBar 将向 macOS 钥匙串请求你的 Claude Cookie 标头,以获取 Claude 网页用量。点击“确定”继续。"; +"CodexBar will ask macOS Keychain for your Cursor cookie header so it can fetch usage. Click OK to continue." = "CodexBar 将向 macOS 钥匙串请求你的 Cursor Cookie 标头,以获取用量。点击“确定”继续。"; +"CodexBar will ask macOS Keychain for your Factory cookie header so it can fetch usage. Click OK to continue." = "CodexBar 将向 macOS 钥匙串请求你的 Factory Cookie 标头,以获取用量。点击“确定”继续。"; +"CodexBar will ask macOS Keychain for your GitHub Copilot token so it can fetch usage. Click OK to continue." = "CodexBar 将向 macOS 钥匙串请求你的 GitHub Copilot token,以获取用量。点击“确定”继续。"; +"CodexBar will ask macOS Keychain for your Kimi K2 API key so it can fetch usage. Click OK to continue." = "CodexBar 将向 macOS 钥匙串请求你的 Kimi K2 API 密钥,以获取用量。点击“确定”继续。"; +"CodexBar will ask macOS Keychain for your Kimi auth token so it can fetch usage. Click OK to continue." = "CodexBar 将向 macOS 钥匙串请求你的 Kimi 认证 token,以获取用量。点击“确定”继续。"; +"CodexBar will ask macOS Keychain for your MiniMax API token so it can fetch usage. Click OK to continue." = "CodexBar 将向 macOS 钥匙串请求你的 MiniMax API token,以获取用量。点击“确定”继续。"; +"CodexBar will ask macOS Keychain for your MiniMax cookie header so it can fetch usage. Click OK to continue." = "CodexBar 将向 macOS 钥匙串请求你的 MiniMax Cookie 标头,以获取用量。点击“确定”继续。"; +"CodexBar will ask macOS Keychain for your OpenAI cookie header so it can fetch Codex dashboard extras. Click OK to continue." = "CodexBar 将向 macOS 钥匙串请求你的 OpenAI Cookie 标头,以获取 Codex 仪表盘额外数据。点击“确定”继续。"; +"CodexBar will ask macOS Keychain for your OpenCode cookie header so it can fetch usage. Click OK to continue." = "CodexBar 将向 macOS 钥匙串请求你的 OpenCode Cookie 标头,以获取用量。点击“确定”继续。"; +"CodexBar will ask macOS Keychain for your Synthetic API key so it can fetch usage. Click OK to continue." = "CodexBar 将向 macOS 钥匙串请求你的 Synthetic API 密钥,以获取用量。点击“确定”继续。"; +"CodexBar will ask macOS Keychain for your z.ai API token so it can fetch usage. Click OK to continue." = "CodexBar 将向 macOS 钥匙串请求你的 z.ai API token,以获取用量。点击“确定”继续。"; +"Could not open Cursor login in your browser." = "无法在浏览器中打开 Cursor 登录。"; +"Could not open browser for Antigravity" = "无法为 Antigravity 打开浏览器"; +"Credits used" = "已用额度"; +"Day" = "日期"; +"Deployment" = "部署"; +"Drag to reorder" = "拖动以重新排序"; +"Endpoint" = "端点"; +"Enterprise host" = "Enterprise 主机"; +"Extra usage balance: %@" = "额外使用量余额:%@"; +"Keychain Access Required" = "需要钥匙串访问权限"; +"Kiro menu bar value" = "Kiro 菜单栏数值"; +"Label" = "标签"; +"No organizations loaded. Click Refresh after setting your API key." = "尚未加载组织。设置 API 密钥后点击“刷新”。"; +"No output captured." = "未捕获到输出。"; +"No system account" = "没有系统账号"; +"Oasis-Token" = "Oasis-Token"; +"Open Augment (Log Out & Back In)" = "打开 Augment(退出后重新登录)"; +"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 Ollama API Keys" = "打开 Ollama API 密钥"; +"Open StepFun Platform" = "打开 StepFun 平台"; +"Open T3 Chat Settings" = "打开 T3 Chat 设置"; +"Open Volcengine Ark Console" = "打开 Volcengine Ark 控制台"; +"Open legacy provider docs" = "打开旧版提供商文档"; +"Open projects" = "打开项目"; +"Open this URL manually to continue login:\n\n%@" = "手动打开此 URL 以继续登录:\n\n%@"; +"Optional organization ID for accounts linked to multiple Anthropic organizations." = "适用于关联多个 Anthropic 组织的账号,可选填组织 ID。"; +"Optional. Applies to the configured Admin API key; selected token accounts do not inherit OPENAI_PROJECT_ID." = "选填。应用到已设置的 Admin API 密钥;选中的 token 账号不会继承 OPENAI_PROJECT_ID。"; +"Optional. Enter your GitHub Enterprise host, for example octocorp.ghe.com. Leave blank for github.com." = "选填。输入你的 GitHub Enterprise 主机,例如 octocorp.ghe.com。留空则使用 github.com。"; +"Optional. Leave blank to discover and aggregate projects visible to the API key." = "选填。留空会发现并汇总 API 密钥可见的项目。"; +"Org ID (optional)" = "组织 ID(选填)"; +"Organizations" = "组织"; +"Password" = "密码"; +"Paste a Cookie header or full cURL capture from T3 Chat settings." = "粘贴 T3 Chat 设置中的 Cookie 标头或完整 cURL 捕获内容。"; +"Paste the Cookie header from a request to admin.mistral.ai. Must contain an ory_session_* cookie." = "粘贴发往 admin.mistral.ai 请求中的 Cookie 标头。必须包含 ory_session_* Cookie。"; +"Paste the Oasis-Token from a logged-in browser session on platform.stepfun.com." = "粘贴 platform.stepfun.com 已登录浏览器会话中的 Oasis-Token。"; +"Personal account" = "个人账号"; +"Project ID" = "项目 ID"; +"Re-auth" = "重新认证"; +"Re-authenticating…" = "正在重新认证…"; +"Refresh Session" = "刷新会话"; +"Refresh organizations" = "刷新组织"; +"Region" = "区域"; +"Reload" = "重新加载"; +"Reorder" = "重新排序"; +"Secret access key" = "秘密访问密钥"; +"Series" = "序列"; +"Service" = "服务"; +"Show or hide Kiro credits, percent, or both next to the menu bar icon." = "在菜单栏图标旁显示或隐藏 Kiro 额度、百分比,或两者都显示。"; +"Show usage for organizations you belong to. Personal account is always shown." = "显示你所属组织的用量。个人账号始终显示。"; +"Sign in to cursor.com in your browser, then refresh Cursor in CodexBar." = "请在浏览器中登录 cursor.com,然后在 CodexBar 刷新 Cursor。"; +"Simulated error text" = "模拟错误文字"; +"StepFun platform account (phone number or email)." = "StepFun 平台账号(手机号或电子邮件)。"; +"Stored in ~/.codexbar/config.json." = "存储在 ~/.codexbar/config.json 中。"; +"Stored in ~/.codexbar/config.json. AZURE_OPENAI_API_KEY is also supported." = "存储在 ~/.codexbar/config.json 中。也支持 AZURE_OPENAI_API_KEY。"; +"Stored in ~/.codexbar/config.json. For the official Kimi API, use Moonshot / Kimi API." = "存储在 ~/.codexbar/config.json 中。官方 Kimi API 请使用 Moonshot / Kimi API。"; +"Stored in ~/.codexbar/config.json. Get your API key from the Volcengine Ark console." = "存储在 ~/.codexbar/config.json 中。请从 Volcengine Ark 控制台获取 API 密钥。"; +"Stored in ~/.codexbar/config.json. Get your key from Ollama settings." = "存储在 ~/.codexbar/config.json 中。请从 Ollama 设置获取密钥。"; +"Stored in ~/.codexbar/config.json. Get your key from console.deepgram.com." = "存储在 ~/.codexbar/config.json 中。请从 console.deepgram.com 获取密钥。"; +"Stored in ~/.codexbar/config.json. Get your key from elevenlabs.io/app/settings/api-keys." = "存储在 ~/.codexbar/config.json 中。请从 elevenlabs.io/app/settings/api-keys 获取密钥。"; +"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. Metrics require Groq Enterprise Prometheus access." = "存储在 ~/.codexbar/config.json 中。指标需要 Groq Enterprise Prometheus 访问权限。"; +"Stored in ~/.codexbar/config.json. OPENAI_ADMIN_KEY is preferred; OPENAI_API_KEY still works." = "存储在 ~/.codexbar/config.json 中。优先使用 OPENAI_ADMIN_KEY;OPENAI_API_KEY 仍可使用。"; +"Stored in ~/.codexbar/config.json. Requires an Anthropic Admin API key." = "存储在 ~/.codexbar/config.json 中。需要 Anthropic Admin API 密钥。"; +"Stored in ~/.codexbar/config.json. Used for /v1/quota-stats." = "存储在 ~/.codexbar/config.json 中。用于 /v1/quota-stats。"; +"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 读取由 `codebuff login` 创建的 ~/.config/manicode/credentials.json。"; +"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)。"; +"T3 Chat cookie" = "T3 Chat Cookie"; +"That account is no longer available in CodexBar. Refresh the account list and try again." = "该账号已无法在 CodexBar 中使用。请刷新账号列表后再试。"; +"The browser login did not complete in time. Try Antigravity login again." = "浏览器登录未在时限内完成。请再次尝试 Antigravity 登录。"; +"Timed out waiting for Cursor login. %@" = "等待 Cursor 登录超时。%@"; +"Timed out waiting for Cursor login. %@ Last error: %@" = "等待 Cursor 登录超时。%@ 最后错误:%@"; +"Today requests" = "今日请求"; +"Total (30d): %@ credits" = "总计(30 天):%@ 额度"; +"Username" = "用户名"; +"Uses username + password to login and obtain an Oasis-Token automatically." = "使用用户名与密码登录,并自动获取 Oasis-Token。"; +"Utilization End" = "使用率终点"; +"Utilization Start" = "使用率起点"; +"Verbosity" = "详细程度"; +"Windsurf session JSON bundle" = "Windsurf 会话 JSON 包"; +"Workspace ID" = "工作区 ID"; +"Your StepFun platform password. Used to login and obtain a session token." = "你的 StepFun 平台密码。用于登录并获取会话 token。"; +"claude /login exited with status %d." = "claude /login 以状态 %d 结束。"; +"codex login exited with status %d." = "codex login 以状态 %d 结束。"; +"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 值"; +"session_id=...\n\nor paste just the session_id value" = "session_id=...\n\n或只粘贴 session_id 值"; diff --git a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings index 35e30c9fa5..391b8f14b7 100644 --- a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings @@ -753,3 +753,159 @@ "%@: %@ · %@ tokens" = "%@:%@ · %@ token"; "No providers selected for Overview." = "概覽尚未選擇提供者。"; "No overview data available." = "概覽尚無可用資料。"; + +/* Additional provider settings and alerts */ +"%@ is waiting for permission" = "%@ 正在等待權限"; +"%@ requests" = "%@ 個請求"; +"%@: %@ credits" = "%@:%@ 額度"; +"30d requests" = "近 30 天請求"; +"4 days" = "4 天"; +"5 days" = "5 天"; +"7 days" = "7 天"; +"API key verifies Ollama Cloud access; cookies still expose quota limits." = "API 金鑰會驗證 Ollama Cloud 存取;Cookie 仍會提供配額限制。"; +"AWS access key ID. Can also be set with AWS_ACCESS_KEY_ID." = "AWS 存取金鑰 ID。也可以用 AWS_ACCESS_KEY_ID 設定。"; +"AWS region. Can also be set with AWS_REGION." = "AWS 區域。也可以用 AWS_REGION 設定。"; +"AWS secret access key. Can also be set with AWS_SECRET_ACCESS_KEY." = "AWS 秘密存取金鑰。也可以用 AWS_SECRET_ACCESS_KEY 設定。"; +"Access key ID" = "存取金鑰 ID"; +"Add Account" = "新增帳號"; +"Adding Account…" = "正在新增帳號…"; +"Antigravity login failed" = "Antigravity 登入失敗"; +"Antigravity login timed out" = "Antigravity 登入逾時"; +"Auth source" = "認證來源"; +"Automatic imports Chrome browser cookies from Xiaomi MiMo." = "自動匯入 Xiaomi MiMo 的 Chrome 瀏覽器 Cookie。"; +"Automatic imports Windsurf session data from Chromium browser localStorage." = "自動從 Chromium 瀏覽器 localStorage 匯入 Windsurf 工作階段資料。"; +"Automatic imports browser cookies from Bailian." = "自動匯入 Bailian 的瀏覽器 Cookie。"; +"Automatically imports browser cookies." = "自動匯入瀏覽器 Cookie。"; +"Automatically imports browser session cookies." = "自動匯入瀏覽器工作階段 Cookie。"; +"Azure OpenAI deployment name. AZURE_OPENAI_DEPLOYMENT_NAME is also supported." = "Azure OpenAI 部署名稱。也支援 AZURE_OPENAI_DEPLOYMENT_NAME。"; +"Azure OpenAI key" = "Azure OpenAI 金鑰"; +"Azure OpenAI resource endpoint. AZURE_OPENAI_ENDPOINT is also supported." = "Azure OpenAI 資源端點。也支援 AZURE_OPENAI_ENDPOINT。"; +"Base URL" = "Base URL"; +"Base URL for the LLM-API-Key-Proxy instance." = "LLM-API-Key-Proxy 實例的 Base URL。"; +"Browser cookies" = "瀏覽器 Cookie"; +"Cap end" = "上限終點"; +"Cap start" = "上限起點"; +"Capacity End" = "容量終點"; +"Capacity Start" = "容量起點"; +"Changelog" = "變更記錄"; +"Choose the Moonshot/Kimi API host for international or China mainland accounts." = "選擇國際或中國大陸帳號使用的 Moonshot/Kimi API 主機。"; +"CodexBar can't replace a system account that is signed in with an API key only setup." = "CodexBar 無法取代僅使用 API 金鑰登入設定的系統帳號。"; +"CodexBar could not find saved auth for that account. Re-authenticate it and try again." = "CodexBar 找不到該帳號已儲存的認證。請重新認證後再試。"; +"CodexBar could not read managed account storage. Recover the store before adding another account." = "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 replace the live Codex auth on this Mac." = "CodexBar 無法取代此 Mac 上的目前 Codex 認證。"; +"CodexBar could not safely preserve the current system account before switching." = "CodexBar 無法在切換前安全保留目前的系統帳號。"; +"CodexBar could not save the current system account before switching." = "CodexBar 無法在切換前儲存目前的系統帳號。"; +"CodexBar could not update managed account storage." = "CodexBar 無法更新受管理帳號儲存區。"; +"CodexBar found another managed account that already uses the current system account. Resolve the duplicate account before switching." = "CodexBar 發現另一個受管理帳號已使用目前的系統帳號。請先解決重複帳號,再進行切換。"; +"CodexBar will ask macOS Keychain for “%@” so it can decrypt browser cookies and authenticate your account. Click OK to continue." = "CodexBar 將向 macOS 鑰匙圈要求「%@」,以解密瀏覽器 Cookie 並認證你的帳號。按一下「確定」繼續。"; +"CodexBar will ask macOS Keychain for the Claude Code OAuth token so it can fetch your Claude usage. Click OK to continue." = "CodexBar 將向 macOS 鑰匙圈要求 Claude Code OAuth token,以取得你的 Claude 使用量。按一下「確定」繼續。"; +"CodexBar will ask macOS Keychain for your Amp cookie header so it can fetch usage. Click OK to continue." = "CodexBar 將向 macOS 鑰匙圈要求你的 Amp Cookie 標頭,以取得使用量。按一下「確定」繼續。"; +"CodexBar will ask macOS Keychain for your Augment cookie header so it can fetch usage. Click OK to continue." = "CodexBar 將向 macOS 鑰匙圈要求你的 Augment Cookie 標頭,以取得使用量。按一下「確定」繼續。"; +"CodexBar will ask macOS Keychain for your Claude cookie header so it can fetch Claude web usage. Click OK to continue." = "CodexBar 將向 macOS 鑰匙圈要求你的 Claude Cookie 標頭,以取得 Claude 網頁使用量。按一下「確定」繼續。"; +"CodexBar will ask macOS Keychain for your Cursor cookie header so it can fetch usage. Click OK to continue." = "CodexBar 將向 macOS 鑰匙圈要求你的 Cursor Cookie 標頭,以取得使用量。按一下「確定」繼續。"; +"CodexBar will ask macOS Keychain for your Factory cookie header so it can fetch usage. Click OK to continue." = "CodexBar 將向 macOS 鑰匙圈要求你的 Factory Cookie 標頭,以取得使用量。按一下「確定」繼續。"; +"CodexBar will ask macOS Keychain for your GitHub Copilot token so it can fetch usage. Click OK to continue." = "CodexBar 將向 macOS 鑰匙圈要求你的 GitHub Copilot token,以取得使用量。按一下「確定」繼續。"; +"CodexBar will ask macOS Keychain for your Kimi K2 API key so it can fetch usage. Click OK to continue." = "CodexBar 將向 macOS 鑰匙圈要求你的 Kimi K2 API 金鑰,以取得使用量。按一下「確定」繼續。"; +"CodexBar will ask macOS Keychain for your Kimi auth token so it can fetch usage. Click OK to continue." = "CodexBar 將向 macOS 鑰匙圈要求你的 Kimi 認證 token,以取得使用量。按一下「確定」繼續。"; +"CodexBar will ask macOS Keychain for your MiniMax API token so it can fetch usage. Click OK to continue." = "CodexBar 將向 macOS 鑰匙圈要求你的 MiniMax API token,以取得使用量。按一下「確定」繼續。"; +"CodexBar will ask macOS Keychain for your MiniMax cookie header so it can fetch usage. Click OK to continue." = "CodexBar 將向 macOS 鑰匙圈要求你的 MiniMax Cookie 標頭,以取得使用量。按一下「確定」繼續。"; +"CodexBar will ask macOS Keychain for your OpenAI cookie header so it can fetch Codex dashboard extras. Click OK to continue." = "CodexBar 將向 macOS 鑰匙圈要求你的 OpenAI Cookie 標頭,以取得 Codex 儀表板額外資料。按一下「確定」繼續。"; +"CodexBar will ask macOS Keychain for your OpenCode cookie header so it can fetch usage. Click OK to continue." = "CodexBar 將向 macOS 鑰匙圈要求你的 OpenCode Cookie 標頭,以取得使用量。按一下「確定」繼續。"; +"CodexBar will ask macOS Keychain for your Synthetic API key so it can fetch usage. Click OK to continue." = "CodexBar 將向 macOS 鑰匙圈要求你的 Synthetic API 金鑰,以取得使用量。按一下「確定」繼續。"; +"CodexBar will ask macOS Keychain for your z.ai API token so it can fetch usage. Click OK to continue." = "CodexBar 將向 macOS 鑰匙圈要求你的 z.ai API token,以取得使用量。按一下「確定」繼續。"; +"Could not open Cursor login in your browser." = "無法在瀏覽器中開啟 Cursor 登入。"; +"Could not open browser for Antigravity" = "無法為 Antigravity 開啟瀏覽器"; +"Credits used" = "已用額度"; +"Day" = "日期"; +"Deployment" = "部署"; +"Drag to reorder" = "拖曳以重新排序"; +"Endpoint" = "端點"; +"Enterprise host" = "Enterprise 主機"; +"Extra usage balance: %@" = "額外使用量餘額:%@"; +"Keychain Access Required" = "需要鑰匙圈存取權"; +"Kiro menu bar value" = "Kiro 選單列數值"; +"Label" = "標籤"; +"No organizations loaded. Click Refresh after setting your API key." = "尚未載入組織。設定 API 金鑰後按一下「重新整理」。"; +"No output captured." = "未擷取到輸出。"; +"No system account" = "沒有系統帳號"; +"Oasis-Token" = "Oasis-Token"; +"Open Augment (Log Out & Back In)" = "開啟 Augment(登出後重新登入)"; +"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 Ollama API Keys" = "開啟 Ollama API 金鑰"; +"Open StepFun Platform" = "開啟 StepFun 平台"; +"Open T3 Chat Settings" = "開啟 T3 Chat 設定"; +"Open Volcengine Ark Console" = "開啟 Volcengine Ark 主控台"; +"Open legacy provider docs" = "開啟舊版提供者文件"; +"Open projects" = "開啟專案"; +"Open this URL manually to continue login:\n\n%@" = "手動開啟此 URL 以繼續登入:\n\n%@"; +"Optional organization ID for accounts linked to multiple Anthropic organizations." = "適用於連結多個 Anthropic 組織的帳號,可選填組織 ID。"; +"Optional. Applies to the configured Admin API key; selected token accounts do not inherit OPENAI_PROJECT_ID." = "選填。套用到已設定的 Admin API 金鑰;選取的 token 帳號不會繼承 OPENAI_PROJECT_ID。"; +"Optional. Enter your GitHub Enterprise host, for example octocorp.ghe.com. Leave blank for github.com." = "選填。輸入你的 GitHub Enterprise 主機,例如 octocorp.ghe.com。留空則使用 github.com。"; +"Optional. Leave blank to discover and aggregate projects visible to the API key." = "選填。留空會探索並彙總 API 金鑰可見的專案。"; +"Org ID (optional)" = "組織 ID(選填)"; +"Organizations" = "組織"; +"Password" = "密碼"; +"Paste a Cookie header or full cURL capture from T3 Chat settings." = "貼上 T3 Chat 設定中的 Cookie 標頭或完整 cURL 擷取內容。"; +"Paste the Cookie header from a request to admin.mistral.ai. Must contain an ory_session_* cookie." = "貼上發往 admin.mistral.ai 請求中的 Cookie 標頭。必須包含 ory_session_* Cookie。"; +"Paste the Oasis-Token from a logged-in browser session on platform.stepfun.com." = "貼上 platform.stepfun.com 已登入瀏覽器工作階段中的 Oasis-Token。"; +"Personal account" = "個人帳號"; +"Project ID" = "專案 ID"; +"Re-auth" = "重新認證"; +"Re-authenticating…" = "正在重新認證…"; +"Refresh Session" = "重新整理工作階段"; +"Refresh organizations" = "重新整理組織"; +"Region" = "區域"; +"Reload" = "重新載入"; +"Reorder" = "重新排序"; +"Secret access key" = "秘密存取金鑰"; +"Series" = "序列"; +"Service" = "服務"; +"Show or hide Kiro credits, percent, or both next to the menu bar icon." = "在選單列圖示旁顯示或隱藏 Kiro 額度、百分比,或兩者都顯示。"; +"Show usage for organizations you belong to. Personal account is always shown." = "顯示你所屬組織的使用量。個人帳號一律顯示。"; +"Sign in to cursor.com in your browser, then refresh Cursor in CodexBar." = "請在瀏覽器中登入 cursor.com,然後在 CodexBar 重新整理 Cursor。"; +"Simulated error text" = "模擬錯誤文字"; +"StepFun platform account (phone number or email)." = "StepFun 平台帳號(電話號碼或電子郵件)。"; +"Stored in ~/.codexbar/config.json." = "儲存在 ~/.codexbar/config.json 中。"; +"Stored in ~/.codexbar/config.json. AZURE_OPENAI_API_KEY is also supported." = "儲存在 ~/.codexbar/config.json 中。也支援 AZURE_OPENAI_API_KEY。"; +"Stored in ~/.codexbar/config.json. For the official Kimi API, use Moonshot / Kimi API." = "儲存在 ~/.codexbar/config.json 中。官方 Kimi API 請使用 Moonshot / Kimi API。"; +"Stored in ~/.codexbar/config.json. Get your API key from the Volcengine Ark console." = "儲存在 ~/.codexbar/config.json 中。請從 Volcengine Ark 主控台取得 API 金鑰。"; +"Stored in ~/.codexbar/config.json. Get your key from Ollama settings." = "儲存在 ~/.codexbar/config.json 中。請從 Ollama 設定取得金鑰。"; +"Stored in ~/.codexbar/config.json. Get your key from console.deepgram.com." = "儲存在 ~/.codexbar/config.json 中。請從 console.deepgram.com 取得金鑰。"; +"Stored in ~/.codexbar/config.json. Get your key from elevenlabs.io/app/settings/api-keys." = "儲存在 ~/.codexbar/config.json 中。請從 elevenlabs.io/app/settings/api-keys 取得金鑰。"; +"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. Metrics require Groq Enterprise Prometheus access." = "儲存在 ~/.codexbar/config.json 中。指標需要 Groq Enterprise Prometheus 存取權。"; +"Stored in ~/.codexbar/config.json. OPENAI_ADMIN_KEY is preferred; OPENAI_API_KEY still works." = "儲存在 ~/.codexbar/config.json 中。優先使用 OPENAI_ADMIN_KEY;OPENAI_API_KEY 仍可使用。"; +"Stored in ~/.codexbar/config.json. Requires an Anthropic Admin API key." = "儲存在 ~/.codexbar/config.json 中。需要 Anthropic Admin API 金鑰。"; +"Stored in ~/.codexbar/config.json. Used for /v1/quota-stats." = "儲存在 ~/.codexbar/config.json 中。用於 /v1/quota-stats。"; +"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 讀取 `codebuff login` 建立的 ~/.config/manicode/credentials.json。"; +"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)。"; +"T3 Chat cookie" = "T3 Chat Cookie"; +"That account is no longer available in CodexBar. Refresh the account list and try again." = "該帳號已無法在 CodexBar 中使用。請重新整理帳號列表後再試。"; +"The browser login did not complete in time. Try Antigravity login again." = "瀏覽器登入未在時限內完成。請再次嘗試 Antigravity 登入。"; +"Timed out waiting for Cursor login. %@" = "等待 Cursor 登入逾時。%@"; +"Timed out waiting for Cursor login. %@ Last error: %@" = "等待 Cursor 登入逾時。%@ 最後錯誤:%@"; +"Today requests" = "今日請求"; +"Total (30d): %@ credits" = "總計(30 天):%@ 額度"; +"Username" = "使用者名稱"; +"Uses username + password to login and obtain an Oasis-Token automatically." = "使用使用者名稱與密碼登入,並自動取得 Oasis-Token。"; +"Utilization End" = "使用率終點"; +"Utilization Start" = "使用率起點"; +"Verbosity" = "詳細程度"; +"Windsurf session JSON bundle" = "Windsurf 工作階段 JSON 組合"; +"Workspace ID" = "工作區 ID"; +"Your StepFun platform password. Used to login and obtain a session token." = "你的 StepFun 平台密碼。用於登入並取得工作階段 token。"; +"claude /login exited with status %d." = "claude /login 以狀態 %d 結束。"; +"codex login exited with status %d." = "codex login 以狀態 %d 結束。"; +"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 值"; +"session_id=...\n\nor paste just the session_id value" = "session_id=...\n\n或只貼上 session_id 值"; diff --git a/Sources/CodexBar/SettingsStore+MenuObservation.swift b/Sources/CodexBar/SettingsStore+MenuObservation.swift index 5de81817ea..3776c1bbae 100644 --- a/Sources/CodexBar/SettingsStore+MenuObservation.swift +++ b/Sources/CodexBar/SettingsStore+MenuObservation.swift @@ -32,6 +32,7 @@ extension SettingsStore { _ = self.menuBarMetricPreferencesRaw _ = self.costUsageEnabled _ = self.costUsageHistoryDays + _ = self.appLanguage _ = self.hidePersonalInfo _ = self.randomBlinkEnabled _ = self.confettiOnWeeklyLimitResetsEnabled diff --git a/Sources/CodexBar/StatusItemController+Actions.swift b/Sources/CodexBar/StatusItemController+Actions.swift index a02500809c..aaa7c02f0e 100644 --- a/Sources/CodexBar/StatusItemController+Actions.swift +++ b/Sources/CodexBar/StatusItemController+Actions.swift @@ -185,9 +185,10 @@ extension StatusItemController: StatusItemMenuPersistentActionDelegate { } guard self.settings.hasUnreadableManagedCodexAccountStore == false else { self.presentLoginAlert( - title: "Managed Codex accounts unavailable", - message: "CodexBar could not read managed account storage. " + - "Recover the store before adding another account.") + title: L("Managed Codex accounts unavailable"), + message: L( + "CodexBar could not read managed account storage. " + + "Recover the store before adding another account.")) return } @@ -386,23 +387,22 @@ extension StatusItemController: StatusItemMenuPersistentActionDelegate { error == .authenticationInProgress { info = LoginAlertInfo( - title: "Codex account login already running", - message: "Wait for the current managed Codex login to finish before adding another account.") + title: L("Codex account login already running"), + message: L("Wait for the current managed Codex login to finish before adding another account.")) } else if let error = error as? ManagedCodexAccountServiceError { let message = switch error { case .loginFailed: L("managed_login_failed") case .missingEmail: - "Codex login completed, but no account email was available. " + - "Try again after confirming the account is fully signed in." + L("managed_login_missing_email") case .workspaceSelectionCancelled: - "CodexBar found multiple workspaces, but no workspace was selected." + L("workspace_selection_cancelled") case let .unsafeManagedHome(path): - "CodexBar refused to modify an unexpected managed home path: \(path)" + String(format: L("unsafe_managed_home"), path) } - info = LoginAlertInfo(title: "Could not add Codex account", message: message) + info = LoginAlertInfo(title: L("Could not add Codex account"), message: message) } else { - info = LoginAlertInfo(title: "Could not add Codex account", message: error.localizedDescription) + info = LoginAlertInfo(title: L("Could not add Codex account"), message: error.localizedDescription) } self.presentLoginAlert(title: info.title, message: info.message) @@ -414,18 +414,18 @@ extension StatusItemController: StatusItemMenuPersistentActionDelegate { return case .missingBinary: self.presentLoginAlert( - title: "Claude CLI not found", - message: "Install the Claude CLI (npm i -g @anthropic-ai/claude-code) and try again.") + title: L("Claude CLI not found"), + message: L("Install the Claude CLI (npm i -g @anthropic-ai/claude-code) and try again.")) case let .launchFailed(message): - self.presentLoginAlert(title: "Could not start claude /login", message: message) + self.presentLoginAlert(title: L("Could not start claude /login"), message: message) case .timedOut: self.presentLoginAlert( - title: "Claude login timed out", + title: L("Claude login timed out"), message: self.trimmedLoginOutput(result.output)) case let .failed(status): - let statusLine = "claude /login exited with status \(status)." + let statusLine = String(format: L("claude /login exited with status %d."), status) let message = self.trimmedLoginOutput(result.output.isEmpty ? statusLine : result.output) - self.presentLoginAlert(title: "Claude login failed", message: message) + self.presentLoginAlert(title: L("Claude login failed"), message: message) } } @@ -493,10 +493,10 @@ extension StatusItemController: StatusItemMenuPersistentActionDelegate { nil case .missingBinary: LoginAlertInfo( - title: "Gemini CLI not found", - message: "Install the Gemini CLI (npm i -g @google/gemini-cli) and try again.") + title: L("Gemini CLI not found"), + message: L("Install the Gemini CLI (npm i -g @google/gemini-cli) and try again.")) case let .launchFailed(message): - LoginAlertInfo(title: "Could not open Terminal for Gemini", message: message) + LoginAlertInfo(title: L("Could not open Terminal for Gemini"), message: message) } } @@ -506,21 +506,21 @@ extension StatusItemController: StatusItemMenuPersistentActionDelegate { nil case .timedOut: LoginAlertInfo( - title: "Antigravity login timed out", - message: "The browser login did not complete in time. Try Antigravity login again.") + title: L("Antigravity login timed out"), + message: L("The browser login did not complete in time. Try Antigravity login again.")) case let .launchFailed(message): LoginAlertInfo( - title: "Could not open browser for Antigravity", - message: "Open this URL manually to continue login:\n\n\(message)") + title: L("Could not open browser for Antigravity"), + message: String(format: L("Open this URL manually to continue login:\n\n%@"), message)) case let .failed(message): - LoginAlertInfo(title: "Antigravity login failed", message: message) + LoginAlertInfo(title: L("Antigravity login failed"), message: message) } } 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() } @@ -528,7 +528,7 @@ extension StatusItemController: StatusItemMenuPersistentActionDelegate { private func trimmedLoginOutput(_ text: String) -> String { let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) let limit = 600 - if trimmed.isEmpty { return "No output captured." } + if trimmed.isEmpty { return L("No output captured.") } if trimmed.count <= limit { return trimmed } let idx = trimmed.index(trimmed.startIndex, offsetBy: limit) return "\(trimmed[.. NSMenuItem { let tooltipLines = Self.costMenuTooltipLines(tokenUsage: model.tokenUsage) diff --git a/Sources/CodexBar/StatusItemController+Menu.swift b/Sources/CodexBar/StatusItemController+Menu.swift index 02614f5326..6eb9fbec82 100644 --- a/Sources/CodexBar/StatusItemController+Menu.swift +++ b/Sources/CodexBar/StatusItemController+Menu.swift @@ -204,6 +204,7 @@ extension StatusItemController { let switcherUsageBarsShowUsedMatch = self.settings.usageBarsShowUsed == self.lastSwitcherUsageBarsShowUsed let switcherSelectionMatches = switcherSelection == self.lastMergedSwitcherSelection let switcherOverviewAvailabilityMatches = includesOverview == self.lastSwitcherIncludesOverview + let menuLocalizationMatches = self.menuLocalizationSignature() == self.lastMenuLocalizationSignature let tokenSwitcherCompatible = tokenAccountDisplay == self.lastTokenAccountMenuDisplay && ((tokenAccountDisplay?.showSwitcher == true && hasTokenSwitcher) || (tokenAccountDisplay?.showSwitcher != true && !hasTokenSwitcher)) @@ -224,6 +225,7 @@ extension StatusItemController { switcherUsageBarsShowUsedMatch && switcherSelectionMatches && switcherOverviewAvailabilityMatches && + menuLocalizationMatches && tokenSwitcherCompatible && codexSwitcherCompatible && reusableRowWidthsMatch && @@ -261,6 +263,7 @@ extension StatusItemController { switcherProvidersMatch && switcherUsageBarsShowUsedMatch && switcherOverviewAvailabilityMatches && + menuLocalizationMatches && providerSwitcherWidthMatches && !menu.items.isEmpty && menu.items.first?.view is ProviderSwitcherView @@ -355,11 +358,8 @@ extension StatusItemController { menu.removeItem(at: contentStartIndex) } - self.lastMergedSwitcherSelection = context.switcherSelection let enabledProviders = self.store.enabledProvidersForDisplay() - self.lastSwitcherProviders = enabledProviders - self.lastSwitcherUsageBarsShowUsed = self.settings.usageBarsShowUsed - self.lastSwitcherIncludesOverview = self.includesOverviewTab(enabledProviders: enabledProviders) + self.rememberMergedSwitcherState(enabledProviders, context.switcherSelection) self.addCodexAccountSwitcherIfNeeded( to: menu, display: context.codexAccountDisplay, @@ -407,10 +407,10 @@ extension StatusItemController { width: context.menuWidth) // Track which providers the switcher was built with for smart update detection if self.shouldMergeIcons, context.enabledProviders.count > 1 { - self.lastSwitcherProviders = context.enabledProviders - self.lastSwitcherUsageBarsShowUsed = self.settings.usageBarsShowUsed - self.lastMergedSwitcherSelection = context.switcherSelection - self.lastSwitcherIncludesOverview = context.includesOverview + self.rememberMergedSwitcherState( + context.enabledProviders, + context.switcherSelection, + context.includesOverview) } self.addCodexAccountSwitcherIfNeeded( to: menu, diff --git a/Sources/CodexBar/StatusItemController+MenuLocalization.swift b/Sources/CodexBar/StatusItemController+MenuLocalization.swift new file mode 100644 index 0000000000..52944810f9 --- /dev/null +++ b/Sources/CodexBar/StatusItemController+MenuLocalization.swift @@ -0,0 +1,36 @@ +import CodexBarCore + +extension StatusItemController { + func menuLocalizationSignature() -> String { + [ + codexBarLocalizationSignature(), + L("Overview"), + L("Cost"), + ].joined(separator: "|") + } + + func rememberMergedSwitcherState(_ providers: [UsageProvider], _ selection: ProviderSwitcherSelection?) { + self.rememberMergedSwitcherState( + providers, + selection, + self.includesOverviewTab(for: providers)) + } + + func rememberMergedSwitcherState( + _ providers: [UsageProvider], + _ selection: ProviderSwitcherSelection?, + _ includesOverview: Bool) + { + self.lastSwitcherProviders = providers + self.lastSwitcherUsageBarsShowUsed = self.settings.usageBarsShowUsed + self.lastMergedSwitcherSelection = selection + self.lastSwitcherIncludesOverview = includesOverview + self.lastMenuLocalizationSignature = self.menuLocalizationSignature() + } + + private func includesOverviewTab(for providers: [UsageProvider]) -> Bool { + !self.settings.resolvedMergedOverviewProviders( + activeProviders: providers, + maxVisibleProviders: SettingsStore.mergedOverviewProviderLimit).isEmpty + } +} diff --git a/Sources/CodexBar/StatusItemController+UsageHistoryMenu.swift b/Sources/CodexBar/StatusItemController+UsageHistoryMenu.swift index 73233cb38d..97908cfee3 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/StatusItemController.swift b/Sources/CodexBar/StatusItemController.swift index a9881e878a..76fb1dec12 100644 --- a/Sources/CodexBar/StatusItemController.swift +++ b/Sources/CodexBar/StatusItemController.swift @@ -160,6 +160,9 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin /// Tracks whether the merged-menu switcher was built with the Overview tab visible. /// Used to force switcher rebuilds when Overview availability toggles. var lastSwitcherIncludesOverview: Bool = false + /// Tracks localization-sensitive labels used by the merged menu. + /// Used to force menu rebuilds when app language changes. + var lastMenuLocalizationSignature: String = "" /// Tracks which providers the merged menu's switcher was built with, to detect when it needs full rebuild. var lastSwitcherProviders: [UsageProvider] = [] /// Tracks which switcher tab state was used for the current merged-menu switcher instance. @@ -605,6 +608,9 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin self.lastObservedUsageBarsShowUsed = usageBarsShowUsed shouldRefresh = true } + if self.menuLocalizationSignature() != self.lastMenuLocalizationSignature { + shouldRefresh = true + } return shouldRefresh } diff --git a/Sources/CodexBar/UsageBreakdownChartMenuView.swift b/Sources/CodexBar/UsageBreakdownChartMenuView.swift index 358c0955a3..0b1ca524dc 100644 --- a/Sources/CodexBar/UsageBreakdownChartMenuView.swift +++ b/Sources/CodexBar/UsageBreakdownChartMenuView.swift @@ -39,16 +39,16 @@ struct UsageBreakdownChartMenuView: View { Chart { ForEach(model.points) { point in BarMark( - x: .value("Day", point.date, unit: .day), - y: .value("Credits used", point.creditsUsed)) - .foregroundStyle(by: .value("Service", point.service)) + x: .value(L("Day"), point.date, unit: .day), + y: .value(L("Credits used"), point.creditsUsed)) + .foregroundStyle(by: .value(L("Service"), point.service)) } if let peak = model.peakPoint { let capStart = max(peak.creditsUsed - Self.capHeight(maxValue: model.maxCreditsUsed), 0) BarMark( - x: .value("Day", peak.date, unit: .day), - yStart: .value("Cap start", capStart), - yEnd: .value("Cap end", peak.creditsUsed)) + x: .value(L("Day"), peak.date, unit: .day), + yStart: .value(L("Cap start"), capStart), + yEnd: .value(L("Cap end"), peak.creditsUsed)) .foregroundStyle(Color(nsColor: .systemYellow)) } } diff --git a/Sources/CodexBar/UsageStore+Refresh.swift b/Sources/CodexBar/UsageStore+Refresh.swift index bf73bb84e6..db3c9d2e21 100644 --- a/Sources/CodexBar/UsageStore+Refresh.swift +++ b/Sources/CodexBar/UsageStore+Refresh.swift @@ -382,7 +382,7 @@ extension UsageStore { let providerName = ProviderDescriptorRegistry.descriptor(for: provider).metadata.displayName AppNotifications.shared.post( idPrefix: "permission-prompt-\(provider.rawValue)", - title: "\(providerName) is waiting for permission", + title: L("%@ is waiting for permission", providerName), body: error.localizedDescription, soundEnabled: false) } diff --git a/Tests/CodexBarTests/StatusMenuLocalizationRefreshTests.swift b/Tests/CodexBarTests/StatusMenuLocalizationRefreshTests.swift new file mode 100644 index 0000000000..992507a63d --- /dev/null +++ b/Tests/CodexBarTests/StatusMenuLocalizationRefreshTests.swift @@ -0,0 +1,123 @@ +import AppKit +import CodexBarCore +import Testing +@testable import CodexBar + +@MainActor +@Suite(.serialized) +struct StatusMenuLocalizationRefreshTests { + @Test + func `open merged menu refreshes localized switcher and cost title when language changes`() { + let previousLanguage = UserDefaults.standard.object(forKey: "appLanguage") + let previousAppleLanguages = UserDefaults.standard.object(forKey: "AppleLanguages") + defer { + if let previousLanguage { + UserDefaults.standard.set(previousLanguage, forKey: "appLanguage") + } else { + UserDefaults.standard.removeObject(forKey: "appLanguage") + } + if let previousAppleLanguages { + UserDefaults.standard.set(previousAppleLanguages, forKey: "AppleLanguages") + } else { + UserDefaults.standard.removeObject(forKey: "AppleLanguages") + } + } + + Self.disableMenuCardsForTesting() + let settings = Self.makeSettings() + settings.statusChecksEnabled = false + settings.refreshFrequency = .manual + settings.mergeIcons = true + settings.switcherShowsIcons = false + settings.selectedMenuProvider = .codex + settings.costUsageEnabled = true + + let registry = ProviderRegistry.shared + for provider in UsageProvider.allCases { + guard let metadata = registry.metadata[provider] else { continue } + let shouldEnable = provider == .codex || provider == .claude + settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: shouldEnable) + } + + let fetcher = UsageFetcher() + let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) + store._setTokenSnapshotForTesting(CostUsageTokenSnapshot( + sessionTokens: 123, + sessionCostUSD: 0.12, + last30DaysTokens: 123, + last30DaysCostUSD: 1.23, + daily: [ + CostUsageDailyReport.Entry( + date: "2025-12-23", + inputTokens: nil, + outputTokens: nil, + totalTokens: 123, + costUSD: 1.23, + modelsUsed: nil, + modelBreakdowns: nil), + ], + updatedAt: Date()), provider: .codex) + let controller = StatusItemController( + store: store, + settings: settings, + account: fetcher.loadAccountInfo(), + updater: DisabledUpdaterController(), + preferencesSelection: PreferencesSelection(), + statusBar: Self.makeStatusBarForTesting()) + defer { controller.releaseStatusItemsForTesting() } + + let menu = controller.makeMenu() + CodexBarLocalizationOverride.$appLanguage.withValue("es") { + controller.menuWillOpen(menu) + } + controller.openMenus[ObjectIdentifier(menu)] = menu + StatusItemController.setMenuRefreshEnabledForTesting(true) + defer { StatusItemController.resetMenuRefreshEnabledForTesting() } + + #expect(Self.switcherButtons(in: menu).first?.title == "Resumen") + #expect(menu.items.first(where: { $0.representedObject as? String == "menuCardCost" })?.title == "Coste") + + let initialSwitcher = menu.items.first?.view as? ProviderSwitcherView + let initialSwitcherID = initialSwitcher.map(ObjectIdentifier.init) + + CodexBarLocalizationOverride.$appLanguage.withValue("en") { + settings.appLanguage = "en" + controller.handleProviderConfigChange(reason: "appLanguage") + } + + let updatedSwitcher = menu.items.first?.view as? ProviderSwitcherView + #expect(Self.switcherButtons(in: menu).first?.title == "Overview") + #expect(menu.items.first(where: { $0.representedObject as? String == "menuCardCost" })?.title == "Cost") + if let initialSwitcherID, let updatedSwitcher { + #expect(initialSwitcherID != ObjectIdentifier(updatedSwitcher)) + } + } + + private static func disableMenuCardsForTesting() { + StatusItemController.menuCardRenderingEnabled = false + StatusItemController.setMenuRefreshEnabledForTesting(false) + } + + private static func makeStatusBarForTesting() -> NSStatusBar { + .system + } + + private static func makeSettings() -> SettingsStore { + let suite = "StatusMenuLocalizationRefreshTests-\(UUID().uuidString)" + let defaults = UserDefaults(suiteName: suite)! + defaults.removePersistentDomain(forName: suite) + let configStore = testConfigStore(suiteName: suite) + return SettingsStore( + userDefaults: defaults, + configStore: configStore, + zaiTokenStore: NoopZaiTokenStore(), + syntheticTokenStore: NoopSyntheticTokenStore()) + } + + private static func switcherButtons(in menu: NSMenu) -> [NSButton] { + guard let switcherView = menu.items.first?.view as? ProviderSwitcherView else { return [] } + return switcherView.subviews + .compactMap { $0 as? NSButton } + .sorted { $0.tag < $1.tag } + } +} diff --git a/Tests/CodexBarTests/UserFacingLocalizationCoverageTests.swift b/Tests/CodexBarTests/UserFacingLocalizationCoverageTests.swift new file mode 100644 index 0000000000..b6545f0743 --- /dev/null +++ b/Tests/CodexBarTests/UserFacingLocalizationCoverageTests.swift @@ -0,0 +1,112 @@ +import Foundation +import Testing + +struct UserFacingLocalizationCoverageTests { + @Test + func `selected user-facing UI surfaces avoid raw English literals`() throws { + let root = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() + .deletingLastPathComponent() + .deletingLastPathComponent() + + let forbiddenMarkersByFile: [String: [String]] = [ + "Sources/CodexBar/CostHistoryChartMenuView.swift": [ + ".value(\"Day\"", + ".value(\"Cost\"", + ".value(\"Cap start\"", + ".value(\"Cap end\"", + ], + "Sources/CodexBar/CreditsHistoryChartMenuView.swift": [ + ".value(\"Day\"", + ".value(\"Credits used\"", + ".value(\"Cap start\"", + ".value(\"Cap end\"", + "Text(\"Total (30d):", + "\\(total) credits", + "\\(used) credits", + ], + "Sources/CodexBar/PlanUtilizationHistoryChartMenuView.swift": [ + ".value(\"Series\"", + ".value(\"Capacity Start\"", + ".value(\"Capacity End\"", + ".value(\"Utilization Start\"", + ".value(\"Utilization End\"", + ], + "Sources/CodexBar/PreferencesCodexAccountsSection.swift": [ + "?? \"No system account\"", + "return \"Adding Account…\"", + "return \"Add Account\"", + "return \"Re-authenticating…\"", + "return \"Re-auth\"", + "ProviderSettingsSection(title: \"Accounts\")", + "Text(\"Active\")", + "Text(\"Choose which Codex account CodexBar should follow.\")", + "Text(\"Account\")", + "Text(\"No Codex accounts detected yet.\")", + "Text(\"System\")", + "Text(\"The default Codex account on this Mac.\")", + "Text(\"(System)\")", + "Button(\"Remove\")", + ], + "Sources/CodexBar/PreferencesProviderDetailView.swift": [ + ".help(\"Refresh\")", + "accessibilityLabel: \"Usage used\"", + ], + "Sources/CodexBar/PreferencesProviderErrorView.swift": [ + ".help(\"Copy error\")", + ], + "Sources/CodexBar/PreferencesProviderSettingsRows.swift": [ + "Text(self.title)", + "Text(self.toggle.title)", + "Text(self.toggle.subtitle)", + "Button(action.title)", + "Text(self.picker.title)", + "Text(option.title)", + "Text(trimmedTitle)", + "Text(trimmedSubtitle)", + "Text(self.descriptor.title)", + "Text(self.descriptor.subtitle)", + "Text(\"No token accounts yet.\")", + "Button(\"Remove\")", + "TextField(\"Label\"", + "Button(\"Add\")", + "TextField(\"Org ID (optional)\"", + ".help(\"Optional organization ID for accounts linked to multiple Anthropic organizations.\")", + "Button(\"Open token file\")", + "Button(\"Reload\")", + "Text(\"No organizations loaded. Click Refresh after setting your API key.\")", + "Button(\"Refresh organizations\")", + ], + "Sources/CodexBar/PreferencesProviderSidebarView.swift": [ + ".help(\"Drag to reorder\")", + "\"Disabled —", + ".accessibilityLabel(\"Reorder\")", + ], + "Sources/CodexBar/StatusItemController+UsageHistoryMenu.swift": [ + "Text(\"Subscription Utilization\")", + ], + "Sources/CodexBar/StatusItemController+CostMenuCard.swift": [ + "static let costMenuTitle", + ], + "Sources/CodexBar/UsageBreakdownChartMenuView.swift": [ + ".value(\"Day\"", + ".value(\"Credits used\"", + ".value(\"Service\"", + ".value(\"Cap start\"", + ".value(\"Cap end\"", + ], + ] + + var violations: [String] = [] + for (relativePath, markers) in forbiddenMarkersByFile.sorted(by: { $0.key < $1.key }) { + let source = try String(contentsOf: root.appendingPathComponent(relativePath), encoding: .utf8) + for marker in markers where source.contains(marker) { + violations.append("\(relativePath): \(marker)") + } + } + + #expect( + violations.isEmpty, + "Raw user-facing localization markers remain:\n\(violations.joined(separator: "\n"))") + } +} From 532b05284131f36008f452cd9038d188b60ea979 Mon Sep 17 00:00:00 2001 From: Shun Min Chang Date: Wed, 27 May 2026 13:36:39 +0800 Subject: [PATCH 3/6] chore: restore tracked gitignore Remove the local screenshot ignore rule from the tracked repository ignore file. Screenshots remain excluded by the local git info exclude entry so they stay out of git. --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8580a0d819..8bb2ec1f8f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ xcuserdata/ .swiftpm/xcode/xcshareddata/ .codexbar/config.json -.codexbar-local/ *.env *.local From 0e32850a34233b37f109fff9e3fd1ecabc7c90b5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 28 May 2026 14:18:37 +0100 Subject: [PATCH 4/6] test: wait for localized menu rebuild --- CHANGELOG.md | 1 + .../StatusMenuLocalizationRefreshTests.swift | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6b5d55bcb..caeadbd6ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Changed - Tests: document and audit that routine validation must not trigger macOS Keychain prompts. +- Localization: localize popup panels and provider settings UI across supported languages (#1181). Thanks @jack24254029! ### Added - AWS Bedrock: support resolving usage and cost-history credentials from a named AWS profile via the AWS CLI (#1190). Thanks @oleksandr-soldatov! diff --git a/Tests/CodexBarTests/StatusMenuLocalizationRefreshTests.swift b/Tests/CodexBarTests/StatusMenuLocalizationRefreshTests.swift index 992507a63d..4a78b364c9 100644 --- a/Tests/CodexBarTests/StatusMenuLocalizationRefreshTests.swift +++ b/Tests/CodexBarTests/StatusMenuLocalizationRefreshTests.swift @@ -7,7 +7,7 @@ import Testing @Suite(.serialized) struct StatusMenuLocalizationRefreshTests { @Test - func `open merged menu refreshes localized switcher and cost title when language changes`() { + func `open merged menu refreshes localized switcher and cost title when language changes`() async { let previousLanguage = UserDefaults.standard.object(forKey: "appLanguage") let previousAppleLanguages = UserDefaults.standard.object(forKey: "AppleLanguages") defer { @@ -79,12 +79,23 @@ struct StatusMenuLocalizationRefreshTests { let initialSwitcher = menu.items.first?.view as? ProviderSwitcherView let initialSwitcherID = initialSwitcher.map(ObjectIdentifier.init) + var rebuildCount = 0 + controller._test_openMenuRebuildObserver = { _ in + rebuildCount += 1 + } + defer { controller._test_openMenuRebuildObserver = nil } CodexBarLocalizationOverride.$appLanguage.withValue("en") { settings.appLanguage = "en" controller.handleProviderConfigChange(reason: "appLanguage") } + for _ in 0..<100 where rebuildCount == 0 { + await Task.yield() + try? await Task.sleep(for: .milliseconds(10)) + } + + #expect(rebuildCount == 1) let updatedSwitcher = menu.items.first?.view as? ProviderSwitcherView #expect(Self.switcherButtons(in: menu).first?.title == "Overview") #expect(menu.items.first(where: { $0.representedObject as? String == "menuCardCost" })?.title == "Cost") From de2a37829eb44c994fbc49993960521b6ba343d9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 28 May 2026 14:35:18 +0100 Subject: [PATCH 5/6] fix: localize dynamic provider settings safely --- .../PreferencesProviderSettingsRows.swift | 4 +- .../Kilo/KiloProviderImplementation.swift | 2 + .../Shared/ProviderCookieSourceUI.swift | 82 +++++++++++++++++-- .../Shared/ProviderSettingsDescriptors.swift | 20 +++++ .../Resources/ca.lproj/Localizable.strings | 15 ++++ .../Resources/en.lproj/Localizable.strings | 15 ++++ .../Resources/es.lproj/Localizable.strings | 15 ++++ .../Resources/pt-BR.lproj/Localizable.strings | 15 ++++ .../zh-Hans.lproj/Localizable.strings | 15 ++++ .../zh-Hant.lproj/Localizable.strings | 15 ++++ .../PopupLocalizationTests.swift | 63 ++++++++++++++ 11 files changed, 254 insertions(+), 7 deletions(-) diff --git a/Sources/CodexBar/PreferencesProviderSettingsRows.swift b/Sources/CodexBar/PreferencesProviderSettingsRows.swift index 154fa5b6fe..5d19dc140f 100644 --- a/Sources/CodexBar/PreferencesProviderSettingsRows.swift +++ b/Sources/CodexBar/PreferencesProviderSettingsRows.swift @@ -424,12 +424,12 @@ struct ProviderSettingsOrganizationsRowView: View { self.descriptor.onToggle(entry.id, newValue) })) { VStack(alignment: .leading, spacing: 1) { - Text(L(entry.title)) + Text(entry.localizesTitle ? L(entry.title) : entry.title) .font(.footnote) if let subtitle = entry.subtitle, !subtitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - Text(L(subtitle)) + Text(entry.localizesSubtitle ? L(subtitle) : subtitle) .font(.caption) .foregroundStyle(.secondary) } diff --git a/Sources/CodexBar/Providers/Kilo/KiloProviderImplementation.swift b/Sources/CodexBar/Providers/Kilo/KiloProviderImplementation.swift index 8038ddb814..792830fbd9 100644 --- a/Sources/CodexBar/Providers/Kilo/KiloProviderImplementation.swift +++ b/Sources/CodexBar/Providers/Kilo/KiloProviderImplementation.swift @@ -110,6 +110,8 @@ struct KiloProviderImplementation: ProviderImplementation { id: org.id, title: org.name, subtitle: org.role, + localizesTitle: false, + localizesSubtitle: false, isEnabled: settings.kiloIsOrganizationEnabled(org.id), isLocked: false)) } diff --git a/Sources/CodexBar/Providers/Shared/ProviderCookieSourceUI.swift b/Sources/CodexBar/Providers/Shared/ProviderCookieSourceUI.swift index 4964f4df4b..7c3fccc720 100644 --- a/Sources/CodexBar/Providers/Shared/ProviderCookieSourceUI.swift +++ b/Sources/CodexBar/Providers/Shared/ProviderCookieSourceUI.swift @@ -1,7 +1,7 @@ import CodexBarCore enum ProviderCookieSourceUI { - static let keychainDisabledPrefix = + static let keychainDisabledPrefixKey = "Keychain access is disabled in Advanced, so browser cookie import is unavailable." static func options(allowsOff: Bool, keychainDisabled: Bool) -> [ProviderSettingsPickerOption] { @@ -29,16 +29,88 @@ enum ProviderCookieSourceUI { manual: String, off: String) -> String { + let localizedAuto = self.localizedSubtitle(auto) + let localizedManual = self.localizedSubtitle(manual) + let localizedOff = self.localizedSubtitle(off) if keychainDisabled { - return source == .off ? off : "\(self.keychainDisabledPrefix) \(manual)" + return source == .off + ? localizedOff + : "\(L(self.keychainDisabledPrefixKey)) \(localizedManual)" } switch source { case .auto: - return auto + return localizedAuto case .manual: - return manual + return localizedManual case .off: - return off + return localizedOff } } + + private static func localizedSubtitle(_ subtitle: String) -> String { + let trimmed = subtitle.trimmingCharacters(in: .whitespacesAndNewlines) + if let source = trimmed.removing(prefix: "Paste a Cookie header or cURL capture from ", suffix: ".") { + return L("Paste a Cookie header or cURL capture from %@.", source) + } + if let source = trimmed.removing(prefix: "Paste a Cookie header or full cURL capture from ", suffix: ".") { + return L("Paste a Cookie header or full cURL capture from %@.", source) + } + if let source = trimmed.removing(prefix: "Paste a Cookie header captured from ", suffix: ".") { + return L("Paste a Cookie header captured from %@.", source) + } + if let source = trimmed.removing(prefix: "Paste a Cookie header from ", suffix: ".") { + return L("Paste a Cookie header from %@.", source) + } + if let token = trimmed.removing(prefix: "Paste a full cookie header or the ", suffix: " value.") { + return L("Paste a full cookie header or the %@ value.", token) + } + if let source = trimmed.removing(prefix: "Paste a Cookie or Authorization header from ", suffix: ".") { + return L("Paste a Cookie or Authorization header from %@.", source) + } + if let token = trimmed.removing(prefix: "Paste the ", suffix: " value or a full Cookie header.") { + return L("Paste the %@ value or a full Cookie header.", token) + } + if let token = trimmed.removing(prefix: "Manually paste an ", suffix: " from a browser session.") { + return L("Manually paste an %@ from a browser session.", token) + } + if let token = trimmed.removing( + prefix: "Uses username + password to login and obtain an ", + suffix: " automatically.") + { + return L("Uses username + password to login and obtain an %@ automatically.", token) + } + if let parts = trimmed.removingTwoParts(prefix: "Paste the ", separator: " JSON bundle from ", suffix: ".") { + return L("Paste the %@ JSON bundle from %@.", parts.0, parts.1) + } + if let provider = trimmed.removing(prefix: "Disable ", suffix: " dashboard cookie usage.") { + return L("Disable %@ dashboard cookie usage.", provider) + } + if let provider = trimmed.removing(prefix: "", suffix: " cookies are disabled.") { + return L("%@ cookies are disabled.", provider) + } + if let provider = trimmed.removing(prefix: "", suffix: " authentication is disabled.") { + return L("%@ authentication is disabled.", provider) + } + if let provider = trimmed.removing(prefix: "", suffix: " web API access is disabled.") { + return L("%@ web API access is disabled.", provider) + } + return L(trimmed) + } +} + +extension String { + fileprivate func removing(prefix: String, suffix: String) -> String? { + guard self.hasPrefix(prefix), self.hasSuffix(suffix) else { return nil } + let start = self.index(self.startIndex, offsetBy: prefix.count) + let end = self.index(self.endIndex, offsetBy: -suffix.count) + guard start <= end else { return nil } + return String(self[start.. (String, String)? { + guard let value = self.removing(prefix: prefix, suffix: suffix), + let range = value.range(of: separator) + else { return nil } + return (String(value[.. SettingsStore { let defaults = try #require(UserDefaults(suiteName: suite)) defaults.removePersistentDomain(forName: suite) From b6969a7efc6b3ecfa647a4540fb07e4c86e9d991 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 28 May 2026 14:41:42 +0100 Subject: [PATCH 6/6] fix: preserve localized JSON bundle argument order --- .../CodexBar/Resources/zh-Hans.lproj/Localizable.strings | 2 +- .../CodexBar/Resources/zh-Hant.lproj/Localizable.strings | 2 +- Tests/CodexBarTests/PopupLocalizationTests.swift | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings index bb273347b6..281da669a3 100644 --- a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings @@ -972,7 +972,7 @@ "Paste a Cookie header or full cURL capture from T3 Chat settings." = "粘贴 T3 Chat 设置中的 Cookie 标头或完整 cURL 捕获内容。"; "Paste the Cookie header from a request to admin.mistral.ai. Must contain an ory_session_* cookie." = "粘贴发往 admin.mistral.ai 请求中的 Cookie 标头。必须包含 ory_session_* Cookie。"; "Paste the Oasis-Token from a logged-in browser session on platform.stepfun.com." = "粘贴 platform.stepfun.com 已登录浏览器会话中的 Oasis-Token。"; -"Paste the %@ JSON bundle from %@." = "粘贴来自 %@ 的 %@ JSON 包。"; +"Paste the %@ JSON bundle from %@." = "粘贴来自 %2$@ 的 %1$@ JSON 包。"; "Paste the %@ value or a full Cookie header." = "粘贴 %@ 值或完整 Cookie 标头。"; "Personal account" = "个人账号"; "Project ID" = "项目 ID"; diff --git a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings index f964a21da9..b390192fc3 100644 --- a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings @@ -868,7 +868,7 @@ "Paste a Cookie header or full cURL capture from T3 Chat settings." = "貼上 T3 Chat 設定中的 Cookie 標頭或完整 cURL 擷取內容。"; "Paste the Cookie header from a request to admin.mistral.ai. Must contain an ory_session_* cookie." = "貼上發往 admin.mistral.ai 請求中的 Cookie 標頭。必須包含 ory_session_* Cookie。"; "Paste the Oasis-Token from a logged-in browser session on platform.stepfun.com." = "貼上 platform.stepfun.com 已登入瀏覽器工作階段中的 Oasis-Token。"; -"Paste the %@ JSON bundle from %@." = "貼上來自 %@ 的 %@ JSON 組合。"; +"Paste the %@ JSON bundle from %@." = "貼上來自 %2$@ 的 %1$@ JSON 組合。"; "Paste the %@ value or a full Cookie header." = "貼上 %@ 值或完整 Cookie 標頭。"; "Personal account" = "個人帳號"; "Project ID" = "專案 ID"; diff --git a/Tests/CodexBarTests/PopupLocalizationTests.swift b/Tests/CodexBarTests/PopupLocalizationTests.swift index e39df8e7fa..8389645f91 100644 --- a/Tests/CodexBarTests/PopupLocalizationTests.swift +++ b/Tests/CodexBarTests/PopupLocalizationTests.swift @@ -108,11 +108,18 @@ struct PopupLocalizationTests { auto: "Automatically imports browser cookies.", manual: "Paste a Cookie header or cURL capture from T3 Chat settings.", off: "T3 Chat cookies are disabled.") + let jsonBundleSubtitle = ProviderCookieSourceUI.subtitle( + source: .manual, + keychainDisabled: false, + auto: "Automatically imports browser cookies.", + manual: "Paste the localStorage JSON bundle from Windsurf session.", + off: "Windsurf cookies are disabled.") #expect(subtitle.contains("貼上")) #expect(!subtitle.contains("Paste a Cookie")) #expect(disabledSubtitle.contains("鑰匙圈")) #expect(!disabledSubtitle.contains("Keychain access")) + #expect(jsonBundleSubtitle.contains("來自 Windsurf session 的 localStorage JSON")) } }