Passed
Push — master ( c2d8e3...289151 )
by Jeroen
06:06
created

Translator::includeLanguageFile()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3.7085

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 4
nop 1
dl 0
loc 13
ccs 4
cts 7
cp 0.5714
crap 3.7085
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Elgg\I18n;
4
5
use Elgg\Config;
6
use Elgg\Includer;
7
8
/**
9
 * WARNING: API IN FLUX. DO NOT USE DIRECTLY.
10
 *
11
 * @access private
12
 *
13
 * @since 1.10.0
14
 */
15
class Translator {
16
17
	/**
18
	 * @var Config
19
	 */
20
	private $config;
21
22
	/**
23
	 * @var array
24
	 */
25
	private $translations = [];
26
27
	/**
28
	 * @var bool
29
	 */
30
	private $is_initialized = false;
31
32
	/**
33
	 * @var string
34
	 */
35
	private $defaultPath = null;
36
37
	/**
38
	 * @var string
39
	 */
40
	private $current_language = null;
41
42
	/**
43
	 * Paths to scan for autoloading languages.
44
	 *
45
	 * Languages are automatically loaded for the site or
46
	 * user's default language.  Plugins can extend or override strings.
47
	 * language_paths is an array of paths to scan for PHP files matching
48
	 * the default language.  The order of paths is determined by the plugin load order,
49
	 * with later entries overriding earlier.  Language files within these paths are
50
	 * named as the two-letter ISO 639-1 country codes for the language they represent.
51
	 *
52
	 * @link http://en.wikipedia.org/wiki/ISO_639-1
53
	 *
54
	 * @var array (paths are keys)
55
	 */
56
	private $language_paths = [];
57
58
	/**
59
	 * @var bool
60
	 */
61
	private $was_reloaded = false;
62
63
	/**
64
	 * @var bool
65
	 */
66
	private $loaded_from_cache = false;
67
68
	/**
69
	 * Constructor
70
	 *
71
	 * @param Config $config Elgg config
72 4417
	 */
73 4417
	public function __construct(Config $config) {
74 4417
		$this->config = $config;
75 4417
		$this->defaultPath = dirname(dirname(dirname(dirname(__DIR__)))) . "/languages/";
76
	}
77
78
	/**
79
	 * @return bool
80 19
	 */
81 19
	public function wasLoadedFromCache() {
82
		return $this->loaded_from_cache;
83
	}
84
85
	/**
86
	 * Get a map of all loaded translations
87
	 *
88
	 * @return array
89 978
	 */
90 978
	public function getLoadedTranslations() {
91
		return $this->translations;
92
	}
93
94
	/**
95
	 * Given a message key, returns an appropriately translated full-text string
96
	 *
97
	 * @param string $message_key The short message code
98
	 * @param array  $args        An array of arguments to pass through vsprintf().
99
	 * @param string $language    Optionally, the standard language code
100
	 *                            (defaults to site/user default, then English)
101
	 *
102
	 * @return string Either the translated string, the English string,
103
	 * or the original language string.
104 867
	 */
105 867
	public function translate($message_key, array $args = [], $language = "") {
106
		if (!is_string($message_key) || strlen($message_key) < 1) {
107
			_elgg_services()->logger->warn(
108
				'$message_key needs to be a string in ' . __METHOD__ . '(), ' . gettype($message_key) . ' provided'
109
			);
110
			return '';
111
		}
112 867
113
		if (!$language) {
114
			// no language provided, get current language
115 853
			// based on detection, user setting or site
116
			$language = $this->getCurrentLanguage();
117
		}
118 867
119
		$this->ensureTranslationsLoaded($language);
120
121
		// build language array for different trys
122 867
		// avoid dupes without overhead of array_unique
123
		$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...
124
125 867
		// load site language
126 867
		$site_language = $this->config->language;
127 867
		if (!empty($site_language)) {
128
			$this->ensureTranslationsLoaded($site_language);
129 867
130
			$langs[$site_language] = true;
131
		}
132
133 867
		// ultimate language fallback
134
		$langs['en'] = true;
135
136 867
		// try to translate
137 867
		$notice = '';
138 867
		$string = $message_key;
139 867
		foreach (array_keys($langs) as $try_lang) {
140 687
			if (isset($this->translations[$try_lang][$message_key])) {
141
				$string = $this->translations[$try_lang][$message_key];
142
143
				// only pass through if we have arguments to allow backward compatibility
144 687
				// with manual sprintf() calls.
145 59
				if ($args) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $args of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
146
					$string = vsprintf($string, $args);
147
				}
148 687
149
				break;
150 450
			} else {
151 450
				$notice = sprintf(
152 450
					'Missing %s translation for "%s" language key',
153 450
					($try_lang === 'en') ? 'English' : $try_lang,
154
					$message_key
155
				);
156
			}
157
		}
158 867
159 450
		if ($notice) {
160
			_elgg_services()->logger->notice($notice);
161
		}
162 867
163
		return $string;
164
	}
