Completed
Push — cookie-changes ( 1957bf )
by
unknown
19:08
created

Translator::getLanguage()   D

Complexity

Conditions 10
Paths 33

Size

Total Lines 35
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 63.0457

Importance

Changes 0
Metric Value
cc 10
eloc 20
nc 33
nop 0
dl 0
loc 35
rs 4.8196
c 0
b 0
f 0
ccs 4
cts 21
cp 0.1905
crap 63.0457

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
namespace Elgg\I18n;
3
4
/**
5
 * WARNING: API IN FLUX. DO NOT USE DIRECTLY.
6
 *
7
 * @access private
8
 *
9
 * @since 1.10.0
10
 */
11
class Translator {
12
	
13
	/**
14
	 * Global Elgg configuration
15
	 * 
16
	 * @var \stdClass
17
	 */
18
	private $CONFIG;
19
20
	/**
21
	 * Initializes new translator
22
	 */
23 3
	public function __construct() {
24 3
		global $CONFIG;
25 3
		$this->CONFIG = $CONFIG;
26 3
		$this->defaultPath = dirname(dirname(dirname(dirname(__DIR__)))) . "/languages/";
0 ignored issues
show
Bug introduced by
The property defaultPath does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
27 3
	}
28
	
29
	/**
30
	 * Given a message key, returns an appropriately translated full-text string
31
	 *
32
	 * @param string $message_key The short message code
33
	 * @param array  $args        An array of arguments to pass through vsprintf().
34
	 * @param string $language    Optionally, the standard language code
35
	 *                            (defaults to site/user default, then English)
36
	 *
37
	 * @return string Either the translated string, the English string,
38
	 * or the original language string.
39
	 */
40
	function translate($message_key, $args = array(), $language = "") {
41
		
42
	
43
		static $CURRENT_LANGUAGE;
44
	
45
		// old param order is deprecated
46
		if (!is_array($args)) {
47
			elgg_deprecated_notice(
48
				'As of Elgg 1.8, the 2nd arg to elgg_echo() is an array of string replacements and the 3rd arg is the language.',
49
				1.8
50
			);
51
	
52
			$language = $args;
53
			$args = array();
54
		}
55
	
56
		if (!isset($this->CONFIG->translations)) {
57
			// this means we probably had an exception before translations were initialized
58
			$this->registerTranslations($this->defaultPath);
59
		}
60
	
61
		if (!$CURRENT_LANGUAGE) {
62
			$CURRENT_LANGUAGE = $this->getLanguage();
63
		}
64
		if (!$language) {
65
			$language = $CURRENT_LANGUAGE;
66
		}
67
68
		if (!isset($this->CONFIG->translations[$language])) {
69
			// The language being requested is not the same as the language of the
70
			// logged in user, so we will have to load it separately. (Most likely
71
			// we're sending a notification and the recipient is using a different
72
			// language than the logged in user.)
73
			_elgg_load_translations_for_language($language);
74
		}
75
76
		if (isset($this->CONFIG->translations[$language][$message_key])) {
77
			$string = $this->CONFIG->translations[$language][$message_key];
78
		} else if (isset($this->CONFIG->translations["en"][$message_key])) {
79
			$string = $this->CONFIG->translations["en"][$message_key];
80
			_elgg_services()->logger->notice(sprintf('Missing %s translation for "%s" language key', $language, $message_key));
81
		} else {
82
			$string = $message_key;
83
			_elgg_services()->logger->notice(sprintf('Missing English translation for "%s" language key', $message_key));
84
		}
85
	
86
		// only pass through if we have arguments to allow backward compatibility
87
		// with manual sprintf() calls.
88
		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...
89
			$string = vsprintf($string, $args);
90
		}
91
	
92
		return $string;
93
	}
94
	
95
	/**
96
	 * Add a translation.
97
	 *
98
	 * Translations are arrays in the Zend Translation array format, eg:
99
	 *
100
	 *	$english = array('message1' => 'message1', 'message2' => 'message2');
101
	 *  $german = array('message1' => 'Nachricht1','message2' => 'Nachricht2');
102
	 *
103
	 * @param string $country_code   Standard country code (eg 'en', 'nl', 'es')
104
	 * @param array  $language_array Formatted array of strings
105
	 *
106
	 * @return bool Depending on success
107
	 */
