import Foundation // i18n:读 bundle 内的 .json(由 web 单一真源经 web/scripts/emit-i18n.mjs 产出,强约束②, // 见 ios/PLAN.md §5)。跟随系统语言、回退 zh-CN。各 JSON 已含完整回退(缺失键在产出时已并入 // zh-CN 值),故 loader 只需选对 locale。Shared 文件——主 app / Share Extension / 控件扩展共用; // 各 target 各读自己 bundle 的 i18n JSON(resourceURL 的无子目录回退兼容扩展打平的资源布局)。 enum I18n { private static let dict: [String: String] = load() static func t(_ key: String) -> String { return dict[key] ?? key } // 插值变体,与 web t(key, vars) 同形:占位符 {{name}}。catalog 里带占位的键(如 // settings.lastSeen 的 {{time}})须经此填充,否则字面量 {{...}} 会漏到 UI。 static func t(_ key: String, _ args: [String: String]) -> String { var s = dict[key] ?? key for (name, value) in args { s = s.replacingOccurrences(of: "{{\(name)}}", with: value) } return s } private static func load() -> [String: String] { guard let url = resourceURL(preferredLocale()) ?? resourceURL("zh-CN"), let data = try? Data(contentsOf: url), let obj = try? JSONSerialization.jsonObject(with: data) as? [String: String] else { return [:] } return obj } private static func resourceURL(_ loc: String) -> URL? { return Bundle.main.url(forResource: loc, withExtension: "json", subdirectory: "i18n") ?? Bundle.main.url(forResource: loc, withExtension: "json") } // 当前解析出的 locale(zh-CN / zh-TW / en-US),供需向后端登记语言的场景复用 // (如 APNs 注册,使推送文案与 UI 语言一致)。 static var locale: String { preferredLocale() } private static func preferredLocale() -> String { for lang in Locale.preferredLanguages { if lang.hasPrefix("zh-Hant") || lang.hasPrefix("zh-TW") || lang.hasPrefix("zh-HK") { return "zh-TW" } if lang.hasPrefix("zh") { return "zh-CN" } if lang.hasPrefix("en") { return "en-US" } } return "zh-CN" } } // 全局简写,与 web 的 t() 对齐。 func t(_ key: String) -> String { return I18n.t(key) } func t(_ key: String, _ args: [String: String]) -> String { return I18n.t(key, args) }