165
166
	/**
167
	 * Add a translation.
168
	 *
169
	 * Translations are arrays in the Zend Translation array format, eg:
170
	 *
171
	 *	$english = array('message1' => 'message1', 'message2' => 'message2');
172
	 *  $german = array('message1' => 'Nachricht1','message2' => 'Nachricht2');
173
	 *
174
	 * @param string $country_code   Standard country code (eg 'en', 'nl', 'es')
175
	 * @param array  $language_array Formatted array of strings
176
	 *
177
	 * @return bool Depending on success
178 4789
	 */
179 4789
	public function addTranslation($country_code, $language_array) {
180 4789
		$country_code = strtolower($country_code);
181
		$country_code = trim($country_code);
182 4789
183
		if (!is_array($language_array) || $country_code === "") {
184
			return false;
185
		}
186 4789
187 4789
		if (count($language_array) > 0) {
188 4418
			if (!isset($this->translations[$country_code])) {
189
				$this->translations[$country_code] = $language_array;
190 1398
			} else {
191
				$this->translations[$country_code] = $language_array + $this->translations[$country_code];
192
			}
193
		}
194 4789
195
		return true;
196
	}
197
198
	/**
199
	 * Get the current system/user language or "en".
200
	 *
201
	 * @return string The language code for the site/user or "en" if not set
202 5084
	 */
203 5084
	public function getCurrentLanguage() {
204 4459
		if (!isset($this->current_language)) {
205
			$this->current_language = $this->detectLanguage();
1 ignored issue
show
Documentation Bug introduced by
It seems like $this->detectLanguage() can also be of type false. However, the property $current_language is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
206
		}
207 5084
208
		if (!$this->current_language) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->current_language of type false|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
209
			$this->current_language = 'en';
210
		}
211 5084
212
		return $this->current_language;
213
	}
214
215
	/**
216
	 * Sets current system language
217
	 *
218
	 * @param string $language Language code
219
	 *
220
	 * @return void
221 1398
	 */
222 1398
	public function setCurrentLanguage($language = null) {
223 1398
		$this->current_language = $language;
224
	}
225
226
	/**
227
	 * Detect the current system/user language or false.
228
	 *
229
	 * @return false|string The language code (eg "en") or false if not set
230 4459
	 */
231
	public function detectLanguage() {
232 4459
		// detect from URL
233 4459
		$url_lang = _elgg_services()->input->get('hl');
234 2
		if (!empty($url_lang)) {
235
			return $url_lang;
236
		}
237
238 4459
		// check logged in user
239 4459
		$user = _elgg_services()->session->getLoggedInUser();
240 1
		if (!empty($user) && !empty($user->language)) {
241
			return $user->language;
242
		}
243
244 4459
		// get site setting
245 4459
		$site_language = $this->config->language;
246 4459
		if (!empty($site_language)) {
247
			return $site_language;
248
		}
249
250
		return false;
251
	}
252
253
	/**
254
	 * Load both core and plugin translations
255
	 *
256
	 * By default this loads only English and the language of the logged
257
	 * in user.
258
	 *
259
	 * The optional $language argument can be used to load translations
260
	 * on-demand in case we need to translate something to a language not
261
	 * loaded by default for the current request.
262
	 *
263
	 * @param string $language Language code
264
	 *
265
	 * @return void
266
	 *
267
	 * @access private
268 4778
	 */
269 4778
	public function loadTranslations($language = null) {
270 14
		if (elgg_is_system_cache_enabled()) {
271
			$loaded = true;
272 14
273 1
			if ($language) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $language of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
274
				$languages = [$language];
275 13
			} else {
276
				$languages = array_unique(['en', $this->getCurrentLanguage()]);
277
			}
278 14
279 14
			foreach ($languages as $language) {
280 14
				$data = elgg_load_system_cache("$language.lang");
281 13
				if ($data) {
282
					$this->addTranslation($language, unserialize($data));
283 14
				} else {
284
					$loaded = false;
285
				}
286
			}
287 14
288 13
			if ($loaded) {
289 13
				$this->loaded_from_cache = true;
290 13
				$this->language_paths[$this->defaultPath] = true;
291 13
				$this->is_initialized = true;
292
				return;
293
			}
294
		}