108 1
	function addTranslation($country_code, $language_array) {
109
		
110 1
		if (!isset($this->CONFIG->translations)) {
111 1
			$this->CONFIG->translations = array();
112 1
		}
113
	
114 1
		$country_code = strtolower($country_code);
115 1
		$country_code = trim($country_code);
116 1
		if (is_array($language_array) && $country_code != "") {
117 1
			if (sizeof($language_array) > 0) { 
118 1
				if (!isset($this->CONFIG->translations[$country_code])) {
119 1
					$this->CONFIG->translations[$country_code] = $language_array;
120 1
				} else {
121
					$this->CONFIG->translations[$country_code] = $language_array + $this->CONFIG->translations[$country_code];
122
				}
123 1
			}
124 1
			return true;
125
		}
126
		return false;
127
	}
128
	
129
	/**
130
	 * Detect the current language being used by the current site or logged in user.
131
	 *
132
	 * @return string The language code for the site/user or "en" if not set
133
	 */
134
	function getCurrentLanguage() {
135
		$language = $this->getLanguage();
136
	
137
		if (!$language) {
138
			$language = 'en';
139
		}
140
	
141
		return $language;
142
	}
143
	
144
	/**
145
	 * Gets the current language in use by the system or user.
146
	 *
147
	 * @return string The language code (eg "en") or false if not set
148
	 */
149 1
	function getLanguage() {
0 ignored issues
show
Coding Style introduced by
getLanguage uses the super-global variable $_COOKIE which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
150 1
		$url_lang = _elgg_services()->input->get('hl');
151 1
		if ($url_lang) {
152 1
			return $url_lang;
153
		}
154
		
155
		$user = _elgg_services()->session->getLoggedInUser();
156
		$language = false;
157
	
158
		if (($user) && ($user->language)) {
159
			$language = $user->language;
160
		}
161
	
162
		if ((!$language) && (isset($this->CONFIG->language)) && ($this->CONFIG->language)) {
163
			$language = $this->CONFIG->language;
164
		}
165
166
		// GC change: use toggle language cookie if it is there.
167
		//$cookie_name = (strpos(elgg_get_site_entity()->name, 'collab') !== false) ? "lang" : "connex_lang";
168
		$cookie_name = "lang";
169
		if ( isset($_COOKIE[$cookie_name]) )
170
  			_elgg_services()->session->set( 'language', $_COOKIE[$cookie_name] );
171
 		else
172
  			_elgg_services()->session->set( 'language', 'en' );
173
  
174
175
  		if ( _elgg_services()->session->get('language') )
176
   			$language = _elgg_services()->session->get('language');
177
	
178
		if ($language) {
179
			return $language;
180
		}
181
	
182
		return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by Elgg\I18n\Translator::getLanguage of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
183
	}
184
	
185
	/**
186
	 * @access private
187
	 */
188
	function loadTranslations() {
189
		
190
	
191
		if ($this->CONFIG->system_cache_enabled) {
192
			$loaded = true;
193
			$languages = array_unique(array('en', $this->getCurrentLanguage()));
194
			foreach ($languages as $language) {
195
				$data = elgg_load_system_cache("$language.lang");
196
				if ($data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data 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...
197
					$this->addTranslation($language, unserialize($data));
198
				} else {
199
					$loaded = false;
200
				}
201
			}
202
	
203
			if ($loaded) {
204
				$this->CONFIG->i18n_loaded_from_cache = true;
205
				// this is here to force 
206
				$this->CONFIG->language_paths[$this->defaultPath] = true;
207
				return;
208
			}
209
		}
210
	
211
		// load core translations from languages directory
212
		$this->registerTranslations($this->defaultPath);
213
	}
214
	
215
	
216
	
217
	/**
218
	 * When given a full path, finds translation files and loads them
219
	 *
220
	 * @param string $path     Full path
221
	 * @param bool   $load_all If true all languages are loaded, if
222
	 *                         false only the current language + en are loaded
223
	 *
224
	 * @return bool success
225
	 */
