<?php
/**
 * @link https://craftcms.com/
 * @copyright Copyright (c) Pixel & Tonic, Inc.
 * @license https://craftcms.github.io/license/
 */

namespace craft\i18n;

use Craft;
use craft\helpers\ArrayHelper;
use craft\helpers\Localization;
use DateTime;
use IntlDateFormatter;
use NumberFormatter;
use yii\base\BaseObject;
use yii\base\Exception;
use yii\base\InvalidArgumentException;
use yii\base\InvalidConfigException;
use yii\helpers\FormatConverter;

/**
 * Stores locale info.
 *
 * @property string $displayName The locale’s display name.
 * @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
 * @since 3.0.0
 */
class Locale extends BaseObject
{
    /**
     * @var int Positive prefix.
     */
    const ATTR_POSITIVE_PREFIX = 0;

    /**
     * @var int Positive suffix.
     */
    const ATTR_POSITIVE_SUFFIX = 1;

    /**
     * @var int Negative prefix.
     */
    const ATTR_NEGATIVE_PREFIX = 2;

    /**
     * @var int Negative suffix.
     */
    const ATTR_NEGATIVE_SUFFIX = 3;

    /**
     * @var int The character used to pad to the format width.
     */
    const ATTR_PADDING_CHARACTER = 4;

    /**
     * @var int The ISO currency code.
     */
    const ATTR_CURRENCY_CODE = 5;

    /**
     * @var int The default rule set. This is only available with rule-based
     * formatters.
     */
    const ATTR_DEFAULT_RULESET = 6;

    /**
     * @var int The public rule sets. This is only available with rule-based
     * formatters. This is a read-only attribute. The public rulesets are
     * returned as a single string, with each ruleset name delimited by ';'
     * (semicolon).
     */
    const ATTR_PUBLIC_RULESETS = 7;

    /**
     * @var int Decimal style
     */
    const STYLE_DECIMAL = 1;

    /**
     * @var int Currency style
     */
    const STYLE_CURRENCY = 2;

    /**
     * @var int Percent style
     */
    const STYLE_PERCENT = 3;

    /**
     * @var int Scientific style
     */
    const STYLE_SCIENTIFIC = 4;

    /**
     * @var int The decimal separator.
     */
    const SYMBOL_DECIMAL_SEPARATOR = 0;

    /**
     * @var int The grouping separator.
     */
    const SYMBOL_GROUPING_SEPARATOR = 1;

    /**
     * @var int The pattern separator.
     */
    const SYMBOL_PATTERN_SEPARATOR = 2;

    /**
     * @var int The percent sign.
     */
    const SYMBOL_PERCENT = 3;

    /**
     * @var int Zero.
     */
    const SYMBOL_ZERO_DIGIT = 4;

    /**
     * @var int Character representing a digit in the pattern.
     */
    const SYMBOL_DIGIT = 5;

    /**
     * @var int The minus sign.
     */
    const SYMBOL_MINUS_SIGN = 6;

    /**
     * @var int The plus sign.
     */
    const SYMBOL_PLUS_SIGN = 7;

    /**
     * @var int The currency symbol.
     */
    const SYMBOL_CURRENCY = 8;

    /**
     * @var int The international currency symbol.
     */
    const SYMBOL_INTL_CURRENCY = 9;

    /**
     * @var int The monetary separator.
     */
    const SYMBOL_MONETARY_SEPARATOR = 10;

    /**
     * @var int The exponential symbol.
     */
    const SYMBOL_EXPONENTIAL = 11;

    /**
     * @var int Per mill symbol.
     */
    const SYMBOL_PERMILL = 12;

    /**
     * @var int Escape padding character.
     */
    const SYMBOL_PAD_ESCAPE = 13;

    /**
     * @var int Infinity symbol.
     */
    const SYMBOL_INFINITY = 14;

    /**
     * @var int Not-a-number symbol.
     */
    const SYMBOL_NAN = 15;

    /**
     * @var int Significant digit symbol.
     */
    const SYMBOL_SIGNIFICANT_DIGIT = 16;