295
296 4766
		// load core translations from languages directory
297
		$this->registerTranslations($this->defaultPath, false, $language);
298
299
		// Plugin translation have already been loaded for the default
300
		// languages by ElggApplication::bootCore(), so there's no need
301 4766
		// to continue unless loading a specific language on-demand
302 989
		if ($language) {
303
			$this->loadPluginTranslations($language);
304 4766
		}
305
	}
306
307
	/**
308
	 * Load plugin translations for a language
309
	 *
310
	 * This is needed only if the current request uses a language
311
	 * that is neither English of the same as the language of the
312
	 * logged in user.
313
	 *
314
	 * @param string $language Language code
315
	 * @return void
316
	 * @throws \PluginException
317 989
	 */
318
	private function loadPluginTranslations($language) {
319 989
		// Get active plugins
320
		$plugins = _elgg_services()->plugins->find('active');
321 989
322
		if (!$plugins) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $plugins of type ElggPlugin[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
323 987
			// Active plugins were not found, so no need to register plugin translations
324
			return;
325
		}
326 2
327 2
		foreach ($plugins as $plugin) {
328
			$languages_path = "{$plugin->getPath()}languages/";
329 2
330
			if (!is_dir($languages_path)) {
331 2
				// This plugin doesn't have anything to translate
332
				continue;
333
			}
334 2
335
			$language_file = "{$languages_path}{$language}.php";
336 2
337
			if (!file_exists($language_file)) {
338
				// This plugin doesn't have translations for the requested language
339
340
				$name = $plugin->getDisplayName();
341
				_elgg_services()->logger->notice("Plugin $name is missing translations for $language language");
342
343
				continue;
344
			}
345
346 2
			// Register translations from the plugin languages directory
347
			if (!$this->registerTranslations($languages_path, false, $language)) {
348 2
				throw new \PluginException(sprintf('Cannot register languages for plugin %s (guid: %s) at %s.',
349
					[$plugin->getID(), $plugin->guid, $languages_path]));
350
			}
351 2
		}
352
	}
353
354
	/**
355
	 * Registers translations in a directory assuming the standard plugin layout.
356
	 *
357
	 * @param string $path Without the trailing slash.
358
	 *
359
	 * @return bool Success
360 24
	 */
361 24
	public function registerPluginTranslations($path) {
362
		$languages_path = rtrim($path, "\\/") . "/languages";
363
364 24
		// don't need to have translations
365 2
		if (!is_dir($languages_path)) {
366
			return true;
367
		}
368 24
369
		return $this->registerTranslations($languages_path);
370
	}
371
372
	/**
373
	 * When given a full path, finds translation files and loads them
374
	 *
375
	 * @param string $path     Full path
376
	 * @param bool   $load_all If true all languages are loaded, if
377
	 *                         false only the current language + en are loaded
378
	 * @param string $language Language code
379
	 *
380
	 * @return bool success
381 4781
	 */
382 4781
	public function registerTranslations($path, $load_all = false, $language = null) {
383
		$path = \Elgg\Project\Paths::sanitize($path);
384
385 4781
		// Make a note of this path just in case we need to register this language later
386 4781
		$this->language_paths[$path] = true;
387
		$this->is_initialized = true;
388 4781
389
		_elgg_services()->logger->info("Translations loaded from: $path");
390 4781
391 989
		if ($language) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $language of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
392 989
			$load_language_files = ["$language.php"];
393
			$load_all = false;
394
		} else {
395 4781
			// Get the current language based on site defaults and user preference
396
			$current_language = $this->getCurrentLanguage();
397
398 4781
			$load_language_files = [
399 4781
				'en.php',
400
				"$current_language.php"
401
			];
402 4781
403
			$load_language_files = array_unique($load_language_files);
404
		}
405 4781
406 4781
		$return = true;
407
		if (!$load_all) {
408
			foreach ($load_language_files as $language_file) {
409
				$return = $return && $this->includeLanguageFile($path . $language_file);
410
			}
411 4781
		} else if ($handle = opendir($path)) {
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
					$return = $return && $this->includeLanguageFile($path . $language_file);
420 4781
				}