226
	function registerTranslations($path, $load_all = false) {
227
		$path = sanitise_filepath($path);
228
	
229
		// Make a note of this path just incase we need to register this language later
230
		if (!isset($this->CONFIG->language_paths)) {
231
			$this->CONFIG->language_paths = array();
232
		}
233
		$this->CONFIG->language_paths[$path] = true;
234
	
235
		// Get the current language based on site defaults and user preference
236
		$current_language = $this->getCurrentLanguage();
237
		_elgg_services()->logger->info("Translations loaded from: $path");
238
239
		// only load these files unless $load_all is true.
240
		$load_language_files = array(
241
			'en.php',
242
			"$current_language.php"
243
		);
244
	
245
		$load_language_files = array_unique($load_language_files);
246
	
247
		$handle = opendir($path);
248
		if (!$handle) {
249
			_elgg_services()->logger->error("Could not open language path: $path");
250
			return false;
251
		}
252
	
253
		$return = true;
254
		while (false !== ($language = readdir($handle))) {
255
			// ignore bad files
256
			if (substr($language, 0, 1) == '.' || substr($language, -4) !== '.php') {
257
				continue;
258
			}
259
	
260
			if (in_array($language, $load_language_files) || $load_all) {
261
				$result = include_once($path . $language);
262
				if ($result === false) {
263
					$return = false;
264
					continue;
265
				} elseif (is_array($result)) {
266
					$this->addTranslation(basename($language, '.php'), $result);
267
				}
268
			}
269
		}
270
	
271
		return $return;
272
	}
273
	
274
	/**
275
	 * Reload all translations from all registered paths.
276
	 *
277
	 * This is only called by functions which need to know all possible translations.
278
	 *
279
	 * @todo Better on demand loading based on language_paths array
280
	 *
281
	 * @return void
282
	 */
283
	function reloadAllTranslations() {
284
		
285
	
286
		static $LANG_RELOAD_ALL_RUN;
287
		if ($LANG_RELOAD_ALL_RUN) {
288
			return;
289
		}
290
	
291
		if ($this->CONFIG->i18n_loaded_from_cache) {
292
			$cache = elgg_get_system_cache();
293
			$cache_dir = $cache->getVariable("cache_path");
294
			$filenames = elgg_get_file_list($cache_dir, array(), array(), array(".lang"));
295
			foreach ($filenames as $filename) {
296
				// Look for files matching for example 'en.lang', 'cmn.lang' or 'pt_br.lang'.
297
				// Note that this regex is just for the system cache. The original language
298
				// files are allowed to have uppercase letters (e.g. pt_BR.php).
299
				if (preg_match('/(([a-z]{2,3})(_[a-z]{2})?)\.lang$/', $filename, $matches)) {
300
					$language = $matches[1];
301
					$data = elgg_load_system_cache("$language.lang");
302
					if ($data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data 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...
303
						$this->addTranslation($language, unserialize($data));
304
					}
305
				}
306
			}
307
		} else {
308
			foreach ($this->CONFIG->language_paths as $path => $dummy) {
309
				$this->registerTranslations($path, true);
310
			}
311
		}
312
	
313
		$LANG_RELOAD_ALL_RUN = true;
314
	}
315
	
316
	/**
317
	 * Return an array of installed translations as an associative
318
	 * array "two letter code" => "native language name".
319
	 *
320
	 * @return array
321
	 */
322
	function getInstalledTranslations() {
323
		
324
	
325
		// Ensure that all possible translations are loaded
326
		$this->reloadAllTranslations();
327
	
328
		$installed = array();
329
		
330
		$admin_logged_in = _elgg_services()->session->isAdminLoggedIn();
331
	
332
		foreach ($this->CONFIG->translations as $k => $v) {
333
			$installed[$k] = $this->translate($k, array(), $k);
334
			if ($admin_logged_in && ($k != 'en')) {
335
				$completeness = $this->getLanguageCompleteness($k);
336
				if ($completeness < 100) {
337
					$installed[$k] .= " (" . $completeness . "% " . $this->translate('complete') . ")";
338
				}
339
			}
340
		}
341
	
342
		return $installed;
343
	}
344
	
345
	/**
346
	 * Return the level of completeness for a given language code (compared to english)
347
	 *
348
	 * @param string $language Language
349
	 *
350
	 * @return int
351
	 */
