Completed
Push — master ( 79f16f...3ff314 )
by Morris
36:57 queued 16:20
created

Factory   F

Complexity

Total Complexity 111

Size/Duplication

Total Lines 603
Duplicated Lines 4.64 %

Coupling/Cohesion

Components 2
Dependencies 9

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 111
c 1
b 0
f 0
lcom 2
cbo 9
dl 28
loc 603
rs 1.997

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
B get() 0 33 9
C findLanguage() 0 54 15
C findLocale() 0 35 12
C findAvailableLanguages() 20 44 14
A findAvailableLocales() 0 10 2
A languageExists() 0 8 2
B iterateLanguage() 0 30 8
A localeExists() 0 12 2
B getLanguageFromRequest() 0 30 7
A respectDefaultLanguage() 0 16 5
A isSubDirectory() 0 13 3
B getL10nFilesForApp() 0 27 8
A findL10nDir() 0 11 5
B createPluralFunction() 0 55 7
C getLanguages() 8 63 11

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Factory often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Factory, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @copyright Copyright (c) 2016, ownCloud, Inc.
4
 * @copyright 2016 Roeland Jago Douma <[email protected]>
5
 * @copyright 2016 Lukas Reschke <[email protected]>
6
 *
7
 * @author Bart Visscher <[email protected]>
8
 * @author Joas Schilling <[email protected]>
9
 * @author Lukas Reschke <[email protected]>
10
 * @author Morris Jobke <[email protected]>
11
 * @author Robin Appelman <[email protected]>
12
 * @author Robin McCorkell <[email protected]>
13
 * @author Roeland Jago Douma <[email protected]>
14
 *
15
 * @license AGPL-3.0
16
 *
17
 * This code is free software: you can redistribute it and/or modify
18
 * it under the terms of the GNU Affero General Public License, version 3,
19
 * as published by the Free Software Foundation.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
 * GNU Affero General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU Affero General Public License, version 3,
27
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
28
 *
29
 */
30
31
namespace OC\L10N;
32
33
use OCP\IConfig;
34
use OCP\IRequest;
35
use OCP\IUser;
36
use OCP\IUserSession;
37
use OCP\L10N\IFactory;
38
39
/**
40
 * A factory that generates language instances
41
 */
