| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- import AppKit
- import Foundation
- extension Notification.Name {
- static let appLanguageDidChange = Notification.Name("appLanguageDidChange")
- }
- enum AppLanguageManager {
- private static let defaultsKey = "settings.appLanguageCode"
- /// 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.
- 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] {
- supportedLanguageCodes.filter { code in
- Bundle.main.path(forResource: code, ofType: "lproj") != nil
- }
- }
- /// Language code shown in Settings → Language when no explicit choice is stored.
- static var pickerSelectedLanguageCode: String {
- if let code = selectedLanguageCode {
- return code
- }
- return bestMatchingAvailableLanguageCode() ?? availableLanguageCodes.first ?? "en"
- }
- private static func bestMatchingAvailableLanguageCode() -> String? {
- 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 Locale.preferredLanguages {
- if let code = match(preferred) { return code }
- }
- return match(Locale.current.identifier)
- }
- 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
- }
- /// User-facing dates/times respect Settings → Language (not only the system locale).
- static func dateFormatter(
- dateFormat: String? = nil,
- dateStyle: DateFormatter.Style = .none,
- timeStyle: DateFormatter.Style = .none
- ) -> DateFormatter {
- let formatter = DateFormatter()
- formatter.locale = formattingLocale
- formatter.timeZone = TimeZone.current
- if let dateFormat {
- formatter.dateFormat = dateFormat
- } else {
- formatter.dateStyle = dateStyle
- formatter.timeStyle = timeStyle
- }
- return formatter
- }
- static func localizedDateString(
- from date: Date,
- dateStyle: DateFormatter.Style,
- timeStyle: DateFormatter.Style
- ) -> String {
- dateFormatter(dateStyle: dateStyle, timeStyle: timeStyle).string(from: date)
- }
- /// Localized product name (splash, share sheet, etc.).
- static var localizedAppDisplayName: String {
- "AI Companion for Meet".localized
- }
- static func displayName(for languageCode: String) -> String {
- switch languageCode {
- case "en":
- return "English"
- case "fr":
- return "Français"
- case "fr-CA":
- return "Français (Canada)"
- case "es-ES":
- return "Español (España)"
- case "es-MX":
- return "Español (México)"
- case "de":
- return "Deutsch"
- case "it":
- return "Italiano"
- case "nl":
- return "Nederlands"
- case "pl":
- return "Polski"
- case "sv":
- return "Svenska"
- case "nb":
- return "Norsk"
- case "fi":
- return "Suomi"
- case "el":
- return "Ελληνικά"
- case "he":
- return "עברית"
- case "ar":
- return "العربية"
- case "ko":
- return "한국어"
- case "pt-BR":
- return "Português (Brasil)"
- case "ru":
- return "Русский"
- case "uk":
- return "Українська"
- case "vi":
- return "Tiếng Việt"
- case "zh-Hans":
- return "简体中文"
- case "zh-Hant":
- return "繁體中文"
- default:
- return Locale.current.localizedString(forIdentifier: languageCode) ?? languageCode
- }
- }
- /// Locale identifier used for layout direction (selected language or system default).
- static var activeLanguageIdentifier: String {
- if let code = selectedLanguageCode {
- return code
- }
- return Locale.preferredLanguages.first ?? Locale.current.identifier
- }
- static var isRightToLeft: Bool {
- let identifier = activeLanguageIdentifier
- let locale = Locale(identifier: identifier)
- let languageCode = locale.languageCode
- ?? identifier.split(whereSeparator: { $0 == "-" || $0 == "_" }).first.map(String.init)
- ?? identifier
- if languageCode == "ar" || languageCode == "he" {
- return true
- }
- return Locale.characterDirection(forLanguage: languageCode) == .rightToLeft
- }
- static var userInterfaceLayoutDirection: NSUserInterfaceLayoutDirection {
- isRightToLeft ? .rightToLeft : .leftToRight
- }
- static var baseWritingDirection: NSWritingDirection {
- isRightToLeft ? .rightToLeft : .leftToRight
- }
- static var textAlignment: NSTextAlignment {
- isRightToLeft ? .right : .left
- }
- /// Alignment for labels pinned to the trailing edge (e.g. numeric counters).
- static var trailingEdgeTextAlignment: NSTextAlignment {
- isRightToLeft ? .left : .right
- }
- static var disclosureChevron: String {
- isRightToLeft ? "‹" : "›"
- }
- static func applyGlobalLayoutDirection() {
- for window in NSApp.windows {
- if let contentView = window.contentView {
- applyLayoutDirection(to: contentView)
- }
- }
- }
- static func configureTextFieldForLayout(_ field: NSTextField, alignment: NSTextAlignment? = nil) {
- field.alignment = alignment ?? textAlignment
- field.baseWritingDirection = baseWritingDirection
- field.userInterfaceLayoutDirection = userInterfaceLayoutDirection
- }
- static func configureContainerForLayout(_ view: NSView) {
- view.userInterfaceLayoutDirection = userInterfaceLayoutDirection
- }
- static func applyLayoutDirection(to view: NSView) {
- view.userInterfaceLayoutDirection = userInterfaceLayoutDirection
- for subview in view.subviews {
- applyLayoutDirection(to: subview)
- }
- }
- 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
- applyGlobalLayoutDirection()
- NotificationCenter.default.post(name: .appLanguageDidChange, object: nil)
- }
- }
|