import Foundation /// 界面语言。源代码里的文案是简体中文;选择英文时,`tr` 在显示前把中文换成英文。 /// /// 翻译表以中文原文为键:没有插值的文案精确匹配;带插值的文案用 `{}` 表示插入的内容, /// 例如 “{} 个进程” → “{} processes”,英文里可以写 `{1}`、`{2}` 调整顺序。 public enum AppLanguage: String, CaseIterable, Sendable { case system, chinese, english /// 跟随系统时,系统首选语言是中文就用中文,否则用英文。 /// 读全局偏好而不是 Locale.preferredLanguages:后者会被应用自己写入的 AppleLanguages 覆盖 public var resolved: AppLanguage { guard self == .system else { return self } let global = CFPreferencesCopyValue("AppleLanguages" as CFString, kCFPreferencesAnyApplication, kCFPreferencesCurrentUser, kCFPreferencesAnyHost) as? [String] let preferred = (global?.first ?? Locale.preferredLanguages.first ?? "zh").lowercased() return preferred.hasPrefix("zh") ? .chinese : .english } /// 让系统提供的文字(日期、显示器名称、应用名称)与界面语言一致:写入应用自己的 AppleLanguages,下次启动生效 public func applyToProcessLocale(defaults: UserDefaults = .standard) { // 地区保持用户设置(影响日期顺序、单位等),只换语言 let region = (CFPreferencesCopyValue("AppleLocale" as CFString, kCFPreferencesAnyApplication, kCFPreferencesCurrentUser, kCFPreferencesAnyHost) as? String) .map { Locale(identifier: $0).region?.identifier ?? "US" } ?? "US" switch self { case .system: defaults.removeObject(forKey: "AppleLanguages") defaults.removeObject(forKey: "AppleLocale") case .chinese: defaults.set(["zh-Hans"], forKey: "AppleLanguages") defaults.set("zh_Hans_\(region)", forKey: "AppleLocale") case .english: defaults.set(["en"], forKey: "AppleLanguages") defaults.set("en_\(region)", forKey: "AppleLocale") } } } public enum L10n { /// 默认中文;应用启动时按设置调用 configure。单元测试与辅助工具不调用,始终保持中文原文 nonisolated(unsafe) public private(set) static var language: AppLanguage = .chinese /// 截图与调试时记录没有翻译的中文,便于补全 nonisolated(unsafe) public static var missLog: URL? public static var isEnglish: Bool { language == .english } /// 格式化日期用的地区:英文界面用英文月份名,地区沿用系统设置 public static var locale: Locale { guard isEnglish else { return .current } return Locale(identifier: "en_" + (Locale.current.region?.identifier ?? "US")) } public static func configure(_ language: AppLanguage) { lock.lock() self.language = language.resolved cache = [:] lock.unlock() } private static let lock = NSLock() nonisolated(unsafe) private static var cache: [String: String] = [:] nonisolated(unsafe) private static var misses = Set() static func translate(_ text: String) -> String { guard language == .english, containsChinese(text) else { return text } lock.lock() defer { lock.unlock() } if let cached = cache[text] { return cached } let result = Translations.shared.translate(text) ?? { recordMiss(text) return text }() // 带数字的文案不断变化,缓存设上限 if cache.count > 4000 { cache.removeAll(keepingCapacity: true) } cache[text] = result return result } static func containsChinese(_ text: String) -> Bool { text.unicodeScalars.contains { (0x4E00...0x9FFF).contains($0.value) || (0x3000...0x303F).contains($0.value) || (0xFF00...0xFFEF).contains($0.value) } } private static func recordMiss(_ text: String) { guard let missLog, misses.insert(text).inserted else { return } if let handle = try? FileHandle(forWritingTo: missLog) { handle.seekToEndOfFile() handle.write(Data((text.replacingOccurrences(of: "\n", with: "\\n") + "\n").utf8)) try? handle.close() } else { try? Data((text + "\n").utf8).write(to: missLog) } } } /// 界面文案:英文界面下返回译文,否则原样返回 @inline(__always) public func tr(_ text: String) -> String { L10n.translate(text) } /// 翻译表:精确匹配表与带 `{}` 的模板 final class Translations: @unchecked Sendable { static let shared = Translations() private var exact: [String: String] = [:] private var templates: [Template] = [] struct Template { let fragments: [String] let regex: NSRegularExpression let english: String } private init() { for table in Translations.tables { for line in table.split(separator: "\n", omittingEmptySubsequences: true) { let parts = line.split(separator: "\t", maxSplits: 1, omittingEmptySubsequences: false) guard parts.count == 2 else { continue } let source = String(parts[0]).replacingOccurrences(of: "\\n", with: "\n") let english = String(parts[1]).replacingOccurrences(of: "\\n", with: "\n") if source.contains("{}") { let fragments = source.components(separatedBy: "{}") let pattern = "^" + fragments.map(NSRegularExpression.escapedPattern(for:)).joined(separator: "(.*?)") + "$" if let regex = try? NSRegularExpression(pattern: pattern, options: [.dotMatchesLineSeparators]) { templates.append(Template(fragments: fragments.filter { !$0.isEmpty }, regex: regex, english: english)) } } else { exact[source] = english } } } // 片段越长越具体,优先匹配 templates.sort { $0.fragments.joined().count > $1.fragments.joined().count } } func translate(_ text: String, depth: Int = 0) -> String? { if let english = exact[text] { return english } let range = NSRange(text.startIndex..., in: text) for template in templates where template.fragments.allSatisfy(text.contains) { guard let match = template.regex.firstMatch(in: text, range: range) else { continue } var values: [String] = [] for index in 1.. String { var result = english // 先替换带序号的 {1}{2},再按顺序替换 {} for (index, value) in values.enumerated() { result = result.replacingOccurrences(of: "{\(index + 1)}", with: value) } var iterator = values.makeIterator() while let range = result.range(of: "{}") { result.replaceSubrange(range, with: iterator.next() ?? "") } return result } }