Source: i18n/lib.js

import PropTypes from 'prop-types';
import Cookies from 'universal-cookie';
import merge from 'lodash.merge';

import '@formatjs/intl-pluralrules/polyfill';
import '@formatjs/intl-pluralrules/locale-data/ar';
import '@formatjs/intl-pluralrules/locale-data/en';
import '@formatjs/intl-pluralrules/locale-data/es';
import '@formatjs/intl-pluralrules/locale-data/fr';
import '@formatjs/intl-pluralrules/locale-data/zh';
import '@formatjs/intl-pluralrules/locale-data/ca';
import '@formatjs/intl-pluralrules/locale-data/he';
import '@formatjs/intl-pluralrules/locale-data/id';
import '@formatjs/intl-pluralrules/locale-data/ko';
import '@formatjs/intl-pluralrules/locale-data/pl';
import '@formatjs/intl-pluralrules/locale-data/pt';
import '@formatjs/intl-pluralrules/locale-data/ru';
import '@formatjs/intl-pluralrules/locale-data/th';
import '@formatjs/intl-pluralrules/locale-data/uk';

import '@formatjs/intl-relativetimeformat/polyfill';
import '@formatjs/intl-relativetimeformat/locale-data/ar';
import '@formatjs/intl-relativetimeformat/locale-data/en';
import '@formatjs/intl-relativetimeformat/locale-data/es';
import '@formatjs/intl-relativetimeformat/locale-data/fr';
import '@formatjs/intl-relativetimeformat/locale-data/zh';
import '@formatjs/intl-relativetimeformat/locale-data/ca';
import '@formatjs/intl-relativetimeformat/locale-data/he';
import '@formatjs/intl-relativetimeformat/locale-data/id';
import '@formatjs/intl-relativetimeformat/locale-data/ko';
import '@formatjs/intl-relativetimeformat/locale-data/pl';
import '@formatjs/intl-relativetimeformat/locale-data/pt';
import '@formatjs/intl-relativetimeformat/locale-data/ru';
import '@formatjs/intl-relativetimeformat/locale-data/th';
import '@formatjs/intl-relativetimeformat/locale-data/uk';

const cookies = new Cookies();
const supportedLocales = [
  'ar', // Arabic
  // NOTE: 'en' is not included in this list intentionally, since it's the fallback.
  'es-419', // Spanish, Latin American
  'fa', // Farsi
  'fa-ir', // Farsi, Iran
  'fr', // French
  'zh-cn', // Chinese, Simplified
  'ca', // Catalan
  'he', // Hebrew
  'id', // Indonesian
  'ko-kr', // Korean (Korea)
  'pl', // Polish
  'pt-br', // Portuguese (Brazil)
  'ru', // Russian
  'th', // Thai
  'uk', // Ukrainian
];
const rtlLocales = [
  'ar', // Arabic
  'he', // Hebrew
  'fa', // Farsi (not currently supported)
  'fa-ir', // Farsi Iran
  'ur', // Urdu (not currently supported)
];

let config = null;
let loggingService = null;
let messages = null;

/**
 * @memberof module:Internationalization
 *
 * Prior versions of react-intl (our primary implementation of the i18n service) included a
 * PropTypes-based 'shape' for its `intl` object.  This has since been removed.  For legacy
 * compatibility, we include an `intlShape` export that is set to PropTypes.object.  Usage of this
 * export is deprecated.
 *
 * @deprecated
 */
export const intlShape = PropTypes.object;

/**
 *
 * @ignore
 * @returns {LoggingService}
 */
export const getLoggingService = () => loggingService;

/**
 * @memberof module:Internationalization
 */
export const LOCALE_TOPIC = 'LOCALE';

/**
 * @memberof module:Internationalization
 */
export const LOCALE_CHANGED = `${LOCALE_TOPIC}.CHANGED`;

/**
 *
 * @memberof module:Internationalization
 * @returns {Cookies}
 */
export function getCookies() {
  return cookies;
}

/**
 * Some of our dependencies function on primary language subtags, rather than full locales.
 * This function strips a locale down to that first subtag.  Depending on the code, this
 * may be 2 or more characters.
 *
 * @param {string} code
 * @memberof module:Internationalization
 */
export function getPrimaryLanguageSubtag(code) {
  return code.split('-')[0];
}

/**
 * Finds the closest supported locale to the one provided.  This is done in three steps:
 *
 * 1. Returning the locale itself if its exact language code is supported.
 * 2. Returning the primary language subtag of the language code if it is supported (ar for ar-eg,
 * for instance).
 * 3. Returning 'en' if neither of the above produce a supported locale.
 *
 * @param {string} locale
 * @returns {string}
 * @memberof module:Internationalization
 */
export function findSupportedLocale(locale) {
  if (messages[locale] !== undefined) {
    return locale;
  }

  if (messages[getPrimaryLanguageSubtag(locale)] !== undefined) {
    return getPrimaryLanguageSubtag(locale);
  }

  return 'en';
}