421
			}
422
			closedir($handle);
423 4781
		} else {
424 4781
			_elgg_services()->logger->error("Could not open language path: $path");
425
			$return = false;
426
		}
427
428
		return $return;
429 4781
	}
430
431
	/**
432
	 * Load cached or include a language file by its path
433
	 *
434
	 * @param string $path Path to file
435
	 * @return bool
436
	 */
437
	protected function includeLanguageFile($path) {
438
		$cache_key = "lang/" . sha1($path);
439
		$result = elgg_get_system_cache()->load($cache_key);
440
		if (!isset($result)) {
441 945
			$result = Includer::includeFile($path);
442 945
			elgg_get_system_cache()->save($cache_key, $result);
443 455
		}
444
		if (is_array($result)) {
445
			$this->addTranslation(basename($path, '.php'), $result);
446 945
			return true;
447
		}
448
449
		return false;
450
	}
451
452
	/**
453
	 * Reload all translations from all registered paths.
454
	 *
455
	 * This is only called by functions which need to know all possible translations.
456
	 *
457
	 * @todo Better on demand loading based on language_paths array
458
	 *
459
	 * @return void
460
	 */
461
	public function reloadAllTranslations() {
462
		if ($this->was_reloaded) {
463 945
			return;
464 945
		}
465
466
		if ($this->loaded_from_cache) {
467
			$cache = elgg_get_system_cache();
468 945
			$cache_dir = $cache->getVariable("cache_path");
469
			$filenames = elgg_get_file_list($cache_dir, [], [], [".lang"]);
470 945
			foreach ($filenames as $filename) {
471 945
				// Look for files matching for example 'en.lang', 'cmn.lang' or 'pt_br.lang'.
472
				// Note that this regex is just for the system cache. The original language
473
				// files are allowed to have uppercase letters (e.g. pt_BR.php).
474
				if (preg_match('/(([a-z]{2,3})(_[a-z]{2})?)\.lang$/', $filename, $matches)) {
475
					$language = $matches[1];
476
					$data = elgg_load_system_cache("$language.lang");
477
					if ($data) {
478
						$this->addTranslation($language, unserialize($data));
479 487
					}
480
				}
481 487
			}
482
		} else {
483 487
			foreach (array_keys($this->language_paths) as $path) {
484
				$this->registerTranslations($path, true);
485 487
			}
486
		}
487 487
488 487
		_elgg_services()->hooks->getEvents()->triggerAfter('reload', 'translations');
489 487
490
		$this->was_reloaded = true;
491 476
	}
492
493
	/**
494 487
	 * Return an array of installed translations as an associative
495
	 * array "two letter code" => "native language name".
496 487
	 *
497 487
	 * @return array
498
	 */
499
	public function getInstalledTranslations() {
500
		// Ensure that all possible translations are loaded
501
		$this->reloadAllTranslations();
502
503
		$installed = [];
504
505
		$admin_logged_in = _elgg_services()->session->isAdminLoggedIn();
506 487
507
		foreach ($this->translations as $k => $v) {
508
			if ($this->languageKeyExists($k, $k)) {
509
				$lang = $this->translate($k, [], $k);
510
			} else {
511
				$lang = $this->translate($k);
512
			}
513
514
			$installed[$k] = $lang;
515
516 487
			if (!$admin_logged_in || ($k === 'en')) {
517
				continue;
518 487
			}
519 32
520
			$completeness = $this->getLanguageCompleteness($k);
521
			if ($completeness < 100) {
522
				$installed[$k] .= " (" . $completeness . "% " . $this->translate('complete') . ")";
523 455
			}
524
		}
525 455
526
		return $installed;
527 455
	}
528
529 455
	/**
530 455
	 * Return the level of completeness for a given language code (compared to english)
531 455
	 *
532
	 * @param string $language Language
533
	 *
534
	 * @return float
535
	 */
536 455
	public function getLanguageCompleteness($language) {
537
538 455
		if ($language == 'en') {
539
			return (float) 100;
540
		}
541
542
		// Ensure that all possible translations are loaded
543
		$this->reloadAllTranslations();
544
545
		$language = sanitise_string($language);
0 ignored issues
show
Deprecated Code introduced by
The function sanitise_string() has been deprecated: Use query parameters where possible ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

545
		$language = /** @scrutinizer ignore-deprecated */ sanitise_string($language);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
546
547
		$en = count($this->translations['en']);
548
549 455
		$missing = $this->getMissingLanguageKeys($language);
550
		if ($missing) {
551
			$missing = count($missing);
552
		} else {
553 455
			$missing = 0;
554
		}
555 455
556
		$lang = $en - $missing;
557 455
558 455
		return round(($lang / $en) * 100, 2);
559 455
	}
