Passed
Push — master ( 9d11d1...a4b028 )
by Jeroen
10:12
created

Translator::registerLanguagePath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 2
ccs 0
cts 0
cp 0
crap 2
rs 10
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 Steve Clay
$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 Steve Clay
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...
introduced by Steve Clay
The condition $args can never be true.
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();
0 ignored issues
show
Documentation Bug introduced by Ismayil Khayredinov
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) {
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 Juho Jaakkola
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->registerLanguagePath($this->defaultPath);
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 Juho Jaakkola
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...
introduced by Juho Jaakkola
The condition ! $plugins can never be false.
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
	 * When given a full path, finds translation files and loads them
356
	 *
357
	 * @param string $path     Full path
358
	 * @param bool   $load_all If true all languages are loaded, if
359
	 *                         false only the current language + en are loaded
360 24
	 * @param string $language Language code
361 24
	 *
362
	 * @return bool success
363
	 */
364 24
	public function registerTranslations($path, $load_all = false, $language = null) {
365 2
		$path = \Elgg\Project\Paths::sanitize($path);
366
		
367
		// don't need to register translations as the folder is missing
368 24
		if (!is_dir($path)) {
369
			_elgg_services()->logger->info("No translations could be loaded from: $path");
370
			return true;
371
		}
372
373
		// Make a note of this path just in case we need to register this language later
374
		$this->registerLanguagePath($path);
375
		$this->is_initialized = true;
376
377
		_elgg_services()->logger->info("Translations loaded from: $path");
378
379
		if ($language) {
0 ignored issues
show
Bug Best Practice introduced by Juho Jaakkola
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...
380
			$load_language_files = ["$language.php"];
381 4781
			$load_all = false;
382 4781
		} else {
383
			// Get the current language based on site defaults and user preference
384
			$current_language = $this->getCurrentLanguage();
385 4781
386 4781
			$load_language_files = [
387
				'en.php',
388 4781
				"$current_language.php"
389
			];
390 4781
391 989
			$load_language_files = array_unique($load_language_files);
392 989
		}
393
394
		$return = true;
395 4781
		if ($handle = opendir($path)) {
396
			while (false !== ($language_file = readdir($handle))) {
0 ignored issues
show
introduced by Ismayil Khayredinov
The condition false !== $language_file = readdir($handle) can never be false.
Loading history...
397
				// ignore bad files
398 4781
				if (substr($language_file, 0, 1) == '.' || substr($language_file, -4) !== '.php') {
399 4781
					continue;
400
				}
401
402 4781
				if (in_array($language_file, $load_language_files) || $load_all) {
403
					$return = $return && $this->includeLanguageFile($path . $language_file);
404
				}
405 4781
			}
406 4781
			closedir($handle);
407
		} else {
408
			_elgg_services()->logger->error("Could not open language path: $path");
409
			$return = false;
410
		}
411 4781
412 4781
		return $return;
413
	}
414 4781
415 4781
	/**
416
	 * Load cached or include a language file by its path
417
	 *
418 4781
	 * @param string $path Path to file
419 4781
	 * @return bool
420 4781
	 */
421
	protected function includeLanguageFile($path) {
422
		$cache_key = "lang/" . sha1($path);
423 4781
		$result = elgg_get_system_cache()->load($cache_key);
424 4781
		if (!isset($result)) {
425
			$result = Includer::includeFile($path);
426
			elgg_get_system_cache()->save($cache_key, $result);
427
		}
428
		if (is_array($result)) {
429 4781
			$this->addTranslation(basename($path, '.php'), $result);
430
			return true;
431
		}
432
433
		return false;
434
	}
435
436
	/**
437
	 * Reload all translations from all registered paths.
438
	 *
439
	 * This is only called by functions which need to know all possible translations.
440
	 *
441 945
	 * @todo Better on demand loading based on language_paths array
442 945
	 *
443 455
	 * @return void
444
	 */
445
	public function reloadAllTranslations() {
446 945
		if ($this->was_reloaded) {
447
			return;
448
		}
449
		
450
		$languages = $this->getAvailableLanguages();
451
		
452
		foreach ($languages as $language) {
453
			if ($this->loaded_from_cache) {
454
				$data = elgg_load_system_cache("{$language}.lang");
455
				if ($data) {
456
					$this->addTranslation($language, unserialize($data));
457
				}
458
			} else {
459
				foreach ($this->getLanguagePaths() as $path) {
460
					$this->registerTranslations($path, false, $language);
461
				}
462
			}
463 945
		}
464 945
		
465
		_elgg_services()->hooks->getEvents()->triggerAfter('reload', 'translations');
466
467
		$this->was_reloaded = true;
468 945
	}
469
470 945
	/**
471 945
	 * Return an array of installed translations as an associative
472
	 * array "two letter code" => "native language name".
473
	 *
474
	 * @param boolean $calculate_completeness Set to true if you want a completeness postfix added to the language text
475
	 *
476
	 * @return array
477
	 */
478
	public function getInstalledTranslations($calculate_completeness = false) {
479 487
		if ($calculate_completeness) {
480
			// Ensure that all possible translations are loaded
481 487
			$this->reloadAllTranslations();
482
		}
483 487
		
484
		$result = [];
485 487
486
		$languages = $this->getAvailableLanguages();
487 487
		foreach ($languages as $language) {
488 487
			if ($this->languageKeyExists($language, $language)) {
489 487
				$value = $this->translate($language, [], $language);
490
			} else {
491 476
				$value = $this->translate($language);
492
			}
493
			
494 487
			if (($language !== 'en') && $calculate_completeness) {
495
				$completeness = $this->getLanguageCompleteness($language);
496 487
				$value .= " (" . $completeness . "% " . $this->translate('complete') . ")";
497 487
			}
498
			
499
			$result[$language] = $value;
500
		}
501
		
502
		natcasesort($result);
503
			
504
		return $result;
505
	}
506 487
507
	/**
508
	 * Return the level of completeness for a given language code (compared to english)
509
	 *
510
	 * @param string $language Language
511
	 *
512
	 * @return float
513
	 */
514
	public function getLanguageCompleteness($language) {
515
516 487
		if ($language == 'en') {
517
			return (float) 100;
518 487
		}
519 32
520
		// Ensure that all possible translations are loaded
521
		$this->reloadAllTranslations();
522
523 455
		$language = sanitise_string($language);
0 ignored issues
show
Deprecated Code introduced by Evan Winslow
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

523
		$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...
524
525 455
		$en = count($this->translations['en']);
526
527 455
		$missing = $this->getMissingLanguageKeys($language);
528
		if ($missing) {
0 ignored issues
show
introduced by Evan Winslow
The condition $missing can never be true.
Loading history...
529 455
			$missing = count($missing);
530 455
		} else {
531 455
			$missing = 0;
532
		}
533
534
		$lang = $en - $missing;
535
536 455
		return round(($lang / $en) * 100, 2);
537
	}
538 455
539
	/**
540
	 * Return the translation keys missing from a given language,
541
	 * or those that are identical to the english version.
542
	 *
543
	 * @param string $language The language
544
	 *
545
	 * @return mixed
546
	 */
547
	public function getMissingLanguageKeys($language) {
548
549 455
550
		// Ensure that all possible translations are loaded
551
		$this->reloadAllTranslations();
552
553 455
		$missing = [];
554
555 455
		foreach ($this->translations['en'] as $k => $v) {
556
			if ((!isset($this->translations[$language][$k]))
557 455
				|| ($this->translations[$language][$k] == $this->translations['en'][$k])) {
558 455
				$missing[] = $k;
559 455
			}
560 455
		}
561
562
		if (count($missing)) {
563
			return $missing;
564 455
		}
565 455
566
		return false;
567
	}
568
569
	/**
570
	 * Check if a given language key exists
571
	 *
572
	 * @param string $key      The translation key
573
	 * @param string $language The specific language to check
574
	 *
575
	 * @return bool
576
	 * @since 1.11
577
	 */
578
	public function languageKeyExists($key, $language = 'en') {
579
		if (empty($key)) {
580 538
			return false;
581 538
		}
582
583
		$this->ensureTranslationsLoaded($language);
584
585 538
		if (!array_key_exists($language, $this->translations)) {
586
			return false;
587 538
		}
588 6
589
		return array_key_exists($key, $this->translations[$language]);
590
	}
591 532
	
592
	/**
593
	 * Returns an array of all available language keys. Triggers a hook to allow plugins to add/remove languages
594
	 *
595
	 * @return array
596
	 * @since 3.0
597
	 */
598
	public function getAvailableLanguages() {
599
		$languages = [];
600 870
		
601 870
		$allowed_languages = $this->getAllLanguageCodes();
602
		
603 11
		foreach ($this->getLanguagePaths() as $path) {
604
			try {
605
				$iterator = new \DirectoryIterator($path);
606 870
			} catch (Exception $e) {
0 ignored issues
show
Bug introduced by Jeroen Dalsem
The type Elgg\I18n\Exception was not found. Did you mean Exception? If so, make sure to prefix the type with \.
Loading history...
607
				continue;
608
			}
609
			
610
			foreach ($iterator as $file) {
611 8
				if ($file->isDir()) {
612
					continue;
613 870
				}
614
				
615
				if ($file->getExtension() !== 'php') {
616
					continue;
617
				}
618
				
619
				$language = $file->getBasename('.php');
620
				if (empty($language) || !in_array($language, $allowed_languages)) {
621
					continue;
622
				}
623
				
624
				$languages[$language] = true;
625
			}
626
		}
627
		
628
		$languages = array_keys($languages);
629
				
630
		return _elgg_services()->hooks->trigger('languages', 'translations', [], $languages);
631
	}
632
	
633
	/**
634
	 * Registers a path for potential translation files
635
	 *
636
	 * @param string $path path to a folder that contains translation files
637
	 *
638
	 * @return void
639
	 */
640
	public function registerLanguagePath($path) {
641
		$this->language_paths[$path] = true;
642
	}
643
	
644
	/**
645
	 * Returns a unique array with locations of translation files
646
	 *
647
	 * @return array
648
	 */
649
	protected function getLanguagePaths() {
650
		return array_keys($this->language_paths);
651
	}
652
653
	/**
654
	 * Make sure translations are loaded
655
	 *
656
	 * @param string $language Language
657
	 * @return void
658
	 */
659
	private function ensureTranslationsLoaded($language) {
660
		if (!$this->is_initialized) {
661
			// this means we probably had an exception before translations were initialized
662
			$this->registerTranslations($this->defaultPath);
663
		}
664
665
		if (!isset($this->translations[$language])) {
666
			// The language being requested is not the same as the language of the
667
			// logged in user, so we will have to load it separately. (Most likely
668
			// we're sending a notification and the recipient is using a different
669
			// language than the logged in user.)
670
			$this->loadTranslations($language);
671
		}
672
	}
673
674
	/**
675
	 * Returns an array of language codes.
676
	 *
677
	 * @return array
678
	 */
679
	public static function getAllLanguageCodes() {
680
		return [
681
			"aa", // "Afar"
682
			"ab", // "Abkhazian"
683
			"af", // "Afrikaans"
684
			"am", // "Amharic"
685
			"ar", // "Arabic"
686
			"as", // "Assamese"
687
			"ay", // "Aymara"
688
			"az", // "Azerbaijani"
689
			"ba", // "Bashkir"
690
			"be", // "Byelorussian"
691
			"bg", // "Bulgarian"
692
			"bh", // "Bihari"
693
			"bi", // "Bislama"
694
			"bn", // "Bengali; Bangla"
695
			"bo", // "Tibetan"
696
			"br", // "Breton"
697
			"ca", // "Catalan"
698
			"cmn", // "Mandarin Chinese" // ISO 639-3
699
			"co", // "Corsican"
700
			"cs", // "Czech"
701
			"cy", // "Welsh"
702
			"da", // "Danish"
703
			"de", // "German"
704
			"dz", // "Bhutani"
705
			"el", // "Greek"
706
			"en", // "English"
707
			"eo", // "Esperanto"
708
			"es", // "Spanish"
709
			"et", // "Estonian"
710
			"eu", // "Basque"
711
			"eu_es", // "Basque (Spain)"
712
			"fa", // "Persian"
713
			"fi", // "Finnish"
714
			"fj", // "Fiji"
715
			"fo", // "Faeroese"
716
			"fr", // "French"
717
			"fy", // "Frisian"
718
			"ga", // "Irish"
719
			"gd", // "Scots / Gaelic"
720
			"gl", // "Galician"
721
			"gn", // "Guarani"
722
			"gu", // "Gujarati"
723
			"he", // "Hebrew"
724
			"ha", // "Hausa"
725
			"hi", // "Hindi"
726
			"hr", // "Croatian"
727
			"hu", // "Hungarian"
728
			"hy", // "Armenian"
729
			"ia", // "Interlingua"
730
			"id", // "Indonesian"
731
			"ie", // "Interlingue"
732
			"ik", // "Inupiak"
733
			"is", // "Icelandic"
734
			"it", // "Italian"
735
			"iu", // "Inuktitut"
736
			"iw", // "Hebrew (obsolete)"
737
			"ja", // "Japanese"
738
			"ji", // "Yiddish (obsolete)"
739
			"jw", // "Javanese"
740
			"ka", // "Georgian"
741
			"kk", // "Kazakh"
742
			"kl", // "Greenlandic"
743
			"km", // "Cambodian"
744
			"kn", // "Kannada"
745
			"ko", // "Korean"
746
			"ks", // "Kashmiri"
747
			"ku", // "Kurdish"
748
			"ky", // "Kirghiz"
749
			"la", // "Latin"
750
			"ln", // "Lingala"
751
			"lo", // "Laothian"
752
			"lt", // "Lithuanian"
753
			"lv", // "Latvian/Lettish"
754
			"mg", // "Malagasy"
755
			"mi", // "Maori"
756
			"mk", // "Macedonian"
757
			"ml", // "Malayalam"
758
			"mn", // "Mongolian"
759
			"mo", // "Moldavian"
760
			"mr", // "Marathi"
761
			"ms", // "Malay"
762
			"mt", // "Maltese"
763
			"my", // "Burmese"
764
			"na", // "Nauru"
765
			"ne", // "Nepali"
766
			"nl", // "Dutch"
767
			"no", // "Norwegian"
768
			"oc", // "Occitan"
769
			"om", // "(Afan) Oromo"
770
			"or", // "Oriya"
771
			"pa", // "Punjabi"
772
			"pl", // "Polish"
773
			"ps", // "Pashto / Pushto"
774
			"pt", // "Portuguese"
775
			"pt_br", // "Portuguese (Brazil)"
776
			"qu", // "Quechua"
777
			"rm", // "Rhaeto-Romance"
778
			"rn", // "Kirundi"
779
			"ro", // "Romanian"
780
			"ro_ro", // "Romanian (Romania)"
781
			"ru", // "Russian"
782
			"rw", // "Kinyarwanda"
783
			"sa", // "Sanskrit"
784
			"sd", // "Sindhi"
785
			"sg", // "Sangro"
786
			"sh", // "Serbo-Croatian"
787
			"si", // "Singhalese"
788
			"sk", // "Slovak"
789
			"sl", // "Slovenian"
790
			"sm", // "Samoan"
791
			"sn", // "Shona"
792
			"so", // "Somali"
793
			"sq", // "Albanian"
794
			"sr", // "Serbian"
795
			"sr_latin", // "Serbian (Latin)"
796
			"ss", // "Siswati"
797
			"st", // "Sesotho"
798
			"su", // "Sundanese"
799
			"sv", // "Swedish"
800
			"sw", // "Swahili"
801
			"ta", // "Tamil"
802
			"te", // "Tegulu"
803
			"tg", // "Tajik"
804
			"th", // "Thai"
805
			"ti", // "Tigrinya"
806
			"tk", // "Turkmen"
807
			"tl", // "Tagalog"
808
			"tn", // "Setswana"
809
			"to", // "Tonga"
810
			"tr", // "Turkish"
811
			"ts", // "Tsonga"
812
			"tt", // "Tatar"
813
			"tw", // "Twi"
814
			"ug", // "Uigur"
815
			"uk", // "Ukrainian"
816
			"ur", // "Urdu"
817
			"uz", // "Uzbek"
818
			"vi", // "Vietnamese"
819
			"vo", // "Volapuk"
820
			"wo", // "Wolof"
821
			"xh", // "Xhosa"
822
			"yi", // "Yiddish"
823
			"yo", // "Yoruba"
824
			"za", // "Zuang"
825
			"zh", // "Chinese"
826
			"zh_hans", // "Chinese Simplified"
827
			"zu", // "Zulu"
828
		];
829
	}
830
831
	/**
832
	 * Normalize a language code (e.g. from Transifex)
833
	 *
834
	 * @param string $code Language code
835
	 *
836
	 * @return string
837
	 */
838
	public static function normalizeLanguageCode($code) {
839
		$code = strtolower($code);
840
		$code = preg_replace('~[^a-z0-9]~', '_', $code);
841
		return $code;
842
	}
843
}