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