db65db6232
Android UI Tests / ui-tests (push) Has been cancelled
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>
175 lines
5.6 KiB
Swift
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
|