Completed
Push — master ( 1801e4...143145 )
by Morris
19:04 queued 02:13
created

Factory::getLanguageFromRequest()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
nc 5
nop 1
dl 0
loc 30
rs 8.5066
c 0
b 0
f 0
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
use OCP\L10N\ILanguageIterator;
39
40
/**
41
 * A factory that generates language instances
42
 */
43
class Factory implements IFactory {
44
45
	/** @var string */
46
	protected $requestLanguage = '';
47
48
	/**
49
	 * cached instances
50
	 * @var array Structure: Lang => App => \OCP\IL10N
51
	 */
52
	protected $instances = [];
53
54
	/**
55
	 * @var array Structure: App => string[]
56
	 */
57
	protected $availableLanguages = [];
58
59
	/**
60
	 * @var array
61
	 */
62
	protected $availableLocales = [];
63
64
	/**
65
	 * @var array Structure: string => callable
66
	 */
67
	protected $pluralFunctions = [];
68
69
	const COMMON_LANGUAGE_CODES = [
70
		'en', 'es', 'fr', 'de', 'de_DE', 'ja', 'ar', 'ru', 'nl', 'it',
71
		'pt_BR', 'pt_PT', 'da', 'fi_FI', 'nb_NO', 'sv', 'tr', 'zh_CN', 'ko'
72
	];
73
74
	/** @var IConfig */
75
	protected $config;
76
77
	/** @var IRequest */
78
	protected $request;
79
80
	/** @var IUserSession */
81
	protected $userSession;
82
83
	/** @var string */
84
	protected $serverRoot;
85
86
	/**
87
	 * @param IConfig $config
88
	 * @param IRequest $request
89
	 * @param IUserSession $userSession
90
	 * @param string $serverRoot
91
	 */
92
	public function __construct(IConfig $config,
93
								IRequest $request,
94
								IUserSession $userSession,
95
								$serverRoot) {
96
		$this->config = $config;
97
		$this->request = $request;
98
		$this->userSession = $userSession;
99
		$this->serverRoot = $serverRoot;
100
	}
101
102
	/**
103
	 * Get a language instance
104
	 *
105
	 * @param string $app
106
	 * @param string|null $lang
107
	 * @param string|null $locale
108
	 * @return \OCP\IL10N
109
	 */
110
	public function get($app, $lang = null, $locale = null) {
111
		$app = \OC_App::cleanAppId($app);
112
		if ($lang !== null) {
113
			$lang = str_replace(array('\0', '/', '\\', '..'), '', (string) $lang);
114
		}
115
116
		$forceLang = $this->config->getSystemValue('force_language', false);
117
		if (is_string($forceLang)) {
118
			$lang = $forceLang;
119
		}
120
121
		$forceLocale = $this->config->getSystemValue('force_locale', false);
122
		if (is_string($forceLocale)) {
123
			$locale = $forceLocale;
124
		}
125
126
		if ($lang === null || !$this->languageExists($app, $lang)) {
127
			$lang = $this->findLanguage($app);
128
		}
129
130
		if ($locale === null || !$this->localeExists($locale)) {
131
			$locale = $this->findLocale($lang);
132
		}
133
134
		if (!isset($this->instances[$lang][$app])) {
135
			$this->instances[$lang][$app] = new L10N(
136
				$this, $app, $lang, $locale,
137
				$this->getL10nFilesForApp($app, $lang)
138
			);
139
		}
140
141
		return $this->instances[$lang][$app];
142
	}
143
144
	/**
145
	 * Find the best language
146
	 *
147
	 * @param string|null $app App id or null for core
148
	 * @return string language If nothing works it returns 'en'
149
	 */
150
	public function findLanguage($app = null) {
151
		$forceLang = $this->config->getSystemValue('force_language', false);
152
		if (is_string($forceLang)) {
153
			$this->requestLanguage = $forceLang;
154
		}
155
156
		if ($this->requestLanguage !== '' && $this->languageExists($app, $this->requestLanguage)) {
157
			return $this->requestLanguage;
158
		}
159
160
		/**
161
		 * At this point Nextcloud might not yet be installed and thus the lookup
162
		 * in the preferences table might fail. For this reason we need to check
163
		 * whether the instance has already been installed
164
		 *
165
		 * @link https://github.com/owncloud/core/issues/21955
166
		 */
167
		if ($this->config->getSystemValue('installed', false)) {
168
			$userId = !is_null($this->userSession->getUser()) ? $this->userSession->getUser()->getUID() :  null;
169
			if (!is_null($userId)) {
170
				$userLang = $this->config->getUserValue($userId, 'core', 'lang', null);
171
			} else {
172
				$userLang = null;
173
			}
174
		} else {
175
			$userId = null;
176
			$userLang = null;
177
		}
178
179
		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...
180
			$this->requestLanguage = $userLang;
181
			if ($this->languageExists($app, $userLang)) {
182
				return $userLang;
183
			}
184
		}
185
186
		try {
187
			// Try to get the language from the Request
188
			$lang = $this->getLanguageFromRequest($app);
189
			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...
190
				$this->config->setUserValue($userId, 'core', 'lang', $lang);
191
			}
192
			return $lang;
193
		} catch (LanguageNotFoundException $e) {
194
			// Finding language from request failed fall back to default language
195
			$defaultLanguage = $this->config->getSystemValue('default_language', false);
196
			if ($defaultLanguage !== false && $this->languageExists($app, $defaultLanguage)) {
197
				return $defaultLanguage;
198
			}
199
		}
