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