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