352
	function getLanguageCompleteness($language) {
353
		
354
	
355
		// Ensure that all possible translations are loaded
356
		$this->reloadAllTranslations();
357
	
358
		$language = sanitise_string($language);
359
	
360
		$en = count($this->CONFIG->translations['en']);
361
	
362
		$missing = $this->getMissingLanguageKeys($language);
363
		if ($missing) {
364
			$missing = count($missing);
365
		} else {
366
			$missing = 0;
367
		}
368
	
369
		//$lang = count($this->CONFIG->translations[$language]);
370
		$lang = $en - $missing;
371
	
372
		return round(($lang / $en) * 100, 2);
373
	}
374
	
375
	/**
376
	 * Return the translation keys missing from a given language,
377
	 * or those that are identical to the english version.
378
	 *
379
	 * @param string $language The language
380
	 *
381
	 * @return mixed
382
	 */
383
	function getMissingLanguageKeys($language) {
384
		
385
	
386
		// Ensure that all possible translations are loaded
387
		$this->reloadAllTranslations();
388
	
389
		$missing = array();
390
	
391
		foreach ($this->CONFIG->translations['en'] as $k => $v) {
392
			if ((!isset($this->CONFIG->translations[$language][$k]))
393
			|| ($this->CONFIG->translations[$language][$k] == $this->CONFIG->translations['en'][$k])) {
394
				$missing[] = $k;
395
			}
396
		}
397
	
398
		if (count($missing)) {
399
			return $missing;
400
		}
401
	
402
		return false;
403
	}
404
	
405
	/**
406
	 * Check if a give language key exists
407
	 *
408
	 * @param string $key      The translation key
409
	 * @param string $language The specific language to check
410
	 *
411
	 * @return bool
412
	 * @since 1.11
413
	 */
414 1
	function languageKeyExists($key, $language = 'en') {
415 1
		if (empty($key)) {
416
			return false;
417
		}
418
	
419 1
		if (($language !== 'en') && !array_key_exists($language, $this->CONFIG->translations)) {
420
			// Ensure that all possible translations are loaded
421
			$this->reloadAllTranslations();
422
		}
423
	
424 1
		if (!array_key_exists($language, $this->CONFIG->translations)) {
425
			return false;
426
		}
427
	
428 1
		return array_key_exists($key, $this->CONFIG->translations[$language]);
429
	}
430
	
431
	/**
432
	 * Returns an array of language codes.
433
	 *
434
	 * @return array
435
	 */
