1b069057dSTomasz Sapeta // Copyright 2021-present 650 Industries. All rights reserved. 2b069057dSTomasz Sapeta 3b069057dSTomasz Sapeta import Foundation 4b069057dSTomasz Sapeta import ExpoModulesCore 5b069057dSTomasz Sapeta 652d64055Saleqsio let LOCALE_SETTINGS_CHANGED = "onLocaleSettingsChanged" 752d64055Saleqsio let CALENDAR_SETTINGS_CHANGED = "onCalendarSettingsChanged" 852d64055Saleqsio 9b069057dSTomasz Sapeta public class LocalizationModule: Module { definitionnull10b069057dSTomasz Sapeta public func definition() -> ModuleDefinition { 112c5ab579STomasz Sapeta Name("ExpoLocalization") 12b069057dSTomasz Sapeta 132c5ab579STomasz Sapeta Constants { 14b069057dSTomasz Sapeta return Self.getCurrentLocalization() 15b069057dSTomasz Sapeta } 162c5ab579STomasz Sapeta AsyncFunction("getLocalizationAsync") { 17b069057dSTomasz Sapeta return Self.getCurrentLocalization() 18b069057dSTomasz Sapeta } 1917185210Saleqsio Function("getLocales") { 2017185210Saleqsio return Self.getLocales() 2117185210Saleqsio } 2217185210Saleqsio Function("getCalendars") { 2317185210Saleqsio return Self.getCalendars() 2417185210Saleqsio } 2512075537Saleqsio OnCreate { 2612075537Saleqsio if let enableRTL = Bundle.main.object(forInfoDictionaryKey: "ExpoLocalization_supportsRTL") as? Bool { 2712075537Saleqsio self.setSupportsRTL(enableRTL) 2812075537Saleqsio } 2912075537Saleqsio } 3052d64055Saleqsio 3152d64055Saleqsio Events(LOCALE_SETTINGS_CHANGED, CALENDAR_SETTINGS_CHANGED) 3252d64055Saleqsio 3352d64055Saleqsio OnStartObserving { 3452d64055Saleqsio NotificationCenter.default.addObserver( 3552d64055Saleqsio self, 3652d64055Saleqsio selector: #selector(LocalizationModule.localeChanged), 3752d64055Saleqsio name: NSLocale.currentLocaleDidChangeNotification, // swiftlint:disable:this legacy_objc_type 3852d64055Saleqsio object: nil 3952d64055Saleqsio ) 4052d64055Saleqsio } 4152d64055Saleqsio 4252d64055Saleqsio OnStopObserving { 4352d64055Saleqsio NotificationCenter.default.removeObserver( 4452d64055Saleqsio self, 4552d64055Saleqsio name: NSLocale.currentLocaleDidChangeNotification, // swiftlint:disable:this legacy_objc_type 4652d64055Saleqsio object: nil 4752d64055Saleqsio ) 4852d64055Saleqsio } 4912075537Saleqsio } 5012075537Saleqsio isRTLPreferredForCurrentLocalenull5112075537Saleqsio func isRTLPreferredForCurrentLocale() -> Bool { 5252d64055Saleqsio // swiftlint:disable:next legacy_objc_type 5312075537Saleqsio return NSLocale.characterDirection(forLanguage: NSLocale.preferredLanguages.first ?? "en-US") == NSLocale.LanguageDirection.rightToLeft 5412075537Saleqsio } 5512075537Saleqsio setSupportsRTLnull5612075537Saleqsio func setSupportsRTL(_ supportsRTL: Bool) { 5712075537Saleqsio // These keys are used by React Native here: https://github.com/facebook/react-native/blob/main/React/Modules/RCTI18nUtil.m 5812075537Saleqsio // We set them before React loads to ensure it gets rendered correctly the first time the app is opened. 5912075537Saleqsio // On iOS we need to set both forceRTL and allowRTL so apps don't have to include localization strings. 6012075537Saleqsio UserDefaults.standard.set(supportsRTL, forKey: "RCTI18nUtil_allowRTL") 6112075537Saleqsio UserDefaults.standard.set(supportsRTL ? isRTLPreferredForCurrentLocale() : false, forKey: "RCTI18nUtil_forceRTL") 6212075537Saleqsio UserDefaults.standard.synchronize() 63b069057dSTomasz Sapeta } 64b069057dSTomasz Sapeta 65f29fbd8bSEvan Bacon // If the application isn't manually localized for the device language then the 66f29fbd8bSEvan Bacon // native `Locale.current` will fallback on using English US 67f29fbd8bSEvan Bacon // [cite](https://stackoverflow.com/questions/48136456/locale-current-reporting-wrong-language-on-device). 68f29fbd8bSEvan Bacon // This method will attempt to return the locale that the device is using regardless of the app, 69f29fbd8bSEvan Bacon // providing better parity across platforms. getLocalenull7017185210Saleqsio static func getLocale() -> Locale { 71f29fbd8bSEvan Bacon guard let preferredIdentifier = Locale.preferredLanguages.first else { 72f29fbd8bSEvan Bacon return Locale.current 73f29fbd8bSEvan Bacon } 74f29fbd8bSEvan Bacon return Locale(identifier: preferredIdentifier) 75f29fbd8bSEvan Bacon } 7617185210Saleqsio /** 7717185210Saleqsio Maps ios unique identifiers to [BCP 47 calendar types] 7817185210Saleqsio (https://github.com/unicode-org/cldr/blob/main/common/bcp47/calendar.xml) 7917185210Saleqsio */ getUnicodeCalendarIdentifiernull8017185210Saleqsio static func getUnicodeCalendarIdentifier(calendar: Calendar) -> String { 8117185210Saleqsio switch calendar.identifier { 8217185210Saleqsio case .buddhist: 8317185210Saleqsio return "buddhist" 8417185210Saleqsio case .chinese: 8517185210Saleqsio return "chinese" 8617185210Saleqsio case .coptic: 8717185210Saleqsio return "coptic" 8817185210Saleqsio case .ethiopicAmeteAlem: 8917185210Saleqsio return "ethioaa" 9017185210Saleqsio case .ethiopicAmeteMihret: 9117185210Saleqsio return "ethiopic" 9217185210Saleqsio case .gregorian: 9317185210Saleqsio return "gregory" 9417185210Saleqsio case .hebrew: 9517185210Saleqsio return "hebrew" 9617185210Saleqsio case .indian: 9717185210Saleqsio return "indian" 9817185210Saleqsio case .islamic: 9917185210Saleqsio return "islamic" 10017185210Saleqsio case .islamicCivil: 10117185210Saleqsio return "islamic-civil" 10217185210Saleqsio case .islamicTabular: 10317185210Saleqsio return "islamic-tbla" 10417185210Saleqsio case .islamicUmmAlQura: 10517185210Saleqsio return "islamic-umalqura" 10617185210Saleqsio case .japanese: 10717185210Saleqsio return "japanese" 10817185210Saleqsio case .persian: 10917185210Saleqsio return "persian" 11017185210Saleqsio case .republicOfChina: 11117185210Saleqsio return "roc" 11217185210Saleqsio case .iso8601: 11317185210Saleqsio return "iso8601" 11417185210Saleqsio } 11517185210Saleqsio } 11617185210Saleqsio getMeasurementSystemForLocalenull11752d64055Saleqsio static func getMeasurementSystemForLocale(_ locale: Locale) -> String { 11852d64055Saleqsio if #available(iOS 16, *) { 11952d64055Saleqsio let measurementSystems = [ 12052d64055Saleqsio Locale.MeasurementSystem.us: "us", 12152d64055Saleqsio Locale.MeasurementSystem.uk: "uk", 12252d64055Saleqsio Locale.MeasurementSystem.metric: "metric" 12352d64055Saleqsio ] 12452d64055Saleqsio return measurementSystems[locale.measurementSystem] ?? "metric" 12552d64055Saleqsio } 12652d64055Saleqsio return locale.usesMetricSystem ? "metric" : "us" 12752d64055Saleqsio } 12852d64055Saleqsio getLocalesnull12917185210Saleqsio static func getLocales() -> [[String: Any?]] { 13052d64055Saleqsio let userSettingsLocale = Locale.current 13152d64055Saleqsio 13217185210Saleqsio return (Locale.preferredLanguages.isEmpty ? [Locale.current.identifier] : Locale.preferredLanguages) 13317185210Saleqsio .map { languageTag -> [String: Any?] in 13452d64055Saleqsio let languageLocale = Locale.init(identifier: languageTag) 13552d64055Saleqsio 13617185210Saleqsio return [ 13717185210Saleqsio "languageTag": languageTag, 13852d64055Saleqsio "languageCode": languageLocale.languageCode, 13952d64055Saleqsio "regionCode": languageLocale.regionCode, 14017185210Saleqsio "textDirection": Locale.characterDirection(forLanguage: languageTag) == .rightToLeft ? "rtl" : "ltr", 14152d64055Saleqsio "decimalSeparator": userSettingsLocale.decimalSeparator, 14252d64055Saleqsio "digitGroupingSeparator": userSettingsLocale.groupingSeparator, 14352d64055Saleqsio "measurementSystem": getMeasurementSystemForLocale(userSettingsLocale), 14452d64055Saleqsio "currencyCode": languageLocale.currencyCode, 145*58fd9d6aSWojciech Dróżdż "currencySymbol": languageLocale.currencySymbol, 146*58fd9d6aSWojciech Dróżdż "temperatureUnit": getTemperatureUnit() 14717185210Saleqsio ] 14817185210Saleqsio } 14917185210Saleqsio } 15017185210Saleqsio 15152d64055Saleqsio @objc localeChangednull15252d64055Saleqsio private func localeChanged() { 15352d64055Saleqsio // we send both events since on iOS it means both calendar and locale needs an update 15452d64055Saleqsio sendEvent(LOCALE_SETTINGS_CHANGED) 15552d64055Saleqsio sendEvent(CALENDAR_SETTINGS_CHANGED) 15652d64055Saleqsio } 15752d64055Saleqsio getTemperatureUnitnull158*58fd9d6aSWojciech Dróżdż static func getTemperatureUnit() -> String? { 159*58fd9d6aSWojciech Dróżdż let formatter = MeasurementFormatter() 160*58fd9d6aSWojciech Dróżdż formatter.locale = Locale.current 161*58fd9d6aSWojciech Dróżdż 162*58fd9d6aSWojciech Dróżdż let temperature = Measurement(value: 0, unit: UnitTemperature.celsius) 163*58fd9d6aSWojciech Dróżdż let formatted = formatter.string(from: temperature) 164*58fd9d6aSWojciech Dróżdż 165*58fd9d6aSWojciech Dróżdż guard let unitCharacter = formatted.last else { 166*58fd9d6aSWojciech Dróżdż return nil 167*58fd9d6aSWojciech Dróżdż } 168*58fd9d6aSWojciech Dróżdż 169*58fd9d6aSWojciech Dróżdż return unitCharacter == "F" ? "fahrenheit" : "celsius" 170*58fd9d6aSWojciech Dróżdż } 171*58fd9d6aSWojciech Dróżdż 17217185210Saleqsio // https://stackoverflow.com/a/28183182 uses24HourClocknull17317185210Saleqsio static func uses24HourClock() -> Bool { 17417185210Saleqsio let dateFormat = DateFormatter.dateFormat(fromTemplate: "j", options: 0, locale: Locale.current)! 17517185210Saleqsio 17617185210Saleqsio return dateFormat.firstIndex(of: "a") == nil 17717185210Saleqsio } 17817185210Saleqsio getCalendarsnull17917185210Saleqsio static func getCalendars() -> [[String: Any?]] { 18017185210Saleqsio var calendar = Locale.current.calendar 18117185210Saleqsio return [ 18217185210Saleqsio [ 18317185210Saleqsio "calendar": getUnicodeCalendarIdentifier(calendar: calendar), 18417185210Saleqsio "timeZone": "\(calendar.timeZone.identifier)", 18517185210Saleqsio "uses24hourClock": uses24HourClock(), 18617185210Saleqsio "firstWeekday": calendar.firstWeekday 18717185210Saleqsio ] 18817185210Saleqsio ] 18917185210Saleqsio } 190f29fbd8bSEvan Bacon getCurrentLocalizationnull191b069057dSTomasz Sapeta static func getCurrentLocalization() -> [String: Any?] { 19217185210Saleqsio let locale = getLocale() 193b069057dSTomasz Sapeta let languageCode = locale.languageCode ?? "en" 194b069057dSTomasz Sapeta var languageIds = Locale.preferredLanguages 195b069057dSTomasz Sapeta 196b069057dSTomasz Sapeta if languageIds.isEmpty { 197b069057dSTomasz Sapeta languageIds.append("en-US") 198b069057dSTomasz Sapeta } 199b069057dSTomasz Sapeta return [ 200b069057dSTomasz Sapeta "currency": locale.currencyCode ?? "USD", 201b069057dSTomasz Sapeta "decimalSeparator": locale.decimalSeparator ?? ".", 202b069057dSTomasz Sapeta "digitGroupingSeparator": locale.groupingSeparator ?? ",", 203b069057dSTomasz Sapeta "isoCurrencyCodes": Locale.isoCurrencyCodes, 204b069057dSTomasz Sapeta "isMetric": locale.usesMetricSystem, 205b069057dSTomasz Sapeta "isRTL": Locale.characterDirection(forLanguage: languageCode) == .rightToLeft, 206b069057dSTomasz Sapeta "locale": languageIds.first, 207b069057dSTomasz Sapeta "locales": languageIds, 208b069057dSTomasz Sapeta "region": locale.regionCode ?? "US", 209b069057dSTomasz Sapeta "timezone": TimeZone.current.identifier 210b069057dSTomasz Sapeta ] 211b069057dSTomasz Sapeta } 212b069057dSTomasz Sapeta } 213