Files
honeyDueKMP/iosApp/iosApp/Helpers/ThemeManager.swift
T
Trey T db65db6232
Android UI Tests / ui-tests (push) Has been cancelled
i18n: complete app-wide localization (10 languages) + audit tooling
Localize all user-facing strings across iOS (SwiftUI), shared Kotlin, and
Android Compose into en/es/fr/de/pt/it/ja/ko/nl/zh:
- iOS String Catalogs: main + widget Localizable.xcstrings, InfoPlist.xcstrings
  (permissions), plural variations, ~200 new keys translated
- Shared Kotlin ClientStrings table + Android composeResources/values-* (884 keys
  ×10), routed Api/ViewModel/util error & UI strings through localization
- Backend-localized lookups/suggestions consumed via display names
- Widget extension catalog; theme names, home-profile fallbacks, validation,
  network errors, accessibility labels all localized

Add re-runnable verification gates:
- scripts/i18n_audit.py  — enumerate every literal, partition to GAP=0
- scripts/i18n_coverage.py — all 10 locales translated, format-specifier parity

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 20:52:28 -05:00

175 lines
5.6 KiB
Swift

import SwiftUI
#if !WIDGET_EXTENSION
import Combine
#endif
// MARK: - Theme ID Enum
enum ThemeID: String, CaseIterable, Codable {
// i18n-ignore-begin: rawValues are stable persistence keys + asset-catalog name roots (non-UI); localized labels are in displayName below
case bright = "Default"
case teal = "Teal"
case ocean = "Ocean"
case forest = "Forest"
case sunset = "Sunset"
case monochrome = "Monochrome"
case lavender = "Lavender"
case crimson = "Crimson"
case midnight = "Midnight"
case desert = "Desert"
case mint = "Mint"
// i18n-ignore-end
var displayName: String {
switch self {
case .bright:
return String(localized: "Default")
case .teal:
return String(localized: "Teal")
case .ocean:
return String(localized: "Ocean")
case .forest:
return String(localized: "Forest")
case .sunset:
return String(localized: "Sunset")
case .monochrome:
return String(localized: "Monochrome")
case .lavender:
return String(localized: "Lavender")
case .crimson:
return String(localized: "Crimson")
case .midnight:
return String(localized: "Midnight")
case .desert:
return String(localized: "Desert")
case .mint:
return String(localized: "Mint")
}
}
var description: String {
switch self {
case .bright:
return String(localized: "Vibrant iOS system colors")
case .teal:
return String(localized: "Blue-green with warm accents")
case .ocean:
return String(localized: "Deep blues and coral tones")
case .forest:
return String(localized: "Earth greens and golden hues")
case .sunset:
return String(localized: "Warm oranges and reds")
case .monochrome:
return String(localized: "Elegant grayscale")
case .lavender:
return String(localized: "Soft purple with pink accents")
case .crimson:
return String(localized: "Bold red with warm highlights")
case .midnight:
return String(localized: "Deep navy with sky blue")
case .desert:
return String(localized: "Warm terracotta and sand tones")
case .mint:
return String(localized: "Fresh green with turquoise")
}
}
// Preview colors for theme selection
var previewColors: [Color] {
let theme = self.rawValue
return [
Color("\(theme)/Primary", bundle: nil),
Color("\(theme)/Secondary", bundle: nil),
Color("\(theme)/Accent", bundle: nil)
]
}
}
// MARK: - Shared App Group UserDefaults
private let appGroupID: String = {
Bundle.main.infoDictionary?["AppGroupIdentifier"] as? String ?? "group.com.myhoneydue.honeyDue.dev"
}()
private let sharedDefaults: UserDefaults = {
guard let defaults = UserDefaults(suiteName: appGroupID) else {
#if DEBUG
assertionFailure("App Group '\(appGroupID)' not configured — theme won't sync to widgets") // i18n-ignore: DEBUG assertion message (non-UI)
#endif
return UserDefaults.standard
}
return defaults
}()
// MARK: - Theme Manager
#if WIDGET_EXTENSION
// Simplified ThemeManager for widget extensions (no ObservableObject needed)
class ThemeManager {
static let shared = ThemeManager()
var currentTheme: ThemeID {
// Load saved theme from shared App Group defaults
if let savedThemeRawValue = sharedDefaults.string(forKey: themeKey),
let savedTheme = ThemeID(rawValue: savedThemeRawValue) {
return savedTheme
}
return .bright
}
var honeycombEnabled: Bool {
sharedDefaults.bool(forKey: honeycombKey)
}
private let themeKey = "selectedTheme" // i18n-ignore: UserDefaults key (non-UI)
private let honeycombKey = "honeycombEnabled" // i18n-ignore: UserDefaults key (non-UI)
private init() {}
}
#else
// Full ThemeManager for main app with ObservableObject support
@MainActor
class ThemeManager: ObservableObject {
static let shared = ThemeManager()
@Published var currentTheme: ThemeID {
didSet {
saveTheme()
}
}
@Published var honeycombEnabled: Bool {
didSet {
sharedDefaults.set(honeycombEnabled, forKey: honeycombKey)
}
}
private let themeKey = "selectedTheme" // i18n-ignore: UserDefaults key (non-UI)
private let honeycombKey = "honeycombEnabled" // i18n-ignore: UserDefaults key (non-UI)
private init() {
// Load saved theme from shared App Group defaults
if let savedThemeRawValue = sharedDefaults.string(forKey: themeKey),
let savedTheme = ThemeID(rawValue: savedThemeRawValue) {
self.currentTheme = savedTheme
} else {
self.currentTheme = .bright
}
self.honeycombEnabled = sharedDefaults.bool(forKey: honeycombKey)
}
private func saveTheme() {
// Save to shared App Group defaults so widgets can access it
sharedDefaults.set(currentTheme.rawValue, forKey: themeKey)
// Update reactive source so all views using Color.appPrimary etc. re-render
AppThemeSource.shared.themeName = currentTheme.rawValue
}
func setTheme(_ theme: ThemeID) {
currentTheme = theme
}
func setHoneycomb(_ enabled: Bool) {
honeycombEnabled = enabled
// Update reactive source so honeycomb overlays re-render
AppThemeSource.shared.honeycombEnabled = enabled
}
}
#endif