Files
hapi/ios/Packages/HapiKit/Sources/HapiClient/Settings/SettingsPrefs.swift
T
d3d8bf2620 feat(ios): add Russian locale (#1964)
* feat(ios): add Russian locale

Add Russian translations to Localizable.xcstrings for all 591 catalog
entries (reusing matching Android translations and translating the rest);
zh-Hans and format placeholders are preserved.

* fix(ios): make Russian selectable and use correct plural forms

- add AppLanguage.russian ("ru"), its display name, the AppleLanguages
  override, and register ru in the Xcode project's knownRegions so the
  language picker can actually select Russian
- add Russian one/few/many/other plural variations for %lld sessions,
  %lld attachments and %@, %lld sessions
- use count-agnostic phrasing for the other dynamic counts (tools, edits,
  lines, chars, findings, notes, queued messages, questions, ...)
- correct Input/Output to Ввод/Вывод
- cover the new language case in ThemePrefsTests

* fix(ios): use Russian plural forms for the remaining count labels

* fix(ios): pass the step count to the plural localization

* fix(ios): pass tool counts to the plural localization

* fix(ios): keep the singular English tool-group labels

---------

Co-authored-by: mrudy <mrudy@local>
2026-10-05 17:12:57 +08:00

114 lines
3.8 KiB
Swift

import Foundation
import Observation
/// Theme choice (web `ThemeMode` twin; Android
/// `feature/settings/ThemePrefs.kt`). ``oled`` is the pure-black variant — it
/// implies dark and maps to the HapiUI `.oled` palette at the app's root.
public enum ThemeMode: String, CaseIterable, Sendable {
case system
case light
case dark
case oled
/// Unknown/corrupt stored values degrade to ``system``.
public init(storageKey: String?) {
self = storageKey.flatMap(ThemeMode.init(rawValue:)) ?? .system
}
public var storageKey: String { rawValue }
/// The palette this mode resolves to: ``system`` follows the OS
/// appearance, explicit modes override it.
public func resolvedAppearance(systemIsDark: Bool) -> ResolvedAppearance {
switch self {
case .system: return systemIsDark ? .dark : .light
case .light: return .light
case .dark: return .dark
case .oled: return .oled
}
}
}
/// The appearance a ``ThemeMode`` resolves to for a given system scheme.
/// The app maps this to the HapiUI palette (`.light` / `.dark` / `.oled`)
/// and to `preferredColorScheme`; kept SwiftUI-free here so it is testable.
public enum ResolvedAppearance: Equatable, Sendable {
case light
case dark
case oled
}
/// App language choice (web `Locale` twin plus a follow-system default).
///
/// A-M5 wires the selection to an `AppleLanguages` override at the app layer
/// (explicit picks pin the app locale, ``system`` removes the override);
/// this type only persists the choice.
public enum AppLanguage: String, CaseIterable, Sendable {
case system = "system"
case english = "en"
case simplifiedChinese = "zh-Hans"
case russian = "ru"
/// Unknown/corrupt/absent stored values degrade to ``system``.
public init(storageKey: String?) {
self = storageKey.flatMap(AppLanguage.init(rawValue:)) ?? .system
}
public var storageKey: String { rawValue }
/// Explicit language names are shown in their own language (standard
/// picker convention). ``system``'s label is UI copy — the app layer
/// localizes it at the display point (this package stays language-free).
public var displayName: String {
switch self {
case .system: return "Follow system"
case .english: return "English"
case .simplifiedChinese: return "简体中文"
case .russian: return "Русский"
}
}
}
/// Appearance persistence over `UserDefaults` (app-wide, like the Android
/// `hapi_prefs` DataStore). Read at the app root to drive the HapiUI palette,
/// written by the settings screen; unreadable values degrade to defaults.
@MainActor @Observable
public final class ThemePrefs {
public private(set) var mode: ThemeMode
// `let` storage is inert under @Observable (no annotation needed).
private let defaults: UserDefaults
public static let modeKey = "settings.themeMode"
public init(defaults: UserDefaults = .standard) {
self.defaults = defaults
self.mode = ThemeMode(storageKey: defaults.string(forKey: Self.modeKey))
}
public func setMode(_ mode: ThemeMode) {
self.mode = mode
defaults.set(mode.storageKey, forKey: Self.modeKey)
}
}
/// Language persistence over `UserDefaults` (persist-only until M5).
@MainActor @Observable
public final class LanguagePrefs {
public private(set) var language: AppLanguage
private let defaults: UserDefaults
public static let languageKey = "settings.appLanguage"
public init(defaults: UserDefaults = .standard) {
self.defaults = defaults
self.language = AppLanguage(storageKey: defaults.string(forKey: Self.languageKey))
}
public func setLanguage(_ language: AppLanguage) {
self.language = language
defaults.set(language.storageKey, forKey: Self.languageKey)
}
}