416 lines
17 KiB
PHP
416 lines
17 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/*
|
|
* This file is part of the TYPO3 CMS project.
|
|
*
|
|
* It is free software; you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License, either version 2
|
|
* of the License, or any later version.
|
|
*
|
|
* For the full copyright and license information, please read the
|
|
* LICENSE.txt file that was distributed with this source code.
|
|
*
|
|
* The TYPO3 project - inspiring people to share!
|
|
*/
|
|
|
|
namespace TYPO3\CMS\Core\Localization;
|
|
|
|
use Psr\Http\Message\ServerRequestInterface;
|
|
use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication;
|
|
use TYPO3\CMS\Core\Core\Environment;
|
|
use TYPO3\CMS\Core\Http\ApplicationType;
|
|
use TYPO3\CMS\Core\Log\LogManager;
|
|
use TYPO3\CMS\Core\SingletonInterface;
|
|
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
|
|
use TYPO3\CMS\Core\Utility\CommandUtility;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
|
|
/**
|
|
* Defines all available TYPO3 system languages, as they differ from actual ISO 639-1 codes.
|
|
* User-defined system languages can be added to $GLOBALS['TYPO3_CONF_VARS']['SYS']['localization']['locales']['user']
|
|
*
|
|
* These system languages are used for determining the proper language labels of XLF files.
|
|
*/
|
|
class Locales implements SingletonInterface
|
|
{
|
|
/**
|
|
* Supported TYPO3 languages with locales
|
|
*
|
|
* @var array<non-empty-string, non-empty-string>
|
|
*/
|
|
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<int, non-empty-string>
|
|
*/
|
|
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<non-empty-string, non-empty-string>
|
|
*/
|
|
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<int, non-empty-string>
|
|
*/
|
|
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<int, non-empty-string>
|
|
*/
|
|
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');
|
|
}
|
|
}
|