Переглянути джерело

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 місяць тому
батько
коміт
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()
+            }
         }
     }