200
201
		// We could not find any language so fall back to english
202
		return 'en';
203
	}
204
205
	/**
206
	 * find the best locale
207
	 *
208
	 * @param string $lang
209
	 * @return null|string
210
	 */
211
	public function findLocale($lang = null) {
212
		$forceLocale = $this->config->getSystemValue('force_locale', false);
213
		if (is_string($forceLocale) && $this->localeExists($forceLocale)) {
214
			return $forceLocale;
215
		}
216
217
		if ($this->config->getSystemValue('installed', false)) {
218
			$userId = null !== $this->userSession->getUser() ? $this->userSession->getUser()->getUID() :  null;
219
			$userLocale = null;
220
			if (null !== $userId) {
221
				$userLocale = $this->config->getUserValue($userId, 'core', 'locale', null);
222
			}
223
		} else {
224
			$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...
225
			$userLocale = null;
226
		}
227
228
		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...
229
			return $userLocale;
230
		}
231
232
		// Default : use system default locale
233
		$defaultLocale = $this->config->getSystemValue('default_locale', false);
234
		if ($defaultLocale !== false && $this->localeExists($defaultLocale)) {
235
			return $defaultLocale;
236
		}
237
238
		// If no user locale set, use lang as locale
239
		if (null !== $lang && $this->localeExists($lang)) {
240
			return $lang;
241
		}
242
243
		// At last, return USA
244
		return 'en_US';
245
	}
246
247
	/**
248
	 * Find all available languages for an app
249
	 *
250
	 * @param string|null $app App id or null for core
251
	 * @return array an array of available languages
252
	 */
253
	public function findAvailableLanguages($app = null) {
254
		$key = $app;
255
		if ($key === null) {
256
			$key = 'null';
257
		}
258
259
		// also works with null as key
260
		if (!empty($this->availableLanguages[$key])) {
261
			return $this->availableLanguages[$key];
262
		}
263
264
		$available = ['en']; //english is always available
265
		$dir = $this->findL10nDir($app);
266 View Code Duplication
		if (is_dir($dir)) {
267
			$files = scandir($dir);
268
			if ($files !== false) {
269
				foreach ($files as $file) {
270
					if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
271
						$available[] = substr($file, 0, -5);
272
					}
273
				}
274
			}
275
		}
276
277
		// merge with translations from theme
