<?php

/**
 * Matomo - free/libre analytics platform
 *
 * @link    https://matomo.org
 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 */

namespace Piwik\Plugins\Intl\Commands;

use DateTimeZone;
use Piwik\Container\StaticContainer;
use Piwik\Development;
use Piwik\Filesystem;
use Piwik\Http;
use Piwik\Plugin\ConsoleCommand;
use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\EncodedEntities;
use Piwik\Plugins\LanguagesManager\TranslationWriter\Filter\UnnecassaryWhitespaces;
use Piwik\Plugins\LanguagesManager\TranslationWriter\Writer;

/**
 * Console Command to generate Intl-data files for Piwik
 *
 * This script uses the master data of unicode-cldr/cldr-localenames-full repository to fetch available translations
 */
class GenerateIntl extends ConsoleCommand
{
    /**
     * @var string
     */
    public $CLDRVersion = "48.1.0";

    /**
     * @return bool
     */
    public function isEnabled()
    {
        return Development::isEnabled();
    }

    /**
     * @return void
     */
    protected function configure()
    {
        $this->setName('translations:generate-intl-data')
            ->addOptionalValueOption('language', 'l', 'language that should be fetched')
            ->addOptionalValueOption('cldr-version', '', 'CLDR version to use for update')
            ->setDescription('Generates Intl-data for Piwik');
    }

    protected function transformLangCode(string $langCode): string
    {
        if (substr_count($langCode, '-') == 1) {
            $langCodeParts = explode('-', $langCode, 2);
            return sprintf('%s-%s', $langCodeParts[0], strtoupper($langCodeParts[1]));
        }
        return $langCode;
    }

    protected function transform(?string $str): string
    {
        if (empty($str)) {
            return '';
        }

        preg_match_all("~^(.)(.*)$~u", $str, $arr);
        return mb_strtoupper($arr[1][0]) . $arr[2][0];
    }

    protected function doExecute(): int
    {
        $input = $this->getInput();
        $matomoLanguages = \Piwik\Plugins\LanguagesManager\API::getInstance()->getAvailableLanguages();

        if ($input->getOption('language')) {
            $matomoLanguages = [$input->getOption('language')];
        }

        if ($input->getOption('cldr-version')) {
            $this->CLDRVersion = $input->getOption('cldr-version');
        }

        $aliasesUrl = 'https://raw.githubusercontent.com/unicode-org/cldr-json/%s/cldr-json/cldr-core/supplemental/aliases.json';
        $aliasesData = Http::fetchRemoteFile(sprintf($aliasesUrl, $this->CLDRVersion));
        /** @var array $aliasesData */
        $aliasesData = json_decode($aliasesData ?: '', true);
        $aliasesData = $aliasesData['supplemental']['metadata']['alias']['languageAlias'] ?? [];

        $this->checkCurrencies();

        foreach ($matomoLanguages as $langCode) {
            if ($langCode === 'dev') {
                continue;
            }

            $requestLangCode = $transformedLangCode = $this->transformLangCode($langCode);

            if (array_key_exists($requestLangCode, $aliasesData)) {
                $requestLangCode = (string)$aliasesData[$requestLangCode]['_replacement'];
            }

            // fix some locales
            $localFixes = [
                'pt' => 'pt-PT',
                'pt-br' => 'pt',
                'zh-cn' => 'zh-Hans',
                'zh-tw' => 'zh-Hant',
            ];

            if (array_key_exists($langCode, $localFixes)) {
                $requestLangCode = $localFixes[$langCode];
            }

            setlocale(LC_ALL, $langCode);

            $translations = [];

            $this->fetchLanguageData($transformedLangCode, $requestLangCode, $translations);
            $this->fetchTerritoryData($transformedLangCode, $requestLangCode, $translations);
            $this->fetchCurrencyData($transformedLangCode, $requestLangCode, $translations);
            $this->fetchCalendarData($transformedLangCode, $requestLangCode, $translations);
            $this->fetchTimeZoneData($transformedLangCode, $requestLangCode, $translations);
            $this->fetchLayoutDirection($transformedLangCode, $requestLangCode, $translations);
            $this->fetchUnitData($transformedLangCode, $requestLangCode, $translations);
            $this->fetchNumberFormattingData($transformedLangCode, $requestLangCode, $translations);
            $this->fetchListingLayouts($transformedLangCode, $requestLangCode, $translations);

            // fix missing language name for territory specific languages (like es-AR)
            if (empty($translations['Intl']['OriginalLanguageName']) && strpos($transformedLangCode, '-')) {
                [$language, $territory] = explode('-', $transformedLangCode);

                if (!empty($translations['Intl']['Language_' . $language])) {
                    $originalName = $this->transform($translations['Intl']['Language_' . $language]);

                    if (!empty($translations['Intl']['Country_' . $territory])) {
                        $originalName .= ' (' . $translations['Intl']['Country_' . $territory] . ')';
                    } else {
                        $originalName .= ' (' . strtoupper($language) . ')';
                    }

                    $translations['Intl']['OriginalLanguageName'] = $originalName;
                }
            }

            ksort($translations['Intl']);

            $translationWriter = new Writer($langCode, 'Intl');
            $translationWriter->setTranslations($translations);
            $translationWriter->addFilter(new UnnecassaryWhitespaces());
            $translationWriter->addFilter(new EncodedEntities());
            $translationWriter->save();
        }

        return self::SUCCESS;
    }

