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