42
class Factory implements IFactory {
43
44
	/** @var string */
45
	protected $requestLanguage = '';
46
47
	/**
48
	 * cached instances
49
	 * @var array Structure: Lang => App => \OCP\IL10N
50
	 */
51
	protected $instances = [];
52
53
	/**
54
	 * @var array Structure: App => string[]
55
	 */
56
	protected $availableLanguages = [];
57
58
	/**
59
	 * @var array
60
	 */
61
	protected $availableLocales = [];
62
63
	/**
64
	 * @var array Structure: string => callable
65
	 */
66
	protected $pluralFunctions = [];
67
68
	const COMMON_LANGUAGE_CODES = [
69
		'en', 'es', 'fr', 'de', 'de_DE', 'ja', 'ar', 'ru', 'nl', 'it',
70
		'pt_BR', 'pt_PT', 'da', 'fi_FI', 'nb_NO', 'sv', 'tr', 'zh_CN', 'ko'
71
	];
72
73
	/** @var IConfig */
74
	protected $config;
75
76
	/** @var IRequest */
77
	protected $request;
78
79
	/** @var IUserSession */
80
	protected $userSession;
81
82
	/** @var string */
83
	protected $serverRoot;
84
85
	/**
86
	 * @param IConfig $config
87
	 * @param IRequest $request
88
	 * @param IUserSession $userSession
89
	 * @param string $serverRoot
90
	 */
91
	public function __construct(IConfig $config,
92
								IRequest $request,
93
								IUserSession $userSession,
94
								$serverRoot) {
95
		$this->config = $config;
96
		$this->request = $request;
97
		$this->userSession = $userSession;
98
		$this->serverRoot = $serverRoot;
99
	}
100
101
	/**
102
	 * Get a language instance
103
	 *
104
	 * @param string $app
105
	 * @param string|null $lang
106
	 * @param string|null $locale
107
	 * @return \OCP\IL10N
108
	 */
109
	public function get($app, $lang = null, $locale = null) {
110
		$app = \OC_App::cleanAppId($app);
111
		if ($lang !== null) {
112
			$lang = str_replace(array('\0', '/', '\\', '..'), '', (string) $lang);
113
		}
114
115
		$forceLang = $this->config->getSystemValue('force_language', false);
116
		if (is_string($forceLang)) {
117
			$lang = $forceLang;
118
		}
119
120
		$forceLocale = $this->config->getSystemValue('force_locale', false);
121
		if (is_string($forceLocale)) {
122
			$locale = $forceLocale;
123
		}
124
125
		if ($lang === null || !$this->languageExists($app, $lang)) {
126
			$lang = $this->findLanguage($app);
127
		}
128
129
		if ($locale === null || !$this->localeExists($locale)) {
130
			$locale = $this->findLocale($lang);
131
		}
132
133
		if (!isset($this->instances[$lang][$app])) {
134
			$this->instances[$lang][$app] = new L10N(
135
				$this, $app, $lang, $locale,
136
				$this->getL10nFilesForApp($app, $lang)
137
			);
138
		}
139
140
		return $this->instances[$lang][$app];
141
	}
142
143
	/**
144
	 * Find the best language
145
	 *
146
	 * @param string|null $app App id or null for core
147
	 * @return string language If nothing works it returns 'en'
148
	 */
149
	public function findLanguage($app = null) {
150
		$forceLang = $this->config->getSystemValue('force_language', false);
151
		if (is_string($forceLang)) {
152
			$this->requestLanguage = $forceLang;
153
		}
154
155
		if ($this->requestLanguage !== '' && $this->languageExists($app, $this->requestLanguage)) {
156
			return $this->requestLanguage;
157
		}
158
159
		/**
160
		 * At this point Nextcloud might not yet be installed and thus the lookup
161
		 * in the preferences table might fail. For this reason we need to check
162
		 * whether the instance has already been installed
163
		 *
164
		 * @link https://github.com/owncloud/core/issues/21955
165
		 */
166
		if ($this->config->getSystemValue('installed', false)) {
167
			$userId = !is_null($this->userSession->getUser()) ? $this->userSession->getUser()->getUID() :  null;
168
			if (!is_null($userId)) {
169
				$userLang = $this->config->getUserValue($userId, 'core', 'lang', null);
170
			} else {
171
				$userLang = null;
172
			}
173
		} else {
174
			$userId = null;
175
			$userLang = null;
176
		}
177
178
		if ($userLang) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $userLang of type string|null 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...
179
			$this->requestLanguage = $userLang;
180
			if ($this->languageExists($app, $userLang)) {
181
				return $userLang;
182
			}
183
		}
184
185
		try {
186
			// Try to get the language from the Request
187
			$lang = $this->getLanguageFromRequest($app);
188
			if ($userId !== null && $app === null && !$userLang) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $userLang of type string|null is loosely compared to false; 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...
189
				$this->config->setUserValue($userId, 'core', 'lang', $lang);
190
			}
191
			return $lang;
192
		} catch (LanguageNotFoundException $e) {
193
			// Finding language from request failed fall back to default language
194
			$defaultLanguage = $this->config->getSystemValue('default_language', false);
195
			if ($defaultLanguage !== false && $this->languageExists($app, $defaultLanguage)) {
196
				return $defaultLanguage;
197
			}
198
		}
199
200
		// We could not find any language so fall back to english
201
		return 'en';
202
	}
203
204
	/**
205
	 * find the best locale
206
	 *
207
	 * @param string $lang
208
	 * @return null|string
209
	 */
210
	public function findLocale($lang = null) {
211
		$forceLocale = $this->config->getSystemValue('force_locale', false);
212
		if (is_string($forceLocale) && $this->localeExists($forceLocale)) {
213
			return $forceLocale;
214
		}
215
216
		if ($this->config->getSystemValue('installed', false)) {
217
			$userId = null !== $this->userSession->getUser() ? $this->userSession->getUser()->getUID() :  null;
218
			$userLocale = null;
219
			if (null !== $userId) {
220
				$userLocale = $this->config->getUserValue($userId, 'core', 'locale', null);
221
			}
222
		} else {
223
			$userId = null;
0 ignored issues
show
Unused Code introduced by
$userId is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
224
			$userLocale = null;
225
		}
226
227
		if ($userLocale && $this->localeExists($userLocale)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $userLocale of type string|null 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...
228
			return $userLocale;
229
		}
230
231
		// Default : use system default locale
232
		$defaultLocale = $this->config->getSystemValue('default_locale', false);
233
		if ($defaultLocale !== false && $this->localeExists($defaultLocale)) {
234
			return $defaultLocale;
235
		}
236
237
		// If no user locale set, use lang as locale
238
		if (null !== $lang && $this->localeExists($lang)) {
239
			return $lang;
240
		}
241
242
		// At last, return USA
243
		return 'en_US';
244
	}
