diff --git a/Sources/CodexBar/CodexLocalProjectUsageInspectorSelection.swift b/Sources/CodexBar/CodexLocalProjectUsageInspectorSelection.swift new file mode 100644 index 0000000000..d3c6e5d79d --- /dev/null +++ b/Sources/CodexBar/CodexLocalProjectUsageInspectorSelection.swift @@ -0,0 +1,27 @@ +import Foundation +import Observation + +/// Persistent Workspaces-window destination selection. The synthetic +/// All Workspaces destination and concrete project destinations survive window +/// reopen without coupling the raw usage snapshot to presentation state. +@MainActor +@Observable +final class CodexLocalProjectUsageInspectorSelection { + private let defaults: UserDefaults + private let projectKey = "codexLocalProjectUsageWindowSelectedProjectId" + + var selectedProjectID: String? { + didSet { + if let selectedProjectID, !selectedProjectID.isEmpty { + self.defaults.set(selectedProjectID, forKey: self.projectKey) + } else { + self.defaults.removeObject(forKey: self.projectKey) + } + } + } + + init(defaults: UserDefaults = .standard) { + self.defaults = defaults + self.selectedProjectID = defaults.string(forKey: self.projectKey) + } +} diff --git a/Sources/CodexBar/CodexLocalProjectUsageSubmenuView.swift b/Sources/CodexBar/CodexLocalProjectUsageSubmenuView.swift new file mode 100644 index 0000000000..ee387d97a3 --- /dev/null +++ b/Sources/CodexBar/CodexLocalProjectUsageSubmenuView.swift @@ -0,0 +1,402 @@ +import AppKit +import CodexBarCore +import SwiftUI + +enum CodexLocalProjectUsageSubmenuLayout { + static let menuWidth: CGFloat = 520 + static let horizontalPadding: CGFloat = 16 + static let verticalPadding: CGFloat = 10 + static let sectionSpacing: CGFloat = 9 + static let chartHeight: CGFloat = 96 + static let projectListHeight: CGFloat = 216 + static let projectRowSpacing: CGFloat = 2 + static let projectRowVerticalPadding: CGFloat = 4 + static let usageBarHeight: CGFloat = 2 +} + +@MainActor +struct CodexLocalProjectUsageSubmenuView: View { + let store: UsageStore + let settings: SettingsStore + let width: CGFloat + + private var resolvedWidth: CGFloat { + max(self.width, CodexLocalProjectUsageSubmenuLayout.menuWidth) + } + + var body: some View { + VStack(alignment: .leading, spacing: CodexLocalProjectUsageSubmenuLayout.sectionSpacing) { + self.header + self.statusLine + if let snapshot = self.store.codexLocalProjectUsageSnapshot { + self.usageContent(snapshot) + } else { + self.emptyState + } + self.footer + } + .padding(.horizontal, CodexLocalProjectUsageSubmenuLayout.horizontalPadding) + .padding(.vertical, CodexLocalProjectUsageSubmenuLayout.verticalPadding) + .frame(width: self.resolvedWidth, alignment: .topLeading) + } + + private var header: some View { + HStack(alignment: .firstTextBaseline, spacing: 12) { + VStack(alignment: .leading, spacing: 2) { + Text(L("codex_workspaces_title")) + .font(.system(size: NSFont.menuFont(ofSize: 0).pointSize, weight: .semibold)) + .lineLimit(1) + Text(L("codex_workspaces_estimated_local_logs")) + .font(.system(size: NSFont.smallSystemFontSize)) + .foregroundStyle(.secondary) + .lineLimit(1) + } + Spacer(minLength: 8) + Text(self.historyLabel) + .font(.system(size: NSFont.smallSystemFontSize)) + .foregroundStyle(.secondary) + .lineLimit(1) + } + } + + @ViewBuilder + private var statusLine: some View { + if self.store.codexLocalProjectUsageRefreshInFlight { + HStack(spacing: 7) { + ProgressView() + .controlSize(.small) + Text( + self.store.codexLocalProjectUsageProgressSubtitle + ?? L("codex_workspaces_indexing_local_logs")) + .font(.system(size: NSFont.smallSystemFontSize)) + .lineLimit(1) + Spacer(minLength: 4) + if let fraction = self.store.codexLocalProjectUsageProgressFraction { + Text(fraction.formatted(.percent.precision(.fractionLength(0)))) + .font(.system(size: NSFont.smallSystemFontSize).monospacedDigit()) + .foregroundStyle(.secondary) + } + } + } else if let snapshot = self.store.codexLocalProjectUsageSnapshot, + snapshot.sourceStatus.isPartial + { + VStack(alignment: .leading, spacing: 1) { + Label(L("codex_workspaces_failed_some_logs"), systemImage: "exclamationmark.triangle") + Text(L("codex_workspaces_showing_last_complete_index")) + .padding(.leading, 20) + } + .font(.system(size: NSFont.smallSystemFontSize)) + .foregroundStyle(.secondary) + } else if self.store.codexLocalProjectUsageError != nil { + Label(L("codex_workspaces_failed_read_logs"), systemImage: "exclamationmark.triangle") + .font(.system(size: NSFont.smallSystemFontSize)) + .foregroundStyle(.secondary) + } else if self.store.codexLocalProjectUsageLoadState == .stale, + self.store.codexLocalProjectUsageSnapshot != nil + { + Label( + L("codex_workspaces_stale_status", self.updatedRelativeDescription), + systemImage: "clock") + .font(.system(size: NSFont.smallSystemFontSize)) + .foregroundStyle(.secondary) + } + } + + private var emptyState: some View { + Text(L("codex_workspaces_no_local_usage")) + .font(.system(size: NSFont.smallSystemFontSize)) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 12) + } + + private func usageContent(_ snapshot: CodexLocalProjectUsageSnapshot) -> some View { + let projects = self.store.codexLocalProjectUsageRankedProjects(snapshot.projects) + return VStack(alignment: .leading, spacing: CodexLocalProjectUsageSubmenuLayout.sectionSpacing) { + self.kpiStrip(snapshot, projects: projects) + Divider() + self.dailyChart(snapshot.daily) + Divider() + self.projectList(projects) + } + } + + private func kpiStrip( + _ snapshot: CodexLocalProjectUsageSnapshot, + projects: [CodexLocalProjectUsage]) -> some View + { + HStack(spacing: 0) { + self.metric( + L("codex_workspaces_tokens"), + self.store.codexLocalProjectUsageDisplayTotal(snapshot.total) + .map(UsageFormatter.tokenCountString) ?? L("codex_workspaces_not_available")) + self.metricDivider + if self.settings.codexLocalProjectUsageShowsEstimatedCost { + self.metric( + L("codex_workspaces_estimated_cost_short"), + self.estimatedCostValue(for: projects)) + self.metricDivider + } + self.metric(L("codex_workspaces_sessions"), "\(snapshot.sessions.count)") + self.metricDivider + self.metric(L("codex_workspaces_workspaces_chats"), "\(projects.count)") + } + } + + private func metric(_ title: String, _ value: String) -> some View { + VStack(alignment: .leading, spacing: 2) { + Text(title) + .font(.system(size: NSFont.smallSystemFontSize)) + .foregroundStyle(.secondary) + .lineLimit(1) + Text(value) + .font(.system(size: NSFont.menuFont(ofSize: 0).pointSize, weight: .semibold).monospacedDigit()) + .lineLimit(1) + .minimumScaleFactor(0.78) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + private var metricDivider: some View { + Divider() + .frame(height: 26) + .padding(.horizontal, 10) + } + + private func dailyChart(_ points: [CodexLocalUsageDailyPoint]) -> some View { + let visible = Array(points.suffix(60)) + let displayed = visible.map { self.store.codexLocalProjectUsageDailyDisplayTotal($0) } + let maximum = max(displayed.max() ?? 0, 1) + let availableWidth = max(self.resolvedWidth - 32, 1) + let barWidth = max( + 2, + min(6, (availableWidth - CGFloat(max(visible.count - 1, 0) * 2)) / CGFloat(max(visible.count, 1)))) + let branding = ProviderDescriptorRegistry.descriptor(for: .codex).branding.color + let barColor = Color(red: branding.red, green: branding.green, blue: branding.blue) + + return VStack(alignment: .leading, spacing: 5) { + HStack(alignment: .firstTextBaseline) { + Text(L("codex_workspaces_daily_token_usage")) + .font(.system(size: NSFont.smallSystemFontSize, weight: .semibold)) + .foregroundStyle(.secondary) + Spacer(minLength: 8) + Text(L("codex_workspaces_last_days", visible.count)) + .font(.system(size: NSFont.smallSystemFontSize)) + .foregroundStyle(.secondary) + } + HStack(alignment: .bottom, spacing: 2) { + ForEach(Array(visible.enumerated()), id: \.element.id) { index, point in + let tokens = displayed[index] + RoundedRectangle(cornerRadius: 1.5, style: .continuous) + .fill(barColor.opacity(0.78)) + .frame( + width: barWidth, + height: max(3, CGFloat(tokens) / CGFloat(maximum) * 62)) + .help(L( + "codex_workspaces_daily_point_help", + point.day, + UsageFormatter.tokenCountString(tokens))) + .accessibilityLabel(L( + "codex_workspaces_daily_point_accessibility", + point.day, + UsageFormatter.tokenCountString(tokens))) + } + if visible.isEmpty { + Text(L("codex_workspaces_no_daily_usage")) + .font(.system(size: NSFont.smallSystemFontSize)) + .foregroundStyle(.secondary) + } + Spacer(minLength: 0) + } + } + .frame(height: CodexLocalProjectUsageSubmenuLayout.chartHeight, alignment: .bottomLeading) + } + + private func projectList(_ projects: [CodexLocalProjectUsage]) -> some View { + VStack(alignment: .leading, spacing: 5) { + HStack(alignment: .firstTextBaseline) { + Text(L("codex_workspaces_title")) + .font(.system(size: NSFont.smallSystemFontSize, weight: .semibold)) + .foregroundStyle(.secondary) + Spacer(minLength: 8) + Text("\(projects.count)") + .font(.system(size: NSFont.smallSystemFontSize).monospacedDigit()) + .foregroundStyle(.secondary) + } + if projects.isEmpty { + Text(L("codex_workspaces_no_projects_or_chats")) + .font(.system(size: NSFont.smallSystemFontSize)) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 12) + } else { + let maximumDisplayedTokens = max( + projects.compactMap { self.store.codexLocalProjectUsageDisplayTotal($0.totals) }.max() ?? 0, + 1) + ScrollView(.vertical) { + LazyVStack(alignment: .leading, spacing: CodexLocalProjectUsageSubmenuLayout.projectRowSpacing) { + ForEach(projects) { project in + self.projectRow(project, maximumDisplayedTokens: maximumDisplayedTokens) + } + } + } + .scrollIndicators(.visible) + .frame(height: CodexLocalProjectUsageSubmenuLayout.projectListHeight) + } + } + } + + private func projectRow(_ project: CodexLocalProjectUsage, maximumDisplayedTokens: Int) -> some View { + let tokens = self.store.codexLocalProjectUsageDisplayTotal(project.totals) + .map(UsageFormatter.tokenCountString) ?? L("codex_workspaces_not_available") + return VStack(alignment: .leading, spacing: 2) { + HStack(alignment: .firstTextBaseline, spacing: 7) { + if project.severity != .normal { + Image(systemName: self.severityIcon(project.severity)) + .imageScale(.small) + .foregroundStyle(self.severityColor(project.severity)) + } + Text(project.displayName) + .font(.system(size: NSFont.menuFont(ofSize: 0).pointSize, weight: .semibold)) + .lineLimit(1) + Spacer(minLength: 8) + Text(tokens) + .font(.system(size: NSFont.smallSystemFontSize, weight: .semibold).monospacedDigit()) + .foregroundStyle(self.severityColor(project.severity)) + } + Text(self.projectPath(for: project)) + .font(.system(size: NSFont.smallSystemFontSize)) + .foregroundStyle(.secondary) + .lineLimit(1) + .truncationMode(.middle) + self.projectUsageBar(project, maximumDisplayedTokens: maximumDisplayedTokens) + HStack(spacing: 7) { + if self.settings.codexLocalProjectUsageShowsEstimatedCost { + Text(self.estimatedCostValue(for: project.costEstimate)) + } + Text(L("codex_workspaces_session_count", project.sessionCount)) + if let model = project.topModel { + Text(L("codex_workspaces_bullet_value", model)) + .lineLimit(1) + .truncationMode(.tail) + } + } + .font(.system(size: NSFont.smallSystemFontSize)) + .foregroundStyle(.secondary) + } + .padding(.vertical, CodexLocalProjectUsageSubmenuLayout.projectRowVerticalPadding) + .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityElement(children: .combine) + .accessibilityLabel(self.projectAccessibilityLabel(project)) + } + + private func projectPath(for project: CodexLocalProjectUsage) -> String { + if project.id == CodexLocalProjectRootResolver.chatsProjectId { + return L("codex_workspaces_chats_description") + } + return project.path ?? L("codex_workspaces_chats_description") + } + + private func projectUsageBar( + _ project: CodexLocalProjectUsage, + maximumDisplayedTokens: Int) -> some View + { + let displayedTokens = self.store.codexLocalProjectUsageDisplayTotal(project.totals) ?? 0 + let fraction = min(max(CGFloat(displayedTokens) / CGFloat(maximumDisplayedTokens), 0), 1) + return GeometryReader { geometry in + ZStack(alignment: .leading) { + Capsule(style: .continuous) + .fill(Color.secondary.opacity(0.18)) + Capsule(style: .continuous) + .fill(Color.accentColor.opacity(0.72)) + .frame(width: geometry.size.width * fraction) + } + } + .frame(height: CodexLocalProjectUsageSubmenuLayout.usageBarHeight) + .accessibilityHidden(true) + } + + private func severityIcon(_ severity: CodexLocalUsageSeverity) -> String { + switch severity { + case .normal: + "" + case .elevated: + "arrow.up.circle.fill" + case .high: + "exclamationmark.triangle.fill" + } + } + + private func severityColor(_ severity: CodexLocalUsageSeverity) -> Color { + switch severity { + case .normal: + .primary + case .elevated: + .orange + case .high: + .red + } + } + + private var footer: some View { + HStack(spacing: 5) { + if let snapshot = self.store.codexLocalProjectUsageSnapshot { + Text( + L( + "codex_workspaces_indexed_files", + snapshot.updatedAt.formatted(.relative(presentation: .named)), + snapshot.indexedFileCount)) + } else { + Text(L("codex_workspaces_estimated_local_logs")) + } + Spacer(minLength: 0) + } + .font(.system(size: NSFont.smallSystemFontSize)) + .foregroundStyle(.secondary) + .lineLimit(1) + .truncationMode(.tail) + } + + private var historyLabel: String { + guard let snapshot = self.store.codexLocalProjectUsageSnapshot else { return "" } + return L("codex_workspaces_last_days", snapshot.historyDays) + } + + private var updatedRelativeDescription: String { + self.store.codexLocalProjectUsageSnapshot?.updatedAt.formatted(.relative(presentation: .named)) ?? "" + } + + private func estimatedCostValue(for projects: [CodexLocalProjectUsage]) -> String { + let estimate = projects.reduce(into: CodexLocalCostEstimate()) { result, project in + result = CodexLocalCostEstimate( + knownUSD: result.knownUSD + project.costEstimate.knownUSD, + unknownTokens: result.unknownTokens + project.costEstimate.unknownTokens) + } + return self.estimatedCostValue(for: estimate) + } + + private func estimatedCostValue(for estimate: CodexLocalCostEstimate) -> String { + let value: String = switch estimate.coverage { + case .known: + UsageFormatter.currencyString(estimate.knownUSD, currencyCode: "USD") + case .partial: + UsageFormatter.currencyString(estimate.knownUSD, currencyCode: "USD") + "+" + case .unavailable: + L("codex_workspaces_cost_unavailable") + } + return String(format: L("codex_workspaces_estimated_cost_value"), value) + } + + private func projectAccessibilityLabel(_ project: CodexLocalProjectUsage) -> String { + let tokens = self.store.codexLocalProjectUsageDisplayTotal(project.totals) + .map(UsageFormatter.tokenCountString) ?? L("codex_workspaces_not_available") + return CodexWorkspaceProjectAccessibilityDescription.make( + projectName: project.displayName, + tokenText: tokens, + severity: project.severity, + estimatedCostText: self.settings.codexLocalProjectUsageShowsEstimatedCost + ? self.estimatedCostValue(for: project.costEstimate) + : nil, + session: (project.sessionCount, project.topModel)) + } +} diff --git a/Sources/CodexBar/CodexLocalProjectUsageWindowController.swift b/Sources/CodexBar/CodexLocalProjectUsageWindowController.swift new file mode 100644 index 0000000000..073561753b --- /dev/null +++ b/Sources/CodexBar/CodexLocalProjectUsageWindowController.swift @@ -0,0 +1,73 @@ +import AppKit +import SwiftUI + +@MainActor +final class CodexLocalProjectUsageWindowController: NSWindowController { + static let selectedTabKey = "codexLocalProjectUsageWindowSelectedTab" + static let frameAutosaveName = "CodexLocalProjectUsageWindow" + static let preferredWidthMigrationKey = "codexLocalProjectUsageWindowPreferredWidthV2" + + private let store: UsageStore + + init( + store: UsageStore, + settings: SettingsStore, + selection: CodexLocalProjectUsageInspectorSelection) + { + self.store = store + let content = CodexLocalProjectUsageWindowView(store: store, settings: settings, selection: selection) + let window = NSWindow( + contentRect: NSRect(origin: .zero, size: CodexLocalProjectUsageWindowLayout.idealSize), + styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], + backing: .buffered, + defer: false) + window.title = L("codex_workspaces_window_title") + window.titleVisibility = .visible + window.titlebarAppearsTransparent = true + window.titlebarSeparatorStyle = .none + window.minSize = CodexLocalProjectUsageWindowLayout.minimumSize + window.contentViewController = NSHostingController(rootView: content) + window.isReleasedWhenClosed = false + window.setFrameAutosaveName(Self.frameAutosaveName) + Self.applyPreferredWidthIfNeeded(to: window) + super.init(window: window) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + nil + } + + func showAndRefresh() { + self.showWindow(nil) + guard let window = self.window else { return } + window.centerIfNeeded() + window.makeKeyAndOrderFront(nil) + NSApp.activate(ignoringOtherApps: true) + self.store.scheduleCodexLocalProjectUsageRefreshIfNeeded() + } + + func showSessions() { + UserDefaults.standard.set(CodexLocalProjectUsageWindowTab.sessions.rawValue, forKey: Self.selectedTabKey) + self.showAndRefresh() + } + + private static func applyPreferredWidthIfNeeded(to window: NSWindow) { + let defaults = UserDefaults.standard + guard !defaults.bool(forKey: Self.preferredWidthMigrationKey) else { return } + defaults.set(true, forKey: Self.preferredWidthMigrationKey) + + let currentSize = window.contentLayoutRect.size + guard currentSize.width < CodexLocalProjectUsageWindowLayout.idealSize.width else { return } + window.setContentSize(NSSize( + width: CodexLocalProjectUsageWindowLayout.idealSize.width, + height: max(currentSize.height, CodexLocalProjectUsageWindowLayout.idealSize.height))) + } +} + +extension NSWindow { + fileprivate func centerIfNeeded() { + guard self.frame.origin == .zero else { return } + self.center() + } +} diff --git a/Sources/CodexBar/CodexLocalProjectUsageWindowOverview.swift b/Sources/CodexBar/CodexLocalProjectUsageWindowOverview.swift new file mode 100644 index 0000000000..03449bee5e --- /dev/null +++ b/Sources/CodexBar/CodexLocalProjectUsageWindowOverview.swift @@ -0,0 +1,1061 @@ +import Charts +import CodexBarCore +import SwiftUI + +struct CodexWorkspaceDetail: View { + let presentation: CodexLocalProjectUsageWindowPresentation + @Binding var selectedTab: CodexLocalProjectUsageWindowTab + let showsEstimatedCost: Bool + let hidePersonalInfo: Bool + let historyDays: Int + @Binding var customHistoryDays: Int + let isRefreshing: Bool + let progressText: String? + let staleMessage: String? + @Binding var associatedSessionIDs: Set? + let setHistoryDays: (Int) -> Void + let refresh: () -> Void + @ViewBuilder let controls: Controls + + @AppStorage(CodexModelsRollout.featureFlagKey) + private var modelsRevampEnabled = CodexModelsRollout.defaultEnabled + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + CodexWorkspaceDetailHeader(presentation: self.presentation) + .padding(.horizontal, CodexLocalProjectUsageWindowLayout.detailPadding) + .padding(.top, 18) + .padding(.bottom, 8) + self.controls + Divider() + switch self.selectedTab { + case .overview: + CodexWorkspaceOverview( + presentation: self.presentation, + selectedTab: self.$selectedTab, + showsEstimatedCost: self.showsEstimatedCost) + case .sessions: + CodexWorkspaceSessionsTable( + rows: self.presentation.sessions, + showsEstimatedCost: self.showsEstimatedCost, + associatedSessionIDs: self.$associatedSessionIDs) + case .models: + if self.modelsRevampEnabled, + let payload = self.presentation.snapshot.modelsAnalytics + { + let snapshot = payload.snapshot(workspaceID: self.presentation.selectedProject?.id) + let dailyUsage = self.presentation.selectedProject?.daily ?? self.presentation.snapshot.daily + CodexModelsDashboardView( + snapshot: self.presentation.projection.projectedModelsAnalyticsSnapshot( + snapshot, + dailyUsage: dailyUsage), + scopeName: self.presentation.selectedProject?.displayName + ?? L("codex_workspaces_all_workspaces"), + showsEstimatedCost: self.showsEstimatedCost, + hidePersonalInfo: self.hidePersonalInfo, + historyDays: self.historyDays, + customHistoryDays: self.$customHistoryDays, + isRefreshing: self.isRefreshing, + progressText: self.progressText, + staleMessage: self.staleMessage, + setHistoryDays: self.setHistoryDays, + refresh: self.refresh, + showAssociatedSessions: { sessionIDs in + self.associatedSessionIDs = Set(sessionIDs) + self.selectedTab = .sessions + }) + } else { + CodexWorkspaceModelsTable( + rows: self.presentation.models, + totalTokens: self.presentation.displayedTokens ?? 0, + totalCost: self.presentation.costEstimate, + showsEstimatedCost: self.showsEstimatedCost) + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + } +} + +private struct CodexWorkspaceDetailHeader: View { + let presentation: CodexLocalProjectUsageWindowPresentation + + var body: some View { + HStack(alignment: .top, spacing: 16) { + VStack(alignment: .leading, spacing: 4) { + Text(self.title) + .font(.title2.weight(.semibold)) + .lineLimit(1) + Text(self.subtitle) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + .truncationMode(.middle) + .textSelection(.enabled) + } + Spacer(minLength: 12) + if let badge = self.statusBadge { + CodexWorkspaceStatusBadge(badge: badge) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityElement(children: .combine) + } + + private var title: String { + self.presentation.selectedProject?.displayName ?? L("codex_workspaces_all_workspaces") + } + + private var subtitle: String { + if let project = self.presentation.selectedProject { + guard let path = project.path else { return L("codex_workspaces_chats_description") } + let home = FileManager.default.homeDirectoryForCurrentUser.path + return path.hasPrefix(home) ? "~" + path.dropFirst(home.count) : path + } + return L("codex_workspaces_workspace_count", self.presentation.rankedProjects.count) + } + + private var statusBadge: CodexWorkspaceStatusBadge.Model? { + if self.presentation.snapshot.sourceStatus.isPartial { + return .init( + title: L("codex_workspaces_partial_data"), + systemImage: "exclamationmark.triangle.fill", + color: .orange, + help: L("codex_workspaces_showing_last_complete_index")) + } + guard let project = self.presentation.selectedProject else { return nil } + switch project.severity { + case .high: + return .init( + title: L("codex_workspaces_high_usage"), + systemImage: "exclamationmark.triangle.fill", + color: .red, + help: L("codex_workspaces_high_usage_help")) + case .elevated: + return .init( + title: L("codex_workspaces_elevated_usage"), + systemImage: "arrow.up.circle.fill", + color: .orange, + help: L("codex_workspaces_elevated_usage_help")) + case .normal: + return nil + } + } +} + +private struct CodexWorkspaceStatusBadge: View { + struct Model { + let title: String + let systemImage: String + let color: Color + let help: String + } + + let badge: Model + + var body: some View { + Label(self.badge.title, systemImage: self.badge.systemImage) + .font(.caption.weight(.medium)) + .foregroundStyle(self.badge.color) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(self.badge.color.opacity(0.1), in: Capsule()) + .help(self.badge.help) + .accessibilityHint(self.badge.help) + } +} + +private struct CodexWorkspaceOverview: View { + let presentation: CodexLocalProjectUsageWindowPresentation + @Binding var selectedTab: CodexLocalProjectUsageWindowTab + let showsEstimatedCost: Bool + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: CodexLocalProjectUsageWindowLayout.sectionSpacing) { + CodexWorkspaceMetricCards( + presentation: self.presentation, + showsEstimatedCost: self.showsEstimatedCost) + .padding(.horizontal, CodexLocalProjectUsageWindowLayout.detailPadding) + CodexWorkspaceDailyUsageCard(presentation: self.presentation) + CodexWorkspaceTopSessionsCard( + rows: Array(self.presentation.sessions.prefix(4)), + showsEstimatedCost: self.showsEstimatedCost, + viewAll: { self.selectedTab = .sessions }) + .padding(.horizontal, CodexLocalProjectUsageWindowLayout.detailPadding) + if self.presentation.consistency.hasMismatch { + CodexWorkspaceConsistencyNotice(consistency: self.presentation.consistency) + .padding(.horizontal, CodexLocalProjectUsageWindowLayout.detailPadding) + } + } + .padding(.bottom, CodexLocalProjectUsageWindowLayout.detailPadding) + .frame(maxWidth: .infinity, alignment: .leading) + } + } +} + +private struct CodexWorkspaceMetricCards: View { + let presentation: CodexLocalProjectUsageWindowPresentation + let showsEstimatedCost: Bool + + var body: some View { + HStack(spacing: 10) { + CodexWorkspaceMetricCard( + title: L("codex_workspaces_tokens"), + value: self.presentation.displayedTokens.map(UsageFormatter.tokenCountString) ?? "—", + systemImage: "number") + if self.showsEstimatedCost { + CodexWorkspaceMetricCard( + title: L("codex_workspaces_estimated_cost_short"), + value: self.costText, + systemImage: "dollarsign.circle") + } + CodexWorkspaceMetricCard( + title: L("codex_workspaces_sessions"), + value: self.presentation.sessionCount.formatted(), + systemImage: "text.bubble") + CodexWorkspaceMetricCard( + title: L("codex_workspaces_top_model"), + value: self.presentation.topModel ?? "—", + systemImage: "cpu") + } + } + + private var costText: String { + switch self.presentation.costEstimate.coverage { + case .known: + UsageFormatter.currencyString(self.presentation.costEstimate.knownUSD, currencyCode: "USD") + case .partial: + UsageFormatter.currencyString(self.presentation.costEstimate.knownUSD, currencyCode: "USD") + "+" + case .unavailable: + L("codex_workspaces_cost_unavailable") + } + } +} + +private struct CodexWorkspaceMetricCard: View { + let title: String + let value: String + let systemImage: String + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Label(self.title, systemImage: self.systemImage) + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + Text(self.value) + .font(.title3.weight(.semibold).monospacedDigit()) + .lineLimit(1) + .minimumScaleFactor(0.72) + } + .padding(12) + .frame(maxWidth: .infinity, minHeight: 78, alignment: .leading) + .modifier(CodexWorkspaceCardStyle()) + .accessibilityElement(children: .combine) + } +} + +private struct CodexWorkspaceDailyUsageCard: View { + private static let contentPadding: CGFloat = 14 + + let presentation: CodexLocalProjectUsageWindowPresentation + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + self.header + .padding(.horizontal, self.horizontalContentInset) + if self.presentation.daily.isEmpty { + ContentUnavailableView( + L("codex_workspaces_daily_unavailable"), + systemImage: "chart.bar.xaxis", + description: Text(self.emptyDescription)) + .frame(maxWidth: .infinity, minHeight: 138) + .padding(.horizontal, self.horizontalContentInset) + } else { + WorkspaceDailyUsageChart( + points: self.presentation.daily, + isAllWorkspaces: self.presentation.isAllWorkspaces, + contentInset: self.horizontalContentInset, + context: WorkspaceDailyUsageChartContext( + destinationID: self.presentation.selectedDestinationID, + historyDays: self.presentation.snapshot.historyDays, + updatedAt: self.presentation.snapshot.updatedAt)) + } + } + .padding(.vertical, Self.contentPadding) + .background { + RoundedRectangle(cornerRadius: CodexLocalProjectUsageWindowLayout.cardRadius) + .fill(Color.primary.opacity(0.035)) + .padding(.horizontal, CodexLocalProjectUsageWindowLayout.detailPadding) + } + .overlay(alignment: .trailing) { + Color(nsColor: .windowBackgroundColor) + .frame(width: CodexLocalProjectUsageWindowLayout.detailPadding) + .allowsHitTesting(false) + } + .overlay { + RoundedRectangle(cornerRadius: CodexLocalProjectUsageWindowLayout.cardRadius) + .stroke(Color.primary.opacity(0.07), lineWidth: 1) + .padding(.horizontal, CodexLocalProjectUsageWindowLayout.detailPadding) + } + } + + private var header: some View { + HStack { + Text(L("codex_workspaces_daily_token_usage")) + .font(.headline) + Spacer() + Text(L("codex_workspaces_last_days", self.presentation.snapshot.historyDays)) + .font(.caption) + .foregroundStyle(.secondary) + } + } + + private var horizontalContentInset: CGFloat { + CodexLocalProjectUsageWindowLayout.detailPadding + Self.contentPadding + } + + private var emptyDescription: String { + if self.presentation.consistency.dailyUnavailable { + return L("codex_workspaces_daily_unavailable_description", self.presentation.sessionCount) + } + return L("codex_workspaces_no_daily_usage") + } +} + +private struct WorkspaceDailyUsageChartContext: Equatable { + let destinationID: String + let historyDays: Int + let updatedAt: Date +} + +private struct WorkspaceDailyUsageChart: View, @MainActor AXChartDescriptorRepresentable { + private static let calloutWidth: CGFloat = 280 + private static let chartToCalloutSpacing: CGFloat = 12 + private static let trailingScrollSpace = Self.calloutWidth + Self.chartToCalloutSpacing + private static let yAxisWidth: CGFloat = 60 + private static let minimumVisibleDayCount = 30 + private static let preferredDayWidth: CGFloat = 10 + + let points: [WorkspaceDailyPoint] + let isAllWorkspaces: Bool + let contentInset: CGFloat + let context: WorkspaceDailyUsageChartContext + + @State private var rawSelectedDate: Date? + @State private var retainedSelectedDate: Date? + @State private var keyboardSelectedDate: Date? + @State private var pinnedSelectedDate: Date? + @FocusState private var chartFocused: Bool + @Environment(\.locale) private var locale + + var body: some View { + GeometryReader { geometry in + let axisScale = WorkspaceDailyUsageAxisScale(points: self.points) + let visibleDayCount = self.visibleDayCount(for: geometry.size.width) + let contentWidth = self.contentWidth( + for: geometry.size.width, + visibleDayCount: visibleDayCount) + + ScrollView(.horizontal) { + self.chart(axisScale: axisScale) + .frame(width: contentWidth, height: 190) + .background { + WorkspaceHorizontalScrollerHider() + } + } + .contentMargins( + .leading, + self.contentInset + Self.yAxisWidth, + for: .scrollContent) + .contentMargins( + .trailing, + self.contentInset + Self.trailingScrollSpace, + for: .scrollContent) + .defaultScrollAnchor(.trailing) + .scrollIndicators(.hidden) + .scrollClipDisabled() + .overlay(alignment: .trailing) { + Group { + if let selectedPoint = self.selectedPoint { + WorkspaceDailyBreakdownCallout( + point: selectedPoint, + isAllWorkspaces: self.isAllWorkspaces, + isPinned: self.pinnedSelectedDate != nil) + } else { + Color.clear + } + } + .frame(width: Self.calloutWidth, height: 190) + .padding(.trailing, self.contentInset) + } + .overlay(alignment: .leading) { + WorkspaceDailyUsageYAxisLane( + scale: axisScale, + contentInset: self.contentInset, + axisWidth: Self.yAxisWidth) + } + } + .frame(height: 190) + .onHover { isHovering in + if !isHovering, self.pinnedSelectedDate == nil { + self.clearPointerSelection() + } + } + .onChange(of: self.rawSelectedDate) { _, newValue in + guard self.pinnedSelectedDate == nil, + let newValue, + newValue != self.retainedSelectedDate + else { return } + self.retainedSelectedDate = newValue + } + .onChange(of: self.points) { _, newPoints in + guard let selectedPoint = self.selectedPoint, + newPoints.contains(where: { $0.id == selectedPoint.id }) + else { + self.clearSelection() + return + } + } + .onChange(of: self.context) { + self.clearSelection() + } + .onChange(of: self.chartFocused) { _, isFocused in + if !isFocused, self.pinnedSelectedDate == nil { + self.keyboardSelectedDate = nil + } + } + } + + private func chart(axisScale: WorkspaceDailyUsageAxisScale) -> some View { + let selectedPoint = self.selectedPoint + return Chart { + ForEach(self.points) { point in + BarMark( + x: .value(L("codex_workspaces_date"), point.date), + y: .value(L("codex_workspaces_tokens"), point.tokens)) + .foregroundStyle(self.barColor(for: point, selectedPointID: selectedPoint?.id)) + .cornerRadius(1.5) + } + if let selectedPoint { + RuleMark(x: .value(L("codex_workspaces_date"), selectedPoint.date)) + .foregroundStyle(.secondary.opacity(0.7)) + .lineStyle(StrokeStyle(lineWidth: 1)) + } + } + .chartYScale(domain: 0...axisScale.upperBound) + .chartYAxis { + AxisMarks(position: .leading, values: axisScale.tickValues) { _ in + AxisGridLine().foregroundStyle(.quaternary) + } + } + .chartXAxis { + AxisMarks(values: .automatic(desiredCount: 5)) { + AxisValueLabel(format: .dateTime.month(.abbreviated).day()) + } + } + .chartXSelection(value: self.chartSelection) + .chartXScale(range: .plotDimension( + startPadding: 12, + endPadding: 24)) + .chartGesture { proxy in + SpatialTapGesture() + .onEnded { event in + guard let date: Date = proxy.value(atX: event.location.x) else { return } + self.togglePinnedSelection(closestTo: date) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .focusable(interactions: .activate) + .focused(self.$chartFocused) + .onKeyPress(.leftArrow) { + self.moveKeyboardSelection(.earlier) ? .handled : .ignored + } + .onKeyPress(.rightArrow) { + self.moveKeyboardSelection(.later) ? .handled : .ignored + } + .onKeyPress(.return) { + self.toggleKeyboardPinnedSelection() ? .handled : .ignored + } + .onKeyPress(.space) { + self.toggleKeyboardPinnedSelection() ? .handled : .ignored + } + .onKeyPress(.escape) { + guard self.effectiveSelectedDate != nil else { return .ignored } + self.clearSelection() + return .handled + } + .accessibilityLabel(L("codex_workspaces_daily_token_usage")) + .accessibilityValue(self.chartAccessibilityValue) + .accessibilityHint(L("codex_workspaces_chart_keyboard_hint")) + .accessibilityChartDescriptor(self) + .accessibilityAdjustableAction { direction in + switch direction { + case .increment: + _ = self.moveKeyboardSelection(.later) + case .decrement: + _ = self.moveKeyboardSelection(.earlier) + @unknown default: + break + } + } + .accessibilityAction(named: self.pinAccessibilityActionName) { + _ = self.toggleKeyboardPinnedSelection() + } + } + + func makeChartDescriptor() -> AXChartDescriptor { + guard let first = self.points.first, let last = self.points.last else { + return AXChartDescriptor( + title: L("codex_workspaces_daily_token_usage"), + summary: nil, + xAxis: AXNumericDataAxisDescriptor( + title: L("codex_workspaces_date"), + range: 0...1, + gridlinePositions: []) { _ in "" }, + yAxis: AXNumericDataAxisDescriptor( + title: L("codex_workspaces_tokens"), + range: 0...1, + gridlinePositions: []) { UsageFormatter.tokenCountString(Int($0)) }, + additionalAxes: [], + series: []) + } + let maximum = Double(self.points.map(\.tokens).max() ?? 0) + let xAxis = AXNumericDataAxisDescriptor( + title: L("codex_workspaces_date"), + range: first.date.timeIntervalSinceReferenceDate...last.date.timeIntervalSinceReferenceDate, + gridlinePositions: []) + { value in + Date(timeIntervalSinceReferenceDate: value) + .formatted(.dateTime.month(.abbreviated).day().locale(self.locale)) + } + let yAxis = AXNumericDataAxisDescriptor( + title: L("codex_workspaces_tokens"), + range: 0...max(1, maximum), + gridlinePositions: []) { UsageFormatter.tokenCountString(Int($0)) } + let series = AXDataSeriesDescriptor( + name: L("codex_workspaces_daily_token_usage"), + isContinuous: false, + dataPoints: self.points.map { + AXDataPoint(x: $0.date.timeIntervalSinceReferenceDate, y: Double($0.tokens)) + }) + return AXChartDescriptor( + title: L("codex_workspaces_daily_token_usage"), + summary: L("codex_workspaces_last_days", self.points.count), + xAxis: xAxis, + yAxis: yAxis, + additionalAxes: [], + series: [series]) + } + + private var effectiveSelectedDate: Date? { + self.pinnedSelectedDate + ?? self.rawSelectedDate + ?? self.keyboardSelectedDate + ?? self.retainedSelectedDate + } + + private var chartSelection: Binding { + Binding( + get: { self.rawSelectedDate }, + set: { newValue in + guard self.pinnedSelectedDate == nil else { return } + let snappedDate = newValue.flatMap { self.point(closestTo: $0)?.date } + if snappedDate != nil { + self.keyboardSelectedDate = nil + } + guard snappedDate != self.rawSelectedDate else { return } + self.rawSelectedDate = snappedDate + }) + } + + private var selectedPoint: WorkspaceDailyPoint? { + guard let date = self.effectiveSelectedDate else { return nil } + return self.point(closestTo: date) + } + + private var chartAccessibilityValue: String { + guard let selectedPoint = self.selectedPoint else { + return L("codex_workspaces_last_days", self.points.count) + } + let date = selectedPoint.date.formatted( + .dateTime.year().month(.wide).day().locale(self.locale)) + return L( + "codex_workspaces_selected_day_accessibility", + date, + UsageFormatter.tokenCountString(selectedPoint.tokens)) + } + + private var pinAccessibilityActionName: String { + L(self.pinnedSelectedDate == nil + ? "codex_workspaces_pin_selected_day" + : "codex_workspaces_unpin_selected_day") + } + + private func visibleDayCount(for totalWidth: CGFloat) -> Int { + let chartWidth = max( + 0, + totalWidth - Self.yAxisWidth - Self.trailingScrollSpace - (self.contentInset * 2)) + let widthBasedCount = max(Self.minimumVisibleDayCount, Int(chartWidth / Self.preferredDayWidth)) + guard let firstDate = self.points.first?.date, let lastDate = self.points.last?.date else { + return widthBasedCount + } + let calendar = Calendar.current + let rangeDayCount = (calendar.dateComponents( + [.day], + from: calendar.startOfDay(for: firstDate), + to: calendar.startOfDay(for: lastDate)).day ?? 0) + 1 + return min(max(1, rangeDayCount), widthBasedCount) + } + + private func contentWidth(for viewportWidth: CGFloat, visibleDayCount: Int) -> CGFloat { + let availableWidth = max(0, viewportWidth - (self.contentInset * 2)) + let rangeDayCount = self.rangeDayCount + let requiredPlotWidth = CGFloat(max(rangeDayCount, visibleDayCount)) * Self.preferredDayWidth + return max(availableWidth, requiredPlotWidth) + } + + private var rangeDayCount: Int { + guard let firstDate = self.points.first?.date, let lastDate = self.points.last?.date else { return 0 } + let calendar = Calendar.current + return max( + 1, + (calendar.dateComponents( + [.day], + from: calendar.startOfDay(for: firstDate), + to: calendar.startOfDay(for: lastDate)).day ?? 0) + 1) + } + + private func togglePinnedSelection(closestTo date: Date) { + guard let candidate = self.point(closestTo: date)?.date else { return } + self.keyboardSelectedDate = nil + if self.pinnedSelectedDate == candidate { + self.pinnedSelectedDate = nil + } else { + self.pinnedSelectedDate = candidate + self.retainedSelectedDate = candidate + } + } + + private func moveKeyboardSelection(_ direction: WorkspaceDailyUsageSelectionDirection) -> Bool { + let candidate = WorkspaceDailyUsageSelectionNavigator.movingDate( + from: self.effectiveSelectedDate, + direction: direction, + availableDates: self.points.map(\.date)) + guard let candidate else { return false } + + self.rawSelectedDate = nil + if self.pinnedSelectedDate != nil { + self.pinnedSelectedDate = candidate + self.retainedSelectedDate = candidate + } else { + self.retainedSelectedDate = nil + self.keyboardSelectedDate = candidate + } + return true + } + + private func toggleKeyboardPinnedSelection() -> Bool { + guard let candidate = self.selectedPoint?.date ?? self.points.last?.date else { return false } + self.rawSelectedDate = nil + if self.pinnedSelectedDate == candidate { + self.pinnedSelectedDate = nil + self.retainedSelectedDate = nil + self.keyboardSelectedDate = candidate + } else { + self.pinnedSelectedDate = candidate + self.retainedSelectedDate = candidate + self.keyboardSelectedDate = nil + } + return true + } + + private func clearPointerSelection() { + self.rawSelectedDate = nil + self.retainedSelectedDate = nil + } + + private func clearSelection() { + self.rawSelectedDate = nil + self.retainedSelectedDate = nil + self.keyboardSelectedDate = nil + self.pinnedSelectedDate = nil + } + + private func point(closestTo date: Date) -> WorkspaceDailyPoint? { + guard !self.points.isEmpty else { return nil } + + var lowerBound = 0 + var upperBound = self.points.count + while lowerBound < upperBound { + let midpoint = lowerBound + (upperBound - lowerBound) / 2 + if self.points[midpoint].date < date { + lowerBound = midpoint + 1 + } else { + upperBound = midpoint + } + } + + guard lowerBound > 0 else { return self.points[0] } + guard lowerBound < self.points.count else { return self.points[self.points.count - 1] } + + let previous = self.points[lowerBound - 1] + let next = self.points[lowerBound] + return abs(previous.date.timeIntervalSince(date)) <= abs(next.date.timeIntervalSince(date)) + ? previous + : next + } + + private func barColor(for point: WorkspaceDailyPoint, selectedPointID: String?) -> Color { + guard let selectedPointID else { return .orange.opacity(0.78) } + return point.id == selectedPointID ? .orange : .orange.opacity(0.32) + } +} + +private struct WorkspaceHorizontalScrollerHider: NSViewRepresentable { + func makeNSView(context: Context) -> WorkspaceHorizontalScrollerConfigurationView { + WorkspaceHorizontalScrollerConfigurationView() + } + + func updateNSView(_ nsView: WorkspaceHorizontalScrollerConfigurationView, context: Context) { + nsView.scheduleConfiguration() + } +} + +private final class WorkspaceHorizontalScrollerConfigurationView: NSView { + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + self.scheduleConfiguration() + } + + override func viewDidMoveToSuperview() { + super.viewDidMoveToSuperview() + self.scheduleConfiguration() + } + + func scheduleConfiguration() { + DispatchQueue.main.async { [weak self] in + guard let scrollView = self?.enclosingScrollView, + scrollView.hasHorizontalScroller + else { return } + scrollView.hasHorizontalScroller = false + } + } +} + +private struct WorkspaceDailyUsageAxisScale: Equatable { + let upperBound: Int + let tickValues: [Int] + + init(points: [WorkspaceDailyPoint]) { + let maximum = max(0, points.map(\.tokens).max() ?? 0) + guard maximum > 0 else { + self.upperBound = 1 + self.tickValues = [0, 1] + return + } + + let roughStep = Double(maximum) / 3 + let magnitude = pow(10, floor(log10(roughStep))) + let normalizedStep = roughStep / magnitude + let niceMultiplier: Double = if normalizedStep <= 1 { + 1 + } else if normalizedStep <= 2 { + 2 + } else if normalizedStep <= 5 { + 5 + } else { + 10 + } + let step = max(1, Int(niceMultiplier * magnitude)) + self.upperBound = Int(ceil(Double(maximum) / Double(step))) * step + self.tickValues = Array(stride(from: 0, through: self.upperBound, by: step)) + } +} + +private struct WorkspaceDailyUsageYAxisLane: View { + let scale: WorkspaceDailyUsageAxisScale + let contentInset: CGFloat + let axisWidth: CGFloat + + var body: some View { + ZStack(alignment: .leading) { + Color(nsColor: .windowBackgroundColor) + self.cardSurface + .padding(.leading, CodexLocalProjectUsageWindowLayout.detailPadding) + Chart { + PointMark( + x: .value(L("codex_workspaces_date"), 0), + y: .value(L("codex_workspaces_tokens"), 0)) + .foregroundStyle(.clear) + PointMark( + x: .value(L("codex_workspaces_date"), 0), + y: .value(L("codex_workspaces_tokens"), self.scale.upperBound)) + .foregroundStyle(.clear) + } + .chartYScale(domain: 0...self.scale.upperBound) + .chartYAxis { + AxisMarks(position: .leading, values: self.scale.tickValues) { value in + AxisValueLabel { + if let tokens = value.as(Int.self) { + Text(UsageFormatter.tokenCountString(tokens)) + .font(.caption) + .monospacedDigit() + } + } + } + } + .chartXAxis { + AxisMarks(values: [0]) { + AxisValueLabel { + Text(" ") + .font(.caption) + } + } + } + .frame(width: self.axisWidth, height: 190) + .padding(.leading, self.contentInset) + } + .frame(width: self.contentInset + self.axisWidth, height: 190) + .allowsHitTesting(false) + .accessibilityHidden(true) + } + + private var cardSurface: some View { + Color.primary.opacity(0.035) + } +} + +private struct WorkspaceDailyBreakdownCallout: View { + private static let visibleRowCount = 6 + private static let rowHeight: CGFloat = 21 + + let point: WorkspaceDailyPoint + let isAllWorkspaces: Bool + let isPinned: Bool + + var body: some View { + Group { + if #available(macOS 26.0, *) { + self.content + .glassEffect(.clear.interactive(), in: .rect(cornerRadius: 9)) + } else { + self.content + .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 9)) + .overlay { + RoundedRectangle(cornerRadius: 9) + .stroke(.quaternary, lineWidth: 1) + } + .shadow(color: .black.opacity(0.18), radius: 8, y: 3) + } + } + .accessibilityElement(children: .contain) + .accessibilityLabel(self.point.date.formatted(.dateTime.month(.wide).day())) + .accessibilityValue(L( + "codex_workspaces_daily_callout_accessibility", + UsageFormatter.tokenCountString(self.point.tokens), + self.countText)) + } + + private var content: some View { + VStack(alignment: .leading, spacing: 7) { + HStack(spacing: 8) { + if self.isPinned { + Image(systemName: "pin.fill") + .font(.caption2) + .foregroundStyle(.secondary) + .accessibilityHidden(true) + } + Text(self.point.date, format: .dateTime.month(.abbreviated).day()) + .font(.caption.weight(.semibold)) + Spacer(minLength: 8) + Text(UsageFormatter.tokenCountString(self.point.tokens)) + .font(.caption.weight(.semibold).monospacedDigit()) + } + Text(self.countText) + .font(.caption2) + .foregroundStyle(.secondary) + Divider() + self.rows + } + .padding(8) + .frame(width: 280, height: 190, alignment: .topLeading) + } + + @ViewBuilder + private var rows: some View { + if #available(macOS 26.0, *) { + self.rowsScrollView + .scrollEdgeEffectStyle(.soft, for: .top) + .scrollEdgeEffectStyle(.soft, for: .bottom) + } else { + self.rowsScrollView + } + } + + private var rowsScrollView: some View { + ScrollView(.vertical) { + LazyVStack(spacing: 0) { + ForEach(self.point.breakdownRows) { row in + WorkspaceDailyBreakdownCalloutRow(row: row) + .frame(height: Self.rowHeight) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .scrollIndicators(self.point.breakdownRows.count > Self.visibleRowCount ? .visible : .hidden) + .contentMargins(.trailing, self.point.breakdownRows.count > Self.visibleRowCount ? 4 : 0) + .frame(height: self.rowsHeight) + } + + private var countText: String { + if self.isAllWorkspaces { + return L("codex_workspaces_workspace_count", self.point.breakdownRows.count) + } + return L("codex_workspaces_session_count", self.point.breakdownRows.count) + } + + private var rowsHeight: CGFloat { + CGFloat(min(self.point.breakdownRows.count, Self.visibleRowCount)) * Self.rowHeight + } +} + +private struct WorkspaceDailyBreakdownCalloutRow: View { + let row: WorkspaceDailyBreakdownRow + + var body: some View { + HStack(spacing: 10) { + Text(self.row.title) + .font(.caption) + .lineLimit(1) + .truncationMode(.tail) + .help(self.row.title) + Spacer(minLength: 8) + Text(self.row.tokenText) + .font(.caption.monospacedDigit()) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityElement(children: .ignore) + .accessibilityLabel(self.row.title) + .accessibilityValue(self.row.tokenText) + } +} + +private struct CodexWorkspaceTopSessionsCard: View { + let rows: [WorkspaceSessionRow] + let showsEstimatedCost: Bool + let viewAll: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + HStack { + Text(L("codex_workspaces_top_sessions")).font(.headline) + Spacer() + Button(L("codex_workspaces_view_all_sessions"), action: self.viewAll) + .buttonStyle(.link) + } + .padding(.bottom, 10) + + if self.rows.isEmpty { + ContentUnavailableView( + L("codex_workspaces_no_sessions"), + systemImage: "text.bubble") + .frame(maxWidth: .infinity, minHeight: 94) + } else { + CodexWorkspaceTopSessionsHeader(showsEstimatedCost: self.showsEstimatedCost) + Divider() + ForEach(self.rows) { row in + CodexWorkspaceTopSessionRow(row: row, showsEstimatedCost: self.showsEstimatedCost) + Divider() + } + } + } + .padding(14) + .modifier(CodexWorkspaceCardStyle()) + } +} + +private struct CodexWorkspaceTopSessionsHeader: View { + let showsEstimatedCost: Bool + + var body: some View { + HStack(spacing: 12) { + Text(L("codex_workspaces_session")).frame(maxWidth: .infinity, alignment: .leading) + Text(L("codex_workspaces_model")).frame(width: 112, alignment: .leading) + Text(L("codex_workspaces_tokens")).frame(width: 84, alignment: .trailing) + if self.showsEstimatedCost { + Text(L("codex_workspaces_estimated_cost_short")).frame(width: 90, alignment: .trailing) + } + } + .font(.caption.weight(.medium)) + .foregroundStyle(.secondary) + .frame(height: 28) + } +} + +private struct CodexWorkspaceTopSessionRow: View { + let row: WorkspaceSessionRow + let showsEstimatedCost: Bool + + var body: some View { + HStack(spacing: 12) { + VStack(alignment: .leading, spacing: 3) { + Text(self.row.title).font(.callout.weight(.medium)).lineLimit(1) + Text(self.row.startedText + " · " + self.row.path) + .font(.caption2) + .foregroundStyle(.secondary) + .lineLimit(1) + .truncationMode(.middle) + } + .frame(maxWidth: .infinity, alignment: .leading) + Text(self.row.model).frame(width: 112, alignment: .leading).lineLimit(1) + Text(self.row.tokenText).monospacedDigit().frame(width: 84, alignment: .trailing) + if self.showsEstimatedCost { + Text(Self.costText(self.row.costEstimate)) + .monospacedDigit() + .frame(width: 90, alignment: .trailing) + } + } + .font(.callout) + .frame(minHeight: 42) + .accessibilityElement(children: .combine) + } + + static func costText(_ estimate: CodexLocalCostEstimate) -> String { + switch estimate.coverage { + case .known: UsageFormatter.currencyString(estimate.knownUSD, currencyCode: "USD") + case .partial: UsageFormatter.currencyString(estimate.knownUSD, currencyCode: "USD") + "+" + case .unavailable: L("codex_workspaces_cost_unavailable") + } + } +} + +private struct CodexWorkspaceConsistencyNotice: View { + let consistency: WorkspaceUsageConsistency + + var body: some View { + Label(L("codex_workspaces_partial_breakdown_notice"), systemImage: "info.circle") + .font(.caption) + .foregroundStyle(.secondary) + .padding(.horizontal, 12) + .padding(.vertical, 8) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.orange.opacity(0.07), in: RoundedRectangle(cornerRadius: 8)) + .accessibilityHint(L("codex_workspaces_partial_breakdown_help")) + } +} + +struct CodexWorkspaceCardStyle: ViewModifier { + func body(content: Content) -> some View { + content + .background(Color.primary.opacity(0.035)) + .overlay { + RoundedRectangle(cornerRadius: CodexLocalProjectUsageWindowLayout.cardRadius) + .stroke(Color.primary.opacity(0.07), lineWidth: 1) + } + .compositingGroup() + .clipShape(.rect(cornerRadius: CodexLocalProjectUsageWindowLayout.cardRadius)) + } +} diff --git a/Sources/CodexBar/CodexLocalProjectUsageWindowPresentation.swift b/Sources/CodexBar/CodexLocalProjectUsageWindowPresentation.swift new file mode 100644 index 0000000000..8277da1413 --- /dev/null +++ b/Sources/CodexBar/CodexLocalProjectUsageWindowPresentation.swift @@ -0,0 +1,466 @@ +import CodexBarCore +import Foundation + +enum CodexLocalProjectUsageWindowDestination { + static let allWorkspacesID = "__codex_all_workspaces__" +} + +enum CodexWorkspacePrimaryContentState { + case content(CodexLocalProjectUsageSnapshot) + case indexing + case empty + + static func resolve( + snapshot: CodexLocalProjectUsageSnapshot?, + isRefreshing: Bool) -> Self + { + if let snapshot { + return .content(snapshot) + } + return isRefreshing ? .indexing : .empty + } +} + +enum CodexWorkspaceSessionModelFilter { + static func reconciledSelection(_ selectedModel: String, availableModels: [String]) -> String { + selectedModel.isEmpty || availableModels.contains(selectedModel) ? selectedModel : "" + } +} + +enum CodexWorkspaceAssociatedSessionFilter { + static func reconciledSessionIDs( + _ sessionIDs: Set?, + from previousProjectID: String?, + to selectedProjectID: String?) -> Set? + { + previousProjectID == selectedProjectID ? sessionIDs : nil + } +} + +enum CodexWorkspaceSessionRows { + static func filteredAndSorted( + _ rows: [WorkspaceSessionRow], + searchText: String, + selectedModel: String, + sortOrder: [KeyPathComparator]) -> [WorkspaceSessionRow] + { + let query = searchText.trimmingCharacters(in: .whitespacesAndNewlines) + return rows.filter { row in + let matchesModel = selectedModel.isEmpty || row.model == selectedModel + let matchesSearch = query.isEmpty + || row.title.localizedCaseInsensitiveContains(query) + || row.path.localizedCaseInsensitiveContains(query) + || row.model.localizedCaseInsensitiveContains(query) + return matchesModel && matchesSearch + } + .sorted(using: sortOrder) + } +} + +enum WorkspaceDailyUsageSelectionDirection { + case earlier + case later +} + +enum WorkspaceDailyUsageSelectionNavigator { + static func movingDate( + from currentDate: Date?, + direction: WorkspaceDailyUsageSelectionDirection, + availableDates: [Date]) -> Date? + { + guard !availableDates.isEmpty else { return nil } + guard let currentDate else { + switch direction { + case .earlier: + return availableDates[max(availableDates.count - 2, 0)] + case .later: + return availableDates[availableDates.count - 1] + } + } + + let currentIndex = Self.closestIndex(to: currentDate, in: availableDates) + let offset = switch direction { + case .earlier: -1 + case .later: 1 + } + let destinationIndex = min(max(currentIndex + offset, 0), availableDates.count - 1) + return availableDates[destinationIndex] + } + + private static func closestIndex(to date: Date, in dates: [Date]) -> Int { + var lowerBound = 0 + var upperBound = dates.count + while lowerBound < upperBound { + let midpoint = lowerBound + (upperBound - lowerBound) / 2 + if dates[midpoint] < date { + lowerBound = midpoint + 1 + } else { + upperBound = midpoint + } + } + + guard lowerBound > 0 else { return 0 } + guard lowerBound < dates.count else { return dates.count - 1 } + let previousIndex = lowerBound - 1 + return abs(dates[previousIndex].timeIntervalSince(date)) <= abs(dates[lowerBound].timeIntervalSince(date)) + ? previousIndex + : lowerBound + } +} + +enum CodexWorkspaceProjectAccessibilityDescription { + static func make( + projectName: String, + tokenText: String, + severity: CodexLocalUsageSeverity, + estimatedCostText: String?, + session: (count: Int, model: String?)) -> String + { + var components = [ + projectName, + L("codex_workspaces_token_count_accessibility", tokenText), + severity.workspaceAccessibilityDescription, + ] + if let estimatedCostText, !estimatedCostText.isEmpty { + components.append(estimatedCostText) + } + components.append(L("codex_workspaces_session_count", session.count)) + if let model = session.model, !model.isEmpty { + components.append(model) + } + return components.joined(separator: ", ") + } +} + +extension CodexLocalUsageSeverity { + var workspaceAccessibilityDescription: String { + switch self { + case .normal: + L("codex_workspaces_normal_usage") + case .elevated: + L("codex_workspaces_elevated_usage") + case .high: + L("codex_workspaces_high_usage") + } + } +} + +struct CodexLocalProjectUsageWindowPresentation: Equatable { + let snapshot: CodexLocalProjectUsageSnapshot + let projection: CodexLocalProjectUsageProjection + let rankedProjects: [CodexLocalProjectUsage] + let selectedProject: CodexLocalProjectUsage? + let selectedDestinationID: String + let allWorkspaceTokens: Int? + let displayedTokens: Int? + let costEstimate: CodexLocalCostEstimate + let sessions: [WorkspaceSessionRow] + let models: [WorkspaceModelRow] + let daily: [WorkspaceDailyPoint] + let consistency: WorkspaceUsageConsistency + let projectSessionCounts: [String: Int] + let displayedProjectTokens: [String: Int] + + var isAllWorkspaces: Bool { + self.selectedProject == nil + } + + var sessionCount: Int { + self.sessions.count + } + + var topModel: String? { + self.models.first?.model + } + + init( + snapshot: CodexLocalProjectUsageSnapshot, + selectedDestinationID: String?, + projection: CodexLocalProjectUsageProjection) + { + self.snapshot = snapshot + self.projection = projection + self.rankedProjects = projection.rankedProjects(snapshot.projects) + let requestedID = selectedDestinationID ?? CodexLocalProjectUsageWindowDestination.allWorkspacesID + self.selectedProject = requestedID == CodexLocalProjectUsageWindowDestination.allWorkspacesID + ? nil + : self.rankedProjects.first(where: { $0.id == requestedID }) + self.selectedDestinationID = self.selectedProject?.id + ?? CodexLocalProjectUsageWindowDestination.allWorkspacesID + + let sourceSessions = self.selectedProject.map { project in + snapshot.sessions.filter { $0.projectId == project.id } + } ?? snapshot.sessions + self.sessions = sourceSessions.map { + WorkspaceSessionRow(session: $0, projection: projection) + } + .sorted { $0.tokens > $1.tokens } + + let sourceModels = self.selectedProject?.modelBreakdowns ?? snapshot.modelBreakdowns + let displayedModelTokens = sourceModels.map { projection.displayedTokens(for: $0.totals) ?? 0 } + let modelTokenTotal = displayedModelTokens.reduce(0, +) + self.models = zip(sourceModels, displayedModelTokens).map { model, tokens in + WorkspaceModelRow( + breakdown: model, + tokens: tokens, + totalTokens: modelTokenTotal) + } + .sorted { $0.tokens > $1.tokens } + + let sourceDaily = self.selectedProject?.daily ?? snapshot.daily + let dailyBreakdowns = self.selectedProject == nil + ? Self.projectBreakdownsByDay(projects: self.rankedProjects, projection: projection) + : Self.sessionBreakdownsByDay(sessions: sourceSessions, projection: projection) + self.daily = sourceDaily.map { + WorkspaceDailyPoint( + point: $0, + tokens: projection.displayedTokens( + totalTokens: $0.totalTokens, + cachedInputTokens: $0.cachedInputTokens), + breakdownRows: dailyBreakdowns[$0.day] ?? []) + } + + let totals = self.selectedProject?.totals ?? snapshot.total + self.allWorkspaceTokens = projection.displayedTokens(for: snapshot.total) + self.displayedTokens = projection.displayedTokens(for: totals) + self.costEstimate = self.selectedProject?.costEstimate + ?? Self.aggregateCost(snapshot.projects) + self.projectSessionCounts = Dictionary(grouping: snapshot.sessions, by: \CodexLocalSessionUsage.projectId) + .mapValues { $0.count } + self.displayedProjectTokens = Dictionary(uniqueKeysWithValues: self.rankedProjects.map { project in + (project.id, projection.displayedTokens(for: project.totals) ?? 0) + }) + self.consistency = WorkspaceUsageConsistency( + expectedTokens: self.displayedTokens, + dailyTokens: self.daily.reduce(0) { $0 + $1.tokens }, + modelTokens: self.models.reduce(0) { $0 + $1.tokens }, + expectedSessionCount: self.selectedProject?.sessionCount ?? snapshot.sessions.count, + visibleSessionCount: self.sessions.count, + hasDailyData: !sourceDaily.isEmpty, + hasModelData: !sourceModels.isEmpty, + dailyBreakdownMismatch: self.daily.contains(where: { !$0.breakdownIsComplete })) + } + + private static func aggregateCost(_ projects: [CodexLocalProjectUsage]) -> CodexLocalCostEstimate { + projects.reduce(into: CodexLocalCostEstimate()) { result, project in + result = CodexLocalCostEstimate( + knownUSD: result.knownUSD + project.costEstimate.knownUSD, + unknownTokens: result.unknownTokens + project.costEstimate.unknownTokens) + } + } + + private static func projectBreakdownsByDay( + projects: [CodexLocalProjectUsage], + projection: CodexLocalProjectUsageProjection) -> [String: [WorkspaceDailyBreakdownRow]] + { + var result: [String: [WorkspaceDailyBreakdownRow]] = [:] + for project in projects { + for point in project.daily { + let tokens = projection.displayedTokens( + totalTokens: point.totalTokens, + cachedInputTokens: point.cachedInputTokens) + guard tokens > 0 else { continue } + result[point.day, default: []].append(WorkspaceDailyBreakdownRow( + id: project.id, + kind: .project, + title: project.displayName, + tokens: tokens)) + } + } + return result.mapValues(Self.sortedBreakdownRows) + } + + private static func sessionBreakdownsByDay( + sessions: [CodexLocalSessionUsage], + projection: CodexLocalProjectUsageProjection) -> [String: [WorkspaceDailyBreakdownRow]] + { + var result: [String: [WorkspaceDailyBreakdownRow]] = [:] + for session in sessions { + let title = session.displayTitle == CodexLocalSessionUsage.localChatFallbackTitle + ? L("codex_workspaces_local_codex_chat") + : session.displayTitle + for point in session.daily { + let tokens = projection.displayedTokens( + totalTokens: point.totalTokens, + cachedInputTokens: point.cachedInputTokens) + guard tokens > 0 else { continue } + result[point.day, default: []].append(WorkspaceDailyBreakdownRow( + id: session.id, + kind: .session, + title: title, + tokens: tokens)) + } + } + return result.mapValues(Self.sortedBreakdownRows) + } + + private static func sortedBreakdownRows( + _ rows: [WorkspaceDailyBreakdownRow]) -> [WorkspaceDailyBreakdownRow] + { + rows.sorted { lhs, rhs in + if lhs.tokens != rhs.tokens { return lhs.tokens > rhs.tokens } + let titleOrder = lhs.title.localizedStandardCompare(rhs.title) + if titleOrder != .orderedSame { return titleOrder == .orderedAscending } + return lhs.id < rhs.id + } + } +} + +struct WorkspaceUsageConsistency: Equatable { + let expectedTokens: Int? + let dailyTokenDelta: Int? + let modelTokenDelta: Int? + let sessionCountDelta: Int + let dailyUnavailable: Bool + let modelsUnavailable: Bool + let dailyBreakdownMismatch: Bool + + init( + expectedTokens: Int?, + dailyTokens: Int, + modelTokens: Int, + expectedSessionCount: Int, + visibleSessionCount: Int, + hasDailyData: Bool, + hasModelData: Bool, + dailyBreakdownMismatch: Bool) + { + self.expectedTokens = expectedTokens + self.dailyTokenDelta = hasDailyData ? expectedTokens.map { dailyTokens - $0 } : nil + self.modelTokenDelta = hasModelData ? expectedTokens.map { modelTokens - $0 } : nil + self.sessionCountDelta = visibleSessionCount - expectedSessionCount + self.dailyUnavailable = !hasDailyData && (expectedTokens ?? 0) > 0 + self.modelsUnavailable = !hasModelData && (expectedTokens ?? 0) > 0 + self.dailyBreakdownMismatch = dailyBreakdownMismatch + } + + var hasMismatch: Bool { + self.dailyTokenDelta.map { $0 != 0 } == true + || self.modelTokenDelta.map { $0 != 0 } == true + || self.sessionCountDelta != 0 + || self.dailyBreakdownMismatch + } +} + +struct WorkspaceSessionRow: Identifiable, Equatable { + let id: String + let title: String + let path: String + let startedAt: Date? + let model: String + let tokens: Int + let costEstimate: CodexLocalCostEstimate + + init(session: CodexLocalSessionUsage, projection: CodexLocalProjectUsageProjection) { + self.id = session.id + self.title = session.displayTitle == CodexLocalSessionUsage.localChatFallbackTitle + ? L("codex_workspaces_local_codex_chat") + : session.displayTitle + self.path = session.cwd ?? "—" + self.startedAt = session.startedAt ?? session.latestActivity + self.model = session.topModel ?? "—" + self.tokens = projection.displayedTokens(for: session.totals) ?? 0 + self.costEstimate = session.costEstimate + } + + var startedSortValue: Date { + self.startedAt ?? .distantPast + } + + var costSortValue: Double { + self.costEstimate.knownUSD + } + + var startedText: String { + self.startedAt?.formatted(.dateTime.month(.abbreviated).day().hour().minute()) ?? "—" + } + + var tokenText: String { + UsageFormatter.tokenCountString(self.tokens) + } +} + +struct WorkspaceModelRow: Identifiable, Equatable { + let id: String + let model: String + let tokens: Int + let percentage: Double + let costEstimate: CodexLocalCostEstimate + + init(breakdown: CodexLocalUsageModelBreakdown, tokens: Int, totalTokens: Int) { + self.id = breakdown.id + self.model = breakdown.model + self.tokens = tokens + self.percentage = totalTokens == 0 ? 0 : Double(tokens) / Double(totalTokens) + self.costEstimate = breakdown.costEstimate + } + + var costSortValue: Double { + self.costEstimate.knownUSD + } + + var tokenText: String { + UsageFormatter.tokenCountString(self.tokens) + } + + var percentText: String { + self.percentage.formatted(.percent.precision(.fractionLength(1))) + } +} + +struct WorkspaceDailyPoint: Identifiable, Equatable { + let id: String + let day: String + let date: Date + let tokens: Int + let estimatedCostUSD: Double? + let breakdownRows: [WorkspaceDailyBreakdownRow] + + init( + point: CodexLocalUsageDailyPoint, + tokens: Int, + breakdownRows: [WorkspaceDailyBreakdownRow] = [], + calendar: Calendar = .current) + { + self.id = point.id + self.day = point.day + self.date = Self.dateFromDayKey(point.day, calendar: calendar) + self.tokens = tokens + self.estimatedCostUSD = point.estimatedCostUSD + self.breakdownRows = breakdownRows + } + + var breakdownIsComplete: Bool { + self.breakdownRows.reduce(0) { $0 + $1.tokens } == self.tokens + } + + static func dateFromDayKey(_ day: String, calendar: Calendar = .current) -> Date { + let values = day.split(separator: "-").compactMap { Int($0) } + guard values.count == 3 else { return .distantPast } + var localCalendar = Calendar(identifier: .gregorian) + localCalendar.timeZone = calendar.timeZone + var components = DateComponents() + components.calendar = localCalendar + components.timeZone = localCalendar.timeZone + components.year = values[0] + components.month = values[1] + components.day = values[2] + return components.date ?? .distantPast + } +} + +struct WorkspaceDailyBreakdownRow: Identifiable, Equatable { + enum Kind: Equatable { + case project + case session + } + + let id: String + let kind: Kind + let title: String + let tokens: Int + + var tokenText: String { + UsageFormatter.tokenCountString(self.tokens) + } +} diff --git a/Sources/CodexBar/CodexLocalProjectUsageWindowSidebar.swift b/Sources/CodexBar/CodexLocalProjectUsageWindowSidebar.swift new file mode 100644 index 0000000000..0d0a24a502 --- /dev/null +++ b/Sources/CodexBar/CodexLocalProjectUsageWindowSidebar.swift @@ -0,0 +1,355 @@ +import AppKit +import CodexBarCore +import SwiftUI + +struct CodexWorkspaceSidebar: View { + let presentation: CodexLocalProjectUsageWindowPresentation + let showsEstimatedCost: Bool + @Binding var selectedDestinationID: String? + @FocusState private var keyboardFocus: Bool + + var body: some View { + let maximumTokens = max(self.presentation.displayedProjectTokens.values.max() ?? 0, 1) + let allWorkspacesID = CodexLocalProjectUsageWindowDestination.allWorkspacesID + let orderedDestinationIDs = [allWorkspacesID] + self.presentation.rankedProjects.map(\.id) + + ScrollViewReader { proxy in + ScrollView(.vertical, showsIndicators: false) { + LazyVStack(alignment: .leading, spacing: 0) { + Button { + self.selectedDestinationID = allWorkspacesID + self.keyboardFocus = true + } label: { + CodexAllWorkspacesSidebarRow( + tokens: self.presentation.allWorkspaceTokens, + projectCount: self.presentation.rankedProjects.count, + selected: self.presentation.isAllWorkspaces) + } + .buttonStyle(.plain) + .frame(maxWidth: .infinity, alignment: .leading) + .id(allWorkspacesID) + .accessibilityAddTraits(self.presentation.isAllWorkspaces ? .isSelected : []) + + ForEach(self.presentation.rankedProjects) { project in + Button { + self.selectedDestinationID = project.id + self.keyboardFocus = true + } label: { + CodexWorkspaceSidebarProjectRow( + project: project, + tokens: self.presentation.displayedProjectTokens[project.id] ?? 0, + sessionCount: self.presentation.projectSessionCounts[project.id] ?? project + .sessionCount, + maximumTokens: maximumTokens, + showsEstimatedCost: self.showsEstimatedCost, + selected: self.presentation.selectedProject?.id == project.id) + } + .buttonStyle(.plain) + .frame(maxWidth: .infinity, alignment: .leading) + .id(project.id) + .accessibilityAddTraits( + self.presentation.selectedProject?.id == project.id ? .isSelected : []) + .contextMenu { + if let path = project.path { + Button(L("codex_workspaces_copy_path")) { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(path, forType: .string) + } + Button(L("codex_workspaces_reveal_in_finder")) { + NSWorkspace.shared.selectFile(path, inFileViewerRootedAtPath: "") + } + } + } + } + } + } + .focusable() + .focusEffectDisabled() + .focused(self.$keyboardFocus) + .contentMargins(.horizontal, 7, for: .scrollContent) + .contentMargins(.vertical, 8, for: .scrollContent) + .onMoveCommand { direction in + let selectionDirection: CodexWorkspaceSidebarSelectionDirection? = switch direction { + case .up: + .up + case .down: + .down + default: + nil + } + guard let selectionDirection, + let nextID = CodexWorkspaceSidebarSelectionNavigator.movingSelection( + from: self.selectedDestinationID, + direction: selectionDirection, + orderedIDs: orderedDestinationIDs) + else { return } + self.selectedDestinationID = nextID + self.keyboardFocus = true + proxy.scrollTo(nextID, anchor: .center) + } + .onAppear { + self.selectedDestinationID = self.presentation.selectedDestinationID + } + .onChange(of: self.presentation.selectedDestinationID) { _, normalizedID in + guard self.selectedDestinationID != normalizedID else { return } + self.selectedDestinationID = normalizedID + } + } + .background { + if #unavailable(macOS 26.0) { + CodexWorkspaceSidebarMaterial().ignoresSafeArea() + } + } + .accessibilityElement(children: .contain) + .accessibilityLabel(L("codex_workspaces_title")) + } +} + +@MainActor +private struct CodexWorkspaceSidebarMaterial: NSViewRepresentable { + func makeNSView(context: Context) -> NSVisualEffectView { + let view = NSVisualEffectView() + self.configure(view) + return view + } + + func updateNSView(_ nsView: NSVisualEffectView, context: Context) { + self.configure(nsView) + } + + private func configure(_ view: NSVisualEffectView) { + view.material = .sidebar + view.blendingMode = .behindWindow + view.state = .followsWindowActiveState + } +} + +enum CodexWorkspaceSidebarSelectionDirection { + case up + case down +} + +enum CodexWorkspaceSidebarSelectionNavigator { + static func movingSelection( + from currentID: String?, + direction: CodexWorkspaceSidebarSelectionDirection, + orderedIDs: [String]) -> String? + { + guard !orderedIDs.isEmpty else { return nil } + guard let currentID, let currentIndex = orderedIDs.firstIndex(of: currentID) else { + return orderedIDs[0] + } + + let offset = direction == .up ? -1 : 1 + let nextIndex = min(max(currentIndex + offset, 0), orderedIDs.count - 1) + return orderedIDs[nextIndex] + } +} + +private struct CodexAllWorkspacesSidebarRow: View { + let tokens: Int? + let projectCount: Int + let selected: Bool + + var body: some View { + CodexWorkspaceSelectionSurface(selected: self.selected) { + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 8) { + Image(systemName: "square.grid.2x2") + .foregroundStyle(self.selected ? Color.accentColor : Color.secondary) + Text(L("codex_workspaces_all_workspaces")) + .font(.callout.weight(.semibold)) + .lineLimit(1) + Spacer(minLength: 6) + Text(self.tokens.map(UsageFormatter.tokenCountString) ?? "—") + .font(.caption.weight(.semibold).monospacedDigit()) + } + Text(L("codex_workspaces_workspace_count", self.projectCount)) + .font(.caption2) + .foregroundStyle(.secondary) + } + } + .accessibilityElement(children: .combine) + } +} + +private struct CodexWorkspaceSidebarProjectRow: View { + let project: CodexLocalProjectUsage + let tokens: Int + let sessionCount: Int + let maximumTokens: Int + let showsEstimatedCost: Bool + let selected: Bool + + var body: some View { + CodexWorkspaceSelectionSurface(selected: self.selected) { + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 7) { + self.statusIcon + Text(self.project.displayName) + .font(.callout.weight(.semibold)) + .lineLimit(1) + if self.project.severity == .high { + Image(systemName: "exclamationmark.triangle.fill") + .font(.system(size: 8, weight: .semibold)) + .foregroundStyle(.red) + } + Spacer(minLength: 6) + Text(UsageFormatter.tokenCountString(self.tokens)) + .font(.caption.weight(.semibold).monospacedDigit()) + .foregroundStyle(self.project.severity == .high ? Color.red : Color.primary) + } + Text(self.displayPath) + .font(.caption2) + .foregroundStyle(.secondary) + .lineLimit(1) + .truncationMode(.middle) + .padding(.top, 3) + GeometryReader { geometry in + Capsule() + .fill(.quaternary) + .overlay(alignment: .leading) { + Capsule() + .fill(Color.accentColor.opacity(self.selected ? 0.9 : 0.52)) + .frame(width: geometry.size.width * self.relativeUsage) + } + } + .frame(height: 3) + .padding(.top, 7) + HStack(spacing: 6) { + if self.showsEstimatedCost { + Text(self.costText) + } + Text(L("codex_workspaces_session_count", self.sessionCount)) + if let model = self.project.topModel { + Text(model).lineLimit(1) + } + } + .font(.caption2) + .foregroundStyle(.secondary) + .padding(.top, 5) + } + } + .accessibilityElement(children: .ignore) + .accessibilityLabel(self.accessibilityDescription) + .accessibilityValue(L("codex_workspaces_relative_usage_accessibility", Int(self.relativeUsage * 100))) + .help(L("codex_workspaces_relative_usage_tooltip", Int(self.relativeUsage * 100))) + } + + @ViewBuilder + private var statusIcon: some View { + switch self.project.severity { + case .high: + Circle().fill(.red).frame(width: 7, height: 7) + case .elevated: + Image(systemName: "arrow.up.circle.fill") + .font(.system(size: 7, weight: .semibold)) + .foregroundStyle(.orange) + .frame(width: 7, height: 7) + case .normal: + Circle().fill(.green).frame(width: 7, height: 7) + } + } + + private var relativeUsage: CGFloat { + min(max(CGFloat(self.tokens) / CGFloat(max(self.maximumTokens, 1)), 0), 1) + } + + private var displayPath: String { + guard let path = self.project.path else { return L("codex_workspaces_chats_description") } + let home = FileManager.default.homeDirectoryForCurrentUser.path + return path.hasPrefix(home) ? "~" + path.dropFirst(home.count) : path + } + + private var costText: String { + switch self.project.costEstimate.coverage { + case .known: + UsageFormatter.currencyString(self.project.costEstimate.knownUSD, currencyCode: "USD") + case .partial: + UsageFormatter.currencyString(self.project.costEstimate.knownUSD, currencyCode: "USD") + "+" + case .unavailable: + L("codex_workspaces_cost_unavailable") + } + } + + private var accessibilityDescription: String { + CodexWorkspaceProjectAccessibilityDescription.make( + projectName: self.project.displayName, + tokenText: UsageFormatter.tokenCountString(self.tokens), + severity: self.project.severity, + estimatedCostText: self.showsEstimatedCost + ? L("codex_workspaces_estimated_cost_value", self.costText) + : nil, + session: (self.sessionCount, self.project.topModel)) + } +} + +private struct CodexWorkspaceSelectionSurface: View { + let selected: Bool + @ViewBuilder let content: Content + @Environment(\.controlActiveState) private var controlActiveState + + var body: some View { + self.content + .padding(.horizontal, 9) + .padding(.vertical, 7) + .frame(maxWidth: .infinity, minHeight: 70, alignment: .leading) + .background { + RoundedRectangle(cornerRadius: 6, style: .continuous) + .fill(self.selected ? Color.accentColor.opacity(self.selectionOpacity) : Color.clear) + .overlay { + RoundedRectangle(cornerRadius: 6, style: .continuous) + .strokeBorder( + self.selected ? Color.accentColor.opacity(0.28) : Color.clear, + lineWidth: 1) + } + } + .contentShape(.rect(cornerRadius: 6)) + } + + private var selectionOpacity: Double { + self.controlActiveState == .key ? 0.14 : 0.08 + } +} + +struct CodexWorkspaceStatusBar: View { + let store: UsageStore + + var body: some View { + if #available(macOS 26.0, *) { + self.content + } else { + self.content.background(.bar) + } + } + + private var content: some View { + HStack(spacing: 7) { + if self.store.codexLocalProjectUsageRefreshInFlight { + ProgressView().controlSize(.mini) + Text(self.store.codexLocalProjectUsageProgressSubtitle ?? L("codex_workspaces_indexing_local_logs")) + } else if let error = self.store.codexLocalProjectUsageError { + Image(systemName: "exclamationmark.triangle.fill").foregroundStyle(.red) + Text(error).lineLimit(1) + } else if let snapshot = self.store.codexLocalProjectUsageSnapshot { + Image(systemName: snapshot.sourceStatus.isPartial + ? "exclamationmark.triangle.fill" : "checkmark.circle.fill") + .foregroundStyle(snapshot.sourceStatus.isPartial ? Color.orange : Color.green) + Text(L( + "codex_workspaces_indexed_files", + snapshot.updatedAt.formatted(.relative(presentation: .named)), + snapshot.indexedFileCount)) + .lineLimit(1) + } else { + Image(systemName: "minus.circle").foregroundStyle(.secondary) + Text(L("codex_workspaces_no_local_usage")) + } + Spacer() + Text(L("codex_workspaces_estimated_local_logs")) + } + .font(.caption) + .foregroundStyle(.secondary) + .padding(.horizontal, 12) + .accessibilityElement(children: .combine) + } +} diff --git a/Sources/CodexBar/CodexLocalProjectUsageWindowTables.swift b/Sources/CodexBar/CodexLocalProjectUsageWindowTables.swift new file mode 100644 index 0000000000..35e9b950cc --- /dev/null +++ b/Sources/CodexBar/CodexLocalProjectUsageWindowTables.swift @@ -0,0 +1,334 @@ +import CodexBarCore +import SwiftUI + +struct CodexWorkspaceSessionsTable: View { + let rows: [WorkspaceSessionRow] + let showsEstimatedCost: Bool + @Binding var associatedSessionIDs: Set? + + @State private var searchText = "" + @State private var selectedModel = "" + @State private var showsSearch = false + @State private var selectedSessionID: WorkspaceSessionRow.ID? + @State private var sortOrder = [KeyPathComparator(\WorkspaceSessionRow.tokens, order: .reverse)] + @FocusState private var searchFocused: Bool + + var body: some View { + let filteredRows = self.associatedSessionIDs.map { ids in self.rows.filter { ids.contains($0.id) } } + ?? self.rows + let displayedRows = CodexWorkspaceSessionRows.filteredAndSorted( + filteredRows, + searchText: self.searchText, + selectedModel: self.selectedModel, + sortOrder: self.sortOrder) + + VStack(spacing: 0) { + self.filterBar(rowCount: displayedRows.count) + Divider() + if displayedRows.isEmpty { + ContentUnavailableView.search(text: self.searchText) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else if self.showsEstimatedCost { + self.tableWithCost(displayedRows) + } else { + self.tableWithoutCost(displayedRows) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .padding(.horizontal, CodexLocalProjectUsageWindowLayout.detailPadding) + .padding(.bottom, CodexLocalProjectUsageWindowLayout.detailPadding) + .background { + Button(L("codex_workspaces_search_sessions")) { + self.showsSearch = true + self.searchFocused = true + } + .keyboardShortcut("f", modifiers: .command) + .frame(width: 0, height: 0) + .opacity(0) + .accessibilityHidden(true) + } + .onChange(of: self.availableModels) { _, availableModels in + let reconciledSelection = CodexWorkspaceSessionModelFilter.reconciledSelection( + self.selectedModel, + availableModels: availableModels) + if reconciledSelection != self.selectedModel { + self.selectedModel = reconciledSelection + } + } + .onChange(of: self.associatedSessionIDs) { _, sessionIDs in + if sessionIDs != nil { self.selectedModel = "" } + } + } + + private func filterBar(rowCount: Int) -> some View { + HStack(spacing: 8) { + if self.associatedSessionIDs != nil { + HStack(spacing: 5) { + Label( + L("codex_workspaces_associated_with_selected_model"), + systemImage: "line.3.horizontal.decrease.circle.fill") + Button(L("Clear")) { self.associatedSessionIDs = nil }.buttonStyle(.link) + } + .font(.caption) + .foregroundStyle(.secondary) + } + if self.showsSearch { + TextField(L("codex_workspaces_search_sessions"), text: self.$searchText) + .textFieldStyle(.roundedBorder) + .focused(self.$searchFocused) + .frame(width: 240) + Button { + self.searchText = "" + self.showsSearch = false + } label: { + Image(systemName: "xmark.circle.fill") + } + .buttonStyle(.borderless) + .help(L("Clear")) + } else { + Button { + self.showsSearch = true + self.searchFocused = true + } label: { + Label(L("codex_workspaces_search_sessions"), systemImage: "magnifyingglass") + } + .buttonStyle(.bordered) + .controlSize(.small) + } + + Menu { + Button(L("codex_workspaces_all_models")) { self.selectedModel = "" } + Divider() + ForEach(self.availableModels, id: \.self) { model in + Button { + self.selectedModel = model + } label: { + if model == self.selectedModel { + Label(model, systemImage: "checkmark") + } else { + Text(model) + } + } + } + } label: { + Label( + self.selectedModel.isEmpty ? L("codex_workspaces_all_models") : self.selectedModel, + systemImage: "line.3.horizontal.decrease.circle") + } + .menuStyle(.borderlessButton) + .controlSize(.small) + + Spacer() + Text(L("codex_workspaces_session_count", rowCount)) + .font(.caption) + .foregroundStyle(.secondary) + } + .frame(height: 38) + } + + private func tableWithCost(_ rows: [WorkspaceSessionRow]) -> some View { + Table(rows, selection: self.$selectedSessionID, sortOrder: self.$sortOrder) { + TableColumn(L("codex_workspaces_session"), value: \WorkspaceSessionRow.title) { row in + self.sessionCell(row) + } + .width(min: 260, ideal: 390) + TableColumn(L("codex_workspaces_started"), value: \WorkspaceSessionRow.startedSortValue) { row in + Text(row.startedText).lineLimit(1) + } + .width(min: 118, ideal: 138, max: 156) + TableColumn(L("codex_workspaces_model"), value: \WorkspaceSessionRow.model) { row in + Text(row.model).lineLimit(1) + } + .width(min: 96, ideal: 122, max: 150) + TableColumn(L("codex_workspaces_tokens"), value: \WorkspaceSessionRow.tokens) { row in + Text(row.tokenText).monospacedDigit().frame(maxWidth: .infinity, alignment: .trailing) + } + .width(min: 82, ideal: 94, max: 112) + TableColumn(L("codex_workspaces_estimated_cost_short"), value: \WorkspaceSessionRow.costSortValue) { row in + Text(Self.costText(row.costEstimate)) + .monospacedDigit() + .frame(maxWidth: .infinity, alignment: .trailing) + } + .width(min: 86, ideal: 100, max: 118) + } + .tableStyle(.inset) + } + + private func tableWithoutCost(_ rows: [WorkspaceSessionRow]) -> some View { + Table(rows, selection: self.$selectedSessionID, sortOrder: self.$sortOrder) { + TableColumn(L("codex_workspaces_session"), value: \WorkspaceSessionRow.title) { row in + self.sessionCell(row) + } + .width(min: 300, ideal: 450) + TableColumn(L("codex_workspaces_started"), value: \WorkspaceSessionRow.startedSortValue) { row in + Text(row.startedText).lineLimit(1) + } + .width(min: 118, ideal: 138, max: 156) + TableColumn(L("codex_workspaces_model"), value: \WorkspaceSessionRow.model) { row in + Text(row.model).lineLimit(1) + } + .width(min: 96, ideal: 122, max: 150) + TableColumn(L("codex_workspaces_tokens"), value: \WorkspaceSessionRow.tokens) { row in + Text(row.tokenText).monospacedDigit().frame(maxWidth: .infinity, alignment: .trailing) + } + .width(min: 82, ideal: 94, max: 112) + } + .tableStyle(.inset) + } + + private func sessionCell(_ row: WorkspaceSessionRow) -> some View { + VStack(alignment: .leading, spacing: 3) { + Text(row.title).font(.callout.weight(.medium)).lineLimit(1) + Text(row.path) + .font(.caption2) + .foregroundStyle(.secondary) + .lineLimit(1) + .truncationMode(.middle) + .help(row.path) + } + .padding(.vertical, 3) + .accessibilityElement(children: .combine) + } + + private var availableModels: [String] { + Array(Set(self.rows.map(\.model).filter { $0 != "—" })).sorted() + } + + static func costText(_ estimate: CodexLocalCostEstimate) -> String { + switch estimate.coverage { + case .known: UsageFormatter.currencyString(estimate.knownUSD, currencyCode: "USD") + case .partial: UsageFormatter.currencyString(estimate.knownUSD, currencyCode: "USD") + "+" + case .unavailable: L("codex_workspaces_cost_unavailable") + } + } +} + +struct CodexWorkspaceModelsTable: View { + let rows: [WorkspaceModelRow] + let totalTokens: Int + let totalCost: CodexLocalCostEstimate + let showsEstimatedCost: Bool + + @State private var sortOrder = [KeyPathComparator(\WorkspaceModelRow.tokens, order: .reverse)] + @State private var selectedModelID: WorkspaceModelRow.ID? + + var body: some View { + VStack(spacing: 0) { + if self.rows.isEmpty { + ContentUnavailableView( + L("codex_workspaces_no_models"), + systemImage: "cpu") + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + if self.showsEstimatedCost { + self.tableWithCost + } else { + self.tableWithoutCost + } + Divider() + CodexWorkspaceModelTotalRow( + totalTokens: self.totalTokens, + totalCost: self.totalCost, + showsEstimatedCost: self.showsEstimatedCost) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .padding(.horizontal, CodexLocalProjectUsageWindowLayout.detailPadding) + .padding(.bottom, CodexLocalProjectUsageWindowLayout.detailPadding) + .modifier(CodexWorkspaceCardStyle()) + } + + private var sortedRows: [WorkspaceModelRow] { + self.rows.sorted(using: self.sortOrder) + } + + private var tableWithCost: some View { + Table(self.sortedRows, selection: self.$selectedModelID, sortOrder: self.$sortOrder) { + TableColumn(L("codex_workspaces_model"), value: \WorkspaceModelRow.model) { row in + self.modelCell(row) + } + .width(min: 260, ideal: 420) + TableColumn(L("codex_workspaces_tokens"), value: \WorkspaceModelRow.tokens) { row in + Text(row.tokenText).monospacedDigit().frame(maxWidth: .infinity, alignment: .trailing) + } + .width(min: 108, ideal: 130) + TableColumn(L("codex_workspaces_percent_total"), value: \WorkspaceModelRow.percentage) { row in + Text(row.percentText).monospacedDigit().frame(maxWidth: .infinity, alignment: .trailing) + } + .width(min: 108, ideal: 126) + TableColumn(L("codex_workspaces_estimated_cost_short"), value: \WorkspaceModelRow.costSortValue) { row in + Text(CodexWorkspaceSessionsTable.costText(row.costEstimate)) + .monospacedDigit() + .frame(maxWidth: .infinity, alignment: .trailing) + } + .width(min: 108, ideal: 132) + } + .tableStyle(.inset) + } + + private var tableWithoutCost: some View { + Table(self.sortedRows, selection: self.$selectedModelID, sortOrder: self.$sortOrder) { + TableColumn(L("codex_workspaces_model"), value: \WorkspaceModelRow.model) { row in + self.modelCell(row) + } + .width(min: 320, ideal: 520) + TableColumn(L("codex_workspaces_tokens"), value: \WorkspaceModelRow.tokens) { row in + Text(row.tokenText).monospacedDigit().frame(maxWidth: .infinity, alignment: .trailing) + } + .width(min: 108, ideal: 130) + TableColumn(L("codex_workspaces_percent_total"), value: \WorkspaceModelRow.percentage) { row in + Text(row.percentText).monospacedDigit().frame(maxWidth: .infinity, alignment: .trailing) + } + .width(min: 108, ideal: 126) + } + .tableStyle(.inset) + } + + private func modelCell(_ row: WorkspaceModelRow) -> some View { + HStack(spacing: 10) { + Text(row.model).font(.callout.weight(.medium)).lineLimit(1) + GeometryReader { geometry in + Capsule() + .fill(.quaternary) + .overlay(alignment: .leading) { + Capsule() + .fill(Color.accentColor.opacity(0.7)) + .frame(width: geometry.size.width * row.percentage) + } + } + .frame(height: 3) + } + .padding(.vertical, 4) + .accessibilityElement(children: .combine) + .accessibilityValue(row.percentText) + } +} + +private struct CodexWorkspaceModelTotalRow: View { + let totalTokens: Int + let totalCost: CodexLocalCostEstimate + let showsEstimatedCost: Bool + + var body: some View { + HStack(spacing: 16) { + Text(L("codex_workspaces_total")).fontWeight(.semibold) + .frame(maxWidth: .infinity, alignment: .leading) + Text(UsageFormatter.tokenCountString(self.totalTokens)) + .monospacedDigit() + .frame(width: 130, alignment: .trailing) + Text(1, format: .percent.precision(.fractionLength(0))) + .monospacedDigit() + .frame(width: 126, alignment: .trailing) + if self.showsEstimatedCost { + Text(CodexWorkspaceSessionsTable.costText(self.totalCost)) + .monospacedDigit() + .frame(width: 132, alignment: .trailing) + } + } + .font(.callout) + .padding(.horizontal, 12) + .frame(height: 40) + .background(.bar) + .accessibilityElement(children: .combine) + } +} diff --git a/Sources/CodexBar/CodexLocalProjectUsageWindowView.swift b/Sources/CodexBar/CodexLocalProjectUsageWindowView.swift new file mode 100644 index 0000000000..03946a76d2 --- /dev/null +++ b/Sources/CodexBar/CodexLocalProjectUsageWindowView.swift @@ -0,0 +1,410 @@ +import AppKit +import CodexBarCore +import SwiftUI + +enum CodexLocalProjectUsageWindowLayout { + static let idealSize = NSSize(width: 1380, height: 780) + static let minimumSize = NSSize(width: 980, height: 640) + static let sidebarMinimumWidth: CGFloat = 236 + static let sidebarIdealWidth: CGFloat = 278 + static let sidebarMaximumWidth: CGFloat = 336 + static let commandHeight: CGFloat = 44 + static let footerHeight: CGFloat = 18 + static let detailPadding: CGFloat = 20 + static let sectionSpacing: CGFloat = 16 + static let cardRadius: CGFloat = 11 +} + +enum CodexLocalProjectUsageWindowTab: String, CaseIterable, Identifiable { + case overview + case sessions + case models + + var id: String { + self.rawValue + } + + var title: String { + switch self { + case .overview: L("codex_workspaces_overview") + case .sessions: L("codex_workspaces_sessions") + case .models: L("codex_workspaces_models") + } + } +} + +@MainActor +struct CodexLocalProjectUsageWindowView: View { + let store: UsageStore + let settings: SettingsStore + @Bindable var selection: CodexLocalProjectUsageInspectorSelection + + @AppStorage(CodexLocalProjectUsageWindowController.selectedTabKey) + private var selectedTabRaw = CodexLocalProjectUsageWindowTab.overview.rawValue + @State private var customHistoryDays = 60 + @State private var associatedSessionIDs: Set? + @Environment(\.colorScheme) private var colorScheme + + private var selectedTab: CodexLocalProjectUsageWindowTab { + get { CodexLocalProjectUsageWindowTab(rawValue: self.selectedTabRaw) ?? .overview } + nonmutating set { self.selectedTabRaw = newValue.rawValue } + } + + var body: some View { + let appLanguage = self.settings.appLanguage + self.windowContent + .frame( + minWidth: CodexLocalProjectUsageWindowLayout.minimumSize.width, + idealWidth: CodexLocalProjectUsageWindowLayout.idealSize.width, + maxWidth: .infinity, + minHeight: CodexLocalProjectUsageWindowLayout.minimumSize.height, + idealHeight: CodexLocalProjectUsageWindowLayout.idealSize.height, + maxHeight: .infinity) + .background { + Color(nsColor: .windowBackgroundColor) + SettingsWindowAppearanceBridge( + colorScheme: self.colorScheme, + windowTitle: L("codex_workspaces_window_title")) + .allowsHitTesting(false) + } + .background { + CodexWorkspaceKeyboardCommands( + selectedTab: self.selectedTabBinding, + refresh: self.refresh) + } + .onAppear { + self.customHistoryDays = self.settings.costUsageHistoryDays + self.reconcileSelection() + } + .onChange(of: self.store.codexLocalProjectUsageSnapshot) { + self.reconcileSelection() + } + .onChange(of: self.selection.selectedProjectID) { previousProjectID, selectedProjectID in + self.associatedSessionIDs = CodexWorkspaceAssociatedSessionFilter.reconciledSessionIDs( + self.associatedSessionIDs, + from: previousProjectID, + to: selectedProjectID) + } + .environment( + \.locale, + appLanguage.isEmpty ? .autoupdatingCurrent : Locale(identifier: appLanguage)) + } + + @ViewBuilder + private var windowContent: some View { + if #available(macOS 26.0, *) { + self.primaryContent + } else { + VStack(spacing: 0) { + self.primaryContent + Divider() + CodexWorkspaceStatusBar(store: self.store) + .frame(height: CodexLocalProjectUsageWindowLayout.footerHeight) + } + } + } + + @ViewBuilder + private var primaryContent: some View { + switch CodexWorkspacePrimaryContentState.resolve( + snapshot: self.store.codexLocalProjectUsageSnapshot, + isRefreshing: self.store.codexLocalProjectUsageRefreshInFlight) + { + case let .content(snapshot): + let presentation = CodexLocalProjectUsageWindowPresentation( + snapshot: snapshot, + selectedDestinationID: self.selection.selectedProjectID, + projection: self.store.codexLocalProjectUsageProjection) + self.workspaceContent(presentation) + case .indexing: + VStack(spacing: 10) { + ProgressView() + .controlSize(.regular) + Text( + self.store.codexLocalProjectUsageProgressSubtitle + ?? L("codex_workspaces_indexing_local_logs")) + .foregroundStyle(.secondary) + if let fraction = self.store.codexLocalProjectUsageProgressFraction { + Text(fraction.formatted(.percent.precision(.fractionLength(0)))) + .font(.caption.monospacedDigit()) + .foregroundStyle(.secondary) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .accessibilityElement(children: .combine) + case .empty: + ContentUnavailableView( + L("codex_workspaces_no_local_usage"), + systemImage: "folder.badge.questionmark", + description: Text(L("codex_workspaces_estimated_local_logs"))) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + + private func workspaceContent(_ presentation: CodexLocalProjectUsageWindowPresentation) -> some View { + NavigationSplitView { + CodexWorkspaceSidebar( + presentation: presentation, + showsEstimatedCost: self.settings.codexLocalProjectUsageShowsEstimatedCost, + selectedDestinationID: self.$selection.selectedProjectID) + .navigationSplitViewColumnWidth( + min: CodexLocalProjectUsageWindowLayout.sidebarMinimumWidth, + ideal: CodexLocalProjectUsageWindowLayout.sidebarIdealWidth, + max: CodexLocalProjectUsageWindowLayout.sidebarMaximumWidth) + } detail: { + CodexWorkspaceDetail( + presentation: presentation, + selectedTab: self.selectedTabBinding, + showsEstimatedCost: self.settings.codexLocalProjectUsageShowsEstimatedCost, + hidePersonalInfo: self.settings.hidePersonalInfo, + historyDays: self.settings.costUsageHistoryDays, + customHistoryDays: self.$customHistoryDays, + isRefreshing: self.store.codexLocalProjectUsageRefreshInFlight, + progressText: self.store.codexLocalProjectUsageProgressSubtitle, + staleMessage: self.store.codexLocalProjectUsageLoadState == .stale + ? self.store.codexLocalProjectUsageError + : nil, + associatedSessionIDs: self.$associatedSessionIDs, + setHistoryDays: self.setHistoryDays, + refresh: self.refresh) + { + CodexWorkspaceCommandBar( + selectedTab: self.selectedTabBinding, + historyDays: self.settings.costUsageHistoryDays, + customHistoryDays: self.$customHistoryDays, + isRefreshing: self.store.codexLocalProjectUsageRefreshInFlight, + setHistoryDays: self.setHistoryDays, + refresh: self.refresh) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .background { + CodexWorkspaceSplitViewUnderlayBridge() + .frame(width: 0, height: 0) + } + .background { + if #unavailable(macOS 26.0) { + Color(nsColor: .windowBackgroundColor) + } + } + .modifier(CodexWorkspaceDetailFooter(store: self.store)) + } + .navigationSplitViewStyle(.balanced) + } + + private var selectedTabBinding: Binding { + Binding(get: { self.selectedTab }, set: { self.selectedTab = $0 }) + } + + private func reconcileSelection() { + guard let snapshot = self.store.codexLocalProjectUsageSnapshot else { return } + let selectedID = self.selection.selectedProjectID + let isValid = selectedID == CodexLocalProjectUsageWindowDestination.allWorkspacesID + || snapshot.projects.contains { $0.id == selectedID } + if !isValid { + self.selection.selectedProjectID = CodexLocalProjectUsageWindowDestination.allWorkspacesID + } + } + + private func refresh() { + self.store.scheduleCodexLocalProjectUsageRefreshIfNeeded(force: true) + } + + private func setHistoryDays(_ days: Int) { + let clamped = min(max(days, 1), 365) + guard clamped != self.settings.costUsageHistoryDays else { return } + self.settings.costUsageHistoryDays = clamped + self.customHistoryDays = clamped + self.refresh() + } +} + +@MainActor +private struct CodexWorkspaceDetailFooter: ViewModifier { + let store: UsageStore + + func body(content: Content) -> some View { + if #available(macOS 26.0, *) { + content + .scrollEdgeEffectStyle(.soft, for: .bottom) + .safeAreaBar(edge: .bottom, spacing: 0) { + CodexWorkspaceStatusBar(store: self.store) + .frame(height: CodexLocalProjectUsageWindowLayout.footerHeight) + } + } else { + content + } + } +} + +private struct CodexWorkspaceSplitViewUnderlayBridge: NSViewRepresentable { + func makeNSView(context: Context) -> CodexWorkspaceSplitViewUnderlayConfigurationView { + CodexWorkspaceSplitViewUnderlayConfigurationView() + } + + func updateNSView(_ nsView: CodexWorkspaceSplitViewUnderlayConfigurationView, context: Context) { + nsView.scheduleConfiguration() + } +} + +private final class CodexWorkspaceSplitViewUnderlayConfigurationView: NSView { + override func viewDidMoveToWindow() { + super.viewDidMoveToWindow() + self.scheduleConfiguration() + } + + override func viewDidMoveToSuperview() { + super.viewDidMoveToSuperview() + self.scheduleConfiguration() + } + + func scheduleConfiguration() { + guard #available(macOS 26.0, *) else { return } + DispatchQueue.main.async { [weak self] in + self?.configureSplitViewItem() + } + } + + @available(macOS 26.0, *) + private func configureSplitViewItem() { + guard let splitViewController = self.enclosingSplitViewController(), + let detailItem = splitViewController.splitViewItems.last, + !detailItem.automaticallyAdjustsSafeAreaInsets + else { return } + detailItem.automaticallyAdjustsSafeAreaInsets = true + } + + private func enclosingSplitViewController() -> NSSplitViewController? { + var responder: NSResponder? = self + while let currentResponder = responder { + if let viewController = currentResponder as? NSViewController, + let splitViewController = Self.enclosingSplitViewController(from: viewController) + { + return splitViewController + } + responder = currentResponder.nextResponder + } + + guard let rootViewController = self.window?.contentViewController else { return nil } + return Self.firstSplitViewController(in: rootViewController) + } + + private static func enclosingSplitViewController(from viewController: NSViewController) + -> NSSplitViewController? + { + var candidate: NSViewController? = viewController + while let current = candidate { + if let splitViewController = current as? NSSplitViewController { + return splitViewController + } + candidate = current.parent + } + return nil + } + + private static func firstSplitViewController(in viewController: NSViewController) -> NSSplitViewController? { + if let splitViewController = viewController as? NSSplitViewController { + return splitViewController + } + for child in viewController.children { + if let splitViewController = Self.firstSplitViewController(in: child) { + return splitViewController + } + } + return nil + } +} + +private struct CodexWorkspaceCommandBar: View { + @Binding var selectedTab: CodexLocalProjectUsageWindowTab + let historyDays: Int + @Binding var customHistoryDays: Int + let isRefreshing: Bool + let setHistoryDays: (Int) -> Void + let refresh: () -> Void + + var body: some View { + HStack(spacing: 10) { + Picker(L("codex_workspaces_view"), selection: self.$selectedTab) { + ForEach(CodexLocalProjectUsageWindowTab.allCases) { tab in + Text(tab.title).tag(tab) + } + } + .pickerStyle(.segmented) + .labelsHidden() + .controlSize(.small) + .frame(width: 228) + + Spacer(minLength: 16) + + if self.selectedTab != .models { + Menu { + ForEach([7, 30, 60, 90, 180, 365], id: \.self) { days in + Button { + self.setHistoryDays(days) + } label: { + if days == self.historyDays { + Label(self.historyTitle(days), systemImage: "checkmark") + } else { + Text(self.historyTitle(days)) + } + } + } + Divider() + HStack { + TextField(L("codex_workspaces_days"), value: self.$customHistoryDays, format: .number) + .frame(width: 58) + Stepper("", value: self.$customHistoryDays, in: 1...365) + .labelsHidden() + Button(L("apply")) { self.setHistoryDays(self.customHistoryDays) } + } + } label: { + Text(self.historyTitle(self.historyDays)) + .monospacedDigit() + } + .menuStyle(.borderlessButton) + .controlSize(.small) + .fixedSize() + + Button(action: self.refresh) { + if self.isRefreshing { + ProgressView().controlSize(.mini) + } else { + Image(systemName: "arrow.clockwise") + } + } + .buttonStyle(.bordered) + .controlSize(.small) + .disabled(self.isRefreshing) + .help(L("codex_workspaces_refresh_usage_data")) + .accessibilityLabel(L("codex_workspaces_refresh_usage_data")) + } + } + .padding(.horizontal, CodexLocalProjectUsageWindowLayout.detailPadding) + .frame(height: CodexLocalProjectUsageWindowLayout.commandHeight) + } + + private func historyTitle(_ days: Int) -> String { + L("codex_workspaces_last_days", days) + } +} + +private struct CodexWorkspaceKeyboardCommands: View { + @Binding var selectedTab: CodexLocalProjectUsageWindowTab + let refresh: () -> Void + + var body: some View { + HStack { + Button(L("codex_workspaces_overview")) { self.selectedTab = .overview } + .keyboardShortcut("1", modifiers: .command) + Button(L("codex_workspaces_sessions")) { self.selectedTab = .sessions } + .keyboardShortcut("2", modifiers: .command) + Button(L("codex_workspaces_models")) { self.selectedTab = .models } + .keyboardShortcut("3", modifiers: .command) + Button(L("codex_workspaces_refresh_usage_data"), action: self.refresh) + .keyboardShortcut("r", modifiers: .command) + } + .frame(width: 0, height: 0) + .opacity(0) + .accessibilityHidden(true) + } +} diff --git a/Sources/CodexBar/CodexModelsDashboardAnalysis.swift b/Sources/CodexBar/CodexModelsDashboardAnalysis.swift new file mode 100644 index 0000000000..c231131dcb --- /dev/null +++ b/Sources/CodexBar/CodexModelsDashboardAnalysis.swift @@ -0,0 +1,813 @@ +import Accessibility +import AppKit +import Charts +import CodexBarCore +import SwiftUI + +enum CodexModelsColorRegistry { + private static let tones: [Double] = [1.00, 0.90, 0.82, 0.74, 0.66, 0.58, 0.50] + + static func color(canonicalID: String, isSelected: Bool, isLeading: Bool = false) -> Color { + if isSelected { return .accentColor } + if isLeading { return .accentColor.opacity(0.72) } + let hash = canonicalID.utf8.reduce(0) { (($0 << 5) &+ $0) &+ Int($1) } + return .secondary.opacity(self.tones[abs(hash) % self.tones.count]) + } +} + +struct CodexModelsAnalysis: View { + let model: CodexModelsDashboardModel + + var body: some View { + switch self.model.layout { + case .wide: + HStack(alignment: .top, spacing: 0) { + CodexModelsRankingChart(model: self.model) + .frame(minWidth: 620, maxWidth: .infinity, maxHeight: .infinity) + Divider() + VStack(spacing: 0) { + CodexModelsTimelineChart(model: self.model) + .frame(height: CodexModelsDashboardTokens.Height.wideTimeline) + Divider() + CodexModelsConcentrationView(model: self.model) + .frame(height: CodexModelsDashboardTokens.Height.wideConcentration) + } + .frame(width: CodexModelsDashboardTokens.Width.wideAnalysisTrailing) + } + .frame(height: CodexModelsDashboardTokens.Height.wideAnalysisBody) + case .medium: + VStack(spacing: 0) { + CodexModelsRankingChart(model: self.model) + .frame(height: CodexModelsDashboardTokens.Height.mediumRanking) + Divider() + ViewThatFits(in: .horizontal) { + HStack(alignment: .top, spacing: 0) { + CodexModelsTimelineChart(model: self.model) + .frame(minWidth: 540, maxWidth: .infinity, maxHeight: .infinity) + Divider() + CodexModelsConcentrationView(model: self.model) + .frame( + minWidth: 360, + maxWidth: CodexModelsDashboardTokens.Width.mediumConcentration, + maxHeight: .infinity) + } + .frame(minWidth: 917, maxWidth: .infinity) + .frame(height: CodexModelsDashboardTokens.Height.mediumAnalysisLower) + VStack(spacing: 0) { + CodexModelsTimelineChart(model: self.model) + .frame(height: CodexModelsDashboardTokens.Height.wideTimeline) + Divider() + CodexModelsConcentrationView(model: self.model) + } + } + } + case .compact: + VStack(spacing: 0) { + CodexModelsRankingChart(model: self.model).frame(minHeight: 330) + Divider() + CodexModelsTimelineChart(model: self.model).frame(minHeight: 200) + Divider() + CodexModelsConcentrationView(model: self.model) + } + } + } +} + +private struct CodexModelsRankingChart: View { + let model: CodexModelsDashboardModel + + var body: some View { + let model = self.model + let groups = model.rankingGroups + let maximum = max(1, groups.map(\.value).max() ?? 1) + let axis = self.axisScale(maximum: maximum) + VStack(alignment: .leading, spacing: 10) { + HStack(alignment: .firstTextBaseline) { + Text(L("codex_models_usage_by_model")).font(.subheadline.weight(.semibold)) + Text(L("codex_models_sorted_by", model.metric.analyticsNoun)) + .font(.caption) + .foregroundStyle(.secondary) + Spacer() + Text(groups.last?.isOther == true + ? L("codex_models_top_seven_other") + : L("codex_models_model_count", groups.count)) + .font(.caption) + .foregroundStyle(.secondary) + } + Chart(groups) { group in + BarMark( + xStart: .value(L("codex_models_track_start"), 0), + xEnd: .value(L("codex_models_track_end"), axis.upperBound), + y: .value(L("codex_models_canonical_model"), group.id)) + .foregroundStyle(Color.secondary.opacity(0.10)) + .cornerRadius(CodexModelsDashboardTokens.Radius.chartBar) + .annotation(position: .trailing, alignment: .leading, spacing: 6) { + HStack(spacing: 4) { + Text(self.value(group)) + Text(L("codex_models_bullet_value", CodexModelsFormatters.percentage(group.share))) + .foregroundStyle(.secondary) + } + .font(.caption2) + .monospacedDigit() + } + .accessibilityHidden(true) + BarMark( + xStart: .value(L("codex_models_value_start"), 0), + xEnd: .value(model.metric.localizedTitle, group.value), + y: .value(L("codex_models_canonical_model"), group.id)) + .foregroundStyle(self.color(group)) + .cornerRadius(CodexModelsDashboardTokens.Radius.chartBar) + .accessibilityLabel(group.displayName) + .accessibilityValue( + L( + "codex_models_value_share_accessibility", + self.value(group), + CodexModelsFormatters.percentage(group.share)) + + (self.isSelected(group) ? L("codex_models_selected_suffix") : "")) + } + .chartLegend(.hidden) + .chartXScale(domain: 0...(axis.upperBound * 1.18)) + .chartXAxis { + AxisMarks(position: .bottom, values: axis.ticks) { value in + AxisGridLine().foregroundStyle(.secondary.opacity(0.12)) + AxisValueLabel { + if let number = value.as(Double.self) { Text(self.axisValue(number)) } + } + } + } + .chartYAxis { + AxisMarks(values: groups.map(\.id)) { value in + AxisValueLabel { + if let id = value.as(String.self), let group = groups.first(where: { $0.id == id }) { + HStack(spacing: 4) { + Text(group.displayName).lineLimit(1) + if model.topModel?.id == group.canonicalModelID { + Text(L("codex_models_top")) + .font(.caption2.weight(.semibold)) + .foregroundStyle(.secondary) + } + } + } + } + } + } + .chartOverlay { proxy in + GeometryReader { geometry in + let plotFrame = geometry[proxy.plotAreaFrame] + Rectangle() + .fill(.clear) + .contentShape(Rectangle()) + .frame(width: plotFrame.width, height: plotFrame.height) + .position(x: plotFrame.midX, y: plotFrame.midY) + .gesture( + SpatialTapGesture(coordinateSpace: .local) + .onEnded { value in + self.selectModel( + at: value.location.y, + proxy: proxy, + groups: groups) + }) + } + } + .accessibilityChartDescriptor(CodexModelsRankingChartDescriptor( + groups: groups, + metric: model.metric, + currencyCode: model.snapshot.cost.currencyCode)) + } + .padding(CodexModelsDashboardTokens.Spacing.large) + .accessibilityElement(children: .contain) + .accessibilityLabel(L("codex_models_ranking_by", model.metric.localizedTitle)) + .accessibilityHint(L("codex_models_ranking_hint")) + } + + private func selectModel( + at yPosition: CGFloat, + proxy: ChartProxy, + groups: [CodexModelsVisualGroup]) + { + guard let groupID = proxy.value(atY: yPosition, as: String.self), + let group = groups.first(where: { $0.id == groupID }), + let canonicalModelID = group.canonicalModelID + else { return } + self.model.selectModel(canonicalModelID) + } + + private func color(_ group: CodexModelsVisualGroup) -> Color { + if self.isSelected(group) { return .accentColor } + guard let id = group.canonicalModelID else { return .secondary.opacity(0.42) } + return CodexModelsColorRegistry.color( + canonicalID: id, + isSelected: false, + isLeading: self.model.selectedModelID == nil && self.model.topModel?.id == id) + } + + private func isSelected(_ group: CodexModelsVisualGroup) -> Bool { + guard let selectedModelID = self.model.selectedModelID else { return false } + return group.rows.contains { $0.id == selectedModelID } + } + + private func value(_ group: CodexModelsVisualGroup) -> String { + switch self.model.metric { + case .tokens: + return CodexModelsFormatters.compactInteger(Int64(group.value)) + case .knownCost: + let cost = group.rows.reduce(CodexModelsCost.zero) { $0.adding($1.cost) } + let tokens = group.rows.reduce(Int64.zero) { $0 + $1.totalTokens } + return CodexModelsFormatters.cost(cost, usageTokens: tokens).primary + case .sessionReferences: + return Int(group.value).formatted() + } + } + + private func axisValue(_ value: Double) -> String { + switch self.model.metric { + case .tokens: CodexModelsFormatters.compactInteger(Int64(value)) + case .knownCost: + CodexModelsFormatters.currency( + Decimal(value), + code: self.model.snapshot.cost.currencyCode, + compact: true) + case .sessionReferences: CodexModelsFormatters.compactNumber(value) + } + } + + private func axisScale(maximum: Double) -> (upperBound: Double, ticks: [Double]) { + let rawStep = maximum / 4 + let exponent = pow(10, floor(log10(max(rawStep, .leastNonzeroMagnitude)))) + let normalized = rawStep / exponent + let multiplier = if normalized <= 1 { + 1.0 + } else if normalized <= 2 { + 2.0 + } else if normalized <= 5 { + 5.0 + } else { + 10.0 + } + let step = multiplier * exponent + let upperBound = max(step, ceil(maximum / step) * step) + return (upperBound, stride(from: 0, through: upperBound, by: step).map(\.self)) + } +} + +private struct CodexModelsTimelineChart: View { + let model: CodexModelsDashboardModel + @State private var hoverSelectionTask: Task? + @State private var pendingHoverBucketStart: Date? + + var body: some View { + let model = self.model + let timeline = model.timelineData + let buckets = timeline.buckets + let plottedBuckets = timeline.plottedBuckets + let presentation = timeline.presentation + VStack(alignment: .leading, spacing: 9) { + HStack(alignment: .firstTextBaseline, spacing: 8) { + Text(model.selectedRow.map { + L("codex_models_named_metric_over_time", $0.displayName, model.metric.analyticsNoun) + } ?? L("codex_models_metric_over_time", model.metric.localizedTitle)) + .font(.subheadline.weight(.semibold)) + Text(L("codex_models_bucket_count", buckets.count, model.granularity.localizedTitle.lowercased())) + .font(.caption) + .foregroundStyle(.secondary) + Spacer() + if timeline.inProgressBucket != nil { + Text(L("codex_models_current_period_in_progress", self.periodNoun)) + .font(.caption) + .foregroundStyle(.secondary) + } else if let changeFromPeak = timeline.changeFromPeak, + let peakBucket = timeline.peakBucket + { + Text(L( + "codex_models_change_from_peak", + changeFromPeak.formatted(.percent.precision(.fractionLength(0...1))), + peakBucket.day.formatted(.dateTime.month(.abbreviated)))) + .font(.caption) + .foregroundStyle(.secondary) + } + } + if let selectedRow = model.selectedRow { + HStack(spacing: 6) { + Label(selectedRow.displayName, systemImage: "checkmark.circle.fill") + .font(.caption.weight(.medium)) + .foregroundStyle(Color.accentColor) + Button(L("Clear")) { + model.selectModel(nil) + model.selectedBucketStart = nil + } + .buttonStyle(.link) + .font(.caption) + } + .accessibilityElement(children: .combine) + .accessibilityLabel(L("codex_models_selected_clear_available", selectedRow.displayName)) + } + if case let .sparse(observedBuckets) = presentation { + Text(L("codex_models_trend_unavailable_observed_buckets", observedBuckets)) + .font(.caption) + .foregroundStyle(.secondary) + } + Chart { + if presentation == .trend { + ForEach(timeline.completedPlottedBuckets) { bucket in + AreaMark( + x: .value(L("codex_models_date"), bucket.day), + y: .value(model.metric.localizedTitle, self.metricValue(bucket))) + .foregroundStyle(.linearGradient( + colors: [self.seriesColor.opacity(0.10), self.seriesColor.opacity(0.01)], + startPoint: .top, + endPoint: .bottom)) + LineMark( + x: .value(L("codex_models_date"), bucket.day), + y: .value(model.metric.localizedTitle, self.metricValue(bucket))) + .foregroundStyle(self.seriesColor) + .lineStyle(.init(lineWidth: 1.5)) + } + if let bucket = timeline.inProgressBucket { + PointMark( + x: .value(L("codex_models_date"), bucket.day), + y: .value(model.metric.localizedTitle, self.metricValue(bucket))) + .foregroundStyle(self.seriesColor.opacity(0.45)) + .symbolSize(42) + PointMark( + x: .value(L("codex_models_date"), bucket.day), + y: .value(model.metric.localizedTitle, self.metricValue(bucket))) + .foregroundStyle(Color(nsColor: .controlBackgroundColor)) + .symbolSize(20) + } + } else { + ForEach(plottedBuckets) { bucket in + BarMark( + x: .value(L("codex_models_date"), bucket.day), + y: .value(model.metric.localizedTitle, self.metricValue(bucket)), + width: .fixed(18)) + .foregroundStyle(self.seriesColor.opacity(0.55)) + PointMark( + x: .value(L("codex_models_date"), bucket.day), + y: .value(model.metric.localizedTitle, self.metricValue(bucket))) + .foregroundStyle(self.seriesColor) + .symbolSize(36) + } + } + if let bucket = timeline.selectedBucket, + model.metric != .knownCost || bucket.cost.pricedTokens > 0 + { + PointMark( + x: .value(L("codex_models_selected_date"), bucket.day), + y: .value(model.metric.localizedTitle, self.metricValue(bucket))) + .foregroundStyle(self.seriesColor) + .symbolSize(44) + } + if let bucket = timeline.selectedBucket, + model.metric != .knownCost || bucket.cost.pricedTokens > 0 + { + RuleMark(x: .value(L("codex_models_selected_bucket"), bucket.day)) + .foregroundStyle(.secondary) + .annotation( + position: .top, + alignment: bucket.day == plottedBuckets.last?.day ? .trailing : .leading) + { + self.bucketPopover(bucket) + } + } + } + .chartLegend(.hidden) + .chartYAxis { + AxisMarks(position: .leading) { value in + AxisGridLine().foregroundStyle(.secondary.opacity(0.12)) + AxisValueLabel { + if let number = value.as(Double.self) { Text(self.axisValue(number)) } + } + } + } + .chartOverlay { proxy in + GeometryReader { geometry in + let plotFrame = geometry[proxy.plotAreaFrame] + Rectangle() + .fill(.clear) + .contentShape(Rectangle()) + .frame(width: plotFrame.width, height: plotFrame.height) + .position(x: plotFrame.midX, y: plotFrame.midY) + .gesture( + SpatialTapGesture(coordinateSpace: .local) + .onEnded { value in + self.commitSelection( + at: value.location.x, + proxy: proxy, + timeline: timeline) + }) + .onContinuousHover(coordinateSpace: .local) { phase in + switch phase { + case let .active(location): + self.scheduleHoverSelection( + at: location.x, + proxy: proxy, + timeline: timeline) + case .ended: + self.cancelHoverSelection() + } + } + } + } + .accessibilityChartDescriptor(CodexModelsTimelineChartDescriptor( + buckets: buckets, + metric: model.metric, + granularity: model.granularity, + currencyCode: model.snapshot.cost.currencyCode, + selectedModelName: model.selectedRow?.displayName, + selectedBucket: timeline.selectedBucket)) + .frame(minHeight: 145) + } + .padding(CodexModelsDashboardTokens.Spacing.large) + .accessibilityElement(children: .contain) + .accessibilityLabel(L( + "codex_models_timeline_accessibility", + model.granularity.localizedTitle, + model.metric.localizedTitle)) + .accessibilityHint(L("codex_models_timeline_hint")) + .onDisappear { self.cancelHoverSelection() } + } + + private func commitSelection( + at xPosition: CGFloat, + proxy: ChartProxy, + timeline: CodexModelsTimelineData) + { + self.cancelHoverSelection() + guard let date = proxy.value(atX: xPosition, as: Date.self) else { return } + self.model.selectedBucketStart = timeline.selectionStart(nearest: date) + } + + private func scheduleHoverSelection( + at xPosition: CGFloat, + proxy: ChartProxy, + timeline: CodexModelsTimelineData) + { + let bucketStart = proxy.value(atX: xPosition, as: Date.self) + .flatMap { timeline.selectionStart(nearest: $0) } + guard self.pendingHoverBucketStart != bucketStart else { return } + + self.hoverSelectionTask?.cancel() + self.pendingHoverBucketStart = bucketStart + guard let bucketStart else { return } + + self.hoverSelectionTask = Task { @MainActor in + try? await Task.sleep(nanoseconds: 1_500_000_000) + guard !Task.isCancelled, self.pendingHoverBucketStart == bucketStart else { return } + self.model.selectedBucketStart = bucketStart + self.hoverSelectionTask = nil + } + } + + private func cancelHoverSelection() { + self.hoverSelectionTask?.cancel() + self.hoverSelectionTask = nil + self.pendingHoverBucketStart = nil + } + + private func bucketPopover(_ bucket: CodexModelsDailyBucket) -> some View { + let cost = CodexModelsFormatters.cost(bucket.cost, usageTokens: bucket.tokens) + return VStack(alignment: .leading, spacing: 3) { + HStack { + Text(CodexModelsFormatters.interval(bucket.effectiveInterval)).fontWeight(.semibold) + Spacer(minLength: 8) + Button(L("Close"), systemImage: "xmark") { self.model.selectedBucketStart = nil } + .labelStyle(.iconOnly) + .buttonStyle(.plain) + } + Text(L("codex_models_token_count", bucket.tokens.formatted())) + Text(L("codex_models_session_reference_count", bucket.sessionReferences.formatted())) + Text(L( + "codex_models_cost_status_currency", + cost.primary, + cost.secondary ?? L("codex_models_fully_priced"), + bucket.cost.currencyCode)) + } + .font(.caption2) + .monospacedDigit() + .padding(CodexModelsDashboardTokens.Spacing.medium) + .frame(width: 225, alignment: .leading) + .background( + .regularMaterial, + in: RoundedRectangle( + cornerRadius: CodexModelsDashboardTokens.Radius.tooltip, + style: .continuous)) + .overlay { + RoundedRectangle(cornerRadius: CodexModelsDashboardTokens.Radius.tooltip, style: .continuous) + .stroke(Color.secondary.opacity(0.25), lineWidth: 1) + } + .accessibilityElement(children: .combine) + } + + private var seriesColor: Color { + self.model.selectedRow == nil ? .accentColor.opacity(0.78) : .accentColor + } + + private var periodNoun: String { + switch self.model.granularity { + case .daily: L("codex_models_day_lowercase") + case .weekly: L("codex_models_week_lowercase") + case .monthly: L("codex_models_month_lowercase") + } + } + + private func metricValue(_ bucket: CodexModelsDailyBucket) -> Double { + switch self.model.metric { + case .tokens: Double(bucket.tokens) + case .knownCost: NSDecimalNumber(decimal: bucket.cost.knownAmount).doubleValue + case .sessionReferences: Double(bucket.sessionReferences) + } + } + + private func axisValue(_ value: Double) -> String { + switch self.model.metric { + case .tokens: CodexModelsFormatters.compactInteger(Int64(value)) + case .knownCost: + CodexModelsFormatters.currency( + Decimal(value), + code: self.model.snapshot.cost.currencyCode, + compact: true) + case .sessionReferences: CodexModelsFormatters.compactNumber(value) + } + } +} + +private struct CodexModelsConcentrationView: View { + let model: CodexModelsDashboardModel + + var body: some View { + let concentration = self.model.concentrationData + let groups = concentration.groups + VStack(alignment: .leading, spacing: 8) { + HStack(alignment: .firstTextBaseline, spacing: 6) { + Text(L("codex_models_model_concentration")).font(.subheadline.weight(.semibold)) + Text(L("codex_models_share_of", self.model.metric.shareBasisTitle)) + .font(.caption) + .foregroundStyle(.secondary) + Spacer(minLength: 0) + } + ViewThatFits(in: .horizontal) { + HStack(alignment: .top, spacing: 14) { + self.insight(topTwoShare: concentration.topTwoShare).frame(width: 112, alignment: .leading) + self.distribution(groups) + } + .frame(minWidth: 350) + VStack(alignment: .leading, spacing: 8) { + self.insight(topTwoShare: concentration.topTwoShare) + self.distribution(groups) + } + } + } + .padding(CodexModelsDashboardTokens.Spacing.large) + .accessibilityElement(children: .contain) + .accessibilityLabel(L("codex_models_model_concentration")) + .accessibilityValue(self.headline(topTwoShare: concentration.topTwoShare)) + } + + private func headline(topTwoShare: Double) -> String { + let share = CodexModelsFormatters.percentage(topTwoShare) + return L("codex_models_top_two_account_for", share, self.model.metric.analyticsNoun) + } + + private func insight(topTwoShare: Double) -> some View { + VStack(alignment: .leading, spacing: 2) { + Text(CodexModelsFormatters.percentage(topTwoShare)) + .font(.title2.weight(.semibold)) + .monospacedDigit() + Text(L("codex_models_top_two_of", self.model.metric.shareBasisTitle)) + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + } + + private func distribution(_ groups: [CodexModelsVisualGroup]) -> some View { + VStack(spacing: 7) { + GeometryReader { proxy in + let gaps = CGFloat(max(0, groups.count - 1)) + let availableWidth = max(0, proxy.size.width - gaps) + HStack(spacing: 1) { + ForEach(groups) { group in + self.segment(group) + .frame(width: max(0, availableWidth * group.share)) + } + } + .clipShape(RoundedRectangle(cornerRadius: 4)) + } + .frame(height: 18) + LazyVGrid(columns: [GridItem(.adaptive(minimum: 110), spacing: 8)], spacing: 2) { + ForEach(groups) { group in + self.legendItem(group) + } + } + } + .frame(maxWidth: .infinity) + } + + @ViewBuilder + private func segment(_ group: CodexModelsVisualGroup) -> some View { + if let id = group.canonicalModelID { + Button { self.model.selectModel(id) } label: { + Rectangle().fill(self.color(group)) + } + .buttonStyle(.plain) + .accessibilityLabel(L("codex_models_select_model", group.displayName)) + .accessibilityValue(CodexModelsFormatters.percentage(group.share)) + } else { + Rectangle() + .fill(self.color(group)) + .accessibilityLabel(group.displayName) + .accessibilityValue( + CodexModelsFormatters.percentage(group.share) + + (self.isSelected(group) ? L("codex_models_contains_selected_suffix") : "")) + } + } + + private func color(_ group: CodexModelsVisualGroup) -> Color { + if self.isSelected(group) { return .accentColor } + guard let id = group.canonicalModelID else { return .secondary.opacity(0.42) } + return CodexModelsColorRegistry.color( + canonicalID: id, + isSelected: false, + isLeading: self.model.selectedModelID == nil && self.model.topModel?.id == id) + } + + private func isSelected(_ group: CodexModelsVisualGroup) -> Bool { + guard let selectedModelID = self.model.selectedModelID else { return false } + return group.rows.contains { $0.id == selectedModelID } + } + + private func legendItem(_ group: CodexModelsVisualGroup) -> some View { + Group { + if let id = group.canonicalModelID { + Button { self.model.selectModel(id) } label: { self.legendLabel(group) } + .buttonStyle(.plain) + } else { + self.legendLabel(group) + } + } + .frame(minHeight: 28) + .accessibilityElement(children: .combine) + .accessibilityLabel(L( + "codex_models_name_value_accessibility", + group.displayName, + CodexModelsFormatters.percentage(group.share))) + } + + private func legendLabel(_ group: CodexModelsVisualGroup) -> some View { + HStack(spacing: 7) { + RoundedRectangle(cornerRadius: 2) + .fill(self.color(group)) + .frame(width: 12, height: 12) + Text(group.displayName).lineLimit(1) + Spacer(minLength: 4) + Text(CodexModelsFormatters.percentage(group.share)).monospacedDigit() + } + .font(.caption) + .contentShape(Rectangle()) + } +} + +private struct CodexModelsRankingChartDescriptor: AXChartDescriptorRepresentable { + let groups: [CodexModelsVisualGroup] + let metric: CodexModelsMetric + let currencyCode: String + + func makeChartDescriptor() -> AXChartDescriptor { + let upperBound = max(1, self.groups.map(\.value).max() ?? 1) + let categories = self.groups.map(self.identity) + let modelAxis = AXCategoricalDataAxisDescriptor( + title: L("codex_models_canonical_model"), + categoryOrder: categories) + let valueAxis = AXNumericDataAxisDescriptor( + title: self.metric.localizedTitle, + range: 0...upperBound, + gridlinePositions: []) { self.axisDescription($0) } + let points = zip(self.groups, categories).map { group, identity in + let share = CodexModelsFormatters.percentage(group.share) + return AXDataPoint( + x: identity, + y: group.value, + label: L("codex_models_ranking_point_accessibility", identity, self.description(group), share)) + } + return AXChartDescriptor( + title: L("codex_models_usage_by_model"), + summary: L("codex_models_ranking_summary", self.metric.analyticsNoun), + xAxis: modelAxis, + yAxis: valueAxis, + series: [AXDataSeriesDescriptor( + name: self.metric.localizedTitle, + isContinuous: false, + dataPoints: points)]) + } + + private func identity(_ group: CodexModelsVisualGroup) -> String { + group.canonicalModelID.map { L("codex_models_name_canonical_id", group.displayName, $0) } + ?? group.displayName + } + + private func axisDescription(_ value: Double) -> String { + switch self.metric { + case .tokens: L("codex_models_token_count", Int64(value).formatted()) + case .knownCost: value.formatted(.currency(code: self.currencyCode)) + case .sessionReferences: L("codex_models_session_reference_count", Int(value).formatted()) + } + } + + private func description(_ group: CodexModelsVisualGroup) -> String { + switch self.metric { + case .tokens: return L("codex_models_token_count", Int64(group.value).formatted()) + case .knownCost: + let cost = group.rows.reduce(CodexModelsCost.zero) { $0.adding($1.cost) } + let tokens = group.rows.reduce(Int64.zero) { $0 + $1.totalTokens } + return CodexModelsFormatters.cost(cost, usageTokens: tokens).accessibility + case .sessionReferences: return L("codex_models_session_reference_count", Int(group.value).formatted()) + } + } +} + +private struct CodexModelsTimelineChartDescriptor: AXChartDescriptorRepresentable { + let buckets: [CodexModelsDailyBucket] + let metric: CodexModelsMetric + let granularity: CodexModelsGranularity + let currencyCode: String + let selectedModelName: String? + let selectedBucket: CodexModelsDailyBucket? + + func makeChartDescriptor() -> AXChartDescriptor { + let describedBuckets = self.buckets.filter { self.metric != .knownCost || $0.cost.pricedTokens > 0 } + let dates = describedBuckets.map(\.day.timeIntervalSinceReferenceDate) + let lowerBound = dates.min() ?? 0 + let upperBound = max(lowerBound + 1, dates.max() ?? 1) + let values = describedBuckets.map(self.value) + let dateAxis = AXNumericDataAxisDescriptor( + title: L("codex_models_bucket_interval"), + range: lowerBound...upperBound, + gridlinePositions: []) + { + Date(timeIntervalSinceReferenceDate: $0).formatted(date: .abbreviated, time: .omitted) + } + let valueAxis = AXNumericDataAxisDescriptor( + title: self.metric.localizedTitle, + range: 0...max(1, values.max() ?? 1), + gridlinePositions: []) { self.description($0) } + let points = zip(describedBuckets, values).map { bucket, value in + let interval = CodexModelsFormatters.interval(bucket.effectiveInterval) + let metricValue = CodexModelsFormatters.metricValue(bucket, metric: self.metric) + let cost = CodexModelsFormatters.cost(bucket.cost, usageTokens: bucket.tokens) + return AXDataPoint( + x: bucket.day.timeIntervalSinceReferenceDate, + y: value, + label: L( + "codex_models_timeline_point_accessibility", + interval, + metricValue, + bucket.tokens.formatted(), + bucket.sessionReferences.formatted(), + cost.accessibility, + bucket.cost.currencyCode)) + } + let subject = self.selectedModelName ?? L("codex_models_all_models") + let selection = self.selectedBucket.map { + let interval = CodexModelsFormatters.interval($0.effectiveInterval) + let metricValue = CodexModelsFormatters.metricValue($0, metric: self.metric) + let cost = CodexModelsFormatters.cost($0.cost, usageTokens: $0.tokens) + return L( + "codex_models_selected_bucket_accessibility", + interval, + metricValue, + $0.tokens.formatted(), + $0.sessionReferences.formatted(), + cost.accessibility, + $0.cost.currencyCode) + } ?? "" + return AXChartDescriptor( + title: L("codex_models_subject_over_time", subject), + summary: L( + "codex_models_timeline_summary", + self.granularity.localizedTitle, + self.metric.analyticsNoun, + selection), + xAxis: dateAxis, + yAxis: valueAxis, + series: [AXDataSeriesDescriptor( + name: subject, + isContinuous: describedBuckets.count >= 3, + dataPoints: points)]) + } + + private func value(_ bucket: CodexModelsDailyBucket) -> Double { + switch self.metric { + case .tokens: Double(bucket.tokens) + case .knownCost: NSDecimalNumber(decimal: bucket.cost.knownAmount).doubleValue + case .sessionReferences: Double(bucket.sessionReferences) + } + } + + private func description(_ value: Double) -> String { + switch self.metric { + case .tokens: L("codex_models_token_count", Int64(value).formatted()) + case .knownCost: value.formatted(.currency(code: self.currencyCode)) + case .sessionReferences: L("codex_models_session_reference_count", Int(value).formatted()) + } + } +} diff --git a/Sources/CodexBar/CodexModelsDashboardModel.swift b/Sources/CodexBar/CodexModelsDashboardModel.swift new file mode 100644 index 0000000000..5021ec5b60 --- /dev/null +++ b/Sources/CodexBar/CodexModelsDashboardModel.swift @@ -0,0 +1,495 @@ +import CodexBarCore +import Foundation +import Observation + +extension CodexModelsMetric { + var localizedTitle: String { + switch self { + case .tokens: L("codex_models_tokens") + case .knownCost: L("codex_models_known_cost") + case .sessionReferences: L("codex_models_session_references") + } + } + + var analyticsNoun: String { + switch self { + case .tokens: L("codex_models_tokens_lowercase") + case .knownCost: L("codex_models_known_cost_lowercase") + case .sessionReferences: L("codex_models_session_references_lowercase") + } + } + + var shareBasisTitle: String { + switch self { + case .tokens: L("codex_models_total_tokens_lowercase") + case .knownCost: L("codex_models_known_cost_lowercase") + case .sessionReferences: L("codex_models_total_session_references_lowercase") + } + } +} + +extension CodexModelsGranularity { + var localizedTitle: String { + switch self { + case .daily: L("Daily") + case .weekly: L("Weekly") + case .monthly: L("Monthly") + } + } +} + +enum CodexModelsGranularityPreference: String, CaseIterable, Identifiable { + case automatic + case daily + case weekly + case monthly + + var id: Self { + self + } + + var title: String { + switch self { + case .automatic: L("Automatic") + case .daily: L("Daily") + case .weekly: L("Weekly") + case .monthly: L("Monthly") + } + } +} + +enum CodexModelsOptionalColumn: String, CaseIterable, Identifiable { + case sessionReferences + case delta + + var id: Self { + self + } + + var title: String { + switch self { + case .sessionReferences: L("codex_models_session_refs") + case .delta: L("codex_models_delta_vs_prior") + } + } +} + +enum CodexModelsTimelinePresentation: Equatable { + case trend + case sparse(observedBuckets: Int) +} + +struct CodexModelsTimelineData { + let buckets: [CodexModelsDailyBucket] + let plottedBuckets: [CodexModelsDailyBucket] + let completedPlottedBuckets: [CodexModelsDailyBucket] + let inProgressBucket: CodexModelsDailyBucket? + let peakBucket: CodexModelsDailyBucket? + let presentation: CodexModelsTimelinePresentation + let selectedBucket: CodexModelsDailyBucket? + let changeFromPeak: Double? + + func selectionStart(nearest date: Date) -> Date? { + guard let nearest = self.plottedBuckets.min(by: { + abs($0.day.timeIntervalSince(date)) < abs($1.day.timeIntervalSince(date)) + }) else { return nil } + let adjacentSpacings = zip(self.plottedBuckets, self.plottedBuckets.dropFirst()) + .map { abs($1.day.timeIntervalSince($0.day)) } + let threshold = if let minimumSpacing = adjacentSpacings.min() { + minimumSpacing * 0.32 + } else { + max(nearest.effectiveInterval.duration * 0.32, 12 * 60 * 60) + } + return abs(nearest.day.timeIntervalSince(date)) <= threshold ? nearest.day : nil + } +} + +struct CodexModelsConcentrationData { + let groups: [CodexModelsVisualGroup] + let topTwoShare: Double +} + +struct CodexModelsSortValue: Comparable { + let value: Value + let canonicalID: String + + static func < (lhs: Self, rhs: Self) -> Bool { + if lhs.value != rhs.value { return lhs.value < rhs.value } + return lhs.canonicalID < rhs.canonicalID + } +} + +struct CodexModelsCostSortValue: Comparable { + let availability: Int + let coverage: Double + let amount: Decimal + let canonicalID: String + + static func < (lhs: Self, rhs: Self) -> Bool { + if lhs.availability != rhs.availability { return lhs.availability < rhs.availability } + if lhs.coverage != rhs.coverage { return lhs.coverage < rhs.coverage } + if lhs.amount != rhs.amount { return lhs.amount < rhs.amount } + return lhs.canonicalID < rhs.canonicalID + } +} + +struct CodexModelsTableRow: Identifiable { + let row: CodexModelsRow + let metric: CodexModelsMetric + let share: Double + + var id: String { + self.row.id + } + + var modelSort: CodexModelsSortValue { + CodexModelsSortValue(value: self.row.displayName.localizedLowercase, canonicalID: self.id) + } + + var tokensSort: CodexModelsSortValue { + CodexModelsSortValue(value: self.row.totalTokens, canonicalID: self.id) + } + + var costSort: CodexModelsCostSortValue { + let availability = if self.row.cost.pricedTokens == 0 { + 0 + } else if self.row.cost.unpricedTokens > 0 { + 1 + } else { + 2 + } + return CodexModelsCostSortValue( + availability: availability, + coverage: self.row.cost.coverage, + amount: self.row.cost.knownAmount, + canonicalID: self.id) + } + + var sessionReferencesSort: CodexModelsSortValue { + CodexModelsSortValue(value: self.row.sessionReferences, canonicalID: self.id) + } + + var shareSort: CodexModelsSortValue { + CodexModelsSortValue(value: self.share, canonicalID: self.id) + } + + var deltaSort: CodexModelsSortValue { + CodexModelsSortValue(value: self.row.comparison(self.metric).sortableValue, canonicalID: self.id) + } +} + +struct CodexModelsVisualGroup: Identifiable { + let id: String + let canonicalModelID: String? + let displayName: String + let value: Double + let share: Double + let modelCount: Int + let rows: [CodexModelsRow] + + var isOther: Bool { + self.canonicalModelID == nil + } +} + +@MainActor +@Observable +final class CodexModelsDashboardModel { + enum Layout: Equatable { + case wide + case medium + case compact + } + + var snapshot: CodexModelsAnalyticsSnapshot + private(set) var metric: CodexModelsMetric = .tokens + private(set) var granularityPreference: CodexModelsGranularityPreference = .automatic + var searchText = "" + var selectedModelID: String? + var selectedBucketStart: Date? + var layout: Layout = .compact + var optionalColumns = Set(CodexModelsOptionalColumn.allCases) + var sortOrder: [KeyPathComparator] + + init(snapshot: CodexModelsAnalyticsSnapshot) { + self.snapshot = snapshot + self.sortOrder = Self.defaultSortOrder(metric: .tokens) + } + + var granularity: CodexModelsGranularity { + switch self.granularityPreference { + case .automatic: + let interval = self.snapshot.currentInterval + let inclusiveEnd = interval.end.addingTimeInterval(-0.001) + let start = Calendar.current.startOfDay(for: interval.start) + let end = Calendar.current.startOfDay(for: inclusiveEnd) + let days = (Calendar.current.dateComponents([.day], from: start, to: end).day ?? 0) + 1 + if days < 45 { return .daily } + if days <= 180 { return .weekly } + return .monthly + case .daily: return .daily + case .weekly: return .weekly + case .monthly: return .monthly + } + } + + var selectedRow: CodexModelsRow? { + self.selectedModelID.flatMap { id in self.snapshot.rows.first { $0.id == id } } + } + + var tableRows: [CodexModelsTableRow] { + self.snapshot.rows.map { row in + CodexModelsTableRow(row: row, metric: self.metric, share: self.snapshot.share(of: row, metric: self.metric)) + } + } + + var visibleTableRows: [CodexModelsTableRow] { + let needle = self.searchText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + let filtered = needle.isEmpty ? self.tableRows : self.tableRows.filter { tableRow in + let row = tableRow.row + return row.displayName.lowercased().contains(needle) + || row.id.lowercased().contains(needle) + || row.rawAliases.contains { $0.lowercased().contains(needle) } + } + return filtered.sorted(using: self.sortOrder) + } + + var selectedModelIsHiddenBySearch: Bool { + guard let selectedModelID else { return false } + return !self.visibleTableRows.contains { $0.id == selectedModelID } + } + + var costMetricIsAvailable: Bool { + self.snapshot.cost.pricedTokens > 0 + } + + var rankedRows: [CodexModelsRow] { + self.snapshot.rows.sorted { + let lhs = $0.metricValue(self.metric) + let rhs = $1.metricValue(self.metric) + if lhs != rhs { return lhs > rhs } + return $0.id < $1.id + } + } + + var rankingGroups: [CodexModelsVisualGroup] { + let rows = self.rankedRows + return self.visualGroups( + rows: rows, + canonicalLimit: rows.count > 8 ? 7 : 8, + otherID: "ranking-other") + } + + var concentrationGroups: [CodexModelsVisualGroup] { + self.concentrationData.groups + } + + var concentrationData: CodexModelsConcentrationData { + let rows = self.rankedRows + let total = self.snapshot.totalValue(self.metric) + let topTwoValue = rows.prefix(2).reduce(0) { $0 + $1.metricValue(self.metric) } + return CodexModelsConcentrationData( + groups: self.visualGroups(rows: rows, canonicalLimit: 5, otherID: "concentration-other"), + topTwoShare: total == 0 ? 0 : topTwoValue / total) + } + + var timeline: [CodexModelsDailyBucket] { + self.timelineData.buckets + } + + var timelinePresentation: CodexModelsTimelinePresentation { + self.timelineData.presentation + } + + var selectedBucket: CodexModelsDailyBucket? { + self.timelineData.selectedBucket + } + + var timelineData: CodexModelsTimelineData { + let source = self.selectedModelID.flatMap { self.snapshot.dailyByModel[$0] } ?? self.snapshot.daily + let buckets = Self.rebucket(source, granularity: self.granularity) + let plottedBuckets = self.metric == .knownCost + ? buckets.filter { $0.cost.pricedTokens > 0 } + : buckets + let presentation: CodexModelsTimelinePresentation = plottedBuckets.count < 3 + ? .sparse(observedBuckets: plottedBuckets.count) + : .trend + let selectedBucket = self.selectedBucketStart.flatMap { start in + buckets.first { $0.day == start } + } + let observationEnd = min(self.snapshot.generatedAt, self.snapshot.currentInterval.end) + let completedPlottedBuckets = plottedBuckets.filter { $0.effectiveInterval.end <= observationEnd } + let inProgressBucket = plottedBuckets.last { bucket in + bucket.effectiveInterval.start <= observationEnd && bucket.effectiveInterval.end > observationEnd + } + let peakBucket = completedPlottedBuckets.max { lhs, rhs in + self.timelineMetricValue(lhs) < self.timelineMetricValue(rhs) + } + let changeFromPeak: Double? = if completedPlottedBuckets.count >= 2, + let peakBucket, + self.timelineMetricValue(peakBucket) > 0, + let latest = completedPlottedBuckets.last, + self.timelineMetricValue(latest) < self.timelineMetricValue(peakBucket) + { + (self.timelineMetricValue(latest) - self.timelineMetricValue(peakBucket)) + / self.timelineMetricValue(peakBucket) + } else { + nil + } + return CodexModelsTimelineData( + buckets: buckets, + plottedBuckets: plottedBuckets, + completedPlottedBuckets: completedPlottedBuckets, + inProgressBucket: inProgressBucket, + peakBucket: peakBucket, + presentation: presentation, + selectedBucket: selectedBucket, + changeFromPeak: changeFromPeak) + } + + var topModel: CodexModelsRow? { + self.snapshot.totalValue(self.metric) > 0 ? self.rankedRows.first : nil + } + + func setMetric(_ metric: CodexModelsMetric) { + guard metric != .knownCost || self.costMetricIsAvailable else { return } + guard self.metric != metric else { return } + self.metric = metric + self.sortOrder = Self.defaultSortOrder(metric: metric) + } + + func reconcileMetric(costDisplayEnabled: Bool) { + guard self.metric == .knownCost, + !costDisplayEnabled || !self.costMetricIsAvailable + else { return } + self.metric = .tokens + self.sortOrder = Self.defaultSortOrder(metric: .tokens) + } + + func setGranularityPreference(_ preference: CodexModelsGranularityPreference) { + guard self.granularityPreference != preference else { return } + self.granularityPreference = preference + self.selectedBucketStart = nil + } + + func replaceSnapshot(_ snapshot: CodexModelsAnalyticsSnapshot) { + let selectedModelID = self.selectedModelID + let selectedBucketStart = self.selectedBucketStart + self.snapshot = snapshot + self.reconcileMetric(costDisplayEnabled: true) + self.selectedModelID = selectedModelID + .flatMap { id in snapshot.rows.contains(where: { $0.id == id }) ? id : nil } + self.selectedBucketStart = selectedBucketStart.flatMap { start in + self.timeline.contains(where: { $0.day == start }) ? start : nil + } + } + + func updateLayout(width: CGFloat) { + self.layout = if width >= CodexModelsDashboardTokens.Width.wide { + .wide + } else if width >= CodexModelsDashboardTokens.Width.medium { + .medium + } else { + .compact + } + } + + func selectModel(_ canonicalID: String?) { + self.selectedModelID = canonicalID + if let selectedBucketStart, + !self.timeline.contains(where: { $0.day == selectedBucketStart }) + { + self.selectedBucketStart = nil + } + } + + func exportRows(_ scope: CodexModelsExportScope) -> [CodexModelsRow] { + switch scope { + case .visible: self.visibleTableRows.map(\.row) + case .all: self.snapshot.rows + case .selected: self.selectedRow.map { [$0] } ?? [] + } + } + + func clearSelection() { + self.selectedBucketStart = nil + self.selectedModelID = nil + } + + private func visualGroups( + rows: [CodexModelsRow], + canonicalLimit: Int, + otherID: String) -> [CodexModelsVisualGroup] + { + let total = self.snapshot.totalValue(self.metric) + let visible = Array(rows.prefix(canonicalLimit)) + var groups = visible.map { row in + let value = row.metricValue(self.metric) + return CodexModelsVisualGroup( + id: row.id, + canonicalModelID: row.id, + displayName: row.displayName, + value: value, + share: total == 0 ? 0 : value / total, + modelCount: 1, + rows: [row]) + } + let remainder = Array(rows.dropFirst(canonicalLimit)) + if !remainder.isEmpty { + let value = remainder.reduce(0) { $0 + $1.metricValue(self.metric) } + groups.append(CodexModelsVisualGroup( + id: otherID, + canonicalModelID: nil, + displayName: L("codex_models_other_count", remainder.count), + value: value, + share: total == 0 ? 0 : value / total, + modelCount: remainder.count, + rows: remainder)) + } + return groups + } + + private func timelineMetricValue(_ bucket: CodexModelsDailyBucket) -> Double { + switch self.metric { + case .tokens: Double(bucket.tokens) + case .knownCost: NSDecimalNumber(decimal: bucket.cost.knownAmount).doubleValue + case .sessionReferences: Double(bucket.sessionReferences) + } + } + + private static func defaultSortOrder( + metric: CodexModelsMetric) -> [KeyPathComparator] + { + switch metric { + case .tokens: [KeyPathComparator(\CodexModelsTableRow.tokensSort, order: .reverse)] + case .knownCost: [KeyPathComparator(\CodexModelsTableRow.costSort, order: .reverse)] + case .sessionReferences: + [KeyPathComparator(\CodexModelsTableRow.sessionReferencesSort, order: .reverse)] + } + } + + private static func rebucket( + _ source: [CodexModelsDailyBucket], + granularity: CodexModelsGranularity) -> [CodexModelsDailyBucket] + { + guard granularity != .daily else { return source } + var calendar = Calendar.autoupdatingCurrent + calendar.timeZone = .autoupdatingCurrent + let component: Calendar.Component = granularity == .weekly ? .weekOfYear : .month + let groups = Dictionary(grouping: source) { bucket in + calendar.dateInterval(of: component, for: bucket.day)?.start ?? bucket.day + } + return groups.map { start, buckets in + let interval = calendar.dateInterval(of: component, for: start) + ?? DateInterval(start: start, end: buckets.map(\.effectiveInterval.end).max() ?? start) + return CodexModelsDailyBucket( + day: start, + interval: interval, + tokens: buckets.reduce(0) { $0 + $1.tokens }, + sessionIDs: Array(Set(buckets.flatMap(\.sessionIDs))).sorted(), + sessionReferenceIDs: Array(Set(buckets.flatMap(\.sessionReferenceIDs))).sorted(), + cost: buckets.reduce(CodexModelsCost.zero) { $0.adding($1.cost) }) + } + .sorted { $0.day < $1.day } + } +} diff --git a/Sources/CodexBar/CodexModelsDashboardStyle.swift b/Sources/CodexBar/CodexModelsDashboardStyle.swift new file mode 100644 index 0000000000..003480b216 --- /dev/null +++ b/Sources/CodexBar/CodexModelsDashboardStyle.swift @@ -0,0 +1,116 @@ +import CodexBarCore +import SwiftUI + +enum CodexModelsDashboardTokens { + enum Spacing { + static let xSmall: CGFloat = 4 + static let small: CGFloat = 6 + static let medium: CGFloat = 8 + static let large: CGFloat = 12 + static let xLarge: CGFloat = 16 + static let outer: CGFloat = 20 + static let xxLarge: CGFloat = 24 + } + + enum Radius { + static let surface: CGFloat = 10 + static let chartBar: CGFloat = 4 + static let tooltip: CGFloat = 6 + } + + enum Width { + static let wide: CGFloat = 1280 + static let medium: CGFloat = 960 + static let inspectorMinimum: CGFloat = 280 + static let inspectorIdeal: CGFloat = 320 + static let inspectorMaximum: CGFloat = 420 + static let wideAnalysisTrailing: CGFloat = 590 + static let mediumConcentration: CGFloat = 480 + } + + enum Height { + static let wideSummary: CGFloat = 112 + static let mediumSummaryCell: CGFloat = 100 + static let compactSummaryCell: CGFloat = 88 + static let wideAnalyticsHeader: CGFloat = 54 + static let wideAnalysisBody: CGFloat = 334 + static let wideTimeline: CGFloat = 200 + static let wideConcentration: CGFloat = 134 + static let mediumRanking: CGFloat = 340 + static let mediumAnalysisLower: CGFloat = 212 + static let wideTableToolbar: CGFloat = 56 + static let table: CGFloat = 300 + } + + static func sectionSpacing(for layout: CodexModelsDashboardModel.Layout) -> CGFloat { + layout == .wide ? Spacing.medium : Spacing.xLarge + } +} + +private struct CodexModelsDashboardSurface: ViewModifier { + func body(content: Content) -> some View { + content + .background(Color(nsColor: .controlBackgroundColor)) + .clipShape(RoundedRectangle(cornerRadius: CodexModelsDashboardTokens.Radius.surface, style: .continuous)) + .overlay { + RoundedRectangle(cornerRadius: CodexModelsDashboardTokens.Radius.surface, style: .continuous) + .stroke(Color(nsColor: .separatorColor).opacity(0.55), lineWidth: 1) + } + } +} + +extension View { + func codexModelsDashboardSurface() -> some View { + self.modifier(CodexModelsDashboardSurface()) + } +} + +struct CodexModelsTableColumnProfile: Equatable { + let showsSessionReferences: Bool + let showsDelta: Bool + let showsKnownCost: Bool + let showsColumnMenu: Bool + + static func resolve( + layout: CodexModelsDashboardModel.Layout, + metric: CodexModelsMetric, + showsEstimatedCost: Bool) -> Self + { + let showsKnownCost = showsEstimatedCost && metric != .knownCost + switch layout { + case .wide: + return Self( + showsSessionReferences: true, + showsDelta: true, + showsKnownCost: showsKnownCost, + showsColumnMenu: true) + case .medium: + return Self( + showsSessionReferences: false, + showsDelta: true, + showsKnownCost: showsKnownCost, + showsColumnMenu: false) + case .compact: + return Self( + showsSessionReferences: false, + showsDelta: false, + showsKnownCost: showsKnownCost, + showsColumnMenu: false) + } + } +} + +enum CodexModelsFreshnessState: Equatable { + case current + case refreshing + case stale(String) +} + +enum CodexModelsSelectionPresentation: Equatable { + case inspector + case sheet + + static func resolve(layout: CodexModelsDashboardModel.Layout) -> Self { + layout == .compact ? .sheet : .inspector + } +} diff --git a/Sources/CodexBar/CodexModelsDashboardTable.swift b/Sources/CodexBar/CodexModelsDashboardTable.swift new file mode 100644 index 0000000000..1ab174f557 --- /dev/null +++ b/Sources/CodexBar/CodexModelsDashboardTable.swift @@ -0,0 +1,1102 @@ +import AppKit +import CodexBarCore +import SwiftUI +import UniformTypeIdentifiers + +enum CodexModelsExportScope { + case visible + case all + case selected +} + +enum CodexModelsPresentationCSVExporter { + private static let protectedScopeID = "hidden_workspace" + + static func export( + snapshot: CodexModelsAnalyticsSnapshot, + rows: [CodexModelsRow], + metric: CodexModelsMetric, + hidePersonalInfo: Bool) -> String + { + guard hidePersonalInfo else { + return CodexModelsCSVExporter.export(snapshot: snapshot, rows: rows, metric: metric) + } + + let protectedRows = rows.map(Self.protectedRow) + guard let protectedSnapshot = Self.protectedSnapshot(snapshot) else { return "" } + return CodexModelsCSVExporter.export( + snapshot: protectedSnapshot, + rows: protectedRows, + metric: metric) + } + + static func protectedSnapshot( + _ snapshot: CodexModelsAnalyticsSnapshot) -> CodexModelsAnalyticsSnapshot? + { + let copy = ProtectedSnapshot( + snapshot: snapshot, + rows: snapshot.rows.map(Self.protectedRow)) + guard let data = try? PropertyListEncoder().encode(copy) else { return nil } + return try? PropertyListDecoder().decode(CodexModelsAnalyticsSnapshot.self, from: data) + } + + private static func protectedRow(_ row: CodexModelsRow) -> CodexModelsRow { + CodexModelsRow( + id: row.id, + displayName: row.displayName, + rawAliases: row.rawAliases, + inputTokens: row.inputTokens, + cachedInputTokens: row.cachedInputTokens, + outputTokens: row.outputTokens, + reasoningTokens: row.reasoningTokens, + totalTokens: row.totalTokens, + share: row.share, + sessionReferences: row.sessionReferences, + cost: row.cost, + previousTotalTokens: row.previousTotalTokens, + previousCost: row.previousCost, + previousSessionReferences: row.previousSessionReferences, + associatedSessionIDs: nil, + tokenComparison: row.tokenComparison, + costComparison: row.costComparison, + sessionReferenceComparison: row.sessionReferenceComparison) + } + + private struct ProtectedSnapshot: Encodable { + let scopeID: String? = CodexModelsPresentationCSVExporter.protectedScopeID + let generatedAt: Date + let indexRevision: String + let currentInterval: DateInterval + let previousInterval: DateInterval + let currentIsComplete: Bool? + let previousIsComplete: Bool? + let totalTokens: Int64 + let cost: CodexModelsCost + let activeModelCount: Int + let previousActiveModelCount: Int? + let newlyActiveModelCount: Int? + let uniqueSessionCount: Int + let sessionReferenceTotal: Int + let previousSessionReferenceTotal: Int? + let tokenComparison: CodexModelsComparison + let costComparison: CodexModelsComparison + let sessionReferenceComparison: CodexModelsComparison? + let rows: [CodexModelsRow] + let daily: [CodexModelsDailyBucket] + let dailyByModel: [String: [CodexModelsDailyBucket]] + let diagnostics: CodexModelsRolloutDiagnostics + + init(snapshot: CodexModelsAnalyticsSnapshot, rows: [CodexModelsRow]) { + self.generatedAt = snapshot.generatedAt + self.indexRevision = snapshot.indexRevision + self.currentInterval = snapshot.currentInterval + self.previousInterval = snapshot.previousInterval + self.currentIsComplete = snapshot.currentIsComplete + self.previousIsComplete = snapshot.previousIsComplete + self.totalTokens = snapshot.totalTokens + self.cost = snapshot.cost + self.activeModelCount = snapshot.activeModelCount + self.previousActiveModelCount = snapshot.previousActiveModelCount + self.newlyActiveModelCount = snapshot.newlyActiveModelCount + self.uniqueSessionCount = snapshot.uniqueSessionCount + self.sessionReferenceTotal = snapshot.sessionReferenceTotal + self.previousSessionReferenceTotal = snapshot.previousSessionReferenceTotal + self.tokenComparison = snapshot.tokenComparison + self.costComparison = snapshot.costComparison + self.sessionReferenceComparison = snapshot.sessionReferenceComparison + self.rows = rows + self.daily = snapshot.daily + self.dailyByModel = snapshot.dailyByModel + self.diagnostics = snapshot.diagnostics + } + } +} + +struct CodexModelsCSVDocument: FileDocument { + static var readableContentTypes: [UTType] { + [.commaSeparatedText] + } + + let csv: String + + init(csv: String) { + self.csv = csv + } + + init(configuration: ReadConfiguration) throws { + let data = configuration.file.regularFileContents ?? Data() + self.csv = String(bytes: data, encoding: .utf8) ?? "" + } + + func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { + FileWrapper(regularFileWithContents: Data(self.csv.utf8)) + } +} + +private struct CodexModelsSearchField: NSViewRepresentable { + @Binding var text: String + @Binding var isFocused: Bool + + func makeCoordinator() -> Coordinator { + Coordinator(parent: self) + } + + func makeNSView(context: Context) -> NSSearchField { + let searchField = NSSearchField() + searchField.placeholderString = L("codex_models_search_models") + searchField.sendsSearchStringImmediately = true + searchField.delegate = context.coordinator + return searchField + } + + func updateNSView(_ searchField: NSSearchField, context: Context) { + context.coordinator.parent = self + if searchField.stringValue != self.text { searchField.stringValue = self.text } + if self.isFocused, searchField.window?.firstResponder !== searchField.currentEditor() { + DispatchQueue.main.async { searchField.window?.makeFirstResponder(searchField) } + } + } + + @MainActor + final class Coordinator: NSObject, NSSearchFieldDelegate { + var parent: CodexModelsSearchField + + init(parent: CodexModelsSearchField) { + self.parent = parent + } + + func controlTextDidChange(_ notification: Notification) { + guard let searchField = notification.object as? NSSearchField else { return } + self.parent.text = searchField.stringValue + } + + func controlTextDidBeginEditing(_ notification: Notification) { + self.parent.isFocused = true + } + + func controlTextDidEndEditing(_ notification: Notification) { + self.parent.isFocused = false + } + } +} + +enum CodexModelsFormatters { + struct CostText { + let primary: String + let secondary: String? + let accessibility: String + } + + static func compactInteger(_ value: Int64) -> String { + value.formatted(.number.notation(.compactName).precision(.fractionLength(0...1))) + } + + static func compactNumber(_ value: Double) -> String { + value.formatted(.number.notation(.compactName).precision(.fractionLength(0...1))) + } + + static func currency(_ value: Decimal, code: String, compact: Bool = false) -> String { + let locale = code.uppercased() == "USD" ? Locale(identifier: "en_US") : Locale.current + if compact { + let magnitude = abs(NSDecimalNumber(decimal: value).doubleValue) + let scale: Decimal + let suffix: String + if magnitude >= 1_000_000_000 { + scale = Decimal(1_000_000_000) + suffix = L("codex_models_compact_billions_suffix") + } else if magnitude >= 1_000_000 { + scale = Decimal(1_000_000) + suffix = L("codex_models_compact_millions_suffix") + } else if magnitude >= 1000 { + scale = Decimal(1000) + suffix = L("codex_models_compact_thousands_suffix") + } else { + scale = Decimal(1) + suffix = "" + } + return (value / scale).formatted( + .currency(code: code) + .precision(.fractionLength(0...1)) + .locale(locale)) + suffix + } + return value.formatted( + .currency(code: code) + .precision(.fractionLength(2)) + .locale(locale)) + } + + static func percentage(_ value: Double) -> String { + if value > 0, value < 0.001 { return "<0.1%" } + return value.formatted(.percent.precision(.fractionLength(0...1))) + } + + static func compactComparison(_ comparison: CodexModelsComparison) -> String { + switch comparison { + case .unavailable: "—" + case .new: L("codex_models_new") + case .ended: L("codex_models_ended") + case .unchanged: "0%" + case let .percent(value): + if value > 0, value < 0.001 { + "+<0.1%" + } else if value < 0, value > -0.001 { + "−<0.1%" + } else { + value.formatted(.percent.precision(.fractionLength(0...1)).sign(strategy: .always())) + } + } + } + + static func comparisonSentence(_ comparison: CodexModelsComparison) -> String { + switch comparison { + case .unavailable: return L("codex_models_previous_period_unavailable") + case .new: return L("codex_models_new_in_period") + case .ended: return L("codex_models_no_usage_in_period") + case .unchanged: return L("codex_models_no_change_from_previous_period") + case let .percent(value): + let percentage = value.formatted( + .percent.precision(.fractionLength(0...1)).sign(strategy: .always())) + return L("codex_models_change_from_previous_period", percentage) + } + } + + static func cost(_ cost: CodexModelsCost, usageTokens: Int64) -> CostText { + guard usageTokens > 0 else { + return CostText(primary: "—", secondary: nil, accessibility: L("codex_models_no_usage")) + } + guard cost.pricedTokens > 0 else { + return CostText( + primary: L("Unavailable"), + secondary: L("codex_models_no_applicable_price"), + accessibility: L("codex_models_cost_unavailable")) + } + let amount = self.currency(cost.knownAmount, code: cost.currencyCode) + if cost.unpricedTokens > 0 { + let coverage = cost.coverage >= 0.999 ? ">99.9%" : self.percentage(cost.coverage) + return CostText( + primary: amount + "+", + secondary: L("codex_models_partial_priced", coverage), + accessibility: L("codex_models_known_cost_partial_accessibility", amount, coverage)) + } + return CostText( + primary: amount, + secondary: nil, + accessibility: L("codex_models_known_cost_fully_priced_accessibility", amount)) + } + + static func metricValue(_ row: CodexModelsRow, metric: CodexModelsMetric) -> String { + switch metric { + case .tokens: self.compactInteger(row.totalTokens) + case .knownCost: self.cost(row.cost, usageTokens: row.totalTokens).primary + case .sessionReferences: row.sessionReferences.formatted() + } + } + + static func fullMetricValue(_ row: CodexModelsRow, metric: CodexModelsMetric) -> String { + switch metric { + case .tokens: L("codex_models_token_count", row.totalTokens.formatted()) + case .knownCost: self.cost(row.cost, usageTokens: row.totalTokens).accessibility + case .sessionReferences: L("codex_models_session_reference_count", row.sessionReferences.formatted()) + } + } + + static func previousMetricValue(_ row: CodexModelsRow, metric: CodexModelsMetric) -> String { + switch metric { + case .tokens: + return row.previousTotalTokens.map { L("codex_models_token_count", $0.formatted()) } ?? "—" + case .knownCost: + guard let cost = row.previousCost else { return "—" } + return self.cost( + cost, + usageTokens: cost.pricedTokens + cost.unpricedTokens).primary + case .sessionReferences: + return row.previousSessionReferences.map { + L("codex_models_session_reference_count", $0.formatted()) + } ?? "—" + } + } + + static func metricValue(_ bucket: CodexModelsDailyBucket, metric: CodexModelsMetric) -> String { + switch metric { + case .tokens: L("codex_models_token_count", bucket.tokens.formatted()) + case .knownCost: self.cost(bucket.cost, usageTokens: bucket.tokens).accessibility + case .sessionReferences: L("codex_models_session_reference_count", bucket.sessionReferences.formatted()) + } + } + + static func interval(_ interval: DateInterval) -> String { + let end = interval.end.addingTimeInterval(-0.001) + if Calendar.current.isDate(interval.start, inSameDayAs: end) { + return interval.start.formatted(date: .abbreviated, time: .omitted) + } + let startText = interval.start.formatted(date: .abbreviated, time: .omitted) + let endText = end.formatted(date: .abbreviated, time: .omitted) + return "\(startText) – \(endText)" + } +} + +struct CodexModelsPrecisionTable: View { + let model: CodexModelsDashboardModel + let showsEstimatedCost: Bool + let freshness: CodexModelsFreshnessState + let showAssociatedSessions: ([String]) -> Void + let requestExport: (CodexModelsExportScope) -> Void + + @State private var searchFocused = false + + var body: some View { + @Bindable var model = self.model + let visibleRows = model.visibleTableRows + let columnProfile = CodexModelsTableColumnProfile.resolve( + layout: model.layout, + metric: model.metric, + showsEstimatedCost: self.showsEstimatedCost) + + VStack(spacing: 0) { + CodexModelsTableToolbar( + searchText: $model.searchText, + searchFocused: self.$searchFocused, + optionalColumns: $model.optionalColumns, + layout: model.layout, + showsColumnsMenu: columnProfile.showsColumnMenu, + visibleRowCount: visibleRows.count, + totalRowCount: model.snapshot.rows.count, + hasSelectedRow: model.selectedRow != nil, + selectedModelIsHiddenBySearch: model.selectedModelIsHiddenBySearch, + clearSearch: { model.searchText = "" }, + requestExport: self.requestExport) + Divider() + CodexModelsTableContent( + model: self.model, + rows: visibleRows, + showsEstimatedCost: self.showsEstimatedCost, + showAssociatedSessions: self.showAssociatedSessions, + requestExport: self.requestExport) + Divider() + CodexModelsTableFooter( + snapshot: model.snapshot, + metric: model.metric, + showsEstimatedCost: self.showsEstimatedCost, + freshness: self.freshness) + } + .codexModelsDashboardSurface() + .accessibilityLabel(L("codex_models_precision_table")) + .background { + Button(L("codex_models_focus_table_search")) { self.searchFocused = true } + .keyboardShortcut("f", modifiers: .command) + .frame(width: 0, height: 0) + .opacity(0) + .accessibilityHidden(true) + } + } +} + +private struct CodexModelsTableToolbar: View { + @Binding var searchText: String + @Binding var searchFocused: Bool + @Binding var optionalColumns: Set + + let layout: CodexModelsDashboardModel.Layout + let showsColumnsMenu: Bool + let visibleRowCount: Int + let totalRowCount: Int + let hasSelectedRow: Bool + let selectedModelIsHiddenBySearch: Bool + let clearSearch: () -> Void + let requestExport: (CodexModelsExportScope) -> Void + + var body: some View { + ViewThatFits(in: .horizontal) { + HStack(spacing: 10) { + self.searchField + self.searchStatus + Spacer(minLength: 8) + if self.showsColumnsMenu { self.columnsMenu } + self.exportMenu + } + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 10) { + self.searchField + self.searchStatus + Spacer(minLength: 0) + } + HStack(spacing: 8) { + Spacer(minLength: 0) + if self.showsColumnsMenu { self.columnsMenu } + self.exportMenu + } + } + } + .controlSize(.small) + .padding(.horizontal, CodexModelsDashboardTokens.Spacing.large) + .padding(.vertical, CodexModelsDashboardTokens.Spacing.medium) + .frame( + minHeight: self.layout == .wide + ? CodexModelsDashboardTokens.Height.wideTableToolbar + : nil) + } + + private var searchField: some View { + CodexModelsSearchField(text: self.$searchText, isFocused: self.$searchFocused) + .frame(minWidth: 180, idealWidth: 230, maxWidth: 230, minHeight: 23) + .accessibilityLabel(L("codex_models_search_table")) + } + + private var searchScopeText: String { + if self.selectedModelIsHiddenBySearch { + return L("codex_models_selected_hidden_by_search") + } + let count = self.searchText.isEmpty + ? L("codex_models_model_count", self.totalRowCount) + : L("codex_models_visible_of_total", self.visibleRowCount, self.totalRowCount) + return L("codex_models_search_scope", count) + } + + private var searchStatus: some View { + HStack(spacing: CodexModelsDashboardTokens.Spacing.small) { + Text(self.searchScopeText) + .foregroundStyle(.secondary) + .monospacedDigit() + .lineLimit(1) + if self.selectedModelIsHiddenBySearch { + Button(L("codex_models_clear_search"), action: self.clearSearch).buttonStyle(.link) + } + } + .font(.caption) + } + + private var columnsMenu: some View { + Menu(L("codex_models_columns")) { + ForEach(CodexModelsOptionalColumn.allCases) { column in + Toggle(column.title, isOn: Binding( + get: { self.optionalColumns.contains(column) }, + set: { visible in + if visible { + self.optionalColumns.insert(column) + } else { + self.optionalColumns.remove(column) + } + })) + } + } + } + + private var exportMenu: some View { + Menu(L("codex_models_export")) { + Button(L("codex_models_visible_rows")) { self.requestExport(.visible) } + .disabled(self.visibleRowCount == 0) + Button(L("codex_models_all_rows")) { self.requestExport(.all) } + .disabled(self.totalRowCount == 0) + Button(L("codex_models_selected_model")) { self.requestExport(.selected) } + .disabled(!self.hasSelectedRow) + } + } +} + +private struct CodexModelsTableFooter: View { + let snapshot: CodexModelsAnalyticsSnapshot + let metric: CodexModelsMetric + let showsEstimatedCost: Bool + let freshness: CodexModelsFreshnessState + + var body: some View { + let cost = CodexModelsFormatters.cost(self.snapshot.cost, usageTokens: self.snapshot.totalTokens) + + VStack(spacing: 0) { + HStack(spacing: 12) { + Text(L("codex_models_all_model_count", self.snapshot.rows.count)).fontWeight(.semibold) + Spacer() + Text(self.snapshotValue).monospacedDigit() + Text("100%").monospacedDigit() + Text(CodexModelsFormatters.compactComparison(self.snapshotComparison)) + .monospacedDigit() + .help(CodexModelsFormatters.comparisonSentence(self.snapshotComparison)) + if self.showsEstimatedCost, self.metric != .knownCost { + Text(cost.primary).monospacedDigit() + } + } + .padding(8) + Divider() + HStack(spacing: 6) { + self.freshnessIndicator + self.freshnessText + Text(L("codex_models_local_logs_source")) + Spacer() + if self.showsEstimatedCost { + Text(cost.secondary ?? L("codex_models_fully_priced_percent")) + } + Text(L("codex_models_totals_reconcile")) + } + .font(.caption2) + .foregroundStyle(.secondary) + .padding(8) + } + .font(.caption) + } + + @ViewBuilder + private var freshnessIndicator: some View { + switch self.freshness { + case .current: + Image(systemName: "circle.fill").font(.system(size: 7)).foregroundStyle(.green) + case .refreshing: + ProgressView().controlSize(.mini) + case .stale: + Image(systemName: "exclamationmark.triangle.fill").foregroundStyle(.orange) + } + } + + @ViewBuilder + private var freshnessText: some View { + switch self.freshness { + case .current: + Text(L( + "codex_models_updated", + self.snapshot.generatedAt.formatted( + .relative(presentation: .named).locale(codexBarLocalizedLocale())))) + case .refreshing: + Text(L("codex_models_refreshing_last_snapshot")) + case let .stale(message): + Text(L("codex_models_showing_last_snapshot")).help(message) + } + } + + private var snapshotValue: String { + switch self.metric { + case .tokens: CodexModelsFormatters.compactInteger(self.snapshot.totalTokens) + case .knownCost: + CodexModelsFormatters.cost(self.snapshot.cost, usageTokens: self.snapshot.totalTokens).primary + case .sessionReferences: self.snapshot.sessionReferenceTotal.formatted() + } + } + + private var snapshotComparison: CodexModelsComparison { + self.snapshot.comparison(self.metric) + } +} + +private struct CodexModelsTableContent: View { + let model: CodexModelsDashboardModel + let rows: [CodexModelsTableRow] + let showsEstimatedCost: Bool + let showAssociatedSessions: ([String]) -> Void + let requestExport: (CodexModelsExportScope) -> Void + + var body: some View { + self.responsiveTable + .overlay { + if !self.model.searchText.isEmpty, self.rows.isEmpty { + ContentUnavailableView { + Label(L("codex_models_no_matching_models"), systemImage: "magnifyingglass") + } description: { + Text(L("codex_models_search_empty_description")) + } actions: { + Button(L("codex_models_clear_search")) { self.model.searchText = "" } + } + .background(.background.opacity(0.94)) + } + } + } + + @ViewBuilder + private var responsiveTable: some View { + if #available(macOS 14.4, *) { + let profile = CodexModelsTableColumnProfile.resolve( + layout: self.model.layout, + metric: self.model.metric, + showsEstimatedCost: self.showsEstimatedCost) + self.table( + showSessionReferences: profile.showsSessionReferences, + showDelta: profile.showsDelta, + showCost: profile.showsKnownCost) + } else { + self.compatibilityTable + } + } + + @available(macOS 14.4, *) + private func table( + showSessionReferences: Bool, + showDelta: Bool, + showCost: Bool) -> some View + { + @Bindable var model = self.model + return Table(self.rows, selection: $model.selectedModelID, sortOrder: $model.sortOrder) { + TableColumn(L("codex_models_model"), value: \CodexModelsTableRow.modelSort) { tableRow in + self.modelCell(tableRow.row) + } + .width(min: 160, ideal: 235) + + switch model.metric { + case .tokens: + TableColumn(L("codex_models_tokens"), value: \CodexModelsTableRow.tokensSort) { tableRow in + self.valueCell(CodexModelsFormatters.compactInteger(tableRow.row.totalTokens), row: tableRow.row) + .help(L("codex_models_token_count", tableRow.row.totalTokens.formatted())) + } + .width(min: 90, ideal: 115) + case .knownCost: + TableColumn(L("codex_models_known_cost"), value: \CodexModelsTableRow.costSort) { tableRow in + self.costCell(tableRow.row) + } + .width(min: 115, ideal: 145) + case .sessionReferences: + TableColumn( + L("codex_models_session_refs"), + value: \CodexModelsTableRow.sessionReferencesSort) + { tableRow in + self.valueCell(tableRow.row.sessionReferences.formatted(), row: tableRow.row) + } + .width(min: 90, ideal: 110) + } + + TableColumn(L("codex_models_share"), value: \CodexModelsTableRow.shareSort) { tableRow in + self.valueCell(CodexModelsFormatters.percentage(tableRow.share), row: tableRow.row) + .help(self.shareHelp) + } + .width(min: 70, ideal: 86) + + if showSessionReferences, + model.metric != .sessionReferences, + model.optionalColumns.contains(.sessionReferences) + { + TableColumn( + L("codex_models_session_refs"), + value: \CodexModelsTableRow.sessionReferencesSort) + { tableRow in + self.valueCell(tableRow.row.sessionReferences.formatted(), row: tableRow.row) + } + .width(min: 86, ideal: 105) + } + if showDelta, model.optionalColumns.contains(.delta) { + TableColumn(L("codex_models_delta_vs_prior"), value: \CodexModelsTableRow.deltaSort) { tableRow in + self.valueCell( + CodexModelsFormatters.compactComparison(tableRow.row.comparison(model.metric)), + row: tableRow.row) + .help(CodexModelsFormatters.comparisonSentence(tableRow.row.comparison(model.metric))) + } + .width(min: 72, ideal: 88) + } + if showCost, + self.showsEstimatedCost, + model.metric != .knownCost + { + TableColumn(L("codex_models_known_cost"), value: \CodexModelsTableRow.costSort) { tableRow in + self.costCell(tableRow.row) + } + .width(min: 115, ideal: 145) + } + } + .tableStyle(.inset) + .frame(minHeight: CodexModelsDashboardTokens.Height.table) + .contextMenu(forSelectionType: String.self) { selectedIDs in + self.rowContextMenu(selectedIDs) + } + } + + @ViewBuilder + private var compatibilityTable: some View { + switch self.model.metric { + case .tokens: + self.compatibilityTokensTable + case .knownCost: + self.compatibilityCostTable + case .sessionReferences: + self.compatibilitySessionTable + } + } + + private var compatibilityTokensTable: some View { + @Bindable var model = self.model + return Table(self.rows, selection: $model.selectedModelID, sortOrder: $model.sortOrder) { + TableColumn(L("codex_models_model"), value: \CodexModelsTableRow.modelSort) { self.modelCell($0.row) } + TableColumn(L("codex_models_tokens"), value: \CodexModelsTableRow.tokensSort) { + self.valueCell(CodexModelsFormatters.compactInteger($0.row.totalTokens), row: $0.row) + } + TableColumn(L("codex_models_share"), value: \CodexModelsTableRow.shareSort) { + self.valueCell(CodexModelsFormatters.percentage($0.share), row: $0.row) + } + } + .tableStyle(.inset) + .frame(minHeight: 300) + .contextMenu(forSelectionType: String.self) { selectedIDs in + self.rowContextMenu(selectedIDs) + } + } + + private var compatibilityCostTable: some View { + @Bindable var model = self.model + return Table(self.rows, selection: $model.selectedModelID, sortOrder: $model.sortOrder) { + TableColumn(L("codex_models_model"), value: \CodexModelsTableRow.modelSort) { self.modelCell($0.row) } + TableColumn(L("codex_models_known_cost"), value: \CodexModelsTableRow.costSort) { self.costCell($0.row) } + TableColumn(L("codex_models_share"), value: \CodexModelsTableRow.shareSort) { + self.valueCell(CodexModelsFormatters.percentage($0.share), row: $0.row) + } + } + .tableStyle(.inset) + .frame(minHeight: 300) + .contextMenu(forSelectionType: String.self) { selectedIDs in + self.rowContextMenu(selectedIDs) + } + } + + private var compatibilitySessionTable: some View { + @Bindable var model = self.model + return Table(self.rows, selection: $model.selectedModelID, sortOrder: $model.sortOrder) { + TableColumn(L("codex_models_model"), value: \CodexModelsTableRow.modelSort) { self.modelCell($0.row) } + TableColumn(L("codex_models_session_refs"), value: \CodexModelsTableRow.sessionReferencesSort) { + self.valueCell($0.row.sessionReferences.formatted(), row: $0.row) + } + TableColumn(L("codex_models_share"), value: \CodexModelsTableRow.shareSort) { + self.valueCell(CodexModelsFormatters.percentage($0.share), row: $0.row) + } + } + .tableStyle(.inset) + .frame(minHeight: 300) + .contextMenu(forSelectionType: String.self) { selectedIDs in + self.rowContextMenu(selectedIDs) + } + } + + private func modelCell(_ row: CodexModelsRow) -> some View { + self.rowInteraction(row) { + HStack(spacing: 7) { + Text(row.displayName).lineLimit(1) + if self.model.topModel?.id == row.id { + Text(L("codex_models_top")) + .font(.caption2.weight(.semibold)) + .foregroundStyle(.secondary) + } + if row.rawAliases.count > 1 { + Text(L("codex_models_alias_count", row.rawAliases.count)) + .font(.caption2) + .foregroundStyle(.secondary) + } + Spacer(minLength: 0) + } + .help(L( + "codex_models_identity_aliases_help", + row.displayName, + row.id, + row.rawAliases.joined(separator: ", "))) + .accessibilityValue(self.model.selectedModelID == row.id ? L("codex_models_selected") : "") + } + } + + private func valueCell(_ value: String, row: CodexModelsRow) -> some View { + self.rowInteraction(row) { + Text(value).monospacedDigit().frame(maxWidth: .infinity, alignment: .trailing) + } + } + + private func costCell(_ row: CodexModelsRow) -> some View { + let cost = CodexModelsFormatters.cost(row.cost, usageTokens: row.totalTokens) + return self.rowInteraction(row) { + Text(cost.primary).monospacedDigit() + .frame(maxWidth: .infinity, alignment: .trailing) + .accessibilityLabel(cost.accessibility) + .help([cost.primary, cost.secondary].compactMap(\.self).joined(separator: " · ")) + } + } + + private func rowInteraction( + _ row: CodexModelsRow, + @ViewBuilder content: () -> some View) -> some View + { + content() + .frame(maxWidth: .infinity, minHeight: 22) + .contentShape(Rectangle()) + } + + @ViewBuilder + private func rowContextMenu(_ selectedIDs: Set) -> some View { + if let id = selectedIDs.first, + selectedIDs.count == 1, + let row = self.model.snapshot.rows.first(where: { $0.id == id }) + { + CodexModelsRowContextMenu( + row: row, + select: { self.model.selectModel(id) }, + showAssociatedSessions: self.showAssociatedSessions, + copy: self.copy, + export: { self.requestExport(.selected) }) + } + } + + private var shareHelp: String { + if self.model.metric == .knownCost, + self.model.snapshot.cost.unpricedTokens > 0 + { + return L("codex_models_share_known_cost_help") + } + return L("codex_models_share_of", self.model.metric.shareBasisTitle) + } + + private func copy(_ value: String) { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(value, forType: .string) + } +} + +private struct CodexModelsRowContextMenu: View { + let row: CodexModelsRow + let select: () -> Void + let showAssociatedSessions: ([String]) -> Void + let copy: (String) -> Void + let export: () -> Void + + var body: some View { + let sessionIDs = self.row.associatedSessionIDs ?? [] + Group { + Button(L("codex_models_show_details"), action: self.select) + Button(L("codex_models_show_associated_sessions")) { + self.select() + self.showAssociatedSessions(sessionIDs) + } + .disabled(sessionIDs.isEmpty) + Divider() + Button(L("codex_models_copy_canonical_model_id")) { + self.select() + self.copy(self.row.id) + } + Button(L("codex_models_copy_raw_aliases")) { + self.select() + self.copy(self.row.rawAliases.joined(separator: "\n")) + } + Divider() + Button(L("codex_models_export_selected_model")) { + self.select() + self.export() + } + } + .onAppear(perform: self.select) + } +} + +struct CodexModelsInspector: View { + let row: CodexModelsRow + let snapshot: CodexModelsAnalyticsSnapshot + let metric: CodexModelsMetric + let showsEstimatedCost: Bool + let close: () -> Void + let showAssociatedSessions: ([String]) -> Void + + @State private var showsPricingCalculation = false + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + self.header + .padding(.bottom, 14) + Divider() + self.usage + .padding(.vertical, 14) + if self.showsEstimatedCost { + Divider() + CodexModelsInspectorPricingSection( + cost: self.row.cost, + previousCost: self.row.previousCost, + usageTokens: self.row.totalTokens, + showsPricing: self.$showsPricingCalculation) + .padding(.vertical, 14) + } + Divider() + CodexModelsInspectorIdentitySection( + canonicalID: self.row.id, + aliases: self.row.rawAliases) + .padding(.vertical, 14) + Divider() + self.interpretation + .padding(.vertical, 14) + Divider() + self.actions + .padding(.vertical, 14) + Divider() + Text(L( + "codex_models_generated", + self.snapshot.generatedAt.formatted(date: .abbreviated, time: .shortened))) + .font(.caption2) + .foregroundStyle(.secondary) + .padding(.top, 12) + } + .padding(16) + } + } + + private var header: some View { + VStack(alignment: .leading, spacing: 6) { + HStack(alignment: .top) { + VStack(alignment: .leading, spacing: 3) { + Text(self.row.displayName).font(.title3.weight(.semibold)) + Text(L( + "codex_models_selected_model_interval", + CodexModelsFormatters.interval(self.snapshot.currentInterval))) + .font(.caption) + .foregroundStyle(.secondary) + } + Spacer() + Button(L("Close"), systemImage: "xmark", action: self.close).labelStyle(.iconOnly) + } + } + } + + private var usage: some View { + VStack(alignment: .leading, spacing: 8) { + self.sectionTitle(L("codex_models_usage_and_change")) + LabeledContent( + L("codex_models_current_metric", self.metric.localizedTitle), + value: CodexModelsFormatters.fullMetricValue(self.row, metric: self.metric)) + LabeledContent( + L("codex_models_previous_metric", self.metric.localizedTitle), + value: CodexModelsFormatters.previousMetricValue(self.row, metric: self.metric)) + LabeledContent( + L("codex_models_change"), + value: CodexModelsFormatters.compactComparison(self.row.comparison(self.metric))) + LabeledContent( + L("codex_models_share"), + value: CodexModelsFormatters.percentage(self.snapshot.share(of: self.row, metric: self.metric))) + LabeledContent(L("codex_models_session_refs"), value: self.row.sessionReferences.formatted()) + } + .monospacedDigit() + } + + private var interpretation: some View { + VStack(alignment: .leading, spacing: 8) { + self.sectionTitle(L("codex_models_interpretation")) + Text(self.interpretationLines.joined(separator: " ")) + .font(.caption) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + + private var actions: some View { + VStack(alignment: .leading, spacing: 8) { + self.sectionTitle(L("codex_models_actions")) + Button { + self.showAssociatedSessions(self.row.associatedSessionIDs ?? []) + } label: { Text(L("codex_models_show_associated_sessions")).frame(maxWidth: .infinity) } + .disabled(self.row.associatedSessionIDs?.isEmpty != false) + Button { self.copy(self.row.id) } label: { + Text(L("codex_models_copy_canonical_id")).frame(maxWidth: .infinity) + } + if self.showsEstimatedCost { + Button { self.showsPricingCalculation = true } label: { + Text(L("codex_models_reveal_pricing_calculation")).frame(maxWidth: .infinity) + } + } else { + Button { self.copy(self.row.rawAliases.joined(separator: "\n")) } label: { + Text(L("codex_models_copy_aliases")).frame(maxWidth: .infinity) + } + } + } + .buttonStyle(.bordered) + .controlSize(.regular) + } + + private func sectionTitle(_ title: String) -> some View { + Text(title).font(.subheadline.weight(.semibold)) + } + + private var interpretationLines: [String] { + var lines: [String] = [] + let share = self.snapshot.share(of: self.row, metric: self.metric) + let shareText = CodexModelsFormatters.percentage(share) + if share >= 0.5 { + lines.append(L("codex_models_dominates_at", self.metric.analyticsNoun, shareText)) + } else if share >= 0.2 { + lines.append(L("codex_models_major_contributor_at", shareText)) + } + lines.append(CodexModelsFormatters.comparisonSentence(self.row.comparison(self.metric)) + ".") + if self.row.sessionReferences > 0 { + let tokensPerSession = self.row.totalTokens / Int64(self.row.sessionReferences) + lines.append(L("codex_models_tokens_per_session_reference", tokensPerSession.formatted())) + } + if self.row.cost.unpricedTokens > 0 { + let coverage = CodexModelsFormatters.percentage(self.row.cost.coverage) + lines.append(L("codex_models_cost_conclusions_limited", coverage)) + } + return lines + } + + private func copy(_ value: String) { + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(value, forType: .string) + } +} + +private struct CodexModelsInspectorPricingSection: View { + let cost: CodexModelsCost + let previousCost: CodexModelsCost? + let usageTokens: Int64 + @Binding var showsPricing: Bool + + var body: some View { + let costText = CodexModelsFormatters.cost(self.cost, usageTokens: self.usageTokens) + let pricedTokens = self.cost.pricedTokens.formatted() + let unpricedTokens = self.cost.unpricedTokens.formatted() + let pricingExplanation = L( + "codex_models_pricing_explanation", + costText.primary, + pricedTokens, + unpricedTokens) + + VStack(alignment: .leading, spacing: 8) { + Text(L("codex_models_pricing_coverage")).font(.subheadline.weight(.semibold)) + LabeledContent(L("codex_models_known_cost"), value: costText.primary) + LabeledContent(L("codex_models_previous_known_cost"), value: self.previousCostText) + LabeledContent(L("codex_models_coverage"), value: CodexModelsFormatters.percentage(self.cost.coverage)) + LabeledContent(L("codex_models_currency"), value: self.cost.currencyCode) + DisclosureGroup(L("codex_models_pricing_calculation"), isExpanded: self.$showsPricing) { + VStack(alignment: .leading, spacing: 7) { + LabeledContent(L("codex_models_priced_tokens"), value: pricedTokens) + LabeledContent(L("codex_models_unpriced_tokens"), value: unpricedTokens) + Text(pricingExplanation) + .font(.caption) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(.top, 6) + } + } + .monospacedDigit() + } + + private var previousCostText: String { + guard let previousCost else { return "—" } + return CodexModelsFormatters.cost( + previousCost, + usageTokens: previousCost.pricedTokens + previousCost.unpricedTokens).primary + } +} + +private struct CodexModelsInspectorIdentitySection: View { + let canonicalID: String + let aliases: [String] + + @State private var showsAllAliases = false + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text(L("codex_models_identity_and_aliases")).font(.subheadline.weight(.semibold)) + LabeledContent(L("codex_models_canonical_id")) { Text(self.canonicalID).font(.caption.monospaced()) } + ForEach(Array(self.aliases.prefix(3)), id: \.self) { alias in + Text(alias).font(.caption.monospaced()) + } + if self.aliases.count > 3 { + DisclosureGroup( + L("codex_models_more_alias_count", self.aliases.count - 3), + isExpanded: self.$showsAllAliases) + { + VStack(alignment: .leading, spacing: 6) { + ForEach(Array(self.aliases.dropFirst(3)), id: \.self) { alias in + Text(alias).font(.caption.monospaced()) + } + } + .padding(.top, 6) + } + } + } + .textSelection(.enabled) + } +} diff --git a/Sources/CodexBar/CodexModelsDashboardView.swift b/Sources/CodexBar/CodexModelsDashboardView.swift new file mode 100644 index 0000000000..77d750c05b --- /dev/null +++ b/Sources/CodexBar/CodexModelsDashboardView.swift @@ -0,0 +1,556 @@ +import CodexBarCore +import SwiftUI +import UniformTypeIdentifiers + +struct CodexModelsDashboardView: View { + let snapshot: CodexModelsAnalyticsSnapshot + let scopeName: String + let showsEstimatedCost: Bool + let hidePersonalInfo: Bool + let historyDays: Int + @Binding var customHistoryDays: Int + let isRefreshing: Bool + let progressText: String? + let staleMessage: String? + let setHistoryDays: (Int) -> Void + let refresh: () -> Void + let showAssociatedSessions: ([String]) -> Void + + @State private var model: CodexModelsDashboardModel + @State private var exportDocument: CodexModelsCSVDocument? + @State private var exportFilename = L("codex_models_export_default_filename") + @State private var isExporting = false + @State private var exportError: String? + @State private var selectionPresentation: CodexModelsSelectionPresentation? + + init( + snapshot: CodexModelsAnalyticsSnapshot, + scopeName: String, + showsEstimatedCost: Bool, + hidePersonalInfo: Bool, + historyDays: Int, + customHistoryDays: Binding, + isRefreshing: Bool, + progressText: String?, + staleMessage: String?, + setHistoryDays: @escaping (Int) -> Void, + refresh: @escaping () -> Void, + showAssociatedSessions: @escaping ([String]) -> Void) + { + self.snapshot = snapshot + self.scopeName = scopeName + self.showsEstimatedCost = showsEstimatedCost + self.hidePersonalInfo = hidePersonalInfo + self.historyDays = historyDays + self._customHistoryDays = customHistoryDays + self.isRefreshing = isRefreshing + self.progressText = progressText + self.staleMessage = staleMessage + self.setHistoryDays = setHistoryDays + self.refresh = refresh + self.showAssociatedSessions = showAssociatedSessions + self._model = State(initialValue: CodexModelsDashboardModel(snapshot: snapshot)) + } + + var body: some View { + ScrollView { + LazyVStack( + alignment: .leading, + spacing: CodexModelsDashboardTokens.sectionSpacing(for: self.model.layout)) + { + if self.model.snapshot.rows.isEmpty { + VStack(spacing: 0) { + CodexModelsAnalyticsHeader( + model: self.model, + scopeName: self.scopeName, + historyDays: self.historyDays, + customHistoryDays: self.$customHistoryDays, + showsEstimatedCost: self.showsEstimatedCost, + isRefreshing: self.isRefreshing, + progressText: self.progressText, + staleMessage: self.staleMessage, + setHistoryDays: self.setHistoryDays, + refresh: self.refresh) + Divider() + ContentUnavailableView { + Label(L("codex_models_no_usage_in_range"), systemImage: "chart.bar.xaxis") + } description: { + Text(L("codex_models_no_usage_description")) + } actions: { + Button(L("codex_models_refresh_model_usage"), action: self.refresh) + .disabled(self.isRefreshing) + } + .frame(maxWidth: .infinity, minHeight: 320) + } + .codexModelsDashboardSurface() + } else { + CodexModelsSummaryStrip(model: self.model, showsEstimatedCost: self.showsEstimatedCost) + VStack(spacing: 0) { + CodexModelsAnalyticsHeader( + model: self.model, + scopeName: self.scopeName, + historyDays: self.historyDays, + customHistoryDays: self.$customHistoryDays, + showsEstimatedCost: self.showsEstimatedCost, + isRefreshing: self.isRefreshing, + progressText: self.progressText, + staleMessage: self.staleMessage, + setHistoryDays: self.setHistoryDays, + refresh: self.refresh) + Divider() + CodexModelsAnalysis(model: self.model) + } + .codexModelsDashboardSurface() + .accessibilityElement(children: .contain) + .accessibilityLabel(L("codex_models_analytics")) + CodexModelsPrecisionTable( + model: self.model, + showsEstimatedCost: self.showsEstimatedCost, + freshness: self.freshness, + showAssociatedSessions: self.showAssociatedSessions, + requestExport: self.requestExport) + } + } + .padding(CodexLocalProjectUsageWindowLayout.detailPadding) + } + .onGeometryChange( + for: CGFloat.self, + of: { $0.size.width }, + action: { width in self.model.updateLayout(width: width) }) + .inspector(isPresented: Binding( + get: { self.model.selectedRow != nil && self.selectionPresentation == .inspector }, + set: { if !$0 { self.model.clearSelection() } })) + { + if let row = self.model.selectedRow { + CodexModelsInspector( + row: row, + snapshot: self.model.snapshot, + metric: self.model.metric, + showsEstimatedCost: self.showsEstimatedCost, + close: self.model.clearSelection, + showAssociatedSessions: self.showAssociatedSessions) + .inspectorColumnWidth( + min: CodexModelsDashboardTokens.Width.inspectorMinimum, + ideal: CodexModelsDashboardTokens.Width.inspectorIdeal, + max: CodexModelsDashboardTokens.Width.inspectorMaximum) + } + } + .sheet(isPresented: Binding( + get: { self.model.selectedRow != nil && self.selectionPresentation == .sheet }, + set: { if !$0 { self.model.clearSelection() } })) + { + if let row = self.model.selectedRow { + CodexModelsInspector( + row: row, + snapshot: self.model.snapshot, + metric: self.model.metric, + showsEstimatedCost: self.showsEstimatedCost, + close: self.model.clearSelection, + showAssociatedSessions: self.showAssociatedSessions) + .frame( + minWidth: 420, + idealWidth: 480, + maxWidth: 560, + minHeight: 520, + idealHeight: 640) + } + } + .fileExporter( + isPresented: self.$isExporting, + document: self.exportDocument, + contentType: .commaSeparatedText, + defaultFilename: self.exportFilename) + { result in + if case let .failure(error) = result { self.exportError = error.localizedDescription } + self.exportDocument = nil + } + .alert(L("codex_models_export_failed"), isPresented: Binding( + get: { self.exportError != nil }, + set: { if !$0 { self.exportError = nil } })) + { + Button(L("OK"), role: .cancel) { self.exportError = nil } + } message: { + Text(self.exportError ?? L("codex_models_unknown_export_error")) + } + .background { self.keyboardCommands } + .onAppear { self.model.reconcileMetric(costDisplayEnabled: self.showsEstimatedCost) } + .onChange(of: self.showsEstimatedCost) { _, value in + self.model.reconcileMetric(costDisplayEnabled: value) + } + .onChange(of: self.snapshot) { _, value in self.model.replaceSnapshot(value) } + .onChange(of: self.model.selectedModelID) { oldValue, newValue in + if oldValue == nil, newValue != nil { + self.selectionPresentation = .resolve(layout: self.model.layout) + } else if newValue == nil { + self.selectionPresentation = nil + } + } + } + + private var freshness: CodexModelsFreshnessState { + if self.isRefreshing { return .refreshing } + if let staleMessage { return .stale(staleMessage) } + return .current + } + + private var keyboardCommands: some View { + HStack { + Button(L("codex_models_export_visible_rows")) { self.requestExport(.visible) } + .keyboardShortcut("e", modifiers: .command) + Button(L("codex_models_clear_selection"), action: self.model.clearSelection) + .keyboardShortcut(.cancelAction) + } + .frame(width: 0, height: 0) + .opacity(0) + .accessibilityHidden(true) + } + + private func requestExport(_ scope: CodexModelsExportScope) { + let rows = self.model.exportRows(scope) + switch scope { + case .visible: + self.exportFilename = L("codex_models_export_visible_filename") + case .all: + self.exportFilename = L("codex_models_export_all_filename") + case .selected: + guard let row = rows.first else { return } + self.exportFilename = L("codex_models_export_selected_filename", row.id) + } + self.exportDocument = CodexModelsCSVDocument(csv: CodexModelsPresentationCSVExporter.export( + snapshot: self.model.snapshot, + rows: rows, + metric: self.model.metric, + hidePersonalInfo: self.hidePersonalInfo)) + self.isExporting = true + } +} + +private struct CodexModelsAnalyticsHeader: View { + let model: CodexModelsDashboardModel + let scopeName: String + let historyDays: Int + @Binding var customHistoryDays: Int + let showsEstimatedCost: Bool + let isRefreshing: Bool + let progressText: String? + let staleMessage: String? + let setHistoryDays: (Int) -> Void + let refresh: () -> Void + + var body: some View { + ViewThatFits(in: .horizontal) { + HStack(spacing: 10) { + self.title + self.status + self.metricPicker + self.historyMenu + self.granularityMenu + self.refreshButton + } + VStack(alignment: .leading, spacing: 8) { + self.title + HStack(spacing: 10) { + self.metricPicker + self.historyMenu + self.granularityMenu + Spacer(minLength: 4) + self.refreshButton + } + self.status.frame(maxWidth: .infinity, alignment: .leading) + } + } + .controlSize(.small) + .padding(.horizontal, CodexModelsDashboardTokens.Spacing.large) + .padding(.vertical, self.model.layout == .wide ? 10 : CodexModelsDashboardTokens.Spacing.medium) + .frame( + minHeight: self.model.layout == .wide + ? CodexModelsDashboardTokens.Height.wideAnalyticsHeader + : nil) + .accessibilityElement(children: .contain) + .accessibilityLabel(L("codex_models_analytics_controls")) + } + + private var title: some View { + HStack(alignment: .firstTextBaseline, spacing: 5) { + Text(L("codex_models_model_usage")).font(.headline) + Text(self.scopeName).font(.subheadline).foregroundStyle(.secondary).lineLimit(1) + } + .fixedSize(horizontal: true, vertical: false) + } + + private var metricPicker: some View { + Picker(L("codex_models_metric"), selection: Binding( + get: { self.model.metric }, + set: { metric in self.model.setMetric(metric) })) + { + ForEach(CodexModelsMetric.allCases.filter { self.showsEstimatedCost || $0 != .knownCost }) { metric in + Text(metric.localizedTitle) + .tag(metric) + .disabled(metric == .knownCost && !self.model.costMetricIsAvailable) + } + } + .pickerStyle(.segmented) + .labelsHidden() + .fixedSize() + .help(self.model.costMetricIsAvailable + ? L("codex_models_choose_metric") + : L("codex_models_cost_unavailable_for_data")) + .accessibilityLabel(L("codex_models_metric")) + } + + private var historyMenu: some View { + Menu { + ForEach([7, 30, 60, 90, 180, 365], id: \.self) { days in + Button { self.setHistoryDays(days) } label: { + if days == self.activeHistoryDays { + Label(self.historyTitle(days), systemImage: "checkmark") + } else { + Text(self.historyTitle(days)) + } + } + } + Divider() + HStack { + TextField(L("codex_models_days"), value: self.$customHistoryDays, format: .number).frame(width: 58) + Stepper("", value: self.$customHistoryDays, in: 1...365).labelsHidden() + Button(L("codex_models_apply")) { self.setHistoryDays(self.customHistoryDays) } + } + } label: { + Text(self.historyTitle(self.activeHistoryDays)) + } + .fixedSize() + } + + private var granularityMenu: some View { + Menu { + ForEach(CodexModelsGranularityPreference.allCases) { preference in + Button { self.model.setGranularityPreference(preference) } label: { + if preference == self.model.granularityPreference { + Label(preference.title, systemImage: "checkmark") + } else { + Text(preference.title) + } + } + } + } label: { + Text(self.model.granularity.localizedTitle) + } + .fixedSize() + .accessibilityLabel( + L( + "codex_models_granularity_accessibility", + self.model.granularityPreference.title, + self.model.granularity.localizedTitle)) + } + + private var status: some View { + Group { + if self.isRefreshing { + HStack(spacing: 6) { + ProgressView().controlSize(.mini) + Text(self.progressText ?? L("codex_models_indexing_last_days", self.historyDays)) + } + } else if let staleMessage { + HStack(spacing: 6) { + Image(systemName: "exclamationmark.triangle.fill").foregroundStyle(.orange) + Text(staleMessage) + Button(L("codex_models_retry"), action: self.refresh).buttonStyle(.link) + } + } else { + Color.clear.accessibilityHidden(true) + } + } + .font(.caption) + .lineLimit(1) + .frame(minWidth: 12, maxWidth: .infinity, alignment: .trailing) + .accessibilityElement(children: .combine) + } + + private var refreshButton: some View { + Button(action: self.refresh) { Image(systemName: "arrow.clockwise") } + .buttonStyle(.bordered) + .disabled(self.isRefreshing) + .help(L("codex_models_refresh_command_help")) + .accessibilityLabel(L("codex_models_refresh_analytics")) + } + + private var activeHistoryDays: Int { + let interval = self.model.snapshot.currentInterval + let inclusiveEnd = interval.end.addingTimeInterval(-0.001) + let start = Calendar.current.startOfDay(for: interval.start) + let end = Calendar.current.startOfDay(for: inclusiveEnd) + return max(1, (Calendar.current.dateComponents([.day], from: start, to: end).day ?? 0) + 1) + } + + private func historyTitle(_ days: Int) -> String { + L("codex_models_last_days", days) + } +} + +private struct CodexModelsSummaryItem: Identifiable { + let id: String + let title: String + let value: String + let detail: String + let accessibilityValue: String +} + +private struct CodexModelsSummaryStrip: View { + let model: CodexModelsDashboardModel + let showsEstimatedCost: Bool + + var body: some View { + Group { + switch self.model.layout { + case .wide: + self.summaryRow + case .medium: + self.summaryGrid + case .compact: + ViewThatFits(in: .horizontal) { + self.summaryGrid.frame(minWidth: 460) + self.summaryStack + } + } + } + .codexModelsDashboardSurface() + .accessibilityElement(children: .contain) + .accessibilityLabel(L("codex_models_summary")) + } + + private var summaryRow: some View { + HStack(spacing: 0) { + ForEach(Array(self.items.enumerated()), id: \.element.id) { index, item in + if index > 0 { Divider().padding(.vertical, 3) } + self.cell(item).frame(maxWidth: .infinity, alignment: .leading) + } + } + } + + private var summaryGrid: some View { + VStack(spacing: 0) { + HStack(spacing: 0) { + self.cell(self.items[0]).frame(maxWidth: .infinity, alignment: .leading) + Divider().padding(.vertical, 3) + self.cell(self.items[1]).frame(maxWidth: .infinity, alignment: .leading) + } + Divider().padding(.horizontal, 3) + HStack(spacing: 0) { + self.cell(self.items[2]).frame(maxWidth: .infinity, alignment: .leading) + Divider().padding(.vertical, 3) + self.cell(self.items[3]).frame(maxWidth: .infinity, alignment: .leading) + } + } + } + + private var summaryStack: some View { + VStack(alignment: .leading, spacing: 0) { + ForEach(Array(self.items.enumerated()), id: \.element.id) { index, item in + if index > 0 { Divider().padding(.horizontal, 3) } + self.cell(item).frame(maxWidth: .infinity, alignment: .leading) + } + } + } + + private var items: [CodexModelsSummaryItem] { + let snapshot = self.model.snapshot + let cost = CodexModelsFormatters.cost(snapshot.cost, usageTokens: snapshot.totalTokens) + let topModel = self.model.topModel + let newlyActive = snapshot.newlyActiveModelCount + let tokenAccessibility = L( + "codex_models_value_comparison_accessibility", + L("codex_models_token_count", snapshot.totalTokens.formatted()), + CodexModelsFormatters.comparisonSentence(snapshot.tokenComparison)) + let topDetail = topModel.map { + let value: String = switch self.model.metric { + case .tokens: L("codex_models_token_count", CodexModelsFormatters.compactInteger($0.totalTokens)) + case .knownCost: CodexModelsFormatters.cost($0.cost, usageTokens: $0.totalTokens).primary + case .sessionReferences: L("codex_models_ref_count", $0.sessionReferences.formatted()) + } + let share = CodexModelsFormatters.percentage(snapshot.share(of: $0, metric: self.model.metric)) + return L("codex_models_value_share", value, share) + } ?? L("codex_models_no_usage") + let topAccessibility = topModel.map { + let value = CodexModelsFormatters.fullMetricValue($0, metric: self.model.metric) + return L("codex_models_name_value_accessibility", $0.displayName, value) + } ?? L("codex_models_no_top_model") + let costCoverage = cost.secondary ?? L("codex_models_fully_priced_percent") + return [ + CodexModelsSummaryItem( + id: "tokens", + title: L("codex_models_total_tokens"), + value: CodexModelsFormatters.compactInteger(snapshot.totalTokens), + detail: self.summaryComparison(snapshot.tokenComparison), + accessibilityValue: tokenAccessibility), + CodexModelsSummaryItem( + id: "cost", + title: L("codex_models_known_cost"), + value: self.showsEstimatedCost ? cost.primary : L("codex_models_hidden"), + detail: self.showsEstimatedCost + ? L("codex_models_value_detail", self.summaryComparison(snapshot.costComparison), costCoverage) + : L("codex_models_disabled_in_settings"), + accessibilityValue: self.showsEstimatedCost + ? cost.accessibility + : L("codex_models_cost_hidden")), + CodexModelsSummaryItem( + id: "top", + title: L("codex_models_top_model"), + value: topModel?.displayName ?? "—", + detail: topDetail, + accessibilityValue: topAccessibility), + CodexModelsSummaryItem( + id: "active", + title: L("codex_models_active_models"), + value: snapshot.activeModelCount.formatted(), + detail: self.newlyActiveDetail(newlyActive), + accessibilityValue: self.newlyActiveAccessibility( + activeCount: snapshot.activeModelCount, + newlyActiveCount: newlyActive)), + ] + } + + private func summaryComparison(_ comparison: CodexModelsComparison) -> String { + comparison == .unavailable + ? L("codex_models_previous_unavailable") + : L("codex_models_vs_previous", CodexModelsFormatters.compactComparison(comparison)) + } + + private func newlyActiveDetail(_ count: Int?) -> String { + guard let count else { return L("codex_models_previous_period_unavailable") } + return count == 0 + ? L("codex_models_no_newly_active") + : L("codex_models_newly_active_count", count) + } + + private func newlyActiveAccessibility(activeCount: Int, newlyActiveCount: Int?) -> String { + guard let newlyActiveCount else { + return L("codex_models_active_count_new_unavailable", activeCount) + } + return L("codex_models_active_and_new_count", activeCount, newlyActiveCount) + } + + private func cell(_ item: CodexModelsSummaryItem) -> some View { + VStack(alignment: .leading, spacing: CodexModelsDashboardTokens.Spacing.small) { + Text(item.title).font(.caption).foregroundStyle(.secondary) + Text(item.value) + .font(.system(size: 24, weight: .semibold)) + .monospacedDigit() + .lineLimit(1) + Text(item.detail).font(.caption2).foregroundStyle(.secondary).lineLimit(2) + } + .padding(.horizontal, CodexModelsDashboardTokens.Spacing.xLarge) + .padding(.vertical, CodexModelsDashboardTokens.Spacing.large) + .frame(minHeight: self.cellHeight, alignment: .leading) + .accessibilityElement(children: .ignore) + .accessibilityLabel(item.title) + .accessibilityValue(item.accessibilityValue) + } + + private var cellHeight: CGFloat { + switch self.model.layout { + case .wide: CodexModelsDashboardTokens.Height.wideSummary + case .medium: CodexModelsDashboardTokens.Height.mediumSummaryCell + case .compact: CodexModelsDashboardTokens.Height.compactSummaryCell + } + } +} diff --git a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings index 656e84539a..9112eecc3e 100644 --- a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings @@ -1353,3 +1353,255 @@ "Cost today unavailable" = "تكلفة اليوم: غير متوفر"; "30-day cost unavailable" = "تكلفة 30 يوماً: غير متوفر"; "Resets" = "إعادات الضبط"; + +/* Codex Workspaces and Models */ +"codex_workspaces_open_in_window" = "فتح في نافذة"; +"codex_models_actions" = "الإجراءات"; +"codex_models_active_and_new_count" = "النماذج النشطة: %1$d; نشط حديثًا: %2$d"; +"codex_models_active_count_new_unavailable" = "النماذج النشطة: %1$d; العدد النشط الجديد غير متاح"; +"codex_models_active_models" = "النماذج النشطة"; +"codex_models_alias_count" = "الأسماء المستعارة: %1$d"; +"codex_models_all_model_count" = "جميع النماذج: %1$d"; +"codex_models_all_models" = "جميع الموديلات"; +"codex_models_all_rows" = "جميع الصفوف"; +"codex_models_analytics" = "تحليلات النماذج"; +"codex_models_analytics_controls" = "عناصر التحكم في تحليلات النماذج"; +"codex_models_apply" = "قدم طلبك"; +"codex_models_bucket_count" = "المستودعات (%2$@): %1$d"; +"codex_models_bucket_interval" = "الفاصل الزمني للجرافة"; +"codex_models_bullet_value" = "· %1$@"; +"codex_models_canonical_id" = "المعرف الكنسي"; +"codex_models_canonical_model" = "النموذج الكنسي"; +"codex_models_change" = "التغيير"; +"codex_models_change_from_peak" = "%1$@ من الذروة %2$@"; +"codex_models_change_from_previous_period" = "%1$@ من الفترة المتساوية السابقة"; +"codex_models_choose_metric" = "اختر مقياس التحليلات"; +"codex_models_clear_search" = "مسح البحث"; +"codex_models_clear_selection" = "مسح تحديد النماذج"; +"codex_models_columns" = "الأعمدة"; +"codex_models_compact_billions_suffix" = "B"; +"codex_models_compact_millions_suffix" = "M"; +"codex_models_compact_thousands_suffix" = "K"; +"codex_models_contains_selected_suffix" = "، يحتوي على النموذج المحدد"; +"codex_models_copy_aliases" = "نسخ الأسماء المستعارة"; +"codex_models_copy_canonical_id" = "انسخ المعرف الأساسي"; +"codex_models_copy_canonical_model_id" = "انسخ معرف النموذج الأساسي"; +"codex_models_copy_raw_aliases" = "انسخ الأسماء المستعارة الأولية"; +"codex_models_cost_conclusions_limited" = "تقتصر استنتاجات التكلفة على %1$@ من الرموز المميزة المسعرة."; +"codex_models_cost_hidden" = "التكلفة مخفية"; +"codex_models_cost_status_currency" = "%1$@ · %2$@ · %3$@"; +"codex_models_cost_unavailable" = "التكلفة غير متاحة"; +"codex_models_cost_unavailable_for_data" = "التكلفة غير متاحة لهذه البيانات"; +"codex_models_coverage" = "التغطية"; +"codex_models_currency" = "العملة"; +"codex_models_current_metric" = "الحالي %1$@"; +"codex_models_current_period_in_progress" = "%1$@ الحالي قيد التقدم"; +"codex_models_date" = "التاريخ"; +"codex_models_day_lowercase" = "يوم"; +"codex_models_days" = "أيام"; +"codex_models_delta_vs_prior" = "Δ مقابل السابق"; +"codex_models_disabled_in_settings" = "معطل في الإعدادات"; +"codex_models_dominates_at" = "هذا النموذج يهيمن على %1$@ عند %2$@."; +"codex_models_ended" = "انتهى"; +"codex_models_export" = "تصدير"; +"codex_models_export_all_filename" = "codex-models-all.csv"; +"codex_models_export_default_filename" = "codex-models.csv"; +"codex_models_export_failed" = "تعذر تصدير تحليلات النماذج"; +"codex_models_export_selected_filename" = "codex-model-%1$@.csv"; +"codex_models_export_selected_model" = "تصدير النموذج المحدد"; +"codex_models_export_visible_filename" = "codex-models-visible.csv"; +"codex_models_export_visible_rows" = "تصدير صفوف النماذج المرئية"; +"codex_models_focus_table_search" = "البحث في جدول نماذج التركيز"; +"codex_models_fully_priced" = "بسعر كامل"; +"codex_models_fully_priced_percent" = "بسعر 100%"; +"codex_models_generated" = "تم إنشاؤه %1$@"; +"codex_models_granularity_accessibility" = "الدقة، %1$@، %2$@"; +"codex_models_hidden" = "مخفي"; +"codex_models_identity_aliases_help" = "%1$@ · %2$@ · الأسماء المستعارة: %3$@"; +"codex_models_identity_and_aliases" = "الهوية والأسماء المستعارة"; +"codex_models_indexing_last_days" = "فهرسة آخر %1$d يوم..."; +"codex_models_interpretation" = "التفسير"; +"codex_models_known_cost" = "التكلفة المعروفة"; +"codex_models_known_cost_fully_priced_accessibility" = "التكلفة المعروفة %1$@، مسعرة بالكامل"; +"codex_models_known_cost_lowercase" = "التكلفة المعروفة"; +"codex_models_known_cost_partial_accessibility" = "التكلفة المعروفة %1$@، جزئية، %2$@ من الرموز المميزة المسعرة"; +"codex_models_last_days" = "آخر %1$d يوم"; +"codex_models_local_logs_source" = "· سجلات الدستور الغذائي المحلية"; +"codex_models_major_contributor_at" = "هذا النموذج هو المساهم الرئيسي في %1$@."; +"codex_models_metric" = "متري"; +"codex_models_metric_over_time" = "%1$@ بمرور الوقت"; +"codex_models_model" = "الموديل"; +"codex_models_model_concentration" = "تركيز النموذج"; +"codex_models_model_count" = "النماذج: %1$d"; +"codex_models_model_usage" = "استخدام النموذج"; +"codex_models_month_lowercase" = "شهر"; +"codex_models_more_alias_count" = "المزيد من الأسماء المستعارة: %1$d"; +"codex_models_name_canonical_id" = "%1$@، المعرف الأساسي %2$@"; +"codex_models_name_value_accessibility" = "%1$@, %2$@"; +"codex_models_named_metric_over_time" = "%1$@ %2$@ بمرور الوقت"; +"codex_models_new" = "جديد"; +"codex_models_new_in_period" = "الجديد في هذه الفترة"; +"codex_models_newly_active_count" = "نشط حديثًا: %1$d"; +"codex_models_no_applicable_price" = "لا يوجد سعر قابل للتطبيق"; +"codex_models_no_change_from_previous_period" = "لا يوجد تغيير عن الفترة المماثلة السابقة"; +"codex_models_no_matching_models" = "لا توجد نماذج متطابقة"; +"codex_models_no_newly_active" = "لا توجد نماذج نشطة حديثًا"; +"codex_models_no_top_model" = "لا يوجد نموذج أعلى"; +"codex_models_no_usage" = "لا يوجد استخدام"; +"codex_models_no_usage_description" = "اختر نطاقًا زمنيًا أطول أعلاه أو قم بالتحديث بعد حفظ جلسات الدستور الغذائي المحلية."; +"codex_models_no_usage_in_period" = "لا يوجد استخدام في هذه الفترة"; +"codex_models_no_usage_in_range" = "لا يوجد استخدام للنموذج في هذا النطاق"; +"codex_models_other_count" = "أخرى (%1$d)"; +"codex_models_partial_priced" = "جزئي · سعر %1$@"; +"codex_models_precision_table" = "جدول دقة النموذج"; +"codex_models_previous_known_cost" = "التكلفة المعروفة السابقة"; +"codex_models_previous_metric" = "السابق %1$@"; +"codex_models_previous_period_unavailable" = "الفترة السابقة غير متاحة"; +"codex_models_previous_unavailable" = "السابق غير متاح"; +"codex_models_priced_tokens" = "الرموز المميزة المسعرة"; +"codex_models_pricing_calculation" = "حساب التسعير"; +"codex_models_pricing_coverage" = "تغطية الأسعار"; +"codex_models_pricing_explanation" = "%1$@ يغطي %2$@ من الرموز المميزة المسعرة. %3$@ يتم استبعاد الرموز المميزة التي لا تحتوي على قاعدة قابلة للتطبيق، ولا يتم تقديرها بالصفر."; +"codex_models_ranking_by" = "تصنيف النموذج بواسطة %1$@"; +"codex_models_ranking_hint" = "انقر فوق شريط النموذج لفتح تفاصيله."; +"codex_models_ranking_point_accessibility" = "%1$@، %2$@، %3$@ مشاركة"; +"codex_models_ranking_summary" = "النماذج الأساسية وغيرها مرتبة حسب %1$@."; +"codex_models_ref_count" = "%1$@ المرجع"; +"codex_models_refresh_analytics" = "تحديث تحليلات النماذج"; +"codex_models_refresh_command_help" = "تحديث تحليلات النماذج (Command-R)"; +"codex_models_refresh_model_usage" = "تحديث استخدام النموذج"; +"codex_models_refreshing_last_snapshot" = "تحديث · عرض آخر لقطة ناجحة"; +"codex_models_retry" = "أعد المحاولة"; +"codex_models_reveal_pricing_calculation" = "كشف حساب التسعير"; +"codex_models_search_empty_description" = "يقوم البحث بتصفية هذا الجدول فقط؛ إجماليات لوحة القيادة لم تتغير."; +"codex_models_search_models" = "نماذج البحث"; +"codex_models_search_scope" = "%1$@ · يقوم البحث بتصفية هذا الجدول فقط"; +"codex_models_search_table" = "جدول نماذج البحث"; +"codex_models_select_model" = "اختر %1$@"; +"codex_models_selected" = "تم التحديد"; +"codex_models_selected_bucket" = "المجموعة المحددة"; +"codex_models_selected_bucket_accessibility" = "المجموعة المحددة: %1$@، %2$@، %3$@ الرموز المميزة، %4$@ مراجع الجلسة، %5$@، العملة %6$@."; +"codex_models_selected_clear_available" = "النموذج المحدد %1$@، التحديد متاح"; +"codex_models_selected_date" = "التاريخ المحدد"; +"codex_models_selected_hidden_by_search" = "النموذج المحدد مخفي بالبحث"; +"codex_models_selected_model" = "النموذج المحدد"; +"codex_models_selected_model_interval" = "النموذج المحدد · %1$@"; +"codex_models_selected_suffix" = "، محدد"; +"codex_models_session_reference_count" = "%1$@ مراجع الجلسة"; +"codex_models_session_references" = "مراجع الجلسة"; +"codex_models_session_references_lowercase" = "مراجع الجلسة"; +"codex_models_session_refs" = "مراجع الجلسة"; +"codex_models_share" = "شارك"; +"codex_models_share_known_cost_help" = "حصة التكلفة المعروفة؛ يستبعد الاستخدام غير المسعر"; +"codex_models_share_of" = "حصة %1$@"; +"codex_models_show_associated_sessions" = "عرض الجلسات المرتبطة"; +"codex_models_show_details" = "عرض التفاصيل"; +"codex_models_showing_last_snapshot" = "عرض آخر لقطة ناجحة"; +"codex_models_sorted_by" = "تم الترتيب حسب %1$@"; +"codex_models_subject_over_time" = "%1$@ بمرور الوقت"; +"codex_models_summary" = "ملخص النماذج"; +"codex_models_timeline_accessibility" = "الجدول الزمني للنماذج، %1$@، %2$@"; +"codex_models_timeline_hint" = "انقر فوق مجموعة زمنية، أو ضع المؤشر فوق إحداها لمدة 1.5 ثانية، لفحص تفاصيلها."; +"codex_models_timeline_point_accessibility" = "%1$@، %2$@، %3$@ الرموز المميزة، %4$@ مراجع الجلسة، %5$@، العملة %6$@"; +"codex_models_timeline_summary" = "%1$@ %2$@ الجدول الزمني. %3$@"; +"codex_models_token_count" = "%1$@ الرموز المميزة"; +"codex_models_tokens" = "الرموز"; +"codex_models_tokens_lowercase" = "الرموز المميزة"; +"codex_models_tokens_per_session_reference" = "حوالي %1$@ من الرموز المميزة لكل مرجع جلسة."; +"codex_models_top" = "الأعلى"; +"codex_models_top_model" = "النموذج الأعلى"; +"codex_models_top_seven_other" = "أعلى 7 + أخرى"; +"codex_models_top_two_account_for" = "يمثل أفضل نموذجين %1$@ من %2$@"; +"codex_models_top_two_of" = "أفضل نموذجين لـ %1$@"; +"codex_models_total_session_references_lowercase" = "إجمالي مراجع الجلسة"; +"codex_models_total_tokens" = "إجمالي الرموز المميزة"; +"codex_models_total_tokens_lowercase" = "إجمالي الرموز"; +"codex_models_totals_reconcile" = "· مطابقة الإجماليات مع اللقطة النشطة"; +"codex_models_track_end" = "نهاية المسار"; +"codex_models_track_start" = "بداية المسار"; +"codex_models_trend_unavailable_observed_buckets" = "المجموعات التي تمت ملاحظتها: %1$d. الاتجاه غير متاح."; +"codex_models_unknown_export_error" = "خطأ تصدير غير معروف"; +"codex_models_unpriced_tokens" = "الرموز غير المسعرة"; +"codex_models_updated" = "تم التحديث %1$@"; +"codex_models_usage_and_change" = "الاستخدام والتغيير"; +"codex_models_usage_by_model" = "الاستخدام حسب الطراز"; +"codex_models_value_comparison_accessibility" = "%1$@, %2$@"; +"codex_models_value_detail" = "%1$@ · %2$@"; +"codex_models_value_share" = "%1$@ · %2$@ مشاركة"; +"codex_models_value_share_accessibility" = "%1$@، %2$@ مشاركة"; +"codex_models_value_start" = "بداية القيمة"; +"codex_models_visible_of_total" = "النماذج المرئية: %1$d من %2$d"; +"codex_models_visible_rows" = "صفوف مرئية"; +"codex_models_vs_previous" = "%1$@ مقارنة بالسابق"; +"codex_models_week_lowercase" = "أسبوع"; +"codex_workspaces_all_models" = "كل النماذج"; +"codex_workspaces_all_workspaces" = "كل مساحات العمل"; +"codex_workspaces_associated_with_selected_model" = "مرتبط بالنموذج المحدد"; +"codex_workspaces_bullet_value" = "· %1$@"; +"codex_workspaces_chart_keyboard_hint" = "استخدم مفتاحي السهم الأيسر والأيمن أو إجراءات ضبط VoiceOver لتحديد يوم. اضغط Return أو المسافة لتثبيته."; +"codex_workspaces_chats_description" = "الدردشات والمحادثات"; +"codex_workspaces_copy_path" = "نسخ المسار"; +"codex_workspaces_cost_unavailable" = "التكلفة غير متاحة"; +"codex_workspaces_daily_callout_accessibility" = "%1$@, %2$@"; +"codex_workspaces_daily_point_accessibility" = "%1$@، الرموز %2$@"; +"codex_workspaces_daily_point_help" = "%1$@ — %2$@ الرموز المميزة"; +"codex_workspaces_daily_token_usage" = "استخدام الرموز اليومي"; +"codex_workspaces_daily_unavailable" = "الاستخدام اليومي غير متاح"; +"codex_workspaces_daily_unavailable_description" = "حُسب الإجمالي من %d جلسة، لكن تعذّر تحديد تواريخها."; +"codex_workspaces_date" = "التاريخ"; +"codex_workspaces_days" = "أيام"; +"codex_workspaces_elevated_usage" = "استخدام أعلى من المعتاد"; +"codex_workspaces_elevated_usage_help" = "الاستخدام أعلى من الوسيط في الفترة المحددة."; +"codex_workspaces_estimated_cost_short" = "التكلفة المقدّرة"; +"codex_workspaces_estimated_cost_value" = "مقدّر %@"; +"codex_workspaces_estimated_local_logs" = "تقدير من سجلات Codex المحلية"; +"codex_workspaces_failed_read_logs" = "تعذّرت قراءة سجلات Codex"; +"codex_workspaces_failed_some_logs" = "تعذّرت قراءة بعض سجلات Codex"; +"codex_workspaces_finalizing_project_index_files" = "جارٍ إنهاء فهرس المشاريع… %d ملف"; +"codex_workspaces_high_usage" = "استخدام مرتفع"; +"codex_workspaces_high_usage_help" = "الاستخدام شاذ بدرجة كبيرة في الفترة المحددة."; +"codex_workspaces_indexed_files" = "تم التحديث منذ %@ · تمت فهرسة %d ملف"; +"codex_workspaces_indexing_local_logs" = "جارٍ فهرسة سجلات Codex المحلية…"; +"codex_workspaces_indexing_projects" = "جارٍ فهرسة المشاريع…"; +"codex_workspaces_indexing_projects_files_left" = "جارٍ فهرسة المشاريع… %d/%d ملف، متبقٍ %d"; +"codex_workspaces_last_days" = "آخر %d يوم"; +"codex_workspaces_local_codex_chat" = "دردشة Codex محلية"; +"codex_workspaces_model" = "النموذج"; +"codex_workspaces_models" = "النماذج"; +"codex_workspaces_no_daily_usage" = "لا يوجد استخدام يومي في نطاق السجل هذا."; +"codex_workspaces_no_local_usage" = "لا يوجد استخدام محلي لـ Codex حتى الآن"; +"codex_workspaces_no_models" = "لا يتوفر تفصيل حسب النموذج"; +"codex_workspaces_no_projects_or_chats" = "لم يتم العثور على مشاريع أو دردشات في نطاق السجل هذا."; +"codex_workspaces_no_sessions" = "لا توجد جلسات في مساحة العمل هذه"; +"codex_workspaces_normal_usage" = "استخدام عادي"; +"codex_workspaces_not_available" = "غير متوفر"; +"codex_workspaces_overview" = "نظرة عامة"; +"codex_workspaces_partial_breakdown_help" = "يظل الملخص هو المرجع بينما تظهر بيانات التاريخ أو النموذج غير المتاحة كبيانات جزئية."; +"codex_workspaces_partial_breakdown_notice" = "بعض التفاصيل لا تغطي الإجمالي الكامل."; +"codex_workspaces_partial_data" = "بيانات جزئية"; +"codex_workspaces_percent_total" = "النسبة من الإجمالي"; +"codex_workspaces_refresh_usage_data" = "تحديث بيانات الاستخدام"; +"codex_workspaces_relative_usage_accessibility" = "%d بالمئة من أعلى استخدام لمساحة عمل"; +"codex_workspaces_relative_usage_tooltip" = "يستخدم %d%% من رموز مساحة العمل الأعلى استخدامًا في هذه الفترة."; +"codex_workspaces_reveal_in_finder" = "إظهار في Finder"; +"codex_workspaces_saving_project_index" = "جارٍ حفظ فهرس المشاريع…"; +"codex_workspaces_scanning_logs" = "جارٍ فحص سجلات Codex…"; +"codex_workspaces_search_sessions" = "البحث في الجلسات"; +"codex_workspaces_selected_day_accessibility" = "%@: %@ من الرموز"; +"codex_workspaces_session" = "جلسة"; +"codex_workspaces_session_count" = "الجلسات: %d"; +"codex_workspaces_sessions" = "الجلسات"; +"codex_workspaces_showing_last_complete_index" = "جارٍ عرض آخر فهرس محلي مكتمل"; +"codex_workspaces_stale_status" = "فهرس الاستخدام قديم · تم التحديث منذ %@"; +"codex_workspaces_started" = "بدأ"; +"codex_workspaces_title" = "مساحات العمل"; +"codex_workspaces_token_count_accessibility" = "%@ من الرموز"; +"codex_workspaces_tokens" = "الرموز"; +"codex_workspaces_top_model" = "النموذج الأكثر استخدامًا"; +"codex_workspaces_top_sessions" = "أهم الجلسات"; +"codex_workspaces_total" = "الإجمالي"; +"codex_workspaces_view" = "العرض"; +"codex_workspaces_view_all_sessions" = "عرض كل الجلسات"; +"codex_workspaces_window_title" = "مساحات عمل Codex"; +"codex_workspaces_workspace_count" = "مساحات العمل والمحادثات: %d"; +"codex_workspaces_workspaces_chats" = "مساحات العمل/الدردشات"; diff --git a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings index 1921f5a94d..c447590a42 100644 --- a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings @@ -1352,3 +1352,255 @@ "Cost today unavailable" = "Cost d’avui: No disponible"; "30-day cost unavailable" = "Cost de 30 dies: No disponible"; "Resets" = "Reinicis"; + +/* Codex Workspaces and Models */ +"codex_workspaces_open_in_window" = "Obre en una finestra"; +"codex_models_actions" = "Accions"; +"codex_models_active_and_new_count" = "Models actius: %1$d; recentment actiu: %2$d"; +"codex_models_active_count_new_unavailable" = "Models actius: %1$d; recompte recentment actiu no disponible"; +"codex_models_active_models" = "Models actius"; +"codex_models_alias_count" = "Àlies: %1$d"; +"codex_models_all_model_count" = "Tots els models: %1$d"; +"codex_models_all_models" = "Tots els models"; +"codex_models_all_rows" = "Totes les files"; +"codex_models_analytics" = "Anàlisi de models"; +"codex_models_analytics_controls" = "Models de controls analítics"; +"codex_models_apply" = "Sol·licitar"; +"codex_models_bucket_count" = "Dipòsits (%2$@): %1$d"; +"codex_models_bucket_interval" = "Interval de cub"; +"codex_models_bullet_value" = "· %1$@"; +"codex_models_canonical_id" = "Identificador canònic"; +"codex_models_canonical_model" = "Model canònic"; +"codex_models_change" = "Canvia"; +"codex_models_change_from_peak" = "%1$@ des del pic de %2$@"; +"codex_models_change_from_previous_period" = "%1$@ del període igual anterior"; +"codex_models_choose_metric" = "Trieu la mètrica d'anàlisi"; +"codex_models_clear_search" = "Esborra la cerca"; +"codex_models_clear_selection" = "Esborra la selecció de models"; +"codex_models_columns" = "Columnes"; +"codex_models_compact_billions_suffix" = "B"; +"codex_models_compact_millions_suffix" = "M"; +"codex_models_compact_thousands_suffix" = "K"; +"codex_models_contains_selected_suffix" = ", conté el model seleccionat"; +"codex_models_copy_aliases" = "Copia els àlies"; +"codex_models_copy_canonical_id" = "Copia l'identificador canònic"; +"codex_models_copy_canonical_model_id" = "Copia l'identificador del model canònic"; +"codex_models_copy_raw_aliases" = "Copia els àlies en brut"; +"codex_models_cost_conclusions_limited" = "Les conclusions de costos es limiten a %1$@ fitxes de preu."; +"codex_models_cost_hidden" = "Cost ocult"; +"codex_models_cost_status_currency" = "%1$@ · %2$@ · %3$@"; +"codex_models_cost_unavailable" = "Cost no disponible"; +"codex_models_cost_unavailable_for_data" = "El cost no està disponible per a aquestes dades"; +"codex_models_coverage" = "Cobertura"; +"codex_models_currency" = "Moneda"; +"codex_models_current_metric" = "%1$@ actual"; +"codex_models_current_period_in_progress" = "%1$@ actual en curs"; +"codex_models_date" = "Data"; +"codex_models_day_lowercase" = "dia"; +"codex_models_days" = "Dies"; +"codex_models_delta_vs_prior" = "Δ vs anterior"; +"codex_models_disabled_in_settings" = "Desactivat a la configuració"; +"codex_models_dominates_at" = "Aquest model domina %1$@ a %2$@."; +"codex_models_ended" = "Finalitzat"; +"codex_models_export" = "Exporta"; +"codex_models_export_all_filename" = "codex-models-all.csv"; +"codex_models_export_default_filename" = "codex-models.csv"; +"codex_models_export_failed" = "No s'han pogut exportar les anàlisis de models"; +"codex_models_export_selected_filename" = "codex-model-%1$@.csv"; +"codex_models_export_selected_model" = "Exporta el model seleccionat"; +"codex_models_export_visible_filename" = "codex-models-visible.csv"; +"codex_models_export_visible_rows" = "Exporta les files visibles dels models"; +"codex_models_focus_table_search" = "Cerca de taula Focus Models"; +"codex_models_fully_priced" = "Preu complet"; +"codex_models_fully_priced_percent" = "Preu al 100%."; +"codex_models_generated" = "generat %1$@"; +"codex_models_granularity_accessibility" = "Granularitat, %1$@, %2$@"; +"codex_models_hidden" = "Amagat"; +"codex_models_identity_aliases_help" = "%1$@ · %2$@ · Àlies: %3$@"; +"codex_models_identity_and_aliases" = "Identitat i àlies"; +"codex_models_indexing_last_days" = "Indexant els últims %1$d dies..."; +"codex_models_interpretation" = "Interpretació"; +"codex_models_known_cost" = "Cost conegut"; +"codex_models_known_cost_fully_priced_accessibility" = "Cost conegut %1$@, amb preu total"; +"codex_models_known_cost_lowercase" = "cost conegut"; +"codex_models_known_cost_partial_accessibility" = "Cost conegut %1$@, parcial, %2$@ de fitxes amb preu"; +"codex_models_last_days" = "Els últims %1$d dies"; +"codex_models_local_logs_source" = "· Registres locals del Codex"; +"codex_models_major_contributor_at" = "Aquest model és un col·laborador important a %1$@."; +"codex_models_metric" = "Mètrica"; +"codex_models_metric_over_time" = "%1$@ al llarg del temps"; +"codex_models_model" = "Model"; +"codex_models_model_concentration" = "Model de concentració"; +"codex_models_model_count" = "Models: %1$d"; +"codex_models_model_usage" = "Ús del model"; +"codex_models_month_lowercase" = "mes"; +"codex_models_more_alias_count" = "Més àlies: %1$d"; +"codex_models_name_canonical_id" = "%1$@, identificador canònic %2$@"; +"codex_models_name_value_accessibility" = "%1$@, %2$@"; +"codex_models_named_metric_over_time" = "%1$@ %2$@ al llarg del temps"; +"codex_models_new" = "Nou"; +"codex_models_new_in_period" = "Nou en aquest període"; +"codex_models_newly_active_count" = "Actius recentment: %1$d"; +"codex_models_no_applicable_price" = "No hi ha preu aplicable"; +"codex_models_no_change_from_previous_period" = "Sense canvis respecte al període igual anterior"; +"codex_models_no_matching_models" = "No hi ha cap model coincident"; +"codex_models_no_newly_active" = "No hi ha cap model nou actiu"; +"codex_models_no_top_model" = "No hi ha cap model superior"; +"codex_models_no_usage" = "Sense ús"; +"codex_models_no_usage_description" = "Trieu un interval de dates més llarg a sobre o actualitzeu-ho després de desar les sessions locals del Codex."; +"codex_models_no_usage_in_period" = "No s'utilitza en aquest període"; +"codex_models_no_usage_in_range" = "No s'utilitza cap model en aquest rang"; +"codex_models_other_count" = "Altres (%1$d)"; +"codex_models_partial_priced" = "Parcial · Preu %1$@"; +"codex_models_precision_table" = "Model de taula de precisió"; +"codex_models_previous_known_cost" = "Cost conegut anterior"; +"codex_models_previous_metric" = "%1$@ anterior"; +"codex_models_previous_period_unavailable" = "El període anterior no està disponible"; +"codex_models_previous_unavailable" = "Anterior no disponible"; +"codex_models_priced_tokens" = "Fitxes amb preu"; +"codex_models_pricing_calculation" = "Càlcul de preus"; +"codex_models_pricing_coverage" = "Cobertura de preus"; +"codex_models_pricing_explanation" = "%1$@ cobreix les fitxes amb preu de %2$@. S'exclouen les fitxes %3$@ sense una regla aplicable, no s'estima com a zero."; +"codex_models_ranking_by" = "Classificació del model per %1$@"; +"codex_models_ranking_hint" = "Feu clic a una barra de model per obrir-ne els detalls."; +"codex_models_ranking_point_accessibility" = "Comparteix %1$@, %2$@, %3$@"; +"codex_models_ranking_summary" = "Models canònics i Altres classificats per %1$@."; +"codex_models_ref_count" = "%1$@ refs"; +"codex_models_refresh_analytics" = "Actualitza l'anàlisi de models"; +"codex_models_refresh_command_help" = "Actualitza l'anàlisi de models (comandament-R)"; +"codex_models_refresh_model_usage" = "Actualitza l'ús del model"; +"codex_models_refreshing_last_snapshot" = "Actualitzant · mostrant la darrera instantània correcta"; +"codex_models_retry" = "Torna-ho a provar"; +"codex_models_reveal_pricing_calculation" = "Mostra el càlcul de preus"; +"codex_models_search_empty_description" = "La cerca només filtra aquesta taula; els totals del tauler no canvien."; +"codex_models_search_models" = "Cerca models"; +"codex_models_search_scope" = "%1$@ · La cerca només filtra aquesta taula"; +"codex_models_search_table" = "Taula de cerca de models"; +"codex_models_select_model" = "Selecciona %1$@"; +"codex_models_selected" = "Seleccionat"; +"codex_models_selected_bucket" = "Cubeta seleccionada"; +"codex_models_selected_bucket_accessibility" = "Dipòsit seleccionat: %1$@, %2$@, %3$@ fitxes, %4$@ referències de sessió, %5$@, moneda %6$@."; +"codex_models_selected_clear_available" = "Model seleccionat %1$@, selecció clara disponible"; +"codex_models_selected_date" = "Data seleccionada"; +"codex_models_selected_hidden_by_search" = "Model seleccionat amagat per la cerca"; +"codex_models_selected_model" = "Model seleccionat"; +"codex_models_selected_model_interval" = "Model seleccionat · %1$@"; +"codex_models_selected_suffix" = ", seleccionat"; +"codex_models_session_reference_count" = "%1$@ referències de sessió"; +"codex_models_session_references" = "Referències de sessió"; +"codex_models_session_references_lowercase" = "referències de sessió"; +"codex_models_session_refs" = "Referències de sessió"; +"codex_models_share" = "Comparteix"; +"codex_models_share_known_cost_help" = "Part del cost conegut; exclou l'ús sense preu"; +"codex_models_share_of" = "Part de %1$@"; +"codex_models_show_associated_sessions" = "Mostra les sessions associades"; +"codex_models_show_details" = "Mostra els detalls"; +"codex_models_showing_last_snapshot" = "S'està mostrant la darrera instantània correcta"; +"codex_models_sorted_by" = "Ordenat per %1$@"; +"codex_models_subject_over_time" = "%1$@ al llarg del temps"; +"codex_models_summary" = "Resum de models"; +"codex_models_timeline_accessibility" = "Cronologia dels models, %1$@, %2$@"; +"codex_models_timeline_hint" = "Feu clic a un grup de temps o manteniu el punter sobre un durant 1,5 segons per inspeccionar-ne els detalls."; +"codex_models_timeline_point_accessibility" = "%1$@, %2$@, %3$@ fitxes, %4$@ referències de sessions, %5$@, moneda %6$@"; +"codex_models_timeline_summary" = "%1$@ %2$@ cronologia. %3$@"; +"codex_models_token_count" = "%1$@ fitxes"; +"codex_models_tokens" = "Fitxes"; +"codex_models_tokens_lowercase" = "fitxes"; +"codex_models_tokens_per_session_reference" = "Uns %1$@ fitxes per referència de sessió."; +"codex_models_top" = "Amunt"; +"codex_models_top_model" = "Model superior"; +"codex_models_top_seven_other" = "Top 7 + Altres"; +"codex_models_top_two_account_for" = "Els dos models principals representen %1$@ de %2$@"; +"codex_models_top_two_of" = "Els dos models principals de %1$@"; +"codex_models_total_session_references_lowercase" = "referències totals de sessions"; +"codex_models_total_tokens" = "Total de fitxes"; +"codex_models_total_tokens_lowercase" = "fitxes totals"; +"codex_models_totals_reconcile" = "· Reconciliació dels totals amb la instantània activa"; +"codex_models_track_end" = "Final de la pista"; +"codex_models_track_start" = "Inici de la pista"; +"codex_models_trend_unavailable_observed_buckets" = "Dipòsits observats: %1$d. Tendència no disponible."; +"codex_models_unknown_export_error" = "Error d'exportació desconegut"; +"codex_models_unpriced_tokens" = "Fitxes sense preu"; +"codex_models_updated" = "Actualitzat %1$@"; +"codex_models_usage_and_change" = "Ús i canvi"; +"codex_models_usage_by_model" = "Ús per model"; +"codex_models_value_comparison_accessibility" = "%1$@, %2$@"; +"codex_models_value_detail" = "%1$@ · %2$@"; +"codex_models_value_share" = "%1$@ · %2$@ comparteix"; +"codex_models_value_share_accessibility" = "%1$@, %2$@ comparteix"; +"codex_models_value_start" = "Valor inicial"; +"codex_models_visible_of_total" = "Models visibles: %1$d de %2$d"; +"codex_models_visible_rows" = "Files visibles"; +"codex_models_vs_previous" = "%1$@ vs anterior"; +"codex_models_week_lowercase" = "setmana"; +"codex_workspaces_all_models" = "Tots els models"; +"codex_workspaces_all_workspaces" = "Tots els espais de treball"; +"codex_workspaces_associated_with_selected_model" = "Associat amb el model seleccionat"; +"codex_workspaces_bullet_value" = "· %1$@"; +"codex_workspaces_chart_keyboard_hint" = "Utilitzeu les tecles de fletxa esquerra i dreta o les accions d'ajust de VoiceOver per seleccionar un dia. Premeu Retorn o Espai per fixar-lo."; +"codex_workspaces_chats_description" = "Xats i converses"; +"codex_workspaces_copy_path" = "Copia el camí"; +"codex_workspaces_cost_unavailable" = "Cost no disponible"; +"codex_workspaces_daily_callout_accessibility" = "%1$@, %2$@"; +"codex_workspaces_daily_point_accessibility" = "%1$@, fitxes %2$@"; +"codex_workspaces_daily_point_help" = "%1$@ — fitxes %2$@"; +"codex_workspaces_daily_token_usage" = "Ús diari de tòkens"; +"codex_workspaces_daily_unavailable" = "L'ús diari no està disponible"; +"codex_workspaces_daily_unavailable_description" = "El total s'ha calculat a partir de %d sessions, però no se n'han pogut determinar les dates."; +"codex_workspaces_date" = "Data"; +"codex_workspaces_days" = "Dies"; +"codex_workspaces_elevated_usage" = "Ús elevat"; +"codex_workspaces_elevated_usage_help" = "L'ús supera la mediana del període seleccionat."; +"codex_workspaces_estimated_cost_short" = "Cost estimat"; +"codex_workspaces_estimated_cost_value" = "est. %@"; +"codex_workspaces_estimated_local_logs" = "Estimat a partir dels registres locals de Codex"; +"codex_workspaces_failed_read_logs" = "No s'han pogut llegir els registres de Codex"; +"codex_workspaces_failed_some_logs" = "No s'han pogut llegir alguns registres de Codex"; +"codex_workspaces_finalizing_project_index_files" = "S'està finalitzant l'índex de projectes… %d fitxers"; +"codex_workspaces_high_usage" = "Ús alt"; +"codex_workspaces_high_usage_help" = "L'ús és un valor atípic significatiu per al període seleccionat."; +"codex_workspaces_indexed_files" = "Actualitzat fa %@ · %d fitxers indexats"; +"codex_workspaces_indexing_local_logs" = "S'estan indexant els registres locals de Codex…"; +"codex_workspaces_indexing_projects" = "S'estan indexant els projectes…"; +"codex_workspaces_indexing_projects_files_left" = "S'estan indexant els projectes… %d/%d fitxers, en queden %d"; +"codex_workspaces_last_days" = "Últims %d dies"; +"codex_workspaces_local_codex_chat" = "Xat local de Codex"; +"codex_workspaces_model" = "Model"; +"codex_workspaces_models" = "Models"; +"codex_workspaces_no_daily_usage" = "No hi ha ús diari en aquest interval de l'historial."; +"codex_workspaces_no_local_usage" = "Encara no hi ha ús local de Codex"; +"codex_workspaces_no_models" = "No hi ha cap desglossament per model disponible"; +"codex_workspaces_no_projects_or_chats" = "No s'ha trobat cap projecte ni xat en aquest interval de l'historial."; +"codex_workspaces_no_sessions" = "No hi ha sessions en aquest espai de treball"; +"codex_workspaces_normal_usage" = "Ús normal"; +"codex_workspaces_not_available" = "No disponible"; +"codex_workspaces_overview" = "Resum"; +"codex_workspaces_partial_breakdown_help" = "El resum continua sent la referència mentre les metadades de data o model no disponibles es mostren com a parcials."; +"codex_workspaces_partial_breakdown_notice" = "Alguns desglossaments detallats no cobreixen el total complet."; +"codex_workspaces_partial_data" = "Dades parcials"; +"codex_workspaces_percent_total" = "% del total"; +"codex_workspaces_refresh_usage_data" = "Actualitza les dades d'ús"; +"codex_workspaces_relative_usage_accessibility" = "%d per cent de l'ús màxim d'un espai de treball"; +"codex_workspaces_relative_usage_tooltip" = "Utilitza el %d%% dels tòkens de l'espai de treball amb més ús en aquest període."; +"codex_workspaces_reveal_in_finder" = "Mostra-ho al Finder"; +"codex_workspaces_saving_project_index" = "S'està desant l'índex de projectes…"; +"codex_workspaces_scanning_logs" = "S'estan analitzant els registres de Codex…"; +"codex_workspaces_search_sessions" = "Cerca sessions"; +"codex_workspaces_selected_day_accessibility" = "%@: %@ tòkens"; +"codex_workspaces_session" = "Sessió"; +"codex_workspaces_session_count" = "Sessions: %d"; +"codex_workspaces_sessions" = "Sessions"; +"codex_workspaces_showing_last_complete_index" = "Es mostra l'últim índex local complet"; +"codex_workspaces_stale_status" = "L'índex d'ús està desactualitzat · Actualitzat fa %@"; +"codex_workspaces_started" = "Iniciada"; +"codex_workspaces_title" = "Espais de treball"; +"codex_workspaces_token_count_accessibility" = "%@ tòkens"; +"codex_workspaces_tokens" = "Tòkens"; +"codex_workspaces_top_model" = "Model principal"; +"codex_workspaces_top_sessions" = "Sessions principals"; +"codex_workspaces_total" = "Total"; +"codex_workspaces_view" = "Visualització"; +"codex_workspaces_view_all_sessions" = "Mostra totes les sessions"; +"codex_workspaces_window_title" = "Espais de treball de Codex"; +"codex_workspaces_workspace_count" = "Espais de treball i xats: %d"; +"codex_workspaces_workspaces_chats" = "Espais de treball/xats"; diff --git a/Sources/CodexBar/Resources/de.lproj/Localizable.strings b/Sources/CodexBar/Resources/de.lproj/Localizable.strings index 2986c311d4..0a8bd88366 100644 --- a/Sources/CodexBar/Resources/de.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/de.lproj/Localizable.strings @@ -1350,3 +1350,255 @@ "Cost today unavailable" = "Kosten heute: Nicht verfügbar"; "30-day cost unavailable" = "Kosten 30 Tage: Nicht verfügbar"; "Resets" = "Zurücksetzungen"; + +/* Codex Workspaces and Models */ +"codex_workspaces_open_in_window" = "In Fenster öffnen"; +"codex_models_actions" = "Aktionen"; +"codex_models_active_and_new_count" = "Aktive Modelle: %1$d; neu aktiv: %2$d"; +"codex_models_active_count_new_unavailable" = "Aktive Modelle: %1$d; Neu aktive Anzahl nicht verfügbar"; +"codex_models_active_models" = "Aktive Modelle"; +"codex_models_alias_count" = "Aliase: %1$d"; +"codex_models_all_model_count" = "Alle Modelle: %1$d"; +"codex_models_all_models" = "Alle Modelle"; +"codex_models_all_rows" = "Alle Zeilen"; +"codex_models_analytics" = "Modellanalyse"; +"codex_models_analytics_controls" = "Modelliert Analysekontrollen"; +"codex_models_apply" = "Bewerben"; +"codex_models_bucket_count" = "Buckets (%2$@): %1$d"; +"codex_models_bucket_interval" = "Bucket-Intervall"; +"codex_models_bullet_value" = "· %1$@"; +"codex_models_canonical_id" = "Kanonische ID"; +"codex_models_canonical_model" = "Kanonisches Modell"; +"codex_models_change" = "Änderung"; +"codex_models_change_from_peak" = "%1$@ vom %2$@-Höhepunkt"; +"codex_models_change_from_previous_period" = "%1$@ aus der vorherigen gleichen Periode"; +"codex_models_choose_metric" = "Wählen Sie die Analysemetrik"; +"codex_models_clear_search" = "Suche löschen"; +"codex_models_clear_selection" = "Modellauswahl löschen"; +"codex_models_columns" = "Spalten"; +"codex_models_compact_billions_suffix" = "B"; +"codex_models_compact_millions_suffix" = "M"; +"codex_models_compact_thousands_suffix" = "K"; +"codex_models_contains_selected_suffix" = ", enthält das ausgewählte Modell"; +"codex_models_copy_aliases" = "Aliase kopieren"; +"codex_models_copy_canonical_id" = "Kanonische ID kopieren"; +"codex_models_copy_canonical_model_id" = "Kanonische Modell-ID kopieren"; +"codex_models_copy_raw_aliases" = "Rohe Aliase kopieren"; +"codex_models_cost_conclusions_limited" = "Kostenabschlüsse sind auf Token mit einem Preis von %1$@ beschränkt."; +"codex_models_cost_hidden" = "Kosten ausgeblendet"; +"codex_models_cost_status_currency" = "%1$@ · %2$@ · %3$@"; +"codex_models_cost_unavailable" = "Kosten nicht verfügbar"; +"codex_models_cost_unavailable_for_data" = "Für diese Daten sind keine Kosten verfügbar"; +"codex_models_coverage" = "Abdeckung"; +"codex_models_currency" = "Währung"; +"codex_models_current_metric" = "Aktuell %1$@"; +"codex_models_current_period_in_progress" = "Aktuell %1$@ in Bearbeitung"; +"codex_models_date" = "Datum"; +"codex_models_day_lowercase" = "Tag"; +"codex_models_days" = "Tage"; +"codex_models_delta_vs_prior" = "Δ vs. vorher"; +"codex_models_disabled_in_settings" = "In den Einstellungen deaktiviert"; +"codex_models_dominates_at" = "Dieses Modell dominiert %1$@ bei %2$@."; +"codex_models_ended" = "Beendet"; +"codex_models_export" = "Exportieren"; +"codex_models_export_all_filename" = "codex-models-all.csv"; +"codex_models_export_default_filename" = "codex-models.csv"; +"codex_models_export_failed" = "Modellanalysen konnten nicht exportiert werden"; +"codex_models_export_selected_filename" = "codex-model-%1$@.csv"; +"codex_models_export_selected_model" = "Ausgewähltes Modell exportieren"; +"codex_models_export_visible_filename" = "codex-models-visible.csv"; +"codex_models_export_visible_rows" = "Sichtbare Modellzeilen exportieren"; +"codex_models_focus_table_search" = "Focus Models-Tabellensuche"; +"codex_models_fully_priced" = "Vollständiger Preis"; +"codex_models_fully_priced_percent" = "100 % Preis"; +"codex_models_generated" = "Generiert %1$@"; +"codex_models_granularity_accessibility" = "Granularität, %1$@, %2$@"; +"codex_models_hidden" = "Versteckt"; +"codex_models_identity_aliases_help" = "%1$@ · %2$@ · Aliase: %3$@"; +"codex_models_identity_and_aliases" = "Identität und Aliase"; +"codex_models_indexing_last_days" = "Indizierung in den letzten %1$d Tagen…"; +"codex_models_interpretation" = "Interpretation"; +"codex_models_known_cost" = "Bekannte Kosten"; +"codex_models_known_cost_fully_priced_accessibility" = "Bekannte Kosten %1$@, vollständiger Preis"; +"codex_models_known_cost_lowercase" = "bekannte Kosten"; +"codex_models_known_cost_partial_accessibility" = "Bekannte Kosten %1$@, teilweise, %2$@ der bepreisten Token"; +"codex_models_last_days" = "Letzte %1$d Tage"; +"codex_models_local_logs_source" = "· Lokale Codex-Protokolle"; +"codex_models_major_contributor_at" = "Dieses Modell leistet einen wichtigen Beitrag bei %1$@."; +"codex_models_metric" = "Metrik"; +"codex_models_metric_over_time" = "%1$@ im Laufe der Zeit"; +"codex_models_model" = "Modell"; +"codex_models_model_concentration" = "Modellkonzentration"; +"codex_models_model_count" = "Modelle: %1$d"; +"codex_models_model_usage" = "Modellverwendung"; +"codex_models_month_lowercase" = "Monat"; +"codex_models_more_alias_count" = "Weitere Aliase: %1$d"; +"codex_models_name_canonical_id" = "%1$@, kanonische ID %2$@"; +"codex_models_name_value_accessibility" = "%1$@, %2$@"; +"codex_models_named_metric_over_time" = "%1$@ %2$@ im Laufe der Zeit"; +"codex_models_new" = "Neu"; +"codex_models_new_in_period" = "Neu in diesem Zeitraum"; +"codex_models_newly_active_count" = "Neu aktiv: %1$d"; +"codex_models_no_applicable_price" = "Kein anwendbarer Preis"; +"codex_models_no_change_from_previous_period" = "Keine Änderung gegenüber dem vorherigen gleichen Zeitraum"; +"codex_models_no_matching_models" = "Keine passenden Modelle"; +"codex_models_no_newly_active" = "Keine neu aktiven Modelle"; +"codex_models_no_top_model" = "Kein Topmodel"; +"codex_models_no_usage" = "Keine Verwendung"; +"codex_models_no_usage_description" = "Wählen Sie oben einen längeren Datumsbereich oder aktualisieren Sie, nachdem lokale Codex-Sitzungen gespeichert wurden."; +"codex_models_no_usage_in_period" = "Keine Nutzung in diesem Zeitraum"; +"codex_models_no_usage_in_range" = "Keine Modellverwendung in diesem Bereich"; +"codex_models_other_count" = "Andere (%1$d)"; +"codex_models_partial_priced" = "Teilweise · Preis %1$@"; +"codex_models_precision_table" = "Modellpräzisionstabelle"; +"codex_models_previous_known_cost" = "Vorherige bekannte Kosten"; +"codex_models_previous_metric" = "Vorheriger %1$@"; +"codex_models_previous_period_unavailable" = "Vorheriger Zeitraum ist nicht verfügbar"; +"codex_models_previous_unavailable" = "Vorheriger Artikel nicht verfügbar"; +"codex_models_priced_tokens" = "Bepreiste Token"; +"codex_models_pricing_calculation" = "Preisberechnung"; +"codex_models_pricing_coverage" = "Preisabdeckung"; +"codex_models_pricing_explanation" = "%1$@ deckt Token mit einem Preis von %2$@ ab. %3$@ Token ohne anwendbare Regel werden ausgeschlossen und nicht als Null geschätzt."; +"codex_models_ranking_by" = "Model-Ranking nach %1$@"; +"codex_models_ranking_hint" = "Klicken Sie auf eine Modellleiste, um deren Details zu öffnen."; +"codex_models_ranking_point_accessibility" = "%1$@, %2$@, %3$@ teilen"; +"codex_models_ranking_summary" = "Kanonische Modelle und andere sortiert nach %1$@."; +"codex_models_ref_count" = "%1$@ refs"; +"codex_models_refresh_analytics" = "Modellanalysen aktualisieren"; +"codex_models_refresh_command_help" = "Modellanalyse aktualisieren (Befehl-R)"; +"codex_models_refresh_model_usage" = "Modellverwendung aktualisieren"; +"codex_models_refreshing_last_snapshot" = "Aktualisiert · zeigt den letzten erfolgreichen Schnappschuss"; +"codex_models_retry" = "Wiederholen"; +"codex_models_reveal_pricing_calculation" = "Preisberechnung anzeigen"; +"codex_models_search_empty_description" = "Die Suche filtert nur diese Tabelle. Die Dashboard-Gesamtwerte bleiben unverändert."; +"codex_models_search_models" = "Modelle suchen"; +"codex_models_search_scope" = "%1$@ · Die Suche filtert nur diese Tabelle"; +"codex_models_search_table" = "Tabelle „Suchmodelle“."; +"codex_models_select_model" = "Wählen Sie %1$@"; +"codex_models_selected" = "Ausgewählt"; +"codex_models_selected_bucket" = "Ausgewählter Bucket"; +"codex_models_selected_bucket_accessibility" = "Ausgewählter Bucket: %1$@, %2$@, %3$@ Token, %4$@ Sitzungsreferenzen, %5$@, Währung %6$@."; +"codex_models_selected_clear_available" = "Ausgewähltes Modell %1$@, Auswahl löschen verfügbar"; +"codex_models_selected_date" = "Ausgewähltes Datum"; +"codex_models_selected_hidden_by_search" = "Ausgewähltes Modell durch Suche ausgeblendet"; +"codex_models_selected_model" = "Ausgewähltes Modell"; +"codex_models_selected_model_interval" = "Ausgewähltes Modell · %1$@"; +"codex_models_selected_suffix" = ", ausgewählt"; +"codex_models_session_reference_count" = "%1$@ Sitzungsreferenzen"; +"codex_models_session_references" = "Sitzungsreferenzen"; +"codex_models_session_references_lowercase" = "Sitzungsreferenzen"; +"codex_models_session_refs" = "Sitzungsref"; +"codex_models_share" = "Teilen"; +"codex_models_share_known_cost_help" = "Anteil der bekannten Kosten; Ausgenommen ist die unentgeltliche Nutzung"; +"codex_models_share_of" = "Anteil von %1$@"; +"codex_models_show_associated_sessions" = "Zugehörige Sitzungen anzeigen"; +"codex_models_show_details" = "Details anzeigen"; +"codex_models_showing_last_snapshot" = "Zeigt den letzten erfolgreichen Schnappschuss"; +"codex_models_sorted_by" = "Sortiert nach %1$@"; +"codex_models_subject_over_time" = "%1$@ im Laufe der Zeit"; +"codex_models_summary" = "Modellübersicht"; +"codex_models_timeline_accessibility" = "Zeitleiste der Modelle, %1$@, %2$@"; +"codex_models_timeline_hint" = "Klicken Sie auf einen Zeitrahmen oder halten Sie den Mauszeiger 1,5 Sekunden lang darüber, um dessen Details zu überprüfen."; +"codex_models_timeline_point_accessibility" = "%1$@, %2$@, %3$@ Token, %4$@ Sitzungsreferenzen, %5$@, Währung %6$@"; +"codex_models_timeline_summary" = "%1$@ %2$@ Zeitleiste. %3$@"; +"codex_models_token_count" = "%1$@ Token"; +"codex_models_tokens" = "Token"; +"codex_models_tokens_lowercase" = "Token"; +"codex_models_tokens_per_session_reference" = "Ungefähr %1$@ Token pro Sitzungsreferenz."; +"codex_models_top" = "Oben"; +"codex_models_top_model" = "Topmodell"; +"codex_models_top_seven_other" = "Top 7 + Sonstiges"; +"codex_models_top_two_account_for" = "Die beiden Top-Modelle machen %1$@ von %2$@ aus"; +"codex_models_top_two_of" = "Die beiden besten Modelle von %1$@"; +"codex_models_total_session_references_lowercase" = "Sitzungsreferenzen insgesamt"; +"codex_models_total_tokens" = "Gesamtzahl der Token"; +"codex_models_total_tokens_lowercase" = "Token insgesamt"; +"codex_models_totals_reconcile" = "· Gesamtwerte werden mit dem aktiven Snapshot abgeglichen"; +"codex_models_track_end" = "Gleisende"; +"codex_models_track_start" = "Gleisanfang"; +"codex_models_trend_unavailable_observed_buckets" = "Beobachtete Buckets: %1$d. Trend nicht verfügbar."; +"codex_models_unknown_export_error" = "Unbekannter Exportfehler"; +"codex_models_unpriced_tokens" = "Unbepreiste Token"; +"codex_models_updated" = "Aktualisiert %1$@"; +"codex_models_usage_and_change" = "Nutzung und Änderung"; +"codex_models_usage_by_model" = "Verwendung nach Modell"; +"codex_models_value_comparison_accessibility" = "%1$@, %2$@"; +"codex_models_value_detail" = "%1$@ · %2$@"; +"codex_models_value_share" = "%1$@ · %2$@ teilen"; +"codex_models_value_share_accessibility" = "%1$@, %2$@ teilen"; +"codex_models_value_start" = "Wertanfang"; +"codex_models_visible_of_total" = "Sichtbare Modelle: %1$d von %2$d"; +"codex_models_visible_rows" = "Sichtbare Zeilen"; +"codex_models_vs_previous" = "%1$@ im Vergleich zum vorherigen"; +"codex_models_week_lowercase" = "Woche"; +"codex_workspaces_all_models" = "Alle Modelle"; +"codex_workspaces_all_workspaces" = "Alle Arbeitsbereiche"; +"codex_workspaces_associated_with_selected_model" = "Mit dem ausgewählten Modell verknüpft"; +"codex_workspaces_bullet_value" = "· %1$@"; +"codex_workspaces_chart_keyboard_hint" = "Wähle mit der linken und rechten Pfeiltaste oder den VoiceOver-Anpassungsaktionen einen Tag aus. Drücke die Eingabe- oder Leertaste, um ihn anzuheften."; +"codex_workspaces_chats_description" = "Chats und Unterhaltungen"; +"codex_workspaces_copy_path" = "Pfad kopieren"; +"codex_workspaces_cost_unavailable" = "Kosten nicht verfügbar"; +"codex_workspaces_daily_callout_accessibility" = "%1$@, %2$@"; +"codex_workspaces_daily_point_accessibility" = "%1$@, Token %2$@"; +"codex_workspaces_daily_point_help" = "%1$@ – %2$@ Token"; +"codex_workspaces_daily_token_usage" = "Tägliche Token-Nutzung"; +"codex_workspaces_daily_unavailable" = "Tägliche Nutzung ist nicht verfügbar"; +"codex_workspaces_daily_unavailable_description" = "Die Summe wurde aus %d Sitzungen berechnet, ihre Daten konnten jedoch nicht ermittelt werden."; +"codex_workspaces_date" = "Datum"; +"codex_workspaces_days" = "Tage"; +"codex_workspaces_elevated_usage" = "Erhöhte Nutzung"; +"codex_workspaces_elevated_usage_help" = "Die Nutzung liegt im ausgewählten Zeitraum über dem Median."; +"codex_workspaces_estimated_cost_short" = "Gesch. Kosten"; +"codex_workspaces_estimated_cost_value" = "gesch. %@"; +"codex_workspaces_estimated_local_logs" = "Geschätzt anhand lokaler Codex-Protokolle"; +"codex_workspaces_failed_read_logs" = "Codex-Protokolle konnten nicht gelesen werden"; +"codex_workspaces_failed_some_logs" = "Einige Codex-Protokolle konnten nicht gelesen werden"; +"codex_workspaces_finalizing_project_index_files" = "Projektindex wird abgeschlossen… %d Dateien"; +"codex_workspaces_high_usage" = "Hohe Nutzung"; +"codex_workspaces_high_usage_help" = "Die Nutzung weicht im ausgewählten Zeitraum deutlich ab."; +"codex_workspaces_indexed_files" = "Vor %@ aktualisiert · %d indizierte Dateien"; +"codex_workspaces_indexing_local_logs" = "Lokale Codex-Protokolle werden indiziert…"; +"codex_workspaces_indexing_projects" = "Projekte werden indiziert…"; +"codex_workspaces_indexing_projects_files_left" = "Projekte werden indiziert… %d/%d Dateien, %d verbleibend"; +"codex_workspaces_last_days" = "Letzte %d Tage"; +"codex_workspaces_local_codex_chat" = "Lokaler Codex-Chat"; +"codex_workspaces_model" = "Modell"; +"codex_workspaces_models" = "Modelle"; +"codex_workspaces_no_daily_usage" = "Keine tägliche Nutzung in diesem Verlauf."; +"codex_workspaces_no_local_usage" = "Noch keine lokale Codex-Nutzung"; +"codex_workspaces_no_models" = "Keine Modellaufschlüsselung verfügbar"; +"codex_workspaces_no_projects_or_chats" = "In diesem Verlauf wurden keine Projekte oder Chats gefunden."; +"codex_workspaces_no_sessions" = "Keine Sitzungen in diesem Arbeitsbereich"; +"codex_workspaces_normal_usage" = "Normale Nutzung"; +"codex_workspaces_not_available" = "Nicht verfügbar"; +"codex_workspaces_overview" = "Übersicht"; +"codex_workspaces_partial_breakdown_help" = "Die Zusammenfassung bleibt maßgeblich; nicht verfügbare Datums- oder Modellmetadaten werden als unvollständig angezeigt."; +"codex_workspaces_partial_breakdown_notice" = "Einige Detailaufschlüsselungen decken die Gesamtsumme nicht vollständig ab."; +"codex_workspaces_partial_data" = "Unvollständige Daten"; +"codex_workspaces_percent_total" = "% der Gesamtsumme"; +"codex_workspaces_refresh_usage_data" = "Nutzungsdaten aktualisieren"; +"codex_workspaces_relative_usage_accessibility" = "%d Prozent der höchsten Arbeitsbereichsnutzung"; +"codex_workspaces_relative_usage_tooltip" = "Verwendet %d%% so viele Token wie der Arbeitsbereich mit der höchsten Nutzung in diesem Zeitraum."; +"codex_workspaces_reveal_in_finder" = "Im Finder anzeigen"; +"codex_workspaces_saving_project_index" = "Projektindex wird gespeichert…"; +"codex_workspaces_scanning_logs" = "Codex-Protokolle werden durchsucht…"; +"codex_workspaces_search_sessions" = "Sitzungen suchen"; +"codex_workspaces_selected_day_accessibility" = "%@: %@ Token"; +"codex_workspaces_session" = "Sitzung"; +"codex_workspaces_session_count" = "Sitzungen: %d"; +"codex_workspaces_sessions" = "Sitzungen"; +"codex_workspaces_showing_last_complete_index" = "Letzter vollständiger lokaler Index wird angezeigt"; +"codex_workspaces_stale_status" = "Nutzungsindex ist veraltet · Vor %@ aktualisiert"; +"codex_workspaces_started" = "Gestartet"; +"codex_workspaces_title" = "Arbeitsbereiche"; +"codex_workspaces_token_count_accessibility" = "%@ Token"; +"codex_workspaces_tokens" = "Token"; +"codex_workspaces_top_model" = "Häufigstes Modell"; +"codex_workspaces_top_sessions" = "Top-Sitzungen"; +"codex_workspaces_total" = "Gesamt"; +"codex_workspaces_view" = "Ansicht"; +"codex_workspaces_view_all_sessions" = "Alle Sitzungen anzeigen"; +"codex_workspaces_window_title" = "Codex-Arbeitsbereiche"; +"codex_workspaces_workspace_count" = "Arbeitsbereiche und Chats: %d"; +"codex_workspaces_workspaces_chats" = "Arbeitsbereiche/Chats"; diff --git a/Sources/CodexBar/Resources/en.lproj/Localizable.strings b/Sources/CodexBar/Resources/en.lproj/Localizable.strings index 18b42e8801..0a10e5a480 100644 --- a/Sources/CodexBar/Resources/en.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/en.lproj/Localizable.strings @@ -1354,3 +1354,255 @@ "Cost today unavailable" = "Cost today unavailable"; "30-day cost unavailable" = "30-day cost unavailable"; "Resets" = "Resets"; + +/* Codex Workspaces and Models */ +"codex_workspaces_open_in_window" = "Open in Window"; +"codex_models_actions" = "Actions"; +"codex_models_active_and_new_count" = "Active models: %1$d; newly active: %2$d"; +"codex_models_active_count_new_unavailable" = "Active models: %1$d; newly active count unavailable"; +"codex_models_active_models" = "Active models"; +"codex_models_alias_count" = "Aliases: %1$d"; +"codex_models_all_model_count" = "All models: %1$d"; +"codex_models_all_models" = "All models"; +"codex_models_all_rows" = "All rows"; +"codex_models_analytics" = "Models analytics"; +"codex_models_analytics_controls" = "Models analytics controls"; +"codex_models_apply" = "Apply"; +"codex_models_bucket_count" = "Buckets (%2$@): %1$d"; +"codex_models_bucket_interval" = "Bucket interval"; +"codex_models_bullet_value" = "· %1$@"; +"codex_models_canonical_id" = "Canonical ID"; +"codex_models_canonical_model" = "Canonical model"; +"codex_models_change" = "Change"; +"codex_models_change_from_peak" = "%1$@ from %2$@ peak"; +"codex_models_change_from_previous_period" = "%1$@ from the previous equal period"; +"codex_models_choose_metric" = "Choose the analytics metric"; +"codex_models_clear_search" = "Clear Search"; +"codex_models_clear_selection" = "Clear Models selection"; +"codex_models_columns" = "Columns"; +"codex_models_compact_billions_suffix" = "B"; +"codex_models_compact_millions_suffix" = "M"; +"codex_models_compact_thousands_suffix" = "K"; +"codex_models_contains_selected_suffix" = ", contains selected model"; +"codex_models_copy_aliases" = "Copy aliases"; +"codex_models_copy_canonical_id" = "Copy canonical ID"; +"codex_models_copy_canonical_model_id" = "Copy canonical model ID"; +"codex_models_copy_raw_aliases" = "Copy raw aliases"; +"codex_models_cost_conclusions_limited" = "Cost conclusions are limited to %1$@ priced tokens."; +"codex_models_cost_hidden" = "Cost hidden"; +"codex_models_cost_status_currency" = "%1$@ · %2$@ · %3$@"; +"codex_models_cost_unavailable" = "Cost unavailable"; +"codex_models_cost_unavailable_for_data" = "Cost is unavailable for this data"; +"codex_models_coverage" = "Coverage"; +"codex_models_currency" = "Currency"; +"codex_models_current_metric" = "Current %1$@"; +"codex_models_current_period_in_progress" = "Current %1$@ in progress"; +"codex_models_date" = "Date"; +"codex_models_day_lowercase" = "day"; +"codex_models_days" = "Days"; +"codex_models_delta_vs_prior" = "Δ vs prior"; +"codex_models_disabled_in_settings" = "Disabled in settings"; +"codex_models_dominates_at" = "This model dominates %1$@ at %2$@."; +"codex_models_ended" = "Ended"; +"codex_models_export" = "Export"; +"codex_models_export_all_filename" = "codex-models-all.csv"; +"codex_models_export_default_filename" = "codex-models.csv"; +"codex_models_export_failed" = "Couldn’t export Models analytics"; +"codex_models_export_selected_filename" = "codex-model-%1$@.csv"; +"codex_models_export_selected_model" = "Export selected model"; +"codex_models_export_visible_filename" = "codex-models-visible.csv"; +"codex_models_export_visible_rows" = "Export visible Models rows"; +"codex_models_focus_table_search" = "Focus Models table search"; +"codex_models_fully_priced" = "Fully priced"; +"codex_models_fully_priced_percent" = "100% priced"; +"codex_models_generated" = "Generated %1$@"; +"codex_models_granularity_accessibility" = "Granularity, %1$@, %2$@"; +"codex_models_hidden" = "Hidden"; +"codex_models_identity_aliases_help" = "%1$@ · %2$@ · Aliases: %3$@"; +"codex_models_identity_and_aliases" = "Identity and aliases"; +"codex_models_indexing_last_days" = "Indexing last %1$d days…"; +"codex_models_interpretation" = "Interpretation"; +"codex_models_known_cost" = "Known cost"; +"codex_models_known_cost_fully_priced_accessibility" = "Known cost %1$@, fully priced"; +"codex_models_known_cost_lowercase" = "known cost"; +"codex_models_known_cost_partial_accessibility" = "Known cost %1$@, partial, %2$@ of tokens priced"; +"codex_models_last_days" = "Last %1$d days"; +"codex_models_local_logs_source" = "· Local Codex logs"; +"codex_models_major_contributor_at" = "This model is a major contributor at %1$@."; +"codex_models_metric" = "Metric"; +"codex_models_metric_over_time" = "%1$@ over time"; +"codex_models_model" = "Model"; +"codex_models_model_concentration" = "Model concentration"; +"codex_models_model_count" = "Models: %1$d"; +"codex_models_model_usage" = "Model usage"; +"codex_models_month_lowercase" = "month"; +"codex_models_more_alias_count" = "More aliases: %1$d"; +"codex_models_name_canonical_id" = "%1$@, canonical ID %2$@"; +"codex_models_name_value_accessibility" = "%1$@, %2$@"; +"codex_models_named_metric_over_time" = "%1$@ %2$@ over time"; +"codex_models_new" = "New"; +"codex_models_new_in_period" = "New in this period"; +"codex_models_newly_active_count" = "Newly active: %1$d"; +"codex_models_no_applicable_price" = "No applicable price"; +"codex_models_no_change_from_previous_period" = "No change from the previous equal period"; +"codex_models_no_matching_models" = "No matching models"; +"codex_models_no_newly_active" = "No newly active models"; +"codex_models_no_top_model" = "No top model"; +"codex_models_no_usage" = "No usage"; +"codex_models_no_usage_description" = "Choose a longer date range above or refresh after local Codex sessions are saved."; +"codex_models_no_usage_in_period" = "No usage in this period"; +"codex_models_no_usage_in_range" = "No model usage in this range"; +"codex_models_other_count" = "Other (%1$d)"; +"codex_models_partial_priced" = "Partial · %1$@ priced"; +"codex_models_precision_table" = "Model precision table"; +"codex_models_previous_known_cost" = "Previous known cost"; +"codex_models_previous_metric" = "Previous %1$@"; +"codex_models_previous_period_unavailable" = "Previous period is unavailable"; +"codex_models_previous_unavailable" = "Previous unavailable"; +"codex_models_priced_tokens" = "Priced tokens"; +"codex_models_pricing_calculation" = "Pricing calculation"; +"codex_models_pricing_coverage" = "Pricing coverage"; +"codex_models_pricing_explanation" = "%1$@ covers %2$@ priced tokens. %3$@ tokens without an applicable rule are excluded, not estimated as zero."; +"codex_models_ranking_by" = "Model ranking by %1$@"; +"codex_models_ranking_hint" = "Click a model bar to open its details."; +"codex_models_ranking_point_accessibility" = "%1$@, %2$@, %3$@ share"; +"codex_models_ranking_summary" = "Canonical models and Other ranked by %1$@."; +"codex_models_ref_count" = "%1$@ refs"; +"codex_models_refresh_analytics" = "Refresh Models analytics"; +"codex_models_refresh_command_help" = "Refresh Models analytics (Command-R)"; +"codex_models_refresh_model_usage" = "Refresh Model Usage"; +"codex_models_refreshing_last_snapshot" = "Refreshing · showing last successful snapshot"; +"codex_models_retry" = "Retry"; +"codex_models_reveal_pricing_calculation" = "Reveal pricing calculation"; +"codex_models_search_empty_description" = "Search filters this table only; dashboard totals are unchanged."; +"codex_models_search_models" = "Search models"; +"codex_models_search_scope" = "%1$@ · Search filters this table only"; +"codex_models_search_table" = "Search Models table"; +"codex_models_select_model" = "Select %1$@"; +"codex_models_selected" = "Selected"; +"codex_models_selected_bucket" = "Selected bucket"; +"codex_models_selected_bucket_accessibility" = "Selected bucket: %1$@, %2$@, %3$@ tokens, %4$@ session references, %5$@, currency %6$@."; +"codex_models_selected_clear_available" = "Selected model %1$@, Clear selection available"; +"codex_models_selected_date" = "Selected date"; +"codex_models_selected_hidden_by_search" = "Selected model hidden by search"; +"codex_models_selected_model" = "Selected model"; +"codex_models_selected_model_interval" = "Selected model · %1$@"; +"codex_models_selected_suffix" = ", selected"; +"codex_models_session_reference_count" = "%1$@ session references"; +"codex_models_session_references" = "Session references"; +"codex_models_session_references_lowercase" = "session references"; +"codex_models_session_refs" = "Session refs"; +"codex_models_share" = "Share"; +"codex_models_share_known_cost_help" = "Share of known cost; excludes unpriced usage"; +"codex_models_share_of" = "Share of %1$@"; +"codex_models_show_associated_sessions" = "Show associated sessions"; +"codex_models_show_details" = "Show details"; +"codex_models_showing_last_snapshot" = "Showing last successful snapshot"; +"codex_models_sorted_by" = "Sorted by %1$@"; +"codex_models_subject_over_time" = "%1$@ over time"; +"codex_models_summary" = "Models summary"; +"codex_models_timeline_accessibility" = "Models timeline, %1$@, %2$@"; +"codex_models_timeline_hint" = "Click a time bucket, or hold the pointer over one for 1.5 seconds, to inspect its details."; +"codex_models_timeline_point_accessibility" = "%1$@, %2$@, %3$@ tokens, %4$@ session references, %5$@, currency %6$@"; +"codex_models_timeline_summary" = "%1$@ %2$@ timeline. %3$@"; +"codex_models_token_count" = "%1$@ tokens"; +"codex_models_tokens" = "Tokens"; +"codex_models_tokens_lowercase" = "tokens"; +"codex_models_tokens_per_session_reference" = "About %1$@ tokens per session reference."; +"codex_models_top" = "Top"; +"codex_models_top_model" = "Top model"; +"codex_models_top_seven_other" = "Top 7 + Other"; +"codex_models_top_two_account_for" = "Top two models account for %1$@ of %2$@"; +"codex_models_top_two_of" = "Top two models of %1$@"; +"codex_models_total_session_references_lowercase" = "total session references"; +"codex_models_total_tokens" = "Total tokens"; +"codex_models_total_tokens_lowercase" = "total tokens"; +"codex_models_totals_reconcile" = "· Totals reconcile to the active snapshot"; +"codex_models_track_end" = "Track end"; +"codex_models_track_start" = "Track start"; +"codex_models_trend_unavailable_observed_buckets" = "Observed buckets: %1$d. Trend unavailable."; +"codex_models_unknown_export_error" = "Unknown export error"; +"codex_models_unpriced_tokens" = "Unpriced tokens"; +"codex_models_updated" = "Updated %1$@"; +"codex_models_usage_and_change" = "Usage and change"; +"codex_models_usage_by_model" = "Usage by model"; +"codex_models_value_comparison_accessibility" = "%1$@, %2$@"; +"codex_models_value_detail" = "%1$@ · %2$@"; +"codex_models_value_share" = "%1$@ · %2$@ share"; +"codex_models_value_share_accessibility" = "%1$@, %2$@ share"; +"codex_models_value_start" = "Value start"; +"codex_models_visible_of_total" = "Visible models: %1$d of %2$d"; +"codex_models_visible_rows" = "Visible rows"; +"codex_models_vs_previous" = "%1$@ vs previous"; +"codex_models_week_lowercase" = "week"; +"codex_workspaces_all_models" = "All Models"; +"codex_workspaces_all_workspaces" = "All Workspaces"; +"codex_workspaces_associated_with_selected_model" = "Associated with selected model"; +"codex_workspaces_bullet_value" = "· %1$@"; +"codex_workspaces_chart_keyboard_hint" = "Use the Left and Right Arrow keys or VoiceOver adjust actions to select a day. Press Return or Space to pin it."; +"codex_workspaces_chats_description" = "Chats and conversations"; +"codex_workspaces_copy_path" = "Copy Path"; +"codex_workspaces_cost_unavailable" = "Cost unavailable"; +"codex_workspaces_daily_callout_accessibility" = "%1$@, %2$@"; +"codex_workspaces_daily_point_accessibility" = "%1$@, tokens %2$@"; +"codex_workspaces_daily_point_help" = "%1$@ — %2$@ tokens"; +"codex_workspaces_daily_token_usage" = "Daily token usage"; +"codex_workspaces_daily_unavailable" = "Daily usage is unavailable"; +"codex_workspaces_daily_unavailable_description" = "The total was calculated from %d sessions, but their dates could not be resolved."; +"codex_workspaces_date" = "Date"; +"codex_workspaces_days" = "Days"; +"codex_workspaces_elevated_usage" = "Elevated usage"; +"codex_workspaces_elevated_usage_help" = "Usage is above the median for the selected period."; +"codex_workspaces_estimated_cost_short" = "Est. cost"; +"codex_workspaces_estimated_cost_value" = "est. %@"; +"codex_workspaces_estimated_local_logs" = "Estimated from local Codex logs"; +"codex_workspaces_failed_read_logs" = "Failed to read Codex logs"; +"codex_workspaces_failed_some_logs" = "Failed to read some Codex logs"; +"codex_workspaces_finalizing_project_index_files" = "Finalizing project index… %d files"; +"codex_workspaces_high_usage" = "High usage"; +"codex_workspaces_high_usage_help" = "Usage is a significant outlier for the selected period."; +"codex_workspaces_indexed_files" = "Updated %@ · %d indexed files"; +"codex_workspaces_indexing_local_logs" = "Indexing local Codex logs…"; +"codex_workspaces_indexing_projects" = "Indexing projects…"; +"codex_workspaces_indexing_projects_files_left" = "Indexing projects… %d/%d files, %d left"; +"codex_workspaces_last_days" = "Last %d days"; +"codex_workspaces_local_codex_chat" = "Local Codex chat"; +"codex_workspaces_model" = "Model"; +"codex_workspaces_models" = "Models"; +"codex_workspaces_no_daily_usage" = "No daily usage in this history window."; +"codex_workspaces_no_local_usage" = "No local Codex usage yet"; +"codex_workspaces_no_models" = "No model breakdown available"; +"codex_workspaces_no_projects_or_chats" = "No projects or chats found in this history window."; +"codex_workspaces_no_sessions" = "No sessions in this workspace"; +"codex_workspaces_normal_usage" = "Normal usage"; +"codex_workspaces_not_available" = "Not available"; +"codex_workspaces_overview" = "Overview"; +"codex_workspaces_partial_breakdown_help" = "The summary remains authoritative while unavailable dated or model metadata is shown as partial."; +"codex_workspaces_partial_breakdown_notice" = "Some detailed breakdowns do not cover the complete total."; +"codex_workspaces_partial_data" = "Partial data"; +"codex_workspaces_percent_total" = "% of total"; +"codex_workspaces_refresh_usage_data" = "Refresh usage data"; +"codex_workspaces_relative_usage_accessibility" = "%d percent of the highest workspace usage"; +"codex_workspaces_relative_usage_tooltip" = "Uses %d%% as many tokens as the highest-usage workspace in this period."; +"codex_workspaces_reveal_in_finder" = "Reveal in Finder"; +"codex_workspaces_saving_project_index" = "Saving project index…"; +"codex_workspaces_scanning_logs" = "Scanning Codex logs…"; +"codex_workspaces_search_sessions" = "Search sessions"; +"codex_workspaces_selected_day_accessibility" = "%@: %@ tokens"; +"codex_workspaces_session" = "Session"; +"codex_workspaces_session_count" = "Sessions: %d"; +"codex_workspaces_sessions" = "Sessions"; +"codex_workspaces_showing_last_complete_index" = "Showing the last complete local index"; +"codex_workspaces_stale_status" = "Usage index is stale · Updated %@"; +"codex_workspaces_started" = "Started"; +"codex_workspaces_title" = "Workspaces"; +"codex_workspaces_token_count_accessibility" = "%@ tokens"; +"codex_workspaces_tokens" = "Tokens"; +"codex_workspaces_top_model" = "Top model"; +"codex_workspaces_top_sessions" = "Top sessions"; +"codex_workspaces_total" = "Total"; +"codex_workspaces_view" = "View"; +"codex_workspaces_view_all_sessions" = "View All Sessions"; +"codex_workspaces_window_title" = "Codex Workspaces"; +"codex_workspaces_workspace_count" = "Workspaces and chats: %d"; +"codex_workspaces_workspaces_chats" = "Workspaces/chats"; diff --git a/Sources/CodexBar/Resources/es.lproj/Localizable.strings b/Sources/CodexBar/Resources/es.lproj/Localizable.strings index 656ac7975c..b56a28a2ec 100644 --- a/Sources/CodexBar/Resources/es.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/es.lproj/Localizable.strings @@ -1348,3 +1348,255 @@ "Cost today unavailable" = "Coste de hoy: No disponible"; "30-day cost unavailable" = "Coste de 30 días: No disponible"; "Resets" = "Reinicios"; + +/* Codex Workspaces and Models */ +"codex_workspaces_open_in_window" = "Abrir en una ventana"; +"codex_models_actions" = "Acciones"; +"codex_models_active_and_new_count" = "Modelos activos: %1$d; recién activo: %2$d"; +"codex_models_active_count_new_unavailable" = "Modelos activos: %1$d; recuento recientemente activo no disponible"; +"codex_models_active_models" = "Modelos activos"; +"codex_models_alias_count" = "Alias: %1$d"; +"codex_models_all_model_count" = "Todos los modelos: %1$d"; +"codex_models_all_models" = "Todos los modelos"; +"codex_models_all_rows" = "Todas las filas"; +"codex_models_analytics" = "Análisis de modelos"; +"codex_models_analytics_controls" = "Controles de análisis de modelos"; +"codex_models_apply" = "Aplicar"; +"codex_models_bucket_count" = "Depósitos (%2$@): %1$d"; +"codex_models_bucket_interval" = "Intervalo del cucharón"; +"codex_models_bullet_value" = "· %1$@"; +"codex_models_canonical_id" = "ID canónica"; +"codex_models_canonical_model" = "Modelo canónico"; +"codex_models_change" = "Cambiar"; +"codex_models_change_from_peak" = "%1$@ desde %2$@ pico"; +"codex_models_change_from_previous_period" = "%1$@ del período igual anterior"; +"codex_models_choose_metric" = "Elija la métrica de análisis"; +"codex_models_clear_search" = "Borrar búsqueda"; +"codex_models_clear_selection" = "Borrar selección de modelos"; +"codex_models_columns" = "Columnas"; +"codex_models_compact_billions_suffix" = "B"; +"codex_models_compact_millions_suffix" = "M"; +"codex_models_compact_thousands_suffix" = "K"; +"codex_models_contains_selected_suffix" = ", contiene el modelo seleccionado"; +"codex_models_copy_aliases" = "Copiar alias"; +"codex_models_copy_canonical_id" = "Copia cédula canónica"; +"codex_models_copy_canonical_model_id" = "Copiar ID de modelo canónico"; +"codex_models_copy_raw_aliases" = "Copiar alias sin formato"; +"codex_models_cost_conclusions_limited" = "Las conclusiones de costos se limitan a tokens con precio de %1$@."; +"codex_models_cost_hidden" = "Costo oculto"; +"codex_models_cost_status_currency" = "%1$@ · %2$@ · %3$@"; +"codex_models_cost_unavailable" = "Costo no disponible"; +"codex_models_cost_unavailable_for_data" = "El costo no está disponible para estos datos."; +"codex_models_coverage" = "Cobertura"; +"codex_models_currency" = "Moneda"; +"codex_models_current_metric" = "Actual %1$@"; +"codex_models_current_period_in_progress" = "%1$@ actual en progreso"; +"codex_models_date" = "Fecha"; +"codex_models_day_lowercase" = "día"; +"codex_models_days" = "Días"; +"codex_models_delta_vs_prior" = "Δ frente a anterior"; +"codex_models_disabled_in_settings" = "Deshabilitado en la configuración"; +"codex_models_dominates_at" = "Este modelo domina %1$@ en %2$@."; +"codex_models_ended" = "Finalizado"; +"codex_models_export" = "Exportar"; +"codex_models_export_all_filename" = "codex-models-all.csv"; +"codex_models_export_default_filename" = "codex-models.csv"; +"codex_models_export_failed" = "No se pudieron exportar análisis de modelos"; +"codex_models_export_selected_filename" = "codex-model-%1$@.csv"; +"codex_models_export_selected_model" = "Exportar modelo seleccionado"; +"codex_models_export_visible_filename" = "codex-models-visible.csv"; +"codex_models_export_visible_rows" = "Exportar filas de modelos visibles"; +"codex_models_focus_table_search" = "Búsqueda de tabla de modelos de enfoque"; +"codex_models_fully_priced" = "Precio completo"; +"codex_models_fully_priced_percent" = "100% de precio"; +"codex_models_generated" = "Generado %1$@"; +"codex_models_granularity_accessibility" = "Granularidad, %1$@, %2$@"; +"codex_models_hidden" = "Oculto"; +"codex_models_identity_aliases_help" = "%1$@ · %2$@ · Alias: %3$@"; +"codex_models_identity_and_aliases" = "Identidad y alias"; +"codex_models_indexing_last_days" = "Indexación de los últimos %1$d días…"; +"codex_models_interpretation" = "Interpretación"; +"codex_models_known_cost" = "Costo conocido"; +"codex_models_known_cost_fully_priced_accessibility" = "Costo conocido %1$@, precio completo"; +"codex_models_known_cost_lowercase" = "costo conocido"; +"codex_models_known_cost_partial_accessibility" = "Costo conocido %1$@, parcial, %2$@ de tokens cotizados"; +"codex_models_last_days" = "Últimos %1$d días"; +"codex_models_local_logs_source" = "· Registros del Codex local"; +"codex_models_major_contributor_at" = "Este modelo contribuye de manera importante a %1$@."; +"codex_models_metric" = "Métrica"; +"codex_models_metric_over_time" = "%1$@ a lo largo del tiempo"; +"codex_models_model" = "Modelo"; +"codex_models_model_concentration" = "Concentración del modelo"; +"codex_models_model_count" = "Modelos: %1$d"; +"codex_models_model_usage" = "Uso del modelo"; +"codex_models_month_lowercase" = "mes"; +"codex_models_more_alias_count" = "Más alias: %1$d"; +"codex_models_name_canonical_id" = "%1$@, ID canónico %2$@"; +"codex_models_name_value_accessibility" = "%1$@, %2$@"; +"codex_models_named_metric_over_time" = "%1$@ %2$@ a lo largo del tiempo"; +"codex_models_new" = "Nuevo"; +"codex_models_new_in_period" = "Nuevo en este periodo"; +"codex_models_newly_active_count" = "Recién activo: %1$d"; +"codex_models_no_applicable_price" = "No hay precio aplicable"; +"codex_models_no_change_from_previous_period" = "Sin cambios respecto al período igual anterior"; +"codex_models_no_matching_models" = "No hay modelos coincidentes"; +"codex_models_no_newly_active" = "No hay modelos activos recientemente"; +"codex_models_no_top_model" = "Sin top model"; +"codex_models_no_usage" = "Sin uso"; +"codex_models_no_usage_description" = "Elija un rango de fechas más largo arriba o actualice después de guardar las sesiones locales del Codex."; +"codex_models_no_usage_in_period" = "Sin uso en este período"; +"codex_models_no_usage_in_range" = "No se utiliza ningún modelo en este rango"; +"codex_models_other_count" = "Otro (%1$d)"; +"codex_models_partial_priced" = "Parcial · %1$@ con precio"; +"codex_models_precision_table" = "Modelo de mesa de precisión"; +"codex_models_previous_known_cost" = "Costo conocido anterior"; +"codex_models_previous_metric" = "Anterior %1$@"; +"codex_models_previous_period_unavailable" = "El período anterior no está disponible"; +"codex_models_previous_unavailable" = "Anterior no disponible"; +"codex_models_priced_tokens" = "Tokens con precio"; +"codex_models_pricing_calculation" = "Cálculo de precios"; +"codex_models_pricing_coverage" = "Cobertura de precios"; +"codex_models_pricing_explanation" = "%1$@ cubre tokens con precio de %2$@. %3$@ tokens sin una regla aplicable se excluyen, no se estiman como cero."; +"codex_models_ranking_by" = "Clasificación del modelo por %1$@"; +"codex_models_ranking_hint" = "Haga clic en una barra de modelo para abrir sus detalles."; +"codex_models_ranking_point_accessibility" = "%1$@, %2$@, %3$@ participación"; +"codex_models_ranking_summary" = "Modelos canónicos y otros clasificados por %1$@."; +"codex_models_ref_count" = "%1$@ referencias"; +"codex_models_refresh_analytics" = "Actualizar análisis de modelos"; +"codex_models_refresh_command_help" = "Actualizar análisis de modelos (Comando-R)"; +"codex_models_refresh_model_usage" = "Actualizar uso del modelo"; +"codex_models_refreshing_last_snapshot" = "Refrescante · mostrando la última instantánea exitosa"; +"codex_models_retry" = "Reintentar"; +"codex_models_reveal_pricing_calculation" = "Revelar cálculo de precios"; +"codex_models_search_empty_description" = "La búsqueda filtra solo esta tabla; Los totales del tablero no cambian."; +"codex_models_search_models" = "Buscar modelos"; +"codex_models_search_scope" = "%1$@ · La búsqueda filtra solo esta tabla"; +"codex_models_search_table" = "Tabla de modelos de búsqueda"; +"codex_models_select_model" = "Seleccione %1$@"; +"codex_models_selected" = "Seleccionado"; +"codex_models_selected_bucket" = "depósito seleccionado"; +"codex_models_selected_bucket_accessibility" = "Depósito seleccionado: %1$@, %2$@, %3$@ tokens, %4$@ referencias de sesión, %5$@, moneda %6$@."; +"codex_models_selected_clear_available" = "Modelo seleccionado %1$@, Borrar selección disponible"; +"codex_models_selected_date" = "Fecha seleccionada"; +"codex_models_selected_hidden_by_search" = "Modelo seleccionado oculto por búsqueda"; +"codex_models_selected_model" = "Modelo seleccionado"; +"codex_models_selected_model_interval" = "Modelo seleccionado · %1$@"; +"codex_models_selected_suffix" = ", seleccionado"; +"codex_models_session_reference_count" = "%1$@ referencias de sesión"; +"codex_models_session_references" = "Referencias de sesión"; +"codex_models_session_references_lowercase" = "referencias de sesión"; +"codex_models_session_refs" = "Referencias de sesión"; +"codex_models_share" = "Compartir"; +"codex_models_share_known_cost_help" = "Parte del costo conocido; excluye el uso sin precio"; +"codex_models_share_of" = "Participación de %1$@"; +"codex_models_show_associated_sessions" = "Mostrar sesiones asociadas"; +"codex_models_show_details" = "Mostrar detalles"; +"codex_models_showing_last_snapshot" = "Mostrando la última instantánea exitosa"; +"codex_models_sorted_by" = "Ordenado por %1$@"; +"codex_models_subject_over_time" = "%1$@ a lo largo del tiempo"; +"codex_models_summary" = "Resumen de modelos"; +"codex_models_timeline_accessibility" = "Cronología de modelos, %1$@, %2$@"; +"codex_models_timeline_hint" = "Haga clic en un segmento de tiempo o mantenga el puntero sobre uno durante 1,5 segundos para inspeccionar sus detalles."; +"codex_models_timeline_point_accessibility" = "%1$@, %2$@, %3$@ tokens, %4$@ referencias de sesión, %5$@, moneda %6$@"; +"codex_models_timeline_summary" = "%1$@ %2$@ línea de tiempo. %3$@"; +"codex_models_token_count" = "%1$@ fichas"; +"codex_models_tokens" = "Fichas"; +"codex_models_tokens_lowercase" = "fichas"; +"codex_models_tokens_per_session_reference" = "Aproximadamente %1$@ tokens por referencia de sesión."; +"codex_models_top" = "arriba"; +"codex_models_top_model" = "Modelo superior"; +"codex_models_top_seven_other" = "Top 7 + Otros"; +"codex_models_top_two_account_for" = "Los dos mejores modelos representan %1$@ de %2$@"; +"codex_models_top_two_of" = "Los dos mejores modelos de %1$@"; +"codex_models_total_session_references_lowercase" = "referencias totales de sesiones"; +"codex_models_total_tokens" = "Total de tokens"; +"codex_models_total_tokens_lowercase" = "tokens totales"; +"codex_models_totals_reconcile" = "· Los totales se concilian con la instantánea activa"; +"codex_models_track_end" = "Fin de pista"; +"codex_models_track_start" = "Inicio de pista"; +"codex_models_trend_unavailable_observed_buckets" = "Depósitos observados: %1$d. Tendencia no disponible."; +"codex_models_unknown_export_error" = "Error de exportación desconocido"; +"codex_models_unpriced_tokens" = "Tokens sin precio"; +"codex_models_updated" = "Actualizado %1$@"; +"codex_models_usage_and_change" = "Uso y cambio"; +"codex_models_usage_by_model" = "Uso por modelo"; +"codex_models_value_comparison_accessibility" = "%1$@, %2$@"; +"codex_models_value_detail" = "%1$@ · %2$@"; +"codex_models_value_share" = "%1$@ · %2$@ compartir"; +"codex_models_value_share_accessibility" = "%1$@, %2$@ compartir"; +"codex_models_value_start" = "Valor inicial"; +"codex_models_visible_of_total" = "Modelos visibles: %1$d de %2$d"; +"codex_models_visible_rows" = "Filas visibles"; +"codex_models_vs_previous" = "%1$@ frente al anterior"; +"codex_models_week_lowercase" = "semana"; +"codex_workspaces_all_models" = "Todos los modelos"; +"codex_workspaces_all_workspaces" = "Todos los espacios de trabajo"; +"codex_workspaces_associated_with_selected_model" = "Asociado al modelo seleccionado"; +"codex_workspaces_bullet_value" = "· %1$@"; +"codex_workspaces_chart_keyboard_hint" = "Usa las teclas de flecha izquierda y derecha o las acciones de ajuste de VoiceOver para seleccionar un día. Pulsa Retorno o Espacio para fijarlo."; +"codex_workspaces_chats_description" = "Chats y conversaciones"; +"codex_workspaces_copy_path" = "Copiar ruta"; +"codex_workspaces_cost_unavailable" = "Coste no disponible"; +"codex_workspaces_daily_callout_accessibility" = "%1$@, %2$@"; +"codex_workspaces_daily_point_accessibility" = "%1$@, tokens %2$@"; +"codex_workspaces_daily_point_help" = "%1$@ — %2$@ tokens"; +"codex_workspaces_daily_token_usage" = "Uso diario de tokens"; +"codex_workspaces_daily_unavailable" = "El uso diario no está disponible"; +"codex_workspaces_daily_unavailable_description" = "El total se calculó a partir de %d sesiones, pero no se pudieron determinar sus fechas."; +"codex_workspaces_date" = "Fecha"; +"codex_workspaces_days" = "Días"; +"codex_workspaces_elevated_usage" = "Uso elevado"; +"codex_workspaces_elevated_usage_help" = "El uso supera la mediana del periodo seleccionado."; +"codex_workspaces_estimated_cost_short" = "Coste est."; +"codex_workspaces_estimated_cost_value" = "est. %@"; +"codex_workspaces_estimated_local_logs" = "Estimado a partir de registros locales de Codex"; +"codex_workspaces_failed_read_logs" = "No se pudieron leer los registros de Codex"; +"codex_workspaces_failed_some_logs" = "No se pudieron leer algunos registros de Codex"; +"codex_workspaces_finalizing_project_index_files" = "Finalizando el índice de proyectos… %d archivos"; +"codex_workspaces_high_usage" = "Uso alto"; +"codex_workspaces_high_usage_help" = "El uso es un valor atípico significativo para el periodo seleccionado."; +"codex_workspaces_indexed_files" = "Actualizado hace %@ · %d archivos indexados"; +"codex_workspaces_indexing_local_logs" = "Indexando registros locales de Codex…"; +"codex_workspaces_indexing_projects" = "Indexando proyectos…"; +"codex_workspaces_indexing_projects_files_left" = "Indexando proyectos… %d/%d archivos, quedan %d"; +"codex_workspaces_last_days" = "Últimos %d días"; +"codex_workspaces_local_codex_chat" = "Chat local de Codex"; +"codex_workspaces_model" = "Modelo"; +"codex_workspaces_models" = "Modelos"; +"codex_workspaces_no_daily_usage" = "No hay uso diario en este intervalo del historial."; +"codex_workspaces_no_local_usage" = "Aún no hay uso local de Codex"; +"codex_workspaces_no_models" = "No hay desglose por modelo disponible"; +"codex_workspaces_no_projects_or_chats" = "No se encontraron proyectos ni chats en este intervalo del historial."; +"codex_workspaces_no_sessions" = "No hay sesiones en este espacio de trabajo"; +"codex_workspaces_normal_usage" = "Uso normal"; +"codex_workspaces_not_available" = "No disponible"; +"codex_workspaces_overview" = "Resumen"; +"codex_workspaces_partial_breakdown_help" = "El resumen sigue siendo la referencia mientras los metadatos de fecha o modelo no disponibles se muestran como parciales."; +"codex_workspaces_partial_breakdown_notice" = "Algunos desgloses detallados no cubren el total completo."; +"codex_workspaces_partial_data" = "Datos parciales"; +"codex_workspaces_percent_total" = "% del total"; +"codex_workspaces_refresh_usage_data" = "Actualizar datos de uso"; +"codex_workspaces_relative_usage_accessibility" = "%d por ciento del uso máximo de un espacio de trabajo"; +"codex_workspaces_relative_usage_tooltip" = "Usa el %d%% de los tokens del espacio de trabajo con mayor uso en este periodo."; +"codex_workspaces_reveal_in_finder" = "Mostrar en el Finder"; +"codex_workspaces_saving_project_index" = "Guardando el índice de proyectos…"; +"codex_workspaces_scanning_logs" = "Analizando registros de Codex…"; +"codex_workspaces_search_sessions" = "Buscar sesiones"; +"codex_workspaces_selected_day_accessibility" = "%@: %@ tokens"; +"codex_workspaces_session" = "Sesión"; +"codex_workspaces_session_count" = "Sesiones: %d"; +"codex_workspaces_sessions" = "Sesiones"; +"codex_workspaces_showing_last_complete_index" = "Mostrando el último índice local completo"; +"codex_workspaces_stale_status" = "El índice de uso está desactualizado · Actualizado hace %@"; +"codex_workspaces_started" = "Iniciada"; +"codex_workspaces_title" = "Espacios de trabajo"; +"codex_workspaces_token_count_accessibility" = "%@ tokens"; +"codex_workspaces_tokens" = "Tokens"; +"codex_workspaces_top_model" = "Modelo principal"; +"codex_workspaces_top_sessions" = "Sesiones principales"; +"codex_workspaces_total" = "Total"; +"codex_workspaces_view" = "Vista"; +"codex_workspaces_view_all_sessions" = "Ver todas las sesiones"; +"codex_workspaces_window_title" = "Espacios de trabajo de Codex"; +"codex_workspaces_workspace_count" = "Espacios de trabajo y chats: %d"; +"codex_workspaces_workspaces_chats" = "Espacios de trabajo/chats"; diff --git a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings index cdea6d030b..438f96740b 100644 --- a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings @@ -1353,3 +1353,255 @@ "Cost today unavailable" = "هزینهٔ امروز: در دسترس نیست"; "30-day cost unavailable" = "هزینهٔ ۳۰ روز: در دسترس نیست"; "Resets" = "بازنشانی‌ها"; + +/* Codex Workspaces and Models */ +"codex_workspaces_open_in_window" = "باز کردن در پنجره"; +"codex_models_actions" = "اقدامات"; +"codex_models_active_and_new_count" = "مدل های فعال: %1$d. تازه فعال: %2$d"; +"codex_models_active_count_new_unavailable" = "مدل های فعال: %1$d. تعداد تازه فعال در دسترس نیست"; +"codex_models_active_models" = "مدل های فعال"; +"codex_models_alias_count" = "نام مستعار: %1$d"; +"codex_models_all_model_count" = "همه مدل ها: %1$d"; +"codex_models_all_models" = "همه مدل ها"; +"codex_models_all_rows" = "همه ردیف ها"; +"codex_models_analytics" = "تجزیه و تحلیل مدل ها"; +"codex_models_analytics_controls" = "کنترل های تحلیلی را مدل می کند"; +"codex_models_apply" = "درخواست دهید"; +"codex_models_bucket_count" = "سطل (%2$@): %1$d"; +"codex_models_bucket_interval" = "فاصله سطل"; +"codex_models_bullet_value" = "· %1$@"; +"codex_models_canonical_id" = "شناسه متعارف"; +"codex_models_canonical_model" = "مدل کانونیکال"; +"codex_models_change" = "تغییر دهید"; +"codex_models_change_from_peak" = "%1$@ از %2$@ اوج"; +"codex_models_change_from_previous_period" = "%1$@ از دوره مشابه قبلی"; +"codex_models_choose_metric" = "معیار تجزیه و تحلیل را انتخاب کنید"; +"codex_models_clear_search" = "جستجو را پاک کنید"; +"codex_models_clear_selection" = "پاک کردن انتخاب مدل ها"; +"codex_models_columns" = "ستون ها"; +"codex_models_compact_billions_suffix" = "B"; +"codex_models_compact_millions_suffix" = "M"; +"codex_models_compact_thousands_suffix" = "K"; +"codex_models_contains_selected_suffix" = "، شامل مدل انتخاب شده است"; +"codex_models_copy_aliases" = "نام مستعار را کپی کنید"; +"codex_models_copy_canonical_id" = "شناسه متعارف را کپی کنید"; +"codex_models_copy_canonical_model_id" = "شناسه مدل متعارف را کپی کنید"; +"codex_models_copy_raw_aliases" = "نام مستعار خام را کپی کنید"; +"codex_models_cost_conclusions_limited" = "نتیجه گیری هزینه به %1$@ نشانه قیمت محدود می شود."; +"codex_models_cost_hidden" = "هزینه پنهان است"; +"codex_models_cost_status_currency" = "%1$@ · %2$@ · %3$@"; +"codex_models_cost_unavailable" = "هزینه در دسترس نیست"; +"codex_models_cost_unavailable_for_data" = "هزینه برای این داده ها در دسترس نیست"; +"codex_models_coverage" = "پوشش"; +"codex_models_currency" = "واحد پول"; +"codex_models_current_metric" = "فعلی %1$@"; +"codex_models_current_period_in_progress" = "%1$@ فعلی در حال انجام است"; +"codex_models_date" = "تاریخ"; +"codex_models_day_lowercase" = "روز"; +"codex_models_days" = "روز"; +"codex_models_delta_vs_prior" = "Δ در مقابل قبل"; +"codex_models_disabled_in_settings" = "در تنظیمات غیرفعال شده است"; +"codex_models_dominates_at" = "این مدل بر %1$@ در %2$@ غالب است."; +"codex_models_ended" = "پایان یافت"; +"codex_models_export" = "صادرات"; +"codex_models_export_all_filename" = "codex-models-all.csv"; +"codex_models_export_default_filename" = "codex-models.csv"; +"codex_models_export_failed" = "تجزیه و تحلیل مدل‌ها صادر نشد"; +"codex_models_export_selected_filename" = "codex-model-%1$@.csv"; +"codex_models_export_selected_model" = "صادرات مدل انتخاب شده"; +"codex_models_export_visible_filename" = "codex-models-visible.csv"; +"codex_models_export_visible_rows" = "ردیف های مدل های قابل مشاهده را صادر کنید"; +"codex_models_focus_table_search" = "جستجوی جدول Focus Models"; +"codex_models_fully_priced" = "قیمت تمام شده"; +"codex_models_fully_priced_percent" = "100% قیمت"; +"codex_models_generated" = "%1$@ ایجاد شد"; +"codex_models_granularity_accessibility" = "دانه بندی، %1$@، %2$@"; +"codex_models_hidden" = "پنهان"; +"codex_models_identity_aliases_help" = "%1$@ · %2$@ · نام مستعار: %3$@"; +"codex_models_identity_and_aliases" = "هویت و نام مستعار"; +"codex_models_indexing_last_days" = "نمایه سازی %1$d روز گذشته…"; +"codex_models_interpretation" = "تفسیر"; +"codex_models_known_cost" = "هزینه شناخته شده"; +"codex_models_known_cost_fully_priced_accessibility" = "هزینه شناخته شده %1$@، قیمت کامل"; +"codex_models_known_cost_lowercase" = "هزینه شناخته شده"; +"codex_models_known_cost_partial_accessibility" = "هزینه شناخته شده %1$@، جزئی، %2$@ قیمت توکن"; +"codex_models_last_days" = "%1$d روز گذشته"; +"codex_models_local_logs_source" = "· سیاهههای مربوط به Codex محلی"; +"codex_models_major_contributor_at" = "این مدل سهم عمده ای در %1$@ دارد."; +"codex_models_metric" = "متریک"; +"codex_models_metric_over_time" = "%1$@ در طول زمان"; +"codex_models_model" = "مدل"; +"codex_models_model_concentration" = "غلظت مدل"; +"codex_models_model_count" = "مدل ها: %1$d"; +"codex_models_model_usage" = "استفاده از مدل"; +"codex_models_month_lowercase" = "ماه"; +"codex_models_more_alias_count" = "نام مستعار بیشتر: %1$d"; +"codex_models_name_canonical_id" = "%1$@، شناسه متعارف %2$@"; +"codex_models_name_value_accessibility" = "%1$@، %2$@"; +"codex_models_named_metric_over_time" = "%1$@ %2$@ در طول زمان"; +"codex_models_new" = "جدید"; +"codex_models_new_in_period" = "جدید در این دوره"; +"codex_models_newly_active_count" = "تازه فعال شده: %1$d"; +"codex_models_no_applicable_price" = "بدون قیمت قابل اجرا"; +"codex_models_no_change_from_previous_period" = "بدون تغییر نسبت به دوره مشابه قبلی"; +"codex_models_no_matching_models" = "هیچ مدل منطبقی وجود ندارد"; +"codex_models_no_newly_active" = "هیچ مدل تازه فعالی وجود ندارد"; +"codex_models_no_top_model" = "بدون مدل بالا"; +"codex_models_no_usage" = "بدون استفاده"; +"codex_models_no_usage_description" = "محدوده تاریخ طولانی تری را در بالا انتخاب کنید یا پس از ذخیره جلسات Codex محلی، آن را بازخوانی کنید."; +"codex_models_no_usage_in_period" = "در این دوره استفاده نشده است"; +"codex_models_no_usage_in_range" = "بدون استفاده از مدل در این محدوده"; +"codex_models_other_count" = "دیگر (%1$d)"; +"codex_models_partial_priced" = "جزئی · %1$@ قیمت"; +"codex_models_precision_table" = "میز دقیق مدل"; +"codex_models_previous_known_cost" = "هزینه شناخته شده قبلی"; +"codex_models_previous_metric" = "قبلی %1$@"; +"codex_models_previous_period_unavailable" = "دوره قبلی در دسترس نیست"; +"codex_models_previous_unavailable" = "قبلی در دسترس نیست"; +"codex_models_priced_tokens" = "توکن های قیمت گذاری شده"; +"codex_models_pricing_calculation" = "محاسبه قیمت"; +"codex_models_pricing_coverage" = "پوشش قیمت"; +"codex_models_pricing_explanation" = "%1$@ %2$@ توکن‌های قیمت‌گذاری شده را پوشش می‌دهد. %3$@ نشانه‌های بدون قاعده قابل اجرا مستثنی هستند، صفر تخمین زده نمی‌شوند."; +"codex_models_ranking_by" = "رتبه بندی مدل توسط %1$@"; +"codex_models_ranking_hint" = "روی نوار مدل کلیک کنید تا جزئیات آن باز شود."; +"codex_models_ranking_point_accessibility" = "%1$@، %2$@، %3$@ سهم"; +"codex_models_ranking_summary" = "مدل‌های Canonical و سایر مدل‌های رتبه‌بندی شده توسط %1$@."; +"codex_models_ref_count" = "%1$@ مراجع"; +"codex_models_refresh_analytics" = "تجزیه و تحلیل مدل‌ها را تازه‌سازی کنید"; +"codex_models_refresh_command_help" = "Refresh Models Analytics (Command-R)"; +"codex_models_refresh_model_usage" = "استفاده از مدل تازه کردن"; +"codex_models_refreshing_last_snapshot" = "تازه کردن · نمایش آخرین عکس فوری موفق"; +"codex_models_retry" = "دوباره امتحان کنید"; +"codex_models_reveal_pricing_calculation" = "محاسبه قیمت گذاری را آشکار کنید"; +"codex_models_search_empty_description" = "جستجو فقط این جدول را فیلتر می کند. مجموع داشبورد بدون تغییر است."; +"codex_models_search_models" = "مدل ها را جستجو کنید"; +"codex_models_search_scope" = "%1$@ · جستجو فقط این جدول را فیلتر می کند"; +"codex_models_search_table" = "جدول مدل های جستجو"; +"codex_models_select_model" = "%1$@ را انتخاب کنید"; +"codex_models_selected" = "انتخاب شد"; +"codex_models_selected_bucket" = "سطل انتخاب شده"; +"codex_models_selected_bucket_accessibility" = "سطل انتخاب شده: %1$@، %2$@، %3$@ توکن، %4$@ مراجع جلسه، %5$@، واحد پول %6$@."; +"codex_models_selected_clear_available" = "مدل انتخابی %1$@، پاک کردن انتخاب موجود است"; +"codex_models_selected_date" = "تاریخ انتخاب شده"; +"codex_models_selected_hidden_by_search" = "مدل انتخاب شده با جستجو پنهان شد"; +"codex_models_selected_model" = "مدل انتخابی"; +"codex_models_selected_model_interval" = "مدل انتخابی · %1$@"; +"codex_models_selected_suffix" = "، انتخاب شد"; +"codex_models_session_reference_count" = "%1$@ مراجع جلسه"; +"codex_models_session_references" = "مراجع جلسه"; +"codex_models_session_references_lowercase" = "مراجع جلسه"; +"codex_models_session_refs" = "مراجع جلسه"; +"codex_models_share" = "به اشتراک بگذارید"; +"codex_models_share_known_cost_help" = "سهم هزینه شناخته شده؛ استفاده بدون قیمت را شامل نمی شود"; +"codex_models_share_of" = "سهم %1$@"; +"codex_models_show_associated_sessions" = "نمایش جلسات مرتبط"; +"codex_models_show_details" = "نمایش جزئیات"; +"codex_models_showing_last_snapshot" = "نمایش آخرین عکس فوری موفق"; +"codex_models_sorted_by" = "مرتب شده بر اساس %1$@"; +"codex_models_subject_over_time" = "%1$@ در طول زمان"; +"codex_models_summary" = "خلاصه مدل ها"; +"codex_models_timeline_accessibility" = "جدول زمانی مدل‌ها، %1$@، %2$@"; +"codex_models_timeline_hint" = "روی یک سطل زمان کلیک کنید، یا نشانگر را روی یکی به مدت 1.5 ثانیه نگه دارید تا جزئیات آن را بررسی کنید."; +"codex_models_timeline_point_accessibility" = "%1$@، %2$@، %3$@ توکن، %4$@ مراجع جلسه، %5$@، واحد پول %6$@"; +"codex_models_timeline_summary" = "خط زمانی %1$@ %2$@. %3$@"; +"codex_models_token_count" = "%1$@ توکن"; +"codex_models_tokens" = "توکن ها"; +"codex_models_tokens_lowercase" = "ژتون های"; +"codex_models_tokens_per_session_reference" = "حدود %1$@ نشانه در هر جلسه مرجع."; +"codex_models_top" = "بالا"; +"codex_models_top_model" = "مدل برتر"; +"codex_models_top_seven_other" = "7 برتر + سایر"; +"codex_models_top_two_account_for" = "دو مدل برتر %1$@ از %2$@ دارند"; +"codex_models_top_two_of" = "دو مدل برتر %1$@"; +"codex_models_total_session_references_lowercase" = "کل مراجع جلسه"; +"codex_models_total_tokens" = "کل توکن ها"; +"codex_models_total_tokens_lowercase" = "کل توکن ها"; +"codex_models_totals_reconcile" = "· جمع ها با عکس فوری فعال تطبیق می کنند"; +"codex_models_track_end" = "پایان آهنگ"; +"codex_models_track_start" = "شروع آهنگ"; +"codex_models_trend_unavailable_observed_buckets" = "سطل های مشاهده شده: %1$d. روند در دسترس نیست."; +"codex_models_unknown_export_error" = "خطای صادرات ناشناخته"; +"codex_models_unpriced_tokens" = "توکن های بی قیمت"; +"codex_models_updated" = "%1$@ به‌روزرسانی شد"; +"codex_models_usage_and_change" = "استفاده و تغییر"; +"codex_models_usage_by_model" = "استفاده بر اساس مدل"; +"codex_models_value_comparison_accessibility" = "%1$@، %2$@"; +"codex_models_value_detail" = "%1$@ · %2$@"; +"codex_models_value_share" = "%1$@ · %2$@ اشتراک"; +"codex_models_value_share_accessibility" = "%1$@، %2$@ سهم"; +"codex_models_value_start" = "شروع ارزش"; +"codex_models_visible_of_total" = "مدل های قابل مشاهده: %1$d از %2$d"; +"codex_models_visible_rows" = "ردیف های قابل مشاهده"; +"codex_models_vs_previous" = "%1$@ در مقایسه با قبلی"; +"codex_models_week_lowercase" = "هفته"; +"codex_workspaces_all_models" = "همهٔ مدل‌ها"; +"codex_workspaces_all_workspaces" = "همهٔ فضاهای کاری"; +"codex_workspaces_associated_with_selected_model" = "مرتبط با مدل انتخاب شده"; +"codex_workspaces_bullet_value" = "· %1$@"; +"codex_workspaces_chart_keyboard_hint" = "برای انتخاب روز از کلیدهای پیکان چپ و راست یا کنش‌های تنظیم VoiceOver استفاده کنید. برای سنجاق کردن Return یا فاصله را فشار دهید."; +"codex_workspaces_chats_description" = "گفتگوها و مکالمه‌ها"; +"codex_workspaces_copy_path" = "کپی مسیر"; +"codex_workspaces_cost_unavailable" = "هزینه در دسترس نیست"; +"codex_workspaces_daily_callout_accessibility" = "%1$@، %2$@"; +"codex_workspaces_daily_point_accessibility" = "%1$@، توکن %2$@"; +"codex_workspaces_daily_point_help" = "%1$@ — %2$@ توکن"; +"codex_workspaces_daily_token_usage" = "استفادهٔ روزانه از توکن"; +"codex_workspaces_daily_unavailable" = "استفادهٔ روزانه در دسترس نیست"; +"codex_workspaces_daily_unavailable_description" = "مجموع از %d نشست محاسبه شد، اما تاریخ آن‌ها مشخص نشد."; +"codex_workspaces_date" = "تاریخ"; +"codex_workspaces_days" = "روز"; +"codex_workspaces_elevated_usage" = "استفادهٔ بالاتر"; +"codex_workspaces_elevated_usage_help" = "استفاده در دورهٔ انتخاب‌شده بالاتر از میانه است."; +"codex_workspaces_estimated_cost_short" = "هزینهٔ تخمینی"; +"codex_workspaces_estimated_cost_value" = "تخمینی %@"; +"codex_workspaces_estimated_local_logs" = "برآوردشده از گزارش‌های محلی Codex"; +"codex_workspaces_failed_read_logs" = "خواندن گزارش‌های Codex ناموفق بود"; +"codex_workspaces_failed_some_logs" = "خواندن برخی گزارش‌های Codex ناموفق بود"; +"codex_workspaces_finalizing_project_index_files" = "در حال نهایی‌سازی فهرست پروژه… %d فایل"; +"codex_workspaces_high_usage" = "استفادهٔ زیاد"; +"codex_workspaces_high_usage_help" = "استفاده در دورهٔ انتخاب‌شده به‌طور قابل‌توجهی نامتعارف است."; +"codex_workspaces_indexed_files" = "%@ پیش به‌روزرسانی شد · %d فایل فهرست شد"; +"codex_workspaces_indexing_local_logs" = "در حال فهرست‌سازی گزارش‌های محلی Codex…"; +"codex_workspaces_indexing_projects" = "در حال فهرست‌سازی پروژه‌ها…"; +"codex_workspaces_indexing_projects_files_left" = "در حال فهرست‌سازی پروژه‌ها… %d/%d فایل، %d باقی‌مانده"; +"codex_workspaces_last_days" = "%d روز گذشته"; +"codex_workspaces_local_codex_chat" = "گفتگوی محلی Codex"; +"codex_workspaces_model" = "مدل"; +"codex_workspaces_models" = "مدل‌ها"; +"codex_workspaces_no_daily_usage" = "در این بازهٔ تاریخچه استفادهٔ روزانه‌ای وجود ندارد."; +"codex_workspaces_no_local_usage" = "هنوز استفادهٔ محلی از Codex وجود ندارد"; +"codex_workspaces_no_models" = "تفکیک مدل در دسترس نیست"; +"codex_workspaces_no_projects_or_chats" = "در این بازهٔ تاریخچه پروژه یا گفتگویی یافت نشد."; +"codex_workspaces_no_sessions" = "در این فضای کاری نشستی وجود ندارد"; +"codex_workspaces_normal_usage" = "استفادهٔ عادی"; +"codex_workspaces_not_available" = "در دسترس نیست"; +"codex_workspaces_overview" = "نمای کلی"; +"codex_workspaces_partial_breakdown_help" = "خلاصه همچنان مرجع است و فرادادهٔ تاریخ یا مدلِ در دسترس‌نبوده به‌صورت جزئی نمایش داده می‌شود."; +"codex_workspaces_partial_breakdown_notice" = "برخی جزئیات تفکیکی کل مجموع را پوشش نمی‌دهند."; +"codex_workspaces_partial_data" = "داده‌های جزئی"; +"codex_workspaces_percent_total" = "درصد از کل"; +"codex_workspaces_refresh_usage_data" = "تازه‌سازی داده‌های استفاده"; +"codex_workspaces_relative_usage_accessibility" = "%d درصد از بیشترین استفادهٔ فضای کاری"; +"codex_workspaces_relative_usage_tooltip" = "از %d%% توکن‌های پرمصرف‌ترین فضای کاری در این دوره استفاده می‌کند."; +"codex_workspaces_reveal_in_finder" = "نمایش در Finder"; +"codex_workspaces_saving_project_index" = "در حال ذخیرهٔ فهرست پروژه…"; +"codex_workspaces_scanning_logs" = "در حال پویش گزارش‌های Codex…"; +"codex_workspaces_search_sessions" = "جستجوی نشست‌ها"; +"codex_workspaces_selected_day_accessibility" = "%@: %@ توکن"; +"codex_workspaces_session" = "نشست"; +"codex_workspaces_session_count" = "جلسه‌ها: %d"; +"codex_workspaces_sessions" = "نشست‌ها"; +"codex_workspaces_showing_last_complete_index" = "در حال نمایش آخرین فهرست محلی کامل"; +"codex_workspaces_stale_status" = "فهرست استفاده قدیمی است · %@ پیش به‌روزرسانی شد"; +"codex_workspaces_started" = "آغازشده"; +"codex_workspaces_title" = "فضاهای کاری"; +"codex_workspaces_token_count_accessibility" = "%@ توکن"; +"codex_workspaces_tokens" = "توکن‌ها"; +"codex_workspaces_top_model" = "مدل برتر"; +"codex_workspaces_top_sessions" = "نشست‌های برتر"; +"codex_workspaces_total" = "کل"; +"codex_workspaces_view" = "نمایش"; +"codex_workspaces_view_all_sessions" = "نمایش همهٔ نشست‌ها"; +"codex_workspaces_window_title" = "فضاهای کاری Codex"; +"codex_workspaces_workspace_count" = "فضاهای کاری و گفت‌وگوها: %d"; +"codex_workspaces_workspaces_chats" = "فضاهای کاری/گفتگوها"; diff --git a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings index ff583450d9..5dde38fb2a 100644 --- a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings @@ -1349,3 +1349,255 @@ "Cost today unavailable" = "Coût aujourd’hui: Indisponible"; "30-day cost unavailable" = "Coût sur 30 j: Indisponible"; "Resets" = "Réinitialisations"; + +/* Codex Workspaces and Models */ +"codex_workspaces_open_in_window" = "Ouvrir dans une fenêtre"; +"codex_models_actions" = "Actions"; +"codex_models_active_and_new_count" = "Modèles actifs : %1$d ; nouvellement actif : %2$d"; +"codex_models_active_count_new_unavailable" = "Modèles actifs : %1$d ; Le nombre de nouveaux actifs n'est pas disponible"; +"codex_models_active_models" = "Modèles actifs"; +"codex_models_alias_count" = "Alias : %1$d"; +"codex_models_all_model_count" = "Tous les modèles : %1$d"; +"codex_models_all_models" = "Tous les modèles"; +"codex_models_all_rows" = "Toutes les lignes"; +"codex_models_analytics" = "Analyse des modèles"; +"codex_models_analytics_controls" = "Contrôles d'analyse des modèles"; +"codex_models_apply" = "Postuler"; +"codex_models_bucket_count" = "Compartiments (%2$@) : %1$d"; +"codex_models_bucket_interval" = "Intervalle de compartiment"; +"codex_models_bullet_value" = "· %1$@"; +"codex_models_canonical_id" = "ID canonique"; +"codex_models_canonical_model" = "Modèle canonique"; +"codex_models_change" = "Modification"; +"codex_models_change_from_peak" = "%1$@ à partir du pic %2$@"; +"codex_models_change_from_previous_period" = "%1$@ de la période égale précédente"; +"codex_models_choose_metric" = "Choisissez la métrique d'analyse"; +"codex_models_clear_search" = "Effacer la recherche"; +"codex_models_clear_selection" = "Effacer la sélection des modèles"; +"codex_models_columns" = "Colonnes"; +"codex_models_compact_billions_suffix" = "B"; +"codex_models_compact_millions_suffix" = "M"; +"codex_models_compact_thousands_suffix" = "K"; +"codex_models_contains_selected_suffix" = ", contient le modèle sélectionné"; +"codex_models_copy_aliases" = "Copier les alias"; +"codex_models_copy_canonical_id" = "Copier l'ID canonique"; +"codex_models_copy_canonical_model_id" = "Copier l'ID du modèle canonique"; +"codex_models_copy_raw_aliases" = "Copier les alias bruts"; +"codex_models_cost_conclusions_limited" = "Les conclusions sur les coûts sont limitées aux jetons tarifés %1$@."; +"codex_models_cost_hidden" = "Coût caché"; +"codex_models_cost_status_currency" = "%1$@ · %2$@ · %3$@"; +"codex_models_cost_unavailable" = "Coût indisponible"; +"codex_models_cost_unavailable_for_data" = "Le coût n'est pas disponible pour ces données"; +"codex_models_coverage" = "Couverture"; +"codex_models_currency" = "Devise"; +"codex_models_current_metric" = "%1$@ actuel"; +"codex_models_current_period_in_progress" = "%1$@ actuel en cours"; +"codex_models_date" = "Date"; +"codex_models_day_lowercase" = "jour"; +"codex_models_days" = "jours"; +"codex_models_delta_vs_prior" = "Δ par rapport au précédent"; +"codex_models_disabled_in_settings" = "Désactivé dans les paramètres"; +"codex_models_dominates_at" = "Ce modèle domine %1$@ à %2$@."; +"codex_models_ended" = "Terminé"; +"codex_models_export" = "Exporter"; +"codex_models_export_all_filename" = "codex-models-all.csv"; +"codex_models_export_default_filename" = "codex-models.csv"; +"codex_models_export_failed" = "Impossible d'exporter les analyses de modèles"; +"codex_models_export_selected_filename" = "codex-model-%1$@.csv"; +"codex_models_export_selected_model" = "Exporter le modèle sélectionné"; +"codex_models_export_visible_filename" = "codex-models-visible.csv"; +"codex_models_export_visible_rows" = "Exporter les lignes de modèles visibles"; +"codex_models_focus_table_search" = "Recherche dans la table des modèles Focus"; +"codex_models_fully_priced" = "Plein tarif"; +"codex_models_fully_priced_percent" = "100 % tarifé"; +"codex_models_generated" = "%1$@ généré"; +"codex_models_granularity_accessibility" = "Granularité, %1$@, %2$@"; +"codex_models_hidden" = "Caché"; +"codex_models_identity_aliases_help" = "%1$@ · %2$@ · Alias : %3$@"; +"codex_models_identity_and_aliases" = "Identité et pseudonymes"; +"codex_models_indexing_last_days" = "Indexation des %1$d derniers jours…"; +"codex_models_interpretation" = "Interprétation"; +"codex_models_known_cost" = "Coût connu"; +"codex_models_known_cost_fully_priced_accessibility" = "Coût connu %1$@, tarif complet"; +"codex_models_known_cost_lowercase" = "coût connu"; +"codex_models_known_cost_partial_accessibility" = "Coût connu %1$@, partiel, %2$@ des jetons tarifés"; +"codex_models_last_days" = "%1$d derniers jours"; +"codex_models_local_logs_source" = "· Journaux locaux du Codex"; +"codex_models_major_contributor_at" = "Ce modèle est un contributeur majeur à %1$@."; +"codex_models_metric" = "Métrique"; +"codex_models_metric_over_time" = "%1$@ au fil du temps"; +"codex_models_model" = "Modèle"; +"codex_models_model_concentration" = "Concentration du modèle"; +"codex_models_model_count" = "Modèles : %1$d"; +"codex_models_model_usage" = "Utilisation du modèle"; +"codex_models_month_lowercase" = "mois"; +"codex_models_more_alias_count" = "Autres alias : %1$d"; +"codex_models_name_canonical_id" = "%1$@, identifiant canonique %2$@"; +"codex_models_name_value_accessibility" = "%1$@, %2$@"; +"codex_models_named_metric_over_time" = "%1$@ %2$@ au fil du temps"; +"codex_models_new" = "Nouveau"; +"codex_models_new_in_period" = "Nouveau dans cette période"; +"codex_models_newly_active_count" = "Nouvellement actif : %1$d"; +"codex_models_no_applicable_price" = "Aucun prix applicable"; +"codex_models_no_change_from_previous_period" = "Aucun changement par rapport à la période égale précédente"; +"codex_models_no_matching_models" = "Aucun modèle correspondant"; +"codex_models_no_newly_active" = "Aucun modèle nouvellement actif"; +"codex_models_no_top_model" = "Pas de modèle haut de gamme"; +"codex_models_no_usage" = "Aucune utilisation"; +"codex_models_no_usage_description" = "Choisissez une plage de dates plus longue ci-dessus ou actualisez une fois les sessions Codex locales enregistrées."; +"codex_models_no_usage_in_period" = "Aucune utilisation pendant cette période"; +"codex_models_no_usage_in_range" = "Aucune utilisation de modèle dans cette gamme"; +"codex_models_other_count" = "Autre (%1$d)"; +"codex_models_partial_priced" = "Partiel · %1$@ tarifé"; +"codex_models_precision_table" = "Tableau de précision du modèle"; +"codex_models_previous_known_cost" = "Coût connu précédent"; +"codex_models_previous_metric" = "Précédent %1$@"; +"codex_models_previous_period_unavailable" = "La période précédente n'est pas disponible"; +"codex_models_previous_unavailable" = "Précédent indisponible"; +"codex_models_priced_tokens" = "Jetons tarifés"; +"codex_models_pricing_calculation" = "Calcul du prix"; +"codex_models_pricing_coverage" = "Couverture tarifaire"; +"codex_models_pricing_explanation" = "%1$@ couvre les jetons tarifés à %2$@. Les jetons %3$@ sans règle applicable sont exclus et ne sont pas estimés à zéro."; +"codex_models_ranking_by" = "Classement du modèle par %1$@"; +"codex_models_ranking_hint" = "Cliquez sur une barre de modèle pour ouvrir ses détails."; +"codex_models_ranking_point_accessibility" = "%1$@, %2$@, %3$@ partage"; +"codex_models_ranking_summary" = "Modèles canoniques et autres classés par %1$@."; +"codex_models_ref_count" = "%1$@ références"; +"codex_models_refresh_analytics" = "Actualiser les analyses de modèles"; +"codex_models_refresh_command_help" = "Actualiser les analyses de modèles (Commande-R)"; +"codex_models_refresh_model_usage" = "Actualiser l'utilisation du modèle"; +"codex_models_refreshing_last_snapshot" = "Actualisation · affichage du dernier instantané réussi"; +"codex_models_retry" = "Réessayer"; +"codex_models_reveal_pricing_calculation" = "Révéler le calcul du prix"; +"codex_models_search_empty_description" = "La recherche filtre cette table uniquement ; les totaux du tableau de bord sont inchangés."; +"codex_models_search_models" = "Rechercher des modèles"; +"codex_models_search_scope" = "%1$@ · La recherche filtre uniquement ce tableau"; +"codex_models_search_table" = "Tableau Modèles de recherche"; +"codex_models_select_model" = "Sélectionnez %1$@"; +"codex_models_selected" = "Sélectionné"; +"codex_models_selected_bucket" = "Godet sélectionné"; +"codex_models_selected_bucket_accessibility" = "Compartiment sélectionné : %1$@, %2$@, %3$@ jetons, %4$@ références de session, %5$@, devise %6$@."; +"codex_models_selected_clear_available" = "Modèle sélectionné %1$@, Effacer la sélection disponible"; +"codex_models_selected_date" = "Date sélectionnée"; +"codex_models_selected_hidden_by_search" = "Modèle sélectionné masqué par la recherche"; +"codex_models_selected_model" = "Modèle sélectionné"; +"codex_models_selected_model_interval" = "Modèle sélectionné · %1$@"; +"codex_models_selected_suffix" = ", sélectionné"; +"codex_models_session_reference_count" = "%1$@ références de session"; +"codex_models_session_references" = "Références de sessions"; +"codex_models_session_references_lowercase" = "références de session"; +"codex_models_session_refs" = "Références de session"; +"codex_models_share" = "Partager"; +"codex_models_share_known_cost_help" = "Part du coût connu ; exclut l'utilisation non tarifée"; +"codex_models_share_of" = "Part de %1$@"; +"codex_models_show_associated_sessions" = "Afficher les sessions associées"; +"codex_models_show_details" = "Afficher les détails"; +"codex_models_showing_last_snapshot" = "Affichage du dernier instantané réussi"; +"codex_models_sorted_by" = "Trié par %1$@"; +"codex_models_subject_over_time" = "%1$@ au fil du temps"; +"codex_models_summary" = "Résumé des modèles"; +"codex_models_timeline_accessibility" = "Chronologie des modèles, %1$@, %2$@"; +"codex_models_timeline_hint" = "Cliquez sur une période de temps ou maintenez le pointeur sur une période pendant 1,5 seconde pour inspecter ses détails."; +"codex_models_timeline_point_accessibility" = "%1$@, %2$@, %3$@ jetons, %4$@ références de session, %5$@, devise %6$@"; +"codex_models_timeline_summary" = "%1$@ %2$@ chronologie. %3$@"; +"codex_models_token_count" = "%1$@ jetons"; +"codex_models_tokens" = "Jetons"; +"codex_models_tokens_lowercase" = "jetons"; +"codex_models_tokens_per_session_reference" = "Environ %1$@ jetons par référence de session."; +"codex_models_top" = "Haut"; +"codex_models_top_model" = "Modèle haut de gamme"; +"codex_models_top_seven_other" = "Top 7 + Autre"; +"codex_models_top_two_account_for" = "Les deux meilleurs modèles représentent %1$@ sur %2$@"; +"codex_models_top_two_of" = "Les deux meilleurs modèles de %1$@"; +"codex_models_total_session_references_lowercase" = "références totales de sessions"; +"codex_models_total_tokens" = "Total de jetons"; +"codex_models_total_tokens_lowercase" = "jetons au total"; +"codex_models_totals_reconcile" = "· Les totaux sont rapprochés de l'instantané actif"; +"codex_models_track_end" = "Fin de piste"; +"codex_models_track_start" = "Début de la piste"; +"codex_models_trend_unavailable_observed_buckets" = "Compartiments observés : %1$d. Tendance indisponible."; +"codex_models_unknown_export_error" = "Erreur d'exportation inconnue"; +"codex_models_unpriced_tokens" = "Jetons sans prix"; +"codex_models_updated" = "%1$@ mis à jour"; +"codex_models_usage_and_change" = "Utilisation et modification"; +"codex_models_usage_by_model" = "Utilisation par modèle"; +"codex_models_value_comparison_accessibility" = "%1$@, %2$@"; +"codex_models_value_detail" = "%1$@ · %2$@"; +"codex_models_value_share" = "%1$@ · %2$@ partage"; +"codex_models_value_share_accessibility" = "%1$@, %2$@ partage"; +"codex_models_value_start" = "Début de la valeur"; +"codex_models_visible_of_total" = "Modèles visibles : %1$d sur %2$d"; +"codex_models_visible_rows" = "Lignes visibles"; +"codex_models_vs_previous" = "%1$@ par rapport au précédent"; +"codex_models_week_lowercase" = "semaine"; +"codex_workspaces_all_models" = "Tous les modèles"; +"codex_workspaces_all_workspaces" = "Tous les espaces de travail"; +"codex_workspaces_associated_with_selected_model" = "Associé au modèle sélectionné"; +"codex_workspaces_bullet_value" = "· %1$@"; +"codex_workspaces_chart_keyboard_hint" = "Utilisez les flèches gauche et droite ou les actions de réglage VoiceOver pour sélectionner un jour. Appuyez sur Retour ou Espace pour l'épingler."; +"codex_workspaces_chats_description" = "Discussions et conversations"; +"codex_workspaces_copy_path" = "Copier le chemin"; +"codex_workspaces_cost_unavailable" = "Coût indisponible"; +"codex_workspaces_daily_callout_accessibility" = "%1$@, %2$@"; +"codex_workspaces_daily_point_accessibility" = "%1$@, jetons %2$@"; +"codex_workspaces_daily_point_help" = "%1$@ — %2$@ jetons"; +"codex_workspaces_daily_token_usage" = "Utilisation quotidienne des jetons"; +"codex_workspaces_daily_unavailable" = "L'utilisation quotidienne est indisponible"; +"codex_workspaces_daily_unavailable_description" = "Le total a été calculé à partir de %d sessions, mais leurs dates n'ont pas pu être déterminées."; +"codex_workspaces_date" = "Date"; +"codex_workspaces_days" = "Jours"; +"codex_workspaces_elevated_usage" = "Utilisation supérieure"; +"codex_workspaces_elevated_usage_help" = "L'utilisation dépasse la médiane de la période sélectionnée."; +"codex_workspaces_estimated_cost_short" = "Coût estimé"; +"codex_workspaces_estimated_cost_value" = "estim. %@"; +"codex_workspaces_estimated_local_logs" = "Estimation basée sur les journaux Codex locaux"; +"codex_workspaces_failed_read_logs" = "Impossible de lire les journaux Codex"; +"codex_workspaces_failed_some_logs" = "Impossible de lire certains journaux Codex"; +"codex_workspaces_finalizing_project_index_files" = "Finalisation de l'index des projets… %d fichiers"; +"codex_workspaces_high_usage" = "Utilisation élevée"; +"codex_workspaces_high_usage_help" = "L'utilisation est nettement atypique pour la période sélectionnée."; +"codex_workspaces_indexed_files" = "Mis à jour il y a %@ · %d fichiers indexés"; +"codex_workspaces_indexing_local_logs" = "Indexation des journaux Codex locaux…"; +"codex_workspaces_indexing_projects" = "Indexation des projets…"; +"codex_workspaces_indexing_projects_files_left" = "Indexation des projets… %d/%d fichiers, %d restants"; +"codex_workspaces_last_days" = "%d derniers jours"; +"codex_workspaces_local_codex_chat" = "Discussion Codex locale"; +"codex_workspaces_model" = "Modèle"; +"codex_workspaces_models" = "Modèles"; +"codex_workspaces_no_daily_usage" = "Aucune utilisation quotidienne dans cette période."; +"codex_workspaces_no_local_usage" = "Aucune utilisation locale de Codex pour le moment"; +"codex_workspaces_no_models" = "Aucune ventilation par modèle disponible"; +"codex_workspaces_no_projects_or_chats" = "Aucun projet ni aucune discussion trouvé dans cette période."; +"codex_workspaces_no_sessions" = "Aucune session dans cet espace de travail"; +"codex_workspaces_normal_usage" = "Utilisation normale"; +"codex_workspaces_not_available" = "Non disponible"; +"codex_workspaces_overview" = "Vue d'ensemble"; +"codex_workspaces_partial_breakdown_help" = "Le résumé reste la référence tandis que les métadonnées de date ou de modèle indisponibles sont indiquées comme partielles."; +"codex_workspaces_partial_breakdown_notice" = "Certaines ventilations détaillées ne couvrent pas la totalité."; +"codex_workspaces_partial_data" = "Données partielles"; +"codex_workspaces_percent_total" = "% du total"; +"codex_workspaces_refresh_usage_data" = "Actualiser les données d'utilisation"; +"codex_workspaces_relative_usage_accessibility" = "%d pour cent de l'utilisation maximale d'un espace de travail"; +"codex_workspaces_relative_usage_tooltip" = "Utilise %d%% des jetons de l'espace de travail le plus utilisé sur cette période."; +"codex_workspaces_reveal_in_finder" = "Afficher dans le Finder"; +"codex_workspaces_saving_project_index" = "Enregistrement de l'index des projets…"; +"codex_workspaces_scanning_logs" = "Analyse des journaux Codex…"; +"codex_workspaces_search_sessions" = "Rechercher des sessions"; +"codex_workspaces_selected_day_accessibility" = "%@ : %@ jetons"; +"codex_workspaces_session" = "Session"; +"codex_workspaces_session_count" = "Sessions : %d"; +"codex_workspaces_sessions" = "Sessions"; +"codex_workspaces_showing_last_complete_index" = "Affichage du dernier index local complet"; +"codex_workspaces_stale_status" = "L'index d'utilisation est obsolète · Mis à jour il y a %@"; +"codex_workspaces_started" = "Démarrée"; +"codex_workspaces_title" = "Espaces de travail"; +"codex_workspaces_token_count_accessibility" = "%@ jetons"; +"codex_workspaces_tokens" = "Jetons"; +"codex_workspaces_top_model" = "Modèle principal"; +"codex_workspaces_top_sessions" = "Principales sessions"; +"codex_workspaces_total" = "Total"; +"codex_workspaces_view" = "Affichage"; +"codex_workspaces_view_all_sessions" = "Afficher toutes les sessions"; +"codex_workspaces_window_title" = "Espaces de travail Codex"; +"codex_workspaces_workspace_count" = "Espaces de travail et discussions : %d"; +"codex_workspaces_workspaces_chats" = "Espaces de travail/discussions"; diff --git a/Sources/CodexBar/Resources/gl.lproj/Localizable.strings b/Sources/CodexBar/Resources/gl.lproj/Localizable.strings index 43bd695894..3bac118a91 100644 --- a/Sources/CodexBar/Resources/gl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/gl.lproj/Localizable.strings @@ -1349,3 +1349,255 @@ "Cost today unavailable" = "Custo de hoxe: Non dispoñible"; "30-day cost unavailable" = "Custo de 30 días: Non dispoñible"; "Resets" = "Reinicios"; + +/* Codex Workspaces and Models */ +"codex_workspaces_open_in_window" = "Abrir nunha xanela"; +"codex_models_actions" = "Accións"; +"codex_models_active_and_new_count" = "Modelos activos: %1$d; recén activo: %2$d"; +"codex_models_active_count_new_unavailable" = "Modelos activos: %1$d; o reconto recentemente activo non está dispoñible"; +"codex_models_active_models" = "Modelos activos"; +"codex_models_alias_count" = "Alias: %1$d"; +"codex_models_all_model_count" = "Todos os modelos: %1$d"; +"codex_models_all_models" = "Todos os modelos"; +"codex_models_all_rows" = "Todas as filas"; +"codex_models_analytics" = "Análise de modelos"; +"codex_models_analytics_controls" = "Controis de análise de modelos"; +"codex_models_apply" = "Solicitar"; +"codex_models_bucket_count" = "Depósitos (%2$@): %1$d"; +"codex_models_bucket_interval" = "Intervalo de balde"; +"codex_models_bullet_value" = "· %1$@"; +"codex_models_canonical_id" = "ID canónico"; +"codex_models_canonical_model" = "Modelo canónico"; +"codex_models_change" = "Cambio"; +"codex_models_change_from_peak" = "%1$@ dende o pico de %2$@"; +"codex_models_change_from_previous_period" = "%1$@ do período igual anterior"; +"codex_models_choose_metric" = "Escolla a métrica de análise"; +"codex_models_clear_search" = "Borrar a busca"; +"codex_models_clear_selection" = "Borrar a selección de modelos"; +"codex_models_columns" = "Columnas"; +"codex_models_compact_billions_suffix" = "B"; +"codex_models_compact_millions_suffix" = "M"; +"codex_models_compact_thousands_suffix" = "K"; +"codex_models_contains_selected_suffix" = ", contén o modelo seleccionado"; +"codex_models_copy_aliases" = "Copiar alias"; +"codex_models_copy_canonical_id" = "Copiar ID canónico"; +"codex_models_copy_canonical_model_id" = "Copiar o ID do modelo canónico"; +"codex_models_copy_raw_aliases" = "Copiar alias en bruto"; +"codex_models_cost_conclusions_limited" = "As conclusións dos custos están limitadas a %1$@ fichas de prezo."; +"codex_models_cost_hidden" = "Custo oculto"; +"codex_models_cost_status_currency" = "%1$@ · %2$@ · %3$@"; +"codex_models_cost_unavailable" = "Custo non dispoñible"; +"codex_models_cost_unavailable_for_data" = "O custo non está dispoñible para estes datos"; +"codex_models_coverage" = "Cobertura"; +"codex_models_currency" = "Moeda"; +"codex_models_current_metric" = "%1$@ actual"; +"codex_models_current_period_in_progress" = "%1$@ actual en curso"; +"codex_models_date" = "Data"; +"codex_models_day_lowercase" = "día"; +"codex_models_days" = "Días"; +"codex_models_delta_vs_prior" = "Δ vs anterior"; +"codex_models_disabled_in_settings" = "Desactivado na configuración"; +"codex_models_dominates_at" = "Este modelo domina %1$@ en %2$@."; +"codex_models_ended" = "Rematou"; +"codex_models_export" = "Exportar"; +"codex_models_export_all_filename" = "codex-models-all.csv"; +"codex_models_export_default_filename" = "codex-models.csv"; +"codex_models_export_failed" = "Non se puideron exportar as análises de modelos"; +"codex_models_export_selected_filename" = "codex-model-%1$@.csv"; +"codex_models_export_selected_model" = "Exportar o modelo seleccionado"; +"codex_models_export_visible_filename" = "codex-models-visible.csv"; +"codex_models_export_visible_rows" = "Exportar filas de modelos visibles"; +"codex_models_focus_table_search" = "Busca na táboa Focus Models"; +"codex_models_fully_priced" = "Prezo total"; +"codex_models_fully_priced_percent" = "100 % de prezo"; +"codex_models_generated" = "Xerouse %1$@"; +"codex_models_granularity_accessibility" = "Granularidade, %1$@, %2$@"; +"codex_models_hidden" = "Oculto"; +"codex_models_identity_aliases_help" = "%1$@ · %2$@ · Alias: %3$@"; +"codex_models_identity_and_aliases" = "Identidade e alias"; +"codex_models_indexing_last_days" = "Indexando os últimos %1$d días..."; +"codex_models_interpretation" = "Interpretación"; +"codex_models_known_cost" = "Custo coñecido"; +"codex_models_known_cost_fully_priced_accessibility" = "Custo coñecido %1$@, co prezo total"; +"codex_models_known_cost_lowercase" = "custo coñecido"; +"codex_models_known_cost_partial_accessibility" = "Custo coñecido %1$@, parcial, %2$@ dos tokens cun prezo"; +"codex_models_last_days" = "Últimos %1$d días"; +"codex_models_local_logs_source" = "· Rexistros locais do Codex"; +"codex_models_major_contributor_at" = "Este modelo é un contribuínte importante en %1$@."; +"codex_models_metric" = "Métrica"; +"codex_models_metric_over_time" = "%1$@ ao longo do tempo"; +"codex_models_model" = "Modelo"; +"codex_models_model_concentration" = "Modelo de concentración"; +"codex_models_model_count" = "Modelos: %1$d"; +"codex_models_model_usage" = "Uso do modelo"; +"codex_models_month_lowercase" = "mes"; +"codex_models_more_alias_count" = "Máis alias: %1$d"; +"codex_models_name_canonical_id" = "%1$@, ID canónico %2$@"; +"codex_models_name_value_accessibility" = "%1$@, %2$@"; +"codex_models_named_metric_over_time" = "%1$@ %2$@ ao longo do tempo"; +"codex_models_new" = "Novo"; +"codex_models_new_in_period" = "Novo neste período"; +"codex_models_newly_active_count" = "Activo recén: %1$d"; +"codex_models_no_applicable_price" = "Prezo non aplicable"; +"codex_models_no_change_from_previous_period" = "Sen cambios respecto ao período igual anterior"; +"codex_models_no_matching_models" = "Non hai modelos coincidentes"; +"codex_models_no_newly_active" = "Non hai novos modelos activos"; +"codex_models_no_top_model" = "Sen modelo superior"; +"codex_models_no_usage" = "Sen uso"; +"codex_models_no_usage_description" = "Escolla un intervalo de datas máis longo arriba ou actualíceo despois de gardar as sesións locais do Codex."; +"codex_models_no_usage_in_period" = "Non hai uso neste período"; +"codex_models_no_usage_in_range" = "Non se utiliza ningún modelo neste intervalo"; +"codex_models_other_count" = "Outro (%1$d)"; +"codex_models_partial_priced" = "Parcial · Prezo de %1$@"; +"codex_models_precision_table" = "Modelo de táboa de precisión"; +"codex_models_previous_known_cost" = "Custo coñecido anterior"; +"codex_models_previous_metric" = "Anterior %1$@"; +"codex_models_previous_period_unavailable" = "O período anterior non está dispoñible"; +"codex_models_previous_unavailable" = "Anterior non dispoñible"; +"codex_models_priced_tokens" = "Fichas con prezo"; +"codex_models_pricing_calculation" = "Cálculo de prezos"; +"codex_models_pricing_coverage" = "Cobertura de prezos"; +"codex_models_pricing_explanation" = "%1$@ cobre as fichas con prezo de %2$@. Exclúense as fichas %3$@ sen unha regra aplicable, pero non se estiman como cero."; +"codex_models_ranking_by" = "Clasificación do modelo por %1$@"; +"codex_models_ranking_hint" = "Fai clic nunha barra de modelo para abrir os seus detalles."; +"codex_models_ranking_point_accessibility" = "%1$@, %2$@, %3$@ compartir"; +"codex_models_ranking_summary" = "Modelos canónicos e Outros clasificados por %1$@."; +"codex_models_ref_count" = "%1$@ refs"; +"codex_models_refresh_analytics" = "Actualizar análise de modelos"; +"codex_models_refresh_command_help" = "Actualizar análise de modelos (Comando-R)"; +"codex_models_refresh_model_usage" = "Actualizar o uso do modelo"; +"codex_models_refreshing_last_snapshot" = "Actualizando · mostrando a última instantánea exitosa"; +"codex_models_retry" = "Tentar de novo"; +"codex_models_reveal_pricing_calculation" = "Amosar o cálculo do prezo"; +"codex_models_search_empty_description" = "A busca só filtra esta táboa; os totais do panel non se modifican."; +"codex_models_search_models" = "Busca modelos"; +"codex_models_search_scope" = "%1$@ · A busca só filtra esta táboa"; +"codex_models_search_table" = "Táboa de modelos de busca"; +"codex_models_select_model" = "Selecciona %1$@"; +"codex_models_selected" = "Seleccionado"; +"codex_models_selected_bucket" = "Cubo seleccionado"; +"codex_models_selected_bucket_accessibility" = "Grupo seleccionado: %1$@, %2$@, %3$@ tokens, %4$@ referencias de sesión, %5$@, moeda %6$@."; +"codex_models_selected_clear_available" = "Modelo seleccionado %1$@, Borrar selección dispoñible"; +"codex_models_selected_date" = "Data seleccionada"; +"codex_models_selected_hidden_by_search" = "Modelo seleccionado oculto pola busca"; +"codex_models_selected_model" = "Modelo seleccionado"; +"codex_models_selected_model_interval" = "Modelo seleccionado · %1$@"; +"codex_models_selected_suffix" = ", seleccionado"; +"codex_models_session_reference_count" = "%1$@ referencias da sesión"; +"codex_models_session_references" = "Referencias da sesión"; +"codex_models_session_references_lowercase" = "referencias da sesión"; +"codex_models_session_refs" = "Sesión ref"; +"codex_models_share" = "Compartir"; +"codex_models_share_known_cost_help" = "Participación do custo coñecido; exclúe o uso sen prezo"; +"codex_models_share_of" = "Compartir de %1$@"; +"codex_models_show_associated_sessions" = "Mostrar sesións asociadas"; +"codex_models_show_details" = "Mostrar detalles"; +"codex_models_showing_last_snapshot" = "Mostrando a última instantánea exitosa"; +"codex_models_sorted_by" = "Ordenado por %1$@"; +"codex_models_subject_over_time" = "%1$@ ao longo do tempo"; +"codex_models_summary" = "Resumo dos modelos"; +"codex_models_timeline_accessibility" = "Cronoloxía dos modelos, %1$@, %2$@"; +"codex_models_timeline_hint" = "Fai clic nun intervalo de tempo ou manteña o punteiro sobre un durante 1,5 segundos para inspeccionar os seus detalles."; +"codex_models_timeline_point_accessibility" = "%1$@, %2$@, %3$@ tokens, %4$@ referencias de sesión, %5$@, moeda %6$@"; +"codex_models_timeline_summary" = "%1$@ %2$@ cronoloxía. %3$@"; +"codex_models_token_count" = "%1$@ tokens"; +"codex_models_tokens" = "Fichas"; +"codex_models_tokens_lowercase" = "fichas"; +"codex_models_tokens_per_session_reference" = "Aproximadamente %1$@ tokens por referencia de sesión."; +"codex_models_top" = "Arriba"; +"codex_models_top_model" = "Modelo superior"; +"codex_models_top_seven_other" = "Top 7 + Outros"; +"codex_models_top_two_account_for" = "Os dous modelos principais representan %1$@ de %2$@"; +"codex_models_top_two_of" = "Os dous modelos principais de %1$@"; +"codex_models_total_session_references_lowercase" = "referencias totais da sesión"; +"codex_models_total_tokens" = "Fichas totais"; +"codex_models_total_tokens_lowercase" = "fichas totais"; +"codex_models_totals_reconcile" = "· Reconciliación dos totais coa instantánea activa"; +"codex_models_track_end" = "Fin da pista"; +"codex_models_track_start" = "Inicio da pista"; +"codex_models_trend_unavailable_observed_buckets" = "Cubos observados: %1$d. Tendencia non dispoñible."; +"codex_models_unknown_export_error" = "Erro de exportación descoñecido"; +"codex_models_unpriced_tokens" = "Fichas sen prezo"; +"codex_models_updated" = "Actualizado %1$@"; +"codex_models_usage_and_change" = "Uso e cambio"; +"codex_models_usage_by_model" = "Uso por modelo"; +"codex_models_value_comparison_accessibility" = "%1$@, %2$@"; +"codex_models_value_detail" = "%1$@ · %2$@"; +"codex_models_value_share" = "%1$@ · %2$@ compartir"; +"codex_models_value_share_accessibility" = "%1$@, %2$@ compartir"; +"codex_models_value_start" = "Valor de inicio"; +"codex_models_visible_of_total" = "Modelos visibles: %1$d de %2$d"; +"codex_models_visible_rows" = "Filas visibles"; +"codex_models_vs_previous" = "%1$@ fronte ao anterior"; +"codex_models_week_lowercase" = "semana"; +"codex_workspaces_all_models" = "Todos os modelos"; +"codex_workspaces_all_workspaces" = "Todos os espazos de traballo"; +"codex_workspaces_associated_with_selected_model" = "Asociado co modelo seleccionado"; +"codex_workspaces_bullet_value" = "· %1$@"; +"codex_workspaces_chart_keyboard_hint" = "Usa as teclas de frecha esquerda e dereita ou as accións de axuste de VoiceOver para seleccionar un día. Preme Retorno ou Espazo para fixalo."; +"codex_workspaces_chats_description" = "Conversas e chats"; +"codex_workspaces_copy_path" = "Copiar a ruta"; +"codex_workspaces_cost_unavailable" = "Custo non dispoñible"; +"codex_workspaces_daily_callout_accessibility" = "%1$@, %2$@"; +"codex_workspaces_daily_point_accessibility" = "%1$@, fichas %2$@"; +"codex_workspaces_daily_point_help" = "%1$@ — fichas %2$@"; +"codex_workspaces_daily_token_usage" = "Uso diario de tokens"; +"codex_workspaces_daily_unavailable" = "O uso diario non está dispoñible"; +"codex_workspaces_daily_unavailable_description" = "O total calculouse a partir de %d sesións, pero non foi posible determinar as súas datas."; +"codex_workspaces_date" = "Data"; +"codex_workspaces_days" = "Días"; +"codex_workspaces_elevated_usage" = "Uso elevado"; +"codex_workspaces_elevated_usage_help" = "O uso supera a mediana do período seleccionado."; +"codex_workspaces_estimated_cost_short" = "Custo estimado"; +"codex_workspaces_estimated_cost_value" = "estim. %@"; +"codex_workspaces_estimated_local_logs" = "Estimado a partir dos rexistros locais de Codex"; +"codex_workspaces_failed_read_logs" = "Non foi posible ler os rexistros de Codex"; +"codex_workspaces_failed_some_logs" = "Non foi posible ler algúns rexistros de Codex"; +"codex_workspaces_finalizing_project_index_files" = "Finalizando o índice de proxectos… %d ficheiros"; +"codex_workspaces_high_usage" = "Uso alto"; +"codex_workspaces_high_usage_help" = "O uso é un valor atípico significativo para o período seleccionado."; +"codex_workspaces_indexed_files" = "Actualizado hai %@ · %d ficheiros indexados"; +"codex_workspaces_indexing_local_logs" = "Indexando os rexistros locais de Codex…"; +"codex_workspaces_indexing_projects" = "Indexando os proxectos…"; +"codex_workspaces_indexing_projects_files_left" = "Indexando os proxectos… %d/%d ficheiros, quedan %d"; +"codex_workspaces_last_days" = "Últimos %d días"; +"codex_workspaces_local_codex_chat" = "Conversa local de Codex"; +"codex_workspaces_model" = "Modelo"; +"codex_workspaces_models" = "Modelos"; +"codex_workspaces_no_daily_usage" = "Non hai uso diario neste intervalo do historial."; +"codex_workspaces_no_local_usage" = "Aínda non hai uso local de Codex"; +"codex_workspaces_no_models" = "Non hai desglose por modelo dispoñible"; +"codex_workspaces_no_projects_or_chats" = "Non se atoparon proxectos nin conversas neste intervalo do historial."; +"codex_workspaces_no_sessions" = "Non hai sesións neste espazo de traballo"; +"codex_workspaces_normal_usage" = "Uso normal"; +"codex_workspaces_not_available" = "Non dispoñible"; +"codex_workspaces_overview" = "Resumo"; +"codex_workspaces_partial_breakdown_help" = "O resumo segue sendo a referencia mentres os metadatos de data ou modelo non dispoñibles se mostran como parciais."; +"codex_workspaces_partial_breakdown_notice" = "Algúns desgloses detallados non cobren o total completo."; +"codex_workspaces_partial_data" = "Datos parciais"; +"codex_workspaces_percent_total" = "% do total"; +"codex_workspaces_refresh_usage_data" = "Actualizar os datos de uso"; +"codex_workspaces_relative_usage_accessibility" = "%d por cento do uso máximo dun espazo de traballo"; +"codex_workspaces_relative_usage_tooltip" = "Usa o %d%% dos tokens do espazo de traballo con máis uso neste período."; +"codex_workspaces_reveal_in_finder" = "Mostrar no Finder"; +"codex_workspaces_saving_project_index" = "Gardando o índice de proxectos…"; +"codex_workspaces_scanning_logs" = "Analizando os rexistros de Codex…"; +"codex_workspaces_search_sessions" = "Buscar sesións"; +"codex_workspaces_selected_day_accessibility" = "%@: %@ tokens"; +"codex_workspaces_session" = "Sesión"; +"codex_workspaces_session_count" = "Sesións: %d"; +"codex_workspaces_sessions" = "Sesións"; +"codex_workspaces_showing_last_complete_index" = "Mostrando o último índice local completo"; +"codex_workspaces_stale_status" = "O índice de uso está desactualizado · Actualizado hai %@"; +"codex_workspaces_started" = "Iniciada"; +"codex_workspaces_title" = "Espazos de traballo"; +"codex_workspaces_token_count_accessibility" = "%@ tokens"; +"codex_workspaces_tokens" = "Tokens"; +"codex_workspaces_top_model" = "Modelo principal"; +"codex_workspaces_top_sessions" = "Sesións principais"; +"codex_workspaces_total" = "Total"; +"codex_workspaces_view" = "Vista"; +"codex_workspaces_view_all_sessions" = "Ver todas as sesións"; +"codex_workspaces_window_title" = "Espazos de traballo de Codex"; +"codex_workspaces_workspace_count" = "Espazos de traballo e conversas: %d"; +"codex_workspaces_workspaces_chats" = "Espazos de traballo/conversas"; diff --git a/Sources/CodexBar/Resources/id.lproj/Localizable.strings b/Sources/CodexBar/Resources/id.lproj/Localizable.strings index 275d7b769e..8ece5b7ec6 100644 --- a/Sources/CodexBar/Resources/id.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/id.lproj/Localizable.strings @@ -1353,3 +1353,255 @@ "Cost today unavailable" = "Biaya hari ini: Tidak tersedia"; "30-day cost unavailable" = "Biaya 30 hari: Tidak tersedia"; "Resets" = "Reset"; + +/* Codex Workspaces and Models */ +"codex_workspaces_open_in_window" = "Buka di Jendela"; +"codex_models_actions" = "Tindakan"; +"codex_models_active_and_new_count" = "Model aktif: %1$d; baru aktif: %2$d"; +"codex_models_active_count_new_unavailable" = "Model aktif: %1$d; hitungan aktif baru tidak tersedia"; +"codex_models_active_models" = "Model aktif"; +"codex_models_alias_count" = "Alias: %1$d"; +"codex_models_all_model_count" = "Semua model: %1$d"; +"codex_models_all_models" = "Semua model"; +"codex_models_all_rows" = "Semua baris"; +"codex_models_analytics" = "Analisis model"; +"codex_models_analytics_controls" = "Kontrol analitik model"; +"codex_models_apply" = "Lamar"; +"codex_models_bucket_count" = "Keranjang (%2$@): %1$d"; +"codex_models_bucket_interval" = "Interval keranjang"; +"codex_models_bullet_value" = "· %1$@"; +"codex_models_canonical_id" = "ID Kanonis"; +"codex_models_canonical_model" = "Model kanonik"; +"codex_models_change" = "Ubah"; +"codex_models_change_from_peak" = "%1$@ dari %2$@ puncak"; +"codex_models_change_from_previous_period" = "%1$@ dari periode yang sama sebelumnya"; +"codex_models_choose_metric" = "Pilih metrik analitik"; +"codex_models_clear_search" = "Hapus Pencarian"; +"codex_models_clear_selection" = "Hapus pemilihan Model"; +"codex_models_columns" = "Kolom"; +"codex_models_compact_billions_suffix" = "B"; +"codex_models_compact_millions_suffix" = "M"; +"codex_models_compact_thousands_suffix" = "K"; +"codex_models_contains_selected_suffix" = ", berisi model yang dipilih"; +"codex_models_copy_aliases" = "Salin alias"; +"codex_models_copy_canonical_id" = "Salin ID kanonis"; +"codex_models_copy_canonical_model_id" = "Salin ID model kanonik"; +"codex_models_copy_raw_aliases" = "Salin alias mentah"; +"codex_models_cost_conclusions_limited" = "Kesimpulan biaya terbatas pada token dengan harga %1$@."; +"codex_models_cost_hidden" = "Biaya tersembunyi"; +"codex_models_cost_status_currency" = "%1$@ · %2$@ · %3$@"; +"codex_models_cost_unavailable" = "Biaya tidak tersedia"; +"codex_models_cost_unavailable_for_data" = "Biaya tidak tersedia untuk data ini"; +"codex_models_coverage" = "Cakupan"; +"codex_models_currency" = "Mata uang"; +"codex_models_current_metric" = "Saat ini %1$@"; +"codex_models_current_period_in_progress" = "Saat ini %1$@ sedang dalam proses"; +"codex_models_date" = "Tanggal"; +"codex_models_day_lowercase" = "hari"; +"codex_models_days" = "Hari"; +"codex_models_delta_vs_prior" = "Δ vs sebelumnya"; +"codex_models_disabled_in_settings" = "Dinonaktifkan di pengaturan"; +"codex_models_dominates_at" = "Model ini mendominasi %1$@ di %2$@."; +"codex_models_ended" = "Berakhir"; +"codex_models_export" = "Ekspor"; +"codex_models_export_all_filename" = "codex-models-all.csv"; +"codex_models_export_default_filename" = "codex-models.csv"; +"codex_models_export_failed" = "Tidak dapat mengekspor analisis Model"; +"codex_models_export_selected_filename" = "codex-model-%1$@.csv"; +"codex_models_export_selected_model" = "Ekspor model yang dipilih"; +"codex_models_export_visible_filename" = "codex-models-visible.csv"; +"codex_models_export_visible_rows" = "Ekspor baris Model yang terlihat"; +"codex_models_focus_table_search" = "Pencarian tabel Model Fokus"; +"codex_models_fully_priced" = "Harga penuh"; +"codex_models_fully_priced_percent" = "100% harga"; +"codex_models_generated" = "Menghasilkan %1$@"; +"codex_models_granularity_accessibility" = "Perincian, %1$@, %2$@"; +"codex_models_hidden" = "Tersembunyi"; +"codex_models_identity_aliases_help" = "%1$@ · %2$@ · Alias: %3$@"; +"codex_models_identity_and_aliases" = "Identitas dan alias"; +"codex_models_indexing_last_days" = "Pengindeksan %1$d hari terakhir…"; +"codex_models_interpretation" = "Interpretasi"; +"codex_models_known_cost" = "Biaya yang diketahui"; +"codex_models_known_cost_fully_priced_accessibility" = "Harga diketahui %1$@, harga penuh"; +"codex_models_known_cost_lowercase" = "biaya yang diketahui"; +"codex_models_known_cost_partial_accessibility" = "Biaya yang diketahui %1$@, sebagian, %2$@ harga token"; +"codex_models_last_days" = "%1$d hari terakhir"; +"codex_models_local_logs_source" = "· Log Codex Lokal"; +"codex_models_major_contributor_at" = "Model ini merupakan kontributor utama di %1$@."; +"codex_models_metric" = "Metrik"; +"codex_models_metric_over_time" = "%1$@ seiring berjalannya waktu"; +"codex_models_model" = "Model"; +"codex_models_model_concentration" = "Konsentrasi model"; +"codex_models_model_count" = "Model: %1$d"; +"codex_models_model_usage" = "Penggunaan model"; +"codex_models_month_lowercase" = "bulan"; +"codex_models_more_alias_count" = "Alias lainnya: %1$d"; +"codex_models_name_canonical_id" = "%1$@, ID kanonis %2$@"; +"codex_models_name_value_accessibility" = "%1$@, %2$@"; +"codex_models_named_metric_over_time" = "%1$@ %2$@ seiring berjalannya waktu"; +"codex_models_new" = "Baru"; +"codex_models_new_in_period" = "Baru di periode ini"; +"codex_models_newly_active_count" = "Baru aktif: %1$d"; +"codex_models_no_applicable_price" = "Tidak ada harga yang berlaku"; +"codex_models_no_change_from_previous_period" = "Tidak ada perubahan dari periode yang sama sebelumnya"; +"codex_models_no_matching_models" = "Tidak ada model yang cocok"; +"codex_models_no_newly_active" = "Tidak ada model baru yang aktif"; +"codex_models_no_top_model" = "Tidak ada model teratas"; +"codex_models_no_usage" = "Tidak ada penggunaan"; +"codex_models_no_usage_description" = "Pilih rentang tanggal yang lebih panjang di atas atau segarkan setelah sesi Codex lokal disimpan."; +"codex_models_no_usage_in_period" = "Tidak ada penggunaan pada periode ini"; +"codex_models_no_usage_in_range" = "Tidak ada penggunaan model dalam kisaran ini"; +"codex_models_other_count" = "Lainnya (%1$d)"; +"codex_models_partial_priced" = "Parsial · %1$@ harga"; +"codex_models_precision_table" = "Tabel presisi model"; +"codex_models_previous_known_cost" = "Biaya yang diketahui sebelumnya"; +"codex_models_previous_metric" = "Sebelumnya %1$@"; +"codex_models_previous_period_unavailable" = "Periode sebelumnya tidak tersedia"; +"codex_models_previous_unavailable" = "Sebelumnya tidak tersedia"; +"codex_models_priced_tokens" = "Token dengan harga"; +"codex_models_pricing_calculation" = "Perhitungan harga"; +"codex_models_pricing_coverage" = "Cakupan harga"; +"codex_models_pricing_explanation" = "%1$@ mencakup %2$@ token dengan harga. %3$@ token tanpa aturan yang berlaku dikecualikan, tidak diperkirakan nol."; +"codex_models_ranking_by" = "Peringkat model sebesar %1$@"; +"codex_models_ranking_hint" = "Klik bilah model untuk membuka detailnya."; +"codex_models_ranking_point_accessibility" = "%1$@, %2$@, %3$@ berbagi"; +"codex_models_ranking_summary" = "Model kanonik dan Lainnya diberi peringkat oleh %1$@."; +"codex_models_ref_count" = "%1$@ referensi"; +"codex_models_refresh_analytics" = "Segarkan analisis Model"; +"codex_models_refresh_command_help" = "Analisis Refresh Model (Command-R)"; +"codex_models_refresh_model_usage" = "Segarkan Penggunaan Model"; +"codex_models_refreshing_last_snapshot" = "Menyegarkan · menampilkan cuplikan terakhir yang berhasil"; +"codex_models_retry" = "Coba lagi"; +"codex_models_reveal_pricing_calculation" = "Mengungkapkan perhitungan harga"; +"codex_models_search_empty_description" = "Pencarian memfilter tabel ini saja; total dasbor tidak berubah."; +"codex_models_search_models" = "Model pencarian"; +"codex_models_search_scope" = "%1$@ · Pencarian memfilter tabel ini saja"; +"codex_models_search_table" = "Tabel Model Pencarian"; +"codex_models_select_model" = "Pilih %1$@"; +"codex_models_selected" = "Terpilih"; +"codex_models_selected_bucket" = "Ember yang dipilih"; +"codex_models_selected_bucket_accessibility" = "Keranjang yang dipilih: %1$@, %2$@, %3$@ token, %4$@ referensi sesi, %5$@, mata uang %6$@."; +"codex_models_selected_clear_available" = "Model terpilih %1$@, Hapus pilihan tersedia"; +"codex_models_selected_date" = "Tanggal yang dipilih"; +"codex_models_selected_hidden_by_search" = "Model yang dipilih disembunyikan oleh pencarian"; +"codex_models_selected_model" = "Model yang dipilih"; +"codex_models_selected_model_interval" = "Model terpilih · %1$@"; +"codex_models_selected_suffix" = ", dipilih"; +"codex_models_session_reference_count" = "%1$@ referensi sesi"; +"codex_models_session_references" = "Referensi sesi"; +"codex_models_session_references_lowercase" = "referensi sesi"; +"codex_models_session_refs" = "Referensi sesi"; +"codex_models_share" = "Bagikan"; +"codex_models_share_known_cost_help" = "Bagian dari biaya yang diketahui; tidak termasuk penggunaan tanpa harga"; +"codex_models_share_of" = "Bagian %1$@"; +"codex_models_show_associated_sessions" = "Tampilkan sesi terkait"; +"codex_models_show_details" = "Tampilkan detailnya"; +"codex_models_showing_last_snapshot" = "Menampilkan cuplikan terakhir yang berhasil"; +"codex_models_sorted_by" = "Diurutkan berdasarkan %1$@"; +"codex_models_subject_over_time" = "%1$@ seiring berjalannya waktu"; +"codex_models_summary" = "Ringkasan model"; +"codex_models_timeline_accessibility" = "Garis waktu model, %1$@, %2$@"; +"codex_models_timeline_hint" = "Klik keranjang waktu, atau tahan penunjuk di atasnya selama 1,5 detik, untuk memeriksa detailnya."; +"codex_models_timeline_point_accessibility" = "%1$@, %2$@, %3$@ token, %4$@ referensi sesi, %5$@, mata uang %6$@"; +"codex_models_timeline_summary" = "%1$@ %2$@ garis waktu. %3$@"; +"codex_models_token_count" = "%1$@ token"; +"codex_models_tokens" = "Token"; +"codex_models_tokens_lowercase" = "token"; +"codex_models_tokens_per_session_reference" = "Tentang %1$@ token per referensi sesi."; +"codex_models_top" = "Atas"; +"codex_models_top_model" = "Model teratas"; +"codex_models_top_seven_other" = "7 Teratas + Lainnya"; +"codex_models_top_two_account_for" = "Dua model teratas menyumbang %1$@ dari %2$@"; +"codex_models_top_two_of" = "Dua model teratas %1$@"; +"codex_models_total_session_references_lowercase" = "total referensi sesi"; +"codex_models_total_tokens" = "Jumlah token"; +"codex_models_total_tokens_lowercase" = "jumlah token"; +"codex_models_totals_reconcile" = "· Total direkonsiliasi dengan snapshot aktif"; +"codex_models_track_end" = "Akhir trek"; +"codex_models_track_start" = "Awal lintasan"; +"codex_models_trend_unavailable_observed_buckets" = "Keranjang yang diamati: %1$d. Tren tidak tersedia."; +"codex_models_unknown_export_error" = "Kesalahan ekspor tidak diketahui"; +"codex_models_unpriced_tokens" = "Token yang tidak diberi harga"; +"codex_models_updated" = "Diperbarui %1$@"; +"codex_models_usage_and_change" = "Penggunaan dan perubahan"; +"codex_models_usage_by_model" = "Penggunaan berdasarkan model"; +"codex_models_value_comparison_accessibility" = "%1$@, %2$@"; +"codex_models_value_detail" = "%1$@ · %2$@"; +"codex_models_value_share" = "%1$@ · %2$@ berbagi"; +"codex_models_value_share_accessibility" = "%1$@, %2$@ berbagi"; +"codex_models_value_start" = "Nilai awal"; +"codex_models_visible_of_total" = "Model yang terlihat: %1$d dari %2$d"; +"codex_models_visible_rows" = "Baris yang terlihat"; +"codex_models_vs_previous" = "%1$@ vs sebelumnya"; +"codex_models_week_lowercase" = "minggu"; +"codex_workspaces_all_models" = "Semua model"; +"codex_workspaces_all_workspaces" = "Semua ruang kerja"; +"codex_workspaces_associated_with_selected_model" = "Terkait dengan model yang dipilih"; +"codex_workspaces_bullet_value" = "· %1$@"; +"codex_workspaces_chart_keyboard_hint" = "Gunakan tombol panah kiri dan kanan atau tindakan penyesuaian VoiceOver untuk memilih hari. Tekan Return atau Spasi untuk menyematkannya."; +"codex_workspaces_chats_description" = "Obrolan dan percakapan"; +"codex_workspaces_copy_path" = "Salin Jalur"; +"codex_workspaces_cost_unavailable" = "Biaya tidak tersedia"; +"codex_workspaces_daily_callout_accessibility" = "%1$@, %2$@"; +"codex_workspaces_daily_point_accessibility" = "%1$@, token %2$@"; +"codex_workspaces_daily_point_help" = "%1$@ — %2$@ token"; +"codex_workspaces_daily_token_usage" = "Penggunaan token harian"; +"codex_workspaces_daily_unavailable" = "Penggunaan harian tidak tersedia"; +"codex_workspaces_daily_unavailable_description" = "Total dihitung dari %d sesi, tetapi tanggalnya tidak dapat ditentukan."; +"codex_workspaces_date" = "Tanggal"; +"codex_workspaces_days" = "hari"; +"codex_workspaces_elevated_usage" = "Penggunaan meningkat"; +"codex_workspaces_elevated_usage_help" = "Penggunaan berada di atas median untuk periode yang dipilih."; +"codex_workspaces_estimated_cost_short" = "Estimasi biaya"; +"codex_workspaces_estimated_cost_value" = "Diperkirakan %@"; +"codex_workspaces_estimated_local_logs" = "Diperkirakan dari log Codex lokal"; +"codex_workspaces_failed_read_logs" = "Gagal membaca log Codex"; +"codex_workspaces_failed_some_logs" = "Gagal membaca beberapa log Codex"; +"codex_workspaces_finalizing_project_index_files" = "Menyelesaikan indeks proyek… %d file"; +"codex_workspaces_high_usage" = "Penggunaan tinggi"; +"codex_workspaces_high_usage_help" = "Penggunaan sangat menyimpang dalam periode yang dipilih."; +"codex_workspaces_indexed_files" = "Diperbarui %@ lalu · %d file diindeks"; +"codex_workspaces_indexing_local_logs" = "Mengindeks log Codex lokal…"; +"codex_workspaces_indexing_projects" = "Mengindeks proyek…"; +"codex_workspaces_indexing_projects_files_left" = "Mengindeks proyek… %d/%d file, %d tersisa"; +"codex_workspaces_last_days" = "%d hari terakhir"; +"codex_workspaces_local_codex_chat" = "Obrolan Codex lokal"; +"codex_workspaces_model" = "Model"; +"codex_workspaces_models" = "Model"; +"codex_workspaces_no_daily_usage" = "Tidak ada penggunaan harian dalam rentang riwayat ini."; +"codex_workspaces_no_local_usage" = "Belum ada penggunaan Codex lokal"; +"codex_workspaces_no_models" = "Perincian model tidak tersedia"; +"codex_workspaces_no_projects_or_chats" = "Tidak ada proyek atau obrolan yang ditemukan dalam rentang riwayat ini."; +"codex_workspaces_no_sessions" = "Tidak ada sesi di ruang kerja ini"; +"codex_workspaces_normal_usage" = "Penggunaan normal"; +"codex_workspaces_not_available" = "Tidak tersedia"; +"codex_workspaces_overview" = "Ikhtisar"; +"codex_workspaces_partial_breakdown_help" = "Ringkasan tetap menjadi acuan; metadata tanggal atau model yang tidak tersedia ditampilkan sebagai sebagian."; +"codex_workspaces_partial_breakdown_notice" = "Beberapa perincian detail tidak mencakup total keseluruhan."; +"codex_workspaces_partial_data" = "Data sebagian"; +"codex_workspaces_percent_total" = "Persentase dari Total"; +"codex_workspaces_refresh_usage_data" = "Segarkan data penggunaan"; +"codex_workspaces_relative_usage_accessibility" = "%d persen dari penggunaan ruang kerja tertinggi"; +"codex_workspaces_relative_usage_tooltip" = "Menggunakan %d%% token dari ruang kerja dengan penggunaan tertinggi pada periode ini."; +"codex_workspaces_reveal_in_finder" = "Tampilkan di Finder"; +"codex_workspaces_saving_project_index" = "Menyimpan indeks proyek…"; +"codex_workspaces_scanning_logs" = "Memindai log Codex…"; +"codex_workspaces_search_sessions" = "Cari sesi"; +"codex_workspaces_selected_day_accessibility" = "%@: %@ token"; +"codex_workspaces_session" = "Sesi"; +"codex_workspaces_session_count" = "Sesi: %d"; +"codex_workspaces_sessions" = "Sesi"; +"codex_workspaces_showing_last_complete_index" = "Menampilkan indeks lokal terakhir yang selesai"; +"codex_workspaces_stale_status" = "Indeks penggunaan sudah usang · diperbarui %@ lalu"; +"codex_workspaces_started" = "Dimulai"; +"codex_workspaces_title" = "Ruang Kerja"; +"codex_workspaces_token_count_accessibility" = "%@ token"; +"codex_workspaces_tokens" = "Token"; +"codex_workspaces_top_model" = "Model teratas"; +"codex_workspaces_top_sessions" = "Sesi teratas"; +"codex_workspaces_total" = "Total"; +"codex_workspaces_view" = "Tampilan"; +"codex_workspaces_view_all_sessions" = "Lihat semua sesi"; +"codex_workspaces_window_title" = "Ruang Kerja Codex"; +"codex_workspaces_workspace_count" = "Ruang kerja dan obrolan: %d"; +"codex_workspaces_workspaces_chats" = "Ruang kerja/obrolan"; diff --git a/Sources/CodexBar/Resources/it.lproj/Localizable.strings b/Sources/CodexBar/Resources/it.lproj/Localizable.strings index 6ce92c2a94..cd58cd54c0 100644 --- a/Sources/CodexBar/Resources/it.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/it.lproj/Localizable.strings @@ -1353,3 +1353,255 @@ "Cost today unavailable" = "Costo oggi: Non disponibile"; "30-day cost unavailable" = "Costo 30 gg: Non disponibile"; "Resets" = "Ripristini"; + +/* Codex Workspaces and Models */ +"codex_workspaces_open_in_window" = "Apri nella finestra"; +"codex_models_actions" = "Azioni"; +"codex_models_active_and_new_count" = "Modelli attivi: %1$d; appena attivo: %2$d"; +"codex_models_active_count_new_unavailable" = "Modelli attivi: %1$d; conteggio appena attivo non disponibile"; +"codex_models_active_models" = "Modelli attivi"; +"codex_models_alias_count" = "Alias: %1$d"; +"codex_models_all_model_count" = "Tutti i modelli: %1$d"; +"codex_models_all_models" = "Tutti i modelli"; +"codex_models_all_rows" = "Tutte le righe"; +"codex_models_analytics" = "Analisi dei modelli"; +"codex_models_analytics_controls" = "Controlli analitici dei modelli"; +"codex_models_apply" = "Applicare"; +"codex_models_bucket_count" = "Bucket (%2$@): %1$d"; +"codex_models_bucket_interval" = "Intervallo di intervallo"; +"codex_models_bullet_value" = "· %1$@"; +"codex_models_canonical_id" = "ID canonico"; +"codex_models_canonical_model" = "Modello canonico"; +"codex_models_change" = "Cambia"; +"codex_models_change_from_peak" = "%1$@ dal picco %2$@"; +"codex_models_change_from_previous_period" = "%1$@ dal periodo uguale precedente"; +"codex_models_choose_metric" = "Scegli la metrica di analisi"; +"codex_models_clear_search" = "Cancella ricerca"; +"codex_models_clear_selection" = "Cancella la selezione dei modelli"; +"codex_models_columns" = "Colonne"; +"codex_models_compact_billions_suffix" = "B"; +"codex_models_compact_millions_suffix" = "M"; +"codex_models_compact_thousands_suffix" = "K"; +"codex_models_contains_selected_suffix" = ", contiene il modello selezionato"; +"codex_models_copy_aliases" = "Copia alias"; +"codex_models_copy_canonical_id" = "Copia ID canonico"; +"codex_models_copy_canonical_model_id" = "Copia l'ID del modello canonico"; +"codex_models_copy_raw_aliases" = "Copia gli alias grezzi"; +"codex_models_cost_conclusions_limited" = "Le conclusioni sui costi sono limitate a token con prezzo %1$@."; +"codex_models_cost_hidden" = "Costo nascosto"; +"codex_models_cost_status_currency" = "%1$@ · %2$@ · %3$@"; +"codex_models_cost_unavailable" = "Costo non disponibile"; +"codex_models_cost_unavailable_for_data" = "Il costo non è disponibile per questi dati"; +"codex_models_coverage" = "Copertura"; +"codex_models_currency" = "Valuta"; +"codex_models_current_metric" = "Attuale %1$@"; +"codex_models_current_period_in_progress" = "%1$@ corrente in corso"; +"codex_models_date" = "Data"; +"codex_models_day_lowercase" = "giorno"; +"codex_models_days" = "Giorni"; +"codex_models_delta_vs_prior" = "Δ vs precedente"; +"codex_models_disabled_in_settings" = "Disabilitato nelle impostazioni"; +"codex_models_dominates_at" = "Questo modello domina %1$@ a %2$@."; +"codex_models_ended" = "Finito"; +"codex_models_export" = "Esporta"; +"codex_models_export_all_filename" = "codex-models-all.csv"; +"codex_models_export_default_filename" = "codex-models.csv"; +"codex_models_export_failed" = "Impossibile esportare l'analisi dei modelli"; +"codex_models_export_selected_filename" = "codex-model-%1$@.csv"; +"codex_models_export_selected_model" = "Esporta il modello selezionato"; +"codex_models_export_visible_filename" = "codex-models-visible.csv"; +"codex_models_export_visible_rows" = "Esporta righe di modelli visibili"; +"codex_models_focus_table_search" = "Ricerca tabella modelli focus"; +"codex_models_fully_priced" = "Prezzo intero"; +"codex_models_fully_priced_percent" = "Prezzo al 100%."; +"codex_models_generated" = "Generato %1$@"; +"codex_models_granularity_accessibility" = "Granularità, %1$@, %2$@"; +"codex_models_hidden" = "Nascosto"; +"codex_models_identity_aliases_help" = "%1$@ · %2$@ · Alias: %3$@"; +"codex_models_identity_and_aliases" = "Identità e alias"; +"codex_models_indexing_last_days" = "Indicizzazione ultimi %1$d giorni…"; +"codex_models_interpretation" = "Interpretazione"; +"codex_models_known_cost" = "Costo noto"; +"codex_models_known_cost_fully_priced_accessibility" = "Costo noto %1$@, prezzo intero"; +"codex_models_known_cost_lowercase" = "costo noto"; +"codex_models_known_cost_partial_accessibility" = "Costo noto %1$@, parziale, %2$@ di token prezzati"; +"codex_models_last_days" = "Ultimi %1$d giorni"; +"codex_models_local_logs_source" = "· Registri del Codex locale"; +"codex_models_major_contributor_at" = "Questo modello è uno dei principali contributori di %1$@."; +"codex_models_metric" = "Metrico"; +"codex_models_metric_over_time" = "%1$@ nel tempo"; +"codex_models_model" = "Modello"; +"codex_models_model_concentration" = "Concentrazione del modello"; +"codex_models_model_count" = "Modelli: %1$d"; +"codex_models_model_usage" = "Utilizzo del modello"; +"codex_models_month_lowercase" = "mese"; +"codex_models_more_alias_count" = "Altri alias: %1$d"; +"codex_models_name_canonical_id" = "%1$@, ID canonico %2$@"; +"codex_models_name_value_accessibility" = "%1$@, %2$@"; +"codex_models_named_metric_over_time" = "%1$@ %2$@ nel tempo"; +"codex_models_new" = "Nuovo"; +"codex_models_new_in_period" = "Novità in questo periodo"; +"codex_models_newly_active_count" = "Appena attivo: %1$d"; +"codex_models_no_applicable_price" = "Nessun prezzo applicabile"; +"codex_models_no_change_from_previous_period" = "Nessuna variazione rispetto al periodo paritetico precedente"; +"codex_models_no_matching_models" = "Nessun modello corrispondente"; +"codex_models_no_newly_active" = "Nessun nuovo modello attivo"; +"codex_models_no_top_model" = "Nessun modello di punta"; +"codex_models_no_usage" = "Nessun utilizzo"; +"codex_models_no_usage_description" = "Scegli un intervallo di date più lungo sopra o aggiorna dopo aver salvato le sessioni del Codex locale."; +"codex_models_no_usage_in_period" = "Nessun utilizzo in questo periodo"; +"codex_models_no_usage_in_range" = "Nessun modello utilizzato in questo intervallo"; +"codex_models_other_count" = "Altro (%1$d)"; +"codex_models_partial_priced" = "Parziale · %1$@ prezzato"; +"codex_models_precision_table" = "Tabella di precisione del modello"; +"codex_models_previous_known_cost" = "Costo precedente noto"; +"codex_models_previous_metric" = "Precedente %1$@"; +"codex_models_previous_period_unavailable" = "Il periodo precedente non è disponibile"; +"codex_models_previous_unavailable" = "Precedente non disponibile"; +"codex_models_priced_tokens" = "Gettoni prezzati"; +"codex_models_pricing_calculation" = "Calcolo del prezzo"; +"codex_models_pricing_coverage" = "Copertura dei prezzi"; +"codex_models_pricing_explanation" = "%1$@ copre token con prezzo %2$@. I token %3$@ senza una regola applicabile sono esclusi, non stimati come zero."; +"codex_models_ranking_by" = "Classifica modello per %1$@"; +"codex_models_ranking_hint" = "Fare clic su una barra del modello per aprirne i dettagli."; +"codex_models_ranking_point_accessibility" = "Quota %1$@, %2$@, %3$@"; +"codex_models_ranking_summary" = "Modelli canonici e altro classificati per %1$@."; +"codex_models_ref_count" = "%1$@ rif"; +"codex_models_refresh_analytics" = "Aggiorna analisi dei modelli"; +"codex_models_refresh_command_help" = "Aggiorna analisi modelli (Comando-R)"; +"codex_models_refresh_model_usage" = "Aggiorna utilizzo modello"; +"codex_models_refreshing_last_snapshot" = "Aggiornamento · mostra l'ultima istantanea riuscita"; +"codex_models_retry" = "Riprova"; +"codex_models_reveal_pricing_calculation" = "Rivela il calcolo dei prezzi"; +"codex_models_search_empty_description" = "La ricerca filtra solo questa tabella; i totali del dashboard rimangono invariati."; +"codex_models_search_models" = "Cerca modelli"; +"codex_models_search_scope" = "%1$@ · La ricerca filtra solo questa tabella"; +"codex_models_search_table" = "Tabella Cerca modelli"; +"codex_models_select_model" = "Seleziona %1$@"; +"codex_models_selected" = "Selezionato"; +"codex_models_selected_bucket" = "Benna selezionata"; +"codex_models_selected_bucket_accessibility" = "Bucket selezionato: token %1$@, %2$@, %3$@, riferimenti sessione %4$@, %5$@, valuta %6$@."; +"codex_models_selected_clear_available" = "Modello selezionato %1$@, Cancella selezione disponibile"; +"codex_models_selected_date" = "Data selezionata"; +"codex_models_selected_hidden_by_search" = "Modello selezionato nascosto dalla ricerca"; +"codex_models_selected_model" = "Modello selezionato"; +"codex_models_selected_model_interval" = "Modello selezionato · %1$@"; +"codex_models_selected_suffix" = ", selezionato"; +"codex_models_session_reference_count" = "%1$@ riferimenti alla sessione"; +"codex_models_session_references" = "Riferimenti alla sessione"; +"codex_models_session_references_lowercase" = "riferimenti alla sessione"; +"codex_models_session_refs" = "Rif. sessione"; +"codex_models_share" = "Condividi"; +"codex_models_share_known_cost_help" = "Quota di costo noto; esclude l'utilizzo senza prezzo"; +"codex_models_share_of" = "Quota di %1$@"; +"codex_models_show_associated_sessions" = "Mostra le sessioni associate"; +"codex_models_show_details" = "Mostra dettagli"; +"codex_models_showing_last_snapshot" = "Visualizzazione dell'ultima istantanea riuscita"; +"codex_models_sorted_by" = "Ordinati per %1$@"; +"codex_models_subject_over_time" = "%1$@ nel tempo"; +"codex_models_summary" = "Riepilogo modelli"; +"codex_models_timeline_accessibility" = "Cronologia dei modelli, %1$@, %2$@"; +"codex_models_timeline_hint" = "Fare clic su un intervallo di tempo o tenere il puntatore su uno di essi per 1,5 secondi per esaminarne i dettagli."; +"codex_models_timeline_point_accessibility" = "Token %1$@, %2$@, %3$@, riferimenti sessione %4$@, %5$@, valuta %6$@"; +"codex_models_timeline_summary" = "%1$@ %2$@ sequenza temporale. %3$@"; +"codex_models_token_count" = "Gettoni %1$@"; +"codex_models_tokens" = "Gettoni"; +"codex_models_tokens_lowercase" = "gettoni"; +"codex_models_tokens_per_session_reference" = "Informazioni sui token %1$@ per riferimento alla sessione."; +"codex_models_top" = "In alto"; +"codex_models_top_model" = "Modello di punta"; +"codex_models_top_seven_other" = "Primi 7 + Altro"; +"codex_models_top_two_account_for" = "I primi due modelli rappresentano %1$@ di %2$@"; +"codex_models_top_two_of" = "I primi due modelli di %1$@"; +"codex_models_total_session_references_lowercase" = "riferimenti totali alla sessione"; +"codex_models_total_tokens" = "Gettoni totali"; +"codex_models_total_tokens_lowercase" = "gettoni totali"; +"codex_models_totals_reconcile" = "· I totali si riconciliano con l'istantanea attiva"; +"codex_models_track_end" = "Fine traccia"; +"codex_models_track_start" = "Inizio traccia"; +"codex_models_trend_unavailable_observed_buckets" = "Bucket osservati: %1$d. Tendenza non disponibile."; +"codex_models_unknown_export_error" = "Errore di esportazione sconosciuto"; +"codex_models_unpriced_tokens" = "Gettoni senza prezzo"; +"codex_models_updated" = "Aggiornato %1$@"; +"codex_models_usage_and_change" = "Utilizzo e modifica"; +"codex_models_usage_by_model" = "Utilizzo per modello"; +"codex_models_value_comparison_accessibility" = "%1$@, %2$@"; +"codex_models_value_detail" = "%1$@ · %2$@"; +"codex_models_value_share" = "%1$@ · %2$@ quota"; +"codex_models_value_share_accessibility" = "%1$@, %2$@ quota"; +"codex_models_value_start" = "Inizio valore"; +"codex_models_visible_of_total" = "Modelli visibili: %1$d di %2$d"; +"codex_models_visible_rows" = "Righe visibili"; +"codex_models_vs_previous" = "%1$@ rispetto al precedente"; +"codex_models_week_lowercase" = "settimana"; +"codex_workspaces_all_models" = "Tutti i modelli"; +"codex_workspaces_all_workspaces" = "Tutti gli spazi di lavoro"; +"codex_workspaces_associated_with_selected_model" = "Associato al modello selezionato"; +"codex_workspaces_bullet_value" = "· %1$@"; +"codex_workspaces_chart_keyboard_hint" = "Usa i tasti Freccia sinistra e Freccia destra o le azioni di regolazione di VoiceOver per selezionare un giorno. Premi Invio o Spazio per fissarlo."; +"codex_workspaces_chats_description" = "Chat e conversazioni"; +"codex_workspaces_copy_path" = "Copia percorso"; +"codex_workspaces_cost_unavailable" = "Costo non disponibile"; +"codex_workspaces_daily_callout_accessibility" = "%1$@, %2$@"; +"codex_workspaces_daily_point_accessibility" = "%1$@, gettoni %2$@"; +"codex_workspaces_daily_point_help" = "%1$@ — gettoni %2$@"; +"codex_workspaces_daily_token_usage" = "Utilizzo giornaliero dei token"; +"codex_workspaces_daily_unavailable" = "Utilizzo giornaliero non disponibile"; +"codex_workspaces_daily_unavailable_description" = "Il totale è stato calcolato da %d sessioni, ma non è stato possibile determinarne le date."; +"codex_workspaces_date" = "Data"; +"codex_workspaces_days" = "Giorni"; +"codex_workspaces_elevated_usage" = "Utilizzo superiore"; +"codex_workspaces_elevated_usage_help" = "L’utilizzo supera la mediana del periodo selezionato."; +"codex_workspaces_estimated_cost_short" = "Costo stimato"; +"codex_workspaces_estimated_cost_value" = "stim. %@"; +"codex_workspaces_estimated_local_logs" = "Stimato dai registri locali di Codex"; +"codex_workspaces_failed_read_logs" = "Impossibile leggere i registri Codex"; +"codex_workspaces_failed_some_logs" = "Impossibile leggere alcuni registri Codex"; +"codex_workspaces_finalizing_project_index_files" = "Finalizzazione dell'indice dei progetti… %d file"; +"codex_workspaces_high_usage" = "Utilizzo elevato"; +"codex_workspaces_high_usage_help" = "L’utilizzo è nettamente anomalo per il periodo selezionato."; +"codex_workspaces_indexed_files" = "Aggiornato %@ · %d file indicizzati"; +"codex_workspaces_indexing_local_logs" = "Indicizzazione dei registri Codex locali…"; +"codex_workspaces_indexing_projects" = "Indicizzazione dei progetti…"; +"codex_workspaces_indexing_projects_files_left" = "Indicizzazione dei progetti… %d/%d file, %d rimanenti"; +"codex_workspaces_last_days" = "Ultimi %d giorni"; +"codex_workspaces_local_codex_chat" = "Chat Codex locale"; +"codex_workspaces_model" = "Modello"; +"codex_workspaces_models" = "Modelli"; +"codex_workspaces_no_daily_usage" = "Nessun utilizzo giornaliero in questa finestra cronologica."; +"codex_workspaces_no_local_usage" = "Nessun utilizzo locale di Codex ancora"; +"codex_workspaces_no_models" = "Nessuna suddivisione per modello disponibile"; +"codex_workspaces_no_projects_or_chats" = "Nessun progetto o chat trovato in questa finestra cronologica."; +"codex_workspaces_no_sessions" = "Nessuna sessione in questo spazio di lavoro"; +"codex_workspaces_normal_usage" = "Utilizzo normale"; +"codex_workspaces_not_available" = "Non disponibile"; +"codex_workspaces_overview" = "Panoramica"; +"codex_workspaces_partial_breakdown_help" = "Il riepilogo resta autorevole mentre i metadati di data o modello non disponibili sono indicati come parziali."; +"codex_workspaces_partial_breakdown_notice" = "Alcune suddivisioni dettagliate non coprono il totale completo."; +"codex_workspaces_partial_data" = "Dati parziali"; +"codex_workspaces_percent_total" = "% del totale"; +"codex_workspaces_refresh_usage_data" = "Aggiorna dati di utilizzo"; +"codex_workspaces_relative_usage_accessibility" = "%d percento dell’utilizzo massimo"; +"codex_workspaces_relative_usage_tooltip" = "Usa il %d%% dei token dello spazio di lavoro con più utilizzo in questo periodo."; +"codex_workspaces_reveal_in_finder" = "Mostra nel Finder"; +"codex_workspaces_saving_project_index" = "Salvataggio dell'indice dei progetti…"; +"codex_workspaces_scanning_logs" = "Analisi dei registri Codex…"; +"codex_workspaces_search_sessions" = "Cerca sessioni"; +"codex_workspaces_selected_day_accessibility" = "%@: %@ token"; +"codex_workspaces_session" = "Sessione"; +"codex_workspaces_session_count" = "Sessioni: %d"; +"codex_workspaces_sessions" = "Sessioni"; +"codex_workspaces_showing_last_complete_index" = "Visualizzazione dell'ultimo indice locale completo"; +"codex_workspaces_stale_status" = "L'indice di utilizzo non è aggiornato · Aggiornato %@"; +"codex_workspaces_started" = "Avviata"; +"codex_workspaces_title" = "Spazi di lavoro"; +"codex_workspaces_token_count_accessibility" = "%@ token"; +"codex_workspaces_tokens" = "Token"; +"codex_workspaces_top_model" = "Modello principale"; +"codex_workspaces_top_sessions" = "Sessioni principali"; +"codex_workspaces_total" = "Totale"; +"codex_workspaces_view" = "Visualizza"; +"codex_workspaces_view_all_sessions" = "Visualizza tutte le sessioni"; +"codex_workspaces_window_title" = "Spazi di lavoro Codex"; +"codex_workspaces_workspace_count" = "Spazi di lavoro e chat: %d"; +"codex_workspaces_workspaces_chats" = "Spazi di lavoro/chat"; diff --git a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings index 7b6911423d..7e43c6ff00 100644 --- a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings @@ -1350,3 +1350,255 @@ "Cost today unavailable" = "今日のコスト: 利用不可"; "30-day cost unavailable" = "30日間のコスト: 利用不可"; "Resets" = "リセット"; + +/* Codex Workspaces and Models */ +"codex_workspaces_open_in_window" = "ウインドウで開く"; +"codex_models_actions" = "アクション"; +"codex_models_active_and_new_count" = "アクティブなモデル: %1$d;新しくアクティブになりました: %2$d"; +"codex_models_active_count_new_unavailable" = "アクティブなモデル: %1$d;新しくアクティブなカウントは利用できません"; +"codex_models_active_models" = "アクティブなモデル"; +"codex_models_alias_count" = "エイリアス: %1$d"; +"codex_models_all_model_count" = "すべてのモデル: %1$d"; +"codex_models_all_models" = "全モデル"; +"codex_models_all_rows" = "すべての行"; +"codex_models_analytics" = "モデル分析"; +"codex_models_analytics_controls" = "モデル分析コントロール"; +"codex_models_apply" = "申し込む"; +"codex_models_bucket_count" = "バケット (%2$@): %1$d"; +"codex_models_bucket_interval" = "バケット間隔"; +"codex_models_bullet_value" = "· %1$@"; +"codex_models_canonical_id" = "正規 ID"; +"codex_models_canonical_model" = "正規モデル"; +"codex_models_change" = "変更"; +"codex_models_change_from_peak" = "%2$@ ピークから %1$@"; +"codex_models_change_from_previous_period" = "前回の同期間からの %1$@"; +"codex_models_choose_metric" = "分析指標を選択してください"; +"codex_models_clear_search" = "検索をクリア"; +"codex_models_clear_selection" = "モデルの選択をクリア"; +"codex_models_columns" = "列"; +"codex_models_compact_billions_suffix" = "B"; +"codex_models_compact_millions_suffix" = "M"; +"codex_models_compact_thousands_suffix" = "K"; +"codex_models_contains_selected_suffix" = "、選択したモデルが含まれます"; +"codex_models_copy_aliases" = "エイリアスのコピー"; +"codex_models_copy_canonical_id" = "正規 ID をコピーします"; +"codex_models_copy_canonical_model_id" = "正規モデル ID をコピーします"; +"codex_models_copy_raw_aliases" = "生のエイリアスをコピーする"; +"codex_models_cost_conclusions_limited" = "コストの結論は %1$@ 価格のトークンに限定されます。"; +"codex_models_cost_hidden" = "非表示のコスト"; +"codex_models_cost_status_currency" = "%1$@ · %2$@ · %3$@"; +"codex_models_cost_unavailable" = "コストが利用できません"; +"codex_models_cost_unavailable_for_data" = "このデータにはコストがありません"; +"codex_models_coverage" = "適用範囲"; +"codex_models_currency" = "通貨"; +"codex_models_current_metric" = "現在 %1$@"; +"codex_models_current_period_in_progress" = "現在 %1$@ が進行中です"; +"codex_models_date" = "日付"; +"codex_models_day_lowercase" = "日"; +"codex_models_days" = "日"; +"codex_models_delta_vs_prior" = "Δ 対以前"; +"codex_models_disabled_in_settings" = "設定で無効になっています"; +"codex_models_dominates_at" = "このモデルは %2$@ で %1$@ を支配しています。"; +"codex_models_ended" = "終了"; +"codex_models_export" = "エクスポート"; +"codex_models_export_all_filename" = "codex-models-all.csv"; +"codex_models_export_default_filename" = "codex-models.csv"; +"codex_models_export_failed" = "モデル分析をエクスポートできませんでした"; +"codex_models_export_selected_filename" = "codex-model-%1$@.csv"; +"codex_models_export_selected_model" = "選択したモデルをエクスポートします"; +"codex_models_export_visible_filename" = "codex-models-visible.csv"; +"codex_models_export_visible_rows" = "表示されているモデル行をエクスポート"; +"codex_models_focus_table_search" = "フォーカスモデルテーブル検索"; +"codex_models_fully_priced" = "完全価格"; +"codex_models_fully_priced_percent" = "100% 価格"; +"codex_models_generated" = "%1$@ が生成されました"; +"codex_models_granularity_accessibility" = "粒度、%1$@、%2$@"; +"codex_models_hidden" = "非表示"; +"codex_models_identity_aliases_help" = "%1$@ · %2$@ · エイリアス: %3$@"; +"codex_models_identity_and_aliases" = "アイデンティティとエイリアス"; +"codex_models_indexing_last_days" = "過去 %1$d 日間のインデックスを作成しています…"; +"codex_models_interpretation" = "解釈"; +"codex_models_known_cost" = "既知のコスト"; +"codex_models_known_cost_fully_priced_accessibility" = "既知のコスト %1$@、完全価格"; +"codex_models_known_cost_lowercase" = "既知のコスト"; +"codex_models_known_cost_partial_accessibility" = "既知のコスト %1$@、部分的な、価格設定されたトークンの %2$@"; +"codex_models_last_days" = "過去 %1$d 日間"; +"codex_models_local_logs_source" = "· ローカル コーデックス ログ"; +"codex_models_major_contributor_at" = "このモデルは %1$@ に大きく貢献しています。"; +"codex_models_metric" = "メトリック"; +"codex_models_metric_over_time" = "%1$@ 時間の経過とともに"; +"codex_models_model" = "モデル"; +"codex_models_model_concentration" = "モデル濃度"; +"codex_models_model_count" = "モデル: %1$d"; +"codex_models_model_usage" = "モデルの使用法"; +"codex_models_month_lowercase" = "月"; +"codex_models_more_alias_count" = "その他のエイリアス: %1$d"; +"codex_models_name_canonical_id" = "%1$@、正規 ID %2$@"; +"codex_models_name_value_accessibility" = "%1$@、%2$@"; +"codex_models_named_metric_over_time" = "%1$@ %2$@ 時間の経過とともに"; +"codex_models_new" = "新規"; +"codex_models_new_in_period" = "この期間の新規"; +"codex_models_newly_active_count" = "新しくアクティブになりました: %1$d"; +"codex_models_no_applicable_price" = "該当する価格はありません"; +"codex_models_no_change_from_previous_period" = "前回の同期間からの変化なし"; +"codex_models_no_matching_models" = "一致するモデルがありません"; +"codex_models_no_newly_active" = "新しくアクティブなモデルはありません"; +"codex_models_no_top_model" = "最上位モデルはありません"; +"codex_models_no_usage" = "使用法はありません"; +"codex_models_no_usage_description" = "上記より長い日付範囲を選択するか、ローカル Codex セッションが保存された後に更新してください。"; +"codex_models_no_usage_in_period" = "この期間中は使用できません"; +"codex_models_no_usage_in_range" = "この範囲ではモデルが使用されていません"; +"codex_models_other_count" = "その他 (%1$d)"; +"codex_models_partial_priced" = "一部 · %1$@ 価格"; +"codex_models_precision_table" = "モデル精度テーブル"; +"codex_models_previous_known_cost" = "以前の既知のコスト"; +"codex_models_previous_metric" = "前 %1$@"; +"codex_models_previous_period_unavailable" = "前の期間は使用できません"; +"codex_models_previous_unavailable" = "以前は利用できません"; +"codex_models_priced_tokens" = "価格付きトークン"; +"codex_models_pricing_calculation" = "価格計算"; +"codex_models_pricing_coverage" = "価格範囲"; +"codex_models_pricing_explanation" = "%1$@ は %2$@ 価格のトークンをカバーします。該当するルールのない %3$@ トークンは除外され、ゼロとして推定されません。"; +"codex_models_ranking_by" = "%1$@ によるモデルのランキング"; +"codex_models_ranking_hint" = "モデル バーをクリックして詳細を開きます。"; +"codex_models_ranking_point_accessibility" = "%1$@、%2$@、%3$@ シェア"; +"codex_models_ranking_summary" = "正規モデルとその他は %1$@ によってランク付けされています。"; +"codex_models_ref_count" = "%1$@ 参照"; +"codex_models_refresh_analytics" = "モデル分析の更新"; +"codex_models_refresh_command_help" = "モデル分析を更新 (Command-R)"; +"codex_models_refresh_model_usage" = "リフレッシュモデルの使用法"; +"codex_models_refreshing_last_snapshot" = "更新中 · 最後に成功したスナップショットを表示しています"; +"codex_models_retry" = "再試行"; +"codex_models_reveal_pricing_calculation" = "価格計算を明らかにする"; +"codex_models_search_empty_description" = "検索では、このテーブルのみがフィルターされます。ダッシュボードの合計は変更されません。"; +"codex_models_search_models" = "モデルの検索"; +"codex_models_search_scope" = "%1$@ · このテーブルのみを検索フィルターします"; +"codex_models_search_table" = "モデル検索テーブル"; +"codex_models_select_model" = "%1$@ を選択してください"; +"codex_models_selected" = "選択済み"; +"codex_models_selected_bucket" = "選択されたバケット"; +"codex_models_selected_bucket_accessibility" = "選択されたバケット: %1$@、%2$@、%3$@ トークン、%4$@ セッション参照、%5$@、通貨 %6$@。"; +"codex_models_selected_clear_available" = "選択されたモデル %1$@、選択をクリアできます"; +"codex_models_selected_date" = "選択された日付"; +"codex_models_selected_hidden_by_search" = "選択したモデルは検索により非表示になりました"; +"codex_models_selected_model" = "選択されたモデル"; +"codex_models_selected_model_interval" = "選択されたモデル · %1$@"; +"codex_models_selected_suffix" = "、選択済み"; +"codex_models_session_reference_count" = "%1$@ セッション参照"; +"codex_models_session_references" = "セッション参照"; +"codex_models_session_references_lowercase" = "セッション参照"; +"codex_models_session_refs" = "セッション参照"; +"codex_models_share" = "シェアする"; +"codex_models_share_known_cost_help" = "既知のコストの割合。価格のない使用を除く"; +"codex_models_share_of" = "%1$@ のシェア"; +"codex_models_show_associated_sessions" = "関連するセッションを表示"; +"codex_models_show_details" = "詳細を表示"; +"codex_models_showing_last_snapshot" = "最後に成功したスナップショットを表示しています"; +"codex_models_sorted_by" = "%1$@ で並べ替え"; +"codex_models_subject_over_time" = "%1$@ 時間の経過とともに"; +"codex_models_summary" = "モデルの概要"; +"codex_models_timeline_accessibility" = "モデルのタイムライン、%1$@、%2$@"; +"codex_models_timeline_hint" = "タイム バケットをクリックするか、ポインタを 1.5 秒間そのバケットの上に押し続けると、その詳細が表示されます。"; +"codex_models_timeline_point_accessibility" = "%1$@、%2$@、%3$@ トークン、%4$@ セッション参照、%5$@、通貨 %6$@"; +"codex_models_timeline_summary" = "%1$@ %2$@ タイムライン。 %3$@"; +"codex_models_token_count" = "%1$@ トークン"; +"codex_models_tokens" = "トークン"; +"codex_models_tokens_lowercase" = "トークン"; +"codex_models_tokens_per_session_reference" = "セッション参照あたり約 %1$@ トークン。"; +"codex_models_top" = "トップへ"; +"codex_models_top_model" = "最上位モデル"; +"codex_models_top_seven_other" = "トップ 7 + その他"; +"codex_models_top_two_account_for" = "上位 2 つのモデルは %2$@ の %1$@ を占めます"; +"codex_models_top_two_of" = "%1$@ の上位 2 つのモデル"; +"codex_models_total_session_references_lowercase" = "合計セッション参照数"; +"codex_models_total_tokens" = "トークンの合計数"; +"codex_models_total_tokens_lowercase" = "合計トークン"; +"codex_models_totals_reconcile" = "· 合計はアクティブなスナップショットと一致します"; +"codex_models_track_end" = "トラックの終了"; +"codex_models_track_start" = "トラックの開始"; +"codex_models_trend_unavailable_observed_buckets" = "観察されたバケット: %1$d。トレンドは利用できません。"; +"codex_models_unknown_export_error" = "不明なエクスポート エラー"; +"codex_models_unpriced_tokens" = "価格のないトークン"; +"codex_models_updated" = "%1$@ を更新しました"; +"codex_models_usage_and_change" = "使用法と変更点"; +"codex_models_usage_by_model" = "モデル別の使用法"; +"codex_models_value_comparison_accessibility" = "%1$@、%2$@"; +"codex_models_value_detail" = "%1$@ · %2$@"; +"codex_models_value_share" = "%1$@ · %2$@ シェア"; +"codex_models_value_share_accessibility" = "%1$@、%2$@ シェア"; +"codex_models_value_start" = "値の開始"; +"codex_models_visible_of_total" = "表示可能なモデル: %1$d/%2$d"; +"codex_models_visible_rows" = "表示可能な行"; +"codex_models_vs_previous" = "%1$@ と以前の比較"; +"codex_models_week_lowercase" = "週"; +"codex_workspaces_all_models" = "すべてのモデル"; +"codex_workspaces_all_workspaces" = "すべてのワークスペース"; +"codex_workspaces_associated_with_selected_model" = "選択したモデルに関連付けられています"; +"codex_workspaces_bullet_value" = "· %1$@"; +"codex_workspaces_chart_keyboard_hint" = "左右の矢印キーまたは VoiceOver の調整アクションで日を選択します。Return またはスペースキーで固定します。"; +"codex_workspaces_chats_description" = "チャットと会話"; +"codex_workspaces_copy_path" = "パスをコピー"; +"codex_workspaces_cost_unavailable" = "コストを利用できません"; +"codex_workspaces_daily_callout_accessibility" = "%1$@、%2$@"; +"codex_workspaces_daily_point_accessibility" = "%1$@、トークン %2$@"; +"codex_workspaces_daily_point_help" = "%1$@ — %2$@ トークン"; +"codex_workspaces_daily_token_usage" = "日別のトークン使用量"; +"codex_workspaces_daily_unavailable" = "日別の使用状況を利用できません"; +"codex_workspaces_daily_unavailable_description" = "合計は %d セッションから計算されましたが、日付を特定できませんでした。"; +"codex_workspaces_date" = "日付"; +"codex_workspaces_days" = "日"; +"codex_workspaces_elevated_usage" = "使用量がやや多い"; +"codex_workspaces_elevated_usage_help" = "選択した期間の中央値を上回っています。"; +"codex_workspaces_estimated_cost_short" = "推定コスト"; +"codex_workspaces_estimated_cost_value" = "推定 %@"; +"codex_workspaces_estimated_local_logs" = "ローカルの Codex ログから推定"; +"codex_workspaces_failed_read_logs" = "Codex ログを読み込めませんでした"; +"codex_workspaces_failed_some_logs" = "一部の Codex ログを読み込めませんでした"; +"codex_workspaces_finalizing_project_index_files" = "プロジェクトインデックスを完了中… %d ファイル"; +"codex_workspaces_high_usage" = "使用量が多い"; +"codex_workspaces_high_usage_help" = "選択した期間では使用量が大きく突出しています。"; +"codex_workspaces_indexed_files" = "%@前に更新 · %d ファイルをインデックス作成"; +"codex_workspaces_indexing_local_logs" = "ローカル Codex ログをインデックス作成中…"; +"codex_workspaces_indexing_projects" = "プロジェクトをインデックス作成中…"; +"codex_workspaces_indexing_projects_files_left" = "プロジェクトをインデックス作成中… %d/%d ファイル、残り %d"; +"codex_workspaces_last_days" = "過去 %d 日"; +"codex_workspaces_local_codex_chat" = "ローカル Codex チャット"; +"codex_workspaces_model" = "モデル"; +"codex_workspaces_models" = "モデル"; +"codex_workspaces_no_daily_usage" = "この履歴期間には日別の使用状況がありません。"; +"codex_workspaces_no_local_usage" = "ローカルの Codex 使用状況はまだありません"; +"codex_workspaces_no_models" = "モデル別の内訳を利用できません"; +"codex_workspaces_no_projects_or_chats" = "この履歴期間にはプロジェクトまたはチャットが見つかりませんでした。"; +"codex_workspaces_no_sessions" = "このワークスペースにはセッションがありません"; +"codex_workspaces_normal_usage" = "通常の使用量"; +"codex_workspaces_not_available" = "利用できません"; +"codex_workspaces_overview" = "概要"; +"codex_workspaces_partial_breakdown_help" = "概要が正しい合計です。利用できない日付またはモデルのメタデータは一部として表示されます。"; +"codex_workspaces_partial_breakdown_notice" = "一部の詳細な内訳は合計全体を網羅していません。"; +"codex_workspaces_partial_data" = "一部のデータ"; +"codex_workspaces_percent_total" = "合計に対する割合"; +"codex_workspaces_refresh_usage_data" = "使用状況データを更新"; +"codex_workspaces_relative_usage_accessibility" = "最大のワークスペース使用量の %d パーセント"; +"codex_workspaces_relative_usage_tooltip" = "この期間で最も使用量が多いワークスペースの %d%% のトークンを使用しています。"; +"codex_workspaces_reveal_in_finder" = "Finder で表示"; +"codex_workspaces_saving_project_index" = "プロジェクトインデックスを保存中…"; +"codex_workspaces_scanning_logs" = "Codex ログをスキャン中…"; +"codex_workspaces_search_sessions" = "セッションを検索"; +"codex_workspaces_selected_day_accessibility" = "%@:%@ トークン"; +"codex_workspaces_session" = "セッション"; +"codex_workspaces_session_count" = "セッション:%d"; +"codex_workspaces_sessions" = "セッション"; +"codex_workspaces_showing_last_complete_index" = "最後に完了したローカルインデックスを表示中"; +"codex_workspaces_stale_status" = "使用状況インデックスが古くなっています · %@前に更新"; +"codex_workspaces_started" = "開始日時"; +"codex_workspaces_title" = "ワークスペース"; +"codex_workspaces_token_count_accessibility" = "%@ トークン"; +"codex_workspaces_tokens" = "トークン"; +"codex_workspaces_top_model" = "上位のモデル"; +"codex_workspaces_top_sessions" = "上位のセッション"; +"codex_workspaces_total" = "合計"; +"codex_workspaces_view" = "表示"; +"codex_workspaces_view_all_sessions" = "すべてのセッションを表示"; +"codex_workspaces_window_title" = "Codex ワークスペース"; +"codex_workspaces_workspace_count" = "ワークスペースとチャット:%d"; +"codex_workspaces_workspaces_chats" = "ワークスペース/チャット"; diff --git a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings index 7458d95078..ef125db5fd 100644 --- a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings @@ -1317,3 +1317,255 @@ "Cost today unavailable" = "오늘 비용: 사용할 수 없음"; "30-day cost unavailable" = "30일 비용: 사용할 수 없음"; "Resets" = "재설정"; + +/* Codex Workspaces and Models */ +"codex_workspaces_open_in_window" = "윈도우에서 열기"; +"codex_models_actions" = "작업"; +"codex_models_active_and_new_count" = "활성 모델: %1$d; 새로 활성화됨: %2$d"; +"codex_models_active_count_new_unavailable" = "활성 모델: %1$d; 새로 활성화된 개수를 확인할 수 없습니다."; +"codex_models_active_models" = "활성 모델"; +"codex_models_alias_count" = "별칭: %1$d"; +"codex_models_all_model_count" = "모든 모델: %1$d"; +"codex_models_all_models" = "모든 모델"; +"codex_models_all_rows" = "모든 행"; +"codex_models_analytics" = "모델 분석"; +"codex_models_analytics_controls" = "모델 분석 제어"; +"codex_models_apply" = "적용"; +"codex_models_bucket_count" = "버킷(%2$@): %1$d"; +"codex_models_bucket_interval" = "버킷 간격"; +"codex_models_bullet_value" = "· %1$@"; +"codex_models_canonical_id" = "정식 ID"; +"codex_models_canonical_model" = "정식 모델"; +"codex_models_change" = "변경"; +"codex_models_change_from_peak" = "%2$@ 피크에서 %1$@"; +"codex_models_change_from_previous_period" = "이전 동일 기간의 %1$@"; +"codex_models_choose_metric" = "분석 지표를 선택하세요"; +"codex_models_clear_search" = "검색 지우기"; +"codex_models_clear_selection" = "모델 선택 지우기"; +"codex_models_columns" = "열"; +"codex_models_compact_billions_suffix" = "B"; +"codex_models_compact_millions_suffix" = "M"; +"codex_models_compact_thousands_suffix" = "K"; +"codex_models_contains_selected_suffix" = ", 선택한 모델 포함"; +"codex_models_copy_aliases" = "별칭 복사"; +"codex_models_copy_canonical_id" = "정식 ID 복사"; +"codex_models_copy_canonical_model_id" = "표준 모델 ID 복사"; +"codex_models_copy_raw_aliases" = "원시 별칭 복사"; +"codex_models_cost_conclusions_limited" = "비용 결론은 %1$@ 가격의 토큰으로 제한됩니다."; +"codex_models_cost_hidden" = "숨겨진 비용"; +"codex_models_cost_status_currency" = "%1$@ · %2$@ · %3$@"; +"codex_models_cost_unavailable" = "비용을 알 수 없음"; +"codex_models_cost_unavailable_for_data" = "이 데이터에는 비용을 사용할 수 없습니다."; +"codex_models_coverage" = "적용 범위"; +"codex_models_currency" = "통화"; +"codex_models_current_metric" = "현재 %1$@"; +"codex_models_current_period_in_progress" = "현재 %1$@ 진행 중"; +"codex_models_date" = "날짜"; +"codex_models_day_lowercase" = "일"; +"codex_models_days" = "일"; +"codex_models_delta_vs_prior" = "Δ 이전 대비"; +"codex_models_disabled_in_settings" = "설정에서 비활성화되었습니다."; +"codex_models_dominates_at" = "이 모델은 %2$@에서 %1$@을(를) 지배하고 있습니다."; +"codex_models_ended" = "종료됨"; +"codex_models_export" = "수출"; +"codex_models_export_all_filename" = "codex-models-all.csv"; +"codex_models_export_default_filename" = "codex-models.csv"; +"codex_models_export_failed" = "모델 분석을 내보낼 수 없습니다."; +"codex_models_export_selected_filename" = "codex-model-%1$@.csv"; +"codex_models_export_selected_model" = "선택한 모델 내보내기"; +"codex_models_export_visible_filename" = "codex-models-visible.csv"; +"codex_models_export_visible_rows" = "보이는 모델 행 내보내기"; +"codex_models_focus_table_search" = "Focus Models 테이블 검색"; +"codex_models_fully_priced" = "정가"; +"codex_models_fully_priced_percent" = "100% 가격"; +"codex_models_generated" = "생성됨 %1$@"; +"codex_models_granularity_accessibility" = "세분성, %1$@, %2$@"; +"codex_models_hidden" = "숨겨진"; +"codex_models_identity_aliases_help" = "%1$@ · %2$@ · 별칭: %3$@"; +"codex_models_identity_and_aliases" = "ID 및 별칭"; +"codex_models_indexing_last_days" = "지난 %1$d일 동안 색인을 생성했습니다…"; +"codex_models_interpretation" = "해석"; +"codex_models_known_cost" = "알려진 비용"; +"codex_models_known_cost_fully_priced_accessibility" = "알려진 비용 %1$@, 정가"; +"codex_models_known_cost_lowercase" = "알려진 비용"; +"codex_models_known_cost_partial_accessibility" = "알려진 비용 %1$@, 부분, 가격이 책정된 토큰의 %2$@"; +"codex_models_last_days" = "지난 %1$d일"; +"codex_models_local_logs_source" = "· 지역 코덱스 로그"; +"codex_models_major_contributor_at" = "이 모델은 %1$@의 주요 기여자입니다."; +"codex_models_metric" = "미터법"; +"codex_models_metric_over_time" = "시간이 지남에 따라 %1$@"; +"codex_models_model" = "모델"; +"codex_models_model_concentration" = "모델 집중"; +"codex_models_model_count" = "모델: %1$d"; +"codex_models_model_usage" = "모델 사용법"; +"codex_models_month_lowercase" = "월"; +"codex_models_more_alias_count" = "추가 별칭: %1$d"; +"codex_models_name_canonical_id" = "%1$@, 정식 ID %2$@"; +"codex_models_name_value_accessibility" = "%1$@, %2$@"; +"codex_models_named_metric_over_time" = "시간이 지남에 따라 %1$@ %2$@"; +"codex_models_new" = "새로운"; +"codex_models_new_in_period" = "이 기간의 새로운 기능"; +"codex_models_newly_active_count" = "새로 활성화됨: %1$d"; +"codex_models_no_applicable_price" = "해당 가격 없음"; +"codex_models_no_change_from_previous_period" = "이전 균등 기간과 변동 없음"; +"codex_models_no_matching_models" = "일치하는 모델이 없습니다."; +"codex_models_no_newly_active" = "새로 활성화된 모델이 없습니다."; +"codex_models_no_top_model" = "상위 모델 없음"; +"codex_models_no_usage" = "사용하지 않음"; +"codex_models_no_usage_description" = "위에서 더 긴 날짜 범위를 선택하거나 로컬 Codex 세션이 저장된 후 새로 고침하세요."; +"codex_models_no_usage_in_period" = "이 기간에는 사용량이 없습니다."; +"codex_models_no_usage_in_range" = "이 범위에는 모델 사용이 없습니다."; +"codex_models_other_count" = "기타(%1$d)"; +"codex_models_partial_priced" = "부분 · %1$@ 가격 책정"; +"codex_models_precision_table" = "모델 정밀도 테이블"; +"codex_models_previous_known_cost" = "이전에 알려진 비용"; +"codex_models_previous_metric" = "이전 %1$@"; +"codex_models_previous_period_unavailable" = "이전 기간을 이용할 수 없습니다."; +"codex_models_previous_unavailable" = "이전에는 사용할 수 없습니다."; +"codex_models_priced_tokens" = "가격이 책정된 토큰"; +"codex_models_pricing_calculation" = "가격 계산"; +"codex_models_pricing_coverage" = "가격 범위"; +"codex_models_pricing_explanation" = "%1$@은 %2$@ 가격의 토큰을 포함합니다. 적용 가능한 규칙이 없는 %3$@ 토큰은 제외되며 0으로 추정되지 않습니다."; +"codex_models_ranking_by" = "%1$@의 모델 순위"; +"codex_models_ranking_hint" = "모델 표시줄을 클릭하여 세부정보를 엽니다."; +"codex_models_ranking_point_accessibility" = "%1$@, %2$@, %3$@ 공유"; +"codex_models_ranking_summary" = "%1$@에 의해 순위가 매겨진 표준 모델 및 기타."; +"codex_models_ref_count" = "%1$@ 심판"; +"codex_models_refresh_analytics" = "모델 분석 새로 고침"; +"codex_models_refresh_command_help" = "모델 분석 새로 고침(Command-R)"; +"codex_models_refresh_model_usage" = "모델 사용 새로 고침"; +"codex_models_refreshing_last_snapshot" = "새로 고치는 중 · 마지막으로 성공한 스냅샷 표시"; +"codex_models_retry" = "재시도"; +"codex_models_reveal_pricing_calculation" = "가격 계산 공개"; +"codex_models_search_empty_description" = "검색은 이 테이블만 필터링합니다. 대시보드 합계는 변경되지 않습니다."; +"codex_models_search_models" = "모델 검색"; +"codex_models_search_scope" = "%1$@ · 검색은 이 테이블만 필터링합니다."; +"codex_models_search_table" = "검색 모델 테이블"; +"codex_models_select_model" = "%1$@ 선택"; +"codex_models_selected" = "선택됨"; +"codex_models_selected_bucket" = "선택한 버킷"; +"codex_models_selected_bucket_accessibility" = "선택한 버킷: %1$@, %2$@, %3$@ 토큰, %4$@ 세션 참조, %5$@, 통화 %6$@."; +"codex_models_selected_clear_available" = "선택한 모델 %1$@, 선택 취소 가능"; +"codex_models_selected_date" = "선택한 날짜"; +"codex_models_selected_hidden_by_search" = "선택한 모델이 검색에 의해 숨겨졌습니다."; +"codex_models_selected_model" = "선택한 모델"; +"codex_models_selected_model_interval" = "선택한 모델 · %1$@"; +"codex_models_selected_suffix" = ", 선택됨"; +"codex_models_session_reference_count" = "%1$@ 세션 참조"; +"codex_models_session_references" = "세션 참조"; +"codex_models_session_references_lowercase" = "세션 참조"; +"codex_models_session_refs" = "세션 참조"; +"codex_models_share" = "공유"; +"codex_models_share_known_cost_help" = "알려진 비용의 비율; 가격이 책정되지 않은 사용량은 제외됩니다."; +"codex_models_share_of" = "%1$@의 지분"; +"codex_models_show_associated_sessions" = "관련 세션 표시"; +"codex_models_show_details" = "세부정보 표시"; +"codex_models_showing_last_snapshot" = "마지막으로 성공한 스냅샷 표시 중"; +"codex_models_sorted_by" = "%1$@으로 정렬됨"; +"codex_models_subject_over_time" = "시간이 지남에 따라 %1$@"; +"codex_models_summary" = "모델 요약"; +"codex_models_timeline_accessibility" = "모델 타임라인, %1$@, %2$@"; +"codex_models_timeline_hint" = "시간 버킷을 클릭하거나 해당 버킷 위에 포인터를 1.5초 동안 유지하여 세부 정보를 검사하세요."; +"codex_models_timeline_point_accessibility" = "%1$@, %2$@, %3$@ 토큰, %4$@ 세션 참조, %5$@, 통화 %6$@"; +"codex_models_timeline_summary" = "%1$@ %2$@ 타임라인. %3$@"; +"codex_models_token_count" = "%1$@ 토큰"; +"codex_models_tokens" = "토큰"; +"codex_models_tokens_lowercase" = "토큰"; +"codex_models_tokens_per_session_reference" = "세션당 약 %1$@ 토큰 참조입니다."; +"codex_models_top" = "상단"; +"codex_models_top_model" = "상위 모델"; +"codex_models_top_seven_other" = "상위 7개 + 기타"; +"codex_models_top_two_account_for" = "상위 2개 모델이 %2$@ 중 %1$@을 차지합니다."; +"codex_models_top_two_of" = "%1$@의 상위 2개 모델"; +"codex_models_total_session_references_lowercase" = "총 세션 참조"; +"codex_models_total_tokens" = "총 토큰"; +"codex_models_total_tokens_lowercase" = "총 토큰"; +"codex_models_totals_reconcile" = "· 총계는 활성 스냅샷에 맞춰 조정됩니다."; +"codex_models_track_end" = "트랙 끝"; +"codex_models_track_start" = "트랙 시작"; +"codex_models_trend_unavailable_observed_buckets" = "관찰된 버킷: %1$d. 추세를 확인할 수 없습니다."; +"codex_models_unknown_export_error" = "알 수 없는 내보내기 오류"; +"codex_models_unpriced_tokens" = "가격이 책정되지 않은 토큰"; +"codex_models_updated" = "%1$@ 업데이트됨"; +"codex_models_usage_and_change" = "사용법 및 변경"; +"codex_models_usage_by_model" = "모델별 사용법"; +"codex_models_value_comparison_accessibility" = "%1$@, %2$@"; +"codex_models_value_detail" = "%1$@ · %2$@"; +"codex_models_value_share" = "%1$@ · %2$@ 공유"; +"codex_models_value_share_accessibility" = "%1$@, %2$@ 공유"; +"codex_models_value_start" = "가치 시작"; +"codex_models_visible_of_total" = "표시되는 모델: %2$d 중 %1$d"; +"codex_models_visible_rows" = "보이는 행"; +"codex_models_vs_previous" = "%1$@ 이전 대비"; +"codex_models_week_lowercase" = "주"; +"codex_workspaces_all_models" = "모든 모델"; +"codex_workspaces_all_workspaces" = "모든 작업 공간"; +"codex_workspaces_associated_with_selected_model" = "선택한 모델과 연결됨"; +"codex_workspaces_bullet_value" = "· %1$@"; +"codex_workspaces_chart_keyboard_hint" = "왼쪽 및 오른쪽 화살표 키나 VoiceOver 조절 동작으로 날짜를 선택하십시오. Return 또는 스페이스를 눌러 고정하십시오."; +"codex_workspaces_chats_description" = "채팅 및 대화"; +"codex_workspaces_copy_path" = "경로 복사"; +"codex_workspaces_cost_unavailable" = "비용을 사용할 수 없음"; +"codex_workspaces_daily_callout_accessibility" = "%1$@, %2$@"; +"codex_workspaces_daily_point_accessibility" = "%1$@, 토큰 %2$@"; +"codex_workspaces_daily_point_help" = "%1$@ — %2$@ 토큰"; +"codex_workspaces_daily_token_usage" = "일별 토큰 사용량"; +"codex_workspaces_daily_unavailable" = "일별 사용량을 사용할 수 없음"; +"codex_workspaces_daily_unavailable_description" = "총계는 %d개 세션에서 계산되었지만 날짜를 확인할 수 없습니다."; +"codex_workspaces_date" = "날짜"; +"codex_workspaces_days" = "일"; +"codex_workspaces_elevated_usage" = "증가한 사용량"; +"codex_workspaces_elevated_usage_help" = "선택한 기간의 중앙값보다 사용량이 많습니다."; +"codex_workspaces_estimated_cost_short" = "예상 비용"; +"codex_workspaces_estimated_cost_value" = "예상 %@"; +"codex_workspaces_estimated_local_logs" = "로컬 Codex 로그에서 추정"; +"codex_workspaces_failed_read_logs" = "Codex 로그를 읽지 못했습니다"; +"codex_workspaces_failed_some_logs" = "일부 Codex 로그를 읽지 못했습니다"; +"codex_workspaces_finalizing_project_index_files" = "프로젝트 인덱스 마무리 중… 파일 %d개"; +"codex_workspaces_high_usage" = "높은 사용량"; +"codex_workspaces_high_usage_help" = "선택한 기간에 사용량이 크게 두드러집니다."; +"codex_workspaces_indexed_files" = "%@ 전에 업데이트됨 · 파일 %d개 인덱싱됨"; +"codex_workspaces_indexing_local_logs" = "로컬 Codex 로그 인덱싱 중…"; +"codex_workspaces_indexing_projects" = "프로젝트 인덱싱 중…"; +"codex_workspaces_indexing_projects_files_left" = "프로젝트 인덱싱 중… 파일 %d/%d개, %d개 남음"; +"codex_workspaces_last_days" = "최근 %d일"; +"codex_workspaces_local_codex_chat" = "로컬 Codex 채팅"; +"codex_workspaces_model" = "모델"; +"codex_workspaces_models" = "모델"; +"codex_workspaces_no_daily_usage" = "이 기록 기간에는 일별 사용량이 없습니다."; +"codex_workspaces_no_local_usage" = "아직 로컬 Codex 사용 내역이 없습니다"; +"codex_workspaces_no_models" = "모델 분석을 사용할 수 없음"; +"codex_workspaces_no_projects_or_chats" = "이 기록 기간에 프로젝트나 채팅이 없습니다."; +"codex_workspaces_no_sessions" = "이 작업 공간에 세션이 없습니다"; +"codex_workspaces_normal_usage" = "정상 사용량"; +"codex_workspaces_not_available" = "사용할 수 없음"; +"codex_workspaces_overview" = "개요"; +"codex_workspaces_partial_breakdown_help" = "요약이 기준이며 사용할 수 없는 날짜 또는 모델 메타데이터는 일부로 표시됩니다."; +"codex_workspaces_partial_breakdown_notice" = "일부 상세 분석은 전체 합계를 포함하지 않습니다."; +"codex_workspaces_partial_data" = "일부 데이터"; +"codex_workspaces_percent_total" = "전체 대비 비율"; +"codex_workspaces_refresh_usage_data" = "사용량 데이터 새로 고침"; +"codex_workspaces_relative_usage_accessibility" = "가장 높은 작업 공간 사용량의 %d퍼센트"; +"codex_workspaces_relative_usage_tooltip" = "이 기간에 사용량이 가장 높은 작업 공간 토큰의 %d%%를 사용합니다."; +"codex_workspaces_reveal_in_finder" = "Finder에서 보기"; +"codex_workspaces_saving_project_index" = "프로젝트 인덱스 저장 중…"; +"codex_workspaces_scanning_logs" = "Codex 로그 스캔 중…"; +"codex_workspaces_search_sessions" = "세션 검색"; +"codex_workspaces_selected_day_accessibility" = "%@: %@개 토큰"; +"codex_workspaces_session" = "세션"; +"codex_workspaces_session_count" = "세션: %d"; +"codex_workspaces_sessions" = "세션"; +"codex_workspaces_showing_last_complete_index" = "마지막으로 완료된 로컬 인덱스 표시 중"; +"codex_workspaces_stale_status" = "사용량 인덱스가 오래되었습니다 · %@ 전에 업데이트됨"; +"codex_workspaces_started" = "시작됨"; +"codex_workspaces_title" = "작업 공간"; +"codex_workspaces_token_count_accessibility" = "%@개 토큰"; +"codex_workspaces_tokens" = "토큰"; +"codex_workspaces_top_model" = "상위 모델"; +"codex_workspaces_top_sessions" = "상위 세션"; +"codex_workspaces_total" = "합계"; +"codex_workspaces_view" = "보기"; +"codex_workspaces_view_all_sessions" = "모든 세션 보기"; +"codex_workspaces_window_title" = "Codex 작업 공간"; +"codex_workspaces_workspace_count" = "작업 공간 및 채팅: %d"; +"codex_workspaces_workspaces_chats" = "작업 공간/채팅"; diff --git a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings index cbe4af8954..9cf82165f6 100644 --- a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings @@ -1349,3 +1349,255 @@ "Cost today unavailable" = "Kosten vandaag: Niet beschikbaar"; "30-day cost unavailable" = "Kosten 30 dagen: Niet beschikbaar"; "Resets" = "Resets"; + +/* Codex Workspaces and Models */ +"codex_workspaces_open_in_window" = "Openen in venster"; +"codex_models_actions" = "Acties"; +"codex_models_active_and_new_count" = "Actieve modellen: %1$d; nieuw actief: %2$d"; +"codex_models_active_count_new_unavailable" = "Actieve modellen: %1$d; nieuw actieve telling niet beschikbaar"; +"codex_models_active_models" = "Actieve modellen"; +"codex_models_alias_count" = "Aliassen: %1$d"; +"codex_models_all_model_count" = "Alle modellen: %1$d"; +"codex_models_all_models" = "Alle modellen"; +"codex_models_all_rows" = "Alle rijen"; +"codex_models_analytics" = "Modelanalyses"; +"codex_models_analytics_controls" = "Modelleert analytische besturingselementen"; +"codex_models_apply" = "Toepassen"; +"codex_models_bucket_count" = "Buckets (%2$@): %1$d"; +"codex_models_bucket_interval" = "Emmerinterval"; +"codex_models_bullet_value" = "· %1$@"; +"codex_models_canonical_id" = "Canonieke ID"; +"codex_models_canonical_model" = "Canoniek model"; +"codex_models_change" = "Wijziging"; +"codex_models_change_from_peak" = "%1$@ vanaf %2$@ piek"; +"codex_models_change_from_previous_period" = "%1$@ van de vorige gelijke periode"; +"codex_models_choose_metric" = "Kies de analysestatistiek"; +"codex_models_clear_search" = "Zoekopdracht wissen"; +"codex_models_clear_selection" = "Modelselectie wissen"; +"codex_models_columns" = "Kolommen"; +"codex_models_compact_billions_suffix" = "B"; +"codex_models_compact_millions_suffix" = "M"; +"codex_models_compact_thousands_suffix" = "K"; +"codex_models_contains_selected_suffix" = ", bevat geselecteerd model"; +"codex_models_copy_aliases" = "Kopieer aliassen"; +"codex_models_copy_canonical_id" = "Kopieer canonieke ID"; +"codex_models_copy_canonical_model_id" = "Kopieer canonieke model-ID"; +"codex_models_copy_raw_aliases" = "Kopieer onbewerkte aliassen"; +"codex_models_cost_conclusions_limited" = "Kostenconclusies zijn beperkt tot tokens met een prijs van %1$@."; +"codex_models_cost_hidden" = "Kosten verborgen"; +"codex_models_cost_status_currency" = "%1$@ · %2$@ · %3$@"; +"codex_models_cost_unavailable" = "Kosten niet beschikbaar"; +"codex_models_cost_unavailable_for_data" = "Er zijn geen kosten beschikbaar voor deze gegevens"; +"codex_models_coverage" = "Dekking"; +"codex_models_currency" = "Valuta"; +"codex_models_current_metric" = "Huidig %1$@"; +"codex_models_current_period_in_progress" = "Huidige %1$@ in uitvoering"; +"codex_models_date" = "Datum"; +"codex_models_day_lowercase" = "dag"; +"codex_models_days" = "Dagen"; +"codex_models_delta_vs_prior" = "Δ vs. eerder"; +"codex_models_disabled_in_settings" = "Uitgeschakeld in instellingen"; +"codex_models_dominates_at" = "Dit model domineert %1$@ op %2$@."; +"codex_models_ended" = "Beëindigd"; +"codex_models_export" = "Exporteren"; +"codex_models_export_all_filename" = "codex-models-all.csv"; +"codex_models_export_default_filename" = "codex-models.csv"; +"codex_models_export_failed" = "Kan modelanalyses niet exporteren"; +"codex_models_export_selected_filename" = "codex-model-%1$@.csv"; +"codex_models_export_selected_model" = "Geselecteerd model exporteren"; +"codex_models_export_visible_filename" = "codex-models-visible.csv"; +"codex_models_export_visible_rows" = "Zichtbare modellenrijen exporteren"; +"codex_models_focus_table_search" = "Focus Modellen tabel zoeken"; +"codex_models_fully_priced" = "Volledig geprijsd"; +"codex_models_fully_priced_percent" = "100% geprijsd"; +"codex_models_generated" = "Gegenereerd %1$@"; +"codex_models_granularity_accessibility" = "Granulariteit, %1$@, %2$@"; +"codex_models_hidden" = "Verborgen"; +"codex_models_identity_aliases_help" = "%1$@ · %2$@ · Aliassen: %3$@"; +"codex_models_identity_and_aliases" = "Identiteit en aliassen"; +"codex_models_indexing_last_days" = "Indexering afgelopen %1$d dagen…"; +"codex_models_interpretation" = "Interpretatie"; +"codex_models_known_cost" = "Bekende kosten"; +"codex_models_known_cost_fully_priced_accessibility" = "Bekende kosten %1$@, volledige prijs"; +"codex_models_known_cost_lowercase" = "bekende kosten"; +"codex_models_known_cost_partial_accessibility" = "Bekende kosten %1$@, gedeeltelijk, %2$@ van de tokens geprijsd"; +"codex_models_last_days" = "Afgelopen %1$d dagen"; +"codex_models_local_logs_source" = "· Lokale Codex-logboeken"; +"codex_models_major_contributor_at" = "Dit model levert een belangrijke bijdrage aan %1$@."; +"codex_models_metric" = "Metrisch"; +"codex_models_metric_over_time" = "%1$@ in de loop van de tijd"; +"codex_models_model" = "Model"; +"codex_models_model_concentration" = "Modelconcentratie"; +"codex_models_model_count" = "Modellen: %1$d"; +"codex_models_model_usage" = "Modelgebruik"; +"codex_models_month_lowercase" = "maand"; +"codex_models_more_alias_count" = "Meer aliassen: %1$d"; +"codex_models_name_canonical_id" = "%1$@, canonieke ID %2$@"; +"codex_models_name_value_accessibility" = "%1$@, %2$@"; +"codex_models_named_metric_over_time" = "%1$@ %2$@ in de loop van de tijd"; +"codex_models_new" = "Nieuw"; +"codex_models_new_in_period" = "Nieuw in deze periode"; +"codex_models_newly_active_count" = "Nieuw actief: %1$d"; +"codex_models_no_applicable_price" = "Geen prijs van toepassing"; +"codex_models_no_change_from_previous_period" = "Geen verandering ten opzichte van de vorige gelijke periode"; +"codex_models_no_matching_models" = "Geen bijpassende modellen"; +"codex_models_no_newly_active" = "Geen nieuwe actieve modellen"; +"codex_models_no_top_model" = "Geen topmodel"; +"codex_models_no_usage" = "Geen gebruik"; +"codex_models_no_usage_description" = "Kies hierboven een langer datumbereik of vernieuw nadat lokale Codex-sessies zijn opgeslagen."; +"codex_models_no_usage_in_period" = "Geen verbruik in deze periode"; +"codex_models_no_usage_in_range" = "Geen modelgebruik in dit bereik"; +"codex_models_other_count" = "Anders (%1$d)"; +"codex_models_partial_priced" = "Gedeeltelijk · %1$@ geprijsd"; +"codex_models_precision_table" = "Model precisietafel"; +"codex_models_previous_known_cost" = "Eerder bekende kosten"; +"codex_models_previous_metric" = "Vorige %1$@"; +"codex_models_previous_period_unavailable" = "Vorige periode is niet beschikbaar"; +"codex_models_previous_unavailable" = "Vorige niet beschikbaar"; +"codex_models_priced_tokens" = "Geprijsde tokens"; +"codex_models_pricing_calculation" = "Prijsberekening"; +"codex_models_pricing_coverage" = "Prijsdekking"; +"codex_models_pricing_explanation" = "%1$@ dekt tokens met een prijs van %2$@. %3$@ tokens zonder toepasselijke regel worden uitgesloten en niet als nul geschat."; +"codex_models_ranking_by" = "Modelrangschikking op %1$@"; +"codex_models_ranking_hint" = "Klik op een modelbalk om de details ervan te openen."; +"codex_models_ranking_point_accessibility" = "%1$@, %2$@, %3$@ delen"; +"codex_models_ranking_summary" = "Canonieke modellen en overige gerangschikt op %1$@."; +"codex_models_ref_count" = "%1$@ ref"; +"codex_models_refresh_analytics" = "Modelanalyse vernieuwen"; +"codex_models_refresh_command_help" = "Modellenanalyse vernieuwen (Command-R)"; +"codex_models_refresh_model_usage" = "Modelgebruik vernieuwen"; +"codex_models_refreshing_last_snapshot" = "Vernieuwend · toont de laatste succesvolle momentopname"; +"codex_models_retry" = "Opnieuw proberen"; +"codex_models_reveal_pricing_calculation" = "Prijsberekening weergeven"; +"codex_models_search_empty_description" = "Zoeken filtert alleen deze tabel; dashboardtotalen zijn ongewijzigd."; +"codex_models_search_models" = "Zoek modellen"; +"codex_models_search_scope" = "%1$@ · Zoeken filtert alleen deze tabel"; +"codex_models_search_table" = "Zoekmodellentabel"; +"codex_models_select_model" = "Selecteer %1$@"; +"codex_models_selected" = "Geselecteerd"; +"codex_models_selected_bucket" = "Geselecteerde bak"; +"codex_models_selected_bucket_accessibility" = "Geselecteerde bucket: %1$@, %2$@, %3$@ tokens, %4$@ sessiereferenties, %5$@, valuta %6$@."; +"codex_models_selected_clear_available" = "Geselecteerd model %1$@, Duidelijke selectie beschikbaar"; +"codex_models_selected_date" = "Geselecteerde datum"; +"codex_models_selected_hidden_by_search" = "Geselecteerd model verborgen door zoeken"; +"codex_models_selected_model" = "Geselecteerd model"; +"codex_models_selected_model_interval" = "Geselecteerd model · %1$@"; +"codex_models_selected_suffix" = ", geselecteerd"; +"codex_models_session_reference_count" = "%1$@ sessiereferenties"; +"codex_models_session_references" = "Sessiereferenties"; +"codex_models_session_references_lowercase" = "sessiereferenties"; +"codex_models_session_refs" = "Sessiereferenties"; +"codex_models_share" = "Delen"; +"codex_models_share_known_cost_help" = "Aandeel van bekende kosten; sluit ongeprijsd gebruik uit"; +"codex_models_share_of" = "Aandeel van %1$@"; +"codex_models_show_associated_sessions" = "Toon gekoppelde sessies"; +"codex_models_show_details" = "Details weergeven"; +"codex_models_showing_last_snapshot" = "Toont de laatste succesvolle momentopname"; +"codex_models_sorted_by" = "Gesorteerd op %1$@"; +"codex_models_subject_over_time" = "%1$@ in de loop van de tijd"; +"codex_models_summary" = "Modeloverzicht"; +"codex_models_timeline_accessibility" = "Modellen tijdlijn, %1$@, %2$@"; +"codex_models_timeline_hint" = "Klik op een tijdsinterval, of houd de aanwijzer er 1,5 seconde boven, om de details ervan te bekijken."; +"codex_models_timeline_point_accessibility" = "%1$@, %2$@, %3$@ tokens, %4$@ sessiereferenties, %5$@, valuta %6$@"; +"codex_models_timeline_summary" = "%1$@ %2$@ tijdlijn. %3$@"; +"codex_models_token_count" = "%1$@ tokens"; +"codex_models_tokens" = "Tokens"; +"codex_models_tokens_lowercase" = "tokens"; +"codex_models_tokens_per_session_reference" = "Ongeveer %1$@ tokens per sessiereferentie."; +"codex_models_top" = "Boven"; +"codex_models_top_model" = "Topmodel"; +"codex_models_top_seven_other" = "Top 7 + Overige"; +"codex_models_top_two_account_for" = "De twee beste modellen zijn goed voor %1$@ van %2$@"; +"codex_models_top_two_of" = "Top twee modellen van %1$@"; +"codex_models_total_session_references_lowercase" = "totaal aantal sessiereferenties"; +"codex_models_total_tokens" = "Totaal aantal tokens"; +"codex_models_total_tokens_lowercase" = "totaal tokens"; +"codex_models_totals_reconcile" = "· Totalen komen overeen met de actieve momentopname"; +"codex_models_track_end" = "Baaneinde"; +"codex_models_track_start" = "Baanstart"; +"codex_models_trend_unavailable_observed_buckets" = "Waargenomen buckets: %1$d. Trend niet beschikbaar."; +"codex_models_unknown_export_error" = "Onbekende exportfout"; +"codex_models_unpriced_tokens" = "Ongeprijsde tokens"; +"codex_models_updated" = "%1$@ bijgewerkt"; +"codex_models_usage_and_change" = "Gebruik en verandering"; +"codex_models_usage_by_model" = "Gebruik per model"; +"codex_models_value_comparison_accessibility" = "%1$@, %2$@"; +"codex_models_value_detail" = "%1$@ · %2$@"; +"codex_models_value_share" = "%1$@ · %2$@ delen"; +"codex_models_value_share_accessibility" = "%1$@, %2$@ aandeel"; +"codex_models_value_start" = "Waardestart"; +"codex_models_visible_of_total" = "Zichtbare modellen: %1$d van %2$d"; +"codex_models_visible_rows" = "Zichtbare rijen"; +"codex_models_vs_previous" = "%1$@ versus vorig"; +"codex_models_week_lowercase" = "week"; +"codex_workspaces_all_models" = "Alle modellen"; +"codex_workspaces_all_workspaces" = "Alle werkruimten"; +"codex_workspaces_associated_with_selected_model" = "Geassocieerd met geselecteerd model"; +"codex_workspaces_bullet_value" = "· %1$@"; +"codex_workspaces_chart_keyboard_hint" = "Gebruik de pijltoetsen links en rechts of de VoiceOver-aanpassingsacties om een dag te selecteren. Druk op Return of Spatie om deze vast te zetten."; +"codex_workspaces_chats_description" = "Chats en gesprekken"; +"codex_workspaces_copy_path" = "Pad kopiëren"; +"codex_workspaces_cost_unavailable" = "Kosten niet beschikbaar"; +"codex_workspaces_daily_callout_accessibility" = "%1$@, %2$@"; +"codex_workspaces_daily_point_accessibility" = "%1$@, tokens %2$@"; +"codex_workspaces_daily_point_help" = "%1$@ — %2$@ tokens"; +"codex_workspaces_daily_token_usage" = "Dagelijks tokengebruik"; +"codex_workspaces_daily_unavailable" = "Dagelijks gebruik is niet beschikbaar"; +"codex_workspaces_daily_unavailable_description" = "Het totaal is berekend uit %d sessies, maar de datums konden niet worden vastgesteld."; +"codex_workspaces_date" = "Datum"; +"codex_workspaces_days" = "Dagen"; +"codex_workspaces_elevated_usage" = "Verhoogd gebruik"; +"codex_workspaces_elevated_usage_help" = "Het gebruik ligt boven de mediaan voor de geselecteerde periode."; +"codex_workspaces_estimated_cost_short" = "Gesch. kosten"; +"codex_workspaces_estimated_cost_value" = "geschat %@"; +"codex_workspaces_estimated_local_logs" = "Geschat op basis van lokale Codex-logbestanden"; +"codex_workspaces_failed_read_logs" = "Codex-logbestanden konden niet worden gelezen"; +"codex_workspaces_failed_some_logs" = "Sommige Codex-logbestanden konden niet worden gelezen"; +"codex_workspaces_finalizing_project_index_files" = "Projectindex voltooien… %d bestanden"; +"codex_workspaces_high_usage" = "Hoog gebruik"; +"codex_workspaces_high_usage_help" = "Het gebruik wijkt in de geselecteerde periode sterk af."; +"codex_workspaces_indexed_files" = "%@ geleden bijgewerkt · %d geïndexeerde bestanden"; +"codex_workspaces_indexing_local_logs" = "Lokale Codex-logbestanden indexeren…"; +"codex_workspaces_indexing_projects" = "Projecten indexeren…"; +"codex_workspaces_indexing_projects_files_left" = "Projecten indexeren… %d/%d bestanden, %d resterend"; +"codex_workspaces_last_days" = "Afgelopen %d dagen"; +"codex_workspaces_local_codex_chat" = "Lokale Codex-chat"; +"codex_workspaces_model" = "Model"; +"codex_workspaces_models" = "Modellen"; +"codex_workspaces_no_daily_usage" = "Geen dagelijks gebruik in dit geschiedenisvenster."; +"codex_workspaces_no_local_usage" = "Nog geen lokaal Codex-gebruik"; +"codex_workspaces_no_models" = "Geen uitsplitsing per model beschikbaar"; +"codex_workspaces_no_projects_or_chats" = "Geen projecten of chats gevonden in dit geschiedenisvenster."; +"codex_workspaces_no_sessions" = "Geen sessies in deze werkruimte"; +"codex_workspaces_normal_usage" = "Normaal gebruik"; +"codex_workspaces_not_available" = "Niet beschikbaar"; +"codex_workspaces_overview" = "Overzicht"; +"codex_workspaces_partial_breakdown_help" = "De samenvatting blijft leidend; niet-beschikbare datum- of modelmetadata worden als gedeeltelijk weergegeven."; +"codex_workspaces_partial_breakdown_notice" = "Sommige gedetailleerde uitsplitsingen dekken niet het volledige totaal."; +"codex_workspaces_partial_data" = "Gedeeltelijke gegevens"; +"codex_workspaces_percent_total" = "% van totaal"; +"codex_workspaces_refresh_usage_data" = "Gebruiksgegevens vernieuwen"; +"codex_workspaces_relative_usage_accessibility" = "%d procent van het hoogste werkruimtegebruik"; +"codex_workspaces_relative_usage_tooltip" = "Gebruikt %d%% van de tokens van de werkruimte met het hoogste gebruik in deze periode."; +"codex_workspaces_reveal_in_finder" = "Toon in Finder"; +"codex_workspaces_saving_project_index" = "Projectindex opslaan…"; +"codex_workspaces_scanning_logs" = "Codex-logbestanden scannen…"; +"codex_workspaces_search_sessions" = "Sessies zoeken"; +"codex_workspaces_selected_day_accessibility" = "%@: %@ tokens"; +"codex_workspaces_session" = "Sessie"; +"codex_workspaces_session_count" = "Sessies: %d"; +"codex_workspaces_sessions" = "Sessies"; +"codex_workspaces_showing_last_complete_index" = "De laatste volledige lokale index wordt getoond"; +"codex_workspaces_stale_status" = "Gebruiksindex is verouderd · %@ geleden bijgewerkt"; +"codex_workspaces_started" = "Gestart"; +"codex_workspaces_title" = "Werkruimten"; +"codex_workspaces_token_count_accessibility" = "%@ tokens"; +"codex_workspaces_tokens" = "Tokens"; +"codex_workspaces_top_model" = "Topmodel"; +"codex_workspaces_top_sessions" = "Belangrijkste sessies"; +"codex_workspaces_total" = "Totaal"; +"codex_workspaces_view" = "Weergave"; +"codex_workspaces_view_all_sessions" = "Alle sessies bekijken"; +"codex_workspaces_window_title" = "Codex-werkruimten"; +"codex_workspaces_workspace_count" = "Werkruimten en chats: %d"; +"codex_workspaces_workspaces_chats" = "Werkruimten/chats"; diff --git a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings index 9958e32f9e..c955aaa701 100644 --- a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings @@ -1353,3 +1353,255 @@ "Cost today unavailable" = "Koszt dzisiaj: Niedostępne"; "30-day cost unavailable" = "Koszt 30 dni: Niedostępne"; "Resets" = "Resety"; + +/* Codex Workspaces and Models */ +"codex_workspaces_open_in_window" = "Otwórz w oknie"; +"codex_models_actions" = "Działania"; +"codex_models_active_and_new_count" = "Aktywne modele: %1$d; nowo aktywny: %2$d"; +"codex_models_active_count_new_unavailable" = "Aktywne modele: %1$d; nowo aktywna liczba jest niedostępna"; +"codex_models_active_models" = "Aktywne modele"; +"codex_models_alias_count" = "Aliasy: %1$d"; +"codex_models_all_model_count" = "Wszystkie modele: %1$d"; +"codex_models_all_models" = "Wszystkie modele"; +"codex_models_all_rows" = "Wszystkie wiersze"; +"codex_models_analytics" = "Analiza modeli"; +"codex_models_analytics_controls" = "Kontrole analityki modeli"; +"codex_models_apply" = "Zastosuj"; +"codex_models_bucket_count" = "Wiadra (%2$@): %1$d"; +"codex_models_bucket_interval" = "Interwał segmentu"; +"codex_models_bullet_value" = "· %1$@"; +"codex_models_canonical_id" = "Identyfikator kanoniczny"; +"codex_models_canonical_model" = "Model kanoniczny"; +"codex_models_change" = "Zmień"; +"codex_models_change_from_peak" = "%1$@ od %2$@ szczytu"; +"codex_models_change_from_previous_period" = "%1$@ z poprzedniego równego okresu"; +"codex_models_choose_metric" = "Wybierz metrykę analityczną"; +"codex_models_clear_search" = "Wyczyść wyszukiwanie"; +"codex_models_clear_selection" = "Wyczyść wybór modeli"; +"codex_models_columns" = "Kolumny"; +"codex_models_compact_billions_suffix" = "B"; +"codex_models_compact_millions_suffix" = "M"; +"codex_models_compact_thousands_suffix" = "K"; +"codex_models_contains_selected_suffix" = ", zawiera wybrany model"; +"codex_models_copy_aliases" = "Skopiuj aliasy"; +"codex_models_copy_canonical_id" = "Skopiuj identyfikator kanoniczny"; +"codex_models_copy_canonical_model_id" = "Skopiuj kanoniczny identyfikator modelu"; +"codex_models_copy_raw_aliases" = "Skopiuj surowe aliasy"; +"codex_models_cost_conclusions_limited" = "Wnioski dotyczące kosztów są ograniczone do tokenów o cenie %1$@."; +"codex_models_cost_hidden" = "Koszt ukryty"; +"codex_models_cost_status_currency" = "%1$@ · %2$@ · %3$@"; +"codex_models_cost_unavailable" = "Koszt niedostępny"; +"codex_models_cost_unavailable_for_data" = "Koszt jest niedostępny dla tych danych"; +"codex_models_coverage" = "Zasięg"; +"codex_models_currency" = "Waluta"; +"codex_models_current_metric" = "Bieżący %1$@"; +"codex_models_current_period_in_progress" = "Bieżący %1$@ w toku"; +"codex_models_date" = "Data"; +"codex_models_day_lowercase" = "dzień"; +"codex_models_days" = "Dni"; +"codex_models_delta_vs_prior" = "Δ w porównaniu z poprzednią"; +"codex_models_disabled_in_settings" = "Wyłączone w ustawieniach"; +"codex_models_dominates_at" = "Ten model dominuje %1$@ przy %2$@."; +"codex_models_ended" = "Zakończono"; +"codex_models_export" = "Eksport"; +"codex_models_export_all_filename" = "codex-models-all.csv"; +"codex_models_export_default_filename" = "codex-models.csv"; +"codex_models_export_failed" = "Nie można wyeksportować analiz modeli"; +"codex_models_export_selected_filename" = "codex-model-%1$@.csv"; +"codex_models_export_selected_model" = "Eksportuj wybrany model"; +"codex_models_export_visible_filename" = "codex-models-visible.csv"; +"codex_models_export_visible_rows" = "Eksportuj widoczne wiersze modeli"; +"codex_models_focus_table_search" = "Przeszukiwanie tabeli modeli fokusu"; +"codex_models_fully_priced" = "Pełna cena"; +"codex_models_fully_priced_percent" = "100% ceny"; +"codex_models_generated" = "Wygenerowano %1$@"; +"codex_models_granularity_accessibility" = "Szczegółowość, %1$@, %2$@"; +"codex_models_hidden" = "Ukryty"; +"codex_models_identity_aliases_help" = "%1$@ · %2$@ · Aliasy: %3$@"; +"codex_models_identity_and_aliases" = "Tożsamość i aliasy"; +"codex_models_indexing_last_days" = "Indeksowanie ostatnich %1$d dni…"; +"codex_models_interpretation" = "Interpretacja"; +"codex_models_known_cost" = "Znany koszt"; +"codex_models_known_cost_fully_priced_accessibility" = "Znany koszt %1$@, pełna cena"; +"codex_models_known_cost_lowercase" = "znany koszt"; +"codex_models_known_cost_partial_accessibility" = "Znany koszt %1$@, częściowy, %2$@ wycenionych tokenów"; +"codex_models_last_days" = "Ostatnie %1$d dni"; +"codex_models_local_logs_source" = "· Lokalne dzienniki Kodeksu"; +"codex_models_major_contributor_at" = "Ten model jest głównym współtwórcą %1$@."; +"codex_models_metric" = "Metryczne"; +"codex_models_metric_over_time" = "%1$@ w miarę upływu czasu"; +"codex_models_model" = "Model"; +"codex_models_model_concentration" = "Stężenie modelowe"; +"codex_models_model_count" = "Modele: %1$d"; +"codex_models_model_usage" = "Użycie modelu"; +"codex_models_month_lowercase" = "miesiąc"; +"codex_models_more_alias_count" = "Więcej aliasów: %1$d"; +"codex_models_name_canonical_id" = "%1$@, identyfikator kanoniczny %2$@"; +"codex_models_name_value_accessibility" = "%1$@, %2$@"; +"codex_models_named_metric_over_time" = "%1$@ %2$@ w czasie"; +"codex_models_new" = "Nowość"; +"codex_models_new_in_period" = "Nowość w tym okresie"; +"codex_models_newly_active_count" = "Nowo aktywny: %1$d"; +"codex_models_no_applicable_price" = "Brak obowiązującej ceny"; +"codex_models_no_change_from_previous_period" = "Brak zmian w porównaniu z poprzednim równym okresem"; +"codex_models_no_matching_models" = "Brak pasujących modeli"; +"codex_models_no_newly_active" = "Brak nowo aktywnych modeli"; +"codex_models_no_top_model" = "Brak topowego modelu"; +"codex_models_no_usage" = "Brak użycia"; +"codex_models_no_usage_description" = "Wybierz dłuższy zakres dat powyżej lub odśwież po zapisaniu lokalnych sesji Kodeksu."; +"codex_models_no_usage_in_period" = "Brak użycia w tym okresie"; +"codex_models_no_usage_in_range" = "Brak użycia modelu w tym zakresie"; +"codex_models_other_count" = "Inne (%1$d)"; +"codex_models_partial_priced" = "Częściowe · %1$@ cena"; +"codex_models_precision_table" = "Tabela precyzji modelu"; +"codex_models_previous_known_cost" = "Poprzedni znany koszt"; +"codex_models_previous_metric" = "Poprzedni %1$@"; +"codex_models_previous_period_unavailable" = "Poprzedni okres jest niedostępny"; +"codex_models_previous_unavailable" = "Poprzednie niedostępne"; +"codex_models_priced_tokens" = "Tokeny cenowe"; +"codex_models_pricing_calculation" = "Kalkulacja cen"; +"codex_models_pricing_coverage" = "Zakres cen"; +"codex_models_pricing_explanation" = "%1$@ obejmuje tokeny płatne %2$@. Tokeny %3$@ bez obowiązującej reguły są wykluczane i nie są szacowane jako zero."; +"codex_models_ranking_by" = "Ranking modeli według %1$@"; +"codex_models_ranking_hint" = "Kliknij pasek modelu, aby otworzyć jego szczegóły."; +"codex_models_ranking_point_accessibility" = "%1$@, %2$@, %3$@ udział"; +"codex_models_ranking_summary" = "Modele kanoniczne i inne uszeregowane według %1$@."; +"codex_models_ref_count" = "%1$@ ref"; +"codex_models_refresh_analytics" = "Odśwież statystyki modeli"; +"codex_models_refresh_command_help" = "Odśwież analizę modeli (Command-R)"; +"codex_models_refresh_model_usage" = "Odśwież użycie modelu"; +"codex_models_refreshing_last_snapshot" = "Odświeżanie · wyświetlanie ostatniej pomyślnej migawki"; +"codex_models_retry" = "Spróbuj ponownie"; +"codex_models_reveal_pricing_calculation" = "Ujawnij kalkulację cen"; +"codex_models_search_empty_description" = "Wyszukiwanie filtruje tylko tę tabelę; sumy wskaźników na pulpicie nawigacyjnym pozostają niezmienione."; +"codex_models_search_models" = "Wyszukaj modele"; +"codex_models_search_scope" = "%1$@ · Wyszukiwanie filtruje tylko tę tabelę"; +"codex_models_search_table" = "Tabela modeli wyszukiwania"; +"codex_models_select_model" = "Wybierz %1$@"; +"codex_models_selected" = "Wybrano"; +"codex_models_selected_bucket" = "Wybrany zasobnik"; +"codex_models_selected_bucket_accessibility" = "Wybrany zasobnik: %1$@, %2$@, %3$@ tokeny, %4$@ referencje sesji, %5$@, waluta %6$@."; +"codex_models_selected_clear_available" = "Wybrany model %1$@, Możliwość wyczyszczenia wyboru"; +"codex_models_selected_date" = "Wybrana data"; +"codex_models_selected_hidden_by_search" = "Wybrany model ukryty w wyszukiwaniu"; +"codex_models_selected_model" = "Wybrany model"; +"codex_models_selected_model_interval" = "Wybrany model · %1$@"; +"codex_models_selected_suffix" = ", wybrano"; +"codex_models_session_reference_count" = "%1$@ odniesienia do sesji"; +"codex_models_session_references" = "Odniesienia do sesji"; +"codex_models_session_references_lowercase" = "odniesienia do sesji"; +"codex_models_session_refs" = "Referencje sesji"; +"codex_models_share" = "Udostępnij"; +"codex_models_share_known_cost_help" = "Udział znanych kosztów; nie obejmuje bezpłatnego użytkowania"; +"codex_models_share_of" = "Udział %1$@"; +"codex_models_show_associated_sessions" = "Pokaż powiązane sesje"; +"codex_models_show_details" = "Pokaż szczegóły"; +"codex_models_showing_last_snapshot" = "Wyświetlanie ostatniej pomyślnej migawki"; +"codex_models_sorted_by" = "Posortowane według %1$@"; +"codex_models_subject_over_time" = "%1$@ w miarę upływu czasu"; +"codex_models_summary" = "Podsumowanie modeli"; +"codex_models_timeline_accessibility" = "Oś czasu modeli, %1$@, %2$@"; +"codex_models_timeline_hint" = "Kliknij segment czasu lub przytrzymaj wskaźnik nad nim przez 1,5 sekundy, aby sprawdzić jego szczegóły."; +"codex_models_timeline_point_accessibility" = "%1$@, %2$@, %3$@ tokeny, %4$@ referencje sesji, %5$@, waluta %6$@"; +"codex_models_timeline_summary" = "%1$@ %2$@ oś czasu. %3$@"; +"codex_models_token_count" = "%1$@ tokeny"; +"codex_models_tokens" = "Tokeny"; +"codex_models_tokens_lowercase" = "tokeny"; +"codex_models_tokens_per_session_reference" = "Około %1$@ tokenów na odwołanie do sesji."; +"codex_models_top" = "Góra"; +"codex_models_top_model" = "Najlepszy model"; +"codex_models_top_seven_other" = "Top 7 + inne"; +"codex_models_top_two_account_for" = "Dwa najlepsze modele odpowiadają za %1$@ z %2$@"; +"codex_models_top_two_of" = "Dwa najlepsze modele %1$@"; +"codex_models_total_session_references_lowercase" = "całkowita liczba odwołań do sesji"; +"codex_models_total_tokens" = "Łączna liczba tokenów"; +"codex_models_total_tokens_lowercase" = "ogółem tokenów"; +"codex_models_totals_reconcile" = "· Sumy zgadzają się z aktywnym zrzutem ekranu"; +"codex_models_track_end" = "Koniec ścieżki"; +"codex_models_track_start" = "Początek ścieżki"; +"codex_models_trend_unavailable_observed_buckets" = "Obserwowane zasobniki: %1$d. Trend niedostępny."; +"codex_models_unknown_export_error" = "Nieznany błąd eksportu"; +"codex_models_unpriced_tokens" = "Tokeny bez ceny"; +"codex_models_updated" = "Zaktualizowano %1$@"; +"codex_models_usage_and_change" = "Użycie i zmiana"; +"codex_models_usage_by_model" = "Zastosowanie według modelu"; +"codex_models_value_comparison_accessibility" = "%1$@, %2$@"; +"codex_models_value_detail" = "%1$@ · %2$@"; +"codex_models_value_share" = "%1$@ · %2$@ udział"; +"codex_models_value_share_accessibility" = "%1$@, %2$@ udział"; +"codex_models_value_start" = "Początek wartości"; +"codex_models_visible_of_total" = "Widoczne modele: %1$d z %2$d"; +"codex_models_visible_rows" = "Widoczne wiersze"; +"codex_models_vs_previous" = "%1$@ w porównaniu z poprzednią"; +"codex_models_week_lowercase" = "tydzień"; +"codex_workspaces_all_models" = "Wszystkie modele"; +"codex_workspaces_all_workspaces" = "Wszystkie obszary robocze"; +"codex_workspaces_associated_with_selected_model" = "Powiązany z wybranym modelem"; +"codex_workspaces_bullet_value" = "· %1$@"; +"codex_workspaces_chart_keyboard_hint" = "Wybierz dzień klawiszami strzałek w lewo i w prawo lub czynnościami regulacji VoiceOver. Naciśnij Return lub Spację, aby go przypiąć."; +"codex_workspaces_chats_description" = "Czaty i rozmowy"; +"codex_workspaces_copy_path" = "Kopiuj ścieżkę"; +"codex_workspaces_cost_unavailable" = "Koszt niedostępny"; +"codex_workspaces_daily_callout_accessibility" = "%1$@, %2$@"; +"codex_workspaces_daily_point_accessibility" = "%1$@, tokeny %2$@"; +"codex_workspaces_daily_point_help" = "%1$@ — %2$@ tokeny"; +"codex_workspaces_daily_token_usage" = "Dzienne użycie tokenów"; +"codex_workspaces_daily_unavailable" = "Dzienne użycie jest niedostępne"; +"codex_workspaces_daily_unavailable_description" = "Suma została obliczona na podstawie %d sesji, ale nie udało się ustalić ich dat."; +"codex_workspaces_date" = "Data"; +"codex_workspaces_days" = "Dni"; +"codex_workspaces_elevated_usage" = "Podwyższone użycie"; +"codex_workspaces_elevated_usage_help" = "Użycie przekracza medianę w wybranym okresie."; +"codex_workspaces_estimated_cost_short" = "Szac. koszt"; +"codex_workspaces_estimated_cost_value" = "szac. %@"; +"codex_workspaces_estimated_local_logs" = "Oszacowano na podstawie lokalnych dzienników Codex"; +"codex_workspaces_failed_read_logs" = "Nie udało się odczytać dzienników Codex"; +"codex_workspaces_failed_some_logs" = "Nie udało się odczytać części dzienników Codex"; +"codex_workspaces_finalizing_project_index_files" = "Finalizowanie indeksu projektów… %d plików"; +"codex_workspaces_high_usage" = "Wysokie użycie"; +"codex_workspaces_high_usage_help" = "Użycie znacząco odbiega od pozostałych w wybranym okresie."; +"codex_workspaces_indexed_files" = "Zaktualizowano %@ temu · %d zindeksowanych plików"; +"codex_workspaces_indexing_local_logs" = "Indeksowanie lokalnych dzienników Codex…"; +"codex_workspaces_indexing_projects" = "Indeksowanie projektów…"; +"codex_workspaces_indexing_projects_files_left" = "Indeksowanie projektów… %d/%d plików, pozostało %d"; +"codex_workspaces_last_days" = "Ostatnie %d dni"; +"codex_workspaces_local_codex_chat" = "Lokalny czat Codex"; +"codex_workspaces_model" = "Model"; +"codex_workspaces_models" = "Modele"; +"codex_workspaces_no_daily_usage" = "Brak dziennego użycia w tym zakresie historii."; +"codex_workspaces_no_local_usage" = "Brak lokalnego użycia Codex"; +"codex_workspaces_no_models" = "Brak zestawienia według modeli"; +"codex_workspaces_no_projects_or_chats" = "Nie znaleziono projektów ani czatów w tym zakresie historii."; +"codex_workspaces_no_sessions" = "Brak sesji w tym obszarze roboczym"; +"codex_workspaces_normal_usage" = "Normalne użycie"; +"codex_workspaces_not_available" = "Niedostępne"; +"codex_workspaces_overview" = "Przegląd"; +"codex_workspaces_partial_breakdown_help" = "Podsumowanie pozostaje wiążące, a niedostępne metadane dat lub modeli są oznaczone jako częściowe."; +"codex_workspaces_partial_breakdown_notice" = "Niektóre szczegółowe zestawienia nie obejmują całej sumy."; +"codex_workspaces_partial_data" = "Dane częściowe"; +"codex_workspaces_percent_total" = "% całości"; +"codex_workspaces_refresh_usage_data" = "Odśwież dane użycia"; +"codex_workspaces_relative_usage_accessibility" = "%d procent najwyższego użycia obszaru roboczego"; +"codex_workspaces_relative_usage_tooltip" = "Używa %d%% tokenów obszaru roboczego o najwyższym użyciu w tym okresie."; +"codex_workspaces_reveal_in_finder" = "Pokaż w Finderze"; +"codex_workspaces_saving_project_index" = "Zapisywanie indeksu projektów…"; +"codex_workspaces_scanning_logs" = "Skanowanie dzienników Codex…"; +"codex_workspaces_search_sessions" = "Szukaj sesji"; +"codex_workspaces_selected_day_accessibility" = "%@: %@ tokenów"; +"codex_workspaces_session" = "Sesja"; +"codex_workspaces_session_count" = "Sesje: %d"; +"codex_workspaces_sessions" = "Sesje"; +"codex_workspaces_showing_last_complete_index" = "Wyświetlanie ostatniego kompletnego lokalnego indeksu"; +"codex_workspaces_stale_status" = "Indeks użycia jest nieaktualny · Zaktualizowano %@ temu"; +"codex_workspaces_started" = "Rozpoczęto"; +"codex_workspaces_title" = "Obszary robocze"; +"codex_workspaces_token_count_accessibility" = "%@ tokenów"; +"codex_workspaces_tokens" = "Tokeny"; +"codex_workspaces_top_model" = "Najczęstszy model"; +"codex_workspaces_top_sessions" = "Najważniejsze sesje"; +"codex_workspaces_total" = "Łącznie"; +"codex_workspaces_view" = "Widok"; +"codex_workspaces_view_all_sessions" = "Wyświetl wszystkie sesje"; +"codex_workspaces_window_title" = "Obszary robocze Codex"; +"codex_workspaces_workspace_count" = "Obszary robocze i czaty: %d"; +"codex_workspaces_workspaces_chats" = "Obszary robocze/czaty"; diff --git a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings index 434ba9148c..d63cbcc8bc 100644 --- a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings @@ -1350,3 +1350,255 @@ "Cost today unavailable" = "Custo hoje: Indisponível"; "30-day cost unavailable" = "Custo em 30 dias: Indisponível"; "Resets" = "Redefinições"; + +/* Codex Workspaces and Models */ +"codex_workspaces_open_in_window" = "Abrir em uma janela"; +"codex_models_actions" = "Ações"; +"codex_models_active_and_new_count" = "Modelos ativos: %1$d; recentemente ativo: %2$d"; +"codex_models_active_count_new_unavailable" = "Modelos ativos: %1$d; contagem recentemente ativa indisponível"; +"codex_models_active_models" = "Modelos ativos"; +"codex_models_alias_count" = "Alias: %1$d"; +"codex_models_all_model_count" = "Todos os modelos: %1$d"; +"codex_models_all_models" = "Todos os modelos"; +"codex_models_all_rows" = "Todas as linhas"; +"codex_models_analytics" = "Análise de modelos"; +"codex_models_analytics_controls" = "Controles de análise de modelos"; +"codex_models_apply" = "Aplicar"; +"codex_models_bucket_count" = "Intervalos (%2$@): %1$d"; +"codex_models_bucket_interval" = "Intervalo de intervalo"; +"codex_models_bullet_value" = "· %1$@"; +"codex_models_canonical_id" = "ID canônico"; +"codex_models_canonical_model" = "Modelo canônico"; +"codex_models_change" = "Alteração"; +"codex_models_change_from_peak" = "%1$@ de %2$@ pico"; +"codex_models_change_from_previous_period" = "%1$@ do período igual anterior"; +"codex_models_choose_metric" = "Escolha a métrica analítica"; +"codex_models_clear_search" = "Limpar pesquisa"; +"codex_models_clear_selection" = "Limpar seleção de modelos"; +"codex_models_columns" = "Colunas"; +"codex_models_compact_billions_suffix" = "B"; +"codex_models_compact_millions_suffix" = "M"; +"codex_models_compact_thousands_suffix" = "K"; +"codex_models_contains_selected_suffix" = ", contém o modelo selecionado"; +"codex_models_copy_aliases" = "Copiar aliases"; +"codex_models_copy_canonical_id" = "Copiar ID canônico"; +"codex_models_copy_canonical_model_id" = "Copiar ID do modelo canônico"; +"codex_models_copy_raw_aliases" = "Copiar aliases brutos"; +"codex_models_cost_conclusions_limited" = "As conclusões de custo são limitadas a tokens com preço de %1$@."; +"codex_models_cost_hidden" = "Custo oculto"; +"codex_models_cost_status_currency" = "%1$@ · %2$@ · %3$@"; +"codex_models_cost_unavailable" = "Custo indisponível"; +"codex_models_cost_unavailable_for_data" = "O custo não está disponível para estes dados"; +"codex_models_coverage" = "Cobertura"; +"codex_models_currency" = "Moeda"; +"codex_models_current_metric" = "%1$@ atual"; +"codex_models_current_period_in_progress" = "%1$@ atual em andamento"; +"codex_models_date" = "Data"; +"codex_models_day_lowercase" = "dia"; +"codex_models_days" = "Dias"; +"codex_models_delta_vs_prior" = "Δ vs anterior"; +"codex_models_disabled_in_settings" = "Desativado nas configurações"; +"codex_models_dominates_at" = "Este modelo domina %1$@ em %2$@."; +"codex_models_ended" = "Terminado"; +"codex_models_export" = "Exportar"; +"codex_models_export_all_filename" = "codex-models-all.csv"; +"codex_models_export_default_filename" = "codex-models.csv"; +"codex_models_export_failed" = "Não foi possível exportar análises de modelos"; +"codex_models_export_selected_filename" = "codex-model-%1$@.csv"; +"codex_models_export_selected_model" = "Exportar modelo selecionado"; +"codex_models_export_visible_filename" = "codex-models-visible.csv"; +"codex_models_export_visible_rows" = "Exportar linhas de modelos visíveis"; +"codex_models_focus_table_search" = "Pesquisa de tabela de modelos de foco"; +"codex_models_fully_priced" = "Preço total"; +"codex_models_fully_priced_percent" = "100% de preço"; +"codex_models_generated" = "Gerado %1$@"; +"codex_models_granularity_accessibility" = "Granularidade, %1$@, %2$@"; +"codex_models_hidden" = "Oculto"; +"codex_models_identity_aliases_help" = "%1$@ · %2$@ · Aliases: %3$@"; +"codex_models_identity_and_aliases" = "Identidade e apelidos"; +"codex_models_indexing_last_days" = "Indexação últimos %1$d dias…"; +"codex_models_interpretation" = "Interpretação"; +"codex_models_known_cost" = "Custo conhecido"; +"codex_models_known_cost_fully_priced_accessibility" = "Custo conhecido %1$@, preço total"; +"codex_models_known_cost_lowercase" = "custo conhecido"; +"codex_models_known_cost_partial_accessibility" = "Custo conhecido %1$@, parcial, %2$@ do preço dos tokens"; +"codex_models_last_days" = "Últimos %1$d dias"; +"codex_models_local_logs_source" = "· Registros locais do Codex"; +"codex_models_major_contributor_at" = "Este modelo é um contribuidor importante em %1$@."; +"codex_models_metric" = "Métrica"; +"codex_models_metric_over_time" = "%1$@ ao longo do tempo"; +"codex_models_model" = "Modelo"; +"codex_models_model_concentration" = "Concentração do modelo"; +"codex_models_model_count" = "Modelos: %1$d"; +"codex_models_model_usage" = "Uso do modelo"; +"codex_models_month_lowercase" = "mês"; +"codex_models_more_alias_count" = "Mais aliases: %1$d"; +"codex_models_name_canonical_id" = "%1$@, ID canônico %2$@"; +"codex_models_name_value_accessibility" = "%1$@, %2$@"; +"codex_models_named_metric_over_time" = "%1$@ %2$@ ao longo do tempo"; +"codex_models_new" = "Novo"; +"codex_models_new_in_period" = "Novidade neste período"; +"codex_models_newly_active_count" = "Recentemente ativo: %1$d"; +"codex_models_no_applicable_price" = "Nenhum preço aplicável"; +"codex_models_no_change_from_previous_period" = "Nenhuma alteração em relação ao período igual anterior"; +"codex_models_no_matching_models" = "Nenhum modelo correspondente"; +"codex_models_no_newly_active" = "Nenhum modelo ativo recentemente"; +"codex_models_no_top_model" = "Nenhum modelo top"; +"codex_models_no_usage" = "Sem uso"; +"codex_models_no_usage_description" = "Escolha um intervalo de datas mais longo acima ou atualize após as sessões locais do Codex serem salvas."; +"codex_models_no_usage_in_period" = "Sem utilização neste período"; +"codex_models_no_usage_in_range" = "Nenhum modelo usado neste intervalo"; +"codex_models_other_count" = "Outro (%1$d)"; +"codex_models_partial_priced" = "Parcial · %1$@ com preço"; +"codex_models_precision_table" = "Tabela de precisão do modelo"; +"codex_models_previous_known_cost" = "Custo conhecido anterior"; +"codex_models_previous_metric" = "%1$@ anterior"; +"codex_models_previous_period_unavailable" = "O período anterior não está disponível"; +"codex_models_previous_unavailable" = "Anterior indisponível"; +"codex_models_priced_tokens" = "Tokens com preço"; +"codex_models_pricing_calculation" = "Cálculo de preços"; +"codex_models_pricing_coverage" = "Cobertura de preços"; +"codex_models_pricing_explanation" = "%1$@ cobre tokens com preço de %2$@. Os tokens %3$@ sem uma regra aplicável são excluídos e não são estimados como zero."; +"codex_models_ranking_by" = "Classificação do modelo por %1$@"; +"codex_models_ranking_hint" = "Clique em uma barra de modelo para abrir seus detalhes."; +"codex_models_ranking_point_accessibility" = "%1$@, %2$@, %3$@ compartilhar"; +"codex_models_ranking_summary" = "Modelos canônicos e outros classificados por %1$@."; +"codex_models_ref_count" = "%1$@ referências"; +"codex_models_refresh_analytics" = "Atualizar análise de modelos"; +"codex_models_refresh_command_help" = "Atualizar análise de modelos (Command-R)"; +"codex_models_refresh_model_usage" = "Atualizar uso do modelo"; +"codex_models_refreshing_last_snapshot" = "Atualizando · mostrando o último instantâneo bem-sucedido"; +"codex_models_retry" = "Tentar novamente"; +"codex_models_reveal_pricing_calculation" = "Revelar cálculo de preços"; +"codex_models_search_empty_description" = "A pesquisa filtra apenas esta tabela; os totais do painel permanecem inalterados."; +"codex_models_search_models" = "Pesquisar modelos"; +"codex_models_search_scope" = "%1$@ · A pesquisa filtra apenas esta tabela"; +"codex_models_search_table" = "Tabela de modelos de pesquisa"; +"codex_models_select_model" = "Selecione %1$@"; +"codex_models_selected" = "Selecionado"; +"codex_models_selected_bucket" = "Intervalo selecionado"; +"codex_models_selected_bucket_accessibility" = "Bucket selecionado: %1$@, %2$@, %3$@ tokens, %4$@ referências de sessão, %5$@, moeda %6$@."; +"codex_models_selected_clear_available" = "Modelo selecionado %1$@, seleção clara disponível"; +"codex_models_selected_date" = "Data selecionada"; +"codex_models_selected_hidden_by_search" = "Modelo selecionado oculto pela pesquisa"; +"codex_models_selected_model" = "Modelo selecionado"; +"codex_models_selected_model_interval" = "Modelo selecionado · %1$@"; +"codex_models_selected_suffix" = ", selecionado"; +"codex_models_session_reference_count" = "%1$@ referências de sessão"; +"codex_models_session_references" = "Referências de sessão"; +"codex_models_session_references_lowercase" = "referências de sessão"; +"codex_models_session_refs" = "Referências de sessão"; +"codex_models_share" = "Compartilhar"; +"codex_models_share_known_cost_help" = "Participação no custo conhecido; exclui uso sem preço"; +"codex_models_share_of" = "Participação de %1$@"; +"codex_models_show_associated_sessions" = "Mostrar sessões associadas"; +"codex_models_show_details" = "Mostrar detalhes"; +"codex_models_showing_last_snapshot" = "Mostrando o último instantâneo bem-sucedido"; +"codex_models_sorted_by" = "Classificado por %1$@"; +"codex_models_subject_over_time" = "%1$@ ao longo do tempo"; +"codex_models_summary" = "Resumo dos modelos"; +"codex_models_timeline_accessibility" = "Linha do tempo dos modelos, %1$@, %2$@"; +"codex_models_timeline_hint" = "Clique em um intervalo de tempo ou mantenha o ponteiro sobre ele por 1,5 segundos para inspecionar seus detalhes."; +"codex_models_timeline_point_accessibility" = "%1$@, %2$@, %3$@ tokens, %4$@ referências de sessão, %5$@, moeda %6$@"; +"codex_models_timeline_summary" = "%1$@ %2$@ linha do tempo. %3$@"; +"codex_models_token_count" = "%1$@ fichas"; +"codex_models_tokens" = "Tokens"; +"codex_models_tokens_lowercase" = "fichas"; +"codex_models_tokens_per_session_reference" = "Cerca de %1$@ tokens por referência de sessão."; +"codex_models_top" = "Superior"; +"codex_models_top_model" = "Modelo superior"; +"codex_models_top_seven_other" = "7 principais + outros"; +"codex_models_top_two_account_for" = "Os dois principais modelos representam %1$@ de %2$@"; +"codex_models_top_two_of" = "Dois principais modelos de %1$@"; +"codex_models_total_session_references_lowercase" = "total de referências de sessão"; +"codex_models_total_tokens" = "Total de tokens"; +"codex_models_total_tokens_lowercase" = "tokens totais"; +"codex_models_totals_reconcile" = "· Totais reconciliados com o instantâneo ativo"; +"codex_models_track_end" = "Fim da trilha"; +"codex_models_track_start" = "Início da trilha"; +"codex_models_trend_unavailable_observed_buckets" = "Intervalos observados: %1$d. Tendência indisponível."; +"codex_models_unknown_export_error" = "Erro de exportação desconhecido"; +"codex_models_unpriced_tokens" = "Tokens sem preço"; +"codex_models_updated" = "Atualizado %1$@"; +"codex_models_usage_and_change" = "Uso e alteração"; +"codex_models_usage_by_model" = "Uso por modelo"; +"codex_models_value_comparison_accessibility" = "%1$@, %2$@"; +"codex_models_value_detail" = "%1$@ · %2$@"; +"codex_models_value_share" = "%1$@ · %2$@ compartilhar"; +"codex_models_value_share_accessibility" = "%1$@, %2$@ compartilhar"; +"codex_models_value_start" = "Valor inicial"; +"codex_models_visible_of_total" = "Modelos visíveis: %1$d de %2$d"; +"codex_models_visible_rows" = "Linhas visíveis"; +"codex_models_vs_previous" = "%1$@ vs anterior"; +"codex_models_week_lowercase" = "semana"; +"codex_workspaces_all_models" = "Todos os modelos"; +"codex_workspaces_all_workspaces" = "Todos os espaços de trabalho"; +"codex_workspaces_associated_with_selected_model" = "Associado ao modelo selecionado"; +"codex_workspaces_bullet_value" = "· %1$@"; +"codex_workspaces_chart_keyboard_hint" = "Use as teclas de seta para a esquerda e para a direita ou as ações de ajuste do VoiceOver para selecionar um dia. Pressione Return ou Espaço para fixá-lo."; +"codex_workspaces_chats_description" = "Chats e conversas"; +"codex_workspaces_copy_path" = "Copiar caminho"; +"codex_workspaces_cost_unavailable" = "Custo indisponível"; +"codex_workspaces_daily_callout_accessibility" = "%1$@, %2$@"; +"codex_workspaces_daily_point_accessibility" = "%1$@, fichas %2$@"; +"codex_workspaces_daily_point_help" = "%1$@ — %2$@ tokens"; +"codex_workspaces_daily_token_usage" = "Uso diário de tokens"; +"codex_workspaces_daily_unavailable" = "O uso diário não está disponível"; +"codex_workspaces_daily_unavailable_description" = "O total foi calculado com base em %d sessões, mas não foi possível determinar as datas."; +"codex_workspaces_date" = "Data"; +"codex_workspaces_days" = "Dias"; +"codex_workspaces_elevated_usage" = "Uso elevado"; +"codex_workspaces_elevated_usage_help" = "O uso está acima da mediana no período selecionado."; +"codex_workspaces_estimated_cost_short" = "Custo estimado"; +"codex_workspaces_estimated_cost_value" = "estim. %@"; +"codex_workspaces_estimated_local_logs" = "Estimado com base nos registros locais do Codex"; +"codex_workspaces_failed_read_logs" = "Não foi possível ler os registros do Codex"; +"codex_workspaces_failed_some_logs" = "Não foi possível ler alguns registros do Codex"; +"codex_workspaces_finalizing_project_index_files" = "Finalizando o índice de projetos… %d arquivos"; +"codex_workspaces_high_usage" = "Uso alto"; +"codex_workspaces_high_usage_help" = "O uso é significativamente atípico no período selecionado."; +"codex_workspaces_indexed_files" = "Atualizado há %@ · %d arquivos indexados"; +"codex_workspaces_indexing_local_logs" = "Indexando registros locais do Codex…"; +"codex_workspaces_indexing_projects" = "Indexando projetos…"; +"codex_workspaces_indexing_projects_files_left" = "Indexando projetos… %d/%d arquivos, faltam %d"; +"codex_workspaces_last_days" = "Últimos %d dias"; +"codex_workspaces_local_codex_chat" = "Chat local do Codex"; +"codex_workspaces_model" = "Modelo"; +"codex_workspaces_models" = "Modelos"; +"codex_workspaces_no_daily_usage" = "Não há uso diário neste intervalo do histórico."; +"codex_workspaces_no_local_usage" = "Ainda não há uso local do Codex"; +"codex_workspaces_no_models" = "Nenhum detalhamento por modelo disponível"; +"codex_workspaces_no_projects_or_chats" = "Nenhum projeto ou chat foi encontrado neste intervalo do histórico."; +"codex_workspaces_no_sessions" = "Não há sessões neste espaço de trabalho"; +"codex_workspaces_normal_usage" = "Uso normal"; +"codex_workspaces_not_available" = "Não disponível"; +"codex_workspaces_overview" = "Visão geral"; +"codex_workspaces_partial_breakdown_help" = "O resumo continua sendo a referência, enquanto metadados de data ou modelo indisponíveis são mostrados como parciais."; +"codex_workspaces_partial_breakdown_notice" = "Alguns detalhamentos não cobrem o total completo."; +"codex_workspaces_partial_data" = "Dados parciais"; +"codex_workspaces_percent_total" = "% do total"; +"codex_workspaces_refresh_usage_data" = "Atualizar dados de uso"; +"codex_workspaces_relative_usage_accessibility" = "%d por cento do maior uso de um espaço de trabalho"; +"codex_workspaces_relative_usage_tooltip" = "Usa %d%% dos tokens do espaço de trabalho com maior uso neste período."; +"codex_workspaces_reveal_in_finder" = "Mostrar no Finder"; +"codex_workspaces_saving_project_index" = "Salvando o índice de projetos…"; +"codex_workspaces_scanning_logs" = "Verificando registros do Codex…"; +"codex_workspaces_search_sessions" = "Buscar sessões"; +"codex_workspaces_selected_day_accessibility" = "%@: %@ tokens"; +"codex_workspaces_session" = "Sessão"; +"codex_workspaces_session_count" = "Sessões: %d"; +"codex_workspaces_sessions" = "Sessões"; +"codex_workspaces_showing_last_complete_index" = "Mostrando o último índice local completo"; +"codex_workspaces_stale_status" = "O índice de uso está desatualizado · Atualizado há %@"; +"codex_workspaces_started" = "Iniciada"; +"codex_workspaces_title" = "Espaços de trabalho"; +"codex_workspaces_token_count_accessibility" = "%@ tokens"; +"codex_workspaces_tokens" = "Tokens"; +"codex_workspaces_top_model" = "Modelo principal"; +"codex_workspaces_top_sessions" = "Principais sessões"; +"codex_workspaces_total" = "Total"; +"codex_workspaces_view" = "Visualização"; +"codex_workspaces_view_all_sessions" = "Ver todas as sessões"; +"codex_workspaces_window_title" = "Espaços de trabalho do Codex"; +"codex_workspaces_workspace_count" = "Espaços de trabalho e chats: %d"; +"codex_workspaces_workspaces_chats" = "Espaços de trabalho/chats"; diff --git a/Sources/CodexBar/Resources/ru.lproj/Localizable.strings b/Sources/CodexBar/Resources/ru.lproj/Localizable.strings index a1015d75f2..b1ec2afd8e 100644 --- a/Sources/CodexBar/Resources/ru.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ru.lproj/Localizable.strings @@ -1351,3 +1351,255 @@ "Cost today unavailable" = "Расход сегодня: Недоступно"; "30-day cost unavailable" = "Расход за 30 дней: Недоступно"; "Resets" = "Сбросы"; + +/* Codex Workspaces and Models */ +"codex_workspaces_open_in_window" = "Открыть в окне"; +"codex_models_actions" = "Действия"; +"codex_models_active_and_new_count" = "Активные модели: %1$d; новый активный: %2$d"; +"codex_models_active_count_new_unavailable" = "Активные модели: %1$d; новый активный счетчик недоступен"; +"codex_models_active_models" = "Активные модели"; +"codex_models_alias_count" = "Псевдонимы: %1$d"; +"codex_models_all_model_count" = "Все модели: %1$d"; +"codex_models_all_models" = "Все модели"; +"codex_models_all_rows" = "Все строки"; +"codex_models_analytics" = "Аналитика моделей"; +"codex_models_analytics_controls" = "Элементы управления аналитикой моделей"; +"codex_models_apply" = "Подать заявку"; +"codex_models_bucket_count" = "Сегменты (%2$@): %1$d"; +"codex_models_bucket_interval" = "Интервал сегмента"; +"codex_models_bullet_value" = "· %1$@"; +"codex_models_canonical_id" = "Канонический идентификатор"; +"codex_models_canonical_model" = "Каноническая модель"; +"codex_models_change" = "Изменить"; +"codex_models_change_from_peak" = "%1$@ от пика %2$@"; +"codex_models_change_from_previous_period" = "%1$@ за предыдущий равный период"; +"codex_models_choose_metric" = "Выберите метрику аналитики"; +"codex_models_clear_search" = "Очистить поиск"; +"codex_models_clear_selection" = "Очистить выбор моделей"; +"codex_models_columns" = "Столбцы"; +"codex_models_compact_billions_suffix" = "B"; +"codex_models_compact_millions_suffix" = "M"; +"codex_models_compact_thousands_suffix" = "K"; +"codex_models_contains_selected_suffix" = ", содержит выбранную модель"; +"codex_models_copy_aliases" = "Копировать псевдонимы"; +"codex_models_copy_canonical_id" = "Копировать канонический идентификатор"; +"codex_models_copy_canonical_model_id" = "Копировать канонический идентификатор модели"; +"codex_models_copy_raw_aliases" = "Копировать необработанные псевдонимы"; +"codex_models_cost_conclusions_limited" = "Выводы о стоимости ограничены токенами с ценой %1$@."; +"codex_models_cost_hidden" = "Стоимость скрыта"; +"codex_models_cost_status_currency" = "%1$@ · %2$@ · %3$@"; +"codex_models_cost_unavailable" = "Стоимость недоступна"; +"codex_models_cost_unavailable_for_data" = "Стоимость для этих данных недоступна."; +"codex_models_coverage" = "Покрытие"; +"codex_models_currency" = "Валюта"; +"codex_models_current_metric" = "Текущий %1$@"; +"codex_models_current_period_in_progress" = "Текущий %1$@ в процессе"; +"codex_models_date" = "Дата"; +"codex_models_day_lowercase" = "день"; +"codex_models_days" = "дней"; +"codex_models_delta_vs_prior" = "Δ по сравнению с предыдущим"; +"codex_models_disabled_in_settings" = "Отключено в настройках"; +"codex_models_dominates_at" = "Эта модель доминирует над %1$@ на %2$@."; +"codex_models_ended" = "Завершено"; +"codex_models_export" = "Экспорт"; +"codex_models_export_all_filename" = "codex-models-all.csv"; +"codex_models_export_default_filename" = "codex-models.csv"; +"codex_models_export_failed" = "Не удалось экспортировать аналитику моделей."; +"codex_models_export_selected_filename" = "codex-model-%1$@.csv"; +"codex_models_export_selected_model" = "Экспорт выбранной модели"; +"codex_models_export_visible_filename" = "codex-models-visible.csv"; +"codex_models_export_visible_rows" = "Экспортировать видимые строки моделей"; +"codex_models_focus_table_search" = "Поиск по таблице фокус-моделей"; +"codex_models_fully_priced" = "Полная цена"; +"codex_models_fully_priced_percent" = "100% цена"; +"codex_models_generated" = "Сгенерировано %1$@"; +"codex_models_granularity_accessibility" = "Детализация, %1$@, %2$@"; +"codex_models_hidden" = "Скрыто"; +"codex_models_identity_aliases_help" = "%1$@ · %2$@ · Псевдонимы: %3$@"; +"codex_models_identity_and_aliases" = "Личность и псевдонимы"; +"codex_models_indexing_last_days" = "Индексирование за последние %1$d дней…"; +"codex_models_interpretation" = "Интерпретация"; +"codex_models_known_cost" = "Известная стоимость"; +"codex_models_known_cost_fully_priced_accessibility" = "Известная стоимость %1$@, полная цена"; +"codex_models_known_cost_lowercase" = "известная стоимость"; +"codex_models_known_cost_partial_accessibility" = "Известная стоимость %1$@, частичная, %2$@ оцененных токенов"; +"codex_models_last_days" = "Последние %1$d дней"; +"codex_models_local_logs_source" = "· Журналы локального Кодекса"; +"codex_models_major_contributor_at" = "Эта модель вносит основной вклад в %1$@."; +"codex_models_metric" = "Метрика"; +"codex_models_metric_over_time" = "%1$@ с течением времени"; +"codex_models_model" = "Модель"; +"codex_models_model_concentration" = "Концентрация модели"; +"codex_models_model_count" = "Модели: %1$d"; +"codex_models_model_usage" = "Использование модели"; +"codex_models_month_lowercase" = "месяц"; +"codex_models_more_alias_count" = "Дополнительные псевдонимы: %1$d"; +"codex_models_name_canonical_id" = "%1$@, канонический идентификатор %2$@"; +"codex_models_name_value_accessibility" = "%1$@, %2$@"; +"codex_models_named_metric_over_time" = "%1$@ %2$@ с течением времени"; +"codex_models_new" = "Новый"; +"codex_models_new_in_period" = "Новое за этот период"; +"codex_models_newly_active_count" = "Недавно активный: %1$d"; +"codex_models_no_applicable_price" = "Нет применимой цены"; +"codex_models_no_change_from_previous_period" = "Никаких изменений по сравнению с предыдущим аналогичным периодом"; +"codex_models_no_matching_models" = "Нет подходящих моделей"; +"codex_models_no_newly_active" = "Новых активных моделей нет."; +"codex_models_no_top_model" = "Нет топ-модели"; +"codex_models_no_usage" = "Не используется"; +"codex_models_no_usage_description" = "Выберите более длинный диапазон дат выше или обновите данные после сохранения локальных сеансов Кодекса."; +"codex_models_no_usage_in_period" = "В этот период не использовалось"; +"codex_models_no_usage_in_range" = "В этом диапазоне модель не используется."; +"codex_models_other_count" = "Другое (%1$d)"; +"codex_models_partial_priced" = "Частично · Цена %1$@"; +"codex_models_precision_table" = "Таблица точности модели"; +"codex_models_previous_known_cost" = "Предыдущая известная стоимость"; +"codex_models_previous_metric" = "Предыдущий %1$@"; +"codex_models_previous_period_unavailable" = "Предыдущий период недоступен"; +"codex_models_previous_unavailable" = "Предыдущий недоступен"; +"codex_models_priced_tokens" = "Ценовые жетоны"; +"codex_models_pricing_calculation" = "Расчет цены"; +"codex_models_pricing_coverage" = "Ценовое покрытие"; +"codex_models_pricing_explanation" = "%1$@ охватывает токены по цене %2$@. Токены %3$@ без применимого правила исключаются и не оцениваются как ноль."; +"codex_models_ranking_by" = "Рейтинг модели %1$@"; +"codex_models_ranking_hint" = "Щелкните панель модели, чтобы открыть сведения о ней."; +"codex_models_ranking_point_accessibility" = "%1$@, %2$@, %3$@ доля"; +"codex_models_ranking_summary" = "Модели Canonical и другие с рейтингом %1$@."; +"codex_models_ref_count" = "%1$@ ссылок"; +"codex_models_refresh_analytics" = "Обновить аналитику моделей"; +"codex_models_refresh_command_help" = "Обновить аналитику моделей (Command-R)"; +"codex_models_refresh_model_usage" = "Обновить использование модели"; +"codex_models_refreshing_last_snapshot" = "Обновление · показ последнего удачного снимка"; +"codex_models_retry" = "Повторить попытку"; +"codex_models_reveal_pricing_calculation" = "Показать расчет цены"; +"codex_models_search_empty_description" = "Поиск фильтрует только эту таблицу; Итоговые данные на приборной панели не изменились."; +"codex_models_search_models" = "Поиск моделей"; +"codex_models_search_scope" = "%1$@ · Поиск фильтрует только эту таблицу"; +"codex_models_search_table" = "Таблица моделей поиска"; +"codex_models_select_model" = "Выберите %1$@"; +"codex_models_selected" = "Выбрано"; +"codex_models_selected_bucket" = "Выбранный сегмент"; +"codex_models_selected_bucket_accessibility" = "Выбранный сегмент: токены %1$@, %2$@, %3$@, ссылки на сеансы %4$@, %5$@, валюта %6$@."; +"codex_models_selected_clear_available" = "Выбранная модель %1$@, доступен очистка выбора"; +"codex_models_selected_date" = "Выбранная дата"; +"codex_models_selected_hidden_by_search" = "Выбранная модель скрыта при поиске"; +"codex_models_selected_model" = "Выбранная модель"; +"codex_models_selected_model_interval" = "Выбранная модель · %1$@"; +"codex_models_selected_suffix" = ", выбрано"; +"codex_models_session_reference_count" = "%1$@ ссылки на сеансы"; +"codex_models_session_references" = "Ссылки на сеансы"; +"codex_models_session_references_lowercase" = "ссылки на сеансы"; +"codex_models_session_refs" = "Ссылки на сеансы"; +"codex_models_share" = "Поделиться"; +"codex_models_share_known_cost_help" = "Доля известной стоимости; исключает бесплатное использование"; +"codex_models_share_of" = "Доля %1$@"; +"codex_models_show_associated_sessions" = "Показать связанные сеансы"; +"codex_models_show_details" = "Показать подробности"; +"codex_models_showing_last_snapshot" = "Показан последний успешный снимок"; +"codex_models_sorted_by" = "Сортировано по %1$@"; +"codex_models_subject_over_time" = "%1$@ с течением времени"; +"codex_models_summary" = "Обзор моделей"; +"codex_models_timeline_accessibility" = "Хронология моделей, %1$@, %2$@"; +"codex_models_timeline_hint" = "Щелкните интервал времени или удерживайте указатель над ним в течение 1,5 секунд, чтобы просмотреть его детали."; +"codex_models_timeline_point_accessibility" = "%1$@, %2$@, %3$@ токенов, %4$@ ссылки на сеанс, %5$@, валюта %6$@"; +"codex_models_timeline_summary" = "%1$@ %2$@ временная шкала. %3$@"; +"codex_models_token_count" = "токенов %1$@"; +"codex_models_tokens" = "Токены"; +"codex_models_tokens_lowercase" = "токенов"; +"codex_models_tokens_per_session_reference" = "Около %1$@ токенов на ссылку на сеанс."; +"codex_models_top" = "Топ"; +"codex_models_top_model" = "Топ-модель"; +"codex_models_top_seven_other" = "Топ 7 + Другое"; +"codex_models_top_two_account_for" = "На долю двух лучших моделей приходится %1$@ из %2$@"; +"codex_models_top_two_of" = "Две лучшие модели %1$@"; +"codex_models_total_session_references_lowercase" = "всего ссылок на сеансы"; +"codex_models_total_tokens" = "Всего токенов"; +"codex_models_total_tokens_lowercase" = "всего токенов"; +"codex_models_totals_reconcile" = "· Итоговые данные сверяются с активным снимком"; +"codex_models_track_end" = "Конец пути"; +"codex_models_track_start" = "Начало трека"; +"codex_models_trend_unavailable_observed_buckets" = "Наблюдаемые сегменты: %1$d. Тренд недоступен."; +"codex_models_unknown_export_error" = "Неизвестная ошибка экспорта"; +"codex_models_unpriced_tokens" = "Токены без цены"; +"codex_models_updated" = "Обновлено %1$@"; +"codex_models_usage_and_change" = "Использование и изменение"; +"codex_models_usage_by_model" = "Использование по модели"; +"codex_models_value_comparison_accessibility" = "%1$@, %2$@"; +"codex_models_value_detail" = "%1$@ · %2$@"; +"codex_models_value_share" = "%1$@ · %2$@ доля"; +"codex_models_value_share_accessibility" = "%1$@, доля %2$@"; +"codex_models_value_start" = "Начало значения"; +"codex_models_visible_of_total" = "Видимые модели: %1$d из %2$d"; +"codex_models_visible_rows" = "Видимые строки"; +"codex_models_vs_previous" = "%1$@ по сравнению с предыдущим"; +"codex_models_week_lowercase" = "неделя"; +"codex_workspaces_all_models" = "Все модели"; +"codex_workspaces_all_workspaces" = "Все рабочие пространства"; +"codex_workspaces_associated_with_selected_model" = "Связано с выбранной моделью"; +"codex_workspaces_bullet_value" = "· %1$@"; +"codex_workspaces_chart_keyboard_hint" = "Выберите день клавишами со стрелками влево и вправо или действиями регулировки VoiceOver. Нажмите Return или Пробел, чтобы закрепить его."; +"codex_workspaces_chats_description" = "Чаты и беседы"; +"codex_workspaces_copy_path" = "Копировать путь"; +"codex_workspaces_cost_unavailable" = "Стоимость недоступна"; +"codex_workspaces_daily_callout_accessibility" = "%1$@, %2$@"; +"codex_workspaces_daily_point_accessibility" = "%1$@, токены %2$@"; +"codex_workspaces_daily_point_help" = "%1$@ — токенов %2$@"; +"codex_workspaces_daily_token_usage" = "Ежедневное использование токенов"; +"codex_workspaces_daily_unavailable" = "Ежедневное использование недоступно"; +"codex_workspaces_daily_unavailable_description" = "Итог рассчитан по %d сеансам, но их даты определить не удалось."; +"codex_workspaces_date" = "Дата"; +"codex_workspaces_days" = "Дни"; +"codex_workspaces_elevated_usage" = "Повышенное использование"; +"codex_workspaces_elevated_usage_help" = "Использование выше медианы за выбранный период."; +"codex_workspaces_estimated_cost_short" = "Оцен. стоимость"; +"codex_workspaces_estimated_cost_value" = "оцен. %@"; +"codex_workspaces_estimated_local_logs" = "Оценка по локальным журналам Codex"; +"codex_workspaces_failed_read_logs" = "Не удалось прочитать журналы Codex"; +"codex_workspaces_failed_some_logs" = "Не удалось прочитать некоторые журналы Codex"; +"codex_workspaces_finalizing_project_index_files" = "Завершение индекса проектов… Файлов: %d"; +"codex_workspaces_high_usage" = "Высокое использование"; +"codex_workspaces_high_usage_help" = "Использование значительно отличается от остальных за выбранный период."; +"codex_workspaces_indexed_files" = "Обновлено %@ назад · Проиндексировано файлов: %d"; +"codex_workspaces_indexing_local_logs" = "Индексирование локальных журналов Codex…"; +"codex_workspaces_indexing_projects" = "Индексирование проектов…"; +"codex_workspaces_indexing_projects_files_left" = "Индексирование проектов… %d/%d файлов, осталось %d"; +"codex_workspaces_last_days" = "Последние %d дней"; +"codex_workspaces_local_codex_chat" = "Локальный чат Codex"; +"codex_workspaces_model" = "Модель"; +"codex_workspaces_models" = "Модели"; +"codex_workspaces_no_daily_usage" = "Нет данных о ежедневном использовании за этот период."; +"codex_workspaces_no_local_usage" = "Локальных данных об использовании Codex пока нет"; +"codex_workspaces_no_models" = "Разбивка по моделям недоступна"; +"codex_workspaces_no_projects_or_chats" = "За этот период проекты и чаты не найдены."; +"codex_workspaces_no_sessions" = "В этом рабочем пространстве нет сеансов"; +"codex_workspaces_normal_usage" = "Обычное использование"; +"codex_workspaces_not_available" = "Недоступно"; +"codex_workspaces_overview" = "Обзор"; +"codex_workspaces_partial_breakdown_help" = "Сводка остается основной, а недоступные метаданные дат или моделей отображаются как частичные."; +"codex_workspaces_partial_breakdown_notice" = "Некоторые подробные разбивки не охватывают итог полностью."; +"codex_workspaces_partial_data" = "Частичные данные"; +"codex_workspaces_percent_total" = "% от итога"; +"codex_workspaces_refresh_usage_data" = "Обновить данные об использовании"; +"codex_workspaces_relative_usage_accessibility" = "%d процентов от наибольшего использования рабочего пространства"; +"codex_workspaces_relative_usage_tooltip" = "Использует %d%% токенов рабочего пространства с наибольшим использованием за этот период."; +"codex_workspaces_reveal_in_finder" = "Показать в Finder"; +"codex_workspaces_saving_project_index" = "Сохранение индекса проектов…"; +"codex_workspaces_scanning_logs" = "Сканирование журналов Codex…"; +"codex_workspaces_search_sessions" = "Поиск сеансов"; +"codex_workspaces_selected_day_accessibility" = "%@: %@ токенов"; +"codex_workspaces_session" = "Сеанс"; +"codex_workspaces_session_count" = "Сеансы: %d"; +"codex_workspaces_sessions" = "Сеансы"; +"codex_workspaces_showing_last_complete_index" = "Показ последнего полного локального индекса"; +"codex_workspaces_stale_status" = "Индекс использования устарел · Обновлено %@ назад"; +"codex_workspaces_started" = "Начат"; +"codex_workspaces_title" = "Рабочие пространства"; +"codex_workspaces_token_count_accessibility" = "%@ токенов"; +"codex_workspaces_tokens" = "Токены"; +"codex_workspaces_top_model" = "Основная модель"; +"codex_workspaces_top_sessions" = "Основные сеансы"; +"codex_workspaces_total" = "Итого"; +"codex_workspaces_view" = "Вид"; +"codex_workspaces_view_all_sessions" = "Показать все сеансы"; +"codex_workspaces_window_title" = "Рабочие пространства Codex"; +"codex_workspaces_workspace_count" = "Рабочие пространства и чаты: %d"; +"codex_workspaces_workspaces_chats" = "Рабочие пространства/чаты"; diff --git a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings index 541451b974..8236ed47ba 100644 --- a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings @@ -1348,3 +1348,255 @@ "Cost today unavailable" = "Kostnad idag: Inte tillgänglig"; "30-day cost unavailable" = "Kostnad 30 dagar: Inte tillgänglig"; "Resets" = "Återställningar"; + +/* Codex Workspaces and Models */ +"codex_workspaces_open_in_window" = "Öppna i fönster"; +"codex_models_actions" = "Åtgärder"; +"codex_models_active_and_new_count" = "Aktiva modeller: %1$d; nyligen aktiv: %2$d"; +"codex_models_active_count_new_unavailable" = "Aktiva modeller: %1$d; antalet nya aktiva är inte tillgängligt"; +"codex_models_active_models" = "Aktiva modeller"; +"codex_models_alias_count" = "Alias: %1$d"; +"codex_models_all_model_count" = "Alla modeller: %1$d"; +"codex_models_all_models" = "Alla modeller"; +"codex_models_all_rows" = "Alla rader"; +"codex_models_analytics" = "Modellanalys"; +"codex_models_analytics_controls" = "Modeller analyskontroller"; +"codex_models_apply" = "Ansök"; +"codex_models_bucket_count" = "Buckets (%2$@): %1$d"; +"codex_models_bucket_interval" = "Hinkintervall"; +"codex_models_bullet_value" = "· %1$@"; +"codex_models_canonical_id" = "Kanoniskt ID"; +"codex_models_canonical_model" = "Kanonisk modell"; +"codex_models_change" = "Ändra"; +"codex_models_change_from_peak" = "%1$@ från %2$@ topp"; +"codex_models_change_from_previous_period" = "%1$@ från föregående lika period"; +"codex_models_choose_metric" = "Välj analysmåttet"; +"codex_models_clear_search" = "Rensa sökning"; +"codex_models_clear_selection" = "Rensa val av modeller"; +"codex_models_columns" = "Kolumner"; +"codex_models_compact_billions_suffix" = "B"; +"codex_models_compact_millions_suffix" = "M"; +"codex_models_compact_thousands_suffix" = "K"; +"codex_models_contains_selected_suffix" = ", innehåller vald modell"; +"codex_models_copy_aliases" = "Kopiera alias"; +"codex_models_copy_canonical_id" = "Kopiera kanoniskt ID"; +"codex_models_copy_canonical_model_id" = "Kopiera kanoniskt modell-ID"; +"codex_models_copy_raw_aliases" = "Kopiera råalias"; +"codex_models_cost_conclusions_limited" = "Kostnadsslutsatser är begränsade till %1$@ prissatta tokens."; +"codex_models_cost_hidden" = "Kostnad dold"; +"codex_models_cost_status_currency" = "%1$@ · %2$@ · %3$@"; +"codex_models_cost_unavailable" = "Kostnaden är inte tillgänglig"; +"codex_models_cost_unavailable_for_data" = "Kostnad är inte tillgänglig för denna data"; +"codex_models_coverage" = "Täckning"; +"codex_models_currency" = "Valuta"; +"codex_models_current_metric" = "Aktuell %1$@"; +"codex_models_current_period_in_progress" = "Aktuell %1$@ pågår"; +"codex_models_date" = "Datum"; +"codex_models_day_lowercase" = "dag"; +"codex_models_days" = "dagar"; +"codex_models_delta_vs_prior" = "Δ jämfört med tidigare"; +"codex_models_disabled_in_settings" = "Inaktiverad i inställningarna"; +"codex_models_dominates_at" = "Den här modellen dominerar %1$@ vid %2$@."; +"codex_models_ended" = "Avslutad"; +"codex_models_export" = "Exportera"; +"codex_models_export_all_filename" = "codex-models-all.csv"; +"codex_models_export_default_filename" = "codex-models.csv"; +"codex_models_export_failed" = "Det gick inte att exportera modellanalyser"; +"codex_models_export_selected_filename" = "codex-model-%1$@.csv"; +"codex_models_export_selected_model" = "Exportera vald modell"; +"codex_models_export_visible_filename" = "codex-models-visible.csv"; +"codex_models_export_visible_rows" = "Exportera synliga modellrader"; +"codex_models_focus_table_search" = "Focus Models tabellsökning"; +"codex_models_fully_priced" = "Fullt prissatt"; +"codex_models_fully_priced_percent" = "100 % prissatt"; +"codex_models_generated" = "Genererade %1$@"; +"codex_models_granularity_accessibility" = "Granularitet, %1$@, %2$@"; +"codex_models_hidden" = "Dold"; +"codex_models_identity_aliases_help" = "%1$@ · %2$@ · Alias: %3$@"; +"codex_models_identity_and_aliases" = "Identitet och alias"; +"codex_models_indexing_last_days" = "Indexerar de senaste %1$d dagarna..."; +"codex_models_interpretation" = "Tolkning"; +"codex_models_known_cost" = "Känd kostnad"; +"codex_models_known_cost_fully_priced_accessibility" = "Känd kostnad %1$@, fullt prissatt"; +"codex_models_known_cost_lowercase" = "känd kostnad"; +"codex_models_known_cost_partial_accessibility" = "Känd kostnad %1$@, partiell, %2$@ av prissatta tokens"; +"codex_models_last_days" = "Senaste %1$d dagarna"; +"codex_models_local_logs_source" = "· Lokala Codex-loggar"; +"codex_models_major_contributor_at" = "Den här modellen är en stor bidragsgivare på %1$@."; +"codex_models_metric" = "Metrisk"; +"codex_models_metric_over_time" = "%1$@ över tid"; +"codex_models_model" = "Modell"; +"codex_models_model_concentration" = "Modellkoncentration"; +"codex_models_model_count" = "Modeller: %1$d"; +"codex_models_model_usage" = "Modellanvändning"; +"codex_models_month_lowercase" = "månad"; +"codex_models_more_alias_count" = "Fler alias: %1$d"; +"codex_models_name_canonical_id" = "%1$@, kanoniskt ID %2$@"; +"codex_models_name_value_accessibility" = "%1$@, %2$@"; +"codex_models_named_metric_over_time" = "%1$@ %2$@ över tid"; +"codex_models_new" = "Nytt"; +"codex_models_new_in_period" = "Nytt under denna period"; +"codex_models_newly_active_count" = "Nyligen aktiv: %1$d"; +"codex_models_no_applicable_price" = "Inget tillämpligt pris"; +"codex_models_no_change_from_previous_period" = "Ingen förändring från föregående lika period"; +"codex_models_no_matching_models" = "Inga matchande modeller"; +"codex_models_no_newly_active" = "Inga nyligen aktiva modeller"; +"codex_models_no_top_model" = "Ingen toppmodell"; +"codex_models_no_usage" = "Ingen användning"; +"codex_models_no_usage_description" = "Välj ett längre datumintervall ovan eller uppdatera efter att lokala Codex-sessioner har sparats."; +"codex_models_no_usage_in_period" = "Ingen användning under denna period"; +"codex_models_no_usage_in_range" = "Ingen modellanvändning i detta sortiment"; +"codex_models_other_count" = "Annat (%1$d)"; +"codex_models_partial_priced" = "Delvis · %1$@ prissatt"; +"codex_models_precision_table" = "Modellprecisionsbord"; +"codex_models_previous_known_cost" = "Tidigare känd kostnad"; +"codex_models_previous_metric" = "Föregående %1$@"; +"codex_models_previous_period_unavailable" = "Föregående period är inte tillgänglig"; +"codex_models_previous_unavailable" = "Föregående ej tillgänglig"; +"codex_models_priced_tokens" = "Prissatta tokens"; +"codex_models_pricing_calculation" = "Prissättningsberäkning"; +"codex_models_pricing_coverage" = "Pristäckning"; +"codex_models_pricing_explanation" = "%1$@ täcker %2$@ prissatta tokens. %3$@ tokens utan tillämplig regel exkluderas, inte uppskattas till noll."; +"codex_models_ranking_by" = "Modellrankning efter %1$@"; +"codex_models_ranking_hint" = "Klicka på en modellrad för att öppna dess detaljer."; +"codex_models_ranking_point_accessibility" = "%1$@, %2$@, %3$@ aktie"; +"codex_models_ranking_summary" = "Kanoniska modeller och övrigt rankade efter %1$@."; +"codex_models_ref_count" = "%1$@ refs"; +"codex_models_refresh_analytics" = "Uppdatera modellanalyser"; +"codex_models_refresh_command_help" = "Uppdatera modellanalys (Command-R)"; +"codex_models_refresh_model_usage" = "Uppdatera modellanvändning"; +"codex_models_refreshing_last_snapshot" = "Uppdaterar · visar senaste lyckade ögonblicksbild"; +"codex_models_retry" = "Försök igen"; +"codex_models_reveal_pricing_calculation" = "Visa prisberäkning"; +"codex_models_search_empty_description" = "Sök filtrerar endast den här tabellen; instrumentpanelsummorna är oförändrade."; +"codex_models_search_models" = "Sök modeller"; +"codex_models_search_scope" = "%1$@ · Sök filtrerar endast den här tabellen"; +"codex_models_search_table" = "Sök modelltabell"; +"codex_models_select_model" = "Välj %1$@"; +"codex_models_selected" = "Vald"; +"codex_models_selected_bucket" = "Vald hink"; +"codex_models_selected_bucket_accessibility" = "Vald bucket: %1$@, %2$@, %3$@ tokens, %4$@ sessionsreferenser, %5$@, valuta %6$@."; +"codex_models_selected_clear_available" = "Vald modell %1$@, Rensa val tillgängligt"; +"codex_models_selected_date" = "Valt datum"; +"codex_models_selected_hidden_by_search" = "Vald modell dold av sökning"; +"codex_models_selected_model" = "Vald modell"; +"codex_models_selected_model_interval" = "Vald modell · %1$@"; +"codex_models_selected_suffix" = ", vald"; +"codex_models_session_reference_count" = "%1$@ sessionsreferenser"; +"codex_models_session_references" = "Sessionsreferenser"; +"codex_models_session_references_lowercase" = "sessionsreferenser"; +"codex_models_session_refs" = "Sessionsref"; +"codex_models_share" = "Dela"; +"codex_models_share_known_cost_help" = "Andel av känd kostnad; utesluter oprissatt användning"; +"codex_models_share_of" = "Andel av %1$@"; +"codex_models_show_associated_sessions" = "Visa associerade sessioner"; +"codex_models_show_details" = "Visa detaljer"; +"codex_models_showing_last_snapshot" = "Visar den senaste lyckade ögonblicksbilden"; +"codex_models_sorted_by" = "Sorterat efter %1$@"; +"codex_models_subject_over_time" = "%1$@ över tid"; +"codex_models_summary" = "Sammanfattning av modeller"; +"codex_models_timeline_accessibility" = "Modellers tidslinje, %1$@, %2$@"; +"codex_models_timeline_hint" = "Klicka på en tidsperiod eller håll pekaren över en i 1,5 sekunder för att inspektera dess detaljer."; +"codex_models_timeline_point_accessibility" = "%1$@, %2$@, %3$@ tokens, %4$@ sessionsreferenser, %5$@, valuta %6$@"; +"codex_models_timeline_summary" = "%1$@ %2$@ tidslinje. %3$@"; +"codex_models_token_count" = "%1$@ tokens"; +"codex_models_tokens" = "Tokens"; +"codex_models_tokens_lowercase" = "tokens"; +"codex_models_tokens_per_session_reference" = "Cirka %1$@ tokens per sessionsreferens."; +"codex_models_top" = "Topp"; +"codex_models_top_model" = "Toppmodell"; +"codex_models_top_seven_other" = "Topp 7 + Övrigt"; +"codex_models_top_two_account_for" = "De två bästa modellerna står för %1$@ av %2$@"; +"codex_models_top_two_of" = "De två bästa modellerna av %1$@"; +"codex_models_total_session_references_lowercase" = "totalt sessionsreferenser"; +"codex_models_total_tokens" = "Totalt antal tokens"; +"codex_models_total_tokens_lowercase" = "totalt tokens"; +"codex_models_totals_reconcile" = "· Summor överensstämmer med den aktiva ögonblicksbilden"; +"codex_models_track_end" = "Spårslut"; +"codex_models_track_start" = "Spårstart"; +"codex_models_trend_unavailable_observed_buckets" = "Observerade segment: %1$d. Trend inte tillgänglig."; +"codex_models_unknown_export_error" = "Okänt exportfel"; +"codex_models_unpriced_tokens" = "Oprisade tokens"; +"codex_models_updated" = "Uppdaterad %1$@"; +"codex_models_usage_and_change" = "Användning och förändring"; +"codex_models_usage_by_model" = "Användning per modell"; +"codex_models_value_comparison_accessibility" = "%1$@, %2$@"; +"codex_models_value_detail" = "%1$@ · %2$@"; +"codex_models_value_share" = "%1$@ · %2$@ aktie"; +"codex_models_value_share_accessibility" = "%1$@, %2$@ aktie"; +"codex_models_value_start" = "Värdestart"; +"codex_models_visible_of_total" = "Synliga modeller: %1$d av %2$d"; +"codex_models_visible_rows" = "Synliga rader"; +"codex_models_vs_previous" = "%1$@ jämfört med föregående"; +"codex_models_week_lowercase" = "vecka"; +"codex_workspaces_all_models" = "Alla modeller"; +"codex_workspaces_all_workspaces" = "Alla arbetsytor"; +"codex_workspaces_associated_with_selected_model" = "Associerad med vald modell"; +"codex_workspaces_bullet_value" = "· %1$@"; +"codex_workspaces_chart_keyboard_hint" = "Välj en dag med vänster och höger piltangent eller VoiceOver-justeringsåtgärder. Tryck på Retur eller Mellanslag för att fästa den."; +"codex_workspaces_chats_description" = "Chattar och konversationer"; +"codex_workspaces_copy_path" = "Kopiera sökväg"; +"codex_workspaces_cost_unavailable" = "Kostnaden är inte tillgänglig"; +"codex_workspaces_daily_callout_accessibility" = "%1$@, %2$@"; +"codex_workspaces_daily_point_accessibility" = "%1$@, tokens %2$@"; +"codex_workspaces_daily_point_help" = "%1$@ — %2$@ tokens"; +"codex_workspaces_daily_token_usage" = "Daglig tokenanvändning"; +"codex_workspaces_daily_unavailable" = "Daglig användning är inte tillgänglig"; +"codex_workspaces_daily_unavailable_description" = "Totalsumman beräknades från %d sessioner, men deras datum kunde inte fastställas."; +"codex_workspaces_date" = "Datum"; +"codex_workspaces_days" = "Dagar"; +"codex_workspaces_elevated_usage" = "Förhöjd användning"; +"codex_workspaces_elevated_usage_help" = "Användningen ligger över medianen för den valda perioden."; +"codex_workspaces_estimated_cost_short" = "Ber. kostnad"; +"codex_workspaces_estimated_cost_value" = "ber. %@"; +"codex_workspaces_estimated_local_logs" = "Beräknat från lokala Codex-loggar"; +"codex_workspaces_failed_read_logs" = "Det gick inte att läsa Codex-loggar"; +"codex_workspaces_failed_some_logs" = "Det gick inte att läsa vissa Codex-loggar"; +"codex_workspaces_finalizing_project_index_files" = "Slutför projektindex… %d filer"; +"codex_workspaces_high_usage" = "Hög användning"; +"codex_workspaces_high_usage_help" = "Användningen avviker markant under den valda perioden."; +"codex_workspaces_indexed_files" = "Uppdaterad för %@ sedan · %d indexerade filer"; +"codex_workspaces_indexing_local_logs" = "Indexerar lokala Codex-loggar…"; +"codex_workspaces_indexing_projects" = "Indexerar projekt…"; +"codex_workspaces_indexing_projects_files_left" = "Indexerar projekt… %d/%d filer, %d återstår"; +"codex_workspaces_last_days" = "Senaste %d dagarna"; +"codex_workspaces_local_codex_chat" = "Lokal Codex-chatt"; +"codex_workspaces_model" = "Modell"; +"codex_workspaces_models" = "Modeller"; +"codex_workspaces_no_daily_usage" = "Ingen daglig användning i det här historikintervallet."; +"codex_workspaces_no_local_usage" = "Ingen lokal Codex-användning ännu"; +"codex_workspaces_no_models" = "Ingen modelluppdelning tillgänglig"; +"codex_workspaces_no_projects_or_chats" = "Inga projekt eller chattar hittades i det här historikintervallet."; +"codex_workspaces_no_sessions" = "Inga sessioner i den här arbetsytan"; +"codex_workspaces_normal_usage" = "Normal användning"; +"codex_workspaces_not_available" = "Ej tillgängligt"; +"codex_workspaces_overview" = "Översikt"; +"codex_workspaces_partial_breakdown_help" = "Sammanfattningen är fortsatt styrande medan otillgängliga datum- eller modellmetadata visas som partiella."; +"codex_workspaces_partial_breakdown_notice" = "Vissa detaljerade uppdelningar täcker inte hela totalsumman."; +"codex_workspaces_partial_data" = "Partiella data"; +"codex_workspaces_percent_total" = "% av totalt"; +"codex_workspaces_refresh_usage_data" = "Uppdatera användningsdata"; +"codex_workspaces_relative_usage_accessibility" = "%d procent av den högsta arbetsyteanvändningen"; +"codex_workspaces_relative_usage_tooltip" = "Använder %d%% av antalet token i arbetsytan med högst användning under perioden."; +"codex_workspaces_reveal_in_finder" = "Visa i Finder"; +"codex_workspaces_saving_project_index" = "Sparar projektindex…"; +"codex_workspaces_scanning_logs" = "Söker igenom Codex-loggar…"; +"codex_workspaces_search_sessions" = "Sök sessioner"; +"codex_workspaces_selected_day_accessibility" = "%@: %@ token"; +"codex_workspaces_session" = "Session"; +"codex_workspaces_session_count" = "Sessioner: %d"; +"codex_workspaces_sessions" = "Sessioner"; +"codex_workspaces_showing_last_complete_index" = "Visar det senaste fullständiga lokala indexet"; +"codex_workspaces_stale_status" = "Användningsindexet är inaktuellt · Uppdaterat för %@ sedan"; +"codex_workspaces_started" = "Startad"; +"codex_workspaces_title" = "Arbetsytor"; +"codex_workspaces_token_count_accessibility" = "%@ token"; +"codex_workspaces_tokens" = "Token"; +"codex_workspaces_top_model" = "Främsta modell"; +"codex_workspaces_top_sessions" = "Främsta sessioner"; +"codex_workspaces_total" = "Totalt"; +"codex_workspaces_view" = "Vy"; +"codex_workspaces_view_all_sessions" = "Visa alla sessioner"; +"codex_workspaces_window_title" = "Codex-arbetsytor"; +"codex_workspaces_workspace_count" = "Arbetsytor och chattar: %d"; +"codex_workspaces_workspaces_chats" = "Arbetsytor/chattar"; diff --git a/Sources/CodexBar/Resources/th.lproj/Localizable.strings b/Sources/CodexBar/Resources/th.lproj/Localizable.strings index 4e0f2ff782..256a8d8c80 100644 --- a/Sources/CodexBar/Resources/th.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/th.lproj/Localizable.strings @@ -1353,3 +1353,255 @@ "Cost today unavailable" = "ค่าใช้จ่ายวันนี้: ไม่พร้อมใช้งาน"; "30-day cost unavailable" = "ค่าใช้จ่าย 30 วัน: ไม่พร้อมใช้งาน"; "Resets" = "การรีเซ็ต"; + +/* Codex Workspaces and Models */ +"codex_workspaces_open_in_window" = "เปิดในหน้าต่าง"; +"codex_models_actions" = "การกระทำ"; +"codex_models_active_and_new_count" = "โมเดลที่ใช้งานอยู่: %1$d; เพิ่งใช้งาน: %2$d"; +"codex_models_active_count_new_unavailable" = "โมเดลที่ใช้งานอยู่: %1$d; จำนวนที่ใช้งานใหม่ไม่พร้อมใช้งาน"; +"codex_models_active_models" = "โมเดลที่ใช้งานอยู่"; +"codex_models_alias_count" = "นามแฝง: %1$d"; +"codex_models_all_model_count" = "ทุกรุ่น: %1$d"; +"codex_models_all_models" = "ทุกรุ่น"; +"codex_models_all_rows" = "ทุกแถว"; +"codex_models_analytics" = "การวิเคราะห์โมเดล"; +"codex_models_analytics_controls" = "การควบคุมการวิเคราะห์โมเดล"; +"codex_models_apply" = "สมัคร"; +"codex_models_bucket_count" = "ที่เก็บข้อมูล (%2$@): %1$d"; +"codex_models_bucket_interval" = "ช่วงเวลาฝากข้อมูล"; +"codex_models_bullet_value" = "· %1$@"; +"codex_models_canonical_id" = "รหัสมาตรฐาน"; +"codex_models_canonical_model" = "โมเดล Canonical"; +"codex_models_change" = "การเปลี่ยนแปลง"; +"codex_models_change_from_peak" = "%1$@ จาก %2$@ จุดสูงสุด"; +"codex_models_change_from_previous_period" = "%1$@ จากช่วงเวลาเท่ากันก่อนหน้า"; +"codex_models_choose_metric" = "เลือกเมตริกการวิเคราะห์"; +"codex_models_clear_search" = "ล้างการค้นหา"; +"codex_models_clear_selection" = "ล้างการเลือกโมเดล"; +"codex_models_columns" = "คอลัมน์"; +"codex_models_compact_billions_suffix" = "B"; +"codex_models_compact_millions_suffix" = "M"; +"codex_models_compact_thousands_suffix" = "K"; +"codex_models_contains_selected_suffix" = "มีรุ่นที่เลือก"; +"codex_models_copy_aliases" = "คัดลอกนามแฝง"; +"codex_models_copy_canonical_id" = "คัดลอก ID ตามรูปแบบบัญญัติ"; +"codex_models_copy_canonical_model_id" = "คัดลอกรหัสโมเดลตามรูปแบบบัญญัติ"; +"codex_models_copy_raw_aliases" = "คัดลอกนามแฝงดิบ"; +"codex_models_cost_conclusions_limited" = "การสรุปต้นทุนถูกจำกัดไว้ที่ %1$@ โทเค็นราคา"; +"codex_models_cost_hidden" = "ซ่อนต้นทุนไว้"; +"codex_models_cost_status_currency" = "%1$@ · %2$@ · %3$@"; +"codex_models_cost_unavailable" = "ไม่มีต้นทุน"; +"codex_models_cost_unavailable_for_data" = "ไม่มีต้นทุนสำหรับข้อมูลนี้"; +"codex_models_coverage" = "ความคุ้มครอง"; +"codex_models_currency" = "สกุลเงิน"; +"codex_models_current_metric" = "ปัจจุบัน %1$@"; +"codex_models_current_period_in_progress" = "กำลังดำเนินการ %1$@ ปัจจุบัน"; +"codex_models_date" = "วันที่"; +"codex_models_day_lowercase" = "วัน"; +"codex_models_days" = "วัน"; +"codex_models_delta_vs_prior" = "Δเทียบกับก่อนหน้า"; +"codex_models_disabled_in_settings" = "ปิดการใช้งานในการตั้งค่า"; +"codex_models_dominates_at" = "โมเดลนี้ครอง %1$@ ที่ %2$@"; +"codex_models_ended" = "สิ้นสุดแล้ว"; +"codex_models_export" = "ส่งออก"; +"codex_models_export_all_filename" = "codex-models-all.csv"; +"codex_models_export_default_filename" = "codex-models.csv"; +"codex_models_export_failed" = "ไม่สามารถส่งออกการวิเคราะห์โมเดลได้"; +"codex_models_export_selected_filename" = "codex-model-%1$@.csv"; +"codex_models_export_selected_model" = "ส่งออกโมเดลที่เลือก"; +"codex_models_export_visible_filename" = "codex-models-visible.csv"; +"codex_models_export_visible_rows" = "ส่งออกแถวโมเดลที่มองเห็นได้"; +"codex_models_focus_table_search" = "ค้นหาตารางโฟกัสโมเดล"; +"codex_models_fully_priced" = "ราคาเต็ม"; +"codex_models_fully_priced_percent" = "ราคา 100%"; +"codex_models_generated" = "สร้างแล้ว %1$@"; +"codex_models_granularity_accessibility" = "รายละเอียด, %1$@, %2$@"; +"codex_models_hidden" = "ซ่อนอยู่"; +"codex_models_identity_aliases_help" = "%1$@ · %2$@ · นามแฝง: %3$@"; +"codex_models_identity_and_aliases" = "ตัวตนและนามแฝง"; +"codex_models_indexing_last_days" = "กำลังจัดทำดัชนี %1$d วันที่ผ่านมา..."; +"codex_models_interpretation" = "การตีความ"; +"codex_models_known_cost" = "ต้นทุนที่ทราบ"; +"codex_models_known_cost_fully_priced_accessibility" = "ราคาที่ทราบ %1$@ ราคาเต็ม"; +"codex_models_known_cost_lowercase" = "ค่าใช้จ่ายที่ทราบ"; +"codex_models_known_cost_partial_accessibility" = "ต้นทุนที่ทราบ %1$@ บางส่วน %2$@ ของราคาโทเค็น"; +"codex_models_last_days" = "%1$d วันที่ผ่านมา"; +"codex_models_local_logs_source" = "· บันทึก Codex ท้องถิ่น"; +"codex_models_major_contributor_at" = "โมเดลนี้เป็นผู้สนับสนุนหลักที่ %1$@"; +"codex_models_metric" = "เมตริก"; +"codex_models_metric_over_time" = "%1$@ เมื่อเวลาผ่านไป"; +"codex_models_model" = "นางแบบ"; +"codex_models_model_concentration" = "โมเดลมีสมาธิ"; +"codex_models_model_count" = "โมเดล: %1$d"; +"codex_models_model_usage" = "การใช้งานโมเดล"; +"codex_models_month_lowercase" = "เดือน"; +"codex_models_more_alias_count" = "นามแฝงเพิ่มเติม: %1$d"; +"codex_models_name_canonical_id" = "%1$@, รหัสมาตรฐาน %2$@"; +"codex_models_name_value_accessibility" = "%1$@, %2$@"; +"codex_models_named_metric_over_time" = "%1$@ %2$@ เมื่อเวลาผ่านไป"; +"codex_models_new" = "ใหม่"; +"codex_models_new_in_period" = "ใหม่ช่วงนี้"; +"codex_models_newly_active_count" = "เพิ่งเปิดใช้งาน: %1$d"; +"codex_models_no_applicable_price" = "ไม่มีราคาที่เกี่ยวข้อง"; +"codex_models_no_change_from_previous_period" = "ไม่มีการเปลี่ยนแปลงจากระยะเวลาเท่ากันก่อนหน้า"; +"codex_models_no_matching_models" = "ไม่มีรุ่นที่ตรงกัน"; +"codex_models_no_newly_active" = "ไม่มีโมเดลที่ใช้งานใหม่"; +"codex_models_no_top_model" = "ไม่มีรุ่นท็อป"; +"codex_models_no_usage" = "ไม่มีการใช้งาน"; +"codex_models_no_usage_description" = "เลือกช่วงวันที่ที่ยาวขึ้นด้านบนหรือรีเฟรชหลังจากบันทึกเซสชัน Codex ในเครื่องแล้ว"; +"codex_models_no_usage_in_period" = "ไม่มีการใช้งานในช่วงนี้"; +"codex_models_no_usage_in_range" = "ไม่มีการใช้งานโมเดลในช่วงนี้"; +"codex_models_other_count" = "อื่นๆ (%1$d)"; +"codex_models_partial_priced" = "บางส่วน · %1$@ ราคา"; +"codex_models_precision_table" = "ตารางความแม่นยำของโมเดล"; +"codex_models_previous_known_cost" = "ต้นทุนที่ทราบก่อนหน้านี้"; +"codex_models_previous_metric" = "ก่อนหน้า %1$@"; +"codex_models_previous_period_unavailable" = "ช่วงเวลาก่อนหน้าไม่พร้อมใช้งาน"; +"codex_models_previous_unavailable" = "ก่อนหน้าไม่พร้อมใช้งาน"; +"codex_models_priced_tokens" = "โทเค็นราคา"; +"codex_models_pricing_calculation" = "การคำนวณราคา"; +"codex_models_pricing_coverage" = "ความครอบคลุมของราคา"; +"codex_models_pricing_explanation" = "%1$@ ครอบคลุมโทเค็นราคา %2$@ โทเค็น %3$@ ที่ไม่มีกฎที่เกี่ยวข้องจะถูกแยกออก ไม่ใช่ประมาณว่าเป็นศูนย์"; +"codex_models_ranking_by" = "จัดอันดับโมเดลโดย %1$@"; +"codex_models_ranking_hint" = "คลิกแถบโมเดลเพื่อเปิดรายละเอียด"; +"codex_models_ranking_point_accessibility" = "%1$@, %2$@, %3$@ แบ่งปัน"; +"codex_models_ranking_summary" = "โมเดล Canonical และอื่นๆ จัดอันดับโดย %1$@"; +"codex_models_ref_count" = "%1$@ ผู้อ้างอิง"; +"codex_models_refresh_analytics" = "รีเฟรชการวิเคราะห์โมเดล"; +"codex_models_refresh_command_help" = "รีเฟรชการวิเคราะห์โมเดล (Command-R)"; +"codex_models_refresh_model_usage" = "รีเฟรชการใช้งานโมเดล"; +"codex_models_refreshing_last_snapshot" = "กำลังรีเฟรช · แสดงสแนปช็อตที่สำเร็จครั้งล่าสุด"; +"codex_models_retry" = "ลองอีกครั้ง"; +"codex_models_reveal_pricing_calculation" = "เผยการคำนวณราคา"; +"codex_models_search_empty_description" = "กรองการค้นหาเฉพาะตารางนี้เท่านั้น ผลรวมแดชบอร์ดไม่เปลี่ยนแปลง"; +"codex_models_search_models" = "ค้นหาโมเดล"; +"codex_models_search_scope" = "%1$@ · ค้นหากรองตารางนี้เท่านั้น"; +"codex_models_search_table" = "ค้นหาตารางโมเดล"; +"codex_models_select_model" = "เลือก %1$@"; +"codex_models_selected" = "เลือกแล้ว"; +"codex_models_selected_bucket" = "ที่เก็บข้อมูลที่เลือก"; +"codex_models_selected_bucket_accessibility" = "ที่เก็บข้อมูลที่เลือก: %1$@, %2$@, โทเค็น %3$@, การอ้างอิงเซสชัน %4$@, %5$@, สกุลเงิน %6$@"; +"codex_models_selected_clear_available" = "เลือกรุ่น %1$@ ยกเลิกการเลือกแล้ว"; +"codex_models_selected_date" = "วันที่เลือก"; +"codex_models_selected_hidden_by_search" = "โมเดลที่เลือกถูกซ่อนโดยการค้นหา"; +"codex_models_selected_model" = "รุ่นที่เลือก"; +"codex_models_selected_model_interval" = "โมเดลที่เลือก · %1$@"; +"codex_models_selected_suffix" = "เลือกแล้ว"; +"codex_models_session_reference_count" = "%1$@ การอ้างอิงเซสชัน"; +"codex_models_session_references" = "การอ้างอิงเซสชัน"; +"codex_models_session_references_lowercase" = "การอ้างอิงเซสชัน"; +"codex_models_session_refs" = "การอ้างอิงเซสชัน"; +"codex_models_share" = "แบ่งปัน"; +"codex_models_share_known_cost_help" = "ส่วนแบ่งของต้นทุนที่ทราบ ไม่รวมการใช้งานที่ไม่มีราคา"; +"codex_models_share_of" = "ส่วนแบ่งของ %1$@"; +"codex_models_show_associated_sessions" = "แสดงเซสชันที่เกี่ยวข้อง"; +"codex_models_show_details" = "แสดงรายละเอียด"; +"codex_models_showing_last_snapshot" = "กำลังแสดงสแนปช็อตที่สำเร็จครั้งล่าสุด"; +"codex_models_sorted_by" = "จัดเรียงตาม %1$@"; +"codex_models_subject_over_time" = "%1$@ เมื่อเวลาผ่านไป"; +"codex_models_summary" = "สรุปโมเดล"; +"codex_models_timeline_accessibility" = "ไทม์ไลน์ของโมเดล %1$@, %2$@"; +"codex_models_timeline_hint" = "คลิกช่วงเวลาหรือวางตัวชี้ไว้เหนือหนึ่งเป็นเวลา 1.5 วินาทีเพื่อตรวจสอบรายละเอียด"; +"codex_models_timeline_point_accessibility" = "%1$@, %2$@, %3$@ โทเค็น, %4$@ การอ้างอิงเซสชัน, %5$@, สกุลเงิน %6$@"; +"codex_models_timeline_summary" = "%1$@ %2$@ ไทม์ไลน์ %3$@"; +"codex_models_token_count" = "%1$@ โทเค็น"; +"codex_models_tokens" = "โทเค็น"; +"codex_models_tokens_lowercase" = "โทเค็น"; +"codex_models_tokens_per_session_reference" = "ประมาณ %1$@ โทเค็นต่อการอ้างอิงเซสชัน"; +"codex_models_top" = "ท็อป"; +"codex_models_top_model" = "รุ่นท็อป"; +"codex_models_top_seven_other" = "อันดับ 7 + อื่นๆ"; +"codex_models_top_two_account_for" = "โมเดลสองอันดับแรกคิดเป็น %1$@ จาก %2$@"; +"codex_models_top_two_of" = "สองโมเดลยอดนิยมของ %1$@"; +"codex_models_total_session_references_lowercase" = "การอ้างอิงเซสชันทั้งหมด"; +"codex_models_total_tokens" = "โทเค็นทั้งหมด"; +"codex_models_total_tokens_lowercase" = "โทเค็นทั้งหมด"; +"codex_models_totals_reconcile" = "· ผลรวมกระทบยอดกับสแน็ปช็อตที่ใช้งานอยู่"; +"codex_models_track_end" = "สิ้นสุดแทร็ก"; +"codex_models_track_start" = "เริ่มติดตาม"; +"codex_models_trend_unavailable_observed_buckets" = "ที่เก็บข้อมูลที่สังเกตได้: %1$d เทรนด์ไม่พร้อมใช้งาน"; +"codex_models_unknown_export_error" = "ข้อผิดพลาดในการส่งออกที่ไม่รู้จัก"; +"codex_models_unpriced_tokens" = "โทเค็นที่ไม่มีราคา"; +"codex_models_updated" = "อัปเดต %1$@"; +"codex_models_usage_and_change" = "การใช้งานและการเปลี่ยนแปลง"; +"codex_models_usage_by_model" = "การใช้งานตามรุ่น"; +"codex_models_value_comparison_accessibility" = "%1$@, %2$@"; +"codex_models_value_detail" = "%1$@ · %2$@"; +"codex_models_value_share" = "%1$@ · %2$@ แบ่งปัน"; +"codex_models_value_share_accessibility" = "%1$@, %2$@ แบ่งปัน"; +"codex_models_value_start" = "มูลค่าเริ่มต้น"; +"codex_models_visible_of_total" = "โมเดลที่มองเห็นได้: %1$d จาก %2$d"; +"codex_models_visible_rows" = "แถวที่มองเห็นได้"; +"codex_models_vs_previous" = "%1$@ เทียบกับก่อนหน้า"; +"codex_models_week_lowercase" = "สัปดาห์"; +"codex_workspaces_all_models" = "โมเดลทั้งหมด"; +"codex_workspaces_all_workspaces" = "พื้นที่ทำงานทั้งหมด"; +"codex_workspaces_associated_with_selected_model" = "เชื่อมโยงกับรุ่นที่เลือก"; +"codex_workspaces_bullet_value" = "· %1$@"; +"codex_workspaces_chart_keyboard_hint" = "ใช้ปุ่มลูกศรซ้ายและขวาหรือการปรับของ VoiceOver เพื่อเลือกวัน กด Return หรือ Space เพื่อปักหมุด"; +"codex_workspaces_chats_description" = "แชทและการสนทนา"; +"codex_workspaces_copy_path" = "คัดลอกเส้นทาง"; +"codex_workspaces_cost_unavailable" = "ไม่มีข้อมูลค่าใช้จ่าย"; +"codex_workspaces_daily_callout_accessibility" = "%1$@, %2$@"; +"codex_workspaces_daily_point_accessibility" = "%1$@, โทเค็น %2$@"; +"codex_workspaces_daily_point_help" = "%1$@ — %2$@ โทเค็น"; +"codex_workspaces_daily_token_usage" = "การใช้งานโทเค็นรายวัน"; +"codex_workspaces_daily_unavailable" = "ไม่มีข้อมูลการใช้งานรายวัน"; +"codex_workspaces_daily_unavailable_description" = "คำนวณยอดรวมจาก %d เซสชัน แต่ไม่สามารถระบุวันที่ได้"; +"codex_workspaces_date" = "วันที่"; +"codex_workspaces_days" = "วัน"; +"codex_workspaces_elevated_usage" = "การใช้งานสูงกว่าปกติ"; +"codex_workspaces_elevated_usage_help" = "การใช้งานสูงกว่าค่ามัธยฐานในช่วงเวลาที่เลือก"; +"codex_workspaces_estimated_cost_short" = "ค่าใช้จ่ายโดยประมาณ"; +"codex_workspaces_estimated_cost_value" = "ประมาณ %@"; +"codex_workspaces_estimated_local_logs" = "ประมาณจากบันทึก Codex ในเครื่อง"; +"codex_workspaces_failed_read_logs" = "อ่านบันทึก Codex ไม่สำเร็จ"; +"codex_workspaces_failed_some_logs" = "อ่านบันทึก Codex บางรายการไม่สำเร็จ"; +"codex_workspaces_finalizing_project_index_files" = "กำลังดำเนินการดัชนีโปรเจ็กต์ให้เสร็จ… %d ไฟล์"; +"codex_workspaces_high_usage" = "การใช้งานสูง"; +"codex_workspaces_high_usage_help" = "การใช้งานผิดปกติอย่างมากในช่วงเวลาที่เลือก"; +"codex_workspaces_indexed_files" = "อัปเดตเมื่อ %@ ที่แล้ว · สร้างดัชนีแล้ว %d ไฟล์"; +"codex_workspaces_indexing_local_logs" = "กำลังสร้างดัชนีบันทึก Codex ในเครื่อง…"; +"codex_workspaces_indexing_projects" = "กำลังสร้างดัชนีโปรเจ็กต์…"; +"codex_workspaces_indexing_projects_files_left" = "กำลังสร้างดัชนีโปรเจ็กต์… %d/%d ไฟล์ เหลือ %d"; +"codex_workspaces_last_days" = "%d วันที่ผ่านมา"; +"codex_workspaces_local_codex_chat" = "แชท Codex ในเครื่อง"; +"codex_workspaces_model" = "โมเดล"; +"codex_workspaces_models" = "โมเดล"; +"codex_workspaces_no_daily_usage" = "ไม่มีข้อมูลการใช้งานรายวันในช่วงประวัตินี้"; +"codex_workspaces_no_local_usage" = "ยังไม่มีข้อมูลการใช้งาน Codex ในเครื่อง"; +"codex_workspaces_no_models" = "ไม่มีรายละเอียดโมเดล"; +"codex_workspaces_no_projects_or_chats" = "ไม่พบโปรเจ็กต์หรือแชทในช่วงประวัตินี้"; +"codex_workspaces_no_sessions" = "ไม่มีเซสชันในพื้นที่ทำงานนี้"; +"codex_workspaces_normal_usage" = "การใช้งานปกติ"; +"codex_workspaces_not_available" = "ไม่ว่าง"; +"codex_workspaces_overview" = "ภาพรวม"; +"codex_workspaces_partial_breakdown_help" = "สรุปยังคงเป็นข้อมูลหลัก ส่วนข้อมูลเมตาของวันที่หรือโมเดลที่ไม่มีจะแสดงเป็นบางส่วน"; +"codex_workspaces_partial_breakdown_notice" = "รายละเอียดบางส่วนไม่ครอบคลุมยอดรวมทั้งหมด"; +"codex_workspaces_partial_data" = "ข้อมูลบางส่วน"; +"codex_workspaces_percent_total" = "เปอร์เซ็นต์ของยอดรวม"; +"codex_workspaces_refresh_usage_data" = "รีเฟรชข้อมูลการใช้งาน"; +"codex_workspaces_relative_usage_accessibility" = "%d เปอร์เซ็นต์ของการใช้งานพื้นที่ทำงานสูงสุด"; +"codex_workspaces_relative_usage_tooltip" = "ใช้โทเค็น %d%% ของพื้นที่ทำงานที่มีการใช้งานสูงสุดในช่วงเวลานี้"; +"codex_workspaces_reveal_in_finder" = "แสดงใน Finder"; +"codex_workspaces_saving_project_index" = "กำลังบันทึกดัชนีโปรเจ็กต์…"; +"codex_workspaces_scanning_logs" = "กำลังสแกนบันทึก Codex…"; +"codex_workspaces_search_sessions" = "ค้นหาเซสชัน"; +"codex_workspaces_selected_day_accessibility" = "%@: %@ โทเค็น"; +"codex_workspaces_session" = "เซสชัน"; +"codex_workspaces_session_count" = "เซสชัน: %d"; +"codex_workspaces_sessions" = "เซสชัน"; +"codex_workspaces_showing_last_complete_index" = "กำลังแสดงดัชนีในเครื่องล่าสุดที่เสร็จสมบูรณ์"; +"codex_workspaces_stale_status" = "ดัชนีการใช้งานเก่าแล้ว · อัปเดตเมื่อ %@ ที่แล้ว"; +"codex_workspaces_started" = "เริ่มเมื่อ"; +"codex_workspaces_title" = "พื้นที่ทำงาน"; +"codex_workspaces_token_count_accessibility" = "%@ โทเค็น"; +"codex_workspaces_tokens" = "โทเค็น"; +"codex_workspaces_top_model" = "โมเดลยอดนิยม"; +"codex_workspaces_top_sessions" = "เซสชันยอดนิยม"; +"codex_workspaces_total" = "รวม"; +"codex_workspaces_view" = "มุมมอง"; +"codex_workspaces_view_all_sessions" = "ดูเซสชันทั้งหมด"; +"codex_workspaces_window_title" = "พื้นที่ทำงาน Codex"; +"codex_workspaces_workspace_count" = "พื้นที่ทำงานและแชท: %d"; +"codex_workspaces_workspaces_chats" = "พื้นที่ทำงาน/แชท"; diff --git a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings index 48a5096e45..2c8cf60ec0 100644 --- a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings @@ -1351,3 +1351,255 @@ "Cost today unavailable" = "Bugünkü maliyet: Kullanılamıyor"; "30-day cost unavailable" = "30 günlük maliyet: Kullanılamıyor"; "Resets" = "Sıfırlamalar"; + +/* Codex Workspaces and Models */ +"codex_workspaces_open_in_window" = "Pencerede Aç"; +"codex_models_actions" = "Eylemler"; +"codex_models_active_and_new_count" = "Etkin modeller: %1$d; yeni aktif: %2$d"; +"codex_models_active_count_new_unavailable" = "Etkin modeller: %1$d; yeni aktif sayım mevcut değil"; +"codex_models_active_models" = "Aktif modeller"; +"codex_models_alias_count" = "Takma adlar: %1$d"; +"codex_models_all_model_count" = "Tüm modeller: %1$d"; +"codex_models_all_models" = "Tüm modeller"; +"codex_models_all_rows" = "Tüm satırlar"; +"codex_models_analytics" = "Model analitiği"; +"codex_models_analytics_controls" = "Model analiz kontrolleri"; +"codex_models_apply" = "Uygula"; +"codex_models_bucket_count" = "Paketler (%2$@): %1$d"; +"codex_models_bucket_interval" = "Paket aralığı"; +"codex_models_bullet_value" = "· %1$@"; +"codex_models_canonical_id" = "Kanonik Kimlik"; +"codex_models_canonical_model" = "Kanonik model"; +"codex_models_change" = "Değiştir"; +"codex_models_change_from_peak" = "%1$@ %2$@ zirveden"; +"codex_models_change_from_previous_period" = "%1$@ önceki eşit dönemden"; +"codex_models_choose_metric" = "Analiz metriğini seçin"; +"codex_models_clear_search" = "Aramayı Temizle"; +"codex_models_clear_selection" = "Model seçimini temizle"; +"codex_models_columns" = "Sütunlar"; +"codex_models_compact_billions_suffix" = "B"; +"codex_models_compact_millions_suffix" = "M"; +"codex_models_compact_thousands_suffix" = "K"; +"codex_models_contains_selected_suffix" = ", seçilen modeli içerir"; +"codex_models_copy_aliases" = "Takma adları kopyala"; +"codex_models_copy_canonical_id" = "Kurallı kimliği kopyala"; +"codex_models_copy_canonical_model_id" = "Kurallı model kimliğini kopyala"; +"codex_models_copy_raw_aliases" = "Ham takma adları kopyala"; +"codex_models_cost_conclusions_limited" = "Maliyet sonuçları %1$@ fiyatlı tokenlarla sınırlıdır."; +"codex_models_cost_hidden" = "Gizli maliyet"; +"codex_models_cost_status_currency" = "%1$@ · %2$@ · %3$@"; +"codex_models_cost_unavailable" = "Maliyet mevcut değil"; +"codex_models_cost_unavailable_for_data" = "Maliyet bu veri için mevcut değil"; +"codex_models_coverage" = "Kapsam"; +"codex_models_currency" = "Para birimi"; +"codex_models_current_metric" = "Mevcut %1$@"; +"codex_models_current_period_in_progress" = "Mevcut %1$@ devam ediyor"; +"codex_models_date" = "Tarih"; +"codex_models_day_lowercase" = "gün"; +"codex_models_days" = "Günler"; +"codex_models_delta_vs_prior" = "Δ öncekine göre"; +"codex_models_disabled_in_settings" = "Ayarlarda devre dışı bırakıldı"; +"codex_models_dominates_at" = "Bu model %2$@'da %1$@'a hakimdir."; +"codex_models_ended" = "Bitti"; +"codex_models_export" = "Dışa Aktar"; +"codex_models_export_all_filename" = "codex-models-all.csv"; +"codex_models_export_default_filename" = "codex-models.csv"; +"codex_models_export_failed" = "Model analizleri dışa aktarılamadı"; +"codex_models_export_selected_filename" = "codex-model-%1$@.csv"; +"codex_models_export_selected_model" = "Seçilen modeli dışa aktar"; +"codex_models_export_visible_filename" = "codex-models-visible.csv"; +"codex_models_export_visible_rows" = "Görünür Modeller satırlarını dışa aktar"; +"codex_models_focus_table_search" = "Odak Modelleri tablosunda arama"; +"codex_models_fully_priced" = "Tam fiyatlı"; +"codex_models_fully_priced_percent" = "%100 fiyatlı"; +"codex_models_generated" = "%1$@ oluşturuldu"; +"codex_models_granularity_accessibility" = "Parçalılık, %1$@, %2$@"; +"codex_models_hidden" = "Gizli"; +"codex_models_identity_aliases_help" = "%1$@ · %2$@ · Takma Adlar: %3$@"; +"codex_models_identity_and_aliases" = "Kimlik ve takma adlar"; +"codex_models_indexing_last_days" = "Son %1$d gün dizine ekleniyor…"; +"codex_models_interpretation" = "Yorumlama"; +"codex_models_known_cost" = "Bilinen maliyet"; +"codex_models_known_cost_fully_priced_accessibility" = "Bilinen maliyet %1$@, tamamen fiyatlandırılmış"; +"codex_models_known_cost_lowercase" = "bilinen maliyet"; +"codex_models_known_cost_partial_accessibility" = "Bilinen maliyet %1$@, kısmi, fiyatlandırılmış tokenların %2$@'i"; +"codex_models_last_days" = "Son %1$d gün"; +"codex_models_local_logs_source" = "· Yerel Kodeks günlükleri"; +"codex_models_major_contributor_at" = "Bu model %1$@'da önemli bir katkıda bulunmaktadır."; +"codex_models_metric" = "Metrik"; +"codex_models_metric_over_time" = "%1$@ zamanla"; +"codex_models_model" = "Modeli"; +"codex_models_model_concentration" = "Model konsantrasyonu"; +"codex_models_model_count" = "Modeller: %1$d"; +"codex_models_model_usage" = "Model kullanımı"; +"codex_models_month_lowercase" = "ay"; +"codex_models_more_alias_count" = "Diğer takma adlar: %1$d"; +"codex_models_name_canonical_id" = "%1$@, kurallı kimlik %2$@"; +"codex_models_name_value_accessibility" = "%1$@, %2$@"; +"codex_models_named_metric_over_time" = "%1$@ %2$@ zamanla"; +"codex_models_new" = "Yeni"; +"codex_models_new_in_period" = "Bu dönemdeki yenilikler"; +"codex_models_newly_active_count" = "Yeni etkin: %1$d"; +"codex_models_no_applicable_price" = "Geçerli fiyat yok"; +"codex_models_no_change_from_previous_period" = "Önceki eşit döneme göre değişiklik yok"; +"codex_models_no_matching_models" = "Eşleşen model yok"; +"codex_models_no_newly_active" = "Yeni aktif model yok"; +"codex_models_no_top_model" = "Üst model yok"; +"codex_models_no_usage" = "Kullanım yok"; +"codex_models_no_usage_description" = "Yukarıdan daha uzun bir tarih aralığı seçin veya yerel Codex oturumları kaydedildikten sonra yenileyin."; +"codex_models_no_usage_in_period" = "Bu dönemde kullanım yok"; +"codex_models_no_usage_in_range" = "Bu aralıkta model kullanımı yok"; +"codex_models_other_count" = "Diğer (%1$d)"; +"codex_models_partial_priced" = "Kısmi · %1$@ fiyatlı"; +"codex_models_precision_table" = "Model kesinlik tablosu"; +"codex_models_previous_known_cost" = "Önceki bilinen maliyet"; +"codex_models_previous_metric" = "Önceki %1$@"; +"codex_models_previous_period_unavailable" = "Önceki dönem mevcut değil"; +"codex_models_previous_unavailable" = "Önceki kullanılamıyor"; +"codex_models_priced_tokens" = "Fiyatlandırılmış jetonlar"; +"codex_models_pricing_calculation" = "Fiyatlandırma hesaplaması"; +"codex_models_pricing_coverage" = "Fiyatlandırma kapsamı"; +"codex_models_pricing_explanation" = "%1$@, %2$@ fiyatlı tokenları kapsar. Uygulanabilir bir kurala sahip olmayan %3$@ jetonları hariç tutulur, sıfır olarak tahmin edilmez."; +"codex_models_ranking_by" = "Model sıralaması %1$@'a göre"; +"codex_models_ranking_hint" = "Ayrıntılarını açmak için bir model çubuğuna tıklayın."; +"codex_models_ranking_point_accessibility" = "%1$@, %2$@, %3$@ paylaş"; +"codex_models_ranking_summary" = "Kanonik modeller ve Diğer %1$@ sıralamasına göre."; +"codex_models_ref_count" = "%1$@ referanslar"; +"codex_models_refresh_analytics" = "Model analizlerini yenile"; +"codex_models_refresh_command_help" = "Model analizlerini yenile (Komut-R)"; +"codex_models_refresh_model_usage" = "Model Kullanımını Yenile"; +"codex_models_refreshing_last_snapshot" = "Yenileniyor · son başarılı anlık görüntü gösteriliyor"; +"codex_models_retry" = "Tekrar dene"; +"codex_models_reveal_pricing_calculation" = "Fiyatlandırma hesaplamasını göster"; +"codex_models_search_empty_description" = "Arama yalnızca bu tabloyu filtreler; kontrol paneli toplamları değişmez."; +"codex_models_search_models" = "Modelleri ara"; +"codex_models_search_scope" = "%1$@ · Arama filtreleri yalnızca bu tabloya aittir"; +"codex_models_search_table" = "Arama Modelleri tablosu"; +"codex_models_select_model" = "%1$@'ı seçin"; +"codex_models_selected" = "Seçildi"; +"codex_models_selected_bucket" = "Seçilen paket"; +"codex_models_selected_bucket_accessibility" = "Seçilen paket: %1$@, %2$@, %3$@ jetonlar, %4$@ oturum referansları, %5$@, para birimi %6$@."; +"codex_models_selected_clear_available" = "Seçilen model %1$@, Seçimi temizle mevcut"; +"codex_models_selected_date" = "Seçilen tarih"; +"codex_models_selected_hidden_by_search" = "Seçilen model aramayla gizlendi"; +"codex_models_selected_model" = "Seçilen model"; +"codex_models_selected_model_interval" = "Seçilen model · %1$@"; +"codex_models_selected_suffix" = ", seçildi"; +"codex_models_session_reference_count" = "%1$@ oturum referansları"; +"codex_models_session_references" = "Oturum referansları"; +"codex_models_session_references_lowercase" = "oturum referansları"; +"codex_models_session_refs" = "Oturum referansları"; +"codex_models_share" = "Paylaş"; +"codex_models_share_known_cost_help" = "Bilinen maliyet payı; fiyatlandırılmamış kullanım hariçtir"; +"codex_models_share_of" = "%1$@ payı"; +"codex_models_show_associated_sessions" = "İlişkili oturumları göster"; +"codex_models_show_details" = "Ayrıntıları göster"; +"codex_models_showing_last_snapshot" = "Son başarılı anlık görüntü gösteriliyor"; +"codex_models_sorted_by" = "%1$@'a göre sıralandı"; +"codex_models_subject_over_time" = "%1$@ zamanla"; +"codex_models_summary" = "Model özeti"; +"codex_models_timeline_accessibility" = "Modeller zaman çizelgesi, %1$@, %2$@"; +"codex_models_timeline_hint" = "Ayrıntılarını incelemek için bir zaman aralığını tıklayın veya işaretçiyi 1,5 saniye boyunca bir zaman aralığının üzerinde tutun."; +"codex_models_timeline_point_accessibility" = "%1$@, %2$@, %3$@ jetonlar, %4$@ oturum referansları, %5$@, para birimi %6$@"; +"codex_models_timeline_summary" = "%1$@ %2$@ zaman çizelgesi. %3$@"; +"codex_models_token_count" = "%1$@ belirteçleri"; +"codex_models_tokens" = "Jetonlar"; +"codex_models_tokens_lowercase" = "jetonlar"; +"codex_models_tokens_per_session_reference" = "Oturum referansı başına yaklaşık %1$@ jeton."; +"codex_models_top" = "Üst"; +"codex_models_top_model" = "Üst model"; +"codex_models_top_seven_other" = "İlk 7 + Diğer"; +"codex_models_top_two_account_for" = "İlk iki model %1$@ / %2$@'ı oluşturuyor"; +"codex_models_top_two_of" = "%1$@'ın en iyi iki modeli"; +"codex_models_total_session_references_lowercase" = "toplam oturum referansı"; +"codex_models_total_tokens" = "Toplam jetonlar"; +"codex_models_total_tokens_lowercase" = "toplam jeton"; +"codex_models_totals_reconcile" = "· Toplamlar etkin anlık görüntüyle uzlaştırılır"; +"codex_models_track_end" = "Parça sonu"; +"codex_models_track_start" = "Parça başlangıcı"; +"codex_models_trend_unavailable_observed_buckets" = "Gözlemlenen paketler: %1$d. Eğilim kullanılamıyor."; +"codex_models_unknown_export_error" = "Bilinmeyen dışa aktarma hatası"; +"codex_models_unpriced_tokens" = "Fiyatlandırılmamış jetonlar"; +"codex_models_updated" = "%1$@ güncellendi"; +"codex_models_usage_and_change" = "Kullanım ve değişiklik"; +"codex_models_usage_by_model" = "Modele göre kullanım"; +"codex_models_value_comparison_accessibility" = "%1$@, %2$@"; +"codex_models_value_detail" = "%1$@ · %2$@"; +"codex_models_value_share" = "%1$@ · %2$@ paylaş"; +"codex_models_value_share_accessibility" = "%1$@, %2$@ paylaş"; +"codex_models_value_start" = "Değer başlangıcı"; +"codex_models_visible_of_total" = "Görünür modeller: %1$d / %2$d"; +"codex_models_visible_rows" = "Görünür satırlar"; +"codex_models_vs_previous" = "%1$@ ile öncekine kıyasla"; +"codex_models_week_lowercase" = "hafta"; +"codex_workspaces_all_models" = "Tüm Modeller"; +"codex_workspaces_all_workspaces" = "Tüm Çalışma Alanları"; +"codex_workspaces_associated_with_selected_model" = "Seçilen modelle ilişkili"; +"codex_workspaces_bullet_value" = "· %1$@"; +"codex_workspaces_chart_keyboard_hint" = "Bir gün seçmek için sol ve sağ ok tuşlarını veya VoiceOver ayarlama eylemlerini kullanın. Sabitlemek için Return ya da Boşluk tuşuna basın."; +"codex_workspaces_chats_description" = "Sohbetler ve görüşmeler"; +"codex_workspaces_copy_path" = "Yolu Kopyala"; +"codex_workspaces_cost_unavailable" = "Maliyet kullanılamıyor"; +"codex_workspaces_daily_callout_accessibility" = "%1$@, %2$@"; +"codex_workspaces_daily_point_accessibility" = "%1$@, jetonlar %2$@"; +"codex_workspaces_daily_point_help" = "%1$@ — %2$@ jetonları"; +"codex_workspaces_daily_token_usage" = "Günlük token kullanımı"; +"codex_workspaces_daily_unavailable" = "Günlük kullanım kullanılamıyor"; +"codex_workspaces_daily_unavailable_description" = "Toplam %d oturumdan hesaplandı, ancak tarihleri belirlenemedi."; +"codex_workspaces_date" = "Tarih"; +"codex_workspaces_days" = "Gün"; +"codex_workspaces_elevated_usage" = "Artmış kullanım"; +"codex_workspaces_elevated_usage_help" = "Kullanım, seçilen dönemin medyanının üzerinde."; +"codex_workspaces_estimated_cost_short" = "Tahm. maliyet"; +"codex_workspaces_estimated_cost_value" = "tahm. %@"; +"codex_workspaces_estimated_local_logs" = "Yerel Codex günlüklerinden tahmin edildi"; +"codex_workspaces_failed_read_logs" = "Codex günlükleri okunamadı"; +"codex_workspaces_failed_some_logs" = "Bazı Codex günlükleri okunamadı"; +"codex_workspaces_finalizing_project_index_files" = "Proje dizini tamamlanıyor… %d dosya"; +"codex_workspaces_high_usage" = "Yüksek kullanım"; +"codex_workspaces_high_usage_help" = "Kullanım, seçilen dönem için belirgin şekilde sıra dışı."; +"codex_workspaces_indexed_files" = "%@ önce güncellendi · %d dosya dizine eklendi"; +"codex_workspaces_indexing_local_logs" = "Yerel Codex günlükleri dizine ekleniyor…"; +"codex_workspaces_indexing_projects" = "Projeler dizine ekleniyor…"; +"codex_workspaces_indexing_projects_files_left" = "Projeler dizine ekleniyor… %d/%d dosya, %d kaldı"; +"codex_workspaces_last_days" = "Son %d gün"; +"codex_workspaces_local_codex_chat" = "Yerel Codex sohbeti"; +"codex_workspaces_model" = "Model"; +"codex_workspaces_models" = "Modeller"; +"codex_workspaces_no_daily_usage" = "Bu geçmiş aralığında günlük kullanım yok."; +"codex_workspaces_no_local_usage" = "Henüz yerel Codex kullanımı yok"; +"codex_workspaces_no_models" = "Model dökümü kullanılamıyor"; +"codex_workspaces_no_projects_or_chats" = "Bu geçmiş aralığında proje veya sohbet bulunamadı."; +"codex_workspaces_no_sessions" = "Bu çalışma alanında oturum yok"; +"codex_workspaces_normal_usage" = "Normal kullanım"; +"codex_workspaces_not_available" = "Mevcut değil"; +"codex_workspaces_overview" = "Genel Bakış"; +"codex_workspaces_partial_breakdown_help" = "Özet geçerliliğini korur; kullanılamayan tarih veya model meta verileri kısmi olarak gösterilir."; +"codex_workspaces_partial_breakdown_notice" = "Bazı ayrıntılı dökümler toplamın tamamını kapsamıyor."; +"codex_workspaces_partial_data" = "Kısmi veriler"; +"codex_workspaces_percent_total" = "toplamın %'si"; +"codex_workspaces_refresh_usage_data" = "Kullanım verilerini yenile"; +"codex_workspaces_relative_usage_accessibility" = "En yüksek çalışma alanı kullanımının yüzde %d kadarı"; +"codex_workspaces_relative_usage_tooltip" = "Bu dönemde en çok kullanılan çalışma alanındaki tokenların %d%% kadarını kullanır."; +"codex_workspaces_reveal_in_finder" = "Finder'da Göster"; +"codex_workspaces_saving_project_index" = "Proje dizini kaydediliyor…"; +"codex_workspaces_scanning_logs" = "Codex günlükleri taranıyor…"; +"codex_workspaces_search_sessions" = "Oturumlarda ara"; +"codex_workspaces_selected_day_accessibility" = "%@: %@ token"; +"codex_workspaces_session" = "Oturum"; +"codex_workspaces_session_count" = "Oturumlar: %d"; +"codex_workspaces_sessions" = "Oturumlar"; +"codex_workspaces_showing_last_complete_index" = "Son eksiksiz yerel dizin gösteriliyor"; +"codex_workspaces_stale_status" = "Kullanım dizini güncel değil · %@ önce güncellendi"; +"codex_workspaces_started" = "Başlatıldı"; +"codex_workspaces_title" = "Çalışma Alanları"; +"codex_workspaces_token_count_accessibility" = "%@ token"; +"codex_workspaces_tokens" = "Tokenlar"; +"codex_workspaces_top_model" = "En çok kullanılan model"; +"codex_workspaces_top_sessions" = "Öne çıkan oturumlar"; +"codex_workspaces_total" = "Toplam"; +"codex_workspaces_view" = "Görünüm"; +"codex_workspaces_view_all_sessions" = "Tüm Oturumları Görüntüle"; +"codex_workspaces_window_title" = "Codex Çalışma Alanları"; +"codex_workspaces_workspace_count" = "Çalışma alanları ve sohbetler: %d"; +"codex_workspaces_workspaces_chats" = "Çalışma alanları/sohbetler"; diff --git a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings index 1915ea7bf9..b39421bdd4 100644 --- a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings @@ -1349,3 +1349,255 @@ "Cost today unavailable" = "Вартість сьогодні: Недоступний"; "30-day cost unavailable" = "Вартість за 30 днів: Недоступний"; "Resets" = "Скидання"; + +/* Codex Workspaces and Models */ +"codex_workspaces_open_in_window" = "Відкрити у вікні"; +"codex_models_actions" = "Дії"; +"codex_models_active_and_new_count" = "Активні моделі: %1$d; нещодавно активні: %2$d"; +"codex_models_active_count_new_unavailable" = "Активні моделі: %1$d; кількість нещодавно активних недоступна"; +"codex_models_active_models" = "Активні моделі"; +"codex_models_alias_count" = "Псевдоніми: %1$d"; +"codex_models_all_model_count" = "Усі моделі: %1$d"; +"codex_models_all_models" = "Всі моделі"; +"codex_models_all_rows" = "Усі рядки"; +"codex_models_analytics" = "Аналітика моделей"; +"codex_models_analytics_controls" = "Моделює елементи керування аналітикою"; +"codex_models_apply" = "Заявка"; +"codex_models_bucket_count" = "Відра (%2$@): %1$d"; +"codex_models_bucket_interval" = "Інтервал ковша"; +"codex_models_bullet_value" = "· %1$@"; +"codex_models_canonical_id" = "Канонічний ідентифікатор"; +"codex_models_canonical_model" = "Канонічна модель"; +"codex_models_change" = "Змінити"; +"codex_models_change_from_peak" = "%1$@ з піку %2$@"; +"codex_models_change_from_previous_period" = "%1$@ за попередній рівний період"; +"codex_models_choose_metric" = "Виберіть показник аналітики"; +"codex_models_clear_search" = "Очистити пошук"; +"codex_models_clear_selection" = "Очистити вибір моделей"; +"codex_models_columns" = "Стовпці"; +"codex_models_compact_billions_suffix" = "B"; +"codex_models_compact_millions_suffix" = "M"; +"codex_models_compact_thousands_suffix" = "K"; +"codex_models_contains_selected_suffix" = ", містить обрану модель"; +"codex_models_copy_aliases" = "Копіювати псевдоніми"; +"codex_models_copy_canonical_id" = "Скопіюйте канонічний ідентифікатор"; +"codex_models_copy_canonical_model_id" = "Скопіюйте ідентифікатор канонічної моделі"; +"codex_models_copy_raw_aliases" = "Скопіюйте необроблені псевдоніми"; +"codex_models_cost_conclusions_limited" = "Висновки щодо вартості обмежуються токенами з ціною %1$@."; +"codex_models_cost_hidden" = "Вартість прихована"; +"codex_models_cost_status_currency" = "%1$@ · %2$@ · %3$@"; +"codex_models_cost_unavailable" = "Вартість недоступна"; +"codex_models_cost_unavailable_for_data" = "Вартість недоступна для цих даних"; +"codex_models_coverage" = "Покриття"; +"codex_models_currency" = "Валюта"; +"codex_models_current_metric" = "Поточний %1$@"; +"codex_models_current_period_in_progress" = "Виконується поточний %1$@"; +"codex_models_date" = "Дата"; +"codex_models_day_lowercase" = "день"; +"codex_models_days" = "днів"; +"codex_models_delta_vs_prior" = "Δ проти попереднього"; +"codex_models_disabled_in_settings" = "Вимкнено в налаштуваннях"; +"codex_models_dominates_at" = "Ця модель домінує в %1$@ на %2$@."; +"codex_models_ended" = "Завершено"; +"codex_models_export" = "Експорт"; +"codex_models_export_all_filename" = "codex-models-all.csv"; +"codex_models_export_default_filename" = "codex-models.csv"; +"codex_models_export_failed" = "Не вдалося експортувати аналітику моделей"; +"codex_models_export_selected_filename" = "codex-model-%1$@.csv"; +"codex_models_export_selected_model" = "Експорт вибраної моделі"; +"codex_models_export_visible_filename" = "codex-models-visible.csv"; +"codex_models_export_visible_rows" = "Експорт видимих рядків моделей"; +"codex_models_focus_table_search" = "Пошук у таблиці Focus Models"; +"codex_models_fully_priced" = "Повна ціна"; +"codex_models_fully_priced_percent" = "Ціна 100%."; +"codex_models_generated" = "Згенеровано %1$@"; +"codex_models_granularity_accessibility" = "Деталізація, %1$@, %2$@"; +"codex_models_hidden" = "Приховано"; +"codex_models_identity_aliases_help" = "%1$@ · %2$@ · Псевдоніми: %3$@"; +"codex_models_identity_and_aliases" = "Особа та псевдоніми"; +"codex_models_indexing_last_days" = "Індексування останніх %1$d днів…"; +"codex_models_interpretation" = "Тлумачення"; +"codex_models_known_cost" = "Відома вартість"; +"codex_models_known_cost_fully_priced_accessibility" = "Відома вартість %1$@, повна ціна"; +"codex_models_known_cost_lowercase" = "відома вартість"; +"codex_models_known_cost_partial_accessibility" = "Відома вартість %1$@, часткова, %2$@ токенів у ціні"; +"codex_models_last_days" = "Останні %1$d днів"; +"codex_models_local_logs_source" = "· Локальні журнали Codex"; +"codex_models_major_contributor_at" = "Ця модель є основним учасником %1$@."; +"codex_models_metric" = "Метрика"; +"codex_models_metric_over_time" = "%1$@ з часом"; +"codex_models_model" = "Модель"; +"codex_models_model_concentration" = "Концентрація моделі"; +"codex_models_model_count" = "Моделі: %1$d"; +"codex_models_model_usage" = "Використання моделі"; +"codex_models_month_lowercase" = "місяць"; +"codex_models_more_alias_count" = "Інші псевдоніми: %1$d"; +"codex_models_name_canonical_id" = "%1$@, канонічний ідентифікатор %2$@"; +"codex_models_name_value_accessibility" = "%1$@, %2$@"; +"codex_models_named_metric_over_time" = "%1$@ %2$@ з часом"; +"codex_models_new" = "Новинка"; +"codex_models_new_in_period" = "Нове за цей період"; +"codex_models_newly_active_count" = "Щойно активний: %1$d"; +"codex_models_no_applicable_price" = "Не актуальна ціна"; +"codex_models_no_change_from_previous_period" = "Без змін порівняно з попереднім рівним періодом"; +"codex_models_no_matching_models" = "Немає відповідних моделей"; +"codex_models_no_newly_active" = "Немає нових активних моделей"; +"codex_models_no_top_model" = "Без топ-моделі"; +"codex_models_no_usage" = "Не використовується"; +"codex_models_no_usage_description" = "Виберіть довший діапазон дат вище або оновіть після збереження локальних сеансів Codex."; +"codex_models_no_usage_in_period" = "Не використовувався в цей період"; +"codex_models_no_usage_in_range" = "Модель не використовується в цьому діапазоні"; +"codex_models_other_count" = "Інше (%1$d)"; +"codex_models_partial_priced" = "Частково · Ціна %1$@"; +"codex_models_precision_table" = "Таблиця точності моделі"; +"codex_models_previous_known_cost" = "Попередня відома вартість"; +"codex_models_previous_metric" = "Попередній %1$@"; +"codex_models_previous_period_unavailable" = "Попередній період недоступний"; +"codex_models_previous_unavailable" = "Попередній недоступний"; +"codex_models_priced_tokens" = "Оцінені токени"; +"codex_models_pricing_calculation" = "Розрахунок ціни"; +"codex_models_pricing_coverage" = "Ціноутворення"; +"codex_models_pricing_explanation" = "%1$@ охоплює %2$@ токенів із ціною. %3$@ маркери без відповідного правила виключаються, не оцінюються як нуль."; +"codex_models_ranking_by" = "Рейтинг моделі за %1$@"; +"codex_models_ranking_hint" = "Натисніть панель моделі, щоб відкрити її деталі."; +"codex_models_ranking_point_accessibility" = "%1$@, %2$@, %3$@ частка"; +"codex_models_ranking_summary" = "Канонічні моделі та інше в рейтингу %1$@."; +"codex_models_ref_count" = "%1$@ посил"; +"codex_models_refresh_analytics" = "Оновити аналітику моделей"; +"codex_models_refresh_command_help" = "Аналітика оновлення моделей (Command-R)"; +"codex_models_refresh_model_usage" = "Оновити використання моделі"; +"codex_models_refreshing_last_snapshot" = "Оновлення · показ останнього успішного знімка"; +"codex_models_retry" = "Повторити спробу"; +"codex_models_reveal_pricing_calculation" = "Розкрити розрахунок ціни"; +"codex_models_search_empty_description" = "Пошук фільтрує лише цю таблицю; загальні дані на інформаційній панелі не змінені."; +"codex_models_search_models" = "Пошук моделей"; +"codex_models_search_scope" = "%1$@ · Пошук фільтрує лише цю таблицю"; +"codex_models_search_table" = "Таблиця моделей пошуку"; +"codex_models_select_model" = "Виберіть %1$@"; +"codex_models_selected" = "Вибрано"; +"codex_models_selected_bucket" = "Вибране відро"; +"codex_models_selected_bucket_accessibility" = "Вибране відро: %1$@, %2$@, %3$@ токенів, %4$@ посилань на сесії, %5$@, валюта %6$@."; +"codex_models_selected_clear_available" = "Вибрана модель %1$@, доступний очистити вибір"; +"codex_models_selected_date" = "Вибрана дата"; +"codex_models_selected_hidden_by_search" = "Вибрана модель прихована пошуком"; +"codex_models_selected_model" = "Вибрана модель"; +"codex_models_selected_model_interval" = "Вибрана модель · %1$@"; +"codex_models_selected_suffix" = ", вибрано"; +"codex_models_session_reference_count" = "%1$@ посилань на сесії"; +"codex_models_session_references" = "Посилання на сесії"; +"codex_models_session_references_lowercase" = "посилання на сесії"; +"codex_models_session_refs" = "Реф"; +"codex_models_share" = "Поділитися"; +"codex_models_share_known_cost_help" = "Частка відомої вартості; виключає безкоштовне використання"; +"codex_models_share_of" = "Частка %1$@"; +"codex_models_show_associated_sessions" = "Показати пов'язані сеанси"; +"codex_models_show_details" = "Показати деталі"; +"codex_models_showing_last_snapshot" = "Показано останній успішний знімок"; +"codex_models_sorted_by" = "Відсортовано за %1$@"; +"codex_models_subject_over_time" = "%1$@ з часом"; +"codex_models_summary" = "Зведення моделей"; +"codex_models_timeline_accessibility" = "Часова шкала моделей, %1$@, %2$@"; +"codex_models_timeline_hint" = "Клацніть відро часу або утримуйте вказівник на одному з них протягом 1,5 секунд, щоб перевірити його деталі."; +"codex_models_timeline_point_accessibility" = "%1$@, %2$@, %3$@ токенів, %4$@ посилань на сесії, %5$@, валюта %6$@"; +"codex_models_timeline_summary" = "%1$@ %2$@ хронологія. %3$@"; +"codex_models_token_count" = "%1$@ токенів"; +"codex_models_tokens" = "Жетони"; +"codex_models_tokens_lowercase" = "токенів"; +"codex_models_tokens_per_session_reference" = "Приблизно %1$@ маркерів на посилання на сеанс."; +"codex_models_top" = "Верх"; +"codex_models_top_model" = "Топ модель"; +"codex_models_top_seven_other" = "Топ 7 + інше"; +"codex_models_top_two_account_for" = "На дві найкращі моделі припадає %1$@ з %2$@"; +"codex_models_top_two_of" = "Дві найкращі моделі %1$@"; +"codex_models_total_session_references_lowercase" = "загальна кількість посилань на сесії"; +"codex_models_total_tokens" = "Усього жетонів"; +"codex_models_total_tokens_lowercase" = "всього токенів"; +"codex_models_totals_reconcile" = "· Підсумки звіряються з активним знімком"; +"codex_models_track_end" = "Кінець треку"; +"codex_models_track_start" = "Початок треку"; +"codex_models_trend_unavailable_observed_buckets" = "Спостережувані сегменти: %1$d. Тенденція недоступна."; +"codex_models_unknown_export_error" = "Невідома помилка експорту"; +"codex_models_unpriced_tokens" = "Жетони без ціни"; +"codex_models_updated" = "Оновлено %1$@"; +"codex_models_usage_and_change" = "Використання та зміни"; +"codex_models_usage_by_model" = "Використання за моделлю"; +"codex_models_value_comparison_accessibility" = "%1$@, %2$@"; +"codex_models_value_detail" = "%1$@ · %2$@"; +"codex_models_value_share" = "%1$@ · %2$@ частка"; +"codex_models_value_share_accessibility" = "%1$@, %2$@ частка"; +"codex_models_value_start" = "Початок значення"; +"codex_models_visible_of_total" = "Видимі моделі: %1$d з %2$d"; +"codex_models_visible_rows" = "Видимі рядки"; +"codex_models_vs_previous" = "%1$@ порівняно з попереднім"; +"codex_models_week_lowercase" = "тиждень"; +"codex_workspaces_all_models" = "Усі моделі"; +"codex_workspaces_all_workspaces" = "Усі робочі простори"; +"codex_workspaces_associated_with_selected_model" = "Пов’язано з вибраною моделлю"; +"codex_workspaces_bullet_value" = "· %1$@"; +"codex_workspaces_chart_keyboard_hint" = "Виберіть день клавішами зі стрілками вліво та вправо або діями регулювання VoiceOver. Натисніть Return або Пробіл, щоб закріпити його."; +"codex_workspaces_chats_description" = "Чати та розмови"; +"codex_workspaces_copy_path" = "Скопіювати шлях"; +"codex_workspaces_cost_unavailable" = "Вартість недоступна"; +"codex_workspaces_daily_callout_accessibility" = "%1$@, %2$@"; +"codex_workspaces_daily_point_accessibility" = "%1$@, токени %2$@"; +"codex_workspaces_daily_point_help" = "%1$@ — %2$@ токенів"; +"codex_workspaces_daily_token_usage" = "Щоденне використання токенів"; +"codex_workspaces_daily_unavailable" = "Щоденне використання недоступне"; +"codex_workspaces_daily_unavailable_description" = "Підсумок обчислено за %d сеансами, але їхні дати визначити не вдалося."; +"codex_workspaces_date" = "Дата"; +"codex_workspaces_days" = "Дні"; +"codex_workspaces_elevated_usage" = "Підвищене використання"; +"codex_workspaces_elevated_usage_help" = "Використання вище медіани за вибраний період."; +"codex_workspaces_estimated_cost_short" = "Оцін. вартість"; +"codex_workspaces_estimated_cost_value" = "оцін. %@"; +"codex_workspaces_estimated_local_logs" = "Оцінено за локальними журналами Codex"; +"codex_workspaces_failed_read_logs" = "Не вдалося прочитати журнали Codex"; +"codex_workspaces_failed_some_logs" = "Не вдалося прочитати деякі журнали Codex"; +"codex_workspaces_finalizing_project_index_files" = "Завершення індексу проєктів… Файлів: %d"; +"codex_workspaces_high_usage" = "Високе використання"; +"codex_workspaces_high_usage_help" = "Використання значно відрізняється від решти за вибраний період."; +"codex_workspaces_indexed_files" = "Оновлено %@ тому · Проіндексовано файлів: %d"; +"codex_workspaces_indexing_local_logs" = "Індексування локальних журналів Codex…"; +"codex_workspaces_indexing_projects" = "Індексування проєктів…"; +"codex_workspaces_indexing_projects_files_left" = "Індексування проєктів… %d/%d файлів, залишилося %d"; +"codex_workspaces_last_days" = "Останні %d днів"; +"codex_workspaces_local_codex_chat" = "Локальний чат Codex"; +"codex_workspaces_model" = "Модель"; +"codex_workspaces_models" = "Моделі"; +"codex_workspaces_no_daily_usage" = "Немає даних про щоденне використання за цей період."; +"codex_workspaces_no_local_usage" = "Локальних даних про використання Codex ще немає"; +"codex_workspaces_no_models" = "Розподіл за моделями недоступний"; +"codex_workspaces_no_projects_or_chats" = "За цей період проєктів або чатів не знайдено."; +"codex_workspaces_no_sessions" = "У цьому робочому просторі немає сеансів"; +"codex_workspaces_normal_usage" = "Звичайне використання"; +"codex_workspaces_not_available" = "Немає в наявності"; +"codex_workspaces_overview" = "Огляд"; +"codex_workspaces_partial_breakdown_help" = "Підсумок залишається основним, а недоступні метадані дат або моделей відображаються як часткові."; +"codex_workspaces_partial_breakdown_notice" = "Деякі докладні розподіли не охоплюють весь підсумок."; +"codex_workspaces_partial_data" = "Часткові дані"; +"codex_workspaces_percent_total" = "% від підсумку"; +"codex_workspaces_refresh_usage_data" = "Оновити дані про використання"; +"codex_workspaces_relative_usage_accessibility" = "%d відсотків від найбільшого використання робочого простору"; +"codex_workspaces_relative_usage_tooltip" = "Використовує %d%% токенів робочого простору з найбільшим використанням за цей період."; +"codex_workspaces_reveal_in_finder" = "Показати у Finder"; +"codex_workspaces_saving_project_index" = "Збереження індексу проєктів…"; +"codex_workspaces_scanning_logs" = "Сканування журналів Codex…"; +"codex_workspaces_search_sessions" = "Пошук сеансів"; +"codex_workspaces_selected_day_accessibility" = "%@: %@ токенів"; +"codex_workspaces_session" = "Сеанс"; +"codex_workspaces_session_count" = "Сеанси: %d"; +"codex_workspaces_sessions" = "Сеанси"; +"codex_workspaces_showing_last_complete_index" = "Показ останнього повного локального індексу"; +"codex_workspaces_stale_status" = "Індекс використання застарів · Оновлено %@ тому"; +"codex_workspaces_started" = "Розпочато"; +"codex_workspaces_title" = "Робочі простори"; +"codex_workspaces_token_count_accessibility" = "%@ токенів"; +"codex_workspaces_tokens" = "Токени"; +"codex_workspaces_top_model" = "Основна модель"; +"codex_workspaces_top_sessions" = "Основні сеанси"; +"codex_workspaces_total" = "Разом"; +"codex_workspaces_view" = "Вигляд"; +"codex_workspaces_view_all_sessions" = "Переглянути всі сеанси"; +"codex_workspaces_window_title" = "Робочі простори Codex"; +"codex_workspaces_workspace_count" = "Робочі простори та чати: %d"; +"codex_workspaces_workspaces_chats" = "Робочі простори/чати"; diff --git a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings index 02c9b8fdf3..dd4f576a47 100644 --- a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings @@ -1350,3 +1350,255 @@ "Cost today unavailable" = "Chi phí hôm nay: Không có sẵn"; "30-day cost unavailable" = "Chi phí 30 ngày: Không có sẵn"; "Resets" = "Lần đặt lại"; + +/* Codex Workspaces and Models */ +"codex_workspaces_open_in_window" = "Mở trong cửa sổ"; +"codex_models_actions" = "Hành động"; +"codex_models_active_and_new_count" = "Các mô hình đang hoạt động: %1$d; mới hoạt động: %2$d"; +"codex_models_active_count_new_unavailable" = "Các mô hình đang hoạt động: %1$d; số lượng mới hoạt động không có sẵn"; +"codex_models_active_models" = "Model đang hoạt động"; +"codex_models_alias_count" = "Bí danh: %1$d"; +"codex_models_all_model_count" = "Tất cả kiểu máy: %1$d"; +"codex_models_all_models" = "Tất cả mẫu mã"; +"codex_models_all_rows" = "Tất cả các hàng"; +"codex_models_analytics" = "Phân tích mô hình"; +"codex_models_analytics_controls" = "Kiểm soát phân tích mô hình"; +"codex_models_apply" = "Nộp đơn"; +"codex_models_bucket_count" = "Nhóm (%2$@): %1$d"; +"codex_models_bucket_interval" = "Khoảng thời gian của nhóm"; +"codex_models_bullet_value" = "· %1$@"; +"codex_models_canonical_id" = "ID chuẩn"; +"codex_models_canonical_model" = "Mô hình kinh điển"; +"codex_models_change" = "Thay đổi"; +"codex_models_change_from_peak" = "%1$@ từ đỉnh %2$@"; +"codex_models_change_from_previous_period" = "%1$@ từ kỳ bằng nhau trước đó"; +"codex_models_choose_metric" = "Chọn số liệu phân tích"; +"codex_models_clear_search" = "Xóa tìm kiếm"; +"codex_models_clear_selection" = "Xóa lựa chọn mẫu"; +"codex_models_columns" = "Cột"; +"codex_models_compact_billions_suffix" = "B"; +"codex_models_compact_millions_suffix" = "M"; +"codex_models_compact_thousands_suffix" = "K"; +"codex_models_contains_selected_suffix" = ", chứa mô hình đã chọn"; +"codex_models_copy_aliases" = "Sao chép bí danh"; +"codex_models_copy_canonical_id" = "Sao chép ID chuẩn"; +"codex_models_copy_canonical_model_id" = "Sao chép ID mẫu chuẩn"; +"codex_models_copy_raw_aliases" = "Sao chép bí danh thô"; +"codex_models_cost_conclusions_limited" = "Kết luận chi phí được giới hạn ở các mã thông báo được định giá %1$@."; +"codex_models_cost_hidden" = "Chi phí ẩn"; +"codex_models_cost_status_currency" = "%1$@ · %2$@ · %3$@"; +"codex_models_cost_unavailable" = "Không có chi phí"; +"codex_models_cost_unavailable_for_data" = "Không có chi phí cho dữ liệu này"; +"codex_models_coverage" = "Bảo hiểm"; +"codex_models_currency" = "Tiền tệ"; +"codex_models_current_metric" = "Hiện tại %1$@"; +"codex_models_current_period_in_progress" = "Đang tiến hành %1$@ hiện tại"; +"codex_models_date" = "Ngày"; +"codex_models_day_lowercase" = "ngày"; +"codex_models_days" = "Ngày"; +"codex_models_delta_vs_prior" = "Δ so với trước đó"; +"codex_models_disabled_in_settings" = "Đã tắt trong cài đặt"; +"codex_models_dominates_at" = "Mô hình này chiếm ưu thế %1$@ tại %2$@."; +"codex_models_ended" = "Đã kết thúc"; +"codex_models_export" = "Xuất khẩu"; +"codex_models_export_all_filename" = "codex-models-all.csv"; +"codex_models_export_default_filename" = "codex-models.csv"; +"codex_models_export_failed" = "Không thể xuất phân tích Mô hình"; +"codex_models_export_selected_filename" = "codex-model-%1$@.csv"; +"codex_models_export_selected_model" = "Xuất mô hình đã chọn"; +"codex_models_export_visible_filename" = "codex-models-visible.csv"; +"codex_models_export_visible_rows" = "Xuất các hàng Mô hình hiển thị"; +"codex_models_focus_table_search" = "Tìm kiếm trong bảng Mô hình tiêu điểm"; +"codex_models_fully_priced" = "Giá đầy đủ"; +"codex_models_fully_priced_percent" = "Giá 100%"; +"codex_models_generated" = "Đã tạo %1$@"; +"codex_models_granularity_accessibility" = "Mức độ chi tiết, %1$@, %2$@"; +"codex_models_hidden" = "Ẩn"; +"codex_models_identity_aliases_help" = "%1$@ · %2$@ · Bí danh: %3$@"; +"codex_models_identity_and_aliases" = "Danh tính và bí danh"; +"codex_models_indexing_last_days" = "Lập chỉ mục %1$d ngày qua…"; +"codex_models_interpretation" = "Giải thích"; +"codex_models_known_cost" = "Chi phí đã biết"; +"codex_models_known_cost_fully_priced_accessibility" = "Chi phí đã biết %1$@, được định giá đầy đủ"; +"codex_models_known_cost_lowercase" = "chi phí đã biết"; +"codex_models_known_cost_partial_accessibility" = "Chi phí đã biết %1$@, một phần, %2$@ của số token được định giá"; +"codex_models_last_days" = "%1$d ngày qua"; +"codex_models_local_logs_source" = "· Nhật ký Codex cục bộ"; +"codex_models_major_contributor_at" = "Mô hình này là người đóng góp chính tại %1$@."; +"codex_models_metric" = "Số liệu"; +"codex_models_metric_over_time" = "%1$@ theo thời gian"; +"codex_models_model" = "Người mẫu"; +"codex_models_model_concentration" = "Nồng độ mô hình"; +"codex_models_model_count" = "Mẫu: %1$d"; +"codex_models_model_usage" = "Cách sử dụng mẫu"; +"codex_models_month_lowercase" = "tháng"; +"codex_models_more_alias_count" = "Bí danh khác: %1$d"; +"codex_models_name_canonical_id" = "%1$@, ID chuẩn %2$@"; +"codex_models_name_value_accessibility" = "%1$@, %2$@"; +"codex_models_named_metric_over_time" = "%1$@ %2$@ theo thời gian"; +"codex_models_new" = "Mới"; +"codex_models_new_in_period" = "Mới trong giai đoạn này"; +"codex_models_newly_active_count" = "Mới hoạt động: %1$d"; +"codex_models_no_applicable_price" = "Không áp dụng giá"; +"codex_models_no_change_from_previous_period" = "Không thay đổi so với kỳ trước"; +"codex_models_no_matching_models" = "Không có mẫu nào phù hợp"; +"codex_models_no_newly_active" = "Không có mẫu nào mới hoạt động"; +"codex_models_no_top_model" = "Không có người mẫu hàng đầu"; +"codex_models_no_usage" = "Không sử dụng"; +"codex_models_no_usage_description" = "Chọn phạm vi ngày dài hơn ở trên hoặc làm mới sau khi lưu các phiên Codex cục bộ."; +"codex_models_no_usage_in_period" = "Không sử dụng trong thời gian này"; +"codex_models_no_usage_in_range" = "Không sử dụng mô hình nào trong phạm vi này"; +"codex_models_other_count" = "Khác (%1$d)"; +"codex_models_partial_priced" = "Một phần · %1$@ có giá"; +"codex_models_precision_table" = "Bảng chính xác mẫu"; +"codex_models_previous_known_cost" = "Chi phí đã biết trước đó"; +"codex_models_previous_metric" = "Trước %1$@"; +"codex_models_previous_period_unavailable" = "Không có kỳ trước"; +"codex_models_previous_unavailable" = "Trước đó không có"; +"codex_models_priced_tokens" = "Token được định giá"; +"codex_models_pricing_calculation" = "Tính giá"; +"codex_models_pricing_coverage" = "Bảo hiểm giá cả"; +"codex_models_pricing_explanation" = "%1$@ bao gồm %2$@ mã thông báo được định giá. %3$@ mã thông báo không có quy tắc áp dụng sẽ bị loại trừ, không được ước tính bằng 0."; +"codex_models_ranking_by" = "Xếp hạng mô hình theo %1$@"; +"codex_models_ranking_hint" = "Nhấp vào thanh mô hình để mở thông tin chi tiết."; +"codex_models_ranking_point_accessibility" = "%1$@, %2$@, %3$@ chia sẻ"; +"codex_models_ranking_summary" = "Mô hình Canonical và Khác được xếp hạng theo %1$@."; +"codex_models_ref_count" = "%1$@ giới thiệu"; +"codex_models_refresh_analytics" = "Làm mới phân tích mô hình"; +"codex_models_refresh_command_help" = "Làm mới phân tích mô hình (Command-R)"; +"codex_models_refresh_model_usage" = "Làm mới cách sử dụng mô hình"; +"codex_models_refreshing_last_snapshot" = "Đang làm mới · hiển thị ảnh chụp nhanh thành công lần cuối"; +"codex_models_retry" = "Thử lại"; +"codex_models_reveal_pricing_calculation" = "Tiết lộ cách tính giá"; +"codex_models_search_empty_description" = "Tìm kiếm chỉ lọc bảng này; tổng số trên trang tổng quan không thay đổi."; +"codex_models_search_models" = "Tìm kiếm mẫu"; +"codex_models_search_scope" = "%1$@ · Tìm kiếm chỉ lọc bảng này"; +"codex_models_search_table" = "Bảng mô hình tìm kiếm"; +"codex_models_select_model" = "Chọn %1$@"; +"codex_models_selected" = "Đã chọn"; +"codex_models_selected_bucket" = "Nhóm đã chọn"; +"codex_models_selected_bucket_accessibility" = "Nhóm đã chọn: %1$@, %2$@, mã thông báo %3$@, tham chiếu phiên %4$@, %5$@, tiền tệ %6$@."; +"codex_models_selected_clear_available" = "Model đã chọn %1$@, Có sẵn lựa chọn rõ ràng"; +"codex_models_selected_date" = "Ngày đã chọn"; +"codex_models_selected_hidden_by_search" = "Model đã chọn bị ẩn khi tìm kiếm"; +"codex_models_selected_model" = "Mẫu đã chọn"; +"codex_models_selected_model_interval" = "Model đã chọn · %1$@"; +"codex_models_selected_suffix" = ", đã chọn"; +"codex_models_session_reference_count" = "%1$@ tài liệu tham khảo phiên"; +"codex_models_session_references" = "Tài liệu tham khảo phiên"; +"codex_models_session_references_lowercase" = "tài liệu tham khảo phiên"; +"codex_models_session_refs" = "Giới thiệu phiên"; +"codex_models_share" = "Chia sẻ"; +"codex_models_share_known_cost_help" = "Phần chi phí đã biết; không bao gồm việc sử dụng không được định giá"; +"codex_models_share_of" = "Phần của %1$@"; +"codex_models_show_associated_sessions" = "Hiển thị các phiên liên quan"; +"codex_models_show_details" = "Hiển thị chi tiết"; +"codex_models_showing_last_snapshot" = "Hiển thị ảnh chụp nhanh thành công cuối cùng"; +"codex_models_sorted_by" = "Sắp xếp theo %1$@"; +"codex_models_subject_over_time" = "%1$@ theo thời gian"; +"codex_models_summary" = "Tóm tắt mô hình"; +"codex_models_timeline_accessibility" = "Dòng thời gian của mô hình, %1$@, %2$@"; +"codex_models_timeline_hint" = "Bấm vào một khung thời gian hoặc giữ con trỏ trên một khung thời gian trong 1,5 giây để kiểm tra chi tiết của khung thời gian đó."; +"codex_models_timeline_point_accessibility" = "%1$@, %2$@, %3$@ mã thông báo, tham chiếu phiên %4$@, %5$@, tiền tệ %6$@"; +"codex_models_timeline_summary" = "%1$@ %2$@ dòng thời gian. %3$@"; +"codex_models_token_count" = "%1$@ mã thông báo"; +"codex_models_tokens" = "Mã thông báo"; +"codex_models_tokens_lowercase" = "mã thông báo"; +"codex_models_tokens_per_session_reference" = "Khoảng %1$@ mã thông báo cho mỗi tham chiếu phiên."; +"codex_models_top" = "Hàng đầu"; +"codex_models_top_model" = "Người mẫu hàng đầu"; +"codex_models_top_seven_other" = "Top 7 + Khác"; +"codex_models_top_two_account_for" = "Hai mô hình hàng đầu chiếm %1$@ trong tổng số %2$@"; +"codex_models_top_two_of" = "Hai mẫu hàng đầu của %1$@"; +"codex_models_total_session_references_lowercase" = "tổng số tài liệu tham khảo phiên"; +"codex_models_total_tokens" = "Tổng số token"; +"codex_models_total_tokens_lowercase" = "tổng số token"; +"codex_models_totals_reconcile" = "· Tổng số đối chiếu với ảnh chụp nhanh đang hoạt động"; +"codex_models_track_end" = "Kết thúc bài hát"; +"codex_models_track_start" = "Bắt đầu theo dõi"; +"codex_models_trend_unavailable_observed_buckets" = "Nhóm được quan sát: %1$d. Xu hướng không có sẵn."; +"codex_models_unknown_export_error" = "Lỗi xuất không xác định"; +"codex_models_unpriced_tokens" = "Token chưa được định giá"; +"codex_models_updated" = "Đã cập nhật %1$@"; +"codex_models_usage_and_change" = "Cách sử dụng và thay đổi"; +"codex_models_usage_by_model" = "Cách sử dụng theo model"; +"codex_models_value_comparison_accessibility" = "%1$@, %2$@"; +"codex_models_value_detail" = "%1$@ · %2$@"; +"codex_models_value_share" = "%1$@ · %2$@ chia sẻ"; +"codex_models_value_share_accessibility" = "%1$@, %2$@ chia sẻ"; +"codex_models_value_start" = "Giá trị bắt đầu"; +"codex_models_visible_of_total" = "Các mô hình hiển thị: %1$d của %2$d"; +"codex_models_visible_rows" = "Hàng hiển thị"; +"codex_models_vs_previous" = "%1$@ so với trước đó"; +"codex_models_week_lowercase" = "tuần"; +"codex_workspaces_all_models" = "Tất cả mô hình"; +"codex_workspaces_all_workspaces" = "Tất cả không gian làm việc"; +"codex_workspaces_associated_with_selected_model" = "Liên kết với mô hình đã chọn"; +"codex_workspaces_bullet_value" = "· %1$@"; +"codex_workspaces_chart_keyboard_hint" = "Dùng phím mũi tên trái và phải hoặc thao tác điều chỉnh VoiceOver để chọn ngày. Nhấn Return hoặc phím cách để ghim."; +"codex_workspaces_chats_description" = "Trò chuyện và hội thoại"; +"codex_workspaces_copy_path" = "Sao chép đường dẫn"; +"codex_workspaces_cost_unavailable" = "Không có dữ liệu chi phí"; +"codex_workspaces_daily_callout_accessibility" = "%1$@, %2$@"; +"codex_workspaces_daily_point_accessibility" = "%1$@, mã thông báo %2$@"; +"codex_workspaces_daily_point_help" = "%1$@ — %2$@ mã thông báo"; +"codex_workspaces_daily_token_usage" = "Mức sử dụng token hằng ngày"; +"codex_workspaces_daily_unavailable" = "Không có dữ liệu sử dụng hằng ngày"; +"codex_workspaces_daily_unavailable_description" = "Tổng được tính từ %d phiên nhưng không thể xác định ngày."; +"codex_workspaces_date" = "Ngày"; +"codex_workspaces_days" = "ngày"; +"codex_workspaces_elevated_usage" = "Mức sử dụng tăng"; +"codex_workspaces_elevated_usage_help" = "Mức sử dụng cao hơn trung vị trong giai đoạn đã chọn."; +"codex_workspaces_estimated_cost_short" = "Chi phí ước tính"; +"codex_workspaces_estimated_cost_value" = "Ước tính %@"; +"codex_workspaces_estimated_local_logs" = "Ước tính từ nhật ký Codex cục bộ"; +"codex_workspaces_failed_read_logs" = "Không thể đọc nhật ký Codex"; +"codex_workspaces_failed_some_logs" = "Không thể đọc một số nhật ký Codex"; +"codex_workspaces_finalizing_project_index_files" = "Đang hoàn tất chỉ mục dự án… %d tệp"; +"codex_workspaces_high_usage" = "Mức sử dụng cao"; +"codex_workspaces_high_usage_help" = "Mức sử dụng bất thường đáng kể trong giai đoạn đã chọn."; +"codex_workspaces_indexed_files" = "Đã cập nhật %@ trước · đã lập chỉ mục %d tệp"; +"codex_workspaces_indexing_local_logs" = "Đang lập chỉ mục nhật ký Codex cục bộ…"; +"codex_workspaces_indexing_projects" = "Đang lập chỉ mục dự án…"; +"codex_workspaces_indexing_projects_files_left" = "Đang lập chỉ mục dự án… %d/%d tệp, còn %d"; +"codex_workspaces_last_days" = "%d ngày qua"; +"codex_workspaces_local_codex_chat" = "Cuộc trò chuyện Codex cục bộ"; +"codex_workspaces_model" = "Mô hình"; +"codex_workspaces_models" = "Mô hình"; +"codex_workspaces_no_daily_usage" = "Không có dữ liệu sử dụng hằng ngày trong khoảng lịch sử này."; +"codex_workspaces_no_local_usage" = "Chưa có dữ liệu sử dụng Codex cục bộ"; +"codex_workspaces_no_models" = "Không có dữ liệu phân tích theo mô hình"; +"codex_workspaces_no_projects_or_chats" = "Không tìm thấy dự án hoặc cuộc trò chuyện nào trong khoảng lịch sử này."; +"codex_workspaces_no_sessions" = "Không có phiên nào trong không gian làm việc này"; +"codex_workspaces_normal_usage" = "Mức sử dụng bình thường"; +"codex_workspaces_not_available" = "Không có sẵn"; +"codex_workspaces_overview" = "Tổng quan"; +"codex_workspaces_partial_breakdown_help" = "Phần tóm tắt vẫn là dữ liệu chuẩn; siêu dữ liệu ngày hoặc mô hình không có sẵn được hiển thị là một phần."; +"codex_workspaces_partial_breakdown_notice" = "Một số chi tiết phân tích không bao gồm toàn bộ tổng số."; +"codex_workspaces_partial_data" = "Dữ liệu một phần"; +"codex_workspaces_percent_total" = "Phần trăm tổng"; +"codex_workspaces_refresh_usage_data" = "Làm mới dữ liệu sử dụng"; +"codex_workspaces_relative_usage_accessibility" = "%d phần trăm mức sử dụng không gian làm việc cao nhất"; +"codex_workspaces_relative_usage_tooltip" = "Sử dụng %d%% số token của không gian làm việc có mức sử dụng cao nhất trong giai đoạn này."; +"codex_workspaces_reveal_in_finder" = "Hiển thị trong Finder"; +"codex_workspaces_saving_project_index" = "Đang lưu chỉ mục dự án…"; +"codex_workspaces_scanning_logs" = "Đang quét nhật ký Codex…"; +"codex_workspaces_search_sessions" = "Tìm kiếm phiên"; +"codex_workspaces_selected_day_accessibility" = "%@: %@ token"; +"codex_workspaces_session" = "Phiên"; +"codex_workspaces_session_count" = "Phiên: %d"; +"codex_workspaces_sessions" = "Phiên"; +"codex_workspaces_showing_last_complete_index" = "Đang hiển thị chỉ mục cục bộ hoàn tất gần nhất"; +"codex_workspaces_stale_status" = "Chỉ mục sử dụng đã cũ · cập nhật %@ trước"; +"codex_workspaces_started" = "Đã bắt đầu"; +"codex_workspaces_title" = "Không gian làm việc"; +"codex_workspaces_token_count_accessibility" = "%@ token"; +"codex_workspaces_tokens" = "Token"; +"codex_workspaces_top_model" = "Mô hình hàng đầu"; +"codex_workspaces_top_sessions" = "Phiên hàng đầu"; +"codex_workspaces_total" = "Tổng"; +"codex_workspaces_view" = "Chế độ xem"; +"codex_workspaces_view_all_sessions" = "Xem tất cả phiên"; +"codex_workspaces_window_title" = "Không gian làm việc Codex"; +"codex_workspaces_workspace_count" = "Không gian làm việc và cuộc trò chuyện: %d"; +"codex_workspaces_workspaces_chats" = "Không gian làm việc/cuộc trò chuyện"; diff --git a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings index 06fc0d4755..d13107c8f2 100644 --- a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings @@ -1325,3 +1325,255 @@ "Cost today unavailable" = "今日费用: 不可用"; "30-day cost unavailable" = "30 天费用: 不可用"; "Resets" = "重置"; + +/* Codex Workspaces and Models */ +"codex_workspaces_open_in_window" = "在窗口中打开"; +"codex_models_actions" = "行动"; +"codex_models_active_and_new_count" = "活动模型:%1$d;新活跃:%2$d"; +"codex_models_active_count_new_unavailable" = "活动模型:%1$d;新活跃计数不可用"; +"codex_models_active_models" = "活跃模型"; +"codex_models_alias_count" = "别名:%1$d"; +"codex_models_all_model_count" = "所有型号:%1$d"; +"codex_models_all_models" = "所有型号"; +"codex_models_all_rows" = "所有行"; +"codex_models_analytics" = "模型分析"; +"codex_models_analytics_controls" = "模型分析控制"; +"codex_models_apply" = "申请"; +"codex_models_bucket_count" = "存储桶 (%2$@): %1$d"; +"codex_models_bucket_interval" = "桶间隔"; +"codex_models_bullet_value" = "· %1$@"; +"codex_models_canonical_id" = "规范 ID"; +"codex_models_canonical_model" = "规范模型"; +"codex_models_change" = "改变"; +"codex_models_change_from_peak" = "%1$@ 来自 %2$@ 峰值"; +"codex_models_change_from_previous_period" = "%1$@ 来自上一个同期"; +"codex_models_choose_metric" = "选择分析指标"; +"codex_models_clear_search" = "清除搜索"; +"codex_models_clear_selection" = "清除型号选择"; +"codex_models_columns" = "列"; +"codex_models_compact_billions_suffix" = "B"; +"codex_models_compact_millions_suffix" = "M"; +"codex_models_compact_thousands_suffix" = "K"; +"codex_models_contains_selected_suffix" = ",包含选定的模型"; +"codex_models_copy_aliases" = "复制别名"; +"codex_models_copy_canonical_id" = "复制规范 ID"; +"codex_models_copy_canonical_model_id" = "复制规范模型 ID"; +"codex_models_copy_raw_aliases" = "复制原始别名"; +"codex_models_cost_conclusions_limited" = "成本结论仅限于 %1$@ 价格的代币。"; +"codex_models_cost_hidden" = "隐藏成本"; +"codex_models_cost_status_currency" = "%1$@ · %2$@ · %3$@"; +"codex_models_cost_unavailable" = "成本不可用"; +"codex_models_cost_unavailable_for_data" = "此数据的成本不可用"; +"codex_models_coverage" = "覆盖范围"; +"codex_models_currency" = "货币"; +"codex_models_current_metric" = "当前 %1$@"; +"codex_models_current_period_in_progress" = "当前 %1$@ 正在进行中"; +"codex_models_date" = "日期"; +"codex_models_day_lowercase" = "日"; +"codex_models_days" = "天"; +"codex_models_delta_vs_prior" = "Δ 与之前相比"; +"codex_models_disabled_in_settings" = "在设置中禁用"; +"codex_models_dominates_at" = "此模型在 %2$@ 处主导 %1$@。"; +"codex_models_ended" = "结束"; +"codex_models_export" = "导出"; +"codex_models_export_all_filename" = "codex-models-all.csv"; +"codex_models_export_default_filename" = "codex-models.csv"; +"codex_models_export_failed" = "无法导出模型分析"; +"codex_models_export_selected_filename" = "codex-model-%1$@.csv"; +"codex_models_export_selected_model" = "导出选定的模型"; +"codex_models_export_visible_filename" = "codex-models-visible.csv"; +"codex_models_export_visible_rows" = "导出可见模型行"; +"codex_models_focus_table_search" = "焦点模型表搜索"; +"codex_models_fully_priced" = "全价"; +"codex_models_fully_priced_percent" = "100% 定价"; +"codex_models_generated" = "生成%1$@"; +"codex_models_granularity_accessibility" = "粒度,%1$@,%2$@"; +"codex_models_hidden" = "隐藏"; +"codex_models_identity_aliases_help" = "%1$@ · %2$@ · 别名:%3$@"; +"codex_models_identity_and_aliases" = "身份和别名"; +"codex_models_indexing_last_days" = "正在对过去 %1$d 天建立索引..."; +"codex_models_interpretation" = "解释"; +"codex_models_known_cost" = "已知成本"; +"codex_models_known_cost_fully_priced_accessibility" = "已知成本 %1$@,全价"; +"codex_models_known_cost_lowercase" = "已知成本"; +"codex_models_known_cost_partial_accessibility" = "已知成本 %1$@,部分,%2$@ 的代币定价"; +"codex_models_last_days" = "过去 %1$d 天"; +"codex_models_local_logs_source" = "· 当地法典日志"; +"codex_models_major_contributor_at" = "该模型是 %1$@ 的主要贡献者。"; +"codex_models_metric" = "公制"; +"codex_models_metric_over_time" = "%1$@ 随着时间的推移"; +"codex_models_model" = "型号"; +"codex_models_model_concentration" = "模型浓度"; +"codex_models_model_count" = "型号:%1$d"; +"codex_models_model_usage" = "模型用途"; +"codex_models_month_lowercase" = "月"; +"codex_models_more_alias_count" = "更多别名:%1$d"; +"codex_models_name_canonical_id" = "%1$@,规范 ID %2$@"; +"codex_models_name_value_accessibility" = "%1$@,%2$@"; +"codex_models_named_metric_over_time" = "%1$@ %2$@ 随着时间的推移"; +"codex_models_new" = "新"; +"codex_models_new_in_period" = "本期新增"; +"codex_models_newly_active_count" = "新活跃:%1$d"; +"codex_models_no_applicable_price" = "无适用价格"; +"codex_models_no_change_from_previous_period" = "与上一同期相比没有变化"; +"codex_models_no_matching_models" = "没有匹配的型号"; +"codex_models_no_newly_active" = "没有新的活跃模型"; +"codex_models_no_top_model" = "无顶级型号"; +"codex_models_no_usage" = "没有使用"; +"codex_models_no_usage_description" = "在上方选择较长的日期范围或在保存本地 Codex 会话后刷新。"; +"codex_models_no_usage_in_period" = "在此期间没有使用"; +"codex_models_no_usage_in_range" = "此范围内没有型号使用"; +"codex_models_other_count" = "其他 (%1$d)"; +"codex_models_partial_priced" = "部分 · %1$@ 已定价"; +"codex_models_precision_table" = "型号精度表"; +"codex_models_previous_known_cost" = "先前已知的成本"; +"codex_models_previous_metric" = "上一个 %1$@"; +"codex_models_previous_period_unavailable" = "上一期间不可用"; +"codex_models_previous_unavailable" = "以前不可用"; +"codex_models_priced_tokens" = "定价代币"; +"codex_models_pricing_calculation" = "定价计算"; +"codex_models_pricing_coverage" = "定价范围"; +"codex_models_pricing_explanation" = "%1$@ 涵盖 %2$@ 定价的代币。没有适用规则的 %3$@ 令牌被排除,不估计为零。"; +"codex_models_ranking_by" = "%1$@ 模型排名"; +"codex_models_ranking_hint" = "单击模型栏以打开其详细信息。"; +"codex_models_ranking_point_accessibility" = "%1$@、%2$@、%3$@ 份额"; +"codex_models_ranking_summary" = "规范模型和其他按 %1$@ 排名。"; +"codex_models_ref_count" = "%1$@ 参考文献"; +"codex_models_refresh_analytics" = "刷新模型分析"; +"codex_models_refresh_command_help" = "刷新模型分析 (Command-R)"; +"codex_models_refresh_model_usage" = "刷新模型用法"; +"codex_models_refreshing_last_snapshot" = "刷新·显示上次成功的快照"; +"codex_models_retry" = "重试"; +"codex_models_reveal_pricing_calculation" = "揭示定价计算"; +"codex_models_search_empty_description" = "搜索仅过滤此表;仪表板总计未变化。"; +"codex_models_search_models" = "搜索型号"; +"codex_models_search_scope" = "%1$@ · 搜索仅过滤此表"; +"codex_models_search_table" = "搜索型号表"; +"codex_models_select_model" = "选择%1$@"; +"codex_models_selected" = "已选择"; +"codex_models_selected_bucket" = "选定的桶"; +"codex_models_selected_bucket_accessibility" = "选定的存储桶:%1$@、%2$@、%3$@ 令牌、%4$@ 会话引用、%5$@、货币 %6$@。"; +"codex_models_selected_clear_available" = "已选择型号 %1$@,可清除选择"; +"codex_models_selected_date" = "选择日期"; +"codex_models_selected_hidden_by_search" = "所选模型被搜索隐藏"; +"codex_models_selected_model" = "所选型号"; +"codex_models_selected_model_interval" = "所选型号 · %1$@"; +"codex_models_selected_suffix" = ",已选择"; +"codex_models_session_reference_count" = "%1$@ 会话引用"; +"codex_models_session_references" = "会话参考"; +"codex_models_session_references_lowercase" = "会话参考"; +"codex_models_session_refs" = "会话参考"; +"codex_models_share" = "分享"; +"codex_models_share_known_cost_help" = "已知成本份额;不包括无价使用"; +"codex_models_share_of" = "%1$@ 份额"; +"codex_models_show_associated_sessions" = "显示关联会话"; +"codex_models_show_details" = "显示详细信息"; +"codex_models_showing_last_snapshot" = "显示最后一个成功的快照"; +"codex_models_sorted_by" = "按 %1$@ 排序"; +"codex_models_subject_over_time" = "%1$@ 随着时间的推移"; +"codex_models_summary" = "型号总结"; +"codex_models_timeline_accessibility" = "模型时间线,%1$@,%2$@"; +"codex_models_timeline_hint" = "单击时间段,或将指针悬停在时间段上 1.5 秒,以检查其详细信息。"; +"codex_models_timeline_point_accessibility" = "%1$@、%2$@、%3$@ 令牌、%4$@ 会话引用、%5$@、货币 %6$@"; +"codex_models_timeline_summary" = "%1$@ %2$@ 时间线。 %3$@"; +"codex_models_token_count" = "%1$@ 代币"; +"codex_models_tokens" = "代币"; +"codex_models_tokens_lowercase" = "代币"; +"codex_models_tokens_per_session_reference" = "每个会话引用大约 %1$@ 令牌。"; +"codex_models_top" = "顶部"; +"codex_models_top_model" = "顶级模特"; +"codex_models_top_seven_other" = "前 7 名 + 其他"; +"codex_models_top_two_account_for" = "前两个模型占 %1$@(共 %2$@)"; +"codex_models_top_two_of" = "%1$@ 的前两个型号"; +"codex_models_total_session_references_lowercase" = "总会话参考数"; +"codex_models_total_tokens" = "代币总数"; +"codex_models_total_tokens_lowercase" = "总代币"; +"codex_models_totals_reconcile" = "· 总计与活动快照一致"; +"codex_models_track_end" = "轨道终点"; +"codex_models_track_start" = "轨道开始"; +"codex_models_trend_unavailable_observed_buckets" = "观察到的存储桶:%1$d。趋势不可用。"; +"codex_models_unknown_export_error" = "未知的导出错误"; +"codex_models_unpriced_tokens" = "未定价的代币"; +"codex_models_updated" = "已更新%1$@"; +"codex_models_usage_and_change" = "使用和变化"; +"codex_models_usage_by_model" = "按型号使用"; +"codex_models_value_comparison_accessibility" = "%1$@,%2$@"; +"codex_models_value_detail" = "%1$@ · %2$@"; +"codex_models_value_share" = "%1$@ · %2$@ 份额"; +"codex_models_value_share_accessibility" = "%1$@,%2$@ 份额"; +"codex_models_value_start" = "值开始"; +"codex_models_visible_of_total" = "可见模型:%1$d,共 %2$d"; +"codex_models_visible_rows" = "可见行"; +"codex_models_vs_previous" = "%1$@ 与之前相比"; +"codex_models_week_lowercase" = "周"; +"codex_workspaces_all_models" = "所有模型"; +"codex_workspaces_all_workspaces" = "所有工作区"; +"codex_workspaces_associated_with_selected_model" = "与所选型号相关"; +"codex_workspaces_bullet_value" = "· %1$@"; +"codex_workspaces_chart_keyboard_hint" = "使用左右箭头键或 VoiceOver 调整操作选择日期。按 Return 或空格键固定。"; +"codex_workspaces_chats_description" = "聊天与对话"; +"codex_workspaces_copy_path" = "复制路径"; +"codex_workspaces_cost_unavailable" = "费用不可用"; +"codex_workspaces_daily_callout_accessibility" = "%1$@,%2$@"; +"codex_workspaces_daily_point_accessibility" = "%1$@,代币%2$@"; +"codex_workspaces_daily_point_help" = "%1$@ — %2$@ 令牌"; +"codex_workspaces_daily_token_usage" = "每日令牌使用量"; +"codex_workspaces_daily_unavailable" = "每日使用情况不可用"; +"codex_workspaces_daily_unavailable_description" = "总计根据 %d 个会话计算,但无法确定其日期。"; +"codex_workspaces_date" = "日期"; +"codex_workspaces_days" = "天"; +"codex_workspaces_elevated_usage" = "使用量偏高"; +"codex_workspaces_elevated_usage_help" = "使用量高于所选期间的中位数。"; +"codex_workspaces_estimated_cost_short" = "预估费用"; +"codex_workspaces_estimated_cost_value" = "预估 %@"; +"codex_workspaces_estimated_local_logs" = "根据本地 Codex 日志估算"; +"codex_workspaces_failed_read_logs" = "无法读取 Codex 日志"; +"codex_workspaces_failed_some_logs" = "无法读取部分 Codex 日志"; +"codex_workspaces_finalizing_project_index_files" = "正在完成项目索引… %d 个文件"; +"codex_workspaces_high_usage" = "高使用量"; +"codex_workspaces_high_usage_help" = "所选期间的使用量明显异常。"; +"codex_workspaces_indexed_files" = "%@前更新 · 已索引 %d 个文件"; +"codex_workspaces_indexing_local_logs" = "正在索引本地 Codex 日志…"; +"codex_workspaces_indexing_projects" = "正在索引项目…"; +"codex_workspaces_indexing_projects_files_left" = "正在索引项目… %d/%d 个文件,剩余 %d 个"; +"codex_workspaces_last_days" = "最近 %d 天"; +"codex_workspaces_local_codex_chat" = "本地 Codex 聊天"; +"codex_workspaces_model" = "模型"; +"codex_workspaces_models" = "模型"; +"codex_workspaces_no_daily_usage" = "此历史时间范围内没有每日使用记录。"; +"codex_workspaces_no_local_usage" = "尚无本地 Codex 使用记录"; +"codex_workspaces_no_models" = "无可用的模型明细"; +"codex_workspaces_no_projects_or_chats" = "此历史时间范围内未找到项目或聊天。"; +"codex_workspaces_no_sessions" = "此工作区中没有会话"; +"codex_workspaces_normal_usage" = "正常使用量"; +"codex_workspaces_not_available" = "不可用"; +"codex_workspaces_overview" = "概览"; +"codex_workspaces_partial_breakdown_help" = "汇总仍为权威数据,缺失的日期或模型元数据显示为部分数据。"; +"codex_workspaces_partial_breakdown_notice" = "部分详细明细未涵盖完整总计。"; +"codex_workspaces_partial_data" = "部分数据"; +"codex_workspaces_percent_total" = "占总计的百分比"; +"codex_workspaces_refresh_usage_data" = "刷新使用情况数据"; +"codex_workspaces_relative_usage_accessibility" = "最高工作区使用量的百分之 %d"; +"codex_workspaces_relative_usage_tooltip" = "使用了此期间使用量最高工作区的 %d%% 令牌。"; +"codex_workspaces_reveal_in_finder" = "在 Finder 中显示"; +"codex_workspaces_saving_project_index" = "正在保存项目索引…"; +"codex_workspaces_scanning_logs" = "正在扫描 Codex 日志…"; +"codex_workspaces_search_sessions" = "搜索会话"; +"codex_workspaces_selected_day_accessibility" = "%@:%@ 个令牌"; +"codex_workspaces_session" = "会话"; +"codex_workspaces_session_count" = "会话:%d"; +"codex_workspaces_sessions" = "会话"; +"codex_workspaces_showing_last_complete_index" = "正在显示上次完成的本地索引"; +"codex_workspaces_stale_status" = "使用情况索引已过期 · %@前更新"; +"codex_workspaces_started" = "已开始"; +"codex_workspaces_title" = "工作区"; +"codex_workspaces_token_count_accessibility" = "%@ 个令牌"; +"codex_workspaces_tokens" = "令牌"; +"codex_workspaces_top_model" = "热门模型"; +"codex_workspaces_top_sessions" = "热门会话"; +"codex_workspaces_total" = "总计"; +"codex_workspaces_view" = "视图"; +"codex_workspaces_view_all_sessions" = "查看所有会话"; +"codex_workspaces_window_title" = "Codex 工作区"; +"codex_workspaces_workspace_count" = "工作区和聊天:%d"; +"codex_workspaces_workspaces_chats" = "工作区/聊天"; diff --git a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings index ec01ed18f3..0929e4139c 100644 --- a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings @@ -1380,3 +1380,255 @@ "Cost today unavailable" = "今日費用: 無法使用"; "30-day cost unavailable" = "30 天費用: 無法使用"; "Resets" = "重設"; + +/* Codex Workspaces and Models */ +"codex_workspaces_open_in_window" = "在視窗中開啟"; +"codex_models_actions" = "行動"; +"codex_models_active_and_new_count" = "活動模型:%1$d;新活躍:%2$d"; +"codex_models_active_count_new_unavailable" = "活動模型:%1$d;新活躍計數不可用"; +"codex_models_active_models" = "活躍模型"; +"codex_models_alias_count" = "別名:%1$d"; +"codex_models_all_model_count" = "所有型號:%1$d"; +"codex_models_all_models" = "所有型號"; +"codex_models_all_rows" = "所有行"; +"codex_models_analytics" = "模型分析"; +"codex_models_analytics_controls" = "模型分析控制"; +"codex_models_apply" = "申請"; +"codex_models_bucket_count" = "存儲桶 (%2$@): %1$d"; +"codex_models_bucket_interval" = "桶間隔"; +"codex_models_bullet_value" = "· %1$@"; +"codex_models_canonical_id" = "規範 ID"; +"codex_models_canonical_model" = "規範模型"; +"codex_models_change" = "改變"; +"codex_models_change_from_peak" = "%1$@ 來自 %2$@ 峰值"; +"codex_models_change_from_previous_period" = "%1$@ 來自上一個同期"; +"codex_models_choose_metric" = "選擇分析指標"; +"codex_models_clear_search" = "清除搜索"; +"codex_models_clear_selection" = "清除型號選擇"; +"codex_models_columns" = "列"; +"codex_models_compact_billions_suffix" = "B"; +"codex_models_compact_millions_suffix" = "M"; +"codex_models_compact_thousands_suffix" = "K"; +"codex_models_contains_selected_suffix" = ",包含選定的模型"; +"codex_models_copy_aliases" = "複製別名"; +"codex_models_copy_canonical_id" = "複製規範 ID"; +"codex_models_copy_canonical_model_id" = "複製規範模型 ID"; +"codex_models_copy_raw_aliases" = "複製原始別名"; +"codex_models_cost_conclusions_limited" = "成本結論僅限於 %1$@ 價格的代幣。"; +"codex_models_cost_hidden" = "隱藏成本"; +"codex_models_cost_status_currency" = "%1$@ · %2$@ · %3$@"; +"codex_models_cost_unavailable" = "成本不可用"; +"codex_models_cost_unavailable_for_data" = "此數據的成本不可用"; +"codex_models_coverage" = "覆蓋範圍"; +"codex_models_currency" = "貨幣"; +"codex_models_current_metric" = "當前 %1$@"; +"codex_models_current_period_in_progress" = "當前 %1$@ 正在進行中"; +"codex_models_date" = "日期"; +"codex_models_day_lowercase" = "日"; +"codex_models_days" = "天"; +"codex_models_delta_vs_prior" = "Δ 與之前相比"; +"codex_models_disabled_in_settings" = "在設置中禁用"; +"codex_models_dominates_at" = "此模型在 %2$@ 處主導 %1$@。"; +"codex_models_ended" = "結束"; +"codex_models_export" = "導出"; +"codex_models_export_all_filename" = "codex-models-all.csv"; +"codex_models_export_default_filename" = "codex-models.csv"; +"codex_models_export_failed" = "無法導出模型分析"; +"codex_models_export_selected_filename" = "codex-model-%1$@.csv"; +"codex_models_export_selected_model" = "導出選定的模型"; +"codex_models_export_visible_filename" = "codex-models-visible.csv"; +"codex_models_export_visible_rows" = "導出可見模型行"; +"codex_models_focus_table_search" = "焦點模型表搜索"; +"codex_models_fully_priced" = "全價"; +"codex_models_fully_priced_percent" = "100% 定價"; +"codex_models_generated" = "生成%1$@"; +"codex_models_granularity_accessibility" = "粒度,%1$@,%2$@"; +"codex_models_hidden" = "隱藏"; +"codex_models_identity_aliases_help" = "%1$@ · %2$@ · 別名:%3$@"; +"codex_models_identity_and_aliases" = "身份和別名"; +"codex_models_indexing_last_days" = "正在對過去 %1$d 天建立索引..."; +"codex_models_interpretation" = "解釋"; +"codex_models_known_cost" = "已知成本"; +"codex_models_known_cost_fully_priced_accessibility" = "已知成本 %1$@,全價"; +"codex_models_known_cost_lowercase" = "已知成本"; +"codex_models_known_cost_partial_accessibility" = "已知成本 %1$@,部分,%2$@ 的代幣定價"; +"codex_models_last_days" = "過去 %1$d 天"; +"codex_models_local_logs_source" = "· 當地法典日誌"; +"codex_models_major_contributor_at" = "該模型是 %1$@ 的主要貢獻者。"; +"codex_models_metric" = "公制"; +"codex_models_metric_over_time" = "%1$@ 隨著時間的推移"; +"codex_models_model" = "型號"; +"codex_models_model_concentration" = "模型濃度"; +"codex_models_model_count" = "型號:%1$d"; +"codex_models_model_usage" = "模型用途"; +"codex_models_month_lowercase" = "月"; +"codex_models_more_alias_count" = "更多別名:%1$d"; +"codex_models_name_canonical_id" = "%1$@,規範 ID %2$@"; +"codex_models_name_value_accessibility" = "%1$@,%2$@"; +"codex_models_named_metric_over_time" = "%1$@ %2$@ 隨著時間的推移"; +"codex_models_new" = "新"; +"codex_models_new_in_period" = "本期新增"; +"codex_models_newly_active_count" = "新活躍:%1$d"; +"codex_models_no_applicable_price" = "無適用價格"; +"codex_models_no_change_from_previous_period" = "與上一同期相比沒有變化"; +"codex_models_no_matching_models" = "沒有匹配的型號"; +"codex_models_no_newly_active" = "沒有新的活躍模型"; +"codex_models_no_top_model" = "無頂級型號"; +"codex_models_no_usage" = "沒有使用"; +"codex_models_no_usage_description" = "在上方選擇較長的日期範圍或在保存本地 Codex 會話後刷新。"; +"codex_models_no_usage_in_period" = "在此期間沒有使用"; +"codex_models_no_usage_in_range" = "此範圍內沒有型號使用"; +"codex_models_other_count" = "其他 (%1$d)"; +"codex_models_partial_priced" = "部分 · %1$@ 已定價"; +"codex_models_precision_table" = "型號精度表"; +"codex_models_previous_known_cost" = "先前已知的成本"; +"codex_models_previous_metric" = "上一個 %1$@"; +"codex_models_previous_period_unavailable" = "上一期間不可用"; +"codex_models_previous_unavailable" = "以前不可用"; +"codex_models_priced_tokens" = "定價代幣"; +"codex_models_pricing_calculation" = "定價計算"; +"codex_models_pricing_coverage" = "定價範圍"; +"codex_models_pricing_explanation" = "%1$@ 涵蓋 %2$@ 定價的代幣。沒有適用規則的 %3$@ 令牌被排除,不估計為零。"; +"codex_models_ranking_by" = "%1$@ 模型排名"; +"codex_models_ranking_hint" = "單擊模型欄以打開其詳細信息。"; +"codex_models_ranking_point_accessibility" = "%1$@、%2$@、%3$@ 份額"; +"codex_models_ranking_summary" = "規範模型和其他按 %1$@ 排名。"; +"codex_models_ref_count" = "%1$@ 參考文獻"; +"codex_models_refresh_analytics" = "刷新模型分析"; +"codex_models_refresh_command_help" = "刷新模型分析 (Command-R)"; +"codex_models_refresh_model_usage" = "刷新模型用法"; +"codex_models_refreshing_last_snapshot" = "刷新·顯示上次成功的快照"; +"codex_models_retry" = "重試"; +"codex_models_reveal_pricing_calculation" = "揭示定價計算"; +"codex_models_search_empty_description" = "搜索僅過濾此表;儀表板總計未變化。"; +"codex_models_search_models" = "搜索型號"; +"codex_models_search_scope" = "%1$@ · 搜索僅過濾此表"; +"codex_models_search_table" = "搜索型號表"; +"codex_models_select_model" = "選擇%1$@"; +"codex_models_selected" = "已選擇"; +"codex_models_selected_bucket" = "選定的桶"; +"codex_models_selected_bucket_accessibility" = "選定的存儲桶:%1$@、%2$@、%3$@ 令牌、%4$@ 會話引用、%5$@、貨幣 %6$@。"; +"codex_models_selected_clear_available" = "已選擇型號 %1$@,可清除選擇"; +"codex_models_selected_date" = "選擇日期"; +"codex_models_selected_hidden_by_search" = "所選模型被搜索隱藏"; +"codex_models_selected_model" = "所選型號"; +"codex_models_selected_model_interval" = "所選型號 · %1$@"; +"codex_models_selected_suffix" = ",已選擇"; +"codex_models_session_reference_count" = "%1$@ 會話引用"; +"codex_models_session_references" = "會話參考"; +"codex_models_session_references_lowercase" = "會話參考"; +"codex_models_session_refs" = "會話參考"; +"codex_models_share" = "分享"; +"codex_models_share_known_cost_help" = "已知成本份額;不包括無價使用"; +"codex_models_share_of" = "%1$@ 份額"; +"codex_models_show_associated_sessions" = "顯示關聯會話"; +"codex_models_show_details" = "顯示詳細信息"; +"codex_models_showing_last_snapshot" = "顯示最後一個成功的快照"; +"codex_models_sorted_by" = "按 %1$@ 排序"; +"codex_models_subject_over_time" = "%1$@ 隨著時間的推移"; +"codex_models_summary" = "型號總結"; +"codex_models_timeline_accessibility" = "模型時間線,%1$@,%2$@"; +"codex_models_timeline_hint" = "單擊時間段,或將指針懸停在時間段上 1.5 秒,以檢查其詳細信息。"; +"codex_models_timeline_point_accessibility" = "%1$@、%2$@、%3$@ 令牌、%4$@ 會話引用、%5$@、貨幣 %6$@"; +"codex_models_timeline_summary" = "%1$@ %2$@ 時間線。 %3$@"; +"codex_models_token_count" = "%1$@ 代幣"; +"codex_models_tokens" = "代幣"; +"codex_models_tokens_lowercase" = "代幣"; +"codex_models_tokens_per_session_reference" = "每個會話引用大約 %1$@ 令牌。"; +"codex_models_top" = "頂部"; +"codex_models_top_model" = "頂級模特"; +"codex_models_top_seven_other" = "前 7 名 + 其他"; +"codex_models_top_two_account_for" = "前兩個模型佔 %1$@(共 %2$@)"; +"codex_models_top_two_of" = "%1$@ 的前兩個型號"; +"codex_models_total_session_references_lowercase" = "總會話參考數"; +"codex_models_total_tokens" = "代幣總數"; +"codex_models_total_tokens_lowercase" = "總代幣"; +"codex_models_totals_reconcile" = "· 總計與活動快照一致"; +"codex_models_track_end" = "軌道終點"; +"codex_models_track_start" = "軌道開始"; +"codex_models_trend_unavailable_observed_buckets" = "觀察到的存儲桶:%1$d。趨勢不可用。"; +"codex_models_unknown_export_error" = "未知的導出錯誤"; +"codex_models_unpriced_tokens" = "未定價的代幣"; +"codex_models_updated" = "已更新%1$@"; +"codex_models_usage_and_change" = "使用和變化"; +"codex_models_usage_by_model" = "按型號使用"; +"codex_models_value_comparison_accessibility" = "%1$@,%2$@"; +"codex_models_value_detail" = "%1$@ · %2$@"; +"codex_models_value_share" = "%1$@ · %2$@ 份額"; +"codex_models_value_share_accessibility" = "%1$@,%2$@ 份額"; +"codex_models_value_start" = "值開始"; +"codex_models_visible_of_total" = "可見模型:%1$d,共 %2$d"; +"codex_models_visible_rows" = "可見行"; +"codex_models_vs_previous" = "%1$@ 與之前相比"; +"codex_models_week_lowercase" = "周"; +"codex_workspaces_all_models" = "所有模型"; +"codex_workspaces_all_workspaces" = "所有工作區"; +"codex_workspaces_associated_with_selected_model" = "與所選型號相關"; +"codex_workspaces_bullet_value" = "· %1$@"; +"codex_workspaces_chart_keyboard_hint" = "使用左右方向鍵或 VoiceOver 調整動作選擇日期。按 Return 或空白鍵固定。"; +"codex_workspaces_chats_description" = "聊天與對話"; +"codex_workspaces_copy_path" = "複製路徑"; +"codex_workspaces_cost_unavailable" = "無法取得費用"; +"codex_workspaces_daily_callout_accessibility" = "%1$@,%2$@"; +"codex_workspaces_daily_point_accessibility" = "%1$@,代幣%2$@"; +"codex_workspaces_daily_point_help" = "%1$@ — %2$@ 令牌"; +"codex_workspaces_daily_token_usage" = "每日權杖使用量"; +"codex_workspaces_daily_unavailable" = "無法取得每日使用情況"; +"codex_workspaces_daily_unavailable_description" = "總計根據 %d 個工作階段計算,但無法判定其日期。"; +"codex_workspaces_date" = "日期"; +"codex_workspaces_days" = "天"; +"codex_workspaces_elevated_usage" = "使用量偏高"; +"codex_workspaces_elevated_usage_help" = "使用量高於所選期間的中位數。"; +"codex_workspaces_estimated_cost_short" = "預估費用"; +"codex_workspaces_estimated_cost_value" = "預估 %@"; +"codex_workspaces_estimated_local_logs" = "根據本機 Codex 日誌估算"; +"codex_workspaces_failed_read_logs" = "無法讀取 Codex 日誌"; +"codex_workspaces_failed_some_logs" = "無法讀取部分 Codex 日誌"; +"codex_workspaces_finalizing_project_index_files" = "正在完成專案索引… %d 個檔案"; +"codex_workspaces_high_usage" = "高使用量"; +"codex_workspaces_high_usage_help" = "所選期間的使用量明顯異常。"; +"codex_workspaces_indexed_files" = "%@前更新 · 已建立 %d 個檔案的索引"; +"codex_workspaces_indexing_local_logs" = "正在建立本機 Codex 日誌索引…"; +"codex_workspaces_indexing_projects" = "正在建立專案索引…"; +"codex_workspaces_indexing_projects_files_left" = "正在建立專案索引… %d/%d 個檔案,剩餘 %d 個"; +"codex_workspaces_last_days" = "最近 %d 天"; +"codex_workspaces_local_codex_chat" = "本機 Codex 聊天"; +"codex_workspaces_model" = "模型"; +"codex_workspaces_models" = "模型"; +"codex_workspaces_no_daily_usage" = "此歷史時間範圍內沒有每日使用記錄。"; +"codex_workspaces_no_local_usage" = "尚無本機 Codex 使用記錄"; +"codex_workspaces_no_models" = "沒有可用的模型明細"; +"codex_workspaces_no_projects_or_chats" = "此歷史時間範圍內找不到專案或聊天。"; +"codex_workspaces_no_sessions" = "此工作區中沒有工作階段"; +"codex_workspaces_normal_usage" = "正常使用量"; +"codex_workspaces_not_available" = "不可用"; +"codex_workspaces_overview" = "概覽"; +"codex_workspaces_partial_breakdown_help" = "摘要仍為權威資料,缺少的日期或模型中繼資料會顯示為部分資料。"; +"codex_workspaces_partial_breakdown_notice" = "部分詳細明細未涵蓋完整總計。"; +"codex_workspaces_partial_data" = "部分資料"; +"codex_workspaces_percent_total" = "佔總計的百分比"; +"codex_workspaces_refresh_usage_data" = "重新整理使用情況資料"; +"codex_workspaces_relative_usage_accessibility" = "最高工作區使用量的百分之 %d"; +"codex_workspaces_relative_usage_tooltip" = "使用了此期間使用量最高工作區的 %d%% 權杖。"; +"codex_workspaces_reveal_in_finder" = "在 Finder 中顯示"; +"codex_workspaces_saving_project_index" = "正在儲存專案索引…"; +"codex_workspaces_scanning_logs" = "正在掃描 Codex 日誌…"; +"codex_workspaces_search_sessions" = "搜尋工作階段"; +"codex_workspaces_selected_day_accessibility" = "%@:%@ 個權杖"; +"codex_workspaces_session" = "工作階段"; +"codex_workspaces_session_count" = "工作階段:%d"; +"codex_workspaces_sessions" = "工作階段"; +"codex_workspaces_showing_last_complete_index" = "正在顯示上次完成的本機索引"; +"codex_workspaces_stale_status" = "使用情況索引已過期 · %@前更新"; +"codex_workspaces_started" = "已開始"; +"codex_workspaces_title" = "工作區"; +"codex_workspaces_token_count_accessibility" = "%@ 個權杖"; +"codex_workspaces_tokens" = "權杖"; +"codex_workspaces_top_model" = "熱門模型"; +"codex_workspaces_top_sessions" = "熱門工作階段"; +"codex_workspaces_total" = "總計"; +"codex_workspaces_view" = "顯示方式"; +"codex_workspaces_view_all_sessions" = "檢視所有工作階段"; +"codex_workspaces_window_title" = "Codex 工作區"; +"codex_workspaces_workspace_count" = "工作區和聊天:%d"; +"codex_workspaces_workspaces_chats" = "工作區/聊天"; diff --git a/Sources/CodexBar/SettingsStore+Defaults.swift b/Sources/CodexBar/SettingsStore+Defaults.swift index c051d5b39b..b3e7978a6c 100644 --- a/Sources/CodexBar/SettingsStore+Defaults.swift +++ b/Sources/CodexBar/SettingsStore+Defaults.swift @@ -546,6 +546,47 @@ extension SettingsStore { set { self.costSummaryDisplayStyleRaw = newValue.rawValue } } + var codexLocalProjectUsageEnabled: Bool { + get { self.defaultsState.codexLocalProjectUsageEnabledOverride ?? self.costUsageEnabled } + set { + let previousEffectiveValue = self.codexLocalProjectUsageEnabled + self.defaultsState.codexLocalProjectUsageEnabledOverride = newValue + self.userDefaults.set(newValue, forKey: "codexLocalProjectUsageEnabled") + if previousEffectiveValue != self.codexLocalProjectUsageEnabled { + self.noteBackgroundWorkSettingsChanged() + } + } + } + + var codexLocalProjectUsageEnabledOverride: Bool? { + self.defaultsState.codexLocalProjectUsageEnabledOverride + } + + func clearCodexLocalProjectUsageEnabledOverride() { + let previousEffectiveValue = self.codexLocalProjectUsageEnabled + self.defaultsState.codexLocalProjectUsageEnabledOverride = nil + self.userDefaults.removeObject(forKey: "codexLocalProjectUsageEnabled") + if previousEffectiveValue != self.codexLocalProjectUsageEnabled { + self.noteBackgroundWorkSettingsChanged() + } + } + + var codexLocalProjectUsageShowsEstimatedCost: Bool { + get { self.defaultsState.codexLocalProjectUsageShowsEstimatedCost } + set { + self.defaultsState.codexLocalProjectUsageShowsEstimatedCost = newValue + self.userDefaults.set(newValue, forKey: "codexLocalProjectUsageShowsEstimatedCost") + } + } + + var codexLocalProjectUsageIncludesCachedInput: Bool { + get { self.defaultsState.codexLocalProjectUsageIncludesCachedInput } + set { + self.defaultsState.codexLocalProjectUsageIncludesCachedInput = newValue + self.userDefaults.set(newValue, forKey: "codexLocalProjectUsageIncludesCachedInput") + } + } + var hidePersonalInfo: Bool { get { self.defaultsState.hidePersonalInfo } set { diff --git a/Sources/CodexBar/SettingsStore+MenuObservation.swift b/Sources/CodexBar/SettingsStore+MenuObservation.swift index f4ecdee6ff..2c6976800d 100644 --- a/Sources/CodexBar/SettingsStore+MenuObservation.swift +++ b/Sources/CodexBar/SettingsStore+MenuObservation.swift @@ -46,6 +46,9 @@ extension SettingsStore { _ = self.costUsageHistoryDays _ = self.costComparisonPeriodsEnabled _ = self.costSummaryDisplayStyle + _ = self.codexLocalProjectUsageEnabled + _ = self.codexLocalProjectUsageShowsEstimatedCost + _ = self.codexLocalProjectUsageIncludesCachedInput _ = self.appLanguage _ = self.hidePersonalInfo _ = self.randomBlinkEnabled diff --git a/Sources/CodexBar/SettingsStore.swift b/Sources/CodexBar/SettingsStore.swift index f453205888..ca43825ebd 100644 --- a/Sources/CodexBar/SettingsStore.swift +++ b/Sources/CodexBar/SettingsStore.swift @@ -476,6 +476,8 @@ extension SettingsStore { let costSummaryDisplayStyleRaw = Self.loadCostSummaryDisplayStyleRaw( userDefaults: userDefaults, costUsageEnabled: costUsageEnabled) + let projectUsageDefaults = Self.loadCodexLocalProjectUsageDefaults( + userDefaults: userDefaults) let hidePersonalInfo = userDefaults.object(forKey: "hidePersonalInfo") as? Bool ?? false let randomBlinkEnabled = userDefaults.object(forKey: "randomBlinkEnabled") as? Bool ?? false let confettiOnReset = Self.loadConfettiOnResetDefaults(userDefaults: userDefaults) @@ -578,6 +580,9 @@ extension SettingsStore { costUsageHistoryDays: costUsageHistoryDays, costComparisonPeriodsEnabled: costComparisonPeriodsEnabled, costSummaryDisplayStyleRaw: costSummaryDisplayStyleRaw, + codexLocalProjectUsageEnabledOverride: projectUsageDefaults.enabledOverride, + codexLocalProjectUsageShowsEstimatedCost: projectUsageDefaults.showsEstimatedCost, + codexLocalProjectUsageIncludesCachedInput: projectUsageDefaults.includesCachedInput, hidePersonalInfo: hidePersonalInfo, randomBlinkEnabled: randomBlinkEnabled, confettiOnSessionLimitResetsEnabled: confettiOnReset.session, @@ -689,6 +694,18 @@ extension SettingsStore { weekly: userDefaults.object(forKey: "confettiOnWeeklyLimitResetsEnabled") as? Bool ?? false) } + private static func loadCodexLocalProjectUsageDefaults( + userDefaults: UserDefaults) + -> (enabledOverride: Bool?, showsEstimatedCost: Bool, includesCachedInput: Bool) + { + let enabledOverride = userDefaults.object(forKey: "codexLocalProjectUsageEnabled") as? Bool + let showsEstimatedCost = userDefaults.object( + forKey: "codexLocalProjectUsageShowsEstimatedCost") as? Bool ?? true + let includesCachedInput = userDefaults.object( + forKey: "codexLocalProjectUsageIncludesCachedInput") as? Bool ?? true + return (enabledOverride, showsEstimatedCost, includesCachedInput) + } + private static func loadMenuBarMetricPreferences(userDefaults: UserDefaults) -> [String: String] { let storedPreferences = userDefaults.dictionary(forKey: "menuBarMetricPreferences") as? [String: String] ?? [:] let preferences: [String: String] = if !storedPreferences.isEmpty { diff --git a/Sources/CodexBar/SettingsStoreState.swift b/Sources/CodexBar/SettingsStoreState.swift index 9f72aeaaa0..96c58f2780 100644 --- a/Sources/CodexBar/SettingsStoreState.swift +++ b/Sources/CodexBar/SettingsStoreState.swift @@ -47,6 +47,9 @@ struct SettingsDefaultsState { var costUsageHistoryDays: Int var costComparisonPeriodsEnabled: Bool var costSummaryDisplayStyleRaw: String + var codexLocalProjectUsageEnabledOverride: Bool? + var codexLocalProjectUsageShowsEstimatedCost: Bool + var codexLocalProjectUsageIncludesCachedInput: Bool var hidePersonalInfo: Bool var randomBlinkEnabled: Bool var confettiOnSessionLimitResetsEnabled: Bool diff --git a/Sources/CodexBar/StatusItemController+CodexLocalProjectUsageMenu.swift b/Sources/CodexBar/StatusItemController+CodexLocalProjectUsageMenu.swift new file mode 100644 index 0000000000..c08e7ed064 --- /dev/null +++ b/Sources/CodexBar/StatusItemController+CodexLocalProjectUsageMenu.swift @@ -0,0 +1,221 @@ +import AppKit +import CodexBarCore +import SwiftUI + +extension StatusItemController { + static let codexLocalProjectUsageSubmenuID = "codexLocalProjectUsageSubmenu" + private static let codexLocalProjectUsageProgressID = "codexLocalProjectUsageProgress" + + func observeCodexLocalProjectUsageProgressChanges() { + withObservationTracking { + _ = self.store.codexLocalProjectUsageProgress + } onChange: { [weak self] in + Task { @MainActor [weak self] in + guard let self else { return } + self.observeCodexLocalProjectUsageProgressChanges() + self.updateCodexLocalProjectUsageRows() + } + } + } + + @discardableResult + func addCodexLocalProjectUsageMenuItemIfNeeded(to menu: NSMenu, provider: UsageProvider, width: CGFloat) -> Bool { + guard provider == .codex else { return false } + guard self.settings.codexLocalProjectUsageEnabled else { return false } + + let submenuWidth = max(width, CodexLocalProjectUsageSubmenuLayout.menuWidth) + let submenu = self.makeHostedSubviewPlaceholderMenu( + chartID: Self.codexLocalProjectUsageSubmenuID, + width: submenuWidth) + let item = NSMenuItem() + item.target = self + item.action = #selector(self.menuCardNoOp(_:)) + item.isEnabled = true + item.title = L("codex_workspaces_title") + item.representedObject = "codexLocalProjectUsage" + item.submenu = submenu + menu.addItem(item) + self.codexLocalProjectUsageRows.add(item) + self.codexLocalProjectUsageRowWidths.setObject(NSNumber(value: Double(width)), forKey: item) + self.updateCodexLocalProjectUsageRow(item) + return true + } + + func addCodexLocalProjectUsageMenuSection(to menu: NSMenu, provider: UsageProvider, width: CGFloat) { + guard provider == .codex, self.settings.codexLocalProjectUsageEnabled else { return } + let hasCost = menu.items.contains { ($0.representedObject as? String) == "menuCardCost" } + let hasCreditsOrExtraUsage = menu.items.contains { + let identifier = $0.representedObject as? String + return identifier == "menuCardCredits" || identifier == "menuCardExtraUsage" + } + if !hasCost, hasCreditsOrExtraUsage, menu.items.last?.isSeparatorItem != true { + menu.addItem(.separator()) + } + if self.addCodexLocalProjectUsageMenuItemIfNeeded(to: menu, provider: provider, width: width) { + menu.addItem(.separator()) + } + } + + func updateCodexLocalProjectUsageRows() { + for row in self.codexLocalProjectUsageRows.allObjects { + self.updateCodexLocalProjectUsageRow(row) + } + } + + func updateCodexLocalProjectUsageRow(_ item: NSMenuItem) { + let title = L("codex_workspaces_title") + item.title = title + let subtitle = self.store.codexLocalProjectUsageRowSubtitle + if self.store.codexLocalProjectUsageRefreshInFlight { + self.clearSubtitle(from: item, title: title) + if let subtitle { + self.updateOrInsertProgressRow( + after: item, + subtitle: subtitle, + width: self.codexLocalProjectUsageRowWidth(for: item)) + } + return + } + self.removeProgressRow(after: item) + if let subtitle { + guard self.currentSubtitle(for: item) != subtitle else { return } + self.applySubtitle(subtitle, to: item, title: title) + } else { + guard self.currentSubtitle(for: item) != nil else { return } + self.clearSubtitle(from: item, title: title) + } + } + + private func codexLocalProjectUsageRowWidth(for item: NSMenuItem) -> CGFloat { + self.codexLocalProjectUsageRowWidths.object(forKey: item)?.doubleValue ?? 0 + } + + private func updateOrInsertProgressRow(after item: NSMenuItem, subtitle: String, width: CGFloat) { + guard let menu = item.menu else { return } + if let progressItem = self.progressRow(after: item) { + progressItem.title = subtitle + (progressItem.view as? CodexLocalProjectUsageProgressMenuRowView)?.update(subtitle: subtitle) + return + } + + let progressItem = NSMenuItem(title: subtitle, action: nil, keyEquivalent: "") + progressItem.isEnabled = false + progressItem.representedObject = Self.codexLocalProjectUsageProgressID + progressItem.view = CodexLocalProjectUsageProgressMenuRowView(width: width, subtitle: subtitle) + guard let index = menu.items.firstIndex(where: { $0 === item }) else { return } + menu.insertItem(progressItem, at: index + 1) + } + + private func removeProgressRow(after item: NSMenuItem) { + guard let menu = item.menu, let progressItem = self.progressRow(after: item) else { return } + menu.removeItem(progressItem) + } + + private func progressRow(after item: NSMenuItem) -> NSMenuItem? { + guard let menu = item.menu, + let index = menu.items.firstIndex(where: { $0 === item }), + index + 1 < menu.items.count + else { return nil } + let candidate = menu.items[index + 1] + guard (candidate.representedObject as? String) == Self.codexLocalProjectUsageProgressID else { + return nil + } + return candidate + } + + private func clearSubtitle(from item: NSMenuItem, title: String) { + item.view = nil + item.toolTip = nil + item.title = title + if #available(macOS 14.4, *) { + item.subtitle = "" + } + } + + private func currentSubtitle(for item: NSMenuItem) -> String? { + if #available(macOS 14.4, *), let subtitle = item.subtitle, !subtitle.isEmpty { + return subtitle + } + return item.toolTip?.components(separatedBy: "—").last?.trimmingCharacters(in: .whitespacesAndNewlines) + } + + @objc + func openCodexLocalProjectUsageWindowFromMenu(_ sender: Any?) { + self.openCodexLocalProjectUsageWindow() + } + + func openCodexLocalProjectUsageWindow() { + let controller = self.codexLocalProjectUsageWindow ?? CodexLocalProjectUsageWindowController( + store: self.store, + settings: self.settings, + selection: self.codexLocalProjectUsageInspectorSelection) + self.codexLocalProjectUsageWindow = controller + controller.showAndRefresh() + } + + func openCodexLocalProjectUsageSessionsWindow() { + let controller = self.codexLocalProjectUsageWindow ?? CodexLocalProjectUsageWindowController( + store: self.store, + settings: self.settings, + selection: self.codexLocalProjectUsageInspectorSelection) + self.codexLocalProjectUsageWindow = controller + controller.showSessions() + } +} + +private final class CodexLocalProjectUsageProgressMenuRowView: NSView { + private static let rowHeight: CGFloat = 24 + private let subtitleField: NSTextField + private let progressIndicator = NSProgressIndicator() + + override var intrinsicContentSize: NSSize { + NSSize(width: self.frame.width > 0 ? self.frame.width : NSView.noIntrinsicMetric, height: Self.rowHeight) + } + + override var fittingSize: NSSize { + NSSize(width: self.frame.width, height: Self.rowHeight) + } + + init(width: CGFloat, subtitle: String) { + self.subtitleField = NSTextField(labelWithString: subtitle) + super.init(frame: NSRect(origin: .zero, size: NSSize(width: width, height: Self.rowHeight))) + self.setupView() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(subtitle: String) { + self.subtitleField.stringValue = subtitle + } + + private func setupView() { + self.subtitleField.font = NSFont.menuFont(ofSize: NSFont.smallSystemFontSize) + self.subtitleField.textColor = NSColor.secondaryLabelColor + self.subtitleField.lineBreakMode = .byTruncatingTail + self.subtitleField.translatesAutoresizingMaskIntoConstraints = false + + self.progressIndicator.style = .spinning + self.progressIndicator.controlSize = .small + self.progressIndicator.isIndeterminate = true + self.progressIndicator.translatesAutoresizingMaskIntoConstraints = false + self.addSubview(self.subtitleField) + self.addSubview(self.progressIndicator) + + NSLayoutConstraint.activate([ + self.subtitleField.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 18), + self.subtitleField.trailingAnchor.constraint( + lessThanOrEqualTo: self.progressIndicator.leadingAnchor, + constant: -10), + self.subtitleField.centerYAnchor.constraint(equalTo: self.centerYAnchor), + + self.progressIndicator.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -18), + self.progressIndicator.centerYAnchor.constraint(equalTo: self.centerYAnchor), + self.progressIndicator.widthAnchor.constraint(equalToConstant: 16), + self.progressIndicator.heightAnchor.constraint(equalToConstant: 16), + ]) + self.progressIndicator.startAnimation(nil) + } +} diff --git a/Sources/CodexBar/StatusItemController+HostedSubmenus.swift b/Sources/CodexBar/StatusItemController+HostedSubmenus.swift index 9923a05a8b..53b8782084 100644 --- a/Sources/CodexBar/StatusItemController+HostedSubmenus.swift +++ b/Sources/CodexBar/StatusItemController+HostedSubmenus.swift @@ -6,6 +6,7 @@ import SwiftUI enum HostedSubviewContentFingerprint: Equatable { case text(String) case costHistory(CostHistoryChartMenuView.RenderFingerprint) + case codexLocalProjectUsage(String) } struct HostedSubviewRenderSignature: Equatable { @@ -60,6 +61,7 @@ extension StatusItemController { Self.storageBreakdownID, Self.statusComponentsID, Self.zaiHourlyUsageChartID, + Self.codexLocalProjectUsageSubmenuID, ] return menu.items.contains { item in guard let id = item.representedObject as? String else { return false } @@ -96,7 +98,11 @@ extension StatusItemController { return false } - let width = requestedWidth ?? self.renderedMenuWidth(for: menu.supermenu ?? menu) + let parentWidth = self.renderedMenuWidth(for: menu.supermenu ?? menu) + let width = requestedWidth ?? ( + chartID == Self.codexLocalProjectUsageSubmenuID + ? max(parentWidth, CodexLocalProjectUsageSubmenuLayout.menuWidth) + : parentWidth) let identity = HostedSubviewIdentity( chartID: chartID, provider: placeholder.toolTip.flatMap(UsageProvider.init(rawValue:)), @@ -151,6 +157,8 @@ extension StatusItemController { } else { false } + case Self.codexLocalProjectUsageSubmenuID: + self.appendCodexLocalProjectUsageSubmenuItem(to: menu, width: width) default: false } @@ -167,11 +175,14 @@ extension StatusItemController { } func refreshHostedSubviewMenu(_ menu: NSMenu) { - let width = self.renderedMenuWidth(for: menu) guard let identity = self.hostedSubviewIdentity(for: menu) else { self.refreshHostedSubviewHeights(in: menu) return } + let renderedWidth = self.renderedMenuWidth(for: menu) + let width = identity.chartID == Self.codexLocalProjectUsageSubmenuID + ? max(renderedWidth, CodexLocalProjectUsageSubmenuLayout.menuWidth) + : renderedWidth let signature = self.hostedSubviewRenderSignature(identity: identity, width: width) if self.hostedSubviewRenderSignatures.object(forKey: menu)?.signature == signature { if identity.chartID == Self.zaiHourlyUsageChartID { @@ -219,6 +230,8 @@ extension StatusItemController { } else { false } + case Self.codexLocalProjectUsageSubmenuID: + self.appendCodexLocalProjectUsageSubmenuItem(to: menu, width: width) default: false } @@ -292,6 +305,8 @@ extension StatusItemController { .text(identity.provider.map(self.statusComponentsRenderSignature(for:)) ?? "missing-provider") case Self.zaiHourlyUsageChartID: .text(identity.provider.map(self.zaiHourlyUsageRenderSignature(for:)) ?? "missing-provider") + case Self.codexLocalProjectUsageSubmenuID: + self.codexLocalProjectUsageRenderFingerprint() default: .text("unknown") } @@ -309,6 +324,55 @@ extension StatusItemController { return .costHistory(CostHistoryChartMenuView.renderFingerprint(from: snapshot, provider: provider)) } + private func codexLocalProjectUsageRenderFingerprint() -> HostedSubviewContentFingerprint { + let state = switch self.store.codexLocalProjectUsageLoadState { + case .idle: "idle" + case .hydratingCache: "hydrating" + case .indexing: "indexing" + case .stale: "stale" + case .failed: "failed" + } + let progress = self.store.codexLocalProjectUsageProgressSubtitle ?? "" + let error = self.store.codexLocalProjectUsageError ?? "" + guard let snapshot = self.store.codexLocalProjectUsageSnapshot else { + return .codexLocalProjectUsage( + [ + state, + progress, + error, + String(self.settings.codexLocalProjectUsageShowsEstimatedCost), + String(self.settings.codexLocalProjectUsageIncludesCachedInput), + ].joined(separator: "|")) + } + let projects = self.store.codexLocalProjectUsageRankedProjects(snapshot.projects) + .map { project in + [ + project.id, + String(self.store.codexLocalProjectUsageDisplayTotal(project.totals) ?? -1), + String(project.costEstimate.knownUSD.bitPattern), + String(project.sessionCount), + ].joined(separator: ":") + } + .joined(separator: ";") + let daily = snapshot.daily + .map { point in + "\(point.day):\(self.store.codexLocalProjectUsageDailyDisplayTotal(point))" + } + .joined(separator: ";") + return .codexLocalProjectUsage( + [ + state, + progress, + error, + snapshot.updatedAt.timeIntervalSince1970.description, + String(snapshot.indexedFileCount), + String(self.settings.codexLocalProjectUsageShowsEstimatedCost), + String(self.settings.codexLocalProjectUsageIncludesCachedInput), + projects, + daily, + ].joined(separator: "|")) + } + private func usageHistoryRenderSignature(for provider: UsageProvider) -> String { let snapshot = self.store.snapshot(for: provider) let selection = self.store.planUtilizationHistorySelection(for: provider) @@ -489,6 +553,53 @@ extension StatusItemController { return true } + @discardableResult + func appendCodexLocalProjectUsageSubmenuItem(to submenu: NSMenu, width: CGFloat) -> Bool { + guard self.settings.codexLocalProjectUsageEnabled else { return false } + + // Native submenus own pointer tracking. Keep project rows read-only here; selection and + // session/model drill-down stay in the persistent window opened by the native action row. + if !self.menuCardRenderingEnabledForController { + let item = NSMenuItem() + item.isEnabled = true + item.representedObject = Self.codexLocalProjectUsageSubmenuID + submenu.addItem(item) + } else { + let view = CodexLocalProjectUsageSubmenuView( + store: self.store, + settings: self.settings, + width: width) + let hosting = MenuHostingView(rootView: view) + hosting.applyMeasuredHeight( + width: width, + height: self.hostedSubviewFittingHeight(for: hosting, width: width)) + + let item = NSMenuItem() + item.view = hosting + item.isEnabled = true + item.representedObject = Self.codexLocalProjectUsageSubmenuID + submenu.addItem(item) + } + + submenu.addItem(.separator()) + let openWindowItem = NSMenuItem( + title: L("codex_workspaces_open_in_window"), + action: #selector(self.openCodexLocalProjectUsageWindowFromMenu(_:)), + keyEquivalent: "") + openWindowItem.target = self + openWindowItem.isEnabled = true + openWindowItem.representedObject = "codexLocalProjectUsageOpenWindow" + if let image = NSImage( + systemSymbolName: "arrow.up.right.square", + accessibilityDescription: nil) + { + image.isTemplate = true + openWindowItem.image = image + } + submenu.addItem(openWindowItem) + return true + } + @discardableResult func appendStorageBreakdownItem( to submenu: NSMenu, diff --git a/Sources/CodexBar/StatusItemController+Menu.swift b/Sources/CodexBar/StatusItemController+Menu.swift index c3ca9b2907..8aeead1600 100644 --- a/Sources/CodexBar/StatusItemController+Menu.swift +++ b/Sources/CodexBar/StatusItemController+Menu.swift @@ -89,6 +89,11 @@ extension StatusItemController { if !self.hydrateHostedSubviewMenuIfNeeded(menu) { self.refreshHostedSubviewMenu(menu) } + if menu.items.contains(where: { + ($0.representedObject as? String) == Self.codexLocalProjectUsageSubmenuID + }) { + self.store.scheduleCodexLocalProjectUsageRefreshIfNeeded() + } if self.isMenuRefreshEnabled, self.isOpenAIWebSubviewMenu(menu) { self.deferOpenAIDashboardRefreshUntilMenuCloses(reason: "submenu open") } @@ -771,6 +776,10 @@ extension StatusItemController { { menu.addItem(.separator()) } + self.addCodexLocalProjectUsageMenuSection( + to: menu, + provider: context.currentProvider, + width: context.menuWidth) if self.shouldMergeIcons, self.store.enabledProvidersForDisplay().count > 1 { // Sized by `applyStableMenuHeightPadding` so provider tabs share one height. menu.addItem(self.makeStableMenuHeightSpacerItem()) diff --git a/Sources/CodexBar/StatusItemController.swift b/Sources/CodexBar/StatusItemController.swift index 40651ad28a..5be2797765 100644 --- a/Sources/CodexBar/StatusItemController.swift +++ b/Sources/CodexBar/StatusItemController.swift @@ -48,7 +48,6 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin private nonisolated static let debugStatusItemAccessibilityTitle = "CodexBar Debug" private nonisolated static let statusItemAccessibilityIdentifierPrefix = "CodexBar.StatusItem" private nonisolated static let mergedLegacyDefaultItemIndex = 0 - enum StatusItemIdentity { case merged case provider(UsageProvider) @@ -142,6 +141,10 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin let hostedSubviewRenderSignatures = NSMapTable.weakToStrongObjects() /// Persistent Refresh rows are weakly tracked so their enabled state can change during menu tracking. let persistentRefreshItems = NSHashTable.weakObjects() + /// Live Workspaces rows are weakly tracked so indexing progress can update during menu tracking. + let codexLocalProjectUsageRows = NSHashTable.weakObjects() + /// Widths for optional indexing rows below native Workspaces items. + let codexLocalProjectUsageRowWidths = NSMapTable.weakToStrongObjects() var menuCardHeightCache: [MenuCardHeightCacheKey: CGFloat] = [:] var measuredStandardMenuWidthCache: [String: CGFloat] = [:] var lastMenuAdjunctReadinessSignature = "" @@ -199,6 +202,7 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin #if DEBUG var onDelayedMenuRefreshAttemptForTesting: (() -> Void)? var onDeferredMenuInteractionRefreshForTesting: (() -> Void)? + var onObservedStoreMenuChangeForTesting: (() -> Void)? var onOpenMenuInvalidationRetryForTesting: (() -> Void)? var isReleasedForTesting = false var lastLoggedClosedMenuRebuildVersion: Int? @@ -216,6 +220,8 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin } var creditsPurchaseWindow: OpenAICreditsPurchaseWindowController? + var codexLocalProjectUsageWindow: CodexLocalProjectUsageWindowController? + let codexLocalProjectUsageInspectorSelection = CodexLocalProjectUsageInspectorSelection() var activeLoginProvider: UsageProvider? { didSet { @@ -250,6 +256,7 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin private var lastMergeIcons: Bool private var lastSwitcherShowsIcons: Bool private var lastObservedUsageBarsShowUsed: Bool + private var lastObservedHidePersonalInfo, lastObservedCodexLocalProjectUsageEnabled: Bool var lastWidgetDisplaySettingsSignature = "" var lastAgentSessionsEnabled: Bool var lastAgentSessionsManualHosts: String @@ -415,6 +422,8 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin self.lastMergeIcons = settings.mergeIcons self.lastSwitcherShowsIcons = settings.switcherShowsIcons self.lastObservedUsageBarsShowUsed = settings.usageBarsShowUsed + self.lastObservedHidePersonalInfo = settings.hidePersonalInfo + self.lastObservedCodexLocalProjectUsageEnabled = settings.codexLocalProjectUsageEnabled self.lastAgentSessionsEnabled = settings.agentSessionsEnabled self.lastAgentSessionsManualHosts = settings.agentSessionsManualHosts self.lastAgentSessionsRefreshFrequency = settings.refreshFrequency @@ -528,8 +537,10 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin } func handleObservedStoreMenuChange() { + self.notifyObservedStoreMenuChangeForTesting() self.observeStoreChanges() self.updatePersistentRefreshItemsEnabled() + self.updateCodexLocalProjectUsageRows() let rootOpenHandledReadiness = self.consumeRootOpenHandledMenuObservationIfNeeded() // `refreshOpenMenus` is only consulted when a menu is currently open. // Computing the readiness signature serializes every enabled provider's @@ -688,6 +699,7 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin let configChanged = self.settings.configRevision != self.lastConfigRevision let orderChanged = self.settings.providerOrder != self.lastProviderOrder let localizationChanged = self.menuLocalizationSignature() != self.lastMenuLocalizationSignature + self.refreshCodexLocalProjectUsageForSettingsChange() let shouldRefreshOpenMenus = self.shouldRefreshOpenMenusForProviderSwitcher() self.invalidateMenus() if orderChanged || configChanged { @@ -750,26 +762,6 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin return self.openMenus[ObjectIdentifier(mergedMenu)] != nil } - func recreateStatusItemsForVisibilityRecovery() { - #if DEBUG - guard !self.isReleasedForTesting else { return } - #endif - self.statusItem.menu = nil - self.statusBar.removeStatusItem(self.statusItem) - self.statusItem = Self.makeStatusItem( - statusBar: self.statusBar, - identity: .merged, - defaults: self.settings.userDefaults, - legacyDefaultItemIndex: Self.mergedLegacyDefaultItemIndex) - for provider in Array(self.statusItems.keys) { - self.removeProviderStatusItem(for: provider) - } - self.lastAppliedMergedIconRenderSignature = nil - self.lastAppliedProviderIconRenderSignatures.removeAll() - self.updateVisibility() - self.updateIcons() - } - private func updateVisibility() { #if DEBUG guard !self.isReleasedForTesting else { return } @@ -937,6 +929,24 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin } } +extension StatusItemController { + private func refreshCodexLocalProjectUsageForSettingsChange() { + let previousHidePersonalInfo = self.lastObservedHidePersonalInfo + self.lastObservedHidePersonalInfo = self.settings.hidePersonalInfo + self.store.refreshCodexLocalProjectUsageProjectionIfNeeded( + previousHidePersonalInfo: previousHidePersonalInfo) + let previousEnabled = self.lastObservedCodexLocalProjectUsageEnabled + self.lastObservedCodexLocalProjectUsageEnabled = self.settings.codexLocalProjectUsageEnabled + self.store.refreshCodexLocalProjectUsageAvailabilityIfNeeded(previousEnabled: previousEnabled) + } + + private func notifyObservedStoreMenuChangeForTesting() { + #if DEBUG + self.onObservedStoreMenuChangeForTesting?() + #endif + } +} + #if DEBUG extension StatusItemController { var _test_manualRefreshOperation: (@MainActor () async -> Void)? { @@ -1010,6 +1020,26 @@ extension StatusItemController { return Self.mergedLegacyDefaultItemIndex + 1 + providerOffset } + func recreateStatusItemsForVisibilityRecovery() { + #if DEBUG + guard !self.isReleasedForTesting else { return } + #endif + self.statusItem.menu = nil + self.statusBar.removeStatusItem(self.statusItem) + self.statusItem = Self.makeStatusItem( + statusBar: self.statusBar, + identity: .merged, + defaults: self.settings.userDefaults, + legacyDefaultItemIndex: Self.mergedLegacyDefaultItemIndex) + for provider in Array(self.statusItems.keys) { + self.removeProviderStatusItem(for: provider) + } + self.lastAppliedMergedIconRenderSignature = nil + self.lastAppliedProviderIconRenderSignatures.removeAll() + self.updateVisibility() + self.updateIcons() + } + func refreshExistingStatusItemsForVisibilityRecovery() { #if DEBUG guard !self.isReleasedForTesting else { return } diff --git a/Sources/CodexBar/UsageStore+CodexLocalProjectUsage.swift b/Sources/CodexBar/UsageStore+CodexLocalProjectUsage.swift new file mode 100644 index 0000000000..8b200208f0 --- /dev/null +++ b/Sources/CodexBar/UsageStore+CodexLocalProjectUsage.swift @@ -0,0 +1,432 @@ +import CodexBarCore +import Foundation + +enum CodexLocalProjectUsageLoadState: Equatable { + case idle + case hydratingCache + case indexing + case stale + case failed +} + +private struct CodexLocalProjectUsageRefreshRequest: Sendable { + let force: Bool + let generation: UInt64 + let requestKey: String + let now: Date + let scopeSignature: String + let codexHomePath: String? + let historyDays: Int + let hidePersonalInfo: Bool +} + +private enum CodexLocalProjectUsageRefreshOutcome: Sendable { + case success(CodexLocalProjectUsageSnapshot) + case cancelled + case failure +} + +private enum CodexLocalProjectUsageRefreshExecutor { + static func run( + fetcher: CostUsageFetcher, + request: CodexLocalProjectUsageRefreshRequest, + progress: @escaping @Sendable (CodexLocalProjectUsageIndexProgress) -> Void) + async -> CodexLocalProjectUsageRefreshOutcome + { + do { + let snapshot = try await fetcher.loadCodexLocalProjectUsageSnapshot( + now: request.now, + forceRefresh: request.force, + codexHomePath: request.codexHomePath, + historyDays: request.historyDays, + hidePersonalInfo: request.hidePersonalInfo, + progress: progress) + return .success(snapshot) + } catch is CancellationError { + return .cancelled + } catch { + return .failure + } + } +} + +extension UsageStore { + func hydrateCachedCodexLocalProjectUsage(now: Date = Date()) { + guard self.shouldLoadCodexLocalProjectUsage else { return } + let scope = self.tokenCostScope(for: .codex) + let historyDays = self.settings.costUsageHistoryDays + let hidePersonalInfo = self.settings.hidePersonalInfo + self.codexLocalProjectUsageRequestGeneration &+= 1 + let generation = self.codexLocalProjectUsageRequestGeneration + self.codexLocalProjectUsageLoadState = .hydratingCache + Task { @MainActor [weak self] in + guard let self else { return } + guard self.codexLocalProjectUsageSnapshot == nil else { + self.codexLocalProjectUsageLoadState = .idle + return + } + guard let snapshot = await self.costUsageFetcher.loadCachedCodexLocalProjectUsageSnapshot( + now: now, + codexHomePath: scope.codexHomePath, + historyDays: historyDays, + hidePersonalInfo: hidePersonalInfo) + else { + if self.codexLocalProjectUsageLoadState == .hydratingCache { + self.codexLocalProjectUsageLoadState = .idle + } + return + } + guard self.shouldLoadCodexLocalProjectUsage, + self.codexLocalProjectUsageRequestGeneration == generation, + self.tokenCostScope(for: .codex).signature == scope.signature, + self.settings.costUsageHistoryDays == historyDays, + self.settings.hidePersonalInfo == hidePersonalInfo, + self.codexLocalProjectUsageSnapshot == nil + else { + if self.codexLocalProjectUsageLoadState == .hydratingCache { + self.codexLocalProjectUsageLoadState = .idle + } + return + } + self.codexLocalProjectUsageSnapshot = snapshot + self.codexLocalProjectUsageError = nil + self.lastCodexLocalProjectUsageSuccessAt = snapshot.updatedAt + self.codexLocalProjectUsageLoadState = .idle + } + } + + func scheduleCodexLocalProjectUsageRefreshIfNeeded(force: Bool = false) { + guard self.shouldLoadCodexLocalProjectUsage else { + self.codexLocalProjectUsageRefreshTask?.cancel() + self.codexLocalProjectUsageRefreshTask = nil + self.codexLocalProjectUsageSnapshot = nil + self.codexLocalProjectUsageError = nil + self.codexLocalProjectUsageProgress = nil + self.codexLocalProjectUsageRefreshInFlight = false + self.lastCodexLocalProjectUsageFetchAt = nil + self.lastCodexLocalProjectUsageFetchScope = nil + self.lastCodexLocalProjectUsageFailureAt = nil + self.codexLocalProjectUsageBackoffUntil = nil + self.codexLocalProjectUsageFailureCount = 0 + self.activeForcedCodexLocalProjectUsageRefresh = false + self.pendingForcedCodexLocalProjectUsageRefresh = false + self.activeCodexLocalProjectUsageRequestKey = nil + self.pendingCodexLocalProjectUsageRequestKey = nil + self.codexLocalProjectUsageRequestGeneration &+= 1 + self.activeCodexLocalProjectUsageRequestGeneration = nil + self.codexLocalProjectUsageLoadState = .idle + return + } + let scope = self.tokenCostScope(for: .codex) + let requestKey = self.codexLocalProjectUsageRequestKey( + scopeSignature: scope.signature, + historyDays: self.settings.costUsageHistoryDays, + hidePersonalInfo: self.settings.hidePersonalInfo) + guard self.codexLocalProjectUsageRefreshTask == nil else { + if self.pendingCodexLocalProjectUsageRequestKey == requestKey { + self.pendingForcedCodexLocalProjectUsageRefresh = + self.pendingForcedCodexLocalProjectUsageRefresh || force + return + } + if self.pendingCodexLocalProjectUsageRequestKey == nil, + self.activeCodexLocalProjectUsageRequestKey == requestKey, + !force || self.activeForcedCodexLocalProjectUsageRefresh + { + return + } + self.codexLocalProjectUsageRequestGeneration &+= 1 + self.pendingCodexLocalProjectUsageRequestKey = requestKey + self.pendingForcedCodexLocalProjectUsageRefresh = force + self.codexLocalProjectUsageRefreshTask?.cancel() + return + } + self.codexLocalProjectUsageRequestGeneration &+= 1 + let generation = self.codexLocalProjectUsageRequestGeneration + self.activeCodexLocalProjectUsageRequestKey = requestKey + self.activeCodexLocalProjectUsageRequestGeneration = generation + self.activeForcedCodexLocalProjectUsageRefresh = force + let refreshExecutor = self._test_codexLocalProjectUsageRefreshExecutor + let fetcher = self.costUsageFetcher + let request = refreshExecutor == nil + ? self.prepareCodexLocalProjectUsageRefresh(force: force, generation: generation, requestKey: requestKey) + : nil + self.codexLocalProjectUsageRefreshTask = Task { @MainActor [weak self, fetcher, refreshExecutor, request] in + if let refreshExecutor { + await refreshExecutor(force, generation) + } else if let request { + let outcome = await CodexLocalProjectUsageRefreshExecutor.run( + fetcher: fetcher, + request: request, + progress: { [weak self] progress in + Task { @MainActor [weak self] in + self?.publishCodexLocalProjectUsageProgress(progress, for: request) + } + }) + guard let self else { return } + self.applyCodexLocalProjectUsageRefreshOutcome(outcome, for: request) + } + guard let self else { return } + self.finishCodexLocalProjectUsageRefresh(generation: generation) + } + } + + func refreshCodexLocalProjectUsageProjectionIfNeeded(previousHidePersonalInfo: Bool) { + let hidePersonalInfo = self.settings.hidePersonalInfo + guard previousHidePersonalInfo != hidePersonalInfo else { return } + if hidePersonalInfo { + self.codexLocalProjectUsageSnapshot = nil + self.codexLocalProjectUsageError = nil + self.codexLocalProjectUsageProgress = nil + self.codexLocalProjectUsageRefreshInFlight = false + self.codexLocalProjectUsageLoadState = .idle + self.lastCodexLocalProjectUsageFetchAt = nil + self.lastCodexLocalProjectUsageFetchScope = nil + self.lastCodexLocalProjectUsageFailureAt = nil + self.codexLocalProjectUsageBackoffUntil = nil + self.codexLocalProjectUsageFailureCount = 0 + } + self.scheduleCodexLocalProjectUsageRefreshIfNeeded() + } + + func refreshCodexLocalProjectUsageAvailabilityIfNeeded(previousEnabled: Bool) { + guard previousEnabled != self.settings.codexLocalProjectUsageEnabled else { return } + self.scheduleCodexLocalProjectUsageRefreshIfNeeded() + } + + private func prepareCodexLocalProjectUsageRefresh( + force: Bool, + generation: UInt64, + requestKey: String) -> CodexLocalProjectUsageRefreshRequest? + { + guard generation == self.codexLocalProjectUsageRequestGeneration else { return nil } + guard self.shouldLoadCodexLocalProjectUsage else { return nil } + if self.codexLocalProjectUsageRefreshInFlight { return nil } + + let now = Date() + let historyDays = self.settings.costUsageHistoryDays + let hidePersonalInfo = self.settings.hidePersonalInfo + let scope = self.tokenCostScope(for: .codex) + let scopeSignature = self.codexLocalProjectUsageRequestKey( + scopeSignature: scope.signature, + historyDays: historyDays, + hidePersonalInfo: hidePersonalInfo) + if !force, + let last = self.lastCodexLocalProjectUsageFetchAt, + self.lastCodexLocalProjectUsageFetchScope == scopeSignature, + now.timeIntervalSince(last) < Self.codexLocalProjectUsageRefreshTTL + { + return nil + } + if !force, let backoffUntil = self.codexLocalProjectUsageBackoffUntil, now < backoffUntil { + return nil + } + + self.lastCodexLocalProjectUsageFetchScope = scopeSignature + self.lastCodexLocalProjectUsageAttemptAt = now + self.codexLocalProjectUsageRefreshInFlight = true + self.codexLocalProjectUsageLoadState = .indexing + self.codexLocalProjectUsageProgress = CodexLocalProjectUsageIndexProgress(phase: .scanningLogs) + + return CodexLocalProjectUsageRefreshRequest( + force: force, + generation: generation, + requestKey: requestKey, + now: now, + scopeSignature: scope.signature, + codexHomePath: scope.codexHomePath, + historyDays: historyDays, + hidePersonalInfo: hidePersonalInfo) + } + + private func publishCodexLocalProjectUsageProgress( + _ progress: CodexLocalProjectUsageIndexProgress, + for request: CodexLocalProjectUsageRefreshRequest) + { + guard self.isActiveCodexLocalProjectUsageRefresh(request) else { return } + self.codexLocalProjectUsageProgress = progress + } + + private func applyCodexLocalProjectUsageRefreshOutcome( + _ outcome: CodexLocalProjectUsageRefreshOutcome, + for request: CodexLocalProjectUsageRefreshRequest) + { + guard self.isActiveCodexLocalProjectUsageRefresh(request) else { return } + switch outcome { + case let .success(snapshot): + self.codexLocalProjectUsageSnapshot = snapshot + self.codexLocalProjectUsageError = nil + self.lastCodexLocalProjectUsageFetchAt = request.now + self.lastCodexLocalProjectUsageSuccessAt = request.now + self.lastCodexLocalProjectUsageFailureAt = nil + self.codexLocalProjectUsageBackoffUntil = nil + self.codexLocalProjectUsageFailureCount = 0 + case .cancelled: + break + case .failure: + self.codexLocalProjectUsageError = L("codex_workspaces_failed_read_logs") + self.lastCodexLocalProjectUsageFailureAt = request.now + self.codexLocalProjectUsageFailureCount += 1 + let backoff = min(30.0 * pow(2, Double(self.codexLocalProjectUsageFailureCount - 1)), 15 * 60) + self.codexLocalProjectUsageBackoffUntil = request.now.addingTimeInterval(backoff) + self.codexLocalProjectUsageLoadState = self.codexLocalProjectUsageSnapshot == nil ? .failed : .stale + } + } + + private func isActiveCodexLocalProjectUsageRefresh(_ request: CodexLocalProjectUsageRefreshRequest) -> Bool { + self.activeCodexLocalProjectUsageRequestGeneration == request.generation && + self.codexLocalProjectUsageRequestGeneration == request.generation && + self.activeCodexLocalProjectUsageRequestKey == request.requestKey && + self.codexLocalProjectUsageRefreshInFlight && + self.shouldLoadCodexLocalProjectUsage && + self.tokenCostScope(for: .codex).signature == request.scopeSignature && + self.settings.costUsageHistoryDays == request.historyDays && + self.settings.hidePersonalInfo == request.hidePersonalInfo + } + + private func finishCodexLocalProjectUsageRefresh(generation: UInt64) { + guard self.activeCodexLocalProjectUsageRequestGeneration == generation else { return } + self.codexLocalProjectUsageRefreshInFlight = false + self.codexLocalProjectUsageProgress = nil + if self.codexLocalProjectUsageLoadState == .indexing { + self.codexLocalProjectUsageLoadState = .idle + } + self.codexLocalProjectUsageRefreshTask = nil + self.activeCodexLocalProjectUsageRequestKey = nil + self.activeCodexLocalProjectUsageRequestGeneration = nil + self.activeForcedCodexLocalProjectUsageRefresh = false + if self.pendingCodexLocalProjectUsageRequestKey != nil { + let pendingForce = self.pendingForcedCodexLocalProjectUsageRefresh + self.pendingCodexLocalProjectUsageRequestKey = nil + self.pendingForcedCodexLocalProjectUsageRefresh = false + self.scheduleCodexLocalProjectUsageRefreshIfNeeded(force: pendingForce) + } + } + + private func codexLocalProjectUsageRequestKey( + scopeSignature: String, + historyDays: Int, + hidePersonalInfo: Bool) -> String + { + "\(scopeSignature)|historyDays=\(historyDays)|hidePersonalInfo=\(hidePersonalInfo)" + } + + func rebuildCodexLocalProjectUsageIndex() { + guard self.shouldLoadCodexLocalProjectUsage else { return } + self.codexLocalProjectUsageError = nil + self.lastCodexLocalProjectUsageFetchAt = nil + self.lastCodexLocalProjectUsageFetchScope = nil + self.lastCodexLocalProjectUsageFailureAt = nil + self.codexLocalProjectUsageBackoffUntil = nil + self.codexLocalProjectUsageFailureCount = 0 + self.scheduleCodexLocalProjectUsageRefreshIfNeeded(force: true) + } + + var codexLocalProjectUsageRowSubtitle: String? { + if self.codexLocalProjectUsageRefreshInFlight { + return L("codex_workspaces_indexing_local_logs") + } + guard let snapshot = self.codexLocalProjectUsageSnapshot else { + if self.codexLocalProjectUsageError != nil { + return L("codex_workspaces_failed_read_logs") + } + return nil + } + if snapshot.sourceStatus.isPartial { + return L("codex_workspaces_failed_some_logs") + } + let age = Date().timeIntervalSince(snapshot.updatedAt) + guard age >= Self.codexLocalProjectUsageStaleAge else { + if self.codexLocalProjectUsageError != nil { + return L("codex_workspaces_failed_read_logs") + } + return nil + } + if self.codexLocalProjectUsageError != nil { + return L("codex_workspaces_failed_read_logs") + } + return L("codex_workspaces_stale_status", Self.formatDuration(age)) + } + + var codexLocalProjectUsageProgressSubtitle: String? { + guard let progress = self.codexLocalProjectUsageProgress else { return nil } + return Self.formatCodexLocalProjectUsageProgress(progress) + } + + var codexLocalProjectUsageProgressFraction: Double? { + guard let progress = self.codexLocalProjectUsageProgress, + progress.phase == .indexingProjects, + let processed = progress.processedFileCount, + let total = progress.totalFileCount, + total > 0 + else { + return nil + } + return min(1, max(0, Double(processed) / Double(total))) + } + + func codexLocalProjectUsageDisplayTotal(_ totals: CodexLocalUsageTotals) -> Int? { + self.codexLocalProjectUsageProjection.displayedTokens(for: totals) + } + + func codexLocalProjectUsageDailyDisplayTotal(_ point: CodexLocalUsageDailyPoint) -> Int { + self.codexLocalProjectUsageProjection.displayedTokens( + totalTokens: point.totalTokens, + cachedInputTokens: point.cachedInputTokens) + } + + func codexLocalProjectUsageRankedProjects(_ projects: [CodexLocalProjectUsage]) -> [CodexLocalProjectUsage] { + self.codexLocalProjectUsageProjection.rankedProjects(projects) + } + + var codexLocalProjectUsageProjection: CodexLocalProjectUsageProjection { + CodexLocalProjectUsageProjection( + includesCachedInput: self.settings.codexLocalProjectUsageIncludesCachedInput, + showsEstimatedCost: self.settings.codexLocalProjectUsageShowsEstimatedCost) + } + + private var shouldLoadCodexLocalProjectUsage: Bool { + self.settings.codexLocalProjectUsageEnabled && + self.isEnabled(.codex) + } + + private static let codexLocalProjectUsageRefreshTTL: TimeInterval = 60 * 60 + private static let codexLocalProjectUsageFailureBackoff: TimeInterval = 5 * 60 + private static let codexLocalProjectUsageStaleAge: TimeInterval = 2 * 60 * 60 + + private static func formatCodexLocalProjectUsageProgress( + _ progress: CodexLocalProjectUsageIndexProgress) -> String + { + switch progress.phase { + case .scanningLogs: + return L("codex_workspaces_scanning_logs") + case .indexingProjects: + guard let processed = progress.processedFileCount, + let total = progress.totalFileCount, + total > 0 + else { + return L("codex_workspaces_indexing_projects") + } + let clampedProcessed = min(max(0, processed), total) + let remaining = max(0, total - clampedProcessed) + if remaining == 0 { + return L("codex_workspaces_finalizing_project_index_files", total) + } + return L("codex_workspaces_indexing_projects_files_left", clampedProcessed, total, remaining) + case .saving: + return L("codex_workspaces_saving_project_index") + } + } + + private static func formatDuration(_ interval: TimeInterval) -> String { + let minutes = max(1, Int(interval / 60)) + if minutes < 60 { + return "\(minutes)m" + } + let hours = minutes / 60 + let remainingMinutes = minutes % 60 + if remainingMinutes == 0 { + return "\(hours)h" + } + return "\(hours)h \(remainingMinutes)m" + } +} diff --git a/Sources/CodexBar/UsageStore+TokenRefreshSequence.swift b/Sources/CodexBar/UsageStore+TokenRefreshSequence.swift index ff719fcea5..5fc068e15c 100644 --- a/Sources/CodexBar/UsageStore+TokenRefreshSequence.swift +++ b/Sources/CodexBar/UsageStore+TokenRefreshSequence.swift @@ -148,6 +148,7 @@ extension UsageStore { await self.refreshTokenUsage(provider, force: force) self.tokenRefreshSequenceProvider = nil } + self.scheduleCodexLocalProjectUsageRefreshIfNeeded(force: force) self.scheduleMemoryPressureRelief() } diff --git a/Sources/CodexBar/UsageStore.swift b/Sources/CodexBar/UsageStore.swift index 19c9488717..9759cc84e6 100644 --- a/Sources/CodexBar/UsageStore.swift +++ b/Sources/CodexBar/UsageStore.swift @@ -23,6 +23,9 @@ extension UsageStore { _ = self.tokenSnapshots _ = self.tokenErrors _ = self.tokenRefreshInFlight + _ = self.codexLocalProjectUsageSnapshot + _ = self.codexLocalProjectUsageError + _ = self.codexLocalProjectUsageRefreshInFlight _ = self.credits _ = self.lastCreditsError _ = self.openAIDashboard @@ -183,6 +186,25 @@ final class UsageStore { var tokenSnapshotPublicationRevisions: [UsageProvider: UInt64] = [:] var tokenErrors: [UsageProvider: String] = [:] var tokenRefreshInFlight: Set = [] + var codexLocalProjectUsageSnapshot: CodexLocalProjectUsageSnapshot? + var codexLocalProjectUsageError: String? + var codexLocalProjectUsageProgress: CodexLocalProjectUsageIndexProgress? + var codexLocalProjectUsageRefreshInFlight = false + var codexLocalProjectUsageLoadState: CodexLocalProjectUsageLoadState = .idle + var lastCodexLocalProjectUsageAttemptAt: Date? + var lastCodexLocalProjectUsageSuccessAt: Date? + @ObservationIgnored var lastCodexLocalProjectUsageFailureAt: Date? + @ObservationIgnored var codexLocalProjectUsageBackoffUntil: Date? + @ObservationIgnored var codexLocalProjectUsageFailureCount = 0 + @ObservationIgnored var activeForcedCodexLocalProjectUsageRefresh = false + @ObservationIgnored var pendingForcedCodexLocalProjectUsageRefresh = false + @ObservationIgnored var activeCodexLocalProjectUsageRequestKey: String? + @ObservationIgnored var pendingCodexLocalProjectUsageRequestKey: String? + @ObservationIgnored var codexLocalProjectUsageRequestGeneration: UInt64 = 0 + @ObservationIgnored var activeCodexLocalProjectUsageRequestGeneration: UInt64? + @ObservationIgnored var codexLocalProjectUsageRefreshTask: Task? + @ObservationIgnored var _test_codexLocalProjectUsageRefreshExecutor: + (@Sendable (_ force: Bool, _ requestGeneration: UInt64) async -> Void)? var credits: CreditsSnapshot? var lastCreditsError: String? var openAIDashboard: OpenAIDashboardSnapshot? @@ -359,6 +381,8 @@ final class UsageStore { @ObservationIgnored var lastPermissionPromptNotificationAt: [UsageProvider: Date] = [:] @ObservationIgnored var lastTokenFetchAt: [UsageProvider: Date] = [:] @ObservationIgnored var lastTokenFetchScope: [UsageProvider: String] = [:] + @ObservationIgnored var lastCodexLocalProjectUsageFetchAt: Date? + @ObservationIgnored var lastCodexLocalProjectUsageFetchScope: String? @ObservationIgnored var planUtilizationHistory: [UsageProvider: PlanUtilizationHistoryBuckets] = [:] @ObservationIgnored var sessionEquivalentBurnCache: [UsageProvider: SessionEquivalentBurnCacheEntry] = [:] @ObservationIgnored var sessionEquivalentHistoryScanCount: Int = 0 @@ -464,6 +488,7 @@ final class UsageStore { loginShellPATH: LoginShellPathCache.shared.current?.joined(separator: ":")) guard self.startupBehavior.automaticallyStartsBackgroundWork else { return } self.hydrateCachedTokenSnapshots() + self.hydrateCachedCodexLocalProjectUsage() self.detectVersions() self.updateProviderRuntimes() Task { @MainActor [weak self] in @@ -856,6 +881,7 @@ final class UsageStore { self.startupConnectivityRetryTask?.cancel() self.storageRefreshTask?.cancel() self.codexPlanHistoryBackfillTask?.cancel() + self.codexLocalProjectUsageRefreshTask?.cancel() self.resetBoundaryRefreshTask?.cancel() self.planUtilizationHistoryLoadTask?.cancel() } diff --git a/Sources/CodexBarCore/CodexLocalProjectUsageProjection.swift b/Sources/CodexBarCore/CodexLocalProjectUsageProjection.swift index d440ab5121..f05f77dfa9 100644 --- a/Sources/CodexBarCore/CodexLocalProjectUsageProjection.swift +++ b/Sources/CodexBarCore/CodexLocalProjectUsageProjection.swift @@ -48,6 +48,122 @@ public struct CodexLocalProjectUsageProjection: Sendable, Equatable { self.showsEstimatedCost ? estimate : nil } + /// Returns an inspector-only view of Models analytics. The persisted snapshot stays raw so toggling + /// cached input remains a pure display choice and never causes an index write or refresh. + public func projectedModelsAnalyticsSnapshot( + _ snapshot: CodexModelsAnalyticsSnapshot, + dailyUsage: [CodexLocalUsageDailyPoint]) -> CodexModelsAnalyticsSnapshot + { + guard !self.includesCachedInput else { return snapshot } + + var cachedByDay: [String: Int64] = [:] + for point in dailyUsage { + cachedByDay[point.day, default: 0] += Int64(max(0, point.cachedInputTokens ?? 0)) + } + + let daily = snapshot.daily.map { bucket in + let cached = min(max(0, cachedByDay[CostUsageLocalDay.key(from: bucket.day)] ?? 0), bucket.tokens) + return CodexModelsDailyBucket( + day: bucket.day, + interval: bucket.interval, + tokens: max(0, bucket.tokens - cached), + sessionIDs: bucket.sessionIDs, + sessionReferenceIDs: bucket.sessionReferenceIDs, + cost: bucket.cost) + } + let totalTokens = daily.isEmpty + ? snapshot.rows.reduce(Int64.zero) { $0 + max(0, $1.totalTokens - $1.cachedInputTokens) } + : daily.reduce(Int64.zero) { $0 + $1.tokens } + + // The snapshot has exact cached totals by day and model, but not their intersection. Allocate each + // day's exact exclusion across existing model buckets in stable model-ID order without taking more + // than that model's known cached total. + var excludedByModelBucket: [String: [Int: Int64]] = [:] + var remainingCachedByModel = Dictionary(uniqueKeysWithValues: snapshot.rows.map { row in + (row.id, max(0, row.cachedInputTokens)) + }) + for (day, cached) in cachedByDay where cached > 0 { + var remaining = cached + var candidates: [(modelID: String, index: Int, bucket: CodexModelsDailyBucket)] = [] + for (modelID, buckets) in snapshot.dailyByModel { + for (index, bucket) in buckets.enumerated() where CostUsageLocalDay.key(from: bucket.day) == day { + candidates.append((modelID, index, bucket)) + } + } + candidates.sort { lhs, rhs in + lhs.0 == rhs.0 ? lhs.1 < rhs.1 : lhs.0 < rhs.0 + } + for (modelID, index, bucket) in candidates where remaining > 0 { + let excluded = min( + remaining, + min(max(0, bucket.tokens), remainingCachedByModel[modelID, default: 0])) + excludedByModelBucket[modelID, default: [:]][index] = excluded + remaining -= excluded + remainingCachedByModel[modelID, default: 0] -= excluded + } + } + let dailyByModel = Dictionary(uniqueKeysWithValues: snapshot.dailyByModel.map { modelID, buckets in + let projected = buckets.enumerated().map { index, bucket in + let excluded = excludedByModelBucket[modelID]?[index] ?? 0 + return CodexModelsDailyBucket( + day: bucket.day, + interval: bucket.interval, + tokens: max(0, bucket.tokens - excluded), + sessionIDs: bucket.sessionIDs, + sessionReferenceIDs: bucket.sessionReferenceIDs, + cost: bucket.cost) + } + return (modelID, projected) + }) + let rows = snapshot.rows.map { row in + let cached = min(max(0, row.cachedInputTokens), max(0, row.totalTokens)) + let tokens = max(0, row.totalTokens - cached) + return CodexModelsRow( + id: row.id, + displayName: row.displayName, + rawAliases: row.rawAliases, + inputTokens: max(0, row.inputTokens - cached), + cachedInputTokens: 0, + outputTokens: row.outputTokens, + reasoningTokens: row.reasoningTokens, + totalTokens: tokens, + share: totalTokens == 0 ? 0 : Double(tokens) / Double(totalTokens), + sessionReferences: row.sessionReferences, + cost: row.cost, + previousTotalTokens: nil, + previousCost: row.previousCost, + previousSessionReferences: row.previousSessionReferences, + associatedSessionIDs: row.associatedSessionIDs, + tokenComparison: .unavailable, + costComparison: row.costComparison, + sessionReferenceComparison: row.sessionReferenceComparison) + } + + return CodexModelsAnalyticsSnapshot( + scopeID: snapshot.scopeID, + generatedAt: snapshot.generatedAt, + indexRevision: snapshot.indexRevision, + currentInterval: snapshot.currentInterval, + previousInterval: snapshot.previousInterval, + currentIsComplete: snapshot.currentIsComplete, + previousIsComplete: snapshot.previousIsComplete, + totalTokens: totalTokens, + cost: snapshot.cost, + activeModelCount: snapshot.activeModelCount, + previousActiveModelCount: snapshot.previousActiveModelCount, + newlyActiveModelCount: snapshot.newlyActiveModelCount, + uniqueSessionCount: snapshot.uniqueSessionCount, + sessionReferenceTotal: snapshot.sessionReferenceTotal, + previousSessionReferenceTotal: snapshot.previousSessionReferenceTotal, + tokenComparison: .unavailable, + costComparison: snapshot.costComparison, + sessionReferenceComparison: snapshot.sessionReferenceComparison, + rows: rows, + daily: daily, + dailyByModel: dailyByModel, + diagnostics: snapshot.diagnostics) + } + private func severities(for projects: [CodexLocalProjectUsage]) -> [String: CodexLocalUsageSeverity] { let nonZero = projects.compactMap { self.displayedTokens(for: $0.totals) }.filter { $0 > 0 }.sorted() let total = nonZero.reduce(0, +) diff --git a/Tests/CodexBarTests/CodexModelsDashboardUXTests.swift b/Tests/CodexBarTests/CodexModelsDashboardUXTests.swift new file mode 100644 index 0000000000..30f7e5e78f --- /dev/null +++ b/Tests/CodexBarTests/CodexModelsDashboardUXTests.swift @@ -0,0 +1,373 @@ +import Foundation +import Testing +@testable import CodexBar +@testable import CodexBarCore + +@Suite("Codex Models dashboard UX") +struct CodexModelsDashboardUXTests { + private let calendar: Calendar = { + var calendar = Calendar(identifier: .gregorian) + calendar.locale = Locale(identifier: "en_US_POSIX") + calendar.timeZone = TimeZone(identifier: "America/Los_Angeles")! + return calendar + }() + + @Test + @MainActor + func `search reports when it hides the selected model`() { + let intervals = self.intervals(days: 7) + let model = CodexModelsDashboardModel(snapshot: self.build( + current: [ + self.fragment(day: intervals.current.start, model: "model-a", input: 10), + self.fragment(day: intervals.current.start, model: "model-b", input: 20), + ], + intervals: intervals, + generatedAt: intervals.current.end)) + model.selectModel("model-b") + model.searchText = "model-a" + + #expect(model.selectedModelIsHiddenBySearch) + model.searchText = "" + #expect(!model.selectedModelIsHiddenBySearch) + } + + @Test + @MainActor + func `responsive profiles preserve known cost while shedding secondary columns`() { + let wide = CodexModelsTableColumnProfile.resolve( + layout: .wide, + metric: .tokens, + showsEstimatedCost: true) + let medium = CodexModelsTableColumnProfile.resolve( + layout: .medium, + metric: .tokens, + showsEstimatedCost: true) + let compact = CodexModelsTableColumnProfile.resolve( + layout: .compact, + metric: .tokens, + showsEstimatedCost: true) + let activeCost = CodexModelsTableColumnProfile.resolve( + layout: .wide, + metric: .knownCost, + showsEstimatedCost: true) + + #expect(wide == CodexModelsTableColumnProfile( + showsSessionReferences: true, + showsDelta: true, + showsKnownCost: true, + showsColumnMenu: true)) + #expect(medium == CodexModelsTableColumnProfile( + showsSessionReferences: false, + showsDelta: true, + showsKnownCost: true, + showsColumnMenu: false)) + #expect(compact == CodexModelsTableColumnProfile( + showsSessionReferences: false, + showsDelta: false, + showsKnownCost: true, + showsColumnMenu: false)) + #expect(!activeCost.showsKnownCost) + #expect(CodexModelsSelectionPresentation.resolve(layout: .wide) == .inspector) + #expect(CodexModelsSelectionPresentation.resolve(layout: .medium) == .inspector) + #expect(CodexModelsSelectionPresentation.resolve(layout: .compact) == .sheet) + } + + @Test + @MainActor + func `cost metric is unavailable without priced tokens and falls back atomically`() { + let intervals = self.intervals(days: 7) + let unavailable = self.build( + current: [self.fragment( + day: intervals.current.start, + model: "model-a", + input: 10, + costNanos: nil)], + intervals: intervals, + generatedAt: intervals.current.end) + let priced = self.build( + current: [self.fragment( + day: intervals.current.start, + model: "model-a", + input: 10, + costNanos: 100_000_000)], + intervals: intervals, + generatedAt: intervals.current.end) + let model = CodexModelsDashboardModel(snapshot: unavailable) + + #expect(!model.costMetricIsAvailable) + model.setMetric(.knownCost) + #expect(model.metric == .tokens) + model.replaceSnapshot(priced) + #expect(model.costMetricIsAvailable) + model.setMetric(.knownCost) + #expect(model.metric == .knownCost) + model.replaceSnapshot(unavailable) + #expect(model.metric == .tokens) + } + + @Test + @MainActor + func `cached input exclusion projects Models analytics without changing the raw snapshot`() throws { + let intervals = self.intervals(days: 7) + let day = try #require(self.calendar.date(byAdding: .day, value: 1, to: intervals.current.start)) + let fragments = [ + CodexModelsUsageFragment( + workspaceID: "workspace", + sessionID: "session-a", + day: day, + rawModelID: "model-a", + inputTokens: 100, + cachedInputTokens: 40, + outputTokens: 10, + costNanos: 100_000_000), + CodexModelsUsageFragment( + workspaceID: "workspace", + sessionID: "session-b", + day: day, + rawModelID: "model-b", + inputTokens: 50, + cachedInputTokens: 10, + outputTokens: 20, + costNanos: 100_000_000), + ] + let raw = CodexModelsAnalyticsBuilder().build(CodexModelsAnalyticsRequest( + source: CodexModelsAnalyticsSource(current: fragments, previous: []), + scopeID: "workspace", + periods: CodexModelsAnalyticsPeriods(current: intervals.current, previous: intervals.previous), + revision: CodexModelsAnalyticsRevision(generatedAt: intervals.current.end, indexRevision: "fixture"), + legacy: CodexModelsLegacyBaseline(totalTokens: 180, modelIDs: ["model-a", "model-b"]))) + let usage = [CodexLocalUsageDailyPoint( + day: "2026-07-10", + totalTokens: 180, + cachedInputTokens: 50, + estimatedCostUSD: 0.2)] + let includeCache = CodexLocalProjectUsageProjection(includesCachedInput: true, showsEstimatedCost: true) + let excludeCache = CodexLocalProjectUsageProjection(includesCachedInput: false, showsEstimatedCost: true) + + #expect(includeCache.projectedModelsAnalyticsSnapshot(raw, dailyUsage: usage) == raw) + + let projected = excludeCache.projectedModelsAnalyticsSnapshot(raw, dailyUsage: usage) + let dashboard = CodexModelsDashboardModel(snapshot: projected) + let csvRows = CodexModelsCSVExporter.export(snapshot: projected) + .split(separator: "\n") + .dropFirst() + .map { $0.split(separator: ",", omittingEmptySubsequences: false) } + + #expect(raw.totalTokens == 180) + #expect(raw.rows.map(\.totalTokens).sorted() == [70, 110]) + #expect(projected.totalTokens == 130) + #expect(projected.rows.map(\.totalTokens).sorted() == [60, 70]) + #expect(projected.daily.reduce(Int64.zero) { $0 + $1.tokens } == 130) + #expect(projected.dailyByModel.values.flatMap(\.self).reduce(Int64.zero) { $0 + $1.tokens } == 130) + #expect(projected.dailyByModel["model-a"]?.map(\.tokens) == [70]) + #expect(projected.dailyByModel["model-b"]?.map(\.tokens) == [60]) + #expect(projected.invariantViolations().isEmpty) + #expect(dashboard.rankedRows.map(\.id) == ["model-a", "model-b"]) + #expect(dashboard.tableRows.map(\.row.totalTokens) == [70, 60]) + #expect(dashboard.timelineData.buckets.reduce(Int64.zero) { $0 + $1.tokens } == 130) + #expect(csvRows.map { String($0[8]) }.sorted() == ["60", "70"]) + #expect(csvRows.allSatisfy { $0[10] == "0" }) + #expect(projected.tokenComparison == .unavailable) + #expect(projected.rows.allSatisfy { $0.tokenComparison == .unavailable && $0.previousTotalTokens == nil }) + #expect(raw.rows.allSatisfy { $0.cachedInputTokens > 0 }) + } + + @Test + @MainActor + func `partial final bucket is excluded from completed peak insight`() throws { + let intervals = self.intervals(days: 7) + let secondDay = try #require(self.calendar.date(byAdding: .day, value: 1, to: intervals.current.start)) + let thirdDay = try #require(self.calendar.date(byAdding: .day, value: 2, to: intervals.current.start)) + let generatedAt = try #require(self.calendar.date(byAdding: .hour, value: 12, to: thirdDay)) + let model = CodexModelsDashboardModel(snapshot: self.build( + current: [ + self.fragment(day: intervals.current.start, model: "model-a", input: 30), + self.fragment(day: secondDay, model: "model-a", input: 10), + self.fragment(day: thirdDay, model: "model-a", input: 100), + ], + intervals: intervals, + generatedAt: generatedAt, + currentIsComplete: false)) + + let timeline = model.timelineData + + #expect(timeline.completedPlottedBuckets.map(\.tokens) == [30, 10]) + #expect(timeline.inProgressBucket?.tokens == 100) + #expect(timeline.peakBucket?.tokens == 30) + #expect(try abs(#require(timeline.changeFromPeak) - (-2.0 / 3.0)) < 0.0001) + } + + @Test + func `protected CSV structurally removes adversarial personal identifiers`() throws { + let rawPath = "/Users/private.person/Developer/Secret,Workspace" + let rawTitle = "Confidential \"acquisition\" planning" + let scopeID = "scope,quoted\"\r\n\(rawPath)" + let sessionIDs = [ + "comma,marker", + "quote\"marker", + "carriage\rreturn", + "line\nfeed", + rawPath, + rawTitle, + ] + let intervals = self.intervals(days: 7) + let snapshot = self.build( + current: sessionIDs.map { + self.fragment( + day: intervals.current.start, + model: "gpt-5", + input: 10, + workspaceID: scopeID, + sessionID: $0) + }, + intervals: intervals, + generatedAt: intervals.current.end, + scopeID: scopeID) + + let protectedCSV = CodexModelsPresentationCSVExporter.export( + snapshot: snapshot, + rows: snapshot.rows, + metric: .tokens, + hidePersonalInfo: true) + let normalCSV = CodexModelsPresentationCSVExporter.export( + snapshot: snapshot, + rows: snapshot.rows, + metric: .tokens, + hidePersonalInfo: false) + let protectedRecords = self.parseCSV(protectedCSV) + let normalRecords = self.parseCSV(normalCSV) + let protectedSnapshot = try #require(CodexModelsPresentationCSVExporter.protectedSnapshot(snapshot)) + + #expect(protectedRecords.count == 2) + #expect(normalRecords.count == 2) + #expect(protectedRecords[0] == normalRecords[0]) + #expect(protectedRecords[0].count == 32) + #expect(protectedRecords.allSatisfy { $0.count == protectedRecords[0].count }) + #expect(normalRecords.allSatisfy { $0.count == normalRecords[0].count }) + #expect(protectedRecords[1][0] == "hidden_workspace") + #expect(protectedRecords[1][6].isEmpty) + #expect(normalRecords[1][0] == scopeID) + for sentinel in sessionIDs { + #expect(normalRecords[1][6].contains(sentinel)) + #expect(!protectedCSV.contains(sentinel)) + } + #expect(!protectedCSV.contains(scopeID)) + + for column in protectedRecords[0].indices where column != 0 && column != 6 { + #expect(protectedRecords[1][column] == normalRecords[1][column]) + } + + #expect(protectedSnapshot.scopeID == "hidden_workspace") + #expect(protectedSnapshot.generatedAt == snapshot.generatedAt) + #expect(protectedSnapshot.indexRevision == snapshot.indexRevision) + #expect(protectedSnapshot.currentInterval == snapshot.currentInterval) + #expect(protectedSnapshot.previousInterval == snapshot.previousInterval) + #expect(protectedSnapshot.currentIsComplete == snapshot.currentIsComplete) + #expect(protectedSnapshot.previousIsComplete == snapshot.previousIsComplete) + #expect(protectedSnapshot.totalTokens == snapshot.totalTokens) + #expect(protectedSnapshot.cost == snapshot.cost) + #expect(protectedSnapshot.activeModelCount == snapshot.activeModelCount) + #expect(protectedSnapshot.previousActiveModelCount == snapshot.previousActiveModelCount) + #expect(protectedSnapshot.newlyActiveModelCount == snapshot.newlyActiveModelCount) + #expect(protectedSnapshot.uniqueSessionCount == snapshot.uniqueSessionCount) + #expect(protectedSnapshot.sessionReferenceTotal == snapshot.sessionReferenceTotal) + #expect(protectedSnapshot.previousSessionReferenceTotal == snapshot.previousSessionReferenceTotal) + #expect(protectedSnapshot.tokenComparison == snapshot.tokenComparison) + #expect(protectedSnapshot.costComparison == snapshot.costComparison) + #expect(protectedSnapshot.sessionReferenceComparison == snapshot.sessionReferenceComparison) + #expect(protectedSnapshot.daily == snapshot.daily) + #expect(protectedSnapshot.dailyByModel == snapshot.dailyByModel) + #expect(protectedSnapshot.diagnostics == snapshot.diagnostics) + #expect(protectedSnapshot.rows.count == snapshot.rows.count) + #expect(protectedSnapshot.rows.allSatisfy { $0.associatedSessionIDs == nil }) + } + + private func parseCSV(_ csv: String) -> [[String]] { + let characters = Array(csv) + var records: [[String]] = [] + var record: [String] = [] + var field = "" + var isQuoted = false + var index = 0 + + while index < characters.count { + let character = characters[index] + if character == "\"" { + if isQuoted, index + 1 < characters.count, characters[index + 1] == "\"" { + field.append("\"") + index += 2 + continue + } + isQuoted.toggle() + } else if character == ",", !isQuoted { + record.append(field) + field = "" + } else if character == "\r" || character == "\n", !isQuoted { + record.append(field) + records.append(record) + record = [] + field = "" + if character == "\r", index + 1 < characters.count, characters[index + 1] == "\n" { + index += 1 + } + } else { + field.append(character) + } + index += 1 + } + + if !field.isEmpty || !record.isEmpty { + record.append(field) + records.append(record) + } + return records + } + + private func build( + current: [CodexModelsUsageFragment], + intervals: (current: DateInterval, previous: DateInterval), + generatedAt: Date, + currentIsComplete: Bool = true, + scopeID: String? = nil) -> CodexModelsAnalyticsSnapshot + { + CodexModelsAnalyticsBuilder().build(CodexModelsAnalyticsRequest( + source: CodexModelsAnalyticsSource(current: current, previous: []), + scopeID: scopeID, + periods: CodexModelsAnalyticsPeriods(current: intervals.current, previous: intervals.previous), + revision: CodexModelsAnalyticsRevision(generatedAt: generatedAt, indexRevision: "dashboard-ux-fixture"), + legacy: CodexModelsLegacyBaseline( + totalTokens: current.reduce(0) { $0 + $1.totalTokens }, + modelIDs: Array(Set(current.map(\.rawModelID)))), + currentIsComplete: currentIsComplete, + previousIsComplete: true)) + } + + private func intervals(days: Int) -> (current: DateInterval, previous: DateInterval) { + let end = self.calendar.date(from: DateComponents(year: 2026, month: 7, day: 16))! + let currentStart = self.calendar.date(byAdding: .day, value: -days, to: end)! + let previousStart = self.calendar.date(byAdding: .day, value: -days, to: currentStart)! + return ( + DateInterval(start: currentStart, end: end), + DateInterval(start: previousStart, end: currentStart)) + } + + private func fragment( + day: Date, + model: String, + input: Int64, + costNanos: Int64? = 100_000_000, + workspaceID: String = "workspace", + sessionID: String? = nil) -> CodexModelsUsageFragment + { + CodexModelsUsageFragment( + workspaceID: workspaceID, + sessionID: sessionID ?? "session-\(model)", + day: day, + timestamp: nil, + rawModelID: model, + inputTokens: input, + cachedInputTokens: 0, + outputTokens: 0, + costNanos: costNanos) + } +} diff --git a/Tests/CodexBarTests/CodexWorkspacePresentationInteractionTests.swift b/Tests/CodexBarTests/CodexWorkspacePresentationInteractionTests.swift new file mode 100644 index 0000000000..91fe5f5df2 --- /dev/null +++ b/Tests/CodexBarTests/CodexWorkspacePresentationInteractionTests.swift @@ -0,0 +1,290 @@ +import Foundation +import Testing +@testable import CodexBar +@testable import CodexBarCore + +@MainActor +struct CodexWorkspacePresentationInteractionTests { + @Test + func `session model filter clears only unavailable selections`() { + #expect(CodexWorkspaceSessionModelFilter.reconciledSelection( + "gpt-5", + availableModels: ["gpt-5", "o3"]) == "gpt-5") + #expect(CodexWorkspaceSessionModelFilter.reconciledSelection( + "missing-model", + availableModels: ["gpt-5", "o3"]).isEmpty) + #expect(CodexWorkspaceSessionModelFilter.reconciledSelection( + "missing-model", + availableModels: []).isEmpty) + #expect(CodexWorkspaceSessionModelFilter.reconciledSelection( + "", + availableModels: ["gpt-5"]).isEmpty) + } + + @Test + func `associated session filter clears when workspace selection changes`() { + let sessionIDs: Set = ["session-one", "session-two"] + + #expect(CodexWorkspaceAssociatedSessionFilter.reconciledSessionIDs( + sessionIDs, + from: "workspace-one", + to: "workspace-one") == sessionIDs) + #expect(CodexWorkspaceAssociatedSessionFilter.reconciledSessionIDs( + sessionIDs, + from: "workspace-one", + to: "workspace-two") == nil) + #expect(CodexWorkspaceAssociatedSessionFilter.reconciledSessionIDs( + sessionIDs, + from: CodexLocalProjectUsageWindowDestination.allWorkspacesID, + to: "workspace-one") == nil) + } + + @Test + func `sidebar selection navigation moves and clamps`() { + let allWorkspacesID = CodexLocalProjectUsageWindowDestination.allWorkspacesID + let orderedIDs = [allWorkspacesID, "workspace-one", "workspace-two"] + + #expect(CodexWorkspaceSidebarSelectionNavigator.movingSelection( + from: allWorkspacesID, + direction: .down, + orderedIDs: orderedIDs) == "workspace-one") + #expect(CodexWorkspaceSidebarSelectionNavigator.movingSelection( + from: "workspace-one", + direction: .up, + orderedIDs: orderedIDs) == allWorkspacesID) + #expect(CodexWorkspaceSidebarSelectionNavigator.movingSelection( + from: allWorkspacesID, + direction: .up, + orderedIDs: orderedIDs) == allWorkspacesID) + #expect(CodexWorkspaceSidebarSelectionNavigator.movingSelection( + from: "workspace-two", + direction: .down, + orderedIDs: orderedIDs) == "workspace-two") + } + + @Test + func `sidebar selection navigation recovers invalid and empty selections`() { + let allWorkspacesID = CodexLocalProjectUsageWindowDestination.allWorkspacesID + let orderedIDs = [allWorkspacesID, "workspace-one"] + + #expect(CodexWorkspaceSidebarSelectionNavigator.movingSelection( + from: nil, + direction: .up, + orderedIDs: orderedIDs) == allWorkspacesID) + #expect(CodexWorkspaceSidebarSelectionNavigator.movingSelection( + from: "missing-workspace", + direction: .down, + orderedIDs: orderedIDs) == allWorkspacesID) + #expect(CodexWorkspaceSidebarSelectionNavigator.movingSelection( + from: nil, + direction: .down, + orderedIDs: [allWorkspacesID]) == allWorkspacesID) + #expect(CodexWorkspaceSidebarSelectionNavigator.movingSelection( + from: allWorkspacesID, + direction: .down, + orderedIDs: []) == nil) + } + + @Test + func `models metric copy distinguishes known cost from complete totals`() { + #expect(CodexModelsMetric.tokens.analyticsNoun == "tokens") + #expect(CodexModelsMetric.tokens.shareBasisTitle == "total tokens") + #expect(CodexModelsMetric.knownCost.analyticsNoun == "known cost") + #expect(CodexModelsMetric.knownCost.shareBasisTitle == "known cost") + #expect(CodexModelsMetric.sessionReferences.shareBasisTitle == "total session references") + } + + @Test + func `daily selection navigation anchors newest and clamps at range edges`() { + let dates = (0..<4).map { day in + Date(timeIntervalSinceReferenceDate: TimeInterval(day * 86400)) + } + + #expect(WorkspaceDailyUsageSelectionNavigator.movingDate( + from: nil, + direction: .earlier, + availableDates: dates) == dates[2]) + #expect(WorkspaceDailyUsageSelectionNavigator.movingDate( + from: nil, + direction: .later, + availableDates: dates) == dates[3]) + #expect(WorkspaceDailyUsageSelectionNavigator.movingDate( + from: dates[0], + direction: .earlier, + availableDates: dates) == dates[0]) + #expect(WorkspaceDailyUsageSelectionNavigator.movingDate( + from: dates[3], + direction: .later, + availableDates: dates) == dates[3]) + #expect(WorkspaceDailyUsageSelectionNavigator.movingDate( + from: dates[2], + direction: .earlier, + availableDates: dates) == dates[1]) + #expect(WorkspaceDailyUsageSelectionNavigator.movingDate( + from: dates[2], + direction: .later, + availableDates: dates) == dates[3]) + } + + @Test + func `daily selection navigation handles empty and single day ranges`() { + let date = Date(timeIntervalSinceReferenceDate: 0) + #expect(WorkspaceDailyUsageSelectionNavigator.movingDate( + from: nil, + direction: .later, + availableDates: []) == nil) + #expect(WorkspaceDailyUsageSelectionNavigator.movingDate( + from: nil, + direction: .earlier, + availableDates: [date]) == date) + #expect(WorkspaceDailyUsageSelectionNavigator.movingDate( + from: date, + direction: .later, + availableDates: [date]) == date) + } + + @Test + func `local day keys remain on their calendar day west and east of GMT`() throws { + var losAngeles = Calendar(identifier: .gregorian) + losAngeles.timeZone = try #require(TimeZone(identifier: "America/Los_Angeles")) + let losAngelesDate = WorkspaceDailyPoint.dateFromDayKey("2026-07-31", calendar: losAngeles) + let losAngelesComponents = losAngeles.dateComponents([.year, .month, .day], from: losAngelesDate) + let formatter = DateFormatter() + formatter.calendar = losAngeles + formatter.timeZone = losAngeles.timeZone + formatter.locale = Locale(identifier: "en_US") + formatter.dateStyle = .long + formatter.timeStyle = .none + let formattedDate = formatter.string(from: losAngelesDate) + + #expect(losAngelesComponents.year == 2026) + #expect(losAngelesComponents.month == 7) + #expect(losAngelesComponents.day == 31) + #expect(formattedDate.contains("July 31")) + #expect(!formattedDate.contains("July 30")) + + var tokyo = Calendar(identifier: .gregorian) + tokyo.timeZone = try #require(TimeZone(identifier: "Asia/Tokyo")) + let tokyoDate = WorkspaceDailyPoint.dateFromDayKey("2026-07-31", calendar: tokyo) + let tokyoComponents = tokyo.dateComponents([.year, .month, .day], from: tokyoDate) + #expect(tokyoComponents.year == 2026) + #expect(tokyoComponents.month == 7) + #expect(tokyoComponents.day == 31) + } + + @Test + func `project accessibility description includes severity and conditional cost`() { + let withoutCost = CodexWorkspaceProjectAccessibilityDescription.make( + projectName: "CodexBar", + tokenText: "1.2K", + severity: .elevated, + estimatedCostText: nil, + session: (3, "gpt-5")) + #expect(withoutCost == [ + "CodexBar", + L("codex_workspaces_token_count_accessibility", "1.2K"), + CodexLocalUsageSeverity.elevated.workspaceAccessibilityDescription, + L("codex_workspaces_session_count", 3), + "gpt-5", + ].joined(separator: ", ")) + + let withCost = CodexWorkspaceProjectAccessibilityDescription.make( + projectName: "CodexBar", + tokenText: "1.2K", + severity: .normal, + estimatedCostText: "est. $1.00", + session: (3, nil)) + #expect(withCost == [ + "CodexBar", + L("codex_workspaces_token_count_accessibility", "1.2K"), + CodexLocalUsageSeverity.normal.workspaceAccessibilityDescription, + "est. $1.00", + L("codex_workspaces_session_count", 3), + ].joined(separator: ", ")) + } + + @Test + func `protected snapshot keeps raw workspace and chat values out of presentation seams`() { + let rawPath = "/Users/private.person/Developer/SecretWorkspace" + let rawTitle = "Confidential acquisition planning" + let totals = CodexLocalUsageTotals( + inputTokens: 100, + cachedInputTokens: 0, + outputTokens: 20, + totalTokens: 120) + let daily = CodexLocalUsageDailyPoint( + day: "2026-07-31", + totalTokens: 120, + estimatedCostUSD: 0.25) + let session = CodexLocalSessionUsage( + id: "stable-session-id", + projectId: "stable-workspace-id", + displayTitle: CodexLocalSessionUsage.localChatFallbackTitle, + cwd: nil, + startedAt: Date(timeIntervalSince1970: 1_700_000_000), + latestActivity: Date(timeIntervalSince1970: 1_700_000_100), + totals: totals, + estimatedCostUSD: 0.25, + hasUnknownCost: false, + topModel: "gpt-5", + daily: [daily]) + let project = CodexLocalProjectUsage( + id: "stable-workspace-id", + displayName: "Workspace", + path: nil, + totals: totals, + estimatedCostUSD: 0.25, + hasUnknownCost: false, + sessionCount: 1, + latestActivity: session.latestActivity, + topModel: "gpt-5", + topSessions: [session], + modelBreakdowns: [], + daily: [daily]) + let snapshot = CodexLocalProjectUsageSnapshot( + updatedAt: Date(timeIntervalSince1970: 1_700_000_100), + historyDays: 30, + scopeSignature: "stable-scope", + rootsFingerprint: [:], + indexedFileCount: 1, + skippedFileCount: 0, + total: totals, + projects: [project], + sessions: [session], + daily: [daily]) + let presentation = CodexLocalProjectUsageWindowPresentation( + snapshot: snapshot, + selectedDestinationID: project.id, + projection: CodexLocalProjectUsageProjection( + includesCachedInput: true, + showsEstimatedCost: true)) + let accessibility = CodexWorkspaceProjectAccessibilityDescription.make( + projectName: presentation.rankedProjects[0].displayName, + tokenText: presentation.sessions[0].tokenText, + severity: presentation.rankedProjects[0].severity, + estimatedCostText: nil, + session: (presentation.sessionCount, presentation.topModel)) + let visibleAndAccessibleValues = [ + presentation.rankedProjects[0].displayName, + presentation.rankedProjects[0].path ?? "—", + presentation.sessions[0].title, + presentation.sessions[0].path, + presentation.daily[0].breakdownRows[0].title, + accessibility, + ].joined(separator: "\n") + + #expect(!visibleAndAccessibleValues.contains(rawPath)) + #expect(!visibleAndAccessibleValues.contains(rawTitle)) + #expect(presentation.selectedDestinationID == "stable-workspace-id") + #expect(CodexWorkspaceSessionRows.filteredAndSorted( + presentation.sessions, + searchText: rawPath, + selectedModel: "", + sortOrder: []).isEmpty) + #expect(CodexWorkspaceSessionRows.filteredAndSorted( + presentation.sessions, + searchText: rawTitle, + selectedModel: "", + sortOrder: []).isEmpty) + } +} diff --git a/Tests/CodexBarTests/LocalizationLanguageCatalogTests.swift b/Tests/CodexBarTests/LocalizationLanguageCatalogTests.swift index 27a81f1d63..aec8098eb7 100644 --- a/Tests/CodexBarTests/LocalizationLanguageCatalogTests.swift +++ b/Tests/CodexBarTests/LocalizationLanguageCatalogTests.swift @@ -30,6 +30,42 @@ struct LocalizationLanguageCatalogTests { "language_galician", ] + @Test + func `Codex Workspaces and Models catalogs have complete key and placeholder parity`() throws { + let root = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() + .deletingLastPathComponent() + .deletingLastPathComponent() + let resourcesURL = root.appendingPathComponent("Sources/CodexBar/Resources") + let englishURL = resourcesURL.appendingPathComponent("en.lproj/Localizable.strings") + let english = try #require(NSDictionary(contentsOf: englishURL) as? [String: String]) + let requiredKeys = english.keys.filter { + $0.hasPrefix("codex_workspaces_") || $0.hasPrefix("codex_models_") + }.sorted() + + #expect(requiredKeys.count(where: { $0.hasPrefix("codex_workspaces_") }) == 72) + #expect(requiredKeys.count(where: { $0.hasPrefix("codex_models_") }) == 178) + + for language in AppLanguage.allCases where language != .system { + let url = resourcesURL.appendingPathComponent("\(language.rawValue).lproj/Localizable.strings") + let catalog = try #require(NSDictionary(contentsOf: url) as? [String: String]) + let localizedKeys = catalog.keys.filter { + $0.hasPrefix("codex_workspaces_") || $0.hasPrefix("codex_models_") + }.sorted() + #expect(localizedKeys == requiredKeys, "\(language.rawValue) feature-key parity") + for key in requiredKeys { + let englishValue = try #require(english[key]) + let value = try #require(catalog[key], "\(language.rawValue).\(key)") + #expect(!value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + let valuePlaceholders = try Self.formatPlaceholders(in: value) + let englishPlaceholders = try Self.formatPlaceholders(in: englishValue) + #expect( + valuePlaceholders == englishPlaceholders, + "\(language.rawValue).\(key)") + } + } + } + @Test func `app language catalog includes Ukrainian`() { #expect(AppLanguage.allCases.contains(.ukrainian)) @@ -524,6 +560,20 @@ struct LocalizationLanguageCatalogTests { "byte_unit_gigabyte", "byte_unit_kilobyte", "byte_unit_megabyte", + "codex_models_bullet_value", + "codex_models_compact_billions_suffix", + "codex_models_compact_millions_suffix", + "codex_models_compact_thousands_suffix", + "codex_models_cost_status_currency", + "codex_models_export_all_filename", + "codex_models_export_default_filename", + "codex_models_export_selected_filename", + "codex_models_export_visible_filename", + "codex_models_name_value_accessibility", + "codex_models_value_comparison_accessibility", + "codex_models_value_detail", + "codex_workspaces_bullet_value", + "codex_workspaces_daily_callout_accessibility", "hooks_executable_placeholder", "hooks_provider", "hooks_threshold_placeholder", @@ -670,4 +720,12 @@ struct LocalizationLanguageCatalogTests { defer { defaults.removePersistentDomain(forName: suiteName) } body(defaults, suiteName) } + + private static func formatPlaceholders(in value: String) throws -> [String] { + let expression = try NSRegularExpression(pattern: "%(?:\\d+\\$)?[@d]") + let range = NSRange(value.startIndex.. (SettingsStore, UserDefaults, String) { + let suite = "SettingsStoreCodexLocalProjectUsageTests-\(UUID().uuidString)" + let defaults = try #require(UserDefaults(suiteName: suite)) + defaults.removePersistentDomain(forName: suite) + return (self.makeSettings(userDefaults: defaults, suite: suite), defaults, suite) + } + + private func makeSettings(userDefaults: UserDefaults, suite: String) -> SettingsStore { + SettingsStore( + userDefaults: userDefaults, + configStore: testConfigStore(suiteName: suite), + zaiTokenStore: NoopZaiTokenStore(), + syntheticTokenStore: NoopSyntheticTokenStore()) + } +} diff --git a/Tests/CodexBarTests/StatusMenuCodexLocalProjectUsageTests.swift b/Tests/CodexBarTests/StatusMenuCodexLocalProjectUsageTests.swift new file mode 100644 index 0000000000..6d76af7585 --- /dev/null +++ b/Tests/CodexBarTests/StatusMenuCodexLocalProjectUsageTests.swift @@ -0,0 +1,469 @@ +import AppKit +import CodexBarCore +import Testing +@testable import CodexBar + +@MainActor +extension StatusMenuTests { + @Test + func `codex workspaces row is present in the populated provider menu`() throws { + self.disableMenuCardsForTesting() + let settings = self.makeSettings() + settings.statusChecksEnabled = false + settings.refreshFrequency = .manual + settings.mergeIcons = false + settings.selectedMenuProvider = .codex + settings.costUsageEnabled = true + settings.codexLocalProjectUsageEnabled = true + + let registry = ProviderRegistry.shared + for provider in UsageProvider.allCases { + guard let metadata = registry.metadata[provider] else { continue } + settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: provider == .codex) + } + + let fetcher = UsageFetcher() + let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) + let controller = StatusItemController( + store: store, + settings: settings, + account: fetcher.loadAccountInfo(), + updater: DisabledUpdaterController(), + preferencesSelection: PreferencesSelection(), + statusBar: self.makeStatusBarForTesting()) + defer { controller.releaseStatusItemsForTesting() } + + let menu = NSMenu() + controller.populateMenu(menu, provider: .codex) + + let item = try #require(menu.items.first { $0.title == "Workspaces" }) + #expect(item.submenu != nil) + } + + @Test + func `codex workspaces section does not duplicate a trailing separator`() { + self.disableMenuCardsForTesting() + let settings = self.makeSettings() + settings.statusChecksEnabled = false + settings.refreshFrequency = .manual + settings.codexLocalProjectUsageEnabled = true + + let fetcher = UsageFetcher() + let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) + let controller = StatusItemController( + store: store, + settings: settings, + account: fetcher.loadAccountInfo(), + updater: DisabledUpdaterController(), + preferencesSelection: PreferencesSelection(), + statusBar: self.makeStatusBarForTesting()) + defer { controller.releaseStatusItemsForTesting() } + + let menu = NSMenu() + let credits = NSMenuItem(title: "Credits", action: nil, keyEquivalent: "") + credits.representedObject = "menuCardCredits" + menu.addItem(credits) + menu.addItem(.separator()) + + controller.addCodexLocalProjectUsageMenuSection(to: menu, provider: .codex, width: 420) + + #expect(menu.items.count == 4) + #expect(menu.items[1].isSeparatorItem) + #expect(menu.items[2].title == "Workspaces") + #expect(menu.items[3].isSeparatorItem) + } + + @Test + func `codex workspaces row exposes a native hosted submenu`() throws { + self.disableMenuCardsForTesting() + let settings = self.makeSettings() + settings.statusChecksEnabled = false + settings.refreshFrequency = .manual + settings.mergeIcons = true + settings.selectedMenuProvider = .codex + settings.costUsageEnabled = true + settings.codexLocalProjectUsageEnabled = true + + let registry = ProviderRegistry.shared + for provider in UsageProvider.allCases { + guard let metadata = registry.metadata[provider] else { continue } + settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: provider == .codex) + } + + let fetcher = UsageFetcher() + let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) + let scope = store.tokenCostScope(for: .codex) + store.lastCodexLocalProjectUsageFetchAt = Date() + store.lastCodexLocalProjectUsageFetchScope = "\(scope.signature)|historyDays=\(settings.costUsageHistoryDays)" + + "|hidePersonalInfo=\(settings.hidePersonalInfo)" + + let controller = StatusItemController( + store: store, + settings: settings, + account: fetcher.loadAccountInfo(), + updater: DisabledUpdaterController(), + preferencesSelection: PreferencesSelection(), + statusBar: self.makeStatusBarForTesting()) + defer { controller.releaseStatusItemsForTesting() } + + let menu = NSMenu() + let didAdd = controller.addCodexLocalProjectUsageMenuItemIfNeeded(to: menu, provider: .codex, width: 420) + + #expect(didAdd) + let item = try #require(menu.items.first { $0.title == "Workspaces" }) + let submenu = try #require(item.submenu) + #expect(submenu.minimumWidth == CodexLocalProjectUsageSubmenuLayout.menuWidth) + #expect(item.view == nil) + #expect(menu.items.count == 1) + #expect(item.target === controller) + #expect(item.action == #selector(controller.menuCardNoOp(_:))) + #expect( + submenu.items.first?.representedObject as? String + == StatusItemController.codexLocalProjectUsageSubmenuID) + } + + @Test + func `codex workspaces row construction does not enqueue refresh work`() async { + self.disableMenuCardsForTesting() + let settings = self.makeSettings() + settings.statusChecksEnabled = false + settings.refreshFrequency = .manual + settings.mergeIcons = true + settings.selectedMenuProvider = .codex + settings.costUsageEnabled = true + settings.codexLocalProjectUsageEnabled = true + + let registry = ProviderRegistry.shared + for provider in UsageProvider.allCases { + guard let metadata = registry.metadata[provider] else { continue } + settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: provider == .codex) + } + + let fetcher = UsageFetcher() + let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) + let controller = StatusItemController( + store: store, + settings: settings, + account: fetcher.loadAccountInfo(), + updater: DisabledUpdaterController(), + preferencesSelection: PreferencesSelection(), + statusBar: self.makeStatusBarForTesting()) + defer { controller.releaseStatusItemsForTesting() } + + let menu = NSMenu() + let didAdd = controller.addCodexLocalProjectUsageMenuItemIfNeeded(to: menu, provider: .codex, width: 420) + await Task.yield() + await Task.yield() + + #expect(didAdd) + #expect(store.codexLocalProjectUsageRefreshInFlight == false) + #expect(store.codexLocalProjectUsageProgress == nil) + #expect(store.lastCodexLocalProjectUsageFetchScope == nil) + } + + @Test + func `codex workspaces row does not require cost summary section`() { + self.disableMenuCardsForTesting() + let settings = self.makeSettings() + settings.statusChecksEnabled = false + settings.refreshFrequency = .manual + settings.mergeIcons = true + settings.selectedMenuProvider = .codex + settings.costUsageEnabled = false + settings.codexLocalProjectUsageEnabled = true + + let registry = ProviderRegistry.shared + for provider in UsageProvider.allCases { + guard let metadata = registry.metadata[provider] else { continue } + settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: provider == .codex) + } + + let fetcher = UsageFetcher() + let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) + let controller = StatusItemController( + store: store, + settings: settings, + account: fetcher.loadAccountInfo(), + updater: DisabledUpdaterController(), + preferencesSelection: PreferencesSelection(), + statusBar: self.makeStatusBarForTesting()) + defer { controller.releaseStatusItemsForTesting() } + + let menu = NSMenu() + let didAdd = controller.addCodexLocalProjectUsageMenuItemIfNeeded(to: menu, provider: .codex, width: 420) + + #expect(didAdd) + #expect(menu.items.first?.title == "Workspaces") + } + + @Test + func `codex workspaces row updates indexing progress in place`() throws { + self.disableMenuCardsForTesting() + let settings = self.makeSettings() + settings.statusChecksEnabled = false + settings.refreshFrequency = .manual + settings.mergeIcons = true + settings.selectedMenuProvider = .codex + settings.costUsageEnabled = true + settings.codexLocalProjectUsageEnabled = true + + let registry = ProviderRegistry.shared + for provider in UsageProvider.allCases { + guard let metadata = registry.metadata[provider] else { continue } + settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: provider == .codex) + } + + let fetcher = UsageFetcher() + let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) + let scope = store.tokenCostScope(for: .codex) + store.lastCodexLocalProjectUsageFetchAt = Date() + store.lastCodexLocalProjectUsageFetchScope = "\(scope.signature)|historyDays=\(settings.costUsageHistoryDays)" + store.codexLocalProjectUsageRefreshInFlight = true + store.codexLocalProjectUsageProgress = CodexLocalProjectUsageIndexProgress( + phase: .indexingProjects, + processedFileCount: 25, + totalFileCount: 503, + indexedFileCount: 25, + skippedFileCount: 0) + + let controller = StatusItemController( + store: store, + settings: settings, + account: fetcher.loadAccountInfo(), + updater: DisabledUpdaterController(), + preferencesSelection: PreferencesSelection(), + statusBar: self.makeStatusBarForTesting()) + defer { controller.releaseStatusItemsForTesting() } + + let menu = NSMenu() + let didAdd = controller.addCodexLocalProjectUsageMenuItemIfNeeded(to: menu, provider: .codex, width: 420) + #expect(didAdd) + let item = try #require(menu.items.first { $0.title == "Workspaces" }) + let progressItem = try #require(self.codexLocalProjectUsageProgressItem(from: item)) + #expect(progressItem.isEnabled == false) + #expect(progressItem.submenu == nil) + #expect(progressItem.view?.fittingSize.height == 24) + let progressView = try #require(progressItem.view) + let progressIndicator = try #require(self.progressIndicators(in: progressView).first) + #expect(progressIndicator.isHidden == false) + #expect(progressIndicator.isIndeterminate) + #expect(self.codexLocalProjectUsageSubtitle(from: item) == "Indexing local Codex logs…") + #expect(store.codexLocalProjectUsageProgressSubtitle == "Indexing projects… 25/503 files, 478 left") + + store.codexLocalProjectUsageProgress = CodexLocalProjectUsageIndexProgress( + phase: .indexingProjects, + processedFileCount: 50, + totalFileCount: 503, + indexedFileCount: 50, + skippedFileCount: 0) + controller.updateCodexLocalProjectUsageRows() + + #expect(self.codexLocalProjectUsageProgressItem(from: item) === progressItem) + #expect(self.codexLocalProjectUsageSubtitle(from: item) == "Indexing local Codex logs…") + #expect(store.codexLocalProjectUsageProgressSubtitle == "Indexing projects… 50/503 files, 453 left") + } + + @Test + func `codex workspaces menu action opens the persistent window`() throws { + self.disableMenuCardsForTesting() + let settings = self.makeSettings() + settings.statusChecksEnabled = false + settings.refreshFrequency = .manual + settings.mergeIcons = true + settings.selectedMenuProvider = .codex + settings.costUsageEnabled = true + settings.codexLocalProjectUsageEnabled = true + + let registry = ProviderRegistry.shared + for provider in UsageProvider.allCases { + guard let metadata = registry.metadata[provider] else { continue } + settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: provider == .codex) + } + + let fetcher = UsageFetcher() + let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) + let scope = store.tokenCostScope(for: .codex) + store.lastCodexLocalProjectUsageFetchAt = Date() + store.lastCodexLocalProjectUsageFetchScope = "\(scope.signature)|historyDays=\(settings.costUsageHistoryDays)" + + let controller = StatusItemController( + store: store, + settings: settings, + account: fetcher.loadAccountInfo(), + updater: DisabledUpdaterController(), + preferencesSelection: PreferencesSelection(), + statusBar: self.makeStatusBarForTesting()) + defer { controller.releaseStatusItemsForTesting() } + + let menu = NSMenu() + let didAdd = controller.addCodexLocalProjectUsageMenuItemIfNeeded(to: menu, provider: .codex, width: 420) + #expect(didAdd) + let item = try #require(menu.items.first { $0.title == "Workspaces" }) + + controller.openCodexLocalProjectUsageWindowFromMenu(item) + + #expect(controller.codexLocalProjectUsageWindow != nil) + controller.codexLocalProjectUsageWindow?.close() + } + + @Test + func `codex workspaces submenu hydrates through the hosted view pipeline`() throws { + self.disableMenuCardsForTesting() + let settings = self.makeSettings() + settings.statusChecksEnabled = false + settings.refreshFrequency = .manual + settings.mergeIcons = true + settings.selectedMenuProvider = .codex + settings.costUsageEnabled = true + settings.codexLocalProjectUsageEnabled = true + + let registry = ProviderRegistry.shared + for provider in UsageProvider.allCases { + guard let metadata = registry.metadata[provider] else { continue } + settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: provider == .codex) + } + + let fetcher = UsageFetcher() + let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) + let scope = store.tokenCostScope(for: .codex) + store.lastCodexLocalProjectUsageFetchAt = Date() + store.lastCodexLocalProjectUsageFetchScope = "\(scope.signature)|historyDays=\(settings.costUsageHistoryDays)" + + let controller = StatusItemController( + store: store, + settings: settings, + account: fetcher.loadAccountInfo(), + updater: DisabledUpdaterController(), + preferencesSelection: PreferencesSelection(), + statusBar: self.makeStatusBarForTesting()) + defer { controller.releaseStatusItemsForTesting() } + + let menu = NSMenu() + #expect(controller.addCodexLocalProjectUsageMenuItemIfNeeded(to: menu, provider: .codex, width: 420)) + let item = try #require(menu.items.first { $0.title == "Workspaces" }) + let submenu = try #require(item.submenu) + + #expect(controller.hydrateHostedSubviewMenuIfNeeded(submenu, width: 520)) + #expect(submenu.items.count == 3) + #expect( + submenu.items.first?.representedObject as? String + == StatusItemController.codexLocalProjectUsageSubmenuID) + #expect(submenu.items.last?.title == "Open in Window") + + controller.menuWillOpen(submenu) + #expect(store.codexLocalProjectUsageRefreshTask != nil) + store.codexLocalProjectUsageRefreshTask?.cancel() + } + + @Test + func `codex workspaces progress does not trigger broad menu invalidation path`() async throws { + self.disableMenuCardsForTesting() + let settings = self.makeSettings() + settings.statusChecksEnabled = false + settings.refreshFrequency = .manual + settings.mergeIcons = true + settings.selectedMenuProvider = .codex + settings.costUsageEnabled = true + settings.codexLocalProjectUsageEnabled = true + + let registry = ProviderRegistry.shared + for provider in UsageProvider.allCases { + guard let metadata = registry.metadata[provider] else { continue } + settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: provider == .codex) + } + + let fetcher = UsageFetcher() + let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) + let scope = store.tokenCostScope(for: .codex) + store.lastCodexLocalProjectUsageFetchAt = Date() + store.lastCodexLocalProjectUsageFetchScope = "\(scope.signature)|historyDays=\(settings.costUsageHistoryDays)" + store.codexLocalProjectUsageRefreshInFlight = true + store.codexLocalProjectUsageProgress = CodexLocalProjectUsageIndexProgress( + phase: .indexingProjects, + processedFileCount: 25, + totalFileCount: 503, + indexedFileCount: 25, + skippedFileCount: 0) + + let controller = StatusItemController( + store: store, + settings: settings, + account: fetcher.loadAccountInfo(), + updater: DisabledUpdaterController(), + preferencesSelection: PreferencesSelection(), + statusBar: self.makeStatusBarForTesting()) + defer { controller.releaseStatusItemsForTesting() } + + var broadMenuChangeCount = 0 + controller.onObservedStoreMenuChangeForTesting = { + broadMenuChangeCount += 1 + } + + let menu = NSMenu() + let didAdd = controller.addCodexLocalProjectUsageMenuItemIfNeeded(to: menu, provider: .codex, width: 420) + #expect(didAdd) + let item = try #require(menu.items.first { $0.title == "Workspaces" }) + + store.codexLocalProjectUsageProgress = CodexLocalProjectUsageIndexProgress( + phase: .indexingProjects, + processedFileCount: 50, + totalFileCount: 503, + indexedFileCount: 50, + skippedFileCount: 0) + await Task.yield() + await Task.yield() + + #expect(broadMenuChangeCount == 0) + #expect(self.codexLocalProjectUsageSubtitle(from: item) == "Indexing local Codex logs…") + #expect(store.codexLocalProjectUsageProgressSubtitle == "Indexing projects… 50/503 files, 453 left") + + store.codexLocalProjectUsageError = "Failed to read Codex logs" + await Task.yield() + await Task.yield() + + #expect(broadMenuChangeCount == 1) + + store.codexLocalProjectUsageRefreshInFlight = false + store.codexLocalProjectUsageProgress = nil + controller.updateCodexLocalProjectUsageRows() + #expect(self.codexLocalProjectUsageProgressItem(from: item) == nil) + #expect(self.codexLocalProjectUsageSubtitle(from: item) == "Failed to read Codex logs") + } + + private func codexLocalProjectUsageSubtitle(from item: NSMenuItem) -> String? { + if #available(macOS 14.4, *), let subtitle = item.subtitle, !subtitle.isEmpty { + return subtitle + } + let view = item.view ?? self.codexLocalProjectUsageProgressItem(from: item)?.view + guard let view else { return nil } + return self.textFields(in: view).map(\.stringValue).first { + $0.hasPrefix("Indexing") || $0.hasPrefix("Scanning") || $0.hasPrefix("Saving") || + $0.hasPrefix("Finalizing") + } + } + + private func codexLocalProjectUsageProgressItem(from item: NSMenuItem) -> NSMenuItem? { + guard let menu = item.menu, + let index = menu.items.firstIndex(where: { $0 === item }), + index + 1 < menu.items.count + else { return nil } + let candidate = menu.items[index + 1] + return (candidate.representedObject as? String) == "codexLocalProjectUsageProgress" ? candidate : nil + } + + private func progressIndicators(in view: NSView) -> [NSProgressIndicator] { + var indicators = view.subviews.flatMap(self.progressIndicators(in:)) + if let indicator = view as? NSProgressIndicator { + indicators.insert(indicator, at: 0) + } + return indicators + } + + private func textFields(in view: NSView) -> [NSTextField] { + var fields = view.subviews.flatMap(self.textFields(in:)) + if let field = view as? NSTextField { + fields.insert(field, at: 0) + } + return fields + } +} diff --git a/Tests/CodexBarTests/UsageStoreCodexLocalProjectUsageRequestTests.swift b/Tests/CodexBarTests/UsageStoreCodexLocalProjectUsageRequestTests.swift new file mode 100644 index 0000000000..64ed77c479 --- /dev/null +++ b/Tests/CodexBarTests/UsageStoreCodexLocalProjectUsageRequestTests.swift @@ -0,0 +1,489 @@ +import CodexBarCore +import Foundation +import Testing +@testable import CodexBar + +@MainActor +@Suite(.serialized) +struct UsageStoreCodexLocalProjectUsageRequestTests { + @Test + func `identical refresh triggers coalesce`() async { + let probe = CodexLocalProjectUsageRefreshExecutorProbe() + let settings = self.makeCodexOnlySettings() + settings.costUsageHistoryDays = 30 + let store = self.makeStore(settings: settings, probe: probe) + + store.scheduleCodexLocalProjectUsageRefreshIfNeeded(force: true) + #expect(await probe.waitUntilStarted(count: 1)) + let activeRequest = store.activeCodexLocalProjectUsageRequestKey + let activeGeneration = store.codexLocalProjectUsageRequestGeneration + #expect(activeRequest != nil) + #expect(store.pendingCodexLocalProjectUsageRequestKey == nil) + + store.scheduleCodexLocalProjectUsageRefreshIfNeeded(force: true) + #expect(store.activeCodexLocalProjectUsageRequestKey == activeRequest) + #expect(store.pendingCodexLocalProjectUsageRequestKey == nil) + #expect(store.codexLocalProjectUsageRequestGeneration == activeGeneration) + let calls = await probe.recordedCalls() + #expect(calls.count == 1) + + await probe.releaseNext() + await probe.cleanup() + } + + @Test + func `changed requests retain only the latest follow-up`() async { + let probe = CodexLocalProjectUsageRefreshExecutorProbe() + let settings = self.makeCodexOnlySettings() + settings.costUsageHistoryDays = 30 + let store = self.makeStore(settings: settings, probe: probe) + + store.scheduleCodexLocalProjectUsageRefreshIfNeeded() + #expect(await probe.waitUntilStarted(count: 1)) + let activeRequest = store.activeCodexLocalProjectUsageRequestKey + + settings.costUsageHistoryDays = 180 + store.scheduleCodexLocalProjectUsageRefreshIfNeeded() + #expect(store.activeCodexLocalProjectUsageRequestKey == activeRequest) + #expect(store.pendingCodexLocalProjectUsageRequestKey?.contains("historyDays=180") == true) + + settings.costUsageHistoryDays = 365 + store.scheduleCodexLocalProjectUsageRefreshIfNeeded() + #expect(store.pendingCodexLocalProjectUsageRequestKey?.contains("historyDays=365") == true) + + #expect(await probe.waitUntilStarted(count: 2)) + let calls = await probe.recordedCalls() + #expect(calls.count == 2) + #expect(store.activeCodexLocalProjectUsageRequestKey?.contains("historyDays=365") == true) + #expect(store.pendingCodexLocalProjectUsageRequestKey == nil) + + await probe.releaseNext() + await probe.cleanup() + } + + @Test + func `forced rebuild routes through the scheduler`() async { + let probe = CodexLocalProjectUsageRefreshExecutorProbe() + let settings = self.makeCodexOnlySettings() + let store = self.makeStore(settings: settings, probe: probe) + + store.scheduleCodexLocalProjectUsageRefreshIfNeeded() + #expect(await probe.waitUntilStarted(count: 1)) + store.lastCodexLocalProjectUsageFetchAt = Date() + store.lastCodexLocalProjectUsageFetchScope = "stale-scope" + store.lastCodexLocalProjectUsageFailureAt = Date() + store.codexLocalProjectUsageBackoffUntil = Date().addingTimeInterval(60) + store.codexLocalProjectUsageFailureCount = 3 + + store.rebuildCodexLocalProjectUsageIndex() + #expect(store.pendingForcedCodexLocalProjectUsageRefresh) + #expect(store.pendingCodexLocalProjectUsageRequestKey == store.activeCodexLocalProjectUsageRequestKey) + #expect(store.lastCodexLocalProjectUsageFetchAt == nil) + #expect(store.lastCodexLocalProjectUsageFetchScope == nil) + #expect(store.lastCodexLocalProjectUsageFailureAt == nil) + #expect(store.codexLocalProjectUsageBackoffUntil == nil) + #expect(store.codexLocalProjectUsageFailureCount == 0) + + #expect(await probe.waitUntilStarted(count: 2)) + let calls = await probe.recordedCalls() + #expect(calls.map(\.force) == [false, true]) + #expect(store.activeForcedCodexLocalProjectUsageRefresh) + #expect(store.pendingCodexLocalProjectUsageRequestKey == nil) + + await probe.releaseNext() + await probe.cleanup() + } + + @Test + func `enabling privacy clears raw projection before scheduling protected refresh`() async { + let probe = CodexLocalProjectUsageRefreshExecutorProbe() + let settings = self.makeCodexOnlySettings() + let store = self.makeStore(settings: settings, probe: probe) + store.scheduleCodexLocalProjectUsageRefreshIfNeeded() + #expect(await probe.waitUntilStarted(count: 1)) + let rawGeneration = store.codexLocalProjectUsageRequestGeneration + let snapshot = CodexLocalProjectUsageSnapshot( + updatedAt: Date(timeIntervalSince1970: 1_700_000_000), + historyDays: 30, + scopeSignature: "stable-scope", + rootsFingerprint: [:], + indexedFileCount: 1, + skippedFileCount: 0, + total: .empty, + projects: [], + daily: []) + store.codexLocalProjectUsageSnapshot = snapshot + store.codexLocalProjectUsageError = "raw projection error" + store.codexLocalProjectUsageProgress = CodexLocalProjectUsageIndexProgress(phase: .scanningLogs) + store.codexLocalProjectUsageRefreshInFlight = true + store.lastCodexLocalProjectUsageFetchAt = Date() + store.lastCodexLocalProjectUsageFetchScope = "raw-scope" + store.lastCodexLocalProjectUsageFailureAt = Date() + store.codexLocalProjectUsageBackoffUntil = Date().addingTimeInterval(60) + store.codexLocalProjectUsageFailureCount = 2 + + let previousHidePersonalInfo = settings.hidePersonalInfo + settings.hidePersonalInfo = true + store.refreshCodexLocalProjectUsageProjectionIfNeeded( + previousHidePersonalInfo: previousHidePersonalInfo) + + #expect(store.codexLocalProjectUsageSnapshot == nil) + #expect(store.codexLocalProjectUsageError == nil) + #expect(store.codexLocalProjectUsageProgress == nil) + #expect(!store.codexLocalProjectUsageRefreshInFlight) + #expect(store.codexLocalProjectUsageLoadState == .idle) + #expect(store.lastCodexLocalProjectUsageFetchAt == nil) + #expect(store.lastCodexLocalProjectUsageFetchScope == nil) + #expect(store.lastCodexLocalProjectUsageFailureAt == nil) + #expect(store.codexLocalProjectUsageBackoffUntil == nil) + #expect(store.codexLocalProjectUsageFailureCount == 0) + #expect(store.pendingCodexLocalProjectUsageRequestKey?.contains("hidePersonalInfo=true") == true) + #expect(store.codexLocalProjectUsageRequestGeneration > rawGeneration) + + #expect(await probe.waitUntilStarted(count: 2)) + let calls = await probe.recordedCalls() + #expect(calls.map(\.force) == [false, false]) + #expect(calls[1].generation > calls[0].generation) + #expect(store.activeCodexLocalProjectUsageRequestKey?.contains("hidePersonalInfo=true") == true) + + await probe.releaseNext() + await probe.cleanup() + } + + @Test + func `disabling privacy retains protected projection while scheduling normal refresh`() async { + let probe = CodexLocalProjectUsageRefreshExecutorProbe() + let settings = self.makeCodexOnlySettings() + settings.hidePersonalInfo = true + let store = self.makeStore(settings: settings, probe: probe) + let protectedSnapshot = CodexLocalProjectUsageSnapshot( + updatedAt: Date(timeIntervalSince1970: 1_700_000_000), + historyDays: 30, + scopeSignature: "protected-scope", + rootsFingerprint: [:], + indexedFileCount: 1, + skippedFileCount: 0, + total: .empty, + projects: [], + daily: []) + store.codexLocalProjectUsageSnapshot = protectedSnapshot + + settings.hidePersonalInfo = false + store.refreshCodexLocalProjectUsageProjectionIfNeeded(previousHidePersonalInfo: true) + + #expect(store.codexLocalProjectUsageSnapshot == protectedSnapshot) + #expect(store.activeCodexLocalProjectUsageRequestKey?.contains("hidePersonalInfo=false") == true) + + #expect(await probe.waitUntilStarted(count: 1)) + let calls = await probe.recordedCalls() + #expect(calls.map(\.force) == [false]) + + await probe.releaseNext() + await probe.cleanup() + } + + @Test + func `display preferences only update projections without scheduling a request`() async { + let probe = CodexLocalProjectUsageRefreshExecutorProbe() + let settings = self.makeCodexOnlySettings() + let store = self.makeStore(settings: settings, probe: probe) + let snapshot = CodexLocalProjectUsageSnapshot( + updatedAt: Date(timeIntervalSince1970: 1_700_000_000), + historyDays: 30, + scopeSignature: "stable-scope", + rootsFingerprint: [:], + indexedFileCount: 1, + skippedFileCount: 0, + total: .empty, + projects: [], + daily: []) + store.codexLocalProjectUsageSnapshot = snapshot + let requestGeneration = store.codexLocalProjectUsageRequestGeneration + + settings.codexLocalProjectUsageShowsEstimatedCost = false + settings.codexLocalProjectUsageIncludesCachedInput = false + store.refreshCodexLocalProjectUsageAvailabilityIfNeeded(previousEnabled: true) + store.refreshCodexLocalProjectUsageProjectionIfNeeded( + previousHidePersonalInfo: settings.hidePersonalInfo) + await Task.yield() + + #expect(!store.codexLocalProjectUsageProjection.showsEstimatedCost) + #expect(!store.codexLocalProjectUsageProjection.includesCachedInput) + #expect(store.codexLocalProjectUsageRequestGeneration == requestGeneration) + #expect(store.codexLocalProjectUsageSnapshot == snapshot) + let calls = await probe.recordedCalls() + #expect(calls.isEmpty) + await probe.cleanup() + } + + @Test + func `availability preference routes enablement through the scheduler`() async { + let probe = CodexLocalProjectUsageRefreshExecutorProbe() + let settings = self.makeCodexOnlySettings() + let store = self.makeStore(settings: settings, probe: probe) + let initialBackgroundRevision = settings.backgroundWorkSettingsRevision + + settings.codexLocalProjectUsageEnabled = false + #expect(settings.backgroundWorkSettingsRevision == initialBackgroundRevision + 1) + store.refreshCodexLocalProjectUsageAvailabilityIfNeeded(previousEnabled: true) + let disabledCalls = await probe.recordedCalls() + #expect(disabledCalls.isEmpty) + #expect(store.codexLocalProjectUsageRefreshTask == nil) + + settings.codexLocalProjectUsageEnabled = true + #expect(settings.backgroundWorkSettingsRevision == initialBackgroundRevision + 2) + store.refreshCodexLocalProjectUsageAvailabilityIfNeeded(previousEnabled: false) + #expect(await probe.waitUntilStarted(count: 1)) + let enabledCalls = await probe.recordedCalls() + #expect(enabledCalls.map(\.force) == [false]) + + await probe.releaseNext() + await probe.cleanup() + } + + @Test + func `disabling cancels and resets refresh scheduling`() async { + let probe = CodexLocalProjectUsageRefreshExecutorProbe() + let settings = self.makeCodexOnlySettings() + let store = self.makeStore(settings: settings, probe: probe) + + store.scheduleCodexLocalProjectUsageRefreshIfNeeded(force: true) + #expect(await probe.waitUntilStarted(count: 1)) + store.codexLocalProjectUsageError = "stale error" + store.codexLocalProjectUsageProgress = CodexLocalProjectUsageIndexProgress(phase: .scanningLogs) + store.codexLocalProjectUsageRefreshInFlight = true + store.lastCodexLocalProjectUsageFetchAt = Date() + store.lastCodexLocalProjectUsageFetchScope = "stale-scope" + store.lastCodexLocalProjectUsageFailureAt = Date() + store.codexLocalProjectUsageBackoffUntil = Date().addingTimeInterval(60) + store.codexLocalProjectUsageFailureCount = 2 + + settings.codexLocalProjectUsageEnabled = false + store.scheduleCodexLocalProjectUsageRefreshIfNeeded() + #expect(await probe.waitUntilCancelled(count: 1)) + #expect(store.codexLocalProjectUsageRefreshTask == nil) + #expect(!store.codexLocalProjectUsageRefreshInFlight) + #expect(store.codexLocalProjectUsageError == nil) + #expect(store.codexLocalProjectUsageProgress == nil) + #expect(store.lastCodexLocalProjectUsageFetchAt == nil) + #expect(store.lastCodexLocalProjectUsageFetchScope == nil) + #expect(store.lastCodexLocalProjectUsageFailureAt == nil) + #expect(store.codexLocalProjectUsageBackoffUntil == nil) + #expect(store.codexLocalProjectUsageFailureCount == 0) + #expect(!store.activeForcedCodexLocalProjectUsageRefresh) + #expect(!store.pendingForcedCodexLocalProjectUsageRefresh) + #expect(store.activeCodexLocalProjectUsageRequestKey == nil) + #expect(store.pendingCodexLocalProjectUsageRequestKey == nil) + #expect(store.activeCodexLocalProjectUsageRequestGeneration == nil) + #expect(store.codexLocalProjectUsageLoadState == .idle) + await probe.cleanup() + } + + @Test + func `scheduled refresh does not retain its store`() async { + let probe = CodexLocalProjectUsageRefreshExecutorProbe() + let settings = self.makeCodexOnlySettings() + var store: UsageStore? = self.makeStore(settings: settings, probe: probe) + weak let weakStore = store + + store?.scheduleCodexLocalProjectUsageRefreshIfNeeded(force: true) + #expect(await probe.waitUntilStarted(count: 1)) + + store = nil + + #expect(weakStore == nil) + #expect(await probe.waitUntilCancelled(count: 1)) + await probe.cleanup() + } + + private func makeStore( + settings: SettingsStore, + probe: CodexLocalProjectUsageRefreshExecutorProbe) -> UsageStore + { + let store = UsageStore( + fetcher: UsageFetcher(), + browserDetection: BrowserDetection(cacheTTL: 0), + costUsageFetcher: CostUsageFetcher(), + settings: settings, + startupBehavior: .testing, + environmentBase: [:]) + store._test_codexLocalProjectUsageRefreshExecutor = { force, generation in + await probe.run(force: force, generation: generation) + } + return store + } + + private func makeCodexOnlySettings() -> SettingsStore { + let suite = "UsageStoreCodexLocalProjectUsageRequestTests-\(UUID().uuidString)" + let defaults = UserDefaults(suiteName: suite)! + defaults.removePersistentDomain(forName: suite) + let settings = SettingsStore( + userDefaults: defaults, + configStore: testConfigStore(suiteName: suite), + zaiTokenStore: NoopZaiTokenStore(), + syntheticTokenStore: NoopSyntheticTokenStore()) + settings.refreshFrequency = .manual + settings.statusChecksEnabled = false + settings.costUsageEnabled = true + settings.codexLocalProjectUsageEnabled = true + settings.openAIWebAccessEnabled = false + settings.codexCookieSource = .off + settings.providerDetectionCompleted = true + + let registry = ProviderRegistry.shared + for provider in UsageProvider.allCases { + guard let metadata = registry.metadata[provider] else { continue } + settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: provider == .codex) + } + return settings + } +} + +private actor CodexLocalProjectUsageRefreshExecutorProbe { + struct Call: Sendable { + let force: Bool + let generation: UInt64 + } + + private struct Waiter { + let id: UUID + let count: Int + let continuation: CheckedContinuation + let timeoutTask: Task + } + + private var calls: [Call] = [] + private var blockedCalls: [(id: UUID, continuation: CheckedContinuation)] = [] + private var cancelledCallIDs: Set = [] + private var startWaiters: [Waiter] = [] + private var cancellationWaiters: [Waiter] = [] + + func run(force: Bool, generation: UInt64) async { + let id = UUID() + await withTaskCancellationHandler { + await withCheckedContinuation { (continuation: CheckedContinuation) in + self.calls.append(Call(force: force, generation: generation)) + if Task.isCancelled { + self.recordCancellation(id: id) + continuation.resume() + } else { + self.blockedCalls.append((id: id, continuation: continuation)) + } + self.resumeSatisfiedStartWaiters() + } + } onCancel: { + Task { await self.cancel(id: id) } + } + } + + func waitUntilStarted(count: Int, timeout: Duration = .seconds(5)) async -> Bool { + guard self.calls.count < count else { return true } + let id = UUID() + return await withCheckedContinuation { continuation in + let timeoutTask = Task { [weak self] in + do { + try await ContinuousClock().sleep(for: timeout) + } catch { + return + } + await self?.timeoutStartWaiter(id: id) + } + self.startWaiters.append(Waiter( + id: id, + count: count, + continuation: continuation, + timeoutTask: timeoutTask)) + } + } + + func waitUntilCancelled(count: Int, timeout: Duration = .seconds(5)) async -> Bool { + guard self.cancelledCallIDs.count < count else { return true } + let id = UUID() + return await withCheckedContinuation { continuation in + let timeoutTask = Task { [weak self] in + do { + try await ContinuousClock().sleep(for: timeout) + } catch { + return + } + await self?.timeoutCancellationWaiter(id: id) + } + self.cancellationWaiters.append(Waiter( + id: id, + count: count, + continuation: continuation, + timeoutTask: timeoutTask)) + } + } + + func releaseNext() { + guard !self.blockedCalls.isEmpty else { return } + self.blockedCalls.removeFirst().continuation.resume() + } + + func recordedCalls() -> [Call] { + self.calls + } + + func cleanup() { + let blockedCalls = self.blockedCalls + self.blockedCalls.removeAll() + for blockedCall in blockedCalls { + blockedCall.continuation.resume() + } + self.resumeAndRemoveAllWaiters(&self.startWaiters) + self.resumeAndRemoveAllWaiters(&self.cancellationWaiters) + } + + private func cancel(id: UUID) { + self.recordCancellation(id: id) + guard let index = self.blockedCalls.firstIndex(where: { $0.id == id }) else { return } + self.blockedCalls.remove(at: index).continuation.resume() + } + + private func recordCancellation(id: UUID) { + guard self.cancelledCallIDs.insert(id).inserted else { return } + var remaining: [Waiter] = [] + for waiter in self.cancellationWaiters { + if self.cancelledCallIDs.count >= waiter.count { + waiter.timeoutTask.cancel() + waiter.continuation.resume(returning: true) + } else { + remaining.append(waiter) + } + } + self.cancellationWaiters = remaining + } + + private func resumeSatisfiedStartWaiters() { + var remaining: [Waiter] = [] + for waiter in self.startWaiters { + if self.calls.count >= waiter.count { + waiter.timeoutTask.cancel() + waiter.continuation.resume(returning: true) + } else { + remaining.append(waiter) + } + } + self.startWaiters = remaining + } + + private func timeoutStartWaiter(id: UUID) { + guard let index = self.startWaiters.firstIndex(where: { $0.id == id }) else { return } + self.startWaiters.remove(at: index).continuation.resume(returning: false) + } + + private func timeoutCancellationWaiter(id: UUID) { + guard let index = self.cancellationWaiters.firstIndex(where: { $0.id == id }) else { return } + self.cancellationWaiters.remove(at: index).continuation.resume(returning: false) + } + + private func resumeAndRemoveAllWaiters(_ waiters: inout [Waiter]) { + let outstandingWaiters = waiters + waiters.removeAll() + for waiter in outstandingWaiters { + waiter.timeoutTask.cancel() + waiter.continuation.resume(returning: false) + } + } +}