[BUGFIX] Fix Typo3DbQueryParserTest for custom php timezones
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Localization / Locales.php
1 <?php
2
3 /*
4 * This file is part of the TYPO3 CMS project.
5 *
6 * It is free software; you can redistribute it and/or modify it under
7 * the terms of the GNU General Public License, either version 2
8 * of the License, or any later version.
9 *
10 * For the full copyright and license information, please read the
11 * LICENSE.txt file that was distributed with this source code.
12 *
13 * The TYPO3 project - inspiring people to share!
14 */
15
16 namespace TYPO3\CMS\Core\Localization;
17
18 use TYPO3\CMS\Core\Log\LogManager;
19 use TYPO3\CMS\Core\SingletonInterface;
20 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
21 use TYPO3\CMS\Core\Utility\GeneralUtility;
22
23 /**
24 * Locales. Used to define TYPO3- system languages
25 * When adding new keys, remember to:
26 * - Update 'setup' extension labels (sysext/setup/Resources/Private/Language/locallang.xlf)
27 * That's it!
28 */
29 class Locales implements SingletonInterface
30 {
31 /**
32 * Supported TYPO3 languages with locales
33 *
34 * @var array
35 */
36 protected $languages = [
37 'default' => 'English',
38 'af' => 'Afrikaans',
39 'ar' => 'Arabic',
40 'bs' => 'Bosnian',
41 'bg' => 'Bulgarian',
42 'ca' => 'Catalan',
43 'ch' => 'Chinese (Simple)',
44 'cs' => 'Czech',
45 'da' => 'Danish',
46 'de' => 'German',
47 'el' => 'Greek',
48 'eo' => 'Esperanto',
49 'es' => 'Spanish',
50 'et' => 'Estonian',
51 'eu' => 'Basque',
52 'fa' => 'Persian',
53 'fi' => 'Finnish',
54 'fo' => 'Faroese',
55 'fr' => 'French',
56 'fr_CA' => 'French (Canada)',
57 'gl' => 'Galician',
58 'he' => 'Hebrew',
59 'hi' => 'Hindi',
60 'hr' => 'Croatian',
61 'hu' => 'Hungarian',
62 'is' => 'Icelandic',
63 'it' => 'Italian',
64 'ja' => 'Japanese',
65 'ka' => 'Georgian',
66 'kl' => 'Greenlandic',
67 'km' => 'Khmer',
68 'ko' => 'Korean',
69 'lt' => 'Lithuanian',
70 'lv' => 'Latvian',
71 'mi' => 'Maori',
72 'mk' => 'Macedonian',
73 'ms' => 'Malay',
74 'nl' => 'Dutch',
75 'no' => 'Norwegian',
76 'pl' => 'Polish',
77 'pt' => 'Portuguese',
78 'pt_BR' => 'Brazilian Portuguese',
79 'ro' => 'Romanian',
80 'ru' => 'Russian',
81 'rw' => 'Kinyarwanda',
82 'sk' => 'Slovak',
83 'sl' => 'Slovenian',
84 'sn' => 'Shona (Bantu)',
85 'sq' => 'Albanian',
86 'sr' => 'Serbian',
87 'sv' => 'Swedish',
88 'th' => 'Thai',
89 'tr' => 'Turkish',
90 'uk' => 'Ukrainian',
91 'vi' => 'Vietnamese',
92 'zh' => 'Chinese (Trad)'
93 ];
94
95 /**
96 * Reversed mapping for backward compatibility codes
97 *
98 * @var array
99 */
100 protected $isoReverseMapping = [
101 'bs' => 'ba', // Bosnian
102 'cs' => 'cz', // Czech
103 'da' => 'dk', // Danish
104 'el' => 'gr', // Greek
105 'fr_CA' => 'qc', // French (Canada)
106 'gl' => 'ga', // Galician
107 'ja' => 'jp', // Japanese
108 'ka' => 'ge', // Georgian
109 'kl' => 'gl', // Greenlandic
110 'ko' => 'kr', // Korean
111 'ms' => 'my', // Malay
112 'pt_BR' => 'br', // Portuguese (Brazil)
113 'sl' => 'si', // Slovenian
114 'sv' => 'se', // Swedish
115 'uk' => 'ua', // Ukrainian
116 'vi' => 'vn', // Vietnamese
117 'zh' => 'hk', // Chinese (China)
118 'zh_CN' => 'ch', // Chinese (Simplified)
119 'zh_HK' => 'hk', // Chinese (Simplified Hong Kong)
120 'zh_Hans_CN' => 'ch' // Chinese (Simplified Han)
121 ];
122
123 /**
124 * Dependencies for locales
125 * This is a reverse mapping for the built-in languages within $this->languages that contain 5-letter codes.
126 *
127 * @var array
128 */
129 protected $localeDependencies = [
130 'pt_BR' => ['pt'],
131 'fr_CA' => ['fr']
132 ];
133
134 public function __construct()
135 {
136 // Allow user-defined locales
137 foreach ($GLOBALS['TYPO3_CONF_VARS']['SYS']['localization']['locales']['user'] ?? [] as $locale => $name) {
138 if (!isset($this->languages[$locale])) {
139 $this->languages[$locale] = $name;
140 }
141 // Initializes the locale dependencies with TYPO3 supported locales
142 if (strlen($locale) === 5) {
143 $this->localeDependencies[$locale] = [substr($locale, 0, 2)];
144 }
145 }
146 // Merge user-provided locale dependencies
147 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SYS']['localization']['locales']['dependencies'] ?? null)) {
148 $this->localeDependencies = array_replace_recursive(
149 $this->localeDependencies,
150 $GLOBALS['TYPO3_CONF_VARS']['SYS']['localization']['locales']['dependencies']
151 );
152 }
153 }
154
155 /**
156 * Returns the locales.
157 *
158 * @return array
159 */
160 public function getLocales()
161 {
162 return array_keys($this->languages);
163 }
164
165 /**
166 * Returns the supported languages indexed by their corresponding locale.
167 *
168 * @return array
169 */
170 public function getLanguages()
171 {
172 return $this->languages;
173 }
174
175 /**
176 * Returns the mapping between TYPO3 (old) language codes and ISO codes.
177 *
178 * @return array
179 */
180 public function getIsoMapping()
181 {
182 return array_flip($this->isoReverseMapping);
183 }
184
185 /**
186 * Returns the dependencies of a given locale, if any.
187 *
188 * @param string $locale
189 * @return array
190 */
191 public function getLocaleDependencies($locale)
192 {
193 $dependencies = [];
194 if (isset($this->localeDependencies[$locale])) {
195 $dependencies = $this->localeDependencies[$locale];
196 // Search for dependencies recursively
197 $localeDependencies = $dependencies;
198 foreach ($localeDependencies as $dependency) {
199 if (isset($this->localeDependencies[$dependency])) {
200 $dependencies = array_merge($dependencies, $this->getLocaleDependencies($dependency));
201 }
202 }
203 }
204 return $dependencies;
205 }
206
207 /**
208 * Converts the language codes that we get from the client (usually HTTP_ACCEPT_LANGUAGE)
209 * into a TYPO3-readable language code
210 *
211 * @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'
212 * @return string A preferred language that TYPO3 supports, or "default" if none found
213 */
214 public function getPreferredClientLanguage($languageCodesList)
215 {
216 $allLanguageCodesFromLocales = ['en' => 'default'];
217 foreach ($this->isoReverseMapping as $isoLang => $typo3Lang) {
218 $isoLang = str_replace('_', '-', $isoLang);
219 $allLanguageCodesFromLocales[$isoLang] = $typo3Lang;
220 }
221 foreach ($this->getLocales() as $locale) {
222 $locale = str_replace('_', '-', $locale);
223 $allLanguageCodesFromLocales[$locale] = $locale;
224 }
225 $selectedLanguage = 'default';
226 $preferredLanguages = GeneralUtility::trimExplode(',', $languageCodesList);
227 // Order the preferred languages after they key
228 $sortedPreferredLanguages = [];
229 foreach ($preferredLanguages as $preferredLanguage) {
230 $quality = 1.0;
231 if (strpos($preferredLanguage, ';q=') !== false) {
232 [$preferredLanguage, $quality] = explode(';q=', $preferredLanguage);
233 }
234 $sortedPreferredLanguages[$preferredLanguage] = $quality;
235 }
236 // Loop through the languages, with the highest priority first
237 arsort($sortedPreferredLanguages, SORT_NUMERIC);
238 foreach ($sortedPreferredLanguages as $preferredLanguage => $quality) {
239 if (isset($allLanguageCodesFromLocales[$preferredLanguage])) {
240 $selectedLanguage = $allLanguageCodesFromLocales[$preferredLanguage];
241 break;
242 }
243 // Strip the country code from the end
244 [$preferredLanguage, ] = explode('-', $preferredLanguage);
245 if (isset($allLanguageCodesFromLocales[$preferredLanguage])) {
246 $selectedLanguage = $allLanguageCodesFromLocales[$preferredLanguage];
247 break;
248 }
249 }
250 if (!$selectedLanguage || $selectedLanguage === 'en') {
251 $selectedLanguage = 'default';
252 }
253 return $selectedLanguage;
254 }
255
256 /**
257 * Setting locale based on a SiteLanguage's defined locale.
258 * Used for frontend rendering, previously set within TSFE->settingLocale
259 *
260 * @param SiteLanguage $siteLanguage
261 * @return bool whether the locale was found on the system (and could be set properly) or not
262 */
263 public static function setSystemLocaleFromSiteLanguage(SiteLanguage $siteLanguage): bool
264 {
265 $locale = $siteLanguage->getLocale();
266 // No locale was given, so return false;
267 if (!$locale) {
268 return false;
269 }
270 $availableLocales = GeneralUtility::trimExplode(',', $locale, true);
271 // If LC_NUMERIC is set e.g. to 'de_DE' PHP parses float values locale-aware resulting in strings with comma
272 // as decimal point which causes problems with value conversions - so we set all locale types except LC_NUMERIC
273 // @see https://bugs.php.net/bug.php?id=53711
274 $locale = setlocale(LC_COLLATE, ...$availableLocales);
275 if ($locale) {
276 // As str_* methods are locale aware and turkish has no upper case I
277 // Class autoloading and other checks depending on case changing break with turkish locale LC_CTYPE
278 // @see http://bugs.php.net/bug.php?id=35050
279 if (strpos($locale, 'tr') !== 0) {
280 setlocale(LC_CTYPE, ...$availableLocales);
281 }
282 setlocale(LC_MONETARY, ...$availableLocales);
283 setlocale(LC_TIME, ...$availableLocales);
284 } else {
285 GeneralUtility::makeInstance(LogManager::class)
286 ->getLogger(__CLASS__)
287 ->error('Locale "' . htmlspecialchars($siteLanguage->getLocale()) . '" not found.');
288 return false;
289 }
290 return true;
291 }
292 }