Completed
Push — lang_cookie_across_services ( 506103 )
by
unknown
27:51
created

Translator::addTranslation()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 6.0852

Importance

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