    protected function checkCurrencies(): void
    {
        $currencyDataUrl = 'https://raw.githubusercontent.com/unicode-org/cldr-json/%s/cldr-json/cldr-core/supplemental/currencyData.json';

        $currencyData = Http::fetchRemoteFile(sprintf($currencyDataUrl, $this->CLDRVersion));
        /** @var array $currencyData */
        $currencyData = json_decode($currencyData ?: '', true);
        $currencyData = $currencyData['supplemental']['currencyData']['region'] ?? [];

        $cldrCurrencies = [];
        foreach ($currencyData as $region) {
            foreach ($region as $regionCurrencies) {
                foreach ($regionCurrencies as $currencyCode => $validity) {
                    if (!isset($validity['_to']) && !isset($validity['_tender'])) {
                        $cldrCurrencies[] = $currencyCode;
                    }
                }
            }
        }

        $file = Filesystem::getPathToPiwikRoot() . '/core/Intl/Data/Resources/currencies.php';
        $matomoCurrencies = array_keys(include $file);

        $missing = array_diff($cldrCurrencies, $matomoCurrencies);
        $additional = array_diff($matomoCurrencies, $cldrCurrencies);

        if ($missing) {
            $this->getOutput()->writeln('Warning: Currencies missing from ' . $file . ': ' . implode(', ', $missing));
        }
        if ($additional) {
            $this->getOutput()->writeln('Warning: Unknown currencies in ' . $file . ': ' . implode(', ', $additional));
        }
    }

    protected function getEnglishLanguageName(string $code, string $alternateCode): string
    {
        $languageDataUrl = 'https://raw.githubusercontent.com/unicode-org/cldr-json/%s/cldr-json/cldr-localenames-full/main/%s/languages.json';

        static $languageData = [];

        try {
            if (empty($languageData)) {
                $languageData = Http::fetchRemoteFile(sprintf($languageDataUrl, $this->CLDRVersion, 'en'));
                /** @var array $languageData */
                $languageData = json_decode($languageData ?: '', true);
                $languageData = $languageData['main']['en']['localeDisplayNames']['languages'] ?? [];
            }

            if (array_key_exists($code, $languageData) && $languageData[$code] != $code) {
                return $this->transform($languageData[$code]);
            }

            if (array_key_exists($alternateCode, $languageData) && $languageData[$alternateCode] != $alternateCode) {
                return $this->transform($languageData[$alternateCode]);
            }

            if (strpos($code, '-')) {
                [$language, $territory] = explode('-', $code);

                if (!array_key_exists($language, $languageData)) {
                    return '';
                }

                $englishName = $this->transform($languageData[$language]);

                $territoryDataUrl = 'https://raw.githubusercontent.com/unicode-org/cldr-json/%s/cldr-json/cldr-localenames-full/main/%s/territories.json';

                try {
                    $territoryData = Http::fetchRemoteFile(sprintf($territoryDataUrl, $this->CLDRVersion, 'en'));
                    /** @var array $territoryData */
                    $territoryData = json_decode($territoryData ?: '', true);
                    $territoryData = $territoryData['main']['en']['localeDisplayNames']['territories'] ?? [];

                    if (array_key_exists($territory, $territoryData)) {
                        $englishName .= ' (' . $territoryData[$territory] . ')';
                    } else {
                        $englishName .= ' (' . strtoupper($language) . ')';
                    }
                } catch (\Exception $e) {
                    $englishName .= ' (' . strtoupper($language) . ')';
                }

                return $englishName;
            }
        } catch (\Exception $e) {
        }

        return '';
    }

