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    // Prefer locales with region codes as they contain more info about calendar.
122    // They seem to always exist in the list for each locale without region
123    const locales = [...getNavigatorLocales()].sort((a, b) =>
124      a.includes('-') === b.includes('-') ? 0 : a.includes('-') ? -1 : 1
125    );
126    const locale = (locales[0] && typeof Intl !== 'undefined'
127      ? new Intl.Locale(locales[0])
128      : null) as unknown as null | ExtendedLocale;
129    return [
130      {
131        calendar: ((locale?.calendar || locale?.calendars?.[0]) as CalendarIdentifier) || null,
132        timeZone: locale?.timeZone || locale?.timeZones?.[0] || null,
133        uses24hourClock: (locale?.hourCycle || locale?.hourCycles?.[0])?.startsWith('h2') ?? null, //https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/hourCycle
134        firstWeekday: locale?.weekInfo?.firstDay || null,
135      },
136    ];
137  },
138  async getLocalizationAsync(): Promise<Omit<Localization, 'getCalendars' | 'getLocales'>> {
139    const {
140      currency,
141      decimalSeparator,
142      digitGroupingSeparator,
143      isoCurrencyCodes,
144      isMetric,
145      isRTL,
146      locale,
147      locales,
148      region,
149      timezone,
150    } = this;
151    return {
152      currency,
153      decimalSeparator,
154      digitGroupingSeparator,
155      isoCurrencyCodes,
156      isMetric,
157      isRTL,
158      locale,
159      locales,
160      region,
161      timezone,
162    };
163  },
164};
165