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