mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-23 23:29:35 +00:00
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).
87 lines
2.8 KiB
Swift
87 lines
2.8 KiB
Swift
import SwiftUI
|
|
|
|
struct ActivitySection: View {
|
|
@Environment(AppStore.self) private var store
|
|
@State private var isExpanded: Bool = true
|
|
|
|
var body: some View {
|
|
CollapsibleSection(
|
|
caption: L("Activity"),
|
|
isExpanded: $isExpanded,
|
|
trailing: {
|
|
HStack(spacing: 8) {
|
|
Text(L("Cost")).frame(minWidth: 54, alignment: .trailing)
|
|
Text(L("Turns")).frame(minWidth: 52, alignment: .trailing)
|
|
Text(L("1-shot")).frame(minWidth: 44, alignment: .trailing)
|
|
}
|
|
.font(.system(size: 10, weight: .medium))
|
|
.foregroundStyle(.tertiary)
|
|
.tracking(-0.05)
|
|
}
|
|
) {
|
|
VStack(alignment: .leading, spacing: 7) {
|
|
let maxCost = max(store.payload.current.topActivities.map(\.cost).max() ?? 1, 0.01)
|
|
ForEach(store.payload.current.topActivities, id: \.name) { activity in
|
|
ActivityRow(activity: activity, maxCost: maxCost)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ActivityRow: View {
|
|
let activity: ActivityEntry
|
|
let maxCost: Double
|
|
|
|
var body: some View {
|
|
HStack(spacing: 8) {
|
|
FixedBar(fraction: activity.cost / maxCost)
|
|
.frame(width: 56, height: 6)
|
|
|
|
Text(activity.name)
|
|
.font(.system(size: 12.5, weight: .medium))
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
Text(activity.cost.asCompactCurrency())
|
|
.font(.codeMono(size: 12, weight: .medium))
|
|
.tracking(-0.2)
|
|
.frame(minWidth: 54, alignment: .trailing)
|
|
|
|
Text("\(activity.turns)")
|
|
.font(.system(size: 11))
|
|
.monospacedDigit()
|
|
.foregroundStyle(.secondary)
|
|
.frame(minWidth: 52, alignment: .trailing)
|
|
|
|
Text(oneShotText)
|
|
.font(.system(size: 10.5))
|
|
.monospacedDigit()
|
|
.foregroundStyle(.secondary)
|
|
.frame(minWidth: 44, alignment: .trailing)
|
|
}
|
|
.padding(.horizontal, 2)
|
|
.padding(.vertical, 1)
|
|
}
|
|
|
|
private var oneShotText: String {
|
|
guard let rate = activity.oneShotRate else { return "—" }
|
|
return "\(Int(rate * 100))%"
|
|
}
|
|
}
|
|
|
|
/// Fixed-width horizontal bar that shows a fill fraction.
|
|
struct FixedBar: View {
|
|
let fraction: Double
|
|
|
|
var body: some View {
|
|
GeometryReader { geo in
|
|
ZStack(alignment: .leading) {
|
|
RoundedRectangle(cornerRadius: 2)
|
|
.fill(.secondary.opacity(0.15))
|
|
RoundedRectangle(cornerRadius: 2)
|
|
.fill(Theme.brandAccent)
|
|
.frame(width: max(0, min(geo.size.width, geo.size.width * CGFloat(fraction))))
|
|
}
|
|
}
|
|
}
|
|
}
|