LanguageManager.swift (4174B)
1 /* 2 * This file is part of GNU Taler 3 * (C) 2026 Bohdan, Volodymyr Potuzhnyi 4 * 5 * GNU Taler is free software; you can redistribute it and/or modify it under the 6 * terms of the GNU General Public License as published by the Free Software 7 * Foundation; either version 3, or (at your option) any later version. 8 * 9 * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY 10 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 * 13 * You should have received a copy of the GNU General Public License along with 14 * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 import Foundation 18 import Combine 19 import os 20 21 private let logger = Logger(subsystem: "net.taler.pos", category: "LanguageManager") 22 23 /// Manages in-app language override. Changes apply immediately 24 /// without requiring an app restart. 25 final class LanguageManager: ObservableObject { 26 static let shared = LanguageManager() 27 28 private static let storageKey = "pos_selected_language" 29 30 @Published private(set) var bundle: Bundle = .main 31 @Published private(set) var selectedLanguage: String = "" 32 33 private init() { 34 let saved = UserDefaults.standard.string(forKey: Self.storageKey) ?? "" 35 selectedLanguage = saved 36 bundle = Self.bundle(for: saved) 37 logger.info("Init: saved=\(saved) appPreferred=\(Locale.preferredLanguages) systemPreferred=\(Self.systemPreferredLanguages) resolved=\(self.bundle.bundlePath)") 38 } 39 40 @Published var languageChangedToast: String? 41 42 func setLanguage(_ tag: String) { 43 let previousTag = selectedLanguage 44 selectedLanguage = tag 45 if tag.isEmpty { 46 UserDefaults.standard.removeObject(forKey: Self.storageKey) 47 UserDefaults.standard.removeObject(forKey: "AppleLanguages") 48 } else { 49 UserDefaults.standard.set(tag, forKey: Self.storageKey) 50 UserDefaults.standard.set([tag], forKey: "AppleLanguages") 51 } 52 bundle = Self.bundle(for: tag) 53 logger.info("setLanguage: tag=\(tag) AppleLanguages=\(UserDefaults.standard.object(forKey: "AppleLanguages") as? [String] ?? []) resolved=\(self.bundle.bundlePath)") 54 if tag != previousTag { 55 languageChangedToast = S.toastLanguageChanged 56 } 57 } 58 59 var locale: Locale { 60 selectedLanguage.isEmpty ? .current : Locale(identifier: selectedLanguage) 61 } 62 63 var resolvedLanguageLabel: String? { 64 guard selectedLanguage.isEmpty else { return nil } 65 let resolved = Self.resolveSystemLanguage() 66 return Locale.current.localizedString(forLanguageCode: resolved) 67 } 68 69 private static var systemPreferredLanguages: [String] { 70 let value = CFPreferencesCopyValue( 71 "AppleLanguages" as CFString, 72 kCFPreferencesAnyApplication, 73 kCFPreferencesCurrentUser, 74 kCFPreferencesAnyHost 75 ) 76 return value as? [String] ?? Locale.preferredLanguages 77 } 78 79 private static func resolveSystemLanguage() -> String { 80 let systemLangs = systemPreferredLanguages 81 let available = Set(Bundle.main.localizations) 82 for lang in systemLangs { 83 if available.contains(lang) { return lang } 84 let base = lang.components(separatedBy: "-").first ?? lang 85 if available.contains(base) { return base } 86 } 87 return "en" 88 } 89 90 private static func bundle(for languageTag: String) -> Bundle { 91 let tag = languageTag.isEmpty ? resolveSystemLanguage() : languageTag 92 return lprojBundle(for: tag) ?? .main 93 } 94 95 private static func lprojBundle(for tag: String) -> Bundle? { 96 if let path = Bundle.main.path(forResource: tag, ofType: "lproj"), 97 let b = Bundle(path: path) { 98 return b 99 } 100 let base = tag.components(separatedBy: "-").first ?? tag 101 if base != tag, 102 let path = Bundle.main.path(forResource: base, ofType: "lproj"), 103 let b = Bundle(path: path) { 104 return b 105 } 106 return nil 107 } 108 }