Files
kangkang/康康/App/Localization.swift
link2026 dad9d43486 ```
feat: 添加自定义提醒功能并优化项目配置

- 添加 CustomReminder 模型支持自由文案周期性提醒功能
- 实现自定义提醒的 UI 界面,包括新建、编辑和列表展示
- 集成本地通知服务支持自定义提醒的时间触发
- 更新项目配置文件添加应用显示名称和加密声明
- 修正 iOS 部署目标版本从 26.0 到 17.0
- 修复 FileDownloader 中的线程安全问题
- 优化 ModelManifest 和 Localization 的并发安全性
- 扩展本地化字符串支持多语言提醒相关文本
- 调整项目支持平台范围仅保留 iphoneos 和 iphonesimulator
```
2026-05-30 11:36:29 +08:00

136 lines
5.3 KiB
Swift

import SwiftUI
import ObjectiveC
/// App `system` = ; .lproj / String Catalog
enum AppLanguage: String, CaseIterable, Identifiable {
case system
case zhHans = "zh-Hans"
case en
case ja
case ko
var id: String { rawValue }
/// ****(,), App
var displayName: String {
switch self {
case .system: return String(appLoc: "跟随系统")
case .zhHans: return "简体中文"
case .en: return "English"
case .ja: return "日本語"
case .ko: return "한국어"
}
}
/// nil = ; .lproj / Locale
var localeIdentifier: String? {
self == .system ? nil : rawValue
}
}
/// App : lproj bundle locale
/// - `Text("")` `\.locale`(+ Bundle );
/// - `String(appLoc:)` bundle/locale, `.current` ,
/// `.id(current)` ,
@Observable
final class LanguageManager {
static let shared = LanguageManager()
private let storageKey = "appLanguage"
private(set) var current: AppLanguage
/// .lproj bundle(system .main),
private(set) var lprojBundle: Bundle = .main
/// locale(system .autoupdatingCurrent)
private(set) var resolvedLocale: Locale = .autoupdatingCurrent
/// SwiftUI 使(/ + Text )
var locale: Locale { resolvedLocale }
private init() {
let saved = UserDefaults.standard.string(forKey: storageKey)
current = AppLanguage(rawValue: saved ?? "") ?? .system
apply()
}
func set(_ language: AppLanguage) {
guard language != current else { return }
current = language
UserDefaults.standard.set(language.rawValue, forKey: storageKey)
// AppleLanguages:, App
if let id = language.localeIdentifier {
UserDefaults.standard.set([id], forKey: "AppleLanguages")
} else {
UserDefaults.standard.removeObject(forKey: "AppleLanguages")
}
apply()
}
private func apply() {
if let id = current.localeIdentifier {
resolvedLocale = Locale(identifier: id)
if let path = Bundle.main.path(forResource: id, ofType: "lproj"),
let b = Bundle(path: path) {
lprojBundle = b
} else {
lprojBundle = .main
}
} else {
resolvedLocale = .autoupdatingCurrent
lprojBundle = .main
}
Bundle.redirectMain(to: current.localeIdentifier)
// nonisolated , String(appLoc:) MainActor
appLocBundle = lprojBundle
appLocLocale = resolvedLocale
}
}
/// nonisolated :`String(appLoc:)` MainActor
/// (LocalizedError.errorDescriptionnonisolated labelstatic )
/// `LanguageManager.apply()`(MainActor),;,
nonisolated(unsafe) private var appLocBundle: Bundle = .main
nonisolated(unsafe) private var appLocLocale: Locale = .autoupdatingCurrent
extension String {
/// · ()
/// `String(localized:)`, bundle + locale,
/// `Locale.current`(/)
nonisolated init(appLoc key: String.LocalizationValue) {
self = String(localized: key, bundle: appLocBundle, locale: appLocLocale)
}
}
// MARK: - Bundle ( Text / NSLocalizedString )
/// key(,)
private var redirectBundleKey: UInt8 = 0
/// `Bundle.main` .lproj
private final class LocalizedMainBundle: Bundle, @unchecked Sendable {
override func localizedString(forKey key: String, value: String?, table tableName: String?) -> String {
if let target = objc_getAssociatedObject(self, &redirectBundleKey) as? Bundle {
return target.localizedString(forKey: key, value: value, table: tableName)
}
return super.localizedString(forKey: key, value: value, table: tableName)
}
}
extension Bundle {
/// language == nil ()
static func redirectMain(to language: String?) {
if !(Bundle.main is LocalizedMainBundle) {
object_setClass(Bundle.main, LocalizedMainBundle.self)
}
let target: Bundle?
if let language,
let path = Bundle.main.path(forResource: language, ofType: "lproj"),
let bundle = Bundle(path: path) {
target = bundle
} else {
target = nil
}
objc_setAssociatedObject(Bundle.main, &redirectBundleKey, target, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}