1 | <?php |
||
2 | /** |
||
3 | * @package toolkit |
||
4 | */ |
||
5 | |||
6 | /** |
||
7 | * The Lang class loads and manages languages |
||
8 | */ |
||
9 | class Lang |
||
10 | { |
||
11 | /** |
||
12 | * Array of transliterations |
||
13 | * @var array |
||
14 | */ |
||
15 | private static $_transliterations; |
||
16 | |||
17 | /** |
||
18 | * Code of active language |
||
19 | * @var string |
||
20 | */ |
||
21 | private static $_lang; |
||
22 | |||
23 | /** |
||
24 | * Context information of all available languages |
||
25 | * @var array |
||
26 | */ |
||
27 | private static $_languages; |
||
28 | |||
29 | /** |
||
30 | * Array of localized strings |
||
31 | * @var array |
||
32 | */ |
||
33 | private static $_dictionary; |
||
34 | |||
35 | /** |
||
36 | * Array of localized date and time strings |
||
37 | * @var array |
||
38 | */ |
||
39 | private static $_datetime_dictionary; |
||
40 | |||
41 | /** |
||
42 | * Get the current dictionary |
||
43 | * |
||
44 | * @return array |
||
45 | * Return the dictionary |
||
46 | */ |
||
47 | public static function Dictionary() |
||
48 | { |
||
49 | return self::$_dictionary; |
||
50 | } |
||
51 | |||
52 | /** |
||
53 | * Get a list of either enabled or disabled languages. Example: |
||
54 | * |
||
55 | * array( |
||
56 | * [...] |
||
57 | * |
||
58 | * 'en' => array( |
||
59 | * 'name' => 'English', |
||
60 | * 'handle' => 'english', |
||
61 | * 'extensions' => array() |
||
62 | * ), |
||
63 | * |
||
64 | * 'it' => array( |
||
65 | * 'name' => 'Italiano', |
||
66 | * 'handle' => 'italian', |
||
67 | * 'extensions' => array( |
||
68 | * [...] |
||
69 | * ) |
||
70 | * ), |
||
71 | * |
||
72 | * [...] |
||
73 | * ) |
||
74 | * |
||
75 | * @see toolkit.Lang#createLanguage() |
||
76 | * @since Symphony 2.3 |
||
77 | * @return array |
||
78 | * Return an array of languages (both enabled and disabled) |
||
79 | */ |
||
80 | public static function Languages() |
||
81 | { |
||
82 | return self::$_languages; |
||
83 | } |
||
84 | |||
85 | /** |
||
86 | * Get transliterations |
||
87 | * |
||
88 | * @return array |
||
89 | * Returns the transliterations array |
||
90 | */ |
||
91 | public static function Transliterations() |
||
92 | { |
||
93 | return self::$_transliterations; |
||
94 | } |
||
95 | |||
96 | /** |
||
97 | * Initialize transliterations, datetime dictionary and languages array. |
||
98 | */ |
||
99 | public static function initialize() |
||
100 | { |
||
101 | self::$_dictionary = array(); |
||
102 | |||
103 | // Load default datetime strings |
||
104 | if (empty(self::$_datetime_dictionary)) { |
||
105 | self::$_datetime_dictionary = include LANG . '/datetime.php'; |
||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
106 | } |
||
107 | |||
108 | // Load default transliterations |
||
109 | if (empty(self::$_transliterations)) { |
||
110 | self::$_transliterations = include LANG . '/transliterations.php'; |
||
111 | } |
||
112 | |||
113 | // Load default English language |
||
114 | if (empty(self::$_languages)) { |
||
115 | self::$_languages = self::createLanguage('en', 'English', 'english'); |
||
116 | } |
||
117 | |||
118 | // Fetch all available languages |
||
119 | self::fetch(); |
||
120 | } |
||
121 | |||
122 | /** |
||
123 | * Create an array of Language information for internal use. |
||
124 | * |
||
125 | * @since Symphony 2.3 |
||
126 | * @param string $code |
||
127 | * Language code, e. g. 'en' or 'pt-br' |
||
128 | * @param string $name |
||
129 | * Language name |
||
130 | * @param string $handle (optional) |
||
131 | * Handle for the given language, used to build a valid 'lang_$handle' extension's handle. |
||
132 | * Defaults to null. |
||
133 | * @param array $extensions (optional) |
||
134 | * An array of extensions that support the given language. |
||
135 | * @return array |
||
136 | * An array of Language information. |
||
137 | */ |
||
138 | private static function createLanguage($code, $name, $handle = null, array $extensions = array()) |
||
0 ignored issues
–
show
|
|||
139 | { |
||
140 | return array( |
||
141 | $code => array( |
||
142 | 'name' => $name, |
||
143 | 'handle' => $handle, |
||
144 | 'extensions' => $extensions |
||
145 | ) |
||
146 | ); |
||
147 | } |
||
148 | |||
149 | /** |
||
150 | * Fetch all languages available in the core language folder and the language extensions. |
||
151 | * The function stores all language information in the private variable `$_languages`. |
||
152 | * It contains an array with the name and handle of each language and an array of all |
||
153 | * extensions available in that language. |
||
154 | * |
||
155 | * @throws UnexpectedValueException |
||
156 | * @throws RuntimeException |
||
157 | */ |
||
158 | private static function fetch() |
||
159 | { |
||
160 | if (!@is_readable(EXTENSIONS)) { |
||
0 ignored issues
–
show
|
|||
161 | return; |
||
162 | } |
||
163 | |||
164 | // Fetch extensions |
||
165 | $extensions = new FilesystemIterator(EXTENSIONS); |
||
166 | |||
167 | // Language extensions |
||
168 | foreach ($extensions as $extension) { |
||
169 | if ($extension->isFile()) { |
||
170 | continue; |
||
171 | } |
||
172 | |||
173 | // Core translations |
||
174 | $core_handle = (strpos($extension->getFilename(), 'lang_') !== false) |
||
175 | ? str_replace('lang_', '', $extension->getFilename()) |
||
0 ignored issues
–
show
|
|||
176 | : null; |
||
177 | |||
178 | // Loop over the `/lang` directory of this `$extension` searching for language |
||
179 | // files. If `/lang` isn't a directory, `UnexpectedValueException` will be |
||
180 | // thrown. |
||
181 | try { |
||
182 | $path = $extension->getPathname() . '/lang'; |
||
183 | if (!is_dir($path)) { |
||
184 | continue; |
||
185 | } |
||
186 | |||
187 | $directory = new GlobIterator($path . '/lang.*.php'); |
||
188 | foreach ($directory as $file) { |
||
189 | include($file->getPathname()); |
||
190 | |||
191 | // Get language code (chars between lang. and .php) |
||
192 | $code = substr($file->getFilename(), 5, -4); |
||
193 | $lang = null; |
||
194 | $handle = null; |
||
195 | $extensions = array(); |
||
196 | |||
197 | // Set lang, handle and extensions if defined. |
||
198 | if (isset(self::$_languages[$code])) { |
||
199 | $lang = self::$_languages[$code]; |
||
200 | $handle = $lang['handle']; |
||
201 | $extensions = $lang['extensions']; |
||
202 | } |
||
203 | |||
204 | // Core translations |
||
205 | if ($core_handle) { |
||
206 | $handle = $core_handle; |
||
207 | |||
208 | // Extension translations |
||
209 | } else { |
||
210 | $extensions = array_merge(array($extension->getFilename()), $extensions); |
||
211 | } |
||
212 | |||
213 | // Merge languages ($about is declared inside the included $file) |
||
214 | $temp = self::createLanguage($code, $about['name'], $handle, $extensions); |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
215 | |||
216 | if (isset($lang)) { |
||
217 | foreach ($lang as $key => $value) { |
||
218 | // Prevent missing or nulled values overwriting existing values |
||
219 | // which can occur if a translation file is not correct. |
||
220 | if (!isset($temp[$code][$key]) || empty($temp[$code][$key])) { |
||
221 | continue; |
||
222 | } |
||
223 | |||
224 | self::$_languages[$code][$key] = $temp[$code][$key]; |
||
225 | } |
||
226 | } else { |
||
227 | self::$_languages[$code] = $temp[$code]; |
||
228 | } |
||
229 | } |
||
230 | } catch (Exception $ex) { |
||
231 | continue; |
||
232 | } |
||
233 | } |
||
234 | } |
||
235 | |||
236 | /** |
||
237 | * Set system language, load translations for core and extensions. If the specified language |
||
238 | * cannot be found, Symphony will default to English. |
||
239 | * |
||
240 | * Note: Beginning with Symphony 2.2 translations bundled with extensions will only be loaded |
||
241 | * when the core dictionary of the specific language is available. |
||
242 | * |
||
243 | * @param string $code |
||
244 | * Language code, e. g. 'en' or 'pt-br' |
||
245 | * @param boolean $checkStatus (optional) |
||
246 | * If false, set the language even if it's not enabled. Defaults to true. |
||
247 | */ |
||
248 | public static function set($code, $checkStatus = true) |
||
0 ignored issues
–
show
|
|||
249 | { |
||
250 | if (!$code || $code == self::get()) { |
||
251 | return; |
||
252 | } |
||
253 | |||
254 | // Language file available |
||
255 | if ($code !== 'en' && (self::isLanguageEnabled($code) || $checkStatus === false)) { |
||
256 | // Store desired language code |
||
257 | self::$_lang = $code; |
||
258 | |||
259 | // Clear dictionary |
||
260 | self::$_dictionary = array(); |
||
261 | |||
262 | // Load core translations |
||
263 | self::load(vsprintf('%s/lang_%s/lang/lang.%s.php', array( |
||
264 | EXTENSIONS, self::$_languages[$code]['handle'], $code |
||
0 ignored issues
–
show
|
|||
265 | ))); |
||
266 | |||
267 | // Load extension translations |
||
268 | if (is_array(self::$_languages[$code]['extensions'])) { |
||
269 | foreach (self::$_languages[$code]['extensions'] as $extension) { |
||
270 | self::load(vsprintf('%s/%s/lang/lang.%s.php', array( |
||
271 | EXTENSIONS, $extension, $code |
||
272 | ))); |
||
273 | } |
||
274 | } |
||
275 | |||
276 | // Language file unavailable, use default language |
||
277 | } else { |
||
278 | self::$_lang = 'en'; |
||
279 | |||
280 | // Log error, if possible |
||
281 | if ($code !== 'en' && class_exists('Symphony', false) && Symphony::Log() instanceof Log) { |
||
282 | Symphony::Log()->pushToLog( |
||
283 | __('The selected language, %s, could not be found. Using default English dictionary instead.', array($code)), |
||
284 | E_ERROR, |
||
285 | true |
||
286 | ); |
||
287 | } |
||
288 | } |
||
289 | } |
||
290 | |||
291 | /** |
||
292 | * Given a valid language code, this function checks if the language is enabled. |
||
293 | * |
||
294 | * @since Symphony 2.3 |
||
295 | * @param string $code |
||
296 | * Language code, e. g. 'en' or 'pt-br' |
||
297 | * @return boolean |
||
298 | * If true, the language is enabled. |
||
299 | */ |
||
300 | public static function isLanguageEnabled($code) |
||
301 | { |
||
302 | if ($code == 'en') { |
||
303 | return true; |
||
304 | } |
||
305 | |||
306 | $handle = (isset(self::$_languages[$code])) ? self::$_languages[$code]['handle'] : ''; |
||
307 | $enabled_extensions = array(); |
||
308 | |||
309 | // Fetch list of active extensions |
||
310 | if (class_exists('Symphony', false) && (!is_null(Symphony::ExtensionManager()))) { |
||
311 | $enabled_extensions = Symphony::ExtensionManager()->listInstalledHandles(); |
||
312 | } |
||
313 | |||
314 | return in_array('lang_' . $handle, $enabled_extensions); |
||
315 | } |
||
316 | |||
317 | /** |
||
318 | * Load language file. Each language file contains three arrays: |
||
319 | * about, dictionary and transliterations. |
||
320 | * |
||
321 | * @param string $path |
||
322 | * Path of the language file that should be loaded |
||
323 | */ |
||
324 | private static function load($path) |
||
325 | { |
||
326 | // Load language file |
||
327 | if (file_exists($path)) { |
||
328 | require($path); |
||
329 | } |
||
330 | |||
331 | // Populate dictionary ($dictionary is declared inside $path) |
||
332 | if (isset($dictionary) && is_array($dictionary)) { |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
333 | self::$_dictionary = array_merge(self::$_dictionary, $dictionary); |
||
334 | } |
||
335 | |||
336 | // Populate transliterations ($transliterations is declared inside $path) |
||
337 | if (isset($transliterations) && is_array($transliterations)) { |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
338 | self::$_transliterations = array_merge(self::$_transliterations, $transliterations); |
||
339 | } |
||
340 | } |
||
341 | |||
342 | /** |
||
343 | * Get current language |
||
344 | * |
||
345 | * @return string |
||
346 | */ |
||
347 | public static function get() |
||
348 | { |
||
349 | return self::$_lang; |
||
350 | } |
||
351 | |||
352 | /** |
||
353 | * This function is an internal alias for `__()`. |
||
354 | * |
||
355 | * @since Symphony 2.3 |
||
356 | * @see toolkit.__() |
||
357 | * @param string $string |
||
358 | * The string that should be translated |
||
359 | * @param array $inserts (optional) |
||
360 | * Optional array used to replace translation placeholders, defaults to NULL |
||
361 | * @param string $namespace (optional) |
||
362 | * Optional string used to define the namespace, defaults to NULL. |
||
363 | * @return string |
||
364 | * Returns the translated string |
||
365 | */ |
||
366 | public static function translate($string, array $inserts = null, $namespace = null) |
||
0 ignored issues
–
show
|
|||
367 | { |
||
368 | if (is_null($namespace) && class_exists('Symphony', false)) { |
||
369 | $namespace = Symphony::getPageNamespace(); |
||
370 | } |
||
371 | |||
372 | if (isset($namespace, self::$_dictionary[$namespace][$string])) { |
||
373 | $translated = self::$_dictionary[$namespace][$string]; |
||
374 | } elseif (isset(self::$_dictionary[$string])) { |
||
375 | $translated = self::$_dictionary[$string]; |
||
376 | } else { |
||
377 | $translated = $string; |
||
378 | } |
||
379 | |||
380 | $translated = empty($translated) ? $string : $translated; |
||
381 | |||
382 | // Replace translation placeholders |
||
383 | if (is_array($inserts) && !empty($inserts)) { |
||
384 | $translated = vsprintf($translated, $inserts); |
||
385 | } |
||
386 | |||
387 | return $translated; |
||
388 | } |
||
389 | |||
390 | /** |
||
391 | * Get an array of the codes and names of all languages that are available system wide. |
||
392 | * |
||
393 | * Note: Beginning with Symphony 2.2 language files are only available |
||
394 | * when the language extension is explicitly enabled. |
||
395 | * |
||
396 | * @param boolean $checkStatus (optional) |
||
397 | * If false, retrieves a list a languages that support core translation. |
||
398 | * @return array |
||
399 | * Returns an associative array of language codes and names, e. g. 'en' => 'English' |
||
400 | */ |
||
401 | public static function getAvailableLanguages($checkStatus = true) |
||
0 ignored issues
–
show
|
|||
402 | { |
||
403 | $languages = array(); |
||
404 | |||
405 | // Get available languages |
||
406 | foreach (self::$_languages as $key => $language) { |
||
407 | if (self::isLanguageEnabled($key) || ($checkStatus === false && isset($language['handle']))) { |
||
408 | $languages[$key] = $language['name']; |
||
409 | } |
||
410 | } |
||
411 | |||
412 | // Return languages codes |
||
413 | return $languages; |
||
414 | } |
||
415 | |||
416 | /** |
||
417 | * Check if Symphony is localised. |
||
418 | * |
||
419 | * @return boolean |
||
420 | * Returns true for localized system, false for English system |
||
421 | */ |
||
422 | public static function isLocalized() |
||
423 | { |
||
424 | return (self::get() !== 'en'); |
||
425 | } |
||
426 | |||
427 | /** |
||
428 | * Localize dates. |
||
429 | * |
||
430 | * @param string $string |
||
431 | * Standard date that should be localized |
||
432 | * @return string |
||
433 | * Return the given date with translated month and day names |
||
434 | */ |
||
435 | public static function localizeDate($string) |
||
436 | { |
||
437 | // Only translate dates in localized environments |
||
438 | if (self::isLocalized()) { |
||
439 | foreach (self::$_datetime_dictionary as $value) { |
||
440 | $string = preg_replace('/\b' . $value . '\b/i', self::translate($value), $string); |
||
441 | } |
||
442 | } |
||
443 | |||
444 | return $string; |
||
445 | } |
||
446 | |||
447 | /** |
||
448 | * Standardize dates. |
||
449 | * |
||
450 | * @param string $string |
||
451 | * Localized date that should be standardized |
||
452 | * @return string |
||
453 | * Returns the given date with English month and day names |
||
454 | */ |
||
455 | public static function standardizeDate($string) |
||
456 | { |
||
457 | // Only standardize dates in localized environments |
||
458 | if (self::isLocalized()) { |
||
0 ignored issues
–
show
|
|||
459 | |||
460 | // Translate names to English |
||
461 | foreach (self::$_datetime_dictionary as $values) { |
||
462 | $string = preg_replace('/\b' . self::translate($values) . '\b/i' . (self::isUnicodeCompiled() === true ? 'u' : null), $values, $string); |
||
0 ignored issues
–
show
|
|||
463 | } |
||
464 | |||
465 | // Replace custom date and time separator with space: |
||
466 | // This is important, otherwise the `DateTime` constructor may break |
||
467 | // @todo Test if this separator is still required. It's a hidden setting |
||
468 | // and users are only aware of it if they go digging/pointed in the right direction |
||
469 | $separator = Symphony::Configuration()->get('datetime_separator', 'region'); |
||
470 | if ($separator !== ' ') { |
||
471 | $string = str_replace($separator, ' ', $string); |
||
472 | } |
||
473 | } |
||
474 | |||
475 | return $string; |
||
476 | } |
||
477 | |||
478 | /** |
||
479 | * Given a string, this will clean it for use as a Symphony handle. Preserves multi-byte characters. |
||
480 | * |
||
481 | * @param string $string |
||
482 | * String to be cleaned up |
||
483 | * @param integer $max_length |
||
484 | * The maximum number of characters in the handle |
||
485 | * @param string $delim |
||
486 | * All non-valid characters will be replaced with this |
||
487 | * @param boolean $uriencode |
||
488 | * Force the resultant string to be uri encoded making it safe for URLs |
||
489 | * @param boolean $apply_transliteration |
||
490 | * If true, this will run the string through an array of substitution characters |
||
491 | * @param array $additional_rule_set |
||
492 | * An array of REGEX patterns that should be applied to the `$string`. This |
||
493 | * occurs after the string has been trimmed and joined with the `$delim` |
||
494 | * @return string |
||
495 | * Returns resultant handle |
||
496 | */ |
||
497 | public static function createHandle($string, $max_length = 255, $delim = '-', $uriencode = false, $apply_transliteration = true, $additional_rule_set = null) |
||
0 ignored issues
–
show
|
|||
498 | { |
||
499 | // Use the transliteration table if provided |
||
500 | if ($apply_transliteration === true) { |
||
501 | $string = self::applyTransliterations($string); |
||
502 | } |
||
503 | |||
504 | return General::createHandle($string, $max_length, $delim, $uriencode, $additional_rule_set); |
||
505 | } |
||
506 | |||
507 | /** |
||
508 | * Given a string, this will clean it for use as a filename. Preserves multi-byte characters. |
||
509 | * |
||
510 | * @param string $string |
||
511 | * String to be cleaned up |
||
512 | * @param string $delim |
||
513 | * Replacement for invalid characters |
||
514 | * @param boolean $apply_transliteration |
||
515 | * If true, umlauts and special characters will be substituted |
||
516 | * @return string |
||
517 | * Returns created filename |
||
518 | */ |
||
519 | public static function createFilename($string, $delim='-', $apply_transliteration = true) |
||
0 ignored issues
–
show
|
|||
520 | { |
||
521 | // Use the transliteration table if provided |
||
522 | if ($apply_transliteration === true) { |
||
523 | $file = pathinfo($string); |
||
524 | $string = self::applyTransliterations($file['filename']) . '.' . $file['extension']; |
||
525 | } |
||
526 | |||
527 | return General::createFilename($string, $delim); |
||
528 | } |
||
529 | |||
530 | /** |
||
531 | * This function replaces special characters according to the values stored inside |
||
532 | * `$_transliterations`. |
||
533 | * |
||
534 | * @since Symphony 2.3 |
||
535 | * @param string $string |
||
536 | * The string that should be cleaned-up |
||
537 | * @return mixed |
||
538 | * Returns the transliterated string |
||
539 | */ |
||
540 | private static function applyTransliterations($string) |
||
541 | { |
||
542 | // Apply the straight transliterations with strtr as it's much faster |
||
543 | $string = strtr($string, self::$_transliterations['straight']); |
||
544 | |||
545 | // Apply the regex rules over the resulting $string |
||
546 | return preg_replace( |
||
547 | array_keys(self::$_transliterations['regexp']), |
||
548 | array_values(self::$_transliterations['regexp']), |
||
549 | $string |
||
550 | ); |
||
551 | } |
||
552 | |||
553 | /** |
||
554 | * Returns boolean if PHP has been compiled with unicode support. This is |
||
555 | * useful to determine if unicode modifier's can be used in regular expression's |
||
556 | * |
||
557 | * @link http://stackoverflow.com/questions/4509576/detect-if-pcre-was-built-without-the-enable-unicode-properties-or-enable-utf8 |
||
558 | * @since Symphony 2.2.2 |
||
559 | * @return boolean |
||
560 | */ |
||
561 | public static function isUnicodeCompiled() |
||
562 | { |
||
563 | return (@preg_match('/\pL/u', 'a') == 1 ? true : false); |
||
0 ignored issues
–
show
|
|||
564 | } |
||
565 | } |
||
566 |