    /**
     * @var int The monetary grouping separator.
     */
    const SYMBOL_MONETARY_GROUPING_SEPARATOR = 17;

    /**
     * @var string The abbreviated date/time format.
     */
    const LENGTH_ABBREVIATED = 'abbreviated';

    /**
     * @var string The short date/time format.
     */
    const LENGTH_SHORT = 'short';

    /**
     * @var string The medium date/time format.
     */
    const LENGTH_MEDIUM = 'medium';

    /**
     * @var string The long date/time format.
     */
    const LENGTH_LONG = 'long';

    /**
     * @var string The full date/time format.
     */
    const LENGTH_FULL = 'full';

    /**
     * @var string ICU format
     */
    const FORMAT_ICU = 'icu';

    /**
     * @var string PHP format
     */
    const FORMAT_PHP = 'php';

    /**
     * @var string jQuery UI format
     */
    const FORMAT_JUI = 'jui';

    /**
     * @var array The languages that use RTL orientation.
     */
    private static $_rtlLanguages = [
        'ar',
        'fa',
        'he',
        'ps',
        'ur',
    ];

    /**
     * @var string|null The locale ID.
     */
    public $id;

    /**
     * @var array|null The configured locale data, used if the [PHP intl extension](https://php.net/manual/en/book.intl.php) isn’t loaded.
     */
    private $_data;

    /**
     * @var Formatter|null The locale's formatter.
     */
    private $_formatter;

    /**
     * Constructor.
     *
     * @param string $id The locale ID.
     * @param array $config Name-value pairs that will be used to initialize the object properties.
     * @throws InvalidArgumentException If $id is an unsupported locale.
     */
    public function __construct(string $id, array $config = [])
    {
        if (strpos($id, '_') !== false) {
            $id = str_replace('_', '-', $id);
        }

        $this->id = $id;

        if (!Craft::$app->getI18n()->getIsIntlLoaded()) {
            $this->_data = Localization::localeData($this->id);

            if ($this->_data === null && ($languageId = $this->getLanguageID()) !== $this->id) {
                $this->_data = Localization::localeData($languageId);
            }

            if ($this->_data === null) {
                $this->_data = Localization::localeData('en-US');
            }
        }

        parent::__construct($config);
    }

    /**
     * Use the ID as the string representation of locales.
     *
     * @return string
     */
    public function __toString(): string
    {
        return (string)$this->id;
    }

    /**
     * Returns this locale’s language ID.
     *
     * @return string This locale’s language ID.
     */
    public function getLanguageID(): string
    {
        if (($pos = strpos($this->id, '-')) !== false) {
            return substr($this->id, 0, $pos);
        }

        return $this->id;
    }

    /**
     * Returns this locale’s script ID.
     *
     * A script ID consists of only the last four characters after a dash in the locale ID.
     *
     * @return string|null The locale’s script ID, if it has one.
     */
    public function getScriptID()
    {
        // Find sub tags
        if (strpos($this->id, '-') !== false) {
            $subTag = explode('-', $this->id);

            // Script sub tags can be distinguished from territory sub tags by length
            if (strlen($subTag[1]) === 4) {
                return $subTag[1];
            }
        }

        return null;
    }

    /**
     * Returns this locale’s territory ID.
     *
     * A territory ID consists of only the last two to three letter or digits after a dash in the locale ID.
     *
     * @return string|null The locale’s territory ID, if it has one.
     */
    public function getTerritoryID()
    {
        // Find sub tags
        if (strpos($this->id, '-') !== false) {
            $subTag = explode('-', $this->id);

            // Territory sub tags can be distinguished from script sub tags by length
            if (isset($subTag[2]) && strlen($subTag[2]) < 4) {
                return $subTag[2];
            }

            if (strlen($subTag[1]) < 4) {
                return $subTag[1];
            }
        }

        return null;
    }