    /**
     * @param array{Intl: array<string, string>} $translations
     */
    protected function fetchLanguageData(string $langCode, string $requestLangCode, array &$translations): void
    {
        $languageCodes = array_keys(StaticContainer::get('Piwik\Intl\Data\Provider\LanguageDataProvider')->getLanguageList());

        $languageDataUrl = 'https://raw.githubusercontent.com/unicode-org/cldr-json/%s/cldr-json/cldr-localenames-full/main/%s/languages.json';

        try {
            $languageData = Http::fetchRemoteFile(sprintf($languageDataUrl, $this->CLDRVersion, $requestLangCode));
            /** @var array $languageData */
            $languageData = json_decode($languageData ?: '', true);
            $languageData = $languageData['main'][$requestLangCode]['localeDisplayNames']['languages'] ?? [];

            if (empty($languageData)) {
                throw new \Exception();
            }

            foreach ($languageCodes as $code) {
                if (!empty($languageData[$code]) && $languageData[$code] != $code) {
                    $translations['Intl']['Language_' . $code] = $this->transform($languageData[$code]);
                }
            }

            if (array_key_exists($langCode, $languageData) && $languageData[$langCode] != $langCode && $langCode !== 'pt') {
                // We ignore `pt` here, as we otherwise would end up with having `pt` and `pt_BR` using the same original name
                $translations['Intl']['OriginalLanguageName'] = $this->transform($languageData[$langCode]);
            } elseif (array_key_exists($requestLangCode, $languageData) && $languageData[$requestLangCode] != $requestLangCode) {
                $translations['Intl']['OriginalLanguageName'] = $this->transform($languageData[$requestLangCode]);
            }
            $translations['Intl']['EnglishLanguageName'] = $this->getEnglishLanguageName($langCode, $requestLangCode);

            $this->getOutput()->writeln('Saved language data for ' . $langCode);
        } catch (\Exception $e) {
            $this->getOutput()->writeln('Unable to import language data for ' . $langCode);
        }
    }

    /**
     * @param array{Intl: array<string, string>} $translations
     */
    protected function fetchLayoutDirection(string $langCode, string $requestLangCode, array &$translations): void
    {
        $layoutDirectionUrl = 'https://raw.githubusercontent.com/unicode-org/cldr-json/%s/cldr-json/cldr-misc-full/main/%s/layout.json';

        try {
            $layoutData = Http::fetchRemoteFile(sprintf($layoutDirectionUrl, $this->CLDRVersion, $requestLangCode));
            /** @var array $layoutData */
            $layoutData = json_decode($layoutData ?: '', true);
            $layoutData = $layoutData['main'][$requestLangCode]['layout']['orientation'] ?? [];

            if (empty($layoutData)) {
                throw new \Exception();
            }

            $translations['Intl']['LayoutDirection'] = 'ltr';
            if ($layoutData['characterOrder'] == 'right-to-left') {
                $translations['Intl']['LayoutDirection'] = 'rtl';
            }

            $this->getOutput()->writeln('Saved language data for ' . $langCode);
        } catch (\Exception $e) {
            $this->getOutput()->writeln('Unable to import language data for ' . $langCode);
        }
    }