560 455
561
	/**
562
	 * Return the translation keys missing from a given language,
563
	 * or those that are identical to the english version.
564 455
	 *
565 455
	 * @param string $language The language
566
	 *
567
	 * @return mixed
568
	 */
569
	public function getMissingLanguageKeys($language) {
570
571
572
		// Ensure that all possible translations are loaded
573
		$this->reloadAllTranslations();
574
575
		$missing = [];
576
577
		foreach ($this->translations['en'] as $k => $v) {
578
			if ((!isset($this->translations[$language][$k]))
579
				|| ($this->translations[$language][$k] == $this->translations['en'][$k])) {
580 538
				$missing[] = $k;
581 538
			}
582
		}
583
584
		if (count($missing)) {
585 538
			return $missing;
586
		}
587 538
588 6
		return false;
589
	}
590
591 532
	/**
592
	 * Check if a given language key exists
593
	 *
594
	 * @param string $key      The translation key
595
	 * @param string $language The specific language to check
596
	 *
597
	 * @return bool
598
	 * @since 1.11
599
	 */
600 870
	function languageKeyExists($key, $language = 'en') {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
601 870
		if (empty($key)) {
602
			return false;
603 11
		}
604
605
		$this->ensureTranslationsLoaded($language);
606 870
607
		if (!array_key_exists($language, $this->translations)) {
608
			return false;
609
		}
610
611 8
		return array_key_exists($key, $this->translations[$language]);
612
	}
613 870
614
	/**
615
	 * Make sure translations are loaded
616
	 *
617
	 * @param string $language Language
618
	 * @return void
619
	 */
620
	private function ensureTranslationsLoaded($language) {
621
		if (!$this->is_initialized) {
622
			// this means we probably had an exception before translations were initialized
623
			$this->registerTranslations($this->defaultPath);
624
		}
625
626
		if (!isset($this->translations[$language])) {
627
			// The language being requested is not the same as the language of the
628
			// logged in user, so we will have to load it separately. (Most likely
629
			// we're sending a notification and the recipient is using a different
630
			// language than the logged in user.)
631
			$this->loadTranslations($language);
632
		}
633
	}
634
635
	/**
636
	 * Returns an array of language codes.
637
	 *
638
	 * @return array
639
	 */