    /**
     * Returns the locale name in a given language.
     *
     * @param string|null $inLocale
     * @return string
     */
    public function getDisplayName(string $inLocale = null): string
    {
        // If no target locale is specified, default to this locale
        if ($inLocale === null) {
            $inLocale = $this->id;
        }

        if (Craft::$app->getI18n()->getIsIntlLoaded()) {
            return \Locale::getDisplayName($this->id, $inLocale);
        }

        if ($inLocale === $this->id) {
            $locale = $this;
        } else {
            try {
                $locale = new Locale($inLocale);
            } catch (InvalidConfigException $e) {
                $locale = $this;
            }
        }

        if (isset($locale->_data['localeDisplayNames'][$this->id])) {
            return $locale->_data['localeDisplayNames'][$this->id];
        }

        // Try just the language
        $languageId = $this->getLanguageID();

        if ($languageId !== $this->id && isset($locale->_data['localeDisplayNames'][$languageId])) {
            return $locale->_data['localeDisplayNames'][$languageId];
        }

        if ($locale !== $this) {
            // Try again with this locale
            return $this->getDisplayName($this->id);
        }

        return $this->id;
    }

    /**
     * Returns the language’s orientation (ltr or rtl).
     *
     * @return string The language’s orientation.
     */
    public function getOrientation(): string
    {
        if (in_array($this->getLanguageID(), self::$_rtlLanguages, true)) {
            return 'rtl';
        }

        return 'ltr';
    }

    /**
     * Returns a [[Formatter]] for this locale.
     *
     * @return Formatter A formatter for this locale.
     */
    public function getFormatter(): Formatter
    {
        if ($this->_formatter === null) {
            $config = [
                'class' => Formatter::class,
                'locale' => $this->id,
                'sizeFormatBase' => 1000,
                'dateTimeFormats' => [
                    self::LENGTH_SHORT => [
                        'date' => $this->getDateFormat(self::LENGTH_SHORT),
                        'time' => $this->getTimeFormat(self::LENGTH_SHORT),
                        'datetime' => $this->getDateTimeFormat(self::LENGTH_SHORT),
                    ],
                    self::LENGTH_MEDIUM => [
                        'date' => $this->getDateFormat(self::LENGTH_MEDIUM),
                        'time' => $this->getTimeFormat(self::LENGTH_MEDIUM),
                        'datetime' => $this->getDateTimeFormat(self::LENGTH_MEDIUM),
                    ],
                    self::LENGTH_LONG => [
                        'date' => $this->getDateFormat(self::LENGTH_LONG),
                        'time' => $this->getTimeFormat(self::LENGTH_LONG),
                        'datetime' => $this->getDateTimeFormat(self::LENGTH_LONG),
                    ],
                    self::LENGTH_FULL => [
                        'date' => $this->getDateFormat(self::LENGTH_FULL),
                        'time' => $this->getTimeFormat(self::LENGTH_FULL),
                        'datetime' => $this->getDateTimeFormat(self::LENGTH_FULL),
                    ],
                ],
            ];

            if (!Craft::$app->getI18n()->getIsIntlLoaded()) {
                $config['standAloneMonthNames'] = $this->_data['standAloneMonthNames'];
                $config['monthNames'] = $this->_data['monthNames'];
                $config['standAloneWeekDayNames'] = $this->_data['standAloneWeekDayNames'];
                $config['weekDayNames'] = $this->_data['weekDayNames'];
                $config['amName'] = $this->_data['amName'];
                $config['pmName'] = $this->_data['pmName'];
                $config['currencySymbols'] = $this->_data['currencySymbols'];
                $config['decimalSeparator'] = $this->getNumberSymbol(self::SYMBOL_DECIMAL_SEPARATOR);
                $config['thousandSeparator'] = $this->getNumberSymbol(self::SYMBOL_GROUPING_SEPARATOR);
                $config['currencyCode'] = $this->getNumberSymbol(self::SYMBOL_INTL_CURRENCY);
            }

            $this->_formatter = Craft::createObject($config);
        }

        return $this->_formatter;
    }

    // Date/Time Formatting
    // -------------------------------------------------------------------------

