‪TYPO3CMS  ‪main
Locales.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Psr\Http\Message\ServerRequestInterface;
28 
36 {
42  protected array ‪$languages = [
43  'default' => 'English',
44  'af' => 'Afrikaans',
45  'ar' => 'Arabic',
46  'bs' => 'Bosnian',
47  'bg' => 'Bulgarian',
48  'ca' => 'Catalan',
49  'ch' => 'Chinese (Simple)',
50  'cs' => 'Czech',
51  'cy' => 'Welsh',
52  'da' => 'Danish',
53  'de' => 'German',
54  'el' => 'Greek',
55  'eo' => 'Esperanto',
56  'es' => 'Spanish',
57  'et' => 'Estonian',
58  'eu' => 'Basque',
59  'fa' => 'Persian',
60  'fi' => 'Finnish',
61  'fo' => 'Faroese',
62  'fr' => 'French',
63  'fr_CA' => 'French (Canada)',
64  'ga' => 'Irish Gaelic',
65  'gd' => 'Scottish Gaelic',
66  'gl' => 'Galician',
67  'he' => 'Hebrew',
68  'hi' => 'Hindi',
69  'hr' => 'Croatian',
70  'hu' => 'Hungarian',
71  'is' => 'Icelandic',
72  'it' => 'Italian',
73  'ja' => 'Japanese',
74  'ka' => 'Georgian',
75  'kl' => 'Greenlandic',
76  'km' => 'Khmer',
77  'ko' => 'Korean',
78  'lb' => 'Luxembourgish',
79  'lt' => 'Lithuanian',
80  'lv' => 'Latvian',
81  'mi' => 'Maori',
82  'mk' => 'Macedonian',
83  'ms' => 'Malay',
84  'mt' => 'Maltese',
85  'nl' => 'Dutch',
86  'no' => 'Norwegian',
87  'pl' => 'Polish',
88  'pt' => 'Portuguese',
89  'pt_BR' => 'Brazilian Portuguese',
90  'ro' => 'Romanian',
91  'ru' => 'Russian',
92  'rw' => 'Kinyarwanda',
93  'sk' => 'Slovak',
94  'sl' => 'Slovenian',
95  'sn' => 'Shona (Bantu)',
96  'sq' => 'Albanian',
97  'sr' => 'Serbian',
98  'sv' => 'Swedish',
99  'th' => 'Thai',
100  'tr' => 'Turkish',
101  'uk' => 'Ukrainian',
102  'vi' => 'Vietnamese',
103  'zh' => 'Chinese (Traditional)',
104  'zh_CN' => 'Chinese (Simplified)',
105  'zh_HK' => 'Chinese (Simplified Hong Kong)',
106  'zh_Hans_CN' => 'Chinese (Simplified Han)',
107  ];
108 
123  protected array ‪$localeDependencies = [
124  'lb' => ['de'],
125  ];
126 
127  public function ‪__construct()
128  {
129  // Allow user-defined locales
130  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['localization']['locales']['user'] ?? [] as $locale => $name) {
131  if (!is_string($locale) || $locale === '') {
132  continue;
133  }
134  if (!isset($this->‪languages[$locale])) {
135  $this->‪languages[$locale] = $name;
136  }
137  }
138  // Merge user-provided locale dependencies
139  if (is_array(‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['localization']['locales']['dependencies'] ?? null)) {
140  $this->localeDependencies = array_replace_recursive(
141  $this->localeDependencies,
142  ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['localization']['locales']['dependencies']
143  );
144  }
145  }
146 
147  public function ‪createLocale(string $localeKey, array $alternativeDependencies = null): ‪Locale
148  {
149  if (strpos($localeKey, '.')) {
150  [$sanitizedLocaleKey] = explode('.', $localeKey);
151  }
152  // Find the requested language in this list based on the $languageKey
153  // Language is found. Configure it:
154  if ($localeKey === 'en' || $this->‪isValidLanguageKey($sanitizedLocaleKey ?? $localeKey)) {
155  return new ‪Locale($localeKey, $alternativeDependencies ?? $this->getLocaleDependencies($sanitizedLocaleKey ?? $localeKey));
156  }
157  return new ‪Locale();
158  }
159 
164  public function getLocales(): array
165  {
166  return array_keys($this->‪languages);
167  }
168 
169  public function ‪isValidLanguageKey(string $locale): bool
170  {
171  // "en" implicitly equals "default", so this is OK
172  if ($locale === 'en' || $locale === 'default') {
173  return true;
174  }
175  if (!isset($this->‪languages[$locale])) {
176  // the given locale is not found in the current locales, let us see if
177  // the base language (iso-639-1) is in the list of supported locales.
178  if (str_contains($locale, '_')) {
179  [$baseIsoCodeLanguageKey] = explode('_', $locale);
180  return $this->‪isValidLanguageKey($baseIsoCodeLanguageKey);
181  }
182  if (str_contains($locale, '-')) {
183  [$baseIsoCodeLanguageKey] = explode('-', $locale);
184  return $this->‪isValidLanguageKey($baseIsoCodeLanguageKey);
185  }
186  return false;
187  }
188  return true;
189  }
190 
195  public function getLanguages(): array
196  {
198  }
199 
204  public function getActiveLanguages(): array
205  {
206  return array_merge(
207  ['default'],
208  array_filter(array_values(‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lang']['availableLanguages'] ?? []))
209  );
210  }
211 
212  public function isLanguageKeyAvailable(string $languageKey): bool
213  {
214  return in_array($languageKey, $this->getActiveLanguages()) || is_dir(‪Environment::getLabelsPath() . '/' . $languageKey);
215  }
216 
222  public function getLocaleDependencies(string $locale): array
223  {
224  $dependencies = [];
225  if (isset($this->localeDependencies[$locale])) {
226  $dependencies = $this->localeDependencies[$locale];
227  // Search for dependencies recursively
228  ‪$localeDependencies = $dependencies;
229  foreach (‪$localeDependencies as $dependency) {
230  if (isset($this->localeDependencies[$dependency])) {
231  $dependencies = array_merge($dependencies, $this->getLocaleDependencies($dependency));
232  }
233  }
234  }
235  // Use automatic dependency resolving.
236  // "de_AT" automatically has a dependency on "de".
237  // but only do this if the actual "de_AT" does not have a custom dependency already defined in
238  // $this->localeDependencies
239  if ($dependencies === [] && str_contains($locale, '_')) {
240  [$languageIsoCode] = explode('_', $locale);
241  // "en" = "default" is always implicitly the default fallback dependency
242  if ($languageIsoCode !== 'en') {
243  $dependencies[] = $languageIsoCode;
244  $dependencies = array_merge($dependencies, $this->getLocaleDependencies($languageIsoCode));
245  }
246  } elseif ($dependencies === [] && str_contains($locale, '-')) {
247  [$languageIsoCode] = explode('-', $locale);
248  // "en" = "default" is always implicitly the default fallback dependency
249  if ($languageIsoCode !== 'en') {
250  $dependencies[] = $languageIsoCode;
251  $dependencies = array_merge($dependencies, $this->getLocaleDependencies($languageIsoCode));
252  }
253  }
254  return array_unique($dependencies);
255  }
256 
264  public function getPreferredClientLanguage(string $languageCodesList): string
265  {
266  $allLanguageCodesFromLocales = ['en' => 'default'];
267  foreach ($this->‪languages as $locale => $localeTitle) {
268  $locale = str_replace('_', '-', $locale);
269  $allLanguageCodesFromLocales[$locale] = $locale;
270  }
271  ‪$selectedLanguage = 'default';
273  // Order the preferred languages after they key
275  foreach (‪$preferredLanguages as $preferredLanguage) {
276  $quality = 1.0;
277  if (str_contains($preferredLanguage, ';q=')) {
278  [$preferredLanguage, $quality] = explode(';q=', $preferredLanguage);
279  }
280  ‪$sortedPreferredLanguages[$preferredLanguage] = $quality;
281  }
282  // Loop through the languages, with the highest priority first
283  arsort(‪$sortedPreferredLanguages, SORT_NUMERIC);
284  foreach (‪$sortedPreferredLanguages as $preferredLanguage => $quality) {
285  if (isset($allLanguageCodesFromLocales[$preferredLanguage])) {
286  ‪$selectedLanguage = $allLanguageCodesFromLocales[$preferredLanguage];
287  break;
288  }
289  // Strip the country code from the end
290  [$preferredLanguage] = explode('-', $preferredLanguage);
291  if (isset($allLanguageCodesFromLocales[$preferredLanguage])) {
292  ‪$selectedLanguage = $allLanguageCodesFromLocales[$preferredLanguage];
293  break;
294  }
295  }
296  if (!‪$selectedLanguage || ‪$selectedLanguage === 'en') {
297  ‪$selectedLanguage = 'default';
298  }
299  return str_replace('-', '_', ‪$selectedLanguage);
300  }
301 
308  public static function ‪setSystemLocaleFromSiteLanguage(‪SiteLanguage $siteLanguage): bool
309  {
310  $locale = $siteLanguage->‪getLocale()->posixFormatted();
311  if ($locale === '') {
312  return false;
313  }
314  return ‪self::setLocale($locale, $locale);
315  }
316 
326  protected static function ‪setLocale(string $locale, string $localeStringForTrigger): bool
327  {
328  $incomingLocale = $locale;
329  $availableLocales = ‪GeneralUtility::trimExplode(',', $locale, true);
330  // If LC_NUMERIC is set e.g. to 'de_DE' PHP parses float values locale-aware resulting in strings with comma
331  // as decimal point which causes problems with value conversions - so we set all locale types except LC_NUMERIC
332  // @see https://bugs.php.net/bug.php?id=53711
333  $locale = setlocale(LC_COLLATE, ...$availableLocales);
334  if ($locale) {
335  // As str_* methods are locale aware and turkish has no upper case I
336  // Class autoloading and other checks depending on case changing break with turkish locale LC_CTYPE
337  // @see http://bugs.php.net/bug.php?id=35050
338  if (!str_starts_with($locale, 'tr')) {
339  setlocale(LC_CTYPE, ...$availableLocales);
340  }
341  setlocale(LC_MONETARY, ...$availableLocales);
342  setlocale(LC_TIME, ...$availableLocales);
343  } else {
344  // Retry again without the "utf-8" POSIX platform suffix if this is given.
345  if (str_contains($incomingLocale, '.')) {
346  [$localeWithoutModifier] = explode('.', $incomingLocale);
347  return ‪self::setLocale($localeWithoutModifier, $incomingLocale);
348  }
349  if ($localeStringForTrigger === $locale) {
350  GeneralUtility::makeInstance(LogManager::class)
351  ->getLogger(__CLASS__)
352  ->error('Locale "' . htmlspecialchars($localeStringForTrigger) . '" not found.');
353  } else {
354  GeneralUtility::makeInstance(LogManager::class)
355  ->getLogger(__CLASS__)
356  ->error('Locale "' . htmlspecialchars($localeStringForTrigger) . '" and "' . htmlspecialchars($incomingLocale) . '" not found.');
357  }
358  return false;
359  }
360  return true;
361  }
362 
363  public function ‪createLocaleFromRequest(?ServerRequestInterface $request): ‪Locale
364  {
365  $languageServiceFactory = GeneralUtility::makeInstance(LanguageServiceFactory::class);
366  if ($request !== null && ‪ApplicationType::fromRequest($request)->isFrontend()) {
367  // @todo: the string conversion is needed for the time being, as long as SiteLanguage does not contain
368  // the full locale with all fallbacks, then getTypo3Language() also needs to be removed.
369  $localeString = (string)($request->getAttribute('language')?->getTypo3Language()
370  ?? $request->getAttribute('site')->getDefaultLanguage()->getTypo3Language());
371  return $this->‪createLocale($localeString);
372  }
373  return $languageServiceFactory->createFromUserPreferences(‪$GLOBALS['BE_USER'] ?? null)->getLocale();
374  }
375 
377  {
378  if ($user && ($user->user['lang'] ?? false)) {
379  return $this->‪createLocale($user->user['lang']);
380  }
381  return $this->‪createLocale('en');
382  }
383 }
‪TYPO3\CMS\Core\Localization\Locales\$sortedPreferredLanguages
‪$sortedPreferredLanguages
Definition: Locales.php:274
‪TYPO3\CMS\Core\Localization\Locales\setSystemLocaleFromSiteLanguage
‪static bool setSystemLocaleFromSiteLanguage(SiteLanguage $siteLanguage)
Definition: Locales.php:308
‪TYPO3\CMS\Core\Localization\Locales\$preferredLanguages
‪$preferredLanguages
Definition: Locales.php:272
‪TYPO3\CMS\Core\Localization\Locales\setLocale
‪static setLocale(string $locale, string $localeStringForTrigger)
Definition: Locales.php:326
‪TYPO3\CMS\Core\Site\Entity\SiteLanguage\getLocale
‪getLocale()
Definition: SiteLanguage.php:174
‪TYPO3\CMS\Core\Core\Environment\getLabelsPath
‪static getLabelsPath()
Definition: Environment.php:233
‪TYPO3\CMS\Core\Localization\Locales\$localeDependencies
‪array $localeDependencies
Definition: Locales.php:123
‪TYPO3\CMS\Core\Localization\Locales
Definition: Locales.php:36
‪TYPO3\CMS\Core\Localization\Locales\__construct
‪__construct()
Definition: Locales.php:127
‪TYPO3\CMS\Core\Localization
Definition: CacheWarmer.php:18
‪TYPO3\CMS\Core\Site\Entity\SiteLanguage
Definition: SiteLanguage.php:27
‪TYPO3\CMS\Core\Localization\Locales\$selectedLanguage
‪foreach($this->languages as $locale=> $localeTitle) $selectedLanguage
Definition: Locales.php:271
‪TYPO3\CMS\Core\Localization\Locales\createLocaleFromRequest
‪createLocaleFromRequest(?ServerRequestInterface $request)
Definition: Locales.php:363
‪TYPO3\CMS\Core\Localization\Locales\createLocale
‪createLocale(string $localeKey, array $alternativeDependencies=null)
Definition: Locales.php:147
‪TYPO3\CMS\Core\Localization\Locales\languages
‪array< non-empty-string, function getLanguages():array { return $this-> languages
Definition: Locales.php:197
‪TYPO3\CMS\Core\SingletonInterface
Definition: SingletonInterface.php:22
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Log\LogManager
Definition: LogManager.php:33
‪TYPO3\CMS\Core\Core\Environment
Definition: Environment.php:41
‪TYPO3\CMS\Core\Localization\Locale
Definition: Locale.php:30
‪TYPO3\CMS\Core\Localization\Locales\isValidLanguageKey
‪array< int, getLocales():array { return array_keys( $this->languages);} public function isValidLanguageKey(string $locale):bool { if( $locale==='en'||$locale==='default') { return true;} if(!isset( $this->languages[ $locale])) { if(str_contains( $locale, '_')) {[ $baseIsoCodeLanguageKey]=explode( '_', $locale);return $this-> isValidLanguageKey($baseIsoCodeLanguageKey)
‪TYPO3\CMS\Core\Http\fromRequest
‪@ fromRequest
Definition: ApplicationType.php:66
‪TYPO3\CMS\Core\Localization\Locales\createLocaleFromUserPreferences
‪createLocaleFromUserPreferences(?AbstractUserAuthentication $user)
Definition: Locales.php:376
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Localization\Locales\$languages
‪array $languages
Definition: Locales.php:42
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode(string $delim, string $string, bool $removeEmptyValues=false, int $limit=0)
Definition: GeneralUtility.php:822
‪TYPO3\CMS\Core\Authentication\AbstractUserAuthentication
Definition: AbstractUserAuthentication.php:65
‪TYPO3\CMS\Core\Http\ApplicationType
‪ApplicationType
Definition: ApplicationType.php:55