245
246
	/**
247
	 * Find all available languages for an app
248
	 *
249
	 * @param string|null $app App id or null for core
250
	 * @return array an array of available languages
251
	 */
252
	public function findAvailableLanguages($app = null) {
253
		$key = $app;
254
		if ($key === null) {
255
			$key = 'null';
256
		}
257
258
		// also works with null as key
259
		if (!empty($this->availableLanguages[$key])) {
260
			return $this->availableLanguages[$key];
261
		}
262
263
		$available = ['en']; //english is always available
264
		$dir = $this->findL10nDir($app);
265 View Code Duplication
		if (is_dir($dir)) {
266
			$files = scandir($dir);
267
			if ($files !== false) {
268
				foreach ($files as $file) {
269
					if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
270
						$available[] = substr($file, 0, -5);
271
					}
272
				}
273
			}
274
		}
275
276
		// merge with translations from theme
277
		$theme = $this->config->getSystemValue('theme');
278
		if (!empty($theme)) {
279
			$themeDir = $this->serverRoot . '/themes/' . $theme . substr($dir, strlen($this->serverRoot));
280
281 View Code Duplication
			if (is_dir($themeDir)) {
282
				$files = scandir($themeDir);
283
				if ($files !== false) {
284
					foreach ($files as $file) {
285
						if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
286
							$available[] = substr($file, 0, -5);
287
						}
288
					}
289
				}
290
			}
291
		}
292
293
		$this->availableLanguages[$key] = $available;
294
		return $available;
295
	}
296
297
	/**
298
	 * @return array|mixed
299
	 */
300
	public function findAvailableLocales() {
301
		if (!empty($this->availableLocales)) {
302
			return $this->availableLocales;
303
		}
304
305
		$localeData = file_get_contents(\OC::$SERVERROOT . '/resources/locales.json');
306
		$this->availableLocales = \json_decode($localeData, true);
0 ignored issues
show
Documentation Bug introduced by
It seems like \json_decode($localeData, true) of type * is incompatible with the declared type array of property $availableLocales.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
307
308
		return $this->availableLocales;
309
	}
310
311
	/**
312
	 * @param string|null $app App id or null for core
313
	 * @param string $lang
314
	 * @return bool
315
	 */
316
	public function languageExists($app, $lang) {
317
		if ($lang === 'en') {//english is always available
318
			return true;
319
		}
320
321
		$languages = $this->findAvailableLanguages($app);
322
		return array_search($lang, $languages) !== false;
323
	}
324
325
	public function iterateLanguage(bool $reset = false): string {
326
		static $i = 0;
327
		if($reset) {
328
			$i = 0;
329
		}
330
		switch($i) {
331
			/** @noinspection PhpMissingBreakStatementInspection */
332
			case 0:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
333
				$i++;
334
				$forcedLang = $this->config->getSystemValue('force_language', false);
335
				if(is_string($forcedLang)) {
336
					return $forcedLang;
337
				}
338
			/** @noinspection PhpMissingBreakStatementInspection */
339
			case 1:
0 ignored issues
show
Coding Style introduced by
There must be a comment when fall-through is intentional in a non-empty case body
Loading history...
340
				$i++;
341
				$user = $this->userSession->getUser();
342
				if($user instanceof IUser) {
343
					$userLang = $this->config->getUserValue($user->getUID(), 'core', 'lang', null);
344
					if(is_string($userLang)) {
345
						return $userLang;
346
					}
347
				}
348
			case 2:
349
				$i++;
350
				return $this->config->getSystemValue('default_language', 'en');
351
			default:
352
				return 'en';
353
		}
354
	}
355
356
	/**
357
	 * @param string $locale
358
	 * @return bool
359
	 */
360
	public function localeExists($locale) {
361
		if ($locale === 'en') { //english is always available
362
			return true;
363
		}
364
365
		$locales = $this->findAvailableLocales();
366
		$userLocale = array_filter($locales, function($value) use ($locale) {
367
			return $locale === $value['code'];
368
		});
369
370
		return !empty($userLocale);
371
	}
372
373
	/**
374
	 * @param string|null $app
375
	 * @return string
376
	 * @throws LanguageNotFoundException
377
	 */
