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