1*fe5cfb17STomasz Sapeta // Copyright 2021-present 650 Industries. All rights reserved.
2*fe5cfb17STomasz Sapeta 
3*fe5cfb17STomasz Sapeta import Foundation
4*fe5cfb17STomasz Sapeta import ABI48_0_0ExpoModulesCore
5*fe5cfb17STomasz Sapeta 
6*fe5cfb17STomasz Sapeta public class LocalizationModule: Module {
definitionnull7*fe5cfb17STomasz Sapeta   public func definition() -> ModuleDefinition {
8*fe5cfb17STomasz Sapeta     Name("ExpoLocalization")
9*fe5cfb17STomasz Sapeta 
10*fe5cfb17STomasz Sapeta     Constants {
11*fe5cfb17STomasz Sapeta       return Self.getCurrentLocalization()
12*fe5cfb17STomasz Sapeta     }
13*fe5cfb17STomasz Sapeta     AsyncFunction("getLocalizationAsync") {
14*fe5cfb17STomasz Sapeta       return Self.getCurrentLocalization()
15*fe5cfb17STomasz Sapeta     }
16*fe5cfb17STomasz Sapeta     Function("getLocales") {
17*fe5cfb17STomasz Sapeta       return Self.getLocales()
18*fe5cfb17STomasz Sapeta     }
19*fe5cfb17STomasz Sapeta     Function("getCalendars") {
20*fe5cfb17STomasz Sapeta       return Self.getCalendars()
21*fe5cfb17STomasz Sapeta     }
22*fe5cfb17STomasz Sapeta     OnCreate {
23*fe5cfb17STomasz Sapeta       if let enableRTL = Bundle.main.object(forInfoDictionaryKey: "ExpoLocalization_supportsRTL") as? Bool {
24*fe5cfb17STomasz Sapeta         self.setSupportsRTL(enableRTL)
25*fe5cfb17STomasz Sapeta       }
26*fe5cfb17STomasz Sapeta     }
27*fe5cfb17STomasz Sapeta   }
28*fe5cfb17STomasz Sapeta 
isRTLPreferredForCurrentLocalenull29*fe5cfb17STomasz Sapeta   func isRTLPreferredForCurrentLocale() -> Bool {
30*fe5cfb17STomasz Sapeta     return NSLocale.characterDirection(forLanguage: NSLocale.preferredLanguages.first ?? "en-US") == NSLocale.LanguageDirection.rightToLeft
31*fe5cfb17STomasz Sapeta   }
32*fe5cfb17STomasz Sapeta 
setSupportsRTLnull33*fe5cfb17STomasz Sapeta   func setSupportsRTL(_ supportsRTL: Bool) {
34*fe5cfb17STomasz Sapeta     // These keys are used by ABI48_0_0React Native here: https://github.com/facebook/react-native/blob/main/ABI48_0_0React/Modules/ABI48_0_0RCTI18nUtil.m
35*fe5cfb17STomasz Sapeta     // We set them before ABI48_0_0React loads to ensure it gets rendered correctly the first time the app is opened.
36*fe5cfb17STomasz Sapeta     // On iOS we need to set both forceRTL and allowRTL so apps don't have to include localization strings.
37*fe5cfb17STomasz Sapeta     UserDefaults.standard.set(supportsRTL, forKey: "ABI48_0_0RCTI18nUtil_allowRTL")
38*fe5cfb17STomasz Sapeta     UserDefaults.standard.set(supportsRTL ? isRTLPreferredForCurrentLocale() : false, forKey: "ABI48_0_0RCTI18nUtil_forceRTL")
39*fe5cfb17STomasz Sapeta     UserDefaults.standard.synchronize()
40*fe5cfb17STomasz Sapeta   }
41*fe5cfb17STomasz Sapeta 
42*fe5cfb17STomasz Sapeta   // If the application isn't manually localized for the device language then the
43*fe5cfb17STomasz Sapeta   // native `Locale.current` will fallback on using English US
44*fe5cfb17STomasz Sapeta   // [cite](https://stackoverflow.com/questions/48136456/locale-current-reporting-wrong-language-on-device).
45*fe5cfb17STomasz Sapeta   // This method will attempt to return the locale that the device is using regardless of the app,
46*fe5cfb17STomasz Sapeta   // providing better parity across platforms.
getLocalenull47*fe5cfb17STomasz Sapeta   static func getLocale() -> Locale {
48*fe5cfb17STomasz Sapeta     guard let preferredIdentifier = Locale.preferredLanguages.first else {
49*fe5cfb17STomasz Sapeta       return Locale.current
50*fe5cfb17STomasz Sapeta     }
51*fe5cfb17STomasz Sapeta     return Locale(identifier: preferredIdentifier)
52*fe5cfb17STomasz Sapeta   }
53*fe5cfb17STomasz Sapeta   /**
54*fe5cfb17STomasz Sapeta    Maps ios unique identifiers to [BCP 47 calendar types]
55*fe5cfb17STomasz Sapeta    (https://github.com/unicode-org/cldr/blob/main/common/bcp47/calendar.xml)
56*fe5cfb17STomasz Sapeta    */
getUnicodeCalendarIdentifiernull57*fe5cfb17STomasz Sapeta   static func getUnicodeCalendarIdentifier(calendar: Calendar) -> String {
58*fe5cfb17STomasz Sapeta     switch calendar.identifier {
59*fe5cfb17STomasz Sapeta     case .buddhist:
60*fe5cfb17STomasz Sapeta       return "buddhist"
61*fe5cfb17STomasz Sapeta     case .chinese:
62*fe5cfb17STomasz Sapeta       return "chinese"
63*fe5cfb17STomasz Sapeta     case .coptic:
64*fe5cfb17STomasz Sapeta       return "coptic"
65*fe5cfb17STomasz Sapeta     case .ethiopicAmeteAlem:
66*fe5cfb17STomasz Sapeta       return "ethioaa"
67*fe5cfb17STomasz Sapeta     case .ethiopicAmeteMihret:
68*fe5cfb17STomasz Sapeta       return "ethiopic"
69*fe5cfb17STomasz Sapeta     case .gregorian:
70*fe5cfb17STomasz Sapeta       return "gregory"
71*fe5cfb17STomasz Sapeta     case .hebrew:
72*fe5cfb17STomasz Sapeta       return "hebrew"
73*fe5cfb17STomasz Sapeta     case .indian:
74*fe5cfb17STomasz Sapeta       return "indian"
75*fe5cfb17STomasz Sapeta     case .islamic:
76*fe5cfb17STomasz Sapeta       return "islamic"
77*fe5cfb17STomasz Sapeta     case .islamicCivil:
78*fe5cfb17STomasz Sapeta       return "islamic-civil"
79*fe5cfb17STomasz Sapeta     case .islamicTabular:
80*fe5cfb17STomasz Sapeta       return "islamic-tbla"
81*fe5cfb17STomasz Sapeta     case .islamicUmmAlQura:
82*fe5cfb17STomasz Sapeta       return "islamic-umalqura"
83*fe5cfb17STomasz Sapeta     case .japanese:
84*fe5cfb17STomasz Sapeta       return "japanese"
85*fe5cfb17STomasz Sapeta     case .persian:
86*fe5cfb17STomasz Sapeta       return "persian"
87*fe5cfb17STomasz Sapeta     case .republicOfChina:
88*fe5cfb17STomasz Sapeta       return "roc"
89*fe5cfb17STomasz Sapeta     case .iso8601:
90*fe5cfb17STomasz Sapeta       return "iso8601"
91*fe5cfb17STomasz Sapeta     }
92*fe5cfb17STomasz Sapeta   }
93*fe5cfb17STomasz Sapeta 
getLocalesnull94*fe5cfb17STomasz Sapeta   static func getLocales() -> [[String: Any?]] {
95*fe5cfb17STomasz Sapeta     return (Locale.preferredLanguages.isEmpty ? [Locale.current.identifier] : Locale.preferredLanguages)
96*fe5cfb17STomasz Sapeta       .map { languageTag -> [String: Any?] in
97*fe5cfb17STomasz Sapeta         var locale = Locale.init(identifier: languageTag)
98*fe5cfb17STomasz Sapeta         return [
99*fe5cfb17STomasz Sapeta           "languageTag": languageTag,
100*fe5cfb17STomasz Sapeta           "languageCode": locale.languageCode,
101*fe5cfb17STomasz Sapeta           "regionCode": locale.regionCode,
102*fe5cfb17STomasz Sapeta           "textDirection": Locale.characterDirection(forLanguage: languageTag) == .rightToLeft ? "rtl" : "ltr",
103*fe5cfb17STomasz Sapeta           "decimalSeparator": locale.decimalSeparator,
104*fe5cfb17STomasz Sapeta           "digitGroupingSeparator": locale.groupingSeparator,
105*fe5cfb17STomasz Sapeta           "measurementSystem": locale.usesMetricSystem ? "metric" : "us",
106*fe5cfb17STomasz Sapeta           "currencyCode": locale.currencyCode,
107*fe5cfb17STomasz Sapeta           "currencySymbol": locale.currencySymbol
108*fe5cfb17STomasz Sapeta         ]
109*fe5cfb17STomasz Sapeta       }
110*fe5cfb17STomasz Sapeta   }
111*fe5cfb17STomasz Sapeta 
112*fe5cfb17STomasz Sapeta   // https://stackoverflow.com/a/28183182
uses24HourClocknull113*fe5cfb17STomasz Sapeta   static func uses24HourClock() -> Bool {
114*fe5cfb17STomasz Sapeta     let dateFormat = DateFormatter.dateFormat(fromTemplate: "j", options: 0, locale: Locale.current)!
115*fe5cfb17STomasz Sapeta 
116*fe5cfb17STomasz Sapeta     return dateFormat.firstIndex(of: "a") == nil
117*fe5cfb17STomasz Sapeta   }
118*fe5cfb17STomasz Sapeta 
getCalendarsnull119*fe5cfb17STomasz Sapeta   static func getCalendars() -> [[String: Any?]] {
120*fe5cfb17STomasz Sapeta     var calendar = Locale.current.calendar
121*fe5cfb17STomasz Sapeta     return [
122*fe5cfb17STomasz Sapeta       [
123*fe5cfb17STomasz Sapeta         "calendar": getUnicodeCalendarIdentifier(calendar: calendar),
124*fe5cfb17STomasz Sapeta         "timeZone": "\(calendar.timeZone.identifier)",
125*fe5cfb17STomasz Sapeta         "uses24hourClock": uses24HourClock(),
126*fe5cfb17STomasz Sapeta         "firstWeekday": calendar.firstWeekday
127*fe5cfb17STomasz Sapeta       ]
128*fe5cfb17STomasz Sapeta     ]
129*fe5cfb17STomasz Sapeta   }
130*fe5cfb17STomasz Sapeta 
getCurrentLocalizationnull131*fe5cfb17STomasz Sapeta   static func getCurrentLocalization() -> [String: Any?] {
132*fe5cfb17STomasz Sapeta     let locale = getLocale()
133*fe5cfb17STomasz Sapeta     let languageCode = locale.languageCode ?? "en"
134*fe5cfb17STomasz Sapeta     var languageIds = Locale.preferredLanguages
135*fe5cfb17STomasz Sapeta 
136*fe5cfb17STomasz Sapeta     if languageIds.isEmpty {
137*fe5cfb17STomasz Sapeta       languageIds.append("en-US")
138*fe5cfb17STomasz Sapeta     }
139*fe5cfb17STomasz Sapeta     return [
140*fe5cfb17STomasz Sapeta       "currency": locale.currencyCode ?? "USD",
141*fe5cfb17STomasz Sapeta       "decimalSeparator": locale.decimalSeparator ?? ".",
142*fe5cfb17STomasz Sapeta       "digitGroupingSeparator": locale.groupingSeparator ?? ",",
143*fe5cfb17STomasz Sapeta       "isoCurrencyCodes": Locale.isoCurrencyCodes,
144*fe5cfb17STomasz Sapeta       "isMetric": locale.usesMetricSystem,
145*fe5cfb17STomasz Sapeta       "isRTL": Locale.characterDirection(forLanguage: languageCode) == .rightToLeft,
146*fe5cfb17STomasz Sapeta       "locale": languageIds.first,
147*fe5cfb17STomasz Sapeta       "locales": languageIds,
148*fe5cfb17STomasz Sapeta       "region": locale.regionCode ?? "US",
149*fe5cfb17STomasz Sapeta       "timezone": TimeZone.current.identifier
150*fe5cfb17STomasz Sapeta     ]
151*fe5cfb17STomasz Sapeta   }
152*fe5cfb17STomasz Sapeta }
153