    /**
     * Returns the localized ICU date format.
     *
     * @param string|null $length The format length that should be returned. Values: Locale::LENGTH_SHORT, ::MEDIUM, ::LONG, ::FULL
     * @param string $format The format type that should be returned. Values: Locale::FORMAT_ICU (default), ::FORMAT_PHP, ::FORMAT_JUI
     * @return string The localized ICU date format.
     */
    public function getDateFormat(string $length = null, string $format = self::FORMAT_ICU): string
    {
        return $this->_getDateTimeFormat($length, true, false, $format);
    }

    /**
     * Returns the localized ICU time format.
     *
     * @param string|null $length The format length that should be returned. Values: Locale::LENGTH_SHORT, ::MEDIUM, ::LONG, ::FULL
     * @param string $format The format type that should be returned. Values: Locale::FORMAT_ICU (default), ::FORMAT_PHP, ::FORMAT_JUI
     * @return string The localized ICU time format.
     */
    public function getTimeFormat(string $length = null, string $format = self::FORMAT_ICU): string
    {
        return $this->_getDateTimeFormat($length, false, true, $format);
    }

    /**
     * Returns the localized ICU date + time format.
     *
     * @param string|null $length The format length that should be returned. Values: Locale::LENGTH_SHORT, ::MEDIUM, ::LONG, ::FULL
     * @param string $format The format type that should be returned. Values: Locale::FORMAT_ICU (default), ::FORMAT_PHP, ::FORMAT_JUI
     * @return string The localized ICU date + time format.
     */
    public function getDateTimeFormat(string $length = null, string $format = self::FORMAT_ICU): string
    {
        return $this->_getDateTimeFormat($length, true, true, $format);
    }

    /**
     * Sets the date/time formats used by this locale.
     *
     * You should pass an array with keys `short`, `medium`, `long`, and/or `full`, set to sub-arrays
     * with keys `date`, `time`, and/or `datetime`, which should be set to the date/time formats using
     * [ICU date/time formatting syntax](https://unicode-org.github.io/icu/userguide/format_parse/datetime/#datetime-format-syntax).
     *
     * For example:
     *
     * ```php
     * [
     *     'short' => [
     *         'date' => 'd/M/y',
     *         'time' => 'HH:mm',
     *         'datetime' => 'd/M/y HH:mm',
     *     ],
     * ]
     * ```
     *
     * @param array $formats
     * @return void
     * @since 3.6.0
     */
    public function setDateTimeFormats(array $formats): void
    {
        $this->_data['dateTimeFormats'] = ArrayHelper::merge($this->_data['dateTimeFormats'] ?? [], $formats);
    }

    /**
     * Returns a localized month name.
     *
     * @param int $month The month to return (1-12).
     * @param string|null $length The format length that should be returned. Values: Locale::LENGTH_ABBREVIATED, ::MEDIUM, ::FULL
     * @param bool $standAlone Whether to return the "stand alone" month name.
     * @return string The localized month name.
     */
    public function getMonthName(int $month, string $length = null, bool $standAlone = true): string
    {
        if ($length === null) {
            $length = self::LENGTH_FULL;
        }

        if (Craft::$app->getI18n()->getIsIntlLoaded()) {
            $formatter = new IntlDateFormatter($this->id, IntlDateFormatter::NONE, IntlDateFormatter::NONE);

            switch ($length) {
                case self::LENGTH_ABBREVIATED:
                    $formatter->setPattern($standAlone ? 'LLLLL' : 'MMMMM');
                    break; // S
                case self::LENGTH_SHORT:
                case self::LENGTH_MEDIUM:
                    $formatter->setPattern($standAlone ? 'LLL' : 'MMM');
                    break; // Sep
                default:
                    $formatter->setPattern($standAlone ? 'LLLL' : 'MMMM');
                    break; // September
            }

            return $formatter->format(new DateTime('1970-' . sprintf('%02d', $month) . '-01'));
        }

        $which = $standAlone ? 'standAloneMonthNames' : 'monthNames';

        switch ($length) {
            case self::LENGTH_ABBREVIATED:
                return $this->_data[$which]['abbreviated'][$month - 1];
                break; // S
            case self::LENGTH_SHORT:
            case self::LENGTH_MEDIUM:
                return $this->_data[$which]['medium'][$month - 1];
                break; // Sep
            default:
                return $this->_data[$which]['full'][$month - 1];
                break; // September
        }
    }

