主体:多语言支持(简体中文源 + 英/日/韩)
- 基础设施:Localizable.xcstrings(String Catalog,sourceLanguage=zh-Hans)
+ pbxproj developmentRegion/knownRegions 注册 en/ja/ko
- 全部硬编码 Locale("zh_CN") → Locale.current;中文 dateFormat → Date.FormatStyle(跟随系统)
- UI 中文字面量统一为 String(appLoc:)(显式绑定所选语言 bundle+locale,即时切换)
Text 字面量走环境 \.locale + Bundle 重定向
- 549 个 catalog key 全部 en/ja/ko 翻译完成(0 未翻译)
- App 内语言切换:我的 → 语言(LanguageManager + 即时生效,无需重启)
- 双用预设(症状/监测指标/慢病)本地化:static→computed 避免缓存
注:本提交为 WIP,一并打包了并行进行的功能模块
(HealthExport 健康导出、Security/Face ID 锁、DiaryAssist 日记 AI 辅助)
及 App 图标、CLAUDE.md、docs/scripts。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
128 lines
4.8 KiB
Swift
128 lines
4.8 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)
|
|
}
|
|
}
|
|
|
|
extension String {
|
|
/// 尊重「我的 · 语言」选择的本地化(可即时切换)。
|
|
/// 等价 `String(localized:)`,但显式绑定当前所选语言的 bundle + locale,
|
|
/// 因此不受 `Locale.current`(系统/启动时语言)限制。
|
|
init(appLoc key: String.LocalizationValue) {
|
|
let m = LanguageManager.shared
|
|
self = String(localized: key, bundle: m.lprojBundle, locale: m.resolvedLocale)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|