codeburn/mac/Sources/CodeBurnMenubar/Theme/ThemeState.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

103 lines
4.3 KiB
Swift

import SwiftUI
import Observation
enum AccentPreset: String, CaseIterable, Identifiable {
case ember = "Ember"
case blue = "Blue"
case purple = "Purple"
case pink = "Pink"
case red = "Red"
case orange = "Orange"
case yellow = "Yellow"
case green = "Green"
case graphite = "Graphite"
var id: String { rawValue }
/// Accessibility / picker label. `rawValue` stays the persisted identity.
var displayLabel: String {
switch self {
case .ember: L("Ember")
case .blue: L("Blue")
case .purple: L("Purple")
case .pink: L("Pink")
case .red: L("Red")
case .orange: L("Orange")
case .yellow: L("Yellow")
case .green: L("Green")
case .graphite: L("Graphite")
}
}
/// Apple macOS dark-mode system accent colors (NSColor.system*).
var base: Color {
switch self {
case .ember: Color(red: 0xC9/255, green: 0x52/255, blue: 0x1D/255)
case .blue: Color(red: 0x0A/255, green: 0x84/255, blue: 0xFF/255)
case .purple: Color(red: 0xBF/255, green: 0x5A/255, blue: 0xF2/255)
case .pink: Color(red: 0xFF/255, green: 0x37/255, blue: 0x5F/255)
case .red: Color(red: 0xFF/255, green: 0x45/255, blue: 0x3A/255)
case .orange: Color(red: 0xFF/255, green: 0x9F/255, blue: 0x0A/255)
case .yellow: Color(red: 0xFF/255, green: 0xD6/255, blue: 0x0A/255)
case .green: Color(red: 0x30/255, green: 0xD1/255, blue: 0x58/255)
case .graphite: Color(red: 0x98/255, green: 0x98/255, blue: 0x9D/255)
}
}
var light: Color {
switch self {
case .ember: Color(red: 0xE8/255, green: 0x77/255, blue: 0x4A/255)
case .blue: Color(red: 0x40/255, green: 0x9C/255, blue: 0xFF/255)
case .purple: Color(red: 0xDA/255, green: 0x8F/255, blue: 0xF7/255)
case .pink: Color(red: 0xFF/255, green: 0x6E/255, blue: 0x8C/255)
case .red: Color(red: 0xFF/255, green: 0x6E/255, blue: 0x63/255)
case .orange: Color(red: 0xFF/255, green: 0xBD/255, blue: 0x4A/255)
case .yellow: Color(red: 0xFF/255, green: 0xE0/255, blue: 0x4A/255)
case .green: Color(red: 0x5A/255, green: 0xE0/255, blue: 0x78/255)
case .graphite: Color(red: 0xAE/255, green: 0xAE/255, blue: 0xB2/255)
}
}
var deep: Color {
switch self {
case .ember: Color(red: 0x8B/255, green: 0x3E/255, blue: 0x13/255)
case .blue: Color(red: 0x06/255, green: 0x52/255, blue: 0xB3/255)
case .purple: Color(red: 0x7C/255, green: 0x38/255, blue: 0xA8/255)
case .pink: Color(red: 0xB3/255, green: 0x26/255, blue: 0x42/255)
case .red: Color(red: 0xB3/255, green: 0x30/255, blue: 0x28/255)
case .orange: Color(red: 0xB3/255, green: 0x6F/255, blue: 0x06/255)
case .yellow: Color(red: 0xB3/255, green: 0x96/255, blue: 0x06/255)
case .green: Color(red: 0x20/255, green: 0x92/255, blue: 0x3D/255)
case .graphite: Color(red: 0x5E/255, green: 0x5E/255, blue: 0x62/255)
}
}
var glow: Color {
switch self {
case .ember: Color(red: 0xF0/255, green: 0xA0/255, blue: 0x70/255)
case .blue: Color(red: 0x80/255, green: 0xC0/255, blue: 0xFF/255)
case .purple: Color(red: 0xE0/255, green: 0xB8/255, blue: 0xFA/255)
case .pink: Color(red: 0xFF/255, green: 0x99/255, blue: 0xB0/255)
case .red: Color(red: 0xFF/255, green: 0x99/255, blue: 0x90/255)
case .orange: Color(red: 0xFF/255, green: 0xD0/255, blue: 0x80/255)
case .yellow: Color(red: 0xFF/255, green: 0xEA/255, blue: 0x80/255)
case .green: Color(red: 0x80/255, green: 0xF0/255, blue: 0x98/255)
case .graphite: Color(red: 0xC8/255, green: 0xC8/255, blue: 0xCC/255)
}
}
}
@MainActor
@Observable
final class ThemeState {
static let shared = ThemeState()
var preset: AccentPreset {
didSet { UserDefaults.standard.set(preset.rawValue, forKey: "CodeBurnAccentPreset") }
}
private init() {
let saved = UserDefaults.standard.string(forKey: "CodeBurnAccentPreset") ?? ""
self.preset = AccentPreset(rawValue: saved) ?? .ember
}
}