UNPKG

6.07 kBPlain TextView Raw
1/* eslint-env browser */
2import { Platform, Subscription } 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
22const WEB_LANGUAGE_CHANGE_EVENT = 'languagechange';
23// https://wisevoter.com/country-rankings/countries-that-use-fahrenheit/
24const USES_FAHRENHEIT = [
25 'AG',
26 'BZ',
27 'VG',
28 'FM',
29 'MH',
30 'MS',
31 'KN',
32 'BS',
33 'CY',
34 'TC',
35 'US',
36 'LR',
37 'PW',
38 'KY',
39];
40
41export function addLocaleListener(listener: (event) => void): Subscription {
42 addEventListener(WEB_LANGUAGE_CHANGE_EVENT, listener);
43 return {
44 remove: () => removeEventListener(WEB_LANGUAGE_CHANGE_EVENT, listener),
45 };
46}
47
48export function addCalendarListener(listener: (event) => void): Subscription {
49 addEventListener(WEB_LANGUAGE_CHANGE_EVENT, listener);
50 return {
51 remove: () => removeEventListener(WEB_LANGUAGE_CHANGE_EVENT, listener),
52 };
53}
54
55export function removeSubscription(subscription: Subscription) {
56 subscription.remove();
57}
58
59export default {
60 get currency(): string | null {
61 // TODO: Add support
62 return null;
63 },
64 get decimalSeparator(): string {
65 return (1.1).toLocaleString().substring(1, 2);
66 },
67 get digitGroupingSeparator(): string {
68 const value = (1000).toLocaleString();
69 return value.length === 5 ? value.substring(1, 2) : '';
70 },
71 get isRTL(): boolean {
72 return rtlDetect.isRtlLang(this.locale) ?? false;
73 },
74 get isMetric(): boolean {
75 const { region } = this;
76 switch (region) {
77 case 'US': // USA
78 case 'LR': // Liberia
79 case 'MM': // Myanmar
80 return false;
81 }
82 return true;
83 },
84 get locale(): string {
85 if (!Platform.isDOMAvailable) {
86 return '';
87 }
88 const locale =
89 navigator.language ||
90 navigator['systemLanguage'] ||
91 navigator['browserLanguage'] ||
92 navigator['userLanguage'] ||
93 this.locales[0];
94 return locale;
95 },
96 get locales(): string[] {
97 if (!Platform.isDOMAvailable) {
98 return [];
99 }
100 const { languages = [] } = navigator;
101 return Array.from(languages);
102 },
103 get timezone(): string {
104 const defaultTimeZone = 'Etc/UTC';
105 if (typeof Intl === 'undefined') {
106 return defaultTimeZone;
107 }
108 return Intl.DateTimeFormat().resolvedOptions().timeZone || defaultTimeZone;
109 },
110 get isoCurrencyCodes(): string[] {
111 // TODO(Bacon): Add this - very low priority
112 return [];
113 },
114 get region(): string | null {
115 // There is no way to obtain the current region, as is possible on native.
116 // Instead, use the country-code from the locale when possible (e.g. "en-US").
117 const { locale } = this;
118 const [, ...suffixes] = typeof locale === 'string' ? locale.split('-') : [];
119 for (const suffix of suffixes) {
120 if (suffix.length === 2) {
121 return suffix.toUpperCase();
122 }
123 }
124 return null;
125 },
126
127 getLocales(): Locale[] {
128 const locales = getNavigatorLocales();
129 return locales?.map((languageTag) => {
130 // TextInfo is an experimental API that is not available in all browsers.
131 // We might want to consider using a locale lookup table instead.
132 const locale =
133 typeof Intl !== 'undefined'
134 ? (new Intl.Locale(languageTag) as unknown as ExtendedLocale)
135 : { region: null, textInfo: null, language: null };
136 const { region, textInfo, language } = locale;
137
138 // Properties added only for compatibility with native, use `toLocaleString` instead.
139 const digitGroupingSeparator =
140 Array.from((10000).toLocaleString(languageTag)).filter((c) => c > '9' || c < '0')[0] ||
141 null; // using 1e5 instead of 1e4 since for some locales (like pl-PL) 1e4 does not use digit grouping
142 const decimalSeparator = (1.1).toLocaleString(languageTag).substring(1, 2);
143 const temperatureUnit = region ? regionToTemperatureUnit(region) : null;
144
145 return {
146 languageTag,
147 languageCode: language || languageTag.split('-')[0] || 'en',
148 textDirection: (textInfo?.direction as 'ltr' | 'rtl') || null,
149 digitGroupingSeparator,
150 decimalSeparator,
151 measurementSystem: null,
152 currencyCode: null,
153 currencySymbol: null,
154 regionCode: region || null,
155 temperatureUnit,
156 };
157 });
158 },
159 getCalendars(): Calendar[] {
160 const locale = ((typeof Intl !== 'undefined'
161 ? Intl.DateTimeFormat().resolvedOptions()
162 : null) ?? null) as unknown as null | ExtendedLocale;
163 return [
164 {
165 calendar: ((locale?.calendar || locale?.calendars?.[0]) as CalendarIdentifier) || null,
166 timeZone: locale?.timeZone || locale?.timeZones?.[0] || null,
167 uses24hourClock: (locale?.hourCycle || locale?.hourCycles?.[0])?.startsWith('h2') ?? null, //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/hourCycle
168 firstWeekday: locale?.weekInfo?.firstDay || null,
169 },
170 ];
171 },
172
173 async getLocalizationAsync(): Promise<Omit<Localization, 'getCalendars' | 'getLocales'>> {
174 const {
175 currency,
176 decimalSeparator,
177 digitGroupingSeparator,
178 isoCurrencyCodes,
179 isMetric,
180 isRTL,
181 locale,
182 locales,
183 region,
184 timezone,
185 } = this;
186 return {
187 currency,
188 decimalSeparator,
189 digitGroupingSeparator,
190 isoCurrencyCodes,
191 isMetric,
192 isRTL,
193 locale,
194 locales,
195 region,
196 timezone,
197 };
198 },
199};
200
201function regionToTemperatureUnit(region: string) {
202 return USES_FAHRENHEIT.includes(region) ? 'fahrenheit' : 'celsius';
203}
204
\No newline at end of file