Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down
29 changes: 15 additions & 14 deletions Sources/CodexBar/CodexAccountPromotionCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 7 additions & 7 deletions Sources/CodexBar/CodexLoginAlertPresentation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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[..<idx])…"
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/CodexLoginRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ struct CodexLoginRunner {
}
let trimmed = merged.trimmingCharacters(in: .whitespacesAndNewlines)
let limited = trimmed.prefix(4000)
return limited.isEmpty ? "No output captured." : String(limited)
return limited.isEmpty ? L("No output captured.") : String(limited)
}

private static func readToEnd(_ pipe: Pipe, timeout: TimeInterval = 3.0) async -> String {
Expand Down
17 changes: 10 additions & 7 deletions Sources/CodexBar/CostHistoryChartMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Expand All @@ -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) {
Expand Down
31 changes: 18 additions & 13 deletions Sources/CodexBar/CreditsHistoryChartMenuView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,24 @@ 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
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))
}
}
Expand All @@ -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) {
Expand Down Expand Up @@ -101,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)
}
Expand Down Expand Up @@ -308,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
Expand All @@ -324,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)
}
}
11 changes: 7 additions & 4 deletions Sources/CodexBar/CursorLoginRunner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
}
Loading