378
	private function getLanguageFromRequest($app) {
379
		$header = $this->request->getHeader('ACCEPT_LANGUAGE');
380
		if ($header !== '') {
381
			$available = $this->findAvailableLanguages($app);
382
383
			// E.g. make sure that 'de' is before 'de_DE'.
384
			sort($available);
385
386
			$preferences = preg_split('/,\s*/', strtolower($header));
387
			foreach ($preferences as $preference) {
388
				list($preferred_language) = explode(';', $preference);
389
				$preferred_language = str_replace('-', '_', $preferred_language);
390
391
				foreach ($available as $available_language) {
392
					if ($preferred_language === strtolower($available_language)) {
393
						return $this->respectDefaultLanguage($app, $available_language);
394
					}
395
				}
396
397
				// Fallback from de_De to de
398
				foreach ($available as $available_language) {
399
					if (substr($preferred_language, 0, 2) === $available_language) {
400
						return $available_language;
401
					}
402
				}
403
			}
404
		}
405
406
		throw new LanguageNotFoundException();
407
	}
408
409
	/**
410
	 * if default language is set to de_DE (formal German) this should be
411
	 * preferred to 'de' (non-formal German) if possible
412
	 *
413
	 * @param string|null $app
414
	 * @param string $lang
415
	 * @return string
416
	 */
417
	protected function respectDefaultLanguage($app, $lang) {
418
		$result = $lang;
419
		$defaultLanguage = $this->config->getSystemValue('default_language', false);
420
421
		// use formal version of german ("Sie" instead of "Du") if the default
422
		// language is set to 'de_DE' if possible
423
		if (is_string($defaultLanguage) &&
424
			strtolower($lang) === 'de' &&
425
			strtolower($defaultLanguage) === 'de_de' &&
426
			$this->languageExists($app, 'de_DE')
427
		) {
428
			$result = 'de_DE';
429
		}
430
431
		return $result;
432
	}
433
434
	/**
435
	 * Checks if $sub is a subdirectory of $parent
436
	 *
437
	 * @param string $sub
438
	 * @param string $parent
439
	 * @return bool
440
	 */
441
	private function isSubDirectory($sub, $parent) {
442
		// Check whether $sub contains no ".."
443
		if (strpos($sub, '..') !== false) {
444
			return false;
445
		}
446
447
		// Check whether $sub is a subdirectory of $parent
448
		if (strpos($sub, $parent) === 0) {
449
			return true;
450
		}
451
452
		return false;
453
	}
454
455
	/**
456
	 * Get a list of language files that should be loaded
457
	 *
458
	 * @param string $app
459
	 * @param string $lang
460
	 * @return string[]
461
	 */
462
	// FIXME This method is only public, until OC_L10N does not need it anymore,
463
	// FIXME This is also the reason, why it is not in the public interface
464
	public function getL10nFilesForApp($app, $lang) {
465
		$languageFiles = [];
466
467
		$i18nDir = $this->findL10nDir($app);
468
		$transFile = strip_tags($i18nDir) . strip_tags($lang) . '.json';
469
470
		if (($this->isSubDirectory($transFile, $this->serverRoot . '/core/l10n/')
471
				|| $this->isSubDirectory($transFile, $this->serverRoot . '/lib/l10n/')
472
				|| $this->isSubDirectory($transFile, $this->serverRoot . '/settings/l10n/')
473
				|| $this->isSubDirectory($transFile, \OC_App::getAppPath($app) . '/l10n/')
474
			)
475
			&& file_exists($transFile)) {
476
			// load the translations file
477
			$languageFiles[] = $transFile;
478
		}
479
480
		// merge with translations from theme
481
		$theme = $this->config->getSystemValue('theme');
482
		if (!empty($theme)) {
483
			$transFile = $this->serverRoot . '/themes/' . $theme . substr($transFile, strlen($this->serverRoot));
484
			if (file_exists($transFile)) {
485
				$languageFiles[] = $transFile;
486
			}
487
		}
488
489
		return $languageFiles;
490
	}
491
492
	/**
493
	 * find the l10n directory
494
	 *
495
	 * @param string $app App id or empty string for core
496
	 * @return string directory
497
	 */
498
	protected function findL10nDir($app = null) {
499
		if (in_array($app, ['core', 'lib', 'settings'])) {
500
			if (file_exists($this->serverRoot . '/' . $app . '/l10n/')) {
501
				return $this->serverRoot . '/' . $app . '/l10n/';
502
			}
503
		} else if ($app && \OC_App::getAppPath($app) !== false) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $app of type string|null 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...
504
			// Check if the app is in the app folder
505
			return \OC_App::getAppPath($app) . '/l10n/';
506
		}
507
		return $this->serverRoot . '/core/l10n/';
508
	}
509
510
511
	/**
512
	 * Creates a function from the plural string
513
	 *
514
	 * Parts of the code is copied from Habari:
515
	 * https://github.com/habari/system/blob/master/classes/locale.php
516
	 * @param string $string
517
	 * @return string
518
	 */