    /**
     * @param array{Intl: array<string, string>} $translations
     */
    protected function fetchTerritoryData(string $langCode, string $requestLangCode, array &$translations): void
    {
        $territoryDataUrl = 'https://raw.githubusercontent.com/unicode-org/cldr-json/%s/cldr-json/cldr-localenames-full/main/%s/territories.json';

        $countryCodes = array_keys(StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider')->getCountryList());
        $countryCodes = array_map('strtoupper', $countryCodes);

        $continentMapping = [
            "afr" => "002",
            "amc" => "013",
            "amn" => "003",
            "ams" => "005",
            "ant" => "AQ",
            "asi" => "142",
            "eur" => "150",
            "oce" => "009",
        ];

        try {
            $territoryData = Http::fetchRemoteFile(sprintf($territoryDataUrl, $this->CLDRVersion, $requestLangCode));
            /** @var array $territoryData */
            $territoryData = json_decode($territoryData ?: '', true);
            $territoryData = $territoryData['main'][$requestLangCode]['localeDisplayNames']['territories'] ?? [];

            if (empty($territoryData)) {
                throw new \Exception();
            }

            foreach ($countryCodes as $code) {
                if (!empty($territoryData[$code]) && $territoryData[$code] != $code) {
                    $translations['Intl']['Country_' . $code] = $this->transform($territoryData[$code]);
                }
            }

            foreach ($continentMapping as $shortCode => $code) {
                if (!empty($territoryData[$code]) && $territoryData[$code] != $code) {
                    $translations['Intl']['Continent_' . $shortCode] = $this->transform($territoryData[$code]);
                }
            }

            $this->getOutput()->writeln('Saved territory data for ' . $langCode);
        } catch (\Exception $e) {
            $this->getOutput()->writeln('Unable to import territory data for ' . $langCode);
        }
    }

    /**
     * @param array{Intl: array<string, string>} $translations
     */
    protected function fetchCalendarData(string $langCode, string $requestLangCode, array &$translations): void
    {
        $calendarDataUrl = 'https://raw.githubusercontent.com/unicode-org/cldr-json/%s/cldr-json/cldr-dates-full/main/%s/ca-gregorian.json';

        try {
            $calendarData = Http::fetchRemoteFile(sprintf($calendarDataUrl, $this->CLDRVersion, $requestLangCode));
            /** @var array $calendarData */
            $calendarData = json_decode($calendarData ?: '', true);
            $calendarData = $calendarData['main'][$requestLangCode]['dates']['calendars']['gregorian'] ?? [];

            if (empty($calendarData)) {
                throw new \Exception();
            }

            for ($i = 1; $i <= 12; $i++) {
                $translations['Intl']['Month_Short_' . $i] = $calendarData['months']['format']['abbreviated'][$i];
                $translations['Intl']['Month_Long_' . $i] = $calendarData['months']['format']['wide'][$i];
                $translations['Intl']['Month_Short_StandAlone_' . $i] = $calendarData['months']['stand-alone']['abbreviated'][$i];
                $translations['Intl']['Month_Long_StandAlone_' . $i] = $calendarData['months']['stand-alone']['wide'][$i];
            }

            $days = [
                1 => 'mon',
                2 => 'tue',
                3 => 'wed',
                4 => 'thu',
                5 => 'fri',
                6 => 'sat',
                7 => 'sun',
            ];

            foreach ($days as $nr => $day) {
                $translations['Intl']['Day_Min_' . $nr] = $calendarData['days']['format']['short'][$day];
                $translations['Intl']['Day_Short_' . $nr] = $calendarData['days']['format']['abbreviated'][$day];
                $translations['Intl']['Day_Long_' . $nr] = $calendarData['days']['format']['wide'][$day];
                $translations['Intl']['Day_Min_StandAlone_' . $nr] = $calendarData['days']['stand-alone']['short'][$day];
                $translations['Intl']['Day_Short_StandAlone_' . $nr] = $calendarData['days']['stand-alone']['abbreviated'][$day];
                $translations['Intl']['Day_Long_StandAlone_' . $nr] = $calendarData['days']['stand-alone']['wide'][$day];
            }

            $translations['Intl']['Time_AM'] = $calendarData['dayPeriods']['format']['wide']['am'];
            $translations['Intl']['Time_PM'] = $calendarData['dayPeriods']['format']['wide']['pm'];
            $translations['Intl']['Format_Time'] = '{time}';
            $translations['Intl']['Format_Time_12'] = $calendarData['dateTimeFormats']['availableFormats']['hms'];
            $translations['Intl']['Format_Time_24'] = $calendarData['dateTimeFormats']['availableFormats']['Hms'];
            $translations['Intl']['Format_Hour_12'] = $calendarData['dateTimeFormats']['availableFormats']['h'];
            $translations['Intl']['Format_Hour_24'] = $calendarData['dateTimeFormats']['availableFormats']['H'];
            $translations['Intl']['Format_Date_Long'] = $calendarData['dateFormats']['full'];
            $translations['Intl']['Format_Date_Day_Month'] = $calendarData['dateTimeFormats']['availableFormats']['MMMEd'];
            $translations['Intl']['Format_Date_Short'] = $calendarData['dateFormats']['medium'];
            $translations['Intl']['Format_Month_Short'] = $calendarData['dateTimeFormats']['availableFormats']['yMMM'];
            $translations['Intl']['Format_Month_Long'] = $this->transformDateFormat($calendarData['dateTimeFormats']['availableFormats']['yMMM'], ['MMM' => 'MMMM', 'LLL' => 'LLLL']);
            if (isset($calendarData['dateTimeFormats']['availableFormats']['yMMMM'])) {
                $translations['Intl']['Format_Month_Long'] = $calendarData['dateTimeFormats']['availableFormats']['yMMMM'];
            }
            $translations['Intl']['Format_Year'] = $calendarData['dateTimeFormats']['availableFormats']['y'];

            $translations['Intl']['Format_DateTime_Long'] = $calendarData['dateFormats']['full'] . ' {time}';
            $translations['Intl']['Format_DateTime_Short'] = $calendarData['dateFormats']['medium'] . ' {time}';

            $translations['Intl']['Format_Interval_Long_D'] = $this->transformDateFormat($calendarData['dateTimeFormats']['intervalFormats']['yMMMd']['d'], ['MMMM' => 'MMM', 'LLLL' => 'LLL', 'MMM' => 'MMMM', 'LLL' => 'LLLL']);
            $translations['Intl']['Format_Interval_Long_M'] = $this->transformDateFormat($calendarData['dateTimeFormats']['intervalFormats']['yMMMd']['M'], ['MMMM' => 'MMM', 'LLLL' => 'LLL', 'MMM' => 'MMMM', 'LLL' => 'LLLL']);
            $translations['Intl']['Format_Interval_Long_Y'] = $this->transformDateFormat($calendarData['dateTimeFormats']['intervalFormats']['yMMMd']['y'], ['MMMM' => 'MMM', 'LLLL' => 'LLL', 'MMM' => 'MMMM', 'LLL' => 'LLLL']);

            if (isset($calendarData['dateTimeFormats']['intervalFormats']['yMMMMd'])) {
                $translations['Intl']['Format_Interval_Long_D'] = $calendarData['dateTimeFormats']['intervalFormats']['yMMMMd']['d'];
                $translations['Intl']['Format_Interval_Long_M'] = $calendarData['dateTimeFormats']['intervalFormats']['yMMMMd']['M'];
                $translations['Intl']['Format_Interval_Long_Y'] = $calendarData['dateTimeFormats']['intervalFormats']['yMMMMd']['y'];
            }

            $translations['Intl']['Format_Interval_Short_D'] = $calendarData['dateTimeFormats']['intervalFormats']['yMMMd']['d'];
            $translations['Intl']['Format_Interval_Short_M'] = $calendarData['dateTimeFormats']['intervalFormats']['yMMMd']['M'];
            $translations['Intl']['Format_Interval_Short_Y'] = $calendarData['dateTimeFormats']['intervalFormats']['yMMMd']['y'];

            $this->getOutput()->writeln('Saved calendar data for ' . $langCode);
        } catch (\Exception $e) {
            $this->getOutput()->writeln('Unable to import calendar data for ' . $langCode);
        }

        $dateFieldsUrl = 'https://raw.githubusercontent.com/unicode-org/cldr-json/%s/cldr-json/cldr-dates-full/main/%s/dateFields.json';

        try {
            $dateFieldData = Http::fetchRemoteFile(sprintf($dateFieldsUrl, $this->CLDRVersion, $requestLangCode));
            /** @var array $dateFieldData */
            $dateFieldData = json_decode($dateFieldData ?: '', true);
            $dateFieldData = $dateFieldData['main'][$requestLangCode]['dates']['fields'] ?? [];

            if (empty($dateFieldData)) {
                throw new \Exception();
            }

            $translations['Intl']['PeriodWeek'] = $dateFieldData['week']['displayName'];
            $translations['Intl']['PeriodYear'] = $dateFieldData['year']['displayName'];
            $translations['Intl']['PeriodDay'] = $dateFieldData['day']['displayName'];
            $translations['Intl']['PeriodMonth'] = $dateFieldData['month']['displayName'];
            $translations['Intl']['Year_Short'] = $dateFieldData['year-narrow']['displayName'];
            $translations['Intl']['Today'] = $this->transform($dateFieldData['day']['relative-type-0']);
            $translations['Intl']['Yesterday'] = $this->transform($dateFieldData['day']['relative-type--1']);

            $this->getOutput()->writeln('Saved date fields for ' . $langCode);
        } catch (\Exception $e) {
            $this->getOutput()->writeln('Unable to import date fields for ' . $langCode);
        }
    }

    /**
     * @param array{Intl: array<string, string>} $translations
     */
    protected function fetchTimeZoneData(string $langCode, string $requestLangCode, array &$translations): void
    {
        $timeZoneDataUrl = 'https://raw.githubusercontent.com/unicode-org/cldr-json/%s/cldr-json/cldr-dates-full/main/%s/timeZoneNames.json';

        try {
            $timeZoneData = Http::fetchRemoteFile(sprintf($timeZoneDataUrl, $this->CLDRVersion, $requestLangCode));
            /** @var array $timeZoneData */
            $timeZoneData = json_decode($timeZoneData ?: '', true);
            $timeZoneData = $timeZoneData['main'][$requestLangCode]['dates']['timeZoneNames']['zone'] ?? [];

            if (empty($timeZoneData)) {
                throw new \Exception();
            }

            $cities = [];
            foreach ($timeZoneData as $key1 => $level1) {
                foreach ($level1 as $key2 => $level2) {
                    if (isset($level2['exemplarCity'])) {
                        $level2 = [$level2];
                    }
                    foreach ($level2 as $key3 => $level3) {
                        if (isset($level3['exemplarCity'])) {
                            $timezone = $key1 . '/' . $key2;
                            if ($key3) {
                                $timezone .= '/' . $key3;
                            }
                            $cities[$timezone] = $level3['exemplarCity'];
                        }
                    }
                }
            }

            foreach ($cities as $timezone => $city) {
                try {
                    $zone = new DateTimeZone($timezone);
                } catch (\Exception $e) {
                    continue;
                }
                $location = $zone->getLocation();
                if (!isset($location['country_code'])) {
                    continue;
                }
                // We only need translations for countries with more than one timezone.
                $timezonesInCountry = DateTimeZone::listIdentifiers(DateTimeZone::PER_COUNTRY, $location['country_code']);
                if (count($timezonesInCountry) > 1) {
                    $translations['Intl']['Timezone_' . str_replace(['_', '/'], ['', '_'], $timezone)] = $city;
                }
            }

            $this->getOutput()->writeln('Saved time zone data for ' . $langCode);
        } catch (\Exception $e) {
            $this->getOutput()->writeln('Unable to import time zone data for ' . $langCode);
        }
    }

    /**
     * @param array<string, string> $changes
     */
    protected function transformDateFormat(?string $dateFormat, array $changes): string
    {
        return str_replace(array_keys($changes), array_values($changes), $dateFormat ?? '');
    }

    /**
     * @param array{Intl: array<string, string>} $translations
     */
    protected function fetchNumberFormattingData(string $langCode, string $requestLangCode, array &$translations): void
    {
        $unitsUrl = 'https://raw.githubusercontent.com/unicode-org/cldr-json/%s/cldr-json/cldr-numbers-full/main/%s/numbers.json';

        try {
            $unitsData = Http::fetchRemoteFile(sprintf($unitsUrl, $this->CLDRVersion, $requestLangCode));
            /** @var array $unitsData */
            $unitsData = json_decode($unitsData ?: '', true);
            $unitsData = $unitsData['main'][$requestLangCode]['numbers'] ?? [];

            if (empty($unitsData)) {
                throw new \Exception();
            }

            $numberingSystem = $unitsData['defaultNumberingSystem'];

            $translations['Intl']['NumberSymbolDecimal']  = $unitsData['symbols-numberSystem-' . $numberingSystem]['decimal'];
            $translations['Intl']['NumberSymbolGroup']    = $unitsData['symbols-numberSystem-' . $numberingSystem]['group'];
            $translations['Intl']['NumberSymbolPercent']  = $unitsData['symbols-numberSystem-' . $numberingSystem]['percentSign'];
            $translations['Intl']['NumberSymbolPlus']     = $unitsData['symbols-numberSystem-' . $numberingSystem]['plusSign'];
            $translations['Intl']['NumberSymbolMinus']    = $unitsData['symbols-numberSystem-' . $numberingSystem]['minusSign'];
            $translations['Intl']['NumberFormatNumber']   = $unitsData['decimalFormats-numberSystem-' . $numberingSystem]['standard'];
            $translations['Intl']['NumberFormatCurrency']   = $unitsData['currencyFormats-numberSystem-' . $numberingSystem]['standard'];
            $translations['Intl']['NumberFormatPercent']  = $unitsData['percentFormats-numberSystem-' . $numberingSystem]['standard'];

            for ($i = 1000; $i <= 1000000000000000000; $i *= 10) {
                $numberCombatFormats = $unitsData['decimalFormats-numberSystem-' . $numberingSystem]['short']['decimalFormat'] ?? [];

                if (!empty($numberCombatFormats)) {
                    $translations['Intl']['NumberFormatNumberCompact' . $i . 'One'] = $numberCombatFormats[$i . '-count-one'] ?? '';
                    $translations['Intl']['NumberFormatNumberCompact' . $i . 'Other'] = $numberCombatFormats[$i . '-count-other'] ?? '';
                }

                $currencyCombatFormats = $unitsData['currencyFormats-numberSystem-' . $numberingSystem]['short']['standard'] ?? [];

                if (!empty($currencyCombatFormats)) {
                    $translations['Intl']['NumberFormatCurrencyCompact' . $i . 'One'] = $currencyCombatFormats[$i . '-count-one'] ?? '';
                    $translations['Intl']['NumberFormatCurrencyCompact' . $i . 'Other'] = $currencyCombatFormats[$i . '-count-other'] ?? '';
                }
            }

            $this->getOutput()->writeln('Saved number formatting data for ' . $langCode);
        } catch (\Exception $e) {
            $this->getOutput()->writeln('Unable to import number formatting data for ' . $langCode);
        }
    }

    /**
     * @param array{Intl: array<string, string>} $translations
     */
    protected function fetchUnitData(string $langCode, string $requestLangCode, array &$translations): void
    {
        $unitsUrl = 'https://raw.githubusercontent.com/unicode-org/cldr-json/%s/cldr-json/cldr-units-full/main/%s/units.json';

        try {
            $unitsData = Http::fetchRemoteFile(sprintf($unitsUrl, $this->CLDRVersion, $requestLangCode));
            /** @var array $unitsData */
            $unitsData = json_decode($unitsData ?: '', true);
            $unitsData = $unitsData['main'][$requestLangCode]['units'] ?? [];

            if (empty($unitsData)) {
                throw new \Exception();
            }

            $translations['Intl']['NSeconds']       = $this->replacePlaceHolder($unitsData['long']['duration-second']['unitPattern-count-other']);
            $translations['Intl']['NSecondsShort']  = $this->replacePlaceHolder($unitsData['narrow']['duration-second']['unitPattern-count-other']);
            $translations['Intl']['Seconds']        = $unitsData['long']['duration-second']['displayName'];

            $translations['Intl']['NMinutes']       = $this->replacePlaceHolder($unitsData['long']['duration-minute']['unitPattern-count-other']);

            if (isset($unitsData['long']['duration-minute']['unitPattern-count-one'])) {
                $translations['Intl']['OneMinute'] = $this->replacePlaceHolder($unitsData['long']['duration-minute']['unitPattern-count-one'], '1');
            } else {
                $translations['Intl']['OneMinute'] = $this->replacePlaceHolder($unitsData['long']['duration-minute']['unitPattern-count-other'], '1');
            }

            if (isset($unitsData['short']['duration-minute']['unitPattern-count-one'])) {
                $translations['Intl']['OneMinuteShort'] = $this->replacePlaceHolder($unitsData['short']['duration-minute']['unitPattern-count-one'], '1');
            } else {
                $translations['Intl']['OneMinuteShort'] = $this->replacePlaceHolder($unitsData['short']['duration-minute']['unitPattern-count-other'], '1');
            }

            $translations['Intl']['NMinutesShort'] = $this->replacePlaceHolder($unitsData['short']['duration-minute']['unitPattern-count-other']);

            $translations['Intl']['Minutes']        = $unitsData['long']['duration-minute']['displayName'];

            $translations['Intl']['Hours']          = $unitsData['long']['duration-hour']['displayName'];
            $translations['Intl']['NHoursShort']    = $this->replacePlaceHolder($unitsData['narrow']['duration-hour']['unitPattern-count-other']);

            $translations['Intl']['NDays']          = $this->replacePlaceHolder($unitsData['long']['duration-day']['unitPattern-count-other']);

            if (isset($unitsData['short']['duration-day']['unitPattern-count-one'])) {
                $translations['Intl']['OneDay']         = $this->replacePlaceHolder($unitsData['long']['duration-day']['unitPattern-count-one'], '1');
            } else {
                $translations['Intl']['OneDay']         = $this->replacePlaceHolder($unitsData['long']['duration-day']['unitPattern-count-other'], '1');
            }

            $translations['Intl']['PeriodWeeks']    = $unitsData['long']['duration-week']['displayName'];
            $translations['Intl']['PeriodYears']    = $unitsData['long']['duration-year']['displayName'];
            $translations['Intl']['PeriodDays']     = $unitsData['long']['duration-day']['displayName'];
            $translations['Intl']['PeriodMonths']   = $unitsData['long']['duration-month']['displayName'];


            $this->getOutput()->writeln('Saved unit data for ' . $langCode);
        } catch (\Exception $e) {
            $this->getOutput()->writeln('Unable to import unit data for ' . $langCode);
        }
    }

    /**
     * @param array{Intl: array<string, string>} $translations
     */
    protected function fetchCurrencyData(string $langCode, string $requestLangCode, array &$translations): void
    {
        $currenciesUrl = 'https://raw.githubusercontent.com/unicode-org/cldr-json/%s/cldr-json/cldr-numbers-full/main/%s/currencies.json';

        try {
            $currencyData = Http::fetchRemoteFile(sprintf($currenciesUrl, $this->CLDRVersion, $requestLangCode));
            /** @var array $currencyData */
            $currencyData = json_decode($currencyData ?: '', true);
            $currencyData = $currencyData['main'][$requestLangCode]['numbers']['currencies'] ?? [];

            if (empty($currencyData)) {
                throw new \Exception();
            }

            $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\CurrencyDataProvider');
            foreach ($dataProvider->getCurrencyList() as $code => $currency) {
                if (isset($currencyData[$code]['displayName'])) {
                    $translations['Intl']['Currency_' . $code] = $this->transform($currencyData[$code]['displayName']);
                } else {
                    $translations['Intl']['Currency_' . $code] = $currency[1];
                }
                if (isset($currencyData[$code]['symbol'])) {
                    $translations['Intl']['CurrencySymbol_' . $code] = $currencyData[$code]['symbol'];
                } else {
                    $translations['Intl']['CurrencySymbol_' . $code] = $currency[0];
                }
            }

            $this->getOutput()->writeln('Saved currency data for ' . $langCode);
        } catch (\Exception $e) {
            $this->getOutput()->writeln('Unable to import currency data for ' . $langCode);
        }
    }

    /**
     * @param array{Intl: array<string, string>} $translations
     */
    protected function fetchListingLayouts(string $langCode, string $requestLangCode, array &$translations): void
    {
        $listingLayoutsUrl = 'https://raw.githubusercontent.com/unicode-org/cldr-json/%s/cldr-json/cldr-misc-full/main/%s/listPatterns.json';

        try {
            $listingLayouts = Http::fetchRemoteFile(sprintf($listingLayoutsUrl, $this->CLDRVersion, $requestLangCode));
            /** @var array $listingLayouts */
            $listingLayouts = json_decode($listingLayouts ?: '', true);
            $listingLayouts = $listingLayouts['main'][$requestLangCode]['listPatterns'] ?? [];

            if (empty($listingLayouts)) {
                throw new \Exception();
            }

            if (isset($listingLayouts['listPattern-type-standard'])) {
                $translations['Intl']['ListPatternAndStart'] = $listingLayouts['listPattern-type-standard']['start'];
                $translations['Intl']['ListPatternAndMiddle'] = $listingLayouts['listPattern-type-standard']['middle'];
                $translations['Intl']['ListPatternAndEnd'] = $listingLayouts['listPattern-type-standard']['end'];
                $translations['Intl']['ListPatternAnd2'] = $listingLayouts['listPattern-type-standard']['2'];
            }

            if (isset($listingLayouts['listPattern-type-or'])) {
                $translations['Intl']['ListPatternOrStart'] = $listingLayouts['listPattern-type-or']['start'];
                $translations['Intl']['ListPatternOrMiddle'] = $listingLayouts['listPattern-type-or']['middle'];
                $translations['Intl']['ListPatternOrEnd'] = $listingLayouts['listPattern-type-or']['end'];
                $translations['Intl']['ListPatternOr2'] = $listingLayouts['listPattern-type-or']['2'];
            }

            $this->getOutput()->writeln('Saved listing layout data for ' . $langCode);
        } catch (\Exception $e) {
            $this->getOutput()->writeln('Unable to import listing layout data for ' . $langCode);
        }
    }

    protected function replacePlaceHolder(string $string, string $replacement = '%s'): string
    {
        return str_replace('{0}', $replacement, $string);
    }
}
