codeburn/mac/Sources/CodeBurnMenubar/Views/ToolingSection.swift
ozymandiashh d62857ea4e feat(menubar): route every user-facing string through a strings catalog
Groundwork for #1219. The menubar app hardcoded ~530 English strings across
Views/, the presentation structs, the status-item menu and the update alerts,
so there was nothing for a translator to translate.

Every user-facing literal now goes through `L(_:)` / `L(_:_:)`, which look the
string up in `Localizable.strings` in the SwiftPM target resource bundle. The
key *is* the English copy, so English stays the development language and a
missing translation degrades to correct English instead of a dotted
identifier. `en.lproj` is therefore an identity table; it exists so the bundle
advertises `en` and so a translation can be diffed against it.

Why the lookups are explicit rather than relying on SwiftUI's implicit
`LocalizedStringKey`: SwiftPM emits target resources into a sibling bundle
(`CodeBurnMenubar_CodeBurnMenubar.bundle`) that the packaging scripts copy into
`Contents/Resources`. `Bundle.main` has no `.lproj` at all, so `Text("literal")`
would always miss. Naming `Bundle.module` is the one form that resolves the
same way in `swift run`, in `swift test` and in the packaged `.app`.

Enum raw values that double as identity (`Period`, `MenubarScope`,
`InsightMode`, `AccentPreset`, `ProviderFilter`) keep their raw value and gain
a `displayLabel`, so persistence and cache keys are untouched by translation.

Three display-only date formatters that were pinned to `en_US_POSIX` with fixed
patterns now use `setLocalizedDateFormatFromTemplate`, and the calendar
popover's weekday row comes from the locale's own symbols. The `yyyy-MM-dd`
formatter stays POSIX: it parses and builds data keys, not display text.

Not extracted, deliberately: provider, model and plan names; units; currency
codes; shell commands and paths; quota window labels that policy code matches
on by English substring (`QuotaSummary.headlineWindow`); and anything the
`codeburn` CLI produces.

Also copies the SwiftPM resource bundle into the app in build-local.sh, using
the named-bundle + `[[ -d ]]` pattern package-app.sh already uses. Without it
the assembled app finds no strings table (and already trapped on first icon
load, the resource-bundle half of #1262).
2026-09-12 15:21:49 -07:00

137 lines
4.7 KiB
Swift

import SwiftUI
struct ToolingSection: View {
@Environment(AppStore.self) private var store
@State private var isExpanded: Bool = false
private var skillsAndAgents: [CostRowData] {
let current = store.payload.current
var merged: [String: (uses: Int, cost: Double)] = [:]
for s in current.skills {
let e = merged[s.name, default: (0, 0)]
merged[s.name] = (e.uses + s.turns, e.cost + s.cost)
}
for a in current.subagents {
let e = merged[a.name, default: (0, 0)]
merged[a.name] = (e.uses + a.calls, e.cost + a.cost)
}
return merged
.map { CostRowData(name: $0.key, uses: $0.value.uses, cost: $0.value.cost) }
.sorted { $0.cost > $1.cost }
}
var body: some View {
let current = store.payload.current
let combined = skillsAndAgents
let hasAny = !current.tools.isEmpty || !combined.isEmpty || !current.mcpServers.isEmpty
if hasAny {
CollapsibleSection(caption: L("Tooling"), isExpanded: $isExpanded) {
VStack(alignment: .leading, spacing: 12) {
if !current.tools.isEmpty {
ToolingSubsection(title: L("Tools")) {
let maxCalls = current.tools.map(\.calls).max() ?? 1
ForEach(current.tools, id: \.name) { t in
CallsRow(name: t.name, calls: t.calls, maxCalls: maxCalls)
}
}
}
if !combined.isEmpty {
ToolingSubsection(title: L("Skills & Agents")) {
let maxCost = max(combined.map(\.cost).max() ?? 0.01, 0.01)
ForEach(combined, id: \.name) { d in
CostRow(name: d.name, cost: d.cost, count: d.uses, countLabel: "uses", maxCost: maxCost)
}
}
}
if !current.mcpServers.isEmpty {
ToolingSubsection(title: L("MCP Servers")) {
let maxCalls = current.mcpServers.map(\.calls).max() ?? 1
ForEach(current.mcpServers, id: \.name) { m in
CallsRow(name: m.name, calls: m.calls, maxCalls: maxCalls)
}
}
}
}
}
}
}
}
private struct CostRowData {
let name: String
let uses: Int
let cost: Double
}
private struct ToolingSubsection<Content: View>: View {
let title: String
let content: Content
init(title: String, @ViewBuilder content: () -> Content) {
self.title = title
self.content = content()
}
var body: some View {
VStack(alignment: .leading, spacing: 5) {
Text(title)
.font(.system(size: 10.5, weight: .semibold))
.foregroundStyle(.tertiary)
.textCase(.uppercase)
.tracking(0.5)
content
}
}
}
private struct CallsRow: View {
let name: String
let calls: Int
let maxCalls: Int
var body: some View {
HStack(spacing: 8) {
FixedBar(fraction: Double(calls) / Double(max(maxCalls, 1)))
.frame(width: 40, height: 5)
Text(name)
.font(.system(size: 12, weight: .medium))
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
Text("\(calls)")
.font(.system(size: 11))
.monospacedDigit()
.foregroundStyle(.secondary)
.frame(minWidth: 36, alignment: .trailing)
}
.padding(.vertical, 1)
}
}
private struct CostRow: View {
let name: String
let cost: Double
let count: Int
let countLabel: String
let maxCost: Double
var body: some View {
HStack(spacing: 8) {
FixedBar(fraction: cost / max(maxCost, 0.01))
.frame(width: 40, height: 5)
Text(name)
.font(.system(size: 12, weight: .medium))
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
Text("\(count)")
.font(.system(size: 11))
.monospacedDigit()
.foregroundStyle(.secondary)
.frame(minWidth: 30, alignment: .trailing)
Text(cost.asCompactCurrency())
.font(.codeMono(size: 11, weight: .medium))
.tracking(-0.2)
.frame(minWidth: 46, alignment: .trailing)
}
.padding(.vertical, 1)
}
}