Ver código fonte

Always launch in the macOS system language instead of restoring the last picker choice.

Resolve locale from bundle and system preferences on startup, keep Settings language changes session-only, and rebuild chrome when the effective language settles after launch.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 mês atrás
pai
commit
d7da97f1c9

+ 93 - 43
meetings_app/Localization/AppLanguageManager.swift

@@ -6,8 +6,11 @@ extension Notification.Name {
 }
 
 enum AppLanguageManager {
-    private static let defaultsKey = "settings.appLanguageCode"
+    private static let legacyDefaultsKey = "settings.appLanguageCode"
     private static var didApplyLaunchLanguage = false
+    private static var cachedSystemLanguageCode: String?
+    /// In-session override from Settings → Language; cleared on every launch so the app follows macOS.
+    private static var sessionLanguageOverride: String?
 
     private static let launchLanguageBootstrap: Void = {
         applyLanguageOnLaunch()
@@ -17,23 +20,35 @@ enum AppLanguageManager {
         _ = launchLanguageBootstrap
     }
 
+    /// Effective macOS language preferences for this app (bundle localization, per-app language, then system).
+    private static var systemPreferredLanguageIdentifiers: [String] {
+        var identifiers: [String] = []
+        identifiers.append(contentsOf: Bundle.main.preferredLocalizations)
+        identifiers.append(contentsOf: Locale.preferredLanguages)
+        if let global = UserDefaults.standard.persistentDomain(forName: UserDefaults.globalDomain),
+           let appleLanguages = global["AppleLanguages"] as? [String] {
+            for language in appleLanguages where identifiers.contains(language) == false {
+                identifiers.append(language)
+            }
+        }
+        let current = Locale.current.identifier
+        if identifiers.contains(current) == false {
+            identifiers.append(current)
+        }
+        return identifiers
+    }
+
     /// UI languages shown in Settings → Language (order preserved).
     private static let supportedLanguageCodes = ["en", "fr", "fr-CA", "es-ES", "es-MX", "de", "it", "nl", "pl", "sv", "nb", "fi", "el", "he", "ar", "ko", "pt-BR", "ru", "uk", "vi", "zh-Hans", "zh-Hant"]
 
-    /// `nil` follows the system language; otherwise uses the selected `.lproj` code.
+    /// `nil` follows the macOS system language; otherwise uses the in-session Settings → Language choice.
     static var selectedLanguageCode: String? {
-        get {
-            guard UserDefaults.standard.object(forKey: defaultsKey) != nil else { return nil }
-            let value = UserDefaults.standard.string(forKey: defaultsKey) ?? ""
-            return value.isEmpty ? nil : value
-        }
-        set {
-            if let code = newValue, code.isEmpty == false {
-                UserDefaults.standard.set(code, forKey: defaultsKey)
-            } else {
-                UserDefaults.standard.removeObject(forKey: defaultsKey)
-            }
-        }
+        sessionLanguageOverride
+    }
+
+    /// True when the app is using the macOS system language (no in-session override).
+    static var isFollowingSystemLanguage: Bool {
+        sessionLanguageOverride == nil
     }
 
     static var availableLanguageCodes: [String] {
@@ -42,15 +57,18 @@ enum AppLanguageManager {
         }
     }
 
-    /// Resolved language code: stored preference, or best match for the system locale.
+    /// Resolved language code: in-session picker choice, or best match for the system locale.
     static var effectiveLanguageCode: String {
-        selectedLanguageCode
+        if let code = sessionLanguageOverride {
+            return resolvedBundleLanguageCode(for: code)
+        }
+        return cachedSystemLanguageCode
             ?? bestMatchingAvailableLanguageCode()
             ?? availableLanguageCodes.first
             ?? "en"
     }
 
-    /// Language code shown in Settings → Language.
+    /// Language code pre-selected in Settings → Language (session choice or best system match).
     static var pickerSelectedLanguageCode: String {
         effectiveLanguageCode
     }
@@ -59,30 +77,45 @@ enum AppLanguageManager {
         let available = availableLanguageCodes
         guard available.isEmpty == false else { return nil }
 
-        func match(_ identifier: String) -> String? {
-            let normalized = identifier.replacingOccurrences(of: "_", with: "-")
-            if available.contains(normalized) { return normalized }
-            let base = normalized.split(separator: "-").first.map(String.init) ?? normalized
-            if available.contains(base) { return base }
-            return available.first { code in
-                code.split(separator: "-").first.map(String.init) == base
-            }
+        for preferred in systemPreferredLanguageIdentifiers {
+            if let code = matchLanguageCode(preferred, in: available) { return code }
+        }
+        return matchLanguageCode(Locale.current.identifier, in: available)
+    }
+
+    private static func matchLanguageCode(_ identifier: String, in available: [String]) -> String? {
+        let normalized = identifier.replacingOccurrences(of: "_", with: "-")
+        if available.contains(normalized) { return normalized }
+        let base = normalized.split(separator: "-").first.map(String.init) ?? normalized
+        if available.contains(base) { return base }
+        return available.first { code in
+            code.split(separator: "-").first.map(String.init) == base
         }
+    }
+
+    private static func resolvedBundleLanguageCode(for code: String) -> String {
+        let available = availableLanguageCodes
+        guard available.isEmpty == false else { return "en" }
+        return matchLanguageCode(code, in: available) ?? available.first(where: { $0 == "en" }) ?? available[0]
+    }
 
-        for preferred in Locale.preferredLanguages {
-            if let code = match(preferred) { return code }
+    private static func bundle(forLanguageCode code: String) -> Bundle {
+        let resolvedCode = resolvedBundleLanguageCode(for: code)
+        if let path = Bundle.main.path(forResource: resolvedCode, ofType: "lproj"),
+           let bundle = Bundle(path: path) {
+            return bundle
         }
-        return match(Locale.current.identifier)
+        // Never fall back to `.main` — it localizes using the macOS system locale, not our resolved language.
+        if let path = Bundle.main.path(forResource: "en", ofType: "lproj"),
+           let bundle = Bundle(path: path) {
+            return bundle
+        }
+        return .main
     }
 
     static var localizationBundle: Bundle {
         bootstrapLaunchLanguageIfNeeded()
-        let code = effectiveLanguageCode
-        guard let path = Bundle.main.path(forResource: code, ofType: "lproj"),
-              let bundle = Bundle(path: path) else {
-            return .main
-        }
-        return bundle
+        return bundle(forLanguageCode: effectiveLanguageCode)
     }
 
     static var formattingLocale: Locale {
@@ -179,26 +212,42 @@ enum AppLanguageManager {
     /// Ensures launch-time language resolution runs before UI strings are read.
     static func syncWithSystemLanguageOnLaunch() {
         bootstrapLaunchLanguageIfNeeded()
+        resetToSystemLanguageForLaunch()
         applyGlobalLayoutDirection()
     }
 
     /// Re-evaluates system language matching after launch; macOS locale preferences can settle after the first UI pass.
-    static func reconcileLanguageAfterLaunch() {
-        guard selectedLanguageCode == nil else {
-            applyGlobalLayoutDirection()
-            return
-        }
-
+    @discardableResult
+    static func reconcileLanguageAfterLaunch() -> Bool {
         let previous = effectiveLanguageCode
+        if isFollowingSystemLanguage {
+            refreshCachedSystemLanguageCode()
+        }
         applyGlobalLayoutDirection()
-        if effectiveLanguageCode != previous {
+        let didChange = effectiveLanguageCode != previous
+        if didChange {
             NotificationCenter.default.post(name: .appLanguageDidChange, object: nil)
         }
+        return didChange
     }
 
     private static func applyLanguageOnLaunch() {
         guard didApplyLaunchLanguage == false else { return }
         didApplyLaunchLanguage = true
+        resetToSystemLanguageForLaunch()
+    }
+
+    private static func resetToSystemLanguageForLaunch() {
+        sessionLanguageOverride = nil
+        // Older builds persisted the picker choice; always follow macOS on a fresh launch.
+        UserDefaults.standard.removeObject(forKey: legacyDefaultsKey)
+        refreshCachedSystemLanguageCode()
+    }
+
+    private static func refreshCachedSystemLanguageCode() {
+        cachedSystemLanguageCode = bestMatchingAvailableLanguageCode()
+            ?? availableLanguageCodes.first
+            ?? "en"
     }
 
     static var isRightToLeft: Bool {
@@ -262,8 +311,9 @@ enum AppLanguageManager {
     static func setLanguage(code: String?) {
         let normalized = code?.trimmingCharacters(in: .whitespacesAndNewlines)
         let cleaned = (normalized?.isEmpty == false) ? normalized : nil
-        guard cleaned != selectedLanguageCode else { return }
-        selectedLanguageCode = cleaned
+        let resolved = cleaned.map { resolvedBundleLanguageCode(for: $0) }
+        guard resolved != sessionLanguageOverride else { return }
+        sessionLanguageOverride = resolved
         applyGlobalLayoutDirection()
         NotificationCenter.default.post(name: .appLanguageDidChange, object: nil)
     }

+ 8 - 4
meetings_app/ViewController.swift

@@ -613,9 +613,9 @@ final class ViewController: NSViewController {
         registerWidgetNotificationObservers()
         registerLanguageChangeObserverIfNeeded()
         AppLanguageManager.applyGlobalLayoutDirection()
+        chromeLanguageCode = AppLanguageManager.effectiveLanguageCode
         setupRootView()
         buildMainLayout()
-        chromeLanguageCode = AppLanguageManager.effectiveLanguageCode
         showLaunchSplashIfNeeded()
         startStoreKit()
     }
@@ -879,9 +879,13 @@ private extension ViewController {
         DispatchQueue.main.async { [weak self] in
             guard let self else { return }
             let builtWith = self.chromeLanguageCode ?? AppLanguageManager.effectiveLanguageCode
-            AppLanguageManager.reconcileLanguageAfterLaunch()
-            guard builtWith != AppLanguageManager.effectiveLanguageCode else { return }
-            self.reloadTheme()
+            let didChange = AppLanguageManager.reconcileLanguageAfterLaunch()
+            let resolved = AppLanguageManager.effectiveLanguageCode
+            // macOS can settle its locale after the first UI pass; rebuild chrome when needed.
+            if didChange || builtWith != resolved {
+                self.chromeLanguageCode = resolved
+                self.reloadTheme()
+            }
         }
     }