/**
 * Get the locale from the cookie or, failing that, the browser setting.
 * Gracefully fall back to a more general primary language subtag or to English (en)
 * if we don't support that language.
 *
 * @param {string} locale If a locale is provided, returns the closest supported locale. Optional.
 * @throws An error if i18n has not yet been configured.
 * @returns {string}
 * @memberof module:Internationalization
 */
export function getLocale(locale) {
  if (messages === null) {
    throw new Error('getLocale called before configuring i18n. Call configure with messages first.');
  }
  // 1. Explicit application request
  if (locale !== undefined) {
    return findSupportedLocale(locale);
  }
  // 2. User setting in cookie
  const cookieLangPref = cookies
    .get(config.LANGUAGE_PREFERENCE_COOKIE_NAME);
  if (cookieLangPref) {
    return findSupportedLocale(cookieLangPref.toLowerCase());
  }
  // 3. Browser language (default)
  // Note that some browers prefer upper case for the region part of the locale, while others don't.
  // Thus the toLowerCase, for consistency.
  // https://developer.mozilla.org/en-US/docs/Web/API/NavigatorLanguage/language
  return findSupportedLocale(globalThis.navigator.language.toLowerCase());
}

/**
 * Returns messages for the provided locale, or the user's preferred locale if no argument is
 * provided.
 *
 * @param {string} [locale=getLocale()]
 * @memberof module:Internationalization
 */
export function getMessages(locale = getLocale()) {
  return messages[locale];
}

/**
 * Determines if the provided locale is a right-to-left language.
 *
 * @param {string} locale
 * @memberof module:Internationalization
 */
export function isRtl(locale) {
  return rtlLocales.includes(locale);
}

/**
 * Handles applying the RTL stylesheet and "dir=rtl" attribute to the html tag if the current locale
 * is a RTL language.
 *
 * @memberof module:Internationalization
 */
export function handleRtl() {
  if (isRtl(getLocale())) {
    globalThis.document.getElementsByTagName('html')[0].setAttribute('dir', 'rtl');
  } else {
    globalThis.document.getElementsByTagName('html')[0].setAttribute('dir', 'ltr');
  }
}

const messagesShape = {
  ar: PropTypes.objectOf(PropTypes.string), // Arabic
  en: PropTypes.objectOf(PropTypes.string),
  'es-419': PropTypes.objectOf(PropTypes.string), // Spanish, Latin American
  fr: PropTypes.objectOf(PropTypes.string), // French
  'zh-cn': PropTypes.objectOf(PropTypes.string), // Chinese, Simplified
  ca: PropTypes.objectOf(PropTypes.string), // Catalan
  he: PropTypes.objectOf(PropTypes.string), // Hebrew
  id: PropTypes.objectOf(PropTypes.string), // Indonesian
  'ko-kr': PropTypes.objectOf(PropTypes.string), // Korean (Korea)
  pl: PropTypes.objectOf(PropTypes.string), // Polish
  'pt-br': PropTypes.objectOf(PropTypes.string), // Portuguese (Brazil)
  ru: PropTypes.objectOf(PropTypes.string), // Russian
  th: PropTypes.objectOf(PropTypes.string), // Thai
  uk: PropTypes.objectOf(PropTypes.string), // Ukrainian
};

const optionsShape = {
  config: PropTypes.object.isRequired,
  loggingService: PropTypes.shape({
    logError: PropTypes.func.isRequired,
  }).isRequired,
  messages: PropTypes.oneOfType([
    PropTypes.shape(messagesShape),
    PropTypes.arrayOf(PropTypes.shape(messagesShape)),
  ]).isRequired,
};

/**
 *
 *
 * @param {Object} newMessages
 * @returns {Object}
 * @memberof module:Internationalization
 */
export function mergeMessages(newMessages) {
  const msgs = Array.isArray(newMessages) ? merge({}, ...newMessages) : newMessages;
  messages = merge(messages, msgs);

  return messages;
}

/**
 * Configures the i18n library with messages for your application.
 *
 * Logs a warning if it detects a locale it doesn't expect (as defined by the supportedLocales list
 * above), or if an expected locale is not provided.
 *
 * @param {Object} options
 * @param {LoggingService} options.loggingService
 * @param {Object} options.config
 * @param {Object} options.messages
 * @memberof module:Internationalization
 */
export function configure(options) {
  PropTypes.checkPropTypes(optionsShape, options, 'property', 'i18n');
  // eslint-disable-next-line prefer-destructuring
  loggingService = options.loggingService;
  // eslint-disable-next-line prefer-destructuring
  config = options.config;
  messages = Array.isArray(options.messages) ? merge({}, ...options.messages) : options.messages;

  if (config.ENVIRONMENT !== 'production') {
    Object.keys(messages).forEach((key) => {
      if (supportedLocales.indexOf(key) < 0) {
        console.warn(`Unexpected locale: ${key}`); // eslint-disable-line no-console
      }
    });

    supportedLocales.forEach((key) => {
      if (messages[key] === undefined) {
        console.warn(`Missing locale: ${key}`); // eslint-disable-line no-console
      }
    });
  }

  handleRtl();
}