Passed
Push — master ( c0a3a7...3b84a4 )
by Jeroen
58:51
created

engine/classes/Elgg/I18n/Translator.php (1 issue)

Check for undefined variables.

Best Practice Comprehensibility Minor
1
<?php
2
3
namespace Elgg\I18n;
4
5
use Elgg\Config;
6
7
/**
8
 * WARNING: API IN FLUX. DO NOT USE DIRECTLY.
9
 *
10
 * @access private
11
 *
12
 * @since 1.10.0
13
 */
14
class Translator {
15
16
	/**
17
	 * @var Config
18
	 */
19
	private $config;
20
21
	/**
22
	 * @var array
23
	 */
24
	private $translations = [];
25
26
	/**
27
	 * @var bool
28
	 */
29
	private $is_initialized = false;
30
31
	/**
32
	 * @var string
33
	 */
34
	private $defaultPath = null;
35
36
	/**
37
	 * @var string
38
	 */
39
	private $current_language = null;
40
41
	/**
42
	 * Paths to scan for autoloading languages.
43
	 *
44
	 * Languages are automatically loaded for the site or
45
	 * user's default language.  Plugins can extend or override strings.
46
	 * language_paths is an array of paths to scan for PHP files matching
47
	 * the default language.  The order of paths is determined by the plugin load order,
48
	 * with later entries overriding earlier.  Language files within these paths are
49
	 * named as the two-letter ISO 639-1 country codes for the language they represent.
50
	 *
51
	 * @link http://en.wikipedia.org/wiki/ISO_639-1
52
	 *
53
	 * @var array (paths are keys)
54
	 */
55
	private $language_paths = [];
56
57
	/**
58
	 * @var bool
59
	 */
60
	private $was_reloaded = false;
61
62
	/**
63
	 * @var bool
64
	 */
65
	private $loaded_from_cache = false;
66
67
	/**
68
	 * Constructor
69
	 *
70
	 * @param Config $config Elgg config
71
	 */
72 4417
	public function __construct(Config $config) {
73 4417
		$this->config = $config;
74 4417
		$this->defaultPath = dirname(dirname(dirname(dirname(__DIR__)))) . "/languages/";
75 4417
	}
76
77
	/**
78
	 * @return bool
79
	 */
80 19
	public function wasLoadedFromCache() {
81 19
		return $this->loaded_from_cache;
82
	}
83
84
	/**
85
	 * Get a map of all loaded translations
86
	 *
87
	 * @return array
88
	 */
89 978
	public function getLoadedTranslations() {
90 978
		return $this->translations;
91
	}
92
93
	/**
94
	 * Given a message key, returns an appropriately translated full-text string
95
	 *
96
	 * @param string $message_key The short message code
97
	 * @param array  $args        An array of arguments to pass through vsprintf().
98
	 * @param string $language    Optionally, the standard language code
99
	 *                            (defaults to site/user default, then English)
100
	 *
101
	 * @return string Either the translated string, the English string,
102
	 * or the original language string.
103
	 */
104 867
	public function translate($message_key, array $args = [], $language = "") {
105 867
		if (!is_string($message_key) || strlen($message_key) < 1) {
106
			_elgg_services()->logger->warn(
107
				'$message_key needs to be a string in ' . __METHOD__ . '(), ' . gettype($message_key) . ' provided'
108
			);
109
			return '';
110
		}
111
112 867
		if (!$language) {
113
			// no language provided, get current language
114
			// based on detection, user setting or site
115 853
			$language = $this->getCurrentLanguage();
116
		}
117
118 867
		$this->ensureTranslationsLoaded($language);
119
120
		// build language array for different trys
121
		// avoid dupes without overhead of array_unique
122 867
		$langs[$language] = true;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$langs was never initialized. Although not strictly required by PHP, it is generally a good practice to add $langs = array(); before regardless.
Loading history...
123
124
		// load site language
125 867
		$site_language = $this->config->language;
126 867
		if (!empty($site_language)) {
127 867
			$this->ensureTranslationsLoaded($site_language);
128
129 867
			$langs[$site_language] = true;
130
		}
131
132
		// ultimate language fallback
133 867
		$langs['en'] = true;
134
135
		// try to translate
136 867
		$notice = '';
137 867
		$string = $message_key;
138 867
		foreach (array_keys($langs) as $try_lang) {
139 867
			if (isset($this->translations[$try_lang][$message_key])) {
140 687
				$string = $this->translations[$try_lang][$message_key];
141
142
				// only pass through if we have arguments to allow backward compatibility
143
				// with manual sprintf() calls.
144 687
				if ($args) {
145 59
					$string = vsprintf($string, $args);
146
				}
147
148 687
				break;
149
			} else {
150 450
				$notice = sprintf(
151 450
					'Missing %s translation for "%s" language key',
152 450
					($try_lang === 'en') ? 'English' : $try_lang,
153 450
					$message_key
154
				);
155
			}
156
		}
157
158 867
		if ($notice) {
159 450
			_elgg_services()->logger->notice($notice);
160
		}
161
162 867
		return $string;
163
	}
164
165
	/**
166
	 * Add a translation.
167
	 *
168
	 * Translations are arrays in the Zend Translation array format, eg:
169
	 *
170
	 *	$english = array('message1' => 'message1', 'message2' => 'message2');
171
	 *  $german = array('message1' => 'Nachricht1','message2' => 'Nachricht2');
172
	 *
173
	 * @param string $country_code   Standard country code (eg 'en', 'nl', 'es')
174
	 * @param array  $language_array Formatted array of strings
175
	 *
176
	 * @return bool Depending on success
177
	 */
178 4789
	public function addTranslation($country_code, $language_array) {
179 4789
		$country_code = strtolower($country_code);
180 4789
		$country_code = trim($country_code);
181
182 4789
		if (!is_array($language_array) || $country_code === "") {
183
			return false;
184
		}
185
186 4789
		if (count($language_array) > 0) {
187 4789
			if (!isset($this->translations[$country_code])) {
188 4418
				$this->translations[$country_code] = $language_array;
189
			} else {
190 1398
				$this->translations[$country_code] = $language_array + $this->translations[$country_code];
191
			}
192
		}
193
194 4789
		return true;
195
	}
196
197
	/**
198
	 * Get the current system/user language or "en".
199
	 *
200
	 * @return string The language code for the site/user or "en" if not set
201
	 */
202 5084
	public function getCurrentLanguage() {
203 5084
		if (!isset($this->current_language)) {
204 4459
			$this->current_language = $this->detectLanguage();
205
		}
206
207 5084
		if (!$this->current_language) {
208
			$this->current_language = 'en';
209
		}
210
211 5084
		return $this->current_language;
212
	}
213
214
	/**
215
	 * Sets current system language
216
	 *
217
	 * @param string $language Language code
218
	 *
219
	 * @return void
220
	 */
221 1398
	public function setCurrentLanguage($language = null) {
222 1398
		$this->current_language = $language;
223 1398
	}
224
225
	/**
226
	 * Detect the current system/user language or false.
227
	 *
228
	 * @return false|string The language code (eg "en") or false if not set
229
	 */
230 4459
	public function detectLanguage() {
231
		// detect from URL
232 4459
		$url_lang = _elgg_services()->input->get('hl');
233 4459
		if (!empty($url_lang)) {
234 2
			return $url_lang;
235
		}
236
237
		// check logged in user
238 4459
		$user = _elgg_services()->session->getLoggedInUser();
239 4459
		if (!empty($user) && !empty($user->language)) {
240 1
			return $user->language;
241
		}
242
243
		// get site setting
244 4459
		$site_language = $this->config->language;
245 4459
		if (!empty($site_language)) {
246 4459
			return $site_language;
247
		}
248
249
		return false;
250
	}
251
252
	/**
253
	 * Load both core and plugin translations
254
	 *
255
	 * By default this loads only English and the language of the logged
256
	 * in user.
257
	 *
258
	 * The optional $language argument can be used to load translations
259
	 * on-demand in case we need to translate something to a language not
260
	 * loaded by default for the current request.
261
	 *
262
	 * @param string $language Language code
263
	 *
264
	 * @return void
265
	 *
266
	 * @access private
267
	 */
268 4778
	public function loadTranslations($language = null) {
269 4778
		if (elgg_is_system_cache_enabled()) {
270 14
			$loaded = true;
271
272 14
			if ($language) {
273 1
				$languages = [$language];
274
			} else {
275 13
				$languages = array_unique(['en', $this->getCurrentLanguage()]);
276
			}
277
278 14
			foreach ($languages as $language) {
279 14
				$data = elgg_load_system_cache("$language.lang");
280 14
				if ($data) {
281 13
					$this->addTranslation($language, unserialize($data));
282
				} else {
283 14
					$loaded = false;
284
				}
285
			}
286
287 14
			if ($loaded) {
288 13
				$this->loaded_from_cache = true;
289 13
				$this->language_paths[$this->defaultPath] = true;
290 13
				$this->is_initialized = true;
291 13
				return;
292
			}
293
		}
294
295
		// load core translations from languages directory
296 4766
		$this->registerTranslations($this->defaultPath, false, $language);
297
298
		// Plugin translation have already been loaded for the default
299
		// languages by ElggApplication::bootCore(), so there's no need
300
		// to continue unless loading a specific language on-demand
301 4766
		if ($language) {
302 989
			$this->loadPluginTranslations($language);
303
		}
304 4766
	}
305
306
	/**
307
	 * Load plugin translations for a language
308
	 *
309
	 * This is needed only if the current request uses a language
310
	 * that is neither English of the same as the language of the
311
	 * logged in user.
312
	 *
313
	 * @param string $language Language code
314
	 * @return void
315
	 * @throws \PluginException
316
	 */
317 989
	private function loadPluginTranslations($language) {
318
		// Get active plugins
319 989
		$plugins = _elgg_services()->plugins->find('active');
320
321 989
		if (!$plugins) {
322
			// Active plugins were not found, so no need to register plugin translations
323 987
			return;
324
		}
325
326 2
		foreach ($plugins as $plugin) {
327 2
			$languages_path = "{$plugin->getPath()}languages/";
328
329 2
			if (!is_dir($languages_path)) {
330
				// This plugin doesn't have anything to translate
331 2
				continue;
332
			}
333
334 2
			$language_file = "{$languages_path}{$language}.php";
335
336 2
			if (!file_exists($language_file)) {
337
				// This plugin doesn't have translations for the requested language
338
339
				$name = $plugin->getDisplayName();
340
				_elgg_services()->logger->notice("Plugin $name is missing translations for $language language");
341
342
				continue;
343
			}
344
345
			// Register translations from the plugin languages directory
346 2
			if (!$this->registerTranslations($languages_path, false, $language)) {
347
				throw new \PluginException(sprintf('Cannot register languages for plugin %s (guid: %s) at %s.',
348 2
					[$plugin->getID(), $plugin->guid, $languages_path]));
349
			}
350
		}
351 2
	}
352
353
	/**
354
	 * Registers translations in a directory assuming the standard plugin layout.
355
	 *
356
	 * @param string $path Without the trailing slash.
357
	 *
358
	 * @return bool Success
359
	 */
360 24
	public function registerPluginTranslations($path) {
361 24
		$languages_path = rtrim($path, "\\/") . "/languages";
362
363
		// don't need to have translations
364 24
		if (!is_dir($languages_path)) {
365 2
			return true;
366
		}
367
368 24
		return $this->registerTranslations($languages_path);
369
	}
370
371
	/**
372
	 * When given a full path, finds translation files and loads them
373
	 *
374
	 * @param string $path     Full path
375
	 * @param bool   $load_all If true all languages are loaded, if
376
	 *                         false only the current language + en are loaded
377
	 * @param string $language Language code
378
	 *
379
	 * @return bool success
380
	 */
381 4781
	public function registerTranslations($path, $load_all = false, $language = null) {
382 4781
		$path = \Elgg\Project\Paths::sanitize($path);
383
384
		// Make a note of this path just in case we need to register this language later
385 4781
		$this->language_paths[$path] = true;
386 4781
		$this->is_initialized = true;
387
388 4781
		_elgg_services()->logger->info("Translations loaded from: $path");
389
390 4781
		if ($language) {
391 989
			$load_language_files = ["$language.php"];
392 989
			$load_all = false;
393
		} else {
394
			// Get the current language based on site defaults and user preference
395 4781
			$current_language = $this->getCurrentLanguage();
396
397
			$load_language_files = [
398 4781
				'en.php',
399 4781
				"$current_language.php"
400
			];
401
402 4781
			$load_language_files = array_unique($load_language_files);
403
		}
404
405 4781
		$handle = opendir($path);
406 4781
		if (!$handle) {
407
			_elgg_services()->logger->error("Could not open language path: $path");
408
			return false;
409
		}
410
411 4781
		$return = true;
412 4781
		while (false !== ($language_file = readdir($handle))) {
413
			// ignore bad files
414 4781
			if (substr($language_file, 0, 1) == '.' || substr($language_file, -4) !== '.php') {
415 4781
				continue;
416
			}
417
418 4781
			if (in_array($language_file, $load_language_files) || $load_all) {
419 4781
				$result = (include $path . $language_file);
420 4781
				if ($result === false) {
421
					$return = false;
422
					continue;
423 4781
				} elseif (is_array($result)) {
424 4781
					$this->addTranslation(basename($language_file, '.php'), $result);
425
				}
426
			}
427
		}
428
429 4781
		return $return;
430
	}
431
432
	/**
433
	 * Reload all translations from all registered paths.
434
	 *
435
	 * This is only called by functions which need to know all possible translations.
436
	 *
437
	 * @todo Better on demand loading based on language_paths array
438
	 *
439
	 * @return void
440
	 */
441 945
	public function reloadAllTranslations() {
442 945
		if ($this->was_reloaded) {
443 455
			return;
444
		}
445
446 945
		if ($this->loaded_from_cache) {
447
			$cache = elgg_get_system_cache();
448
			$cache_dir = $cache->getVariable("cache_path");
449
			$filenames = elgg_get_file_list($cache_dir, [], [], [".lang"]);
450
			foreach ($filenames as $filename) {
451
				// Look for files matching for example 'en.lang', 'cmn.lang' or 'pt_br.lang'.
452
				// Note that this regex is just for the system cache. The original language
453
				// files are allowed to have uppercase letters (e.g. pt_BR.php).
454
				if (preg_match('/(([a-z]{2,3})(_[a-z]{2})?)\.lang$/', $filename, $matches)) {
455
					$language = $matches[1];
456
					$data = elgg_load_system_cache("$language.lang");
457
					if ($data) {
458
						$this->addTranslation($language, unserialize($data));
459
					}
460
				}
461
			}
462
		} else {
463 945
			foreach (array_keys($this->language_paths) as $path) {
464 945
				$this->registerTranslations($path, true);
465
			}
466
		}
467
		
468 945
		_elgg_services()->hooks->getEvents()->triggerAfter('reload', 'translations');
469
470 945
		$this->was_reloaded = true;
471 945
	}
472
473
	/**
474
	 * Return an array of installed translations as an associative
475
	 * array "two letter code" => "native language name".
476
	 *
477
	 * @return array
478
	 */
479 487
	public function getInstalledTranslations() {
480
		// Ensure that all possible translations are loaded
481 487
		$this->reloadAllTranslations();
482
483 487
		$installed = [];
484
485 487
		$admin_logged_in = _elgg_services()->session->isAdminLoggedIn();
486
487 487
		foreach ($this->translations as $k => $v) {
488 487
			if ($this->languageKeyExists($k, $k)) {
489 487
				$lang = $this->translate($k, [], $k);
490
			} else {
491 476
				$lang = $this->translate($k);
492
			}
493
			
494 487
			$installed[$k] = $lang;
495
			
496 487
			if (!$admin_logged_in || ($k === 'en')) {
497 487
				continue;
498
			}
499
			
500
			$completeness = $this->getLanguageCompleteness($k);
501
			if ($completeness < 100) {
502
				$installed[$k] .= " (" . $completeness . "% " . $this->translate('complete') . ")";
503
			}
504
		}
505
506 487
		return $installed;
507
	}
508
509
	/**
510
	 * Return the level of completeness for a given language code (compared to english)
511
	 *
512
	 * @param string $language Language
513
	 *
514
	 * @return float
515
	 */
516 487
	public function getLanguageCompleteness($language) {
517
518 487
		if ($language == 'en') {
519 32
			return (float) 100;
520
		}
521
522
		// Ensure that all possible translations are loaded
523 455
		$this->reloadAllTranslations();
524
525 455
		$language = sanitise_string($language);
526
527 455
		$en = count($this->translations['en']);
528
529 455
		$missing = $this->getMissingLanguageKeys($language);
530 455
		if ($missing) {
531 455
			$missing = count($missing);
532
		} else {
533
			$missing = 0;
534
		}
535
536 455
		$lang = $en - $missing;
537
538 455
		return round(($lang / $en) * 100, 2);
539
	}
540
541
	/**
542
	 * Return the translation keys missing from a given language,
543
	 * or those that are identical to the english version.
544
	 *
545
	 * @param string $language The language
546
	 *
547
	 * @return mixed
548
	 */
549 455
	public function getMissingLanguageKeys($language) {
550
551
552
		// Ensure that all possible translations are loaded
553 455
		$this->reloadAllTranslations();
554
555 455
		$missing = [];
556
557 455
		foreach ($this->translations['en'] as $k => $v) {
558 455
			if ((!isset($this->translations[$language][$k]))
559 455
			|| ($this->translations[$language][$k] == $this->translations['en'][$k])) {
560 455
				$missing[] = $k;
561
			}
562
		}
563
564 455
		if (count($missing)) {
565 455
			return $missing;
566
		}
567
568
		return false;
569
	}
570
571
	/**
572
	 * Check if a given language key exists
573
	 *
574
	 * @param string $key      The translation key
575
	 * @param string $language The specific language to check
576
	 *
577
	 * @return bool
578
	 * @since 1.11
579
	 */
580 538
	function languageKeyExists($key, $language = 'en') {
581 538
		if (empty($key)) {
582
			return false;
583
		}
584
585 538
		$this->ensureTranslationsLoaded($language);
586
587 538
		if (!array_key_exists($language, $this->translations)) {
588 6
			return false;
589
		}
590
591 532
		return array_key_exists($key, $this->translations[$language]);
592
	}
593
594
	/**
595
	 * Make sure translations are loaded
596
	 *
597
	 * @param string $language Language
598
	 * @return void
599
	 */
600 870
	private function ensureTranslationsLoaded($language) {
601 870
		if (!$this->is_initialized) {
602
			// this means we probably had an exception before translations were initialized
603 11
			$this->registerTranslations($this->defaultPath);
604
		}
605
606 870
		if (!isset($this->translations[$language])) {
607
			// The language being requested is not the same as the language of the
608
			// logged in user, so we will have to load it separately. (Most likely
609
			// we're sending a notification and the recipient is using a different
610
			// language than the logged in user.)
611 8
			$this->loadTranslations($language);
612
		}
613 870
	}
614
615
	/**
616
	 * Returns an array of language codes.
617
	 *
618
	 * @return array
619
	 */
620
	public static function getAllLanguageCodes() {
621
		return [
622
			"aa", // "Afar"
623
			"ab", // "Abkhazian"
624
			"af", // "Afrikaans"
625
			"am", // "Amharic"
626
			"ar", // "Arabic"
627
			"as", // "Assamese"
628
			"ay", // "Aymara"
629
			"az", // "Azerbaijani"
630
			"ba", // "Bashkir"
631
			"be", // "Byelorussian"
632
			"bg", // "Bulgarian"
633
			"bh", // "Bihari"
634
			"bi", // "Bislama"
635
			"bn", // "Bengali; Bangla"
636
			"bo", // "Tibetan"
637
			"br", // "Breton"
638
			"ca", // "Catalan"
639
			"cmn", // "Mandarin Chinese" // ISO 639-3
640
			"co", // "Corsican"
641
			"cs", // "Czech"
642
			"cy", // "Welsh"
643
			"da", // "Danish"
644
			"de", // "German"
645
			"dz", // "Bhutani"
646
			"el", // "Greek"
647
			"en", // "English"
648
			"eo", // "Esperanto"
649
			"es", // "Spanish"
650
			"et", // "Estonian"
651
			"eu", // "Basque"
652
			"eu_es", // "Basque (Spain)"
653
			"fa", // "Persian"
654
			"fi", // "Finnish"
655
			"fj", // "Fiji"
656
			"fo", // "Faeroese"
657
			"fr", // "French"
658
			"fy", // "Frisian"
659
			"ga", // "Irish"
660
			"gd", // "Scots / Gaelic"
661
			"gl", // "Galician"
662
			"gn", // "Guarani"
663
			"gu", // "Gujarati"
664
			"he", // "Hebrew"
665
			"ha", // "Hausa"
666
			"hi", // "Hindi"
667
			"hr", // "Croatian"
668
			"hu", // "Hungarian"
669
			"hy", // "Armenian"
670
			"ia", // "Interlingua"
671
			"id", // "Indonesian"
672
			"ie", // "Interlingue"
673
			"ik", // "Inupiak"
674
			"is", // "Icelandic"
675
			"it", // "Italian"
676
			"iu", // "Inuktitut"
677
			"iw", // "Hebrew (obsolete)"
678
			"ja", // "Japanese"
679
			"ji", // "Yiddish (obsolete)"
680
			"jw", // "Javanese"
681
			"ka", // "Georgian"
682
			"kk", // "Kazakh"
683
			"kl", // "Greenlandic"
684
			"km", // "Cambodian"
685
			"kn", // "Kannada"
686
			"ko", // "Korean"
687
			"ks", // "Kashmiri"
688
			"ku", // "Kurdish"
689
			"ky", // "Kirghiz"
690
			"la", // "Latin"
691
			"ln", // "Lingala"
692
			"lo", // "Laothian"
693
			"lt", // "Lithuanian"
694
			"lv", // "Latvian/Lettish"
695
			"mg", // "Malagasy"
696
			"mi", // "Maori"
697
			"mk", // "Macedonian"
698
			"ml", // "Malayalam"
699
			"mn", // "Mongolian"
700
			"mo", // "Moldavian"
701
			"mr", // "Marathi"
702
			"ms", // "Malay"
703
			"mt", // "Maltese"
704
			"my", // "Burmese"
705
			"na", // "Nauru"
706
			"ne", // "Nepali"
707
			"nl", // "Dutch"
708
			"no", // "Norwegian"
709
			"oc", // "Occitan"
710
			"om", // "(Afan) Oromo"
711
			"or", // "Oriya"
712
			"pa", // "Punjabi"
713
			"pl", // "Polish"
714
			"ps", // "Pashto / Pushto"
715
			"pt", // "Portuguese"
716
			"pt_br", // "Portuguese (Brazil)"
717
			"qu", // "Quechua"
718
			"rm", // "Rhaeto-Romance"
719
			"rn", // "Kirundi"
720
			"ro", // "Romanian"
721
			"ro_ro", // "Romanian (Romania)"
722
			"ru", // "Russian"
723
			"rw", // "Kinyarwanda"
724
			"sa", // "Sanskrit"
725
			"sd", // "Sindhi"
726
			"sg", // "Sangro"
727
			"sh", // "Serbo-Croatian"
728
			"si", // "Singhalese"
729
			"sk", // "Slovak"
730
			"sl", // "Slovenian"
731
			"sm", // "Samoan"
732
			"sn", // "Shona"
733
			"so", // "Somali"
734
			"sq", // "Albanian"
735
			"sr", // "Serbian"
736
			"sr_latin", // "Serbian (Latin)"
737
			"ss", // "Siswati"
738
			"st", // "Sesotho"
739
			"su", // "Sundanese"
740
			"sv", // "Swedish"
741
			"sw", // "Swahili"
742
			"ta", // "Tamil"
743
			"te", // "Tegulu"
744
			"tg", // "Tajik"
745
			"th", // "Thai"
746
			"ti", // "Tigrinya"
747
			"tk", // "Turkmen"
748
			"tl", // "Tagalog"
749
			"tn", // "Setswana"
750
			"to", // "Tonga"
751
			"tr", // "Turkish"
752
			"ts", // "Tsonga"
753
			"tt", // "Tatar"
754
			"tw", // "Twi"
755
			"ug", // "Uigur"
756
			"uk", // "Ukrainian"
757
			"ur", // "Urdu"
758
			"uz", // "Uzbek"
759
			"vi", // "Vietnamese"
760
			"vo", // "Volapuk"
761
			"wo", // "Wolof"
762
			"xh", // "Xhosa"
763
			"yi", // "Yiddish"
764
			"yo", // "Yoruba"
765
			"za", // "Zuang"
766
			"zh", // "Chinese"
767
			"zh_hans", // "Chinese Simplified"
768
			"zu", // "Zulu"
769
		];
770
	}
771
772
	/**
773
	 * Normalize a language code (e.g. from Transifex)
774
	 *
775
	 * @param string $code Language code
776
	 *
777
	 * @return string
778
	 */
779
	public static function normalizeLanguageCode($code) {
780
		$code = strtolower($code);
781
		$code = preg_replace('~[^a-z0-9]~', '_', $code);
782
		return $code;
783
	}
784
}
785