*/ protected array $languages = [ 'en' => 'English', 'af' => 'Afrikaans', 'ar' => 'Arabic', 'bs' => 'Bosnian', 'bg' => 'Bulgarian', 'ca' => 'Catalan', 'ch' => 'Chinese (Simple)', 'cs' => 'Czech', 'cy' => 'Welsh', 'da' => 'Danish', 'de' => 'German', 'el' => 'Greek', 'eo' => 'Esperanto', 'es' => 'Spanish', 'et' => 'Estonian', 'eu' => 'Basque', 'fa' => 'Persian', 'fi' => 'Finnish', 'fo' => 'Faroese', 'fr' => 'French', 'fr_CA' => 'French (Canada)', 'ga' => 'Irish Gaelic', 'gd' => 'Scottish Gaelic', 'gl' => 'Galician', 'he' => 'Hebrew', 'hi' => 'Hindi', 'hr' => 'Croatian', 'hu' => 'Hungarian', 'is' => 'Icelandic', 'it' => 'Italian', 'ja' => 'Japanese', 'ka' => 'Georgian', 'kl' => 'Greenlandic', 'km' => 'Khmer', 'ko' => 'Korean', 'lb' => 'Luxembourgish', 'lo' => 'Lao', 'lt' => 'Lithuanian', 'lv' => 'Latvian', 'mi' => 'Maori', 'mk' => 'Macedonian', 'ms' => 'Malay', 'mt' => 'Maltese', 'nl' => 'Dutch', 'no' => 'Norwegian', 'pl' => 'Polish', 'pt' => 'Portuguese', 'pt_BR' => 'Brazilian Portuguese', 'ro' => 'Romanian', 'ru' => 'Russian', 'rw' => 'Kinyarwanda', 'sk' => 'Slovak', 'sl' => 'Slovenian', 'sn' => 'Shona (Bantu)', 'sq' => 'Albanian', 'sr' => 'Serbian', 'sv' => 'Swedish', 'th' => 'Thai', 'tr' => 'Turkish', 'uk' => 'Ukrainian', 'vi' => 'Vietnamese', 'zh' => 'Chinese (Traditional)', 'zh_CN' => 'Chinese (Simplified)', 'zh_HK' => 'Chinese (Simplified Hong Kong)', 'zh_Hans_CN' => 'Chinese (Simplified Han)', ]; /** * Dependencies for locales. * By default, locales with a country/region suffix such as "de_AT" will automatically have the "de" * locale as fallback. This way TYPO3 only needs to know about the actual "base" language, however * also allows to use country-specific languages. * However, when a specific locale such as "lb" has a dependency to a different "de" suffix, this should * is defined here. * With * $GLOBALS['TYPO3_CONF_VARS']['SYS']['localization']['locales']['dependencies'] * it is possible to extend the dependency list. * * Example: * If "lb" is chosen, but no label was found, a fallback to the label in "de" is used. */ protected array $localeDependencies = [ 'lb' => ['de'], ]; public function __construct() { // Allow user-defined locales foreach ($GLOBALS['TYPO3_CONF_VARS']['SYS']['localization']['locales']['user'] ?? [] as $locale => $name) { if (!is_string($locale) || $locale === '') { continue; } if (!isset($this->languages[$locale])) { $this->languages[$locale] = $name; } } // Merge user-provided locale dependencies if (is_array($GLOBALS['TYPO3_CONF_VARS']['SYS']['localization']['locales']['dependencies'] ?? null)) { $this->localeDependencies = array_replace_recursive( $this->localeDependencies, $GLOBALS['TYPO3_CONF_VARS']['SYS']['localization']['locales']['dependencies'] ); } } public function createLocale(string $localeKey, ?array $alternativeDependencies = null): Locale { if (strpos($localeKey, '.')) { [$sanitizedLocaleKey] = explode('.', $localeKey); } // Find the requested language in this list based on the $languageKey // Language is found. Configure it: if ($localeKey === 'en' || $this->isValidLanguageKey($sanitizedLocaleKey ?? $localeKey)) { return new Locale($localeKey, $alternativeDependencies ?? $this->getLocaleDependencies($sanitizedLocaleKey ?? $localeKey)); } return new Locale(); } /** * Returns the locales. * @return array */ public function getLocales(): array { return array_keys($this->languages); } public function isValidLanguageKey(string $locale): bool { // "en" implicitly equals "default", so this is OK if ($locale === 'en' || $locale === 'default') { return true; } if (!isset($this->languages[$locale])) { // the given locale is not found in the current locales, let us see if // the base language (iso-639-1) is in the list of supported locales. if (str_contains($locale, '_')) { [$baseIsoCodeLanguageKey] = explode('_', $locale); return $this->isValidLanguageKey($baseIsoCodeLanguageKey); } if (str_contains($locale, '-')) { [$baseIsoCodeLanguageKey] = explode('-', $locale); return $this->isValidLanguageKey($baseIsoCodeLanguageKey); } return false; } return true; } /** * Returns the supported languages indexed by their corresponding locale. * @return array */ public function getLanguages(): array { return $this->languages; } /** * Returns a list of all ISO codes / TYPO3 languages that have active language packs, including "en" (English). * @return array */ public function getActiveLanguages(): array { return array_merge( ['en'], array_filter(array_values($GLOBALS['TYPO3_CONF_VARS']['LANG']['availableLocales'] ?? [])) ); } public function isLanguageKeyAvailable(string $languageKey): bool { return in_array($languageKey, $this->getActiveLanguages()) || is_dir(Environment::getLabelsPath() . '/' . $languageKey); } /** * Returns the dependencies of a given locale, if any. * * @return array */ public function getLocaleDependencies(string $locale): array { $dependencies = []; if (isset($this->localeDependencies[$locale])) { $dependencies = $this->localeDependencies[$locale]; // Search for dependencies recursively $localeDependencies = $dependencies; foreach ($localeDependencies as $dependency) { if (isset($this->localeDependencies[$dependency])) { $dependencies = array_merge($dependencies, $this->getLocaleDependencies($dependency)); } } } // Use automatic dependency resolving. // "de_AT" automatically has a dependency on "de". // but only do this if the actual "de_AT" does not have a custom dependency already defined in // $this->localeDependencies if ($dependencies === [] && str_contains($locale, '_')) { [$languageIsoCode] = explode('_', $locale); // "en" and "default" is always implicitly the default fallback dependency if ($languageIsoCode !== 'en') { $dependencies[] = $languageIsoCode; $dependencies = array_merge($dependencies, $this->getLocaleDependencies($languageIsoCode)); } } elseif ($dependencies === [] && str_contains($locale, '-')) { [$languageIsoCode] = explode('-', $locale); // "en" and "default" is always implicitly the default fallback dependency if ($languageIsoCode !== 'en') { $dependencies[] = $languageIsoCode; $dependencies = array_merge($dependencies, $this->getLocaleDependencies($languageIsoCode)); } } return array_unique($dependencies); } /** * Converts the language codes that we get from the client (usually HTTP_ACCEPT_LANGUAGE) * into a TYPO3-readable language code * * @param string $languageCodesList List of language codes. something like 'de,en-us;q=0.9,de-de;q=0.7,es-cl;q=0.6,en;q=0.4,es;q=0.3,zh;q=0.1' * @return non-empty-string A preferred language that TYPO3 supports, or "en" as fallback if none found */ public function getPreferredClientLanguage(string $languageCodesList): string { $allLanguageCodesFromLocales = []; foreach ($this->languages as $locale => $localeTitle) { $locale = str_replace('_', '-', $locale); $allLanguageCodesFromLocales[$locale] = $locale; } $selectedLanguage = 'en'; $preferredLanguages = GeneralUtility::trimExplode(',', $languageCodesList); // Order the preferred languages after they key $sortedPreferredLanguages = []; foreach ($preferredLanguages as $preferredLanguage) { $quality = 1.0; if (str_contains($preferredLanguage, ';q=')) { [$preferredLanguage, $quality] = explode(';q=', $preferredLanguage); } $sortedPreferredLanguages[$preferredLanguage] = $quality; } // Loop through the languages, with the highest priority first arsort($sortedPreferredLanguages, SORT_NUMERIC); foreach ($sortedPreferredLanguages as $preferredLanguage => $quality) { if (isset($allLanguageCodesFromLocales[$preferredLanguage])) { $selectedLanguage = $allLanguageCodesFromLocales[$preferredLanguage]; break; } // Strip the country code from the end [$preferredLanguage] = explode('-', $preferredLanguage); if (isset($allLanguageCodesFromLocales[$preferredLanguage])) { $selectedLanguage = $allLanguageCodesFromLocales[$preferredLanguage]; break; } } return str_replace('-', '_', $selectedLanguage); } /** * Setting locale based on a SiteLanguage's defined locale. Used for frontend rendering. * * @return bool whether the locale was found on the system (and could be set properly) or not */ public static function setSystemLocaleFromSiteLanguage(SiteLanguage $siteLanguage): bool { $locale = $siteLanguage->getLocale()->posixFormatted(); if ($locale === '') { return false; } return self::setLocale($locale, $locale); } /** * @internal not part of TYPO3 Core API, don't use outside of TYPO3 Core */ public static function getAllSystemLocales(): array { $disabledFunctions = GeneralUtility::trimExplode(',', (string)ini_get('disable_functions'), true); if (in_array('exec', $disabledFunctions, true)) { return []; } $rawOutput = []; CommandUtility::exec('locale -a', $rawOutput); sort($rawOutput, SORT_NATURAL); $locales = []; $usedLocaleBasenames = []; foreach ($rawOutput as $item) { // do not show C/POSIX in the list of locales, as this is the default anyway $obj = new Locale($item); if ($obj->getPosixCodeSet() === 'C' || $obj->getPosixCodeSet() === 'POSIX') { continue; } // Skip locales with appended language or country code (e.g. "de_DE.UTF-8", "de_DE.ISO8859-1"). // The user should only choose "de_DE". if (in_array($obj->getName(), $usedLocaleBasenames, true)) { continue; } $locales[] = $item; $usedLocaleBasenames[] = $obj->getName(); } return $locales; } /** * Internal method, which calls itself again, in order to avoid multiple logging issues. * The main reason for this method is that it calls itself again by trying again to set * the locale. Due to sensible defaults, people used the locale "de_AT.utf-8" with the POSIX platform * (see https://en.wikipedia.org/wiki/Locale_(computer_software)#POSIX_platforms) in their site configuration, * even though the target system has "de_AT" and not "de_AT.UTF-8" defined. * "setLocale()" is now called again without the POSIX platform suffix and is checked again if the locale * is then available, and then logs the failed information. */ protected static function setLocale(string $locale, string $localeStringForTrigger): bool { $incomingLocale = $locale; $availableLocales = GeneralUtility::trimExplode(',', $locale, true); // If LC_NUMERIC is set e.g. to 'de_DE' PHP parses float values locale-aware resulting in strings with comma // as decimal point which causes problems with value conversions - so we set all locale types except LC_NUMERIC // @see https://bugs.php.net/bug.php?id=53711 $locale = setlocale(LC_COLLATE, ...$availableLocales); if ($locale) { // As str_* methods are locale aware and turkish has no upper case I // Class autoloading and other checks depending on case changing break with turkish locale LC_CTYPE // @see http://bugs.php.net/bug.php?id=35050 if (!str_starts_with($locale, 'tr')) { setlocale(LC_CTYPE, ...$availableLocales); } setlocale(LC_MONETARY, ...$availableLocales); setlocale(LC_TIME, ...$availableLocales); } else { // Retry again without the "utf-8" POSIX platform suffix if this is given. if (str_contains($incomingLocale, '.')) { [$localeWithoutModifier] = explode('.', $incomingLocale); return self::setLocale($localeWithoutModifier, $incomingLocale); } if ($localeStringForTrigger === $locale) { GeneralUtility::makeInstance(LogManager::class) ->getLogger(__CLASS__) ->error('Locale "' . htmlspecialchars($localeStringForTrigger) . '" not found.'); } else { GeneralUtility::makeInstance(LogManager::class) ->getLogger(__CLASS__) ->error('Locale "' . htmlspecialchars($localeStringForTrigger) . '" and "' . htmlspecialchars($incomingLocale) . '" not found.'); } return false; } return true; } public function createLocaleFromRequest(?ServerRequestInterface $request): Locale { $languageServiceFactory = GeneralUtility::makeInstance(LanguageServiceFactory::class); if ($request !== null && ApplicationType::fromRequest($request)->isFrontend()) { // @todo: the string conversion is needed for the time being, as long as SiteLanguage does not contain // the full locale with all fallbacks, then getTypo3Language() also needs to be removed. $localeString = (string)($request->getAttribute('language')?->getTypo3Language() ?? $request->getAttribute('site')->getDefaultLanguage()->getTypo3Language()); return $this->createLocale($localeString); } return $languageServiceFactory->createFromUserPreferences($GLOBALS['BE_USER'] ?? null)->getLocale(); } public function createLocaleFromUserPreferences(?AbstractUserAuthentication $user): Locale { if ($user && ($user->user['lang'] ?? false)) { return $this->createLocale($user->user['lang']); } return $this->createLocale('en'); } }