    /**
     * Returns all of the localized month names.
     *
     * @param string|null $length The format length that should be returned. Values: Locale::LENGTH_ABBREVIATED, ::MEDIUM, ::FULL
     * @param bool $standAlone Whether to return the "stand alone" month names.
     * @return array The localized month names.
     */
    public function getMonthNames(string $length = null, bool $standAlone = true): array
    {
        $monthNames = [];

        for ($month = 1; $month <= 12; $month++) {
            $monthNames[] = $this->getMonthName($month, $length, $standAlone);
        }

        return $monthNames;
    }

    /**
     * Returns a localized day of the week name.
     *
     * @param int $day The day of the week to return (0-6), where 0 stands for Sunday.
     * @param string|null $length The format length that should be returned. Values: Locale::LENGTH_ABBREVIATED, ::SHORT, ::MEDIUM, ::FULL
     * @param bool $standAlone Whether to return the "stand alone" day of the week name.
     * @return string The localized day of the week name.
     */
    public function getWeekDayName(int $day, string $length = null, bool $standAlone = true): string
    {
        if ($length === null) {
            $length = self::LENGTH_FULL;
        }

        if (Craft::$app->getI18n()->getIsIntlLoaded()) {
            $formatter = new IntlDateFormatter($this->id, IntlDateFormatter::NONE, IntlDateFormatter::NONE);

            switch ($length) {
                case self::LENGTH_ABBREVIATED:
                    // T
                    $formatter->setPattern($standAlone ? 'ccccc' : 'eeeee');
                    break;
                case self::LENGTH_SHORT:
                    // Tu
                    $formatter->setPattern($standAlone ? 'cccccc' : 'eeeeee');
                    break;
                case self::LENGTH_MEDIUM:
                    // Tue
                    $formatter->setPattern($standAlone ? 'ccc' : 'eee');
                    break;
                default:
                    // Tuesday
                    $formatter->setPattern($standAlone ? 'cccc' : 'eeee');
                    break;
            }

            // 1970-01-04 => Sunday (0 + 4)
            // 1970-01-05 => Monday (1 + 4)
            // 1970-01-06 => Tuesday (2 + 4)
            // 1970-01-07 => Wednesday (3 + 4)
            // 1970-01-08 => Thursday (4 + 4)
            // 1970-01-09 => Friday (5 + 4)
            // 1970-01-10 => Saturday (6 + 4)
            return $formatter->format(new DateTime('1970-01-' . sprintf('%02d', $day + 4)));
        }

        $which = $standAlone ? 'standAloneWeekDayNames' : 'weekDayNames';

        switch ($length) {
            case self::LENGTH_ABBREVIATED:
                // T
                return $this->_data[$which]['abbreviated'][$day];
            case self::LENGTH_SHORT:
                // Tu
                return $this->_data[$which]['short'][$day];
            case self::LENGTH_MEDIUM:
                // Tue
                return $this->_data[$which]['medium'][$day];
            default:
                // Tuesday
                return $this->_data[$which]['full'][$day];
        }
    }

    /**
     * Returns all of the localized day of the week names.
     *
     * @param string|null $length The format length that should be returned. Values: Locale::LENGTH_ABBREVIATED, ::MEDIUM, ::FULL
     * @param bool $standAlone Whether to return the "stand alone" day of the week names.
     * @return array The localized day of the week names.
     */
    public function getWeekDayNames(string $length = null, bool $standAlone = true): array
    {
        $weekDayNames = [];

        for ($day = 0; $day <= 6; $day++) {
            $weekDayNames[] = $this->getWeekDayName($day, $length, $standAlone);
        }

        return $weekDayNames;
    }

