import Foundation import MacToolsPluginKit enum AppLanguagePreference: String, CaseIterable, Identifiable { case system case zhHans = "zh-Hans" case zhHant = "zh-Hant" case en case es case fr case ru case pt case de case ja case ko case ar static let userDefaultsKey = "app.languagePreference" private static let appleLanguagesKey = "AppleLanguages" private static let rightClickFinderSyncBundleSuffix = ".right-click.finder-sync" var id: String { rawValue } var title: String { switch self { case .system: return AppL10n.settings("language.system", defaultValue: "跟随系统") case .zhHans: return AppL10n.settings("language.zh-Hans", defaultValue: "简体中文") case .zhHant: return AppL10n.settings("language.zh-Hant", defaultValue: "繁體中文") case .en: return AppL10n.settings("language.en", defaultValue: "English") case .es: return AppL10n.settings("language.es", defaultValue: "Español") case .fr: return AppL10n.settings("language.fr", defaultValue: "Français") case .ru: return AppL10n.settings("language.ru", defaultValue: "Русский") case .pt: return AppL10n.settings("language.pt", defaultValue: "Português") case .de: return AppL10n.settings("language.de", defaultValue: "Deutsch") case .ja: return AppL10n.settings("language.ja", defaultValue: "日本語") case .ko: return AppL10n.settings("language.ko", defaultValue: "한국어") case .ar: return AppL10n.settings("language.ar", defaultValue: "العربية") } } /// A language picker option combines the name as written in the system's /// language with the language's own name, so it remains recognizable even /// when the app is currently displayed in another language. var pickerTitle: String { pickerTitle(systemLanguageIdentifier: Self.systemPreferredLanguageIdentifier) } func pickerTitle(systemLanguageIdentifier: String) -> String { switch self { case .system: let languageCode = Locale(identifier: systemLanguageIdentifier).language.languageCode?.identifier ?? systemLanguageIdentifier let systemLanguageName = Self.localizedLanguageName( for: languageCode, in: systemLanguageIdentifier ) return "\(title) (\(systemLanguageName))" default: let localizedName = Self.localizedLanguageName( for: localeIdentifier, in: systemLanguageIdentifier ) let nativeName = nativeTitle return localizedName == nativeName ? nativeName : "\(localizedName) / \(nativeName)" } } var appleLanguagesOverride: [String]? { switch self { case .system: return nil case .zhHans: return ["zh-Hans"] case .zhHant: return ["zh-Hant"] case .en: return ["en"] case .es: return ["es"] case .fr: return ["fr"] case .ru: return ["ru"] case .pt: return ["pt"] case .de: return ["de"] case .ja: return ["ja"] case .ko: return ["ko"] case .ar: return ["ar"] } } private var localeIdentifier: String { switch self { case .system: Self.systemPreferredLanguageIdentifier case .zhHans: "zh-Hans" case .zhHant: "zh-Hant" case .en: "en" case .es: "es" case .fr: "fr" case .ru: "ru" case .pt: "pt-BR" case .de: "de" case .ja: "ja" case .ko: "ko" case .ar: "ar" } } private var nativeTitle: String { switch self { case .system: title case .zhHans: "简体中文" case .zhHant: "繁體中文" case .en: "English" case .es: "Español" case .fr: "Français" case .ru: "Русский" case .pt: "Português (Brasil)" case .de: "Deutsch" case .ja: "日本語" case .ko: "한국어" case .ar: "العربية" } } func store(in userDefaults: UserDefaults = .standard) { userDefaults.set(rawValue, forKey: Self.userDefaultsKey) applyAppleLanguagesOverride(in: userDefaults) userDefaults.synchronize() PluginRuntimeLocalization.source.setPreference(rawValue) } func applyAppleLanguagesOverride(in userDefaults: UserDefaults = .standard) { if let appleLanguagesOverride { userDefaults.set(appleLanguagesOverride, forKey: Self.appleLanguagesKey) } else { userDefaults.removeObject(forKey: Self.appleLanguagesKey) } if let extensionBundleIdentifier = Self.rightClickFinderSyncBundleIdentifier() { Self.applyAppleLanguagesOverride( appleLanguagesOverride, toBundleIdentifier: extensionBundleIdentifier ) } Self.applyRightClickFinderSyncLanguageOverride(appleLanguagesOverride) } static func stored(in userDefaults: UserDefaults = .standard) -> AppLanguagePreference { guard let rawValue = userDefaults.string(forKey: userDefaultsKey), let preference = AppLanguagePreference(rawValue: rawValue) else { return .system } return preference } static func applyStoredPreference(userDefaults: UserDefaults = .standard) { let preference = stored(in: userDefaults) preference.applyAppleLanguagesOverride(in: userDefaults) PluginRuntimeLocalization.source.setPreference(preference.rawValue) } private static var systemPreferredLanguageIdentifier: String { let globalDomain = UserDefaults.standard.persistentDomain(forName: UserDefaults.globalDomain) if let languages = globalDomain?[appleLanguagesKey] as? [String], let language = languages.first, !language.isEmpty { return language } return Locale.current.identifier } private static func localizedLanguageName( for languageIdentifier: String, in displayLanguageIdentifier: String ) -> String { let displayLocale = Locale(identifier: displayLanguageIdentifier) return displayLocale.localizedString(forIdentifier: languageIdentifier) ?? displayLocale.localizedString( forLanguageCode: Locale(identifier: languageIdentifier).language.languageCode?.identifier ?? languageIdentifier ) ?? languageIdentifier } private static func rightClickFinderSyncBundleIdentifier(bundle: Bundle = .main) -> String? { guard let bundleIdentifier = bundle.bundleIdentifier else { return nil } return bundleIdentifier + rightClickFinderSyncBundleSuffix } private static func applyAppleLanguagesOverride( _ appleLanguagesOverride: [String]?, toBundleIdentifier bundleIdentifier: String ) { let key = appleLanguagesKey as CFString let applicationID = bundleIdentifier as CFString let value = appleLanguagesOverride.map { $0 as CFArray } CFPreferencesSetValue( key, value, applicationID, kCFPreferencesCurrentUser, kCFPreferencesAnyHost ) CFPreferencesAppSynchronize(applicationID) } private static func applyRightClickFinderSyncLanguageOverride(_ appleLanguagesOverride: [String]?) { var configuration = RightClickConfigurationStore.load() guard configuration.preferredLanguages != appleLanguagesOverride else { return } configuration.preferredLanguages = appleLanguagesOverride RightClickConfigurationStore.save(configuration) } }