519
	public function createPluralFunction($string) {
520
		if (isset($this->pluralFunctions[$string])) {
521
			return $this->pluralFunctions[$string];
522
		}
523
524
		if (preg_match( '/^\s*nplurals\s*=\s*(\d+)\s*;\s*plural=(.*)$/u', $string, $matches)) {
525
			// sanitize
526
			$nplurals = preg_replace( '/[^0-9]/', '', $matches[1] );
527
			$plural = preg_replace( '#[^n0-9:\(\)\?\|\&=!<>+*/\%-]#', '', $matches[2] );
528
529
			$body = str_replace(
530
				array( 'plural', 'n', '$n$plurals', ),
531
				array( '$plural', '$n', '$nplurals', ),
532
				'nplurals='. $nplurals . '; plural=' . $plural
533
			);
534
535
			// add parents
536
			// important since PHP's ternary evaluates from left to right
537
			$body .= ';';
538
			$res = '';
539
			$p = 0;
540
			$length = strlen($body);
541
			for($i = 0; $i < $length; $i++) {
542
				$ch = $body[$i];
543
				switch ( $ch ) {
544
					case '?':
545
						$res .= ' ? (';
546
						$p++;
547
						break;
548
					case ':':
549
						$res .= ') : (';
550
						break;
551
					case ';':
552
						$res .= str_repeat( ')', $p ) . ';';
553
						$p = 0;
554
						break;
555
					default:
556
						$res .= $ch;
557
				}
558
			}
559
560
			$body = $res . 'return ($plural>=$nplurals?$nplurals-1:$plural);';
561
			$function = create_function('$n', $body);
562
			$this->pluralFunctions[$string] = $function;
563
			return $function;
564
		} else {
565
			// default: one plural form for all cases but n==1 (english)
566
			$function = create_function(
567
				'$n',
568
				'$nplurals=2;$plural=($n==1?0:1);return ($plural>=$nplurals?$nplurals-1:$plural);'
569
			);
570
			$this->pluralFunctions[$string] = $function;
571
			return $function;
572
		}
573
	}
574
575
	/**
576
	 * returns the common language and other languages in an
577
	 * associative array
578
	 *
579
	 * @return array
580
	 */
581
	public function getLanguages() {
582
		$forceLanguage = $this->config->getSystemValue('force_language', false);
583
		if ($forceLanguage !== false) {
584
			return [];
585
		}
586
587
		$languageCodes = $this->findAvailableLanguages();
588
589
		$commonLanguages = [];
590
		$languages = [];
591
592
		foreach($languageCodes as $lang) {
593
			$l = $this->get('lib', $lang);
594
			// TRANSLATORS this is the language name for the language switcher in the personal settings and should be the localized version
595
			$potentialName = (string) $l->t('__language_name__');
596
			if ($l->getLanguageCode() === $lang && $potentialName[0] !== '_') {//first check if the language name is in the translation file
597
				$ln = array(
598
					'code' => $lang,
599
					'name' => $potentialName
600
				);
601
			} else if ($lang === 'en') {
602
				$ln = array(
603
					'code' => $lang,
604
					'name' => 'English (US)'
605
				);
606
			} else {//fallback to language code
607
				$ln = array(
608
					'code' => $lang,
609
					'name' => $lang
610
				);
611
			}
612
613
			// put appropriate languages into appropriate arrays, to print them sorted
614
			// common languages -> divider -> other languages
615
			if (in_array($lang, self::COMMON_LANGUAGE_CODES)) {
616
				$commonLanguages[array_search($lang, self::COMMON_LANGUAGE_CODES)] = $ln;
617
			} else {
618
				$languages[] = $ln;
619
			}
620
		}
621
622
		ksort($commonLanguages);
623
624
		// sort now by displayed language not the iso-code
625
		usort( $languages, function ($a, $b) {
626 View Code Duplication
			if ($a['code'] === $a['name'] && $b['code'] !== $b['name']) {
627
				// If a doesn't have a name, but b does, list b before a
628
				return 1;
629
			}
630 View Code Duplication
			if ($a['code'] !== $a['name'] && $b['code'] === $b['name']) {
631
				// If a does have a name, but b doesn't, list a before b
632
				return -1;
633
			}
634
			// Otherwise compare the names
635
			return strcmp($a['name'], $b['name']);
636
		});
637
638
		return [
639
			// reset indexes
640
			'commonlanguages' => array_values($commonLanguages),
641
			'languages' => $languages
642
		];
643
	}
644
}
645