436
	public static function getAllLanguageCodes() {
437
		return [
438
			"aa", // "Afar"
439
			"ab", // "Abkhazian"
440
			"af", // "Afrikaans"
441
			"am", // "Amharic"
442
			"ar", // "Arabic"
443
			"as", // "Assamese"
444
			"ay", // "Aymara"
445
			"az", // "Azerbaijani"
446
			"ba", // "Bashkir"
447
			"be", // "Byelorussian"
448
			"bg", // "Bulgarian"
449
			"bh", // "Bihari"
450
			"bi", // "Bislama"
451
			"bn", // "Bengali; Bangla"
452
			"bo", // "Tibetan"
453
			"br", // "Breton"
454
			"ca", // "Catalan"
455
			"cmn", // "Mandarin Chinese" // ISO 639-3
456
			"co", // "Corsican"
457
			"cs", // "Czech"
458
			"cy", // "Welsh"
459
			"da", // "Danish"
460
			"de", // "German"
461
			"dz", // "Bhutani"
462
			"el", // "Greek"
463
			"en", // "English"
464
			"eo", // "Esperanto"
465
			"es", // "Spanish"
466
			"et", // "Estonian"
467
			"eu", // "Basque"
468
			"eu_es", // "Basque (Spain)"
469
			"fa", // "Persian"
470
			"fi", // "Finnish"
471
			"fj", // "Fiji"
472
			"fo", // "Faeroese"
473
			"fr", // "French"
474
			"fy", // "Frisian"
475
			"ga", // "Irish"
476
			"gd", // "Scots / Gaelic"
477
			"gl", // "Galician"
478
			"gn", // "Guarani"
479
			"gu", // "Gujarati"
480
			"he", // "Hebrew"
481
			"ha", // "Hausa"
482
			"hi", // "Hindi"
483
			"hr", // "Croatian"
484
			"hu", // "Hungarian"
485
			"hy", // "Armenian"
486
			"ia", // "Interlingua"
487
			"id", // "Indonesian"
488
			"ie", // "Interlingue"
489
			"ik", // "Inupiak"
490
			"is", // "Icelandic"
491
			"it", // "Italian"
492
			"iu", // "Inuktitut"
493
			"iw", // "Hebrew (obsolete)"
494
			"ja", // "Japanese"
495
			"ji", // "Yiddish (obsolete)"
496
			"jw", // "Javanese"
497
			"ka", // "Georgian"
498
			"kk", // "Kazakh"
499
			"kl", // "Greenlandic"
500
			"km", // "Cambodian"
501
			"kn", // "Kannada"
502
			"ko", // "Korean"
503
			"ks", // "Kashmiri"
504
			"ku", // "Kurdish"
505
			"ky", // "Kirghiz"
506
			"la", // "Latin"
507
			"ln", // "Lingala"
508
			"lo", // "Laothian"
509
			"lt", // "Lithuanian"
510
			"lv", // "Latvian/Lettish"
511
			"mg", // "Malagasy"
512
			"mi", // "Maori"
513
			"mk", // "Macedonian"
514
			"ml", // "Malayalam"
515
			"mn", // "Mongolian"
516
			"mo", // "Moldavian"
517
			"mr", // "Marathi"
518
			"ms", // "Malay"
519
			"mt", // "Maltese"
520
			"my", // "Burmese"
521
			"na", // "Nauru"
522
			"ne", // "Nepali"
523
			"nl", // "Dutch"
524
			"no", // "Norwegian"
525
			"oc", // "Occitan"
526
			"om", // "(Afan) Oromo"
527
			"or", // "Oriya"
528
			"pa", // "Punjabi"
529
			"pl", // "Polish"
530
			"ps", // "Pashto / Pushto"
531
			"pt", // "Portuguese"
532
			"pt_br", // "Portuguese (Brazil)"
533
			"qu", // "Quechua"
534
			"rm", // "Rhaeto-Romance"
535
			"rn", // "Kirundi"
536
			"ro", // "Romanian"
537
			"ro_ro", // "Romanian (Romania)"
538
			"ru", // "Russian"
539
			"rw", // "Kinyarwanda"
540
			"sa", // "Sanskrit"
541
			"sd", // "Sindhi"
542
			"sg", // "Sangro"
543
			"sh", // "Serbo-Croatian"
544
			"si", // "Singhalese"
545
			"sk", // "Slovak"
546
			"sl", // "Slovenian"
547
			"sm", // "Samoan"
548
			"sn", // "Shona"
549
			"so", // "Somali"
550
			"sq", // "Albanian"
551
			"sr", // "Serbian"
552
			"sr_latin", // "Serbian (Latin)"
553
			"ss", // "Siswati"
554
			"st", // "Sesotho"
555
			"su", // "Sundanese"
556
			"sv", // "Swedish"
557
			"sw", // "Swahili"
558
			"ta", // "Tamil"
559
			"te", // "Tegulu"
560
			"tg", // "Tajik"
561
			"th", // "Thai"
562
			"ti", // "Tigrinya"
563
			"tk", // "Turkmen"
564
			"tl", // "Tagalog"
565
			"tn", // "Setswana"
566
			"to", // "Tonga"
567
			"tr", // "Turkish"
568
			"ts", // "Tsonga"
569
			"tt", // "Tatar"
570
			"tw", // "Twi"
571
			"ug", // "Uigur"
572
			"uk", // "Ukrainian"
573
			"ur", // "Urdu"
574
			"uz", // "Uzbek"
575
			"vi", // "Vietnamese"
576
			"vo", // "Volapuk"
577
			"wo", // "Wolof"
578
			"xh", // "Xhosa"
579
			"yi", // "Yiddish"
580
			"yo", // "Yoruba"
581
			"za", // "Zuang"
582
			"zh", // "Chinese"
583
			"zh_hans", // "Chinese Simplified"
584
			"zu", // "Zulu"
585
		];
586
	}
587
588
	/**
589
	 * Normalize a language code (e.g. from Transifex)
590
	 *
591
	 * @param string $code Language code
592
	 *
593
	 * @return string
594
	 */
595
	public static function normalizeLanguageCode($code) {
596
		$code = strtolower($code);
597
		$code = preg_replace('~[^a-z0-9]~', '_', $code);
598
		return $code;
599
	}
600
}