Pārlūkot izejas kodu

Add app language picker in Settings.

Let users choose system language or a bundled locale, apply strings via AppLanguageManager, and refresh the UI and status bar menu when the selection changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 mēnesi atpakaļ
vecāks
revīzija
132bdc9174

+ 59 - 0
meetings_app/Localization/AppLanguageManager.swift

@@ -0,0 +1,59 @@
+import Foundation
+
+extension Notification.Name {
+    static let appLanguageDidChange = Notification.Name("appLanguageDidChange")
+}
+
+enum AppLanguageManager {
+    private static let defaultsKey = "settings.appLanguageCode"
+
+    /// `nil` follows the system language; otherwise uses the selected `.lproj` code.
+    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)
+            }
+        }
+    }
+
+    static var availableLanguageCodes: [String] {
+        Bundle.main.localizations
+            .filter { $0 != "Base" }
+            .sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
+    }
+
+    static var localizationBundle: Bundle {
+        guard let code = selectedLanguageCode,
+              let path = Bundle.main.path(forResource: code, ofType: "lproj"),
+              let bundle = Bundle(path: path) else {
+            return .main
+        }
+        return bundle
+    }
+
+    static var formattingLocale: Locale {
+        if let code = selectedLanguageCode {
+            return Locale(identifier: code)
+        }
+        return .current
+    }
+
+    static func displayName(for languageCode: String) -> String {
+        Locale.current.localizedString(forIdentifier: languageCode) ?? languageCode
+    }
+
+    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
+        NotificationCenter.default.post(name: .appLanguageDidChange, object: nil)
+    }
+}

+ 2 - 2
meetings_app/Localization/String+Localization.swift

@@ -3,7 +3,7 @@ import Foundation
 extension String {
     /// Looks up this string in `Localizable.strings`.
     var localized: String {
-        NSLocalizedString(self, bundle: .main, comment: "")
+        NSLocalizedString(self, bundle: AppLanguageManager.localizationBundle, comment: "")
     }
 
     /// Localizes UI copy, except the fixed app brand name.
@@ -12,6 +12,6 @@ extension String {
     }
 
     func localized(with arguments: CVarArg...) -> String {
-        String(format: localized, locale: Locale.current, arguments: arguments)
+        String(format: localized, locale: AppLanguageManager.formattingLocale, arguments: arguments)
     }
 }

+ 3 - 0
meetings_app/StatusBar/StatusBarController.swift

@@ -44,6 +44,9 @@ final class StatusBarController: NSObject {
             }
             self.rebuildMenu()
         })
+        observers.append(center.addObserver(forName: .appLanguageDidChange, object: nil, queue: .main) { [weak self] _ in
+            self?.rebuildMenu()
+        })
     }
 
     deinit {

+ 50 - 1
meetings_app/ViewController.swift

@@ -504,6 +504,7 @@ final class ViewController: NSViewController {
     private weak var schedulePageCardsScrollView: NSScrollView?
     private weak var settingsPagePrimaryLanguagePopup: NSPopUpButton?
     private weak var settingsPageSecondaryLanguagePopup: NSPopUpButton?
+    private weak var settingsPageAppLanguagePopup: NSPopUpButton?
 
     // MARK: - Calendar page (custom month UI)
     private var calendarPageMonthAnchor: Date = Calendar.current.startOfDay(for: Date())
@@ -4532,7 +4533,10 @@ private extension ViewController {
         let darkModeRow = makeSettingsDarkModeRow()
         stack.addArrangedSubview(darkModeRow)
         darkModeRow.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
-        stack.setCustomSpacing(24, after: darkModeRow)
+        let appLanguageRow = makeSettingsAppLanguageRow()
+        stack.addArrangedSubview(appLanguageRow)
+        appLanguageRow.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
+        stack.setCustomSpacing(24, after: appLanguageRow)
 
         let aiCompanionTitle = textLabel("AI Companion".localized, font: typography.joinWithURLTitle, color: palette.textPrimary)
         stack.addArrangedSubview(aiCompanionTitle)
@@ -4648,6 +4652,44 @@ private extension ViewController {
         return row
     }
 
+    private func makeSettingsAppLanguageRow() -> NSView {
+        let row = roundedContainer(cornerRadius: 10, color: palette.inputBackground)
+        row.translatesAutoresizingMaskIntoConstraints = false
+        row.heightAnchor.constraint(equalToConstant: 72).isActive = true
+        styleSurface(row, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
+
+        let titleLabel = textLabel("Language".localized, font: NSFont.systemFont(ofSize: 13, weight: .semibold), color: palette.textPrimary)
+        let popup = NSPopUpButton(frame: .zero, pullsDown: false)
+        popup.translatesAutoresizingMaskIntoConstraints = false
+        popup.target = self
+        popup.action = #selector(settingsPageAppLanguageChanged(_:))
+
+        popup.addItem(withTitle: "System Language".localized)
+        popup.lastItem?.representedObject = ""
+        for code in AppLanguageManager.availableLanguageCodes {
+            popup.addItem(withTitle: AppLanguageManager.displayName(for: code))
+            popup.lastItem?.representedObject = code
+        }
+
+        let selectedCode = AppLanguageManager.selectedLanguageCode ?? ""
+        selectSettingsPageLanguage(identifier: selectedCode, in: popup)
+        settingsPageAppLanguagePopup = popup
+
+        row.addSubview(titleLabel)
+        row.addSubview(popup)
+        NSLayoutConstraint.activate([
+            titleLabel.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 14),
+            titleLabel.topAnchor.constraint(equalTo: row.topAnchor, constant: 10),
+            titleLabel.trailingAnchor.constraint(equalTo: row.trailingAnchor, constant: -14),
+
+            popup.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 14),
+            popup.trailingAnchor.constraint(equalTo: row.trailingAnchor, constant: -14),
+            popup.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8),
+            popup.heightAnchor.constraint(equalToConstant: 28)
+        ])
+        return row
+    }
+
     private func makeSettingsSpeechLanguageRow(title: String, isPrimary: Bool) -> NSView {
         let row = roundedContainer(cornerRadius: 10, color: palette.inputBackground)
         row.translatesAutoresizingMaskIntoConstraints = false
@@ -4710,6 +4752,13 @@ private extension ViewController {
         }
     }
 
+    @objc private func settingsPageAppLanguageChanged(_ sender: NSPopUpButton) {
+        let raw = sender.selectedItem?.representedObject as? String ?? ""
+        let code: String? = raw.isEmpty ? nil : raw
+        AppLanguageManager.setLanguage(code: code)
+        reloadTheme()
+    }
+
     @objc private func settingsPageSpeechLanguageChanged(_ sender: NSPopUpButton) {
         guard let primary = settingsPagePrimaryLanguagePopup?.selectedItem?.representedObject as? String,
               primary.isEmpty == false else { return }

+ 2 - 0
meetings_app/en.lproj/Localizable.strings

@@ -141,6 +141,8 @@
 "Notifications" = "Notifications";
 "Help & Legal" = "Help & Legal";
 "Dark Mode" = "Dark Mode";
+"Language" = "Language";
+"System Language" = "System Language";
 "Rate Us" = "Rate Us";
 "Share App" = "Share App";
 "Upgrade" = "Upgrade";