278
		$theme = $this->config->getSystemValue('theme');
279
		if (!empty($theme)) {
280
			$themeDir = $this->serverRoot . '/themes/' . $theme . substr($dir, strlen($this->serverRoot));
281
282 View Code Duplication
			if (is_dir($themeDir)) {
283
				$files = scandir($themeDir);
284
				if ($files !== false) {
285
					foreach ($files as $file) {
286
						if (substr($file, -5) === '.json' && substr($file, 0, 4) !== 'l10n') {
287
							$available[] = substr($file, 0, -5);
288
						}
289
					}
290
				}
291
			}
292
		}
293
294
		$this->availableLanguages[$key] = $available;
295
		return $available;
296
	}
297
298
	/**
299
	 * @return array|mixed
300
	 */
301
	public function findAvailableLocales() {
302
		if (!empty($this->availableLocales)) {
303
			return $this->availableLocales;
304
		}
305
306
		$localeData = file_get_contents(\OC::$SERVERROOT . '/resources/locales.json');
307
		$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...
308
309
		return $this->availableLocales;
310
	}
311
312
	/**
313
	 * @param string|null $app App id or null for core
314
	 * @param string $lang
315
	 * @return bool
316
	 */
317
	public function languageExists($app, $lang) {
318
		if ($lang === 'en') {//english is always available
319
			return true;
320
		}
321
322
		$languages = $this->findAvailableLanguages($app);
323
		return array_search($lang, $languages) !== false;
324
	}
325
326
	public function getLanguageIterator(IUser $user = null): ILanguageIterator {
327
		$user = $user ?? $this->userSession->getUser();
328
		if($user === null) {
329
			throw new \RuntimeException('Failed to get an IUser instance');
330
		}
331
		return new LanguageIterator($user, $this->config);
332
	}
333
334
	/**
335
	 * @param string $locale
336
	 * @return bool
337
	 */
338
	public function localeExists($locale) {
339
		if ($locale === 'en') { //english is always available
340
			return true;
341
		}
342
343
		$locales = $this->findAvailableLocales();
344
		$userLocale = array_filter($locales, function($value) use ($locale) {
345
			return $locale === $value['code'];
346
		});
347
348
		return !empty($userLocale);
349
	}
350
351
	/**
352
	 * @param string|null $app
353
	 * @return string
354
	 * @throws LanguageNotFoundException
355
	 */
356
	private function getLanguageFromRequest($app) {
357
		$header = $this->request->getHeader('ACCEPT_LANGUAGE');
358
		if ($header !== '') {
359
			$available = $this->findAvailableLanguages($app);
360
361
			// E.g. make sure that 'de' is before 'de_DE'.
362
			sort($available);
363
364
			$preferences = preg_split('/,\s*/', strtolower($header));
365
			foreach ($preferences as $preference) {
366
				list($preferred_language) = explode(';', $preference);
367
				$preferred_language = str_replace('-', '_', $preferred_language);
368
369
				foreach ($available as $available_language) {
370
					if ($preferred_language === strtolower($available_language)) {
371
						return $this->respectDefaultLanguage($app, $available_language);
372
					}
373
				}
374
375
				// Fallback from de_De to de
376
				foreach ($available as $available_language) {
377
					if (substr($preferred_language, 0, 2) === $available_language) {
378
						return $available_language;
379
					}
380
				}
381
			}
382
		}
383
384
		throw new LanguageNotFoundException();
385
	}
386
387
	/**
388
	 * if default language is set to de_DE (formal German) this should be
389
	 * preferred to 'de' (non-formal German) if possible
390
	 *
391
	 * @param string|null $app
392
	 * @param string $lang
393
	 * @return string
394
	 */
