taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

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 }