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