395
	protected function respectDefaultLanguage($app, $lang) {
396
		$result = $lang;
397
		$defaultLanguage = $this->config->getSystemValue('default_language', false);
398
399
		// use formal version of german ("Sie" instead of "Du") if the default
400
		// language is set to 'de_DE' if possible
401
		if (is_string($defaultLanguage) &&
402
			strtolower($lang) === 'de' &&
403
			strtolower($defaultLanguage) === 'de_de' &&
404
			$this->languageExists($app, 'de_DE')
405
		) {
406
			$result = 'de_DE';
407
		}
408
409
		return $result;
410
	}
411
412
	/**
413
	 * Checks if $sub is a subdirectory of $parent
414
	 *
415
	 * @param string $sub
416
	 * @param string $parent
417
	 * @return bool
418
	 */
419
	private function isSubDirectory($sub, $parent) {
420
		// Check whether $sub contains no ".."
421
		if (strpos($sub, '..') !== false) {
422
			return false;
423
		}
424
425
		// Check whether $sub is a subdirectory of $parent
426
		if (strpos($sub, $parent) === 0) {
427
			return true;
428
		}
429
430
		return false;
431
	}
432
433
	/**
434
	 * Get a list of language files that should be loaded
435
	 *
436
	 * @param string $app
437
	 * @param string $lang
438
	 * @return string[]
439
	 */
440
	// FIXME This method is only public, until OC_L10N does not need it anymore,
441
	// FIXME This is also the reason, why it is not in the public interface
442
	public function getL10nFilesForApp($app, $lang) {
443
		$languageFiles = [];
444
445
		$i18nDir = $this->findL10nDir($app);
446
		$transFile = strip_tags($i18nDir) . strip_tags($lang) . '.json';
447
448
		if (($this->isSubDirectory($transFile, $this->serverRoot . '/core/l10n/')
449
				|| $this->isSubDirectory($transFile, $this->serverRoot . '/lib/l10n/')
450
				|| $this->isSubDirectory($transFile, $this->serverRoot . '/settings/l10n/')
451
				|| $this->isSubDirectory($transFile, \OC_App::getAppPath($app) . '/l10n/')
452
			)
453
			&& file_exists($transFile)) {
454
			// load the translations file
455
			$languageFiles[] = $transFile;
456
		}
457
458
		// merge with translations from theme
459
		$theme = $this->config->getSystemValue('theme');
460
		if (!empty($theme)) {
461
			$transFile = $this->serverRoot . '/themes/' . $theme . substr($transFile, strlen($this->serverRoot));
462
			if (file_exists($transFile)) {
463
				$languageFiles[] = $transFile;
464
			}
465
		}
466
467
		return $languageFiles;
468
	}
469
470
	/**
471
	 * find the l10n directory
472
	 *
473
	 * @param string $app App id or empty string for core
474
	 * @return string directory
475
	 */
476
	protected function findL10nDir($app = null) {
477
		if (in_array($app, ['core', 'lib', 'settings'])) {
478
			if (file_exists($this->serverRoot . '/' . $app . '/l10n/')) {
479
				return $this->serverRoot . '/' . $app . '/l10n/';
480
			}
481
		} 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...
482
			// Check if the app is in the app folder
483
			return \OC_App::getAppPath($app) . '/l10n/';
484
		}
485
		return $this->serverRoot . '/core/l10n/';
486
	}
487
488
489
	/**
490
	 * Creates a function from the plural string
491
	 *
492
	 * Parts of the code is copied from Habari:
493
	 * https://github.com/habari/system/blob/master/classes/locale.php
494
	 * @param string $string
495
	 * @return string
496
	 */