    /**
     * Returns the "AM" name for this locale.
     *
     * @return string The "AM" name.
     */
    public function getAMName(): string
    {
        if (Craft::$app->getI18n()->getIsIntlLoaded()) {
            return $this->getFormatter()->asDate(new DateTime('00:00'), 'a');
        }

        return $this->_data['amName'];
    }

    /**
     * Returns the "PM" name for this locale.
     *
     * @return string The "PM" name.
     */
    public function getPMName(): string
    {
        if (Craft::$app->getI18n()->getIsIntlLoaded()) {
            return $this->getFormatter()->asDate(new DateTime('12:00'), 'a');
        }

        return $this->_data['pmName'];
    }

    // Text Attributes and Symbols
    // -------------------------------------------------------------------------

    /**
     * Returns a text attribute used by this locale.
     *
     * @param int $attribute The attribute to return. Values: Locale::
     * @return string|null The attribute.
     */
    public function getTextAttribute(int $attribute)
    {
        if (Craft::$app->getI18n()->getIsIntlLoaded()) {
            $formatter = new NumberFormatter($this->id, NumberFormatter::DECIMAL);

            return $formatter->getTextAttribute($attribute);
        }

        switch ($attribute) {
            case self::ATTR_POSITIVE_PREFIX:
                return $this->_data['textAttributes']['positivePrefix'];
            case self::ATTR_POSITIVE_SUFFIX:
                return $this->_data['textAttributes']['positiveSuffix'];
            case self::ATTR_NEGATIVE_PREFIX:
                return $this->_data['textAttributes']['negativePrefix'];
            case self::ATTR_NEGATIVE_SUFFIX:
                return $this->_data['textAttributes']['negativeSuffix'];
            case self::ATTR_PADDING_CHARACTER:
                return $this->_data['textAttributes']['paddingCharacter'];
            case self::ATTR_CURRENCY_CODE:
                return $this->_data['textAttributes']['currencyCode'];
            case self::ATTR_DEFAULT_RULESET:
                return $this->_data['textAttributes']['defaultRuleset'];
            case self::ATTR_PUBLIC_RULESETS:
                return $this->_data['textAttributes']['publicRulesets'];
        }

        return null;
    }

    /**
     * Returns a number pattern used by this locale.
     *
     * @param int $style The pattern style to return.
     * Accepted values: Locale::STYLE_DECIMAL, ::STYLE_CURRENCY, ::STYLE_PERCENT, ::STYLE_SCIENTIFIC
     * @return string|null The pattern
     */
    public function getNumberPattern(int $style)
    {
        if (Craft::$app->getI18n()->getIsIntlLoaded()) {
            $formatter = new NumberFormatter($this->id, $style);

            return $formatter->getPattern();
        }

        switch ($style) {
            case self::STYLE_DECIMAL:
                return $this->_data['numberPatterns']['decimal'];
            case self::STYLE_CURRENCY:
                return $this->_data['numberPatterns']['currency'];
            case self::STYLE_PERCENT:
                return $this->_data['numberPatterns']['percent'];
            case self::STYLE_SCIENTIFIC:
                return $this->_data['numberPatterns']['scientific'];
        }

        return null;
    }

