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