497
	public function createPluralFunction($string) {
498
		if (isset($this->pluralFunctions[$string])) {
499
			return $this->pluralFunctions[$string];
500
		}
501
502
		if (preg_match( '/^\s*nplurals\s*=\s*(\d+)\s*;\s*plural=(.*)$/u', $string, $matches)) {
503
			// sanitize
504
			$nplurals = preg_replace( '/[^0-9]/', '', $matches[1] );
505
			$plural = preg_replace( '#[^n0-9:\(\)\?\|\&=!<>+*/\%-]#', '', $matches[2] );
506
507
			$body = str_replace(
508
				array( 'plural', 'n', '$n$plurals', ),
509
				array( '$plural', '$n', '$nplurals', ),
510
				'nplurals='. $nplurals . '; plural=' . $plural
511
			);
512
513
			// add parents
514
			// important since PHP's ternary evaluates from left to right
515
			$body .= ';';
516
			$res = '';
517
			$p = 0;
518
			$length = strlen($body);
519
			for($i = 0; $i < $length; $i++) {
520
				$ch = $body[$i];
521
				switch ( $ch ) {
522
					case '?':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
523
						$res .= ' ? (';
524
						$p++;
525
						break;
526
					case ':':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
527
						$res .= ') : (';
528
						break;
529
					case ';':
530
						$res .= str_repeat( ')', $p ) . ';';
531
						$p = 0;
532
						break;
533
					default:
534
						$res .= $ch;
535
				}
536
			}
537
538
			$body = $res . 'return ($plural>=$nplurals?$nplurals-1:$plural);';
539
			$function = create_function('$n', $body);
540
			$this->pluralFunctions[$string] = $function;
541
			return $function;
542
		} else {
543
			// default: one plural form for all cases but n==1 (english)
544
			$function = create_function(
545
				'$n',
546
				'$nplurals=2;$plural=($n==1?0:1);return ($plural>=$nplurals?$nplurals-1:$plural);'
547
			);
548
			$this->pluralFunctions[$string] = $function;
549
			return $function;
550
		}
551
	}
552
553
	/**
554
	 * returns the common language and other languages in an
555
	 * associative array
556
	 *
557
	 * @return array
558
	 */
559
	public function getLanguages() {
560
		$forceLanguage = $this->config->getSystemValue('force_language', false);
561
		if ($forceLanguage !== false) {
562
			return [];
563
		}
564
565
		$languageCodes = $this->findAvailableLanguages();
566
567
		$commonLanguages = [];
568
		$languages = [];
569
570
		foreach($languageCodes as $lang) {
571
			$l = $this->get('lib', $lang);
572
			// TRANSLATORS this is the language name for the language switcher in the personal settings and should be the localized version
573
			$potentialName = (string) $l->t('__language_name__');
574
			if ($l->getLanguageCode() === $lang && $potentialName[0] !== '_') {//first check if the language name is in the translation file
575
				$ln = array(
576
					'code' => $lang,
577
					'name' => $potentialName
578
				);
579
			} else if ($lang === 'en') {
580
				$ln = array(
581
					'code' => $lang,
582
					'name' => 'English (US)'
583
				);
584
			} else {//fallback to language code
585
				$ln = array(
586
					'code' => $lang,
587
					'name' => $lang
588
				);
589
			}
590
591
			// put appropriate languages into appropriate arrays, to print them sorted
592
			// common languages -> divider -> other languages
593
			if (in_array($lang, self::COMMON_LANGUAGE_CODES)) {
594
				$commonLanguages[array_search($lang, self::COMMON_LANGUAGE_CODES)] = $ln;
595
			} else {
596
				$languages[] = $ln;
597
			}
598
		}
599
600
		ksort($commonLanguages);
601
602
		// sort now by displayed language not the iso-code
603
		usort( $languages, function ($a, $b) {
604 View Code Duplication
			if ($a['code'] === $a['name'] && $b['code'] !== $b['name']) {
605
				// If a doesn't have a name, but b does, list b before a
606
				return 1;
607
			}
608 View Code Duplication
			if ($a['code'] !== $a['name'] && $b['code'] === $b['name']) {
609
				// If a does have a name, but b doesn't, list a before b
610
				return -1;
611
			}
612
			// Otherwise compare the names
613
			return strcmp($a['name'], $b['name']);
614
		});
615
616
		return [
617
			// reset indexes
618
			'commonlanguages' => array_values($commonLanguages),
619
			'languages' => $languages
620
		];
621
	}
622
}
623