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