    /**
     * Returns a number symbol used by this locale.
     *
     * @param int $symbol The symbol to return. Accepted values: Locale::SYMBOL_DECIMAL_SEPARATOR,
     * ::SYMBOL_GROUPING_SEPARATOR, ::SYMBOL_PATTERN_SEPARATOR, ::SYMBOL_PERCENT, ::SYMBOL_ZERO_DIGIT,
     * ::SYMBOL_DIGIT, ::SYMBOL_MINUS_SIGN, ::SYMBOL_PLUS_SIGN, ::SYMBOL_CURRENCY, ::SYMBOL_INTL_CURRENCY,
     * ::SYMBOL_MONETARY_SEPARATOR, ::SYMBOL_EXPONENTIAL, ::SYMBOL_PERMILL, ::SYMBOL_PAD_ESCAPE,
     * ::SYMBOL_INFINITY, ::SYMBOL_NAN, ::SYMBOL_SIGNIFICANT_DIGIT, ::SYMBOL_MONETARY_GROUPING_SEPARATOR
     * @return string|null The symbol.
     */
    public function getNumberSymbol(int $symbol)
    {
        if (Craft::$app->getI18n()->getIsIntlLoaded()) {
            $formatter = new NumberFormatter($this->id, NumberFormatter::DECIMAL);

            return $formatter->getSymbol($symbol);
        }

        switch ($symbol) {
            case self::SYMBOL_DECIMAL_SEPARATOR:
                return $this->_data['numberSymbols']['decimalSeparator'];
            case self::SYMBOL_GROUPING_SEPARATOR:
                return $this->_data['numberSymbols']['groupingSeparator'];
            case self::SYMBOL_PATTERN_SEPARATOR:
                return $this->_data['numberSymbols']['patternSeparator'];
            case self::SYMBOL_PERCENT:
                return $this->_data['numberSymbols']['percent'];
            case self::SYMBOL_ZERO_DIGIT:
                return $this->_data['numberSymbols']['zeroDigit'];
            case self::SYMBOL_DIGIT:
                return $this->_data['numberSymbols']['digit'];
            case self::SYMBOL_MINUS_SIGN:
                return $this->_data['numberSymbols']['minusSign'];
            case self::SYMBOL_PLUS_SIGN:
                return $this->_data['numberSymbols']['plusSign'];
            case self::SYMBOL_CURRENCY:
                return $this->_data['numberSymbols']['currency'];
            case self::SYMBOL_INTL_CURRENCY:
                return $this->_data['numberSymbols']['intlCurrency'];
            case self::SYMBOL_MONETARY_SEPARATOR:
                return $this->_data['numberSymbols']['monetarySeparator'];
            case self::SYMBOL_EXPONENTIAL:
                return $this->_data['numberSymbols']['exponential'];
            case self::SYMBOL_PERMILL:
                return $this->_data['numberSymbols']['permill'];
            case self::SYMBOL_PAD_ESCAPE:
                return $this->_data['numberSymbols']['padEscape'];
            case self::SYMBOL_INFINITY:
                return $this->_data['numberSymbols']['infinity'];
            case self::SYMBOL_NAN:
                return $this->_data['numberSymbols']['nan'];
            case self::SYMBOL_SIGNIFICANT_DIGIT:
                return $this->_data['numberSymbols']['significantDigit'];
            case self::SYMBOL_MONETARY_GROUPING_SEPARATOR:
                return $this->_data['numberSymbols']['monetaryGroupingSeparator'];
        }

        return null;
    }

    /**
     * Returns this locale’s symbol for a given currency.
     *
     * @param string $currency The 3-letter ISO 4217 currency code indicating the currency to use.
     * @return string The currency symbol.
     */
    public function getCurrencySymbol(string $currency): string
    {
        if (Craft::$app->getI18n()->getIsIntlLoaded()) {
            // hat tip: https://stackoverflow.com/a/30026774
            $formatter = new NumberFormatter("{$this->id}@currency={$currency}", NumberFormatter::CURRENCY);
            return $formatter->getSymbol(NumberFormatter::CURRENCY_SYMBOL);
        }

        if (isset($this->_data['currencySymbols'][$currency])) {
            return $this->_data['currencySymbols'][$currency];
        }

        return $currency;
    }

    // Deprecated Methods
    // -------------------------------------------------------------------------

    /**
     * Returns the locale ID.
     *
     * @return string
     * @deprecated in 3.0.0. Use id instead.
     */
    public function getId(): string
    {
        Craft::$app->getDeprecator()->log('Locale::getId()', '`Locale::getId()` has been deprecated. Use the `id` property instead.');

        return $this->id;
    }

    /**
     * Returns the locale name in a given language.
     *
     * @param string|null $targetLocaleId
     * @return string|null
     * @deprecated in 3.0.0. Use getDisplayName() instead.
     */
    public function getName(string $targetLocaleId = null)
    {
        Craft::$app->getDeprecator()->log('Locale::getName()', '`Locale::getName()` has been deprecated. Use `getDisplayName()` instead.');

        // In Craft 2, getName() with no $targetLocaleId would default to the active language
        if ($targetLocaleId === null) {
            $targetLocaleId = Craft::$app->language;
        }

        return $this->getDisplayName($targetLocaleId);
    }