640
	public static function getAllLanguageCodes() {
641
		return [
642
			"aa", // "Afar"
643
			"ab", // "Abkhazian"
644
			"af", // "Afrikaans"
645
			"am", // "Amharic"
646
			"ar", // "Arabic"
647
			"as", // "Assamese"
648
			"ay", // "Aymara"
649
			"az", // "Azerbaijani"
650
			"ba", // "Bashkir"
651
			"be", // "Byelorussian"
652
			"bg", // "Bulgarian"
653
			"bh", // "Bihari"
654
			"bi", // "Bislama"
655
			"bn", // "Bengali; Bangla"
656
			"bo", // "Tibetan"
657
			"br", // "Breton"
658
			"ca", // "Catalan"
659
			"cmn", // "Mandarin Chinese" // ISO 639-3
660
			"co", // "Corsican"
661
			"cs", // "Czech"
662
			"cy", // "Welsh"
663
			"da", // "Danish"
664
			"de", // "German"
665
			"dz", // "Bhutani"
666
			"el", // "Greek"
667
			"en", // "English"
668
			"eo", // "Esperanto"
669
			"es", // "Spanish"
670
			"et", // "Estonian"
671
			"eu", // "Basque"
672
			"eu_es", // "Basque (Spain)"
673
			"fa", // "Persian"
674
			"fi", // "Finnish"
675
			"fj", // "Fiji"
676
			"fo", // "Faeroese"
677
			"fr", // "French"
678
			"fy", // "Frisian"
679
			"ga", // "Irish"
680
			"gd", // "Scots / Gaelic"
681
			"gl", // "Galician"
682
			"gn", // "Guarani"
683
			"gu", // "Gujarati"
684
			"he", // "Hebrew"
685
			"ha", // "Hausa"
686
			"hi", // "Hindi"
687
			"hr", // "Croatian"
688
			"hu", // "Hungarian"
689
			"hy", // "Armenian"
690
			"ia", // "Interlingua"
691
			"id", // "Indonesian"
692
			"ie", // "Interlingue"
693
			"ik", // "Inupiak"
694
			"is", // "Icelandic"
695
			"it", // "Italian"
696
			"iu", // "Inuktitut"
697
			"iw", // "Hebrew (obsolete)"
698
			"ja", // "Japanese"
699
			"ji", // "Yiddish (obsolete)"
700
			"jw", // "Javanese"
701
			"ka", // "Georgian"
702
			"kk", // "Kazakh"
703
			"kl", // "Greenlandic"
704
			"km", // "Cambodian"
705
			"kn", // "Kannada"
706
			"ko", // "Korean"
707
			"ks", // "Kashmiri"
708
			"ku", // "Kurdish"
709
			"ky", // "Kirghiz"
710
			"la", // "Latin"
711
			"ln", // "Lingala"
712
			"lo", // "Laothian"
713
			"lt", // "Lithuanian"
714
			"lv", // "Latvian/Lettish"
715
			"mg", // "Malagasy"
716
			"mi", // "Maori"
717
			"mk", // "Macedonian"
718
			"ml", // "Malayalam"
719
			"mn", // "Mongolian"
720
			"mo", // "Moldavian"
721
			"mr", // "Marathi"
722
			"ms", // "Malay"
723
			"mt", // "Maltese"
724
			"my", // "Burmese"
725
			"na", // "Nauru"
726
			"ne", // "Nepali"
727
			"nl", // "Dutch"
728
			"no", // "Norwegian"
729
			"oc", // "Occitan"
730
			"om", // "(Afan) Oromo"
731
			"or", // "Oriya"
732
			"pa", // "Punjabi"
733
			"pl", // "Polish"
734
			"ps", // "Pashto / Pushto"
735
			"pt", // "Portuguese"
736
			"pt_br", // "Portuguese (Brazil)"
737
			"qu", // "Quechua"
738
			"rm", // "Rhaeto-Romance"
739
			"rn", // "Kirundi"
740
			"ro", // "Romanian"
741
			"ro_ro", // "Romanian (Romania)"
742
			"ru", // "Russian"
743
			"rw", // "Kinyarwanda"
744
			"sa", // "Sanskrit"
745
			"sd", // "Sindhi"
746
			"sg", // "Sangro"
747
			"sh", // "Serbo-Croatian"
748
			"si", // "Singhalese"
749
			"sk", // "Slovak"
750
			"sl", // "Slovenian"
751
			"sm", // "Samoan"
752
			"sn", // "Shona"
753
			"so", // "Somali"
754
			"sq", // "Albanian"
755
			"sr", // "Serbian"
756
			"sr_latin", // "Serbian (Latin)"
757
			"ss", // "Siswati"
758
			"st", // "Sesotho"
759
			"su", // "Sundanese"
760
			"sv", // "Swedish"
761
			"sw", // "Swahili"
762
			"ta", // "Tamil"
763
			"te", // "Tegulu"
764
			"tg", // "Tajik"
765
			"th", // "Thai"
766
			"ti", // "Tigrinya"
767
			"tk", // "Turkmen"
768
			"tl", // "Tagalog"
769
			"tn", // "Setswana"
770
			"to", // "Tonga"
771
			"tr", // "Turkish"
772
			"ts", // "Tsonga"
773
			"tt", // "Tatar"
774
			"tw", // "Twi"
775
			"ug", // "Uigur"
776
			"uk", // "Ukrainian"
777
			"ur", // "Urdu"
778
			"uz", // "Uzbek"
779
			"vi", // "Vietnamese"
780
			"vo", // "Volapuk"
781
			"wo", // "Wolof"
782
			"xh", // "Xhosa"
783
			"yi", // "Yiddish"
784
			"yo", // "Yoruba"
785
			"za", // "Zuang"
786
			"zh", // "Chinese"
787
			"zh_hans", // "Chinese Simplified"
788
			"zu", // "Zulu"
789
		];
790
	}
791
792
	/**
793
	 * Normalize a language code (e.g. from Transifex)
794
	 *
795
	 * @param string $code Language code
796
	 *
797
	 * @return string
798
	 */
799
	public static function normalizeLanguageCode($code) {
800
		$code = strtolower($code);
801
		$code = preg_replace('~[^a-z0-9]~', '_', $code);
802
		return $code;
803
	}
804
}