1*af2ec015STomasz Sapeta // Copyright 2021-present 650 Industries. All rights reserved. 2*af2ec015STomasz Sapeta 3*af2ec015STomasz Sapeta import Foundation 4*af2ec015STomasz Sapeta import ABI49_0_0ExpoModulesCore 5*af2ec015STomasz Sapeta 6*af2ec015STomasz Sapeta let LOCALE_SETTINGS_CHANGED = "onLocaleSettingsChanged" 7*af2ec015STomasz Sapeta let CALENDAR_SETTINGS_CHANGED = "onCalendarSettingsChanged" 8*af2ec015STomasz Sapeta 9*af2ec015STomasz Sapeta public class LocalizationModule: Module { definitionnull10*af2ec015STomasz Sapeta public func definition() -> ModuleDefinition { 11*af2ec015STomasz Sapeta Name("ExpoLocalization") 12*af2ec015STomasz Sapeta 13*af2ec015STomasz Sapeta Constants { 14*af2ec015STomasz Sapeta return Self.getCurrentLocalization() 15*af2ec015STomasz Sapeta } 16*af2ec015STomasz Sapeta AsyncFunction("getLocalizationAsync") { 17*af2ec015STomasz Sapeta return Self.getCurrentLocalization() 18*af2ec015STomasz Sapeta } 19*af2ec015STomasz Sapeta Function("getLocales") { 20*af2ec015STomasz Sapeta return Self.getLocales() 21*af2ec015STomasz Sapeta } 22*af2ec015STomasz Sapeta Function("getCalendars") { 23*af2ec015STomasz Sapeta return Self.getCalendars() 24*af2ec015STomasz Sapeta } 25*af2ec015STomasz Sapeta OnCreate { 26*af2ec015STomasz Sapeta if let enableRTL = Bundle.main.object(forInfoDictionaryKey: "ExpoLocalization_supportsRTL") as? Bool { 27*af2ec015STomasz Sapeta self.setSupportsRTL(enableRTL) 28*af2ec015STomasz Sapeta } 29*af2ec015STomasz Sapeta } 30*af2ec015STomasz Sapeta 31*af2ec015STomasz Sapeta Events(LOCALE_SETTINGS_CHANGED, CALENDAR_SETTINGS_CHANGED) 32*af2ec015STomasz Sapeta 33*af2ec015STomasz Sapeta OnStartObserving { 34*af2ec015STomasz Sapeta NotificationCenter.default.addObserver( 35*af2ec015STomasz Sapeta self, 36*af2ec015STomasz Sapeta selector: #selector(LocalizationModule.localeChanged), 37*af2ec015STomasz Sapeta name: NSLocale.currentLocaleDidChangeNotification, // swiftlint:disable:this legacy_objc_type 38*af2ec015STomasz Sapeta object: nil 39*af2ec015STomasz Sapeta ) 40*af2ec015STomasz Sapeta } 41*af2ec015STomasz Sapeta 42*af2ec015STomasz Sapeta OnStopObserving { 43*af2ec015STomasz Sapeta NotificationCenter.default.removeObserver( 44*af2ec015STomasz Sapeta self, 45*af2ec015STomasz Sapeta name: NSLocale.currentLocaleDidChangeNotification, // swiftlint:disable:this legacy_objc_type 46*af2ec015STomasz Sapeta object: nil 47*af2ec015STomasz Sapeta ) 48*af2ec015STomasz Sapeta } 49*af2ec015STomasz Sapeta } 50*af2ec015STomasz Sapeta isRTLPreferredForCurrentLocalenull51*af2ec015STomasz Sapeta func isRTLPreferredForCurrentLocale() -> Bool { 52*af2ec015STomasz Sapeta // swiftlint:disable:next legacy_objc_type 53*af2ec015STomasz Sapeta return NSLocale.characterDirection(forLanguage: NSLocale.preferredLanguages.first ?? "en-US") == NSLocale.LanguageDirection.rightToLeft 54*af2ec015STomasz Sapeta } 55*af2ec015STomasz Sapeta setSupportsRTLnull56*af2ec015STomasz Sapeta func setSupportsRTL(_ supportsRTL: Bool) { 57*af2ec015STomasz Sapeta // These keys are used by ABI49_0_0React Native here: https://github.com/facebook/react-native/blob/main/ABI49_0_0React/Modules/ABI49_0_0RCTI18nUtil.m 58*af2ec015STomasz Sapeta // We set them before ABI49_0_0React loads to ensure it gets rendered correctly the first time the app is opened. 59*af2ec015STomasz Sapeta // On iOS we need to set both forceRTL and allowRTL so apps don't have to include localization strings. 60*af2ec015STomasz Sapeta UserDefaults.standard.set(supportsRTL, forKey: "ABI49_0_0RCTI18nUtil_allowRTL") 61*af2ec015STomasz Sapeta UserDefaults.standard.set(supportsRTL ? isRTLPreferredForCurrentLocale() : false, forKey: "ABI49_0_0RCTI18nUtil_forceRTL") 62*af2ec015STomasz Sapeta UserDefaults.standard.synchronize() 63*af2ec015STomasz Sapeta } 64*af2ec015STomasz Sapeta 65*af2ec015STomasz Sapeta // If the application isn't manually localized for the device language then the 66*af2ec015STomasz Sapeta // native `Locale.current` will fallback on using English US 67*af2ec015STomasz Sapeta // [cite](https://stackoverflow.com/questions/48136456/locale-current-reporting-wrong-language-on-device). 68*af2ec015STomasz Sapeta // This method will attempt to return the locale that the device is using regardless of the app, 69*af2ec015STomasz Sapeta // providing better parity across platforms. getLocalenull70*af2ec015STomasz Sapeta static func getLocale() -> Locale { 71*af2ec015STomasz Sapeta guard let preferredIdentifier = Locale.preferredLanguages.first else { 72*af2ec015STomasz Sapeta return Locale.current 73*af2ec015STomasz Sapeta } 74*af2ec015STomasz Sapeta return Locale(identifier: preferredIdentifier) 75*af2ec015STomasz Sapeta } 76*af2ec015STomasz Sapeta /** 77*af2ec015STomasz Sapeta Maps ios unique identifiers to [BCP 47 calendar types] 78*af2ec015STomasz Sapeta (https://github.com/unicode-org/cldr/blob/main/common/bcp47/calendar.xml) 79*af2ec015STomasz Sapeta */ getUnicodeCalendarIdentifiernull80*af2ec015STomasz Sapeta static func getUnicodeCalendarIdentifier(calendar: Calendar) -> String { 81*af2ec015STomasz Sapeta switch calendar.identifier { 82*af2ec015STomasz Sapeta case .buddhist: 83*af2ec015STomasz Sapeta return "buddhist" 84*af2ec015STomasz Sapeta case .chinese: 85*af2ec015STomasz Sapeta return "chinese" 86*af2ec015STomasz Sapeta case .coptic: 87*af2ec015STomasz Sapeta return "coptic" 88*af2ec015STomasz Sapeta case .ethiopicAmeteAlem: 89*af2ec015STomasz Sapeta return "ethioaa" 90*af2ec015STomasz Sapeta case .ethiopicAmeteMihret: 91*af2ec015STomasz Sapeta return "ethiopic" 92*af2ec015STomasz Sapeta case .gregorian: 93*af2ec015STomasz Sapeta return "gregory" 94*af2ec015STomasz Sapeta case .hebrew: 95*af2ec015STomasz Sapeta return "hebrew" 96*af2ec015STomasz Sapeta case .indian: 97*af2ec015STomasz Sapeta return "indian" 98*af2ec015STomasz Sapeta case .islamic: 99*af2ec015STomasz Sapeta return "islamic" 100*af2ec015STomasz Sapeta case .islamicCivil: 101*af2ec015STomasz Sapeta return "islamic-civil" 102*af2ec015STomasz Sapeta case .islamicTabular: 103*af2ec015STomasz Sapeta return "islamic-tbla" 104*af2ec015STomasz Sapeta case .islamicUmmAlQura: 105*af2ec015STomasz Sapeta return "islamic-umalqura" 106*af2ec015STomasz Sapeta case .japanese: 107*af2ec015STomasz Sapeta return "japanese" 108*af2ec015STomasz Sapeta case .persian: 109*af2ec015STomasz Sapeta return "persian" 110*af2ec015STomasz Sapeta case .republicOfChina: 111*af2ec015STomasz Sapeta return "roc" 112*af2ec015STomasz Sapeta case .iso8601: 113*af2ec015STomasz Sapeta return "iso8601" 114*af2ec015STomasz Sapeta } 115*af2ec015STomasz Sapeta } 116*af2ec015STomasz Sapeta getMeasurementSystemForLocalenull117*af2ec015STomasz Sapeta static func getMeasurementSystemForLocale(_ locale: Locale) -> String { 118*af2ec015STomasz Sapeta if #available(iOS 16, *) { 119*af2ec015STomasz Sapeta let measurementSystems = [ 120*af2ec015STomasz Sapeta Locale.MeasurementSystem.us: "us", 121*af2ec015STomasz Sapeta Locale.MeasurementSystem.uk: "uk", 122*af2ec015STomasz Sapeta Locale.MeasurementSystem.metric: "metric" 123*af2ec015STomasz Sapeta ] 124*af2ec015STomasz Sapeta return measurementSystems[locale.measurementSystem] ?? "metric" 125*af2ec015STomasz Sapeta } 126*af2ec015STomasz Sapeta return locale.usesMetricSystem ? "metric" : "us" 127*af2ec015STomasz Sapeta } 128*af2ec015STomasz Sapeta getLocalesnull129*af2ec015STomasz Sapeta static func getLocales() -> [[String: Any?]] { 130*af2ec015STomasz Sapeta let userSettingsLocale = Locale.current 131*af2ec015STomasz Sapeta 132*af2ec015STomasz Sapeta return (Locale.preferredLanguages.isEmpty ? [Locale.current.identifier] : Locale.preferredLanguages) 133*af2ec015STomasz Sapeta .map { languageTag -> [String: Any?] in 134*af2ec015STomasz Sapeta let languageLocale = Locale.init(identifier: languageTag) 135*af2ec015STomasz Sapeta 136*af2ec015STomasz Sapeta return [ 137*af2ec015STomasz Sapeta "languageTag": languageTag, 138*af2ec015STomasz Sapeta "languageCode": languageLocale.languageCode, 139*af2ec015STomasz Sapeta "regionCode": languageLocale.regionCode, 140*af2ec015STomasz Sapeta "textDirection": Locale.characterDirection(forLanguage: languageTag) == .rightToLeft ? "rtl" : "ltr", 141*af2ec015STomasz Sapeta "decimalSeparator": userSettingsLocale.decimalSeparator, 142*af2ec015STomasz Sapeta "digitGroupingSeparator": userSettingsLocale.groupingSeparator, 143*af2ec015STomasz Sapeta "measurementSystem": getMeasurementSystemForLocale(userSettingsLocale), 144*af2ec015STomasz Sapeta "currencyCode": languageLocale.currencyCode, 145*af2ec015STomasz Sapeta "currencySymbol": languageLocale.currencySymbol 146*af2ec015STomasz Sapeta ] 147*af2ec015STomasz Sapeta } 148*af2ec015STomasz Sapeta } 149*af2ec015STomasz Sapeta 150*af2ec015STomasz Sapeta @objc localeChangednull151*af2ec015STomasz Sapeta private func localeChanged() { 152*af2ec015STomasz Sapeta // we send both events since on iOS it means both calendar and locale needs an update 153*af2ec015STomasz Sapeta sendEvent(LOCALE_SETTINGS_CHANGED) 154*af2ec015STomasz Sapeta sendEvent(CALENDAR_SETTINGS_CHANGED) 155*af2ec015STomasz Sapeta } 156*af2ec015STomasz Sapeta 157*af2ec015STomasz Sapeta // https://stackoverflow.com/a/28183182 uses24HourClocknull158*af2ec015STomasz Sapeta static func uses24HourClock() -> Bool { 159*af2ec015STomasz Sapeta let dateFormat = DateFormatter.dateFormat(fromTemplate: "j", options: 0, locale: Locale.current)! 160*af2ec015STomasz Sapeta 161*af2ec015STomasz Sapeta return dateFormat.firstIndex(of: "a") == nil 162*af2ec015STomasz Sapeta } 163*af2ec015STomasz Sapeta getCalendarsnull164*af2ec015STomasz Sapeta static func getCalendars() -> [[String: Any?]] { 165*af2ec015STomasz Sapeta var calendar = Locale.current.calendar 166*af2ec015STomasz Sapeta return [ 167*af2ec015STomasz Sapeta [ 168*af2ec015STomasz Sapeta "calendar": getUnicodeCalendarIdentifier(calendar: calendar), 169*af2ec015STomasz Sapeta "timeZone": "\(calendar.timeZone.identifier)", 170*af2ec015STomasz Sapeta "uses24hourClock": uses24HourClock(), 171*af2ec015STomasz Sapeta "firstWeekday": calendar.firstWeekday 172*af2ec015STomasz Sapeta ] 173*af2ec015STomasz Sapeta ] 174*af2ec015STomasz Sapeta } 175*af2ec015STomasz Sapeta getCurrentLocalizationnull176*af2ec015STomasz Sapeta static func getCurrentLocalization() -> [String: Any?] { 177*af2ec015STomasz Sapeta let locale = getLocale() 178*af2ec015STomasz Sapeta let languageCode = locale.languageCode ?? "en" 179*af2ec015STomasz Sapeta var languageIds = Locale.preferredLanguages 180*af2ec015STomasz Sapeta 181*af2ec015STomasz Sapeta if languageIds.isEmpty { 182*af2ec015STomasz Sapeta languageIds.append("en-US") 183*af2ec015STomasz Sapeta } 184*af2ec015STomasz Sapeta return [ 185*af2ec015STomasz Sapeta "currency": locale.currencyCode ?? "USD", 186*af2ec015STomasz Sapeta "decimalSeparator": locale.decimalSeparator ?? ".", 187*af2ec015STomasz Sapeta "digitGroupingSeparator": locale.groupingSeparator ?? ",", 188*af2ec015STomasz Sapeta "isoCurrencyCodes": Locale.isoCurrencyCodes, 189*af2ec015STomasz Sapeta "isMetric": locale.usesMetricSystem, 190*af2ec015STomasz Sapeta "isRTL": Locale.characterDirection(forLanguage: languageCode) == .rightToLeft, 191*af2ec015STomasz Sapeta "locale": languageIds.first, 192*af2ec015STomasz Sapeta "locales": languageIds, 193*af2ec015STomasz Sapeta "region": locale.regionCode ?? "US", 194*af2ec015STomasz Sapeta "timezone": TimeZone.current.identifier 195*af2ec015STomasz Sapeta ] 196*af2ec015STomasz Sapeta } 197*af2ec015STomasz Sapeta } 198