    /**
     * Returns the locale name in its own language.
     *
     * @return string|false
     * @deprecated in 3.0.0. Use getDisplayName() instead.
     */
    public function getNativeName()
    {
        Craft::$app->getDeprecator()->log('Locale::getNativeName()', '`Locale::getNativeName()` has been deprecated. Use `getDisplayName()` instead.');

        return $this->getDisplayName();
    }

    /**
     * Returns a localized date/time format.
     *
     * @param string $length The format length that should be returned. Values: Locale::LENGTH_SHORT, ::MEDIUM, ::LONG, ::FULL
     * @param bool $withDate Whether the date should be included in the format.
     * @param bool $withTime Whether the time should be included in the format.
     * @param string $format The format type that should be returned. Values: Locale::FORMAT_ICU (default), ::FORMAT_PHP, ::FORMAT_JUI
     * @return string The date/time format
     */
    private function _getDateTimeFormat(string $length, bool $withDate, bool $withTime, string $format): string
    {
        $icuFormat = $this->_getDateTimeIcuFormat($length, $withDate, $withTime);

        if ($format !== self::FORMAT_ICU) {
            $type = ($withDate ? 'date' : '') . ($withTime ? 'time' : '');

            switch ($format) {
                case self::FORMAT_PHP:
                    return FormatConverter::convertDateIcuToPhp($icuFormat, $type, $this->id);
                case self::FORMAT_JUI:
                    return FormatConverter::convertDateIcuToJui($icuFormat, $type, $this->id);
            }
        }

        return $icuFormat;
    }

    /**
     * Returns a localized ICU date/time format.
     *
     * @param string $length The format length that should be returned. Values: Locale::LENGTH_SHORT, ::MEDIUM, ::LONG, ::FULL
     * @param bool $withDate Whether the date should be included in the format.
     * @param bool $withTime Whether the time should be included in the format.
     * @return string|null The ICU date/time format
     * @throws Exception if $length is invalid
     */
    private function _getDateTimeIcuFormat(string $length, bool $withDate, bool $withTime)
    {
        if ($length === null) {
            $length = self::LENGTH_MEDIUM;
        }

        // See if it's been explicitly defined already
        $type = ($withDate ? 'date' : '') . ($withTime ? 'time' : '');
        if (isset($this->_data['dateTimeFormats'][$length][$type])) {
            return $this->_data['dateTimeFormats'][$length][$type];
        }

        if (Craft::$app->getI18n()->getIsIntlLoaded()) {
            // Convert length to IntlDateFormatter constants
            switch ($length) {
                case self::LENGTH_FULL:
                    /** @noinspection CallableParameterUseCaseInTypeContextInspection */
                    $length = IntlDateFormatter::FULL;
                    break;
                case self::LENGTH_LONG:
                    /** @noinspection CallableParameterUseCaseInTypeContextInspection */
                    $length = IntlDateFormatter::LONG;
                    break;
                case self::LENGTH_MEDIUM:
                    /** @noinspection CallableParameterUseCaseInTypeContextInspection */
                    $length = IntlDateFormatter::MEDIUM;
                    break;
                case self::LENGTH_SHORT:
                    /** @noinspection CallableParameterUseCaseInTypeContextInspection */
                    $length = IntlDateFormatter::SHORT;
                    break;
                default:
                    throw new Exception('Invalid date/time format length: ' . $length);
            }

            $dateType = ($withDate ? $length : IntlDateFormatter::NONE);
            $timeType = ($withTime ? $length : IntlDateFormatter::NONE);
            $formatter = new IntlDateFormatter($this->id, $dateType, $timeType);
            $pattern = $formatter->getPattern();

            // Use 4-digit years
            return strtr($pattern, [
                'yyyy' => 'yyyy',
                'yy' => 'yyyy',
            ]);
        }

        return null;
    }
}
