1/* eslint-env browser */ 2import { Platform } from 'expo-modules-core'; 3import * as rtlDetect from 'rtl-detect'; 4 5import { Localization, Calendar, Locale, CalendarIdentifier } from './Localization.types'; 6 7const getNavigatorLocales = () => { 8 return Platform.isDOMAvailable ? navigator.languages || [navigator.language] : []; 9}; 10 11type ExtendedLocale = Intl.Locale & 12 // typescript definitions for navigator language don't include some modern Intl properties 13 Partial<{ 14 textInfo: { direction: 'ltr' | 'rtl' }; 15 timeZones: string[]; 16 weekInfo: { firstDay: number }; 17 hourCycles: string[]; 18 timeZone: string; 19 calendars: string[]; 20 }>; 21 22export default { 23 get currency(): string | null { 24 // TODO: Add support 25 return null; 26 }, 27 get decimalSeparator(): string { 28 return (1.1).toLocaleString().substring(1, 2); 29 }, 30 get digitGroupingSeparator(): string { 31 const value = (1000).toLocaleString(); 32 return value.length === 5 ? value.substring(1, 2) : ''; 33 }, 34 get isRTL(): boolean { 35 return rtlDetect.isRtlLang(this.locale) ?? false; 36 }, 37 get isMetric(): boolean { 38 const { region } = this; 39 switch (region) { 40 case 'US': // USA 41 case 'LR': // Liberia 42 case 'MM': // Myanmar 43 return false; 44 } 45 return true; 46 }, 47 get locale(): string { 48 if (!Platform.isDOMAvailable) { 49 return ''; 50 } 51 const locale = 52 navigator.language || 53 navigator['systemLanguage'] || 54 navigator['browserLanguage'] || 55 navigator['userLanguage'] || 56 this.locales[0]; 57 return locale; 58 }, 59 get locales(): string[] { 60 if (!Platform.isDOMAvailable) { 61 return []; 62 } 63 const { languages = [] } = navigator; 64 return Array.from(languages); 65 }, 66 get timezone(): string { 67 const defaultTimeZone = 'Etc/UTC'; 68 if (typeof Intl === 'undefined') { 69 return defaultTimeZone; 70 } 71 return Intl.DateTimeFormat().resolvedOptions().timeZone || defaultTimeZone; 72 }, 73 get isoCurrencyCodes(): string[] { 74 // TODO(Bacon): Add this - very low priority 75 return []; 76 }, 77 get region(): string | null { 78 // There is no way to obtain the current region, as is possible on native. 79 // Instead, use the country-code from the locale when possible (e.g. "en-US"). 80 const { locale } = this; 81 const [, ...suffixes] = typeof locale === 'string' ? locale.split('-') : []; 82 for (const suffix of suffixes) { 83 if (suffix.length === 2) { 84 return suffix.toUpperCase(); 85 } 86 } 87 return null; 88 }, 89 90 getLocales(): Locale[] { 91 const locales = getNavigatorLocales(); 92 return locales?.map((languageTag) => { 93 // TextInfo is an experimental API that is not available in all browsers. 94 // We might want to consider using a locale lookup table instead. 95 const locale = 96 typeof Intl !== 'undefined' 97 ? (new Intl.Locale(languageTag) as unknown as ExtendedLocale) 98 : { region: null, textInfo: null, language: null }; 99 const { region, textInfo, language } = locale; 100 101 // Properties added only for compatibility with native, use `toLocaleString` instead. 102 const digitGroupingSeparator = 103 Array.from((10000).toLocaleString(languageTag)).filter((c) => c > '9' || c < '0')[0] || 104 null; // using 1e5 instead of 1e4 since for some locales (like pl-PL) 1e4 does not use digit grouping 105 const decimalSeparator = (1.1).toLocaleString(languageTag).substring(1, 2); 106 107 return { 108 languageTag, 109 languageCode: language || languageTag.split('-')[0] || 'en', 110 textDirection: (textInfo?.direction as 'ltr' | 'rtl') || null, 111 digitGroupingSeparator, 112 decimalSeparator, 113 measurementSystem: null, 114 currencyCode: null, 115 currencySymbol: null, 116 regionCode: region || null, 117 }; 118 }); 119 }, 120 getCalendars(): Calendar[] { 121 const locale = ((typeof Intl !== 'undefined' 122 ? Intl.DateTimeFormat().resolvedOptions() 123 : null) ?? null) as unknown as null | ExtendedLocale; 124 return [ 125 { 126 calendar: ((locale?.calendar || locale?.calendars?.[0]) as CalendarIdentifier) || null, 127 timeZone: locale?.timeZone || locale?.timeZones?.[0] || null, 128 uses24hourClock: (locale?.hourCycle || locale?.hourCycles?.[0])?.startsWith('h2') ?? null, //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/hourCycle 129 firstWeekday: locale?.weekInfo?.firstDay || null, 130 }, 131 ]; 132 }, 133 async getLocalizationAsync(): Promise<Omit<Localization, 'getCalendars' | 'getLocales'>> { 134 const { 135 currency, 136 decimalSeparator, 137 digitGroupingSeparator, 138 isoCurrencyCodes, 139 isMetric, 140 isRTL, 141 locale, 142 locales, 143 region, 144 timezone, 145 } = this; 146 return { 147 currency, 148 decimalSeparator, 149 digitGroupingSeparator, 150 isoCurrencyCodes, 151 isMetric, 152 isRTL, 153 locale, 154 locales, 155 region, 156 timezone, 157 }; 158 }, 159}; 160