[TASK] Add language debug mode to All Configuration
[Packages/TYPO3.CMS.git] / typo3 / sysext / lang / Classes / LanguageService.php
1 <?php
2 namespace TYPO3\CMS\Lang;
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\Charset\CharsetConverter;
18 use TYPO3\CMS\Core\Localization\Locales;
19 use TYPO3\CMS\Core\Localization\LocalizationFactory;
20 use TYPO3\CMS\Core\Utility\ArrayUtility;
21 use TYPO3\CMS\Core\Utility\GeneralUtility;
22
23 /**
24 * Contains the TYPO3 Backend Language class
25 * For detailed information about how localization is handled,
26 * please refer to the 'Inside TYPO3' document which describes this.
27 * This class is normally instantiated as the global variable $GLOBALS['LANG']
28 * It's only available in the backend and under certain circumstances in the frontend
29 * @see \TYPO3\CMS\Backend\Template\DocumentTemplate
30 */
31 class LanguageService
32 {
33 /**
34 * This is set to the language that is currently running for the user
35 *
36 * @var string
37 */
38 public $lang = 'default';
39
40 /**
41 * Default charset in backend, is always UTF-8, option is not in use anymore, but is kept for third-party extensions
42 * using this option. Will be removed in future versions
43 *
44 * @var string
45 */
46 public $charSet = 'utf-8';
47
48 /**
49 * If TRUE, will show the key/location of labels in the backend.
50 *
51 * @var bool
52 */
53 public $debugKey = false;
54
55 /**
56 * Can contain labels and image references from the backend modules.
57 * Relies on \TYPO3\CMS\Backend\Module\ModuleLoader to initialize modules after a global instance of $LANG has been created.
58 * @deprecated since TYPO3 v8, will be removed in TYPO3 v9 - use ModuleLoader directly instead.
59 *
60 * @var array
61 */
62 public $moduleLabels = [];
63
64 /**
65 * Internal cache for read LL-files
66 *
67 * @var array
68 */
69 public $LL_files_cache = [];
70
71 /**
72 * Internal cache for ll-labels (filled as labels are requested)
73 *
74 * @var array
75 */
76 public $LL_labels_cache = [];
77
78 /**
79 * instance of the "\TYPO3\CMS\Core\Charset\CharsetConverter" class. May be used by any application.
80 *
81 * @var \TYPO3\CMS\Core\Charset\CharsetConverter
82 * @deprecated since TYPO3 v8, will be removed in TYPO3 v9. Charset is a singleton, load it via GeneralUtility directly
83 */
84 public $csConvObj;
85
86 /**
87 * instance of the parser factory
88 *
89 * @var LocalizationFactory
90 * @deprecated since TYPO3 v8, will be removed in TYPO3 v9, as LocalizationFactory is a singleton, load it via
91 * GeneralUtility directly
92 */
93 public $parserFactory;
94
95 /**
96 * List of language dependencies for actual language. This is used for local variants of a language
97 * that depend on their "main" language, like Brazilian Portuguese or Canadian French.
98 *
99 * @var array
100 */
101 protected $languageDependencies = [];
102
103 /**
104 * LanguageService constructor.
105 */
106 public function __construct()
107 {
108 // Initialize the conversion object
109 $this->csConvObj = GeneralUtility::makeInstance(CharsetConverter::class);
110 // Initialize the localization factory object
111 $this->parserFactory = GeneralUtility::makeInstance(LocalizationFactory::class);
112 if ($GLOBALS['TYPO3_CONF_VARS']['BE']['languageDebug']) {
113 $this->debugKey = true;
114 }
115 }
116
117 /**
118 * Initializes the backend language.
119 * This is for example done in \TYPO3\CMS\Backend\Template\DocumentTemplate with lines like these:
120 * $LANG = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Lang\LanguageService::class);
121 * $LANG->init($GLOBALS['BE_USER']->uc['lang']);
122 *
123 * @throws \RuntimeException
124 * @param string $languageKey The language key (two character string from backend users profile)
125 * @return void
126 */
127 public function init($languageKey)
128 {
129 // Find the requested language in this list based on the $languageKey
130 /** @var $locales \TYPO3\CMS\Core\Localization\Locales */
131 $locales = GeneralUtility::makeInstance(Locales::class);
132 // Language is found. Configure it:
133 if (in_array($languageKey, $locales->getLocales())) {
134 // The current language key
135 $this->lang = $languageKey;
136 $this->languageDependencies[] = $languageKey;
137 foreach ($locales->getLocaleDependencies($languageKey) as $language) {
138 $this->languageDependencies[] = $language;
139 }
140 }
141 }
142
143 /**
144 * Gets the parser factory.
145 *
146 * @return LocalizationFactory
147 * @deprecated since TYPO3 v8, will be removed in TYPO3 v9
148 */
149 public function getParserFactory()
150 {
151 GeneralUtility::logDeprecatedFunction();
152 return $this->parserFactory;
153 }
154
155 /**
156 * Adds labels and image references from the backend modules to the internal moduleLabels array
157 *
158 * @param array $arr Array with references to module labels, keys: ['labels']['table'],
159 * @param string $prefix Module name prefix
160 * @return void
161 * @see \TYPO3\CMS\Backend\Module\ModuleLoader
162 * @deprecated since TYPO3 v8, will be removed in TYPO3 v9 - use ModuleLoader instead.
163 */
164 public function addModuleLabels($arr, $prefix)
165 {
166 GeneralUtility::logDeprecatedFunction();
167 if (is_array($arr)) {
168 foreach ($arr as $k => $larr) {
169 if (!isset($this->moduleLabels[$k])) {
170 $this->moduleLabels[$k] = [];
171 }
172 if (is_array($larr)) {
173 foreach ($larr as $l => $v) {
174 $this->moduleLabels[$k][$prefix . $l] = $v;
175 }
176 }
177 }
178 }
179 }
180
181 /**
182 * Will convert the input strings special chars (all above 127) to entities.
183 * The string is expected to be encoded in UTF-8
184 * This function is used to create strings that can be used in the Click Menu
185 * (Context Sensitive Menus). The reason is that the values that are dynamically
186 * written into the <div> layer is decoded as iso-8859-1 no matter what charset
187 * is used in the document otherwise (only MSIE, Mozilla is OK).
188 * So by converting we by-pass this problem.
189 *
190 * @param string $str Input string
191 * @return string Output string
192 * @deprecated since TYPO3 v8, will be removed in TYPO3 v9
193 */
194 public function makeEntities($str)
195 {
196 GeneralUtility::logDeprecatedFunction();
197 // Convert string back again, but using the full entity conversion:
198 return $this->csConvObj->utf8_to_entities($str);
199 }
200
201 /**
202 * Debugs localization key.
203 *
204 * @param string $value value to debug
205 * @return string
206 */
207 public function debugLL($value)
208 {
209 return $this->debugKey ? '[' . $value . ']' : '';
210 }
211
212 /**
213 * Returns the label with key $index from the globally loaded $LOCAL_LANG array.
214 * Mostly used from modules with only one LOCAL_LANG file loaded into the global space.
215 *
216 * @param string $index Label key
217 * @param bool $hsc DEPRECATED If set, the return value is htmlspecialchar'ed
218 * @return string
219 */
220 public function getLL($index, $hsc = false)
221 {
222 return $this->getLLL($index, $GLOBALS['LOCAL_LANG'], $hsc);
223 }
224
225 /**
226 * Returns the label with key $index from the $LOCAL_LANG array used as the second argument
227 *
228 * @param string $index Label key
229 * @param array $localLanguage $LOCAL_LANG array to get label key from
230 * @param bool $hsc DEPRECATED If set, the return value is htmlspecialchar'ed
231 * @return string
232 */
233 public function getLLL($index, $localLanguage, $hsc = false)
234 {
235 // @deprecated since TYPO3 v8, will be removed in TYPO3 v9
236 if ($hsc) {
237 GeneralUtility::deprecationLog(
238 'Calling getLLL() with argument \'hsc\' has been deprecated.'
239 );
240 }
241
242 // Get Local Language. Special handling for all extensions that
243 // read PHP LL files and pass arrays here directly.
244 if (isset($localLanguage[$this->lang][$index])) {
245 $value = is_string($localLanguage[$this->lang][$index])
246 ? $localLanguage[$this->lang][$index]
247 : $localLanguage[$this->lang][$index][0]['target'];
248 } elseif (isset($localLanguage['default'][$index])) {
249 $value = is_string($localLanguage['default'][$index])
250 ? $localLanguage['default'][$index]
251 : $localLanguage['default'][$index][0]['target'];
252 } else {
253 $value = '';
254 }
255 if ($hsc) {
256 $value = htmlspecialchars($value);
257 }
258 return $value . $this->debugLL($index);
259 }
260
261 /**
262 * splitLabel function
263 *
264 * All translations are based on $LOCAL_LANG variables.
265 * 'language-splitted' labels can therefore refer to a local-lang file + index.
266 * Refer to 'Inside TYPO3' for more details
267 *
268 * @param string $input Label key/reference
269 * @param bool $hsc DEPRECATED If set, the return value is htmlspecialchar'ed
270 * @return string
271 */
272 public function sL($input, $hsc = false)
273 {
274 // @deprecated since TYPO3 v8, will be removed in TYPO3 v9
275 if ($hsc) {
276 GeneralUtility::deprecationLog(
277 'Calling sL() with argument \'hsc\' has been deprecated.'
278 );
279 }
280
281 $identifier = $input . '_' . (int)$hsc . '_' . (int)$this->debugKey;
282 if (isset($this->LL_labels_cache[$this->lang][$identifier])) {
283 return $this->LL_labels_cache[$this->lang][$identifier];
284 }
285 if (strpos($input, 'LLL:') === 0) {
286 $restStr = trim(substr($input, 4));
287 $extPrfx = '';
288 // ll-file referred to is found in an extension.
289 if (strpos($restStr, 'EXT:') === 0) {
290 $restStr = trim(substr($restStr, 4));
291 $extPrfx = 'EXT:';
292 }
293 $parts = explode(':', $restStr);
294 $parts[0] = $extPrfx . $parts[0];
295 // Getting data if not cached
296 if (!isset($this->LL_files_cache[$parts[0]])) {
297 $this->LL_files_cache[$parts[0]] = $this->readLLfile($parts[0]);
298 }
299 $output = $this->getLLL($parts[1], $this->LL_files_cache[$parts[0]]);
300 } else {
301 // Use a constant non-localizable label
302 $output = $input;
303 }
304 if ($hsc) {
305 $output = htmlspecialchars($output, ENT_COMPAT, 'UTF-8', false);
306 }
307 $output .= $this->debugLL($input);
308 $this->LL_labels_cache[$this->lang][$identifier] = $output;
309 return $output;
310 }
311
312 /**
313 * Loading $TCA_DESCR[$table]['columns'] with content from locallang files
314 * as defined in $TCA_DESCR[$table]['refs']
315 * $TCA_DESCR is a global var
316 *
317 * @param string $table Table name found as key in global array $TCA_DESCR
318 * @return void
319 */
320 public function loadSingleTableDescription($table)
321 {
322 // First the 'table' cannot already be loaded in [columns]
323 // and secondly there must be a references to locallang files available in [refs]
324 if (is_array($GLOBALS['TCA_DESCR'][$table]) && !isset($GLOBALS['TCA_DESCR'][$table]['columns']) && is_array($GLOBALS['TCA_DESCR'][$table]['refs'])) {
325 // Init $TCA_DESCR for $table-key
326 $GLOBALS['TCA_DESCR'][$table]['columns'] = [];
327 // Get local-lang for each file in $TCA_DESCR[$table]['refs'] as they are ordered.
328 foreach ($GLOBALS['TCA_DESCR'][$table]['refs'] as $llfile) {
329 $localLanguage = $this->includeLLFile($llfile, 0, 1);
330 // Traverse all keys
331 if (is_array($localLanguage['default'])) {
332 foreach ($localLanguage['default'] as $lkey => $lVal) {
333 // Exploding by '.':
334 // 0-n => fieldname,
335 // n+1 => type from (alttitle, description, details, syntax, image_descr,image,seeAlso),
336 // n+2 => special instruction, if any
337 $keyParts = explode('.', $lkey);
338 $keyPartsCount = count($keyParts);
339 // Check if last part is special instruction
340 // Only "+" is currently supported
341 $specialInstruction = $keyParts[$keyPartsCount - 1] === '+';
342 if ($specialInstruction) {
343 array_pop($keyParts);
344 }
345 // If there are more than 2 parts, get the type from the last part
346 // and merge back the other parts with a dot (.)
347 // Otherwise just get type and field name straightaway
348 if ($keyPartsCount > 2) {
349 $type = array_pop($keyParts);
350 $fieldName = implode('.', $keyParts);
351 } else {
352 $fieldName = $keyParts[0];
353 $type = $keyParts[1];
354 }
355 // Detecting 'hidden' labels, converting to normal fieldname
356 if ($fieldName === '_') {
357 $fieldName = '';
358 }
359 if ($fieldName !== '' && $fieldName[0] === '_') {
360 $fieldName = substr($fieldName, 1);
361 }
362 // Append label
363 $label = $lVal[0]['target'] ? :
364 $lVal[0]['source'];
365 if ($specialInstruction) {
366 $GLOBALS['TCA_DESCR'][$table]['columns'][$fieldName][$type] .= LF . $label;
367 } else {
368 // Substitute label
369 $GLOBALS['TCA_DESCR'][$table]['columns'][$fieldName][$type] = $label;
370 }
371 }
372 }
373 }
374 }
375 }
376
377 /**
378 * Includes locallang file (and possibly additional localized version if configured for)
379 * Read language labels will be merged with $LOCAL_LANG (if $setGlobal = TRUE).
380 *
381 * @param string $fileRef $fileRef is a file-reference
382 * @param bool $setGlobal Setting in global variable $LOCAL_LANG (or returning the variable)
383 * @param bool $mergeLocalOntoDefault
384 * @return mixed if $setGlobal===TRUE, LL-files set $LOCAL_LANG in global scope, or array is returned from function
385 */
386 public function includeLLFile($fileRef, $setGlobal = true, $mergeLocalOntoDefault = false)
387 {
388 $globalLanguage = [];
389 // Get default file
390 $localLanguage = $this->readLLfile($fileRef);
391 if (is_array($localLanguage) && !empty($localLanguage)) {
392 // it depends on, whether we should return the result or set it in the global $LOCAL_LANG array
393 if ($setGlobal) {
394 $globalLanguage = (array)$GLOBALS['LOCAL_LANG'];
395 ArrayUtility::mergeRecursiveWithOverrule($globalLanguage, $localLanguage);
396 } else {
397 $globalLanguage = $localLanguage;
398 }
399 // Merge local onto default
400 if ($mergeLocalOntoDefault && $this->lang !== 'default' && is_array($globalLanguage[$this->lang]) && is_array($globalLanguage['default'])) {
401 // array_merge can be used so far the keys are not
402 // numeric - which we assume they are not...
403 $globalLanguage['default'] = array_merge($globalLanguage['default'], $globalLanguage[$this->lang]);
404 unset($globalLanguage[$this->lang]);
405 }
406 }
407 // Return value if not global is set.
408 if (!$setGlobal) {
409 return $globalLanguage;
410 } else {
411 $GLOBALS['LOCAL_LANG'] = $globalLanguage;
412 return null;
413 }
414 }
415
416 /**
417 * Includes a locallang file and returns the $LOCAL_LANG array found inside.
418 *
419 * @param string $fileRef Input is a file-reference to be a 'local_lang' file containing a $LOCAL_LANG array
420 * @return array value of $LOCAL_LANG found in the included file, empty if non found
421 */
422 protected function readLLfile($fileRef)
423 {
424 /** @var $languageFactory LocalizationFactory */
425 $languageFactory = GeneralUtility::makeInstance(LocalizationFactory::class);
426
427 if ($this->lang !== 'default') {
428 $languages = array_reverse($this->languageDependencies);
429 } else {
430 $languages = ['default'];
431 }
432 $localLanguage = [];
433 foreach ($languages as $language) {
434 $tempLL = $languageFactory->getParsedData($fileRef, $language, 'utf-8');
435 $localLanguage['default'] = $tempLL['default'];
436 if (!isset($localLanguage[$this->lang])) {
437 $localLanguage[$this->lang] = $localLanguage['default'];
438 }
439 if ($this->lang !== 'default' && isset($tempLL[$language])) {
440 // Merge current language labels onto labels from previous language
441 // This way we have a labels with fall back applied
442 ArrayUtility::mergeRecursiveWithOverrule($localLanguage[$this->lang], $tempLL[$language], true, false);
443 }
444 }
445 return $localLanguage;
446 }
447
448 /**
449 * Overrides a label.
450 *
451 * @param string $index
452 * @param string $value
453 * @param bool $overrideDefault Overrides default language
454 * @return void
455 * @deprecated since TYPO3 v8, will be removed in TYPO3 v9
456 */
457 public function overrideLL($index, $value, $overrideDefault = true)
458 {
459 GeneralUtility::logDeprecatedFunction();
460 if (!isset($GLOBALS['LOCAL_LANG'])) {
461 $GLOBALS['LOCAL_LANG'] = [];
462 }
463 $GLOBALS['LOCAL_LANG'][$this->lang][$index][0]['target'] = $value;
464 if ($overrideDefault) {
465 $GLOBALS['LOCAL_LANG']['default'][$index][0]['target'] = $value;
466 }
467 }
468
469 /**
470 * Gets labels with a specific fetched from the current locallang file.
471 * This is useful for e.g gathering javascript labels.
472 *
473 * @param string $prefix Prefix to select the correct labels
474 * @param string $strip Sub-prefix to be removed from label names in the result
475 * @return array Processed labels
476 */
477 public function getLabelsWithPrefix($prefix, $strip = '')
478 {
479 $extraction = [];
480 $labels = array_merge((array)$GLOBALS['LOCAL_LANG']['default'], (array)$GLOBALS['LOCAL_LANG'][$this->lang]);
481 // Regular expression to strip the selection prefix and possibly something from the label name:
482 $labelPattern = '#^' . preg_quote($prefix, '#') . '(' . preg_quote($strip, '#') . ')?#';
483 // Iterate through all locallang labels:
484 foreach ($labels as $label => $value) {
485 if (strpos($label, $prefix) === 0) {
486 $key = preg_replace($labelPattern, '', $label);
487 $extraction[$key] = $value;
488 }
489 }
490 return $extraction;
491 }
492 }