Completed
Push — master ( 3771a9...a90ddc )
by Nazar
10:06
created

Language   C

Complexity

Total Complexity 78

Size/Duplication

Total Lines 543
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 9

Test Coverage

Coverage 89.67%

Importance

Changes 0
Metric Value
dl 0
loc 543
ccs 165
cts 184
cp 0.8967
rs 5.4563
c 0
b 0
f 0
wmc 78
lcom 2
cbo 9

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __set() 0 3 1
B url_language() 0 19 5
A check_accept_header() 0 18 3
B check_locale_header() 0 16 5
A set() 0 8 3
A __get() 0 3 1
B init() 0 28 2
B init_internal() 0 22 6
A prefix() 0 3 1
A get_aliases() 0 16 2
C get() 0 23 9
B can_be_changed_to() 0 15 5
A get_translation() 0 8 1
B get_translation_internal() 0 40 4
A get_translation_from_json() 0 5 1
A get_translation_from_json_internal() 0 13 4
A fill_required_translation_keys() 0 12 1
A __call() 0 3 1
A format() 0 3 1
B to_locale() 0 55 5
A jsonSerialize() 0 3 1
C change() 0 32 8
C time() 0 22 8

How to fix   Complexity   

Complex Class

Complex classes like Language 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 Language, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @package   CleverStyle Framework
4
 * @author    Nazar Mokrynskyi <[email protected]>
5
 * @copyright Copyright (c) 2011-2016, Nazar Mokrynskyi
6
 * @license   MIT License, see license.txt
7
 */
8
namespace cs;
9
use
10
	JsonSerializable,
11
	cs\Language\Prefix;
12
13
/**
14
 * Provides next events:
15
 *  System/Language/change/before
16
 *
17
 *  System/Language/change/after
18
 *
19
 *  System/Language/load
20
 *  [
21
 *   'clanguage'        => clanguage
22
 *   'clang'            => clang
23
 *   'cregion'          => cregion
24
 *  ]
25
 *
26
 * @method static $this instance($check = false)
27
 *
28
 * @property string $clanguage
29
 * @property string $clang
30
 * @property string $cregion
31
 * @property string $content_language
32
 * @property string $_datetime_long
33
 * @property string $_datetime
34
 * @property string $_date
35
 * @property string $_time
36
 */
37
class Language implements JsonSerializable {
38
	use
39
		Singleton;
40
	const INIT_STATE_METHOD = 'init';
41
	/**
42
	 * Callable for time processing
43
	 *
44
	 * @var callable
45
	 */
46
	public $time;
47
	/**
48
	 * @var string
49
	 */
50
	protected $current_language;
51
	/**
52
	 * Local cache of translations
53
	 *
54
	 * @var array
55
	 */
56
	protected $translation = [];
57
	/**
58
	 * Cache to optimize frequent calls
59
	 *
60
	 * @var array
61
	 */
62
	protected $localized_url = [];
63 68
	protected function init () {
64
		/**
65
		 * Initialization: set default language based on system configuration and request-specific parameters
66
		 */
67 68
		$Config = Config::instance(true);
68
		/**
69
		 * We need Config for initialization
70
		 */
71 68
		if (!$Config) {
72
			Event::instance()->once(
73
				'System/Config/init/after',
74
				function () {
75
					$this->init_internal();
76
				}
77
			);
78
		} else {
79 68
			$this->init_internal();
80
		}
81
		/**
82
		 * Change language when configuration changes
83
		 */
84 68
		Event::instance()->on(
85 68
			'System/Config/changed',
86
			function () {
87 2
				$this->init_internal();
88 68
			}
89
		);
90 68
	}
91 68
	protected function init_internal () {
92 68
		$Config   = Config::instance();
93 68
		$language = '';
94 68
		if ($Config->core['multilingual']) {
95 12
			$language = User::instance(true)->language;
96
			/**
97
			 * Highest priority - `-Locale` header
98
			 */
99
			/** @noinspection PhpParamsInspection */
100 12
			$language = $language ?: $this->check_locale_header($Config->core['active_languages']);
101
			/**
102
			 * Second priority - URL
103
			 */
104 12
			$language = $language ?: $this->url_language(Request::instance()->path);
105
			/**
106
			 * Third - `Accept-Language` header
107
			 */
108
			/** @noinspection PhpParamsInspection */
109 12
			$language = $language ?: $this->check_accept_header($Config->core['active_languages']);
110
		}
111 68
		$this->current_language = $language ?: $Config->core['language'];
112 68
	}
113
	/**
114
	 * Returns instance for simplified work with translations, when using common prefix
115
	 *
116
	 * @param string $prefix
117
	 *
118
	 * @return Prefix
119
	 */
120 28
	public static function prefix ($prefix) {
121 28
		return new Prefix($prefix);
122
	}
123
	/**
124
	 * Does URL have language prefix
125
	 *
126
	 * @param false|string $url Relative url, `Request::instance()->path` by default
127
	 *
128
	 * @return false|string If there is language prefix - language will be returned, `false` otherwise
129
	 */
130 42
	public function url_language ($url = false) {
131
		/**
132
		 * @var string $url
133
		 */
134 42
		$url = $url ?: Request::instance()->path;
135 42
		if (isset($this->localized_url[$url])) {
136
			return $this->localized_url[$url];
137
		}
138 42
		$aliases = $this->get_aliases();
139 42
		$clang   = explode('?', $url, 2)[0];
140 42
		$clang   = explode('/', trim($clang, '/'), 2)[0];
141 42
		if (isset($aliases[$clang])) {
142 6
			if (count($this->localized_url) > 100) {
143
				$this->localized_url = [];
144
			}
145 6
			return $this->localized_url[$url] = $aliases[$clang];
146
		}
147 40
		return false;
148
	}
149
	/**
150
	 * Checking Accept-Language header for languages that exists in configuration
151
	 *
152
	 * @param array $active_languages
153
	 *
154
	 * @return false|string
155
	 */
156 8
	protected function check_accept_header ($active_languages) {
157 8
		$aliases          = $this->get_aliases();
158 8
		$accept_languages = array_filter(
159
			explode(
160 8
				',',
161
				strtolower(
162 8
					str_replace('-', '_', Request::instance()->header('accept-language'))
163
				)
164
			)
165
		);
166 8
		foreach ($accept_languages as $language) {
167 6
			$language = explode(';', $language, 2)[0];
168 6
			if (@in_array($aliases[$language], $active_languages)) {
169 6
				return $aliases[$language];
170
			}
171
		}
172 2
		return false;
173
	}
174
	/**
175
	 * Check `*-Locale` header (for instance, `X-Facebook-Locale`) that exists in configuration
176
	 *
177
	 * @param string[] $active_languages
178
	 *
179
	 * @return false|string
180
	 */
181 12
	protected function check_locale_header ($active_languages) {
182 12
		$aliases = $this->get_aliases();
183
		/**
184
		 * For `X-Facebook-Locale` and other similar
185
		 */
186 12
		foreach (Request::instance()->headers ?: [] as $i => $v) {
187 10
			if (stripos($i, '-locale') !== false) {
188 2
				$language = strtolower($v);
189 2
				if (@in_array($aliases[$language], $active_languages)) {
190 2
					return $aliases[$language];
191
				}
192 10
				return false;
193
			}
194
		}
195 10
		return false;
196
	}
197
	/**
198
	 * Get languages aliases
199
	 *
200
	 * @return array|false
201
	 */
202 42
	protected function get_aliases () {
203 42
		return Cache::instance()->get(
204 42
			'languages/aliases',
205
			function () {
206 4
				$aliases = [];
207
				/**
208
				 * @var string[] $aliases_list
209
				 */
210 4
				$aliases_list = _strtolower(get_files_list(LANGUAGES.'/aliases'));
211 4
				foreach ($aliases_list as $alias) {
212 4
					$aliases[$alias] = trim(file_get_contents(LANGUAGES."/aliases/$alias"));
213
				}
214 4
				return $aliases;
215 42
			}
216
		);
217
	}
218
	/**
219
	 * Get translation
220
	 *
221
	 * @param bool|string  $item
222
	 * @param false|string $language If specified - translation for specified language will be returned, otherwise for current
223
	 * @param string       $prefix   Used by `\cs\Language\Prefix`, usually no need to use it directly
224
	 *
225
	 * @return string
226
	 */
227 60
	public function get ($item, $language = false, $prefix = '') {
228
		/**
229
		 * Small optimization, we can actually return value without translations
230
		 */
231 60
		if ($item == 'clanguage' && $this->current_language && $language === false && !$prefix) {
232 50
			return $this->current_language;
233
		}
234 60
		$language = $language ?: $this->current_language;
235 60
		if (isset($this->translation[$language])) {
236 60
			$translation = $this->translation[$language];
237 60
			if (isset($translation[$prefix.$item])) {
238 60
				return $translation[$prefix.$item];
239 6
			} elseif (isset($translation[$item])) {
240 4
				return $translation[$item];
241
			}
242 2
			return ucfirst(str_replace('_', ' ', $item));
243
		}
244 56
		$current_language = $this->current_language;
245 56
		$this->change($language);
246 56
		$return = $this->get($item, $this->current_language, $prefix);
247 56
		$this->change($current_language);
248 56
		return $return;
249
	}
250
	/**
251
	 * Set translation
252
	 *
253
	 * @param array|string $item Item string, or key-value array
254
	 * @param null|string  $value
255
	 *
256
	 * @return void
257
	 */
258
	public function set ($item, $value = null) {
259
		$translate = &$this->translation[$this->current_language];
260
		if (is_array($item)) {
261
			$translate = $item + ($translate ?: []);
262
		} else {
263
			$translate[$item] = $value;
264
		}
265
	}
266
	/**
267
	 * Get translation
268
	 *
269
	 * @param string $item
270
	 *
271
	 * @return string
272
	 */
273 60
	public function __get ($item) {
274 60
		return $this->get($item);
275
	}
276
	/**
277
	 * Set translation
278
	 *
279
	 * @param array|string $item
280
	 * @param null|string  $value
281
	 */
282
	public function __set ($item, $value = null) {
283
		$this->set($item, $value);
284
	}
285
	/**
286
	 * Change language
287
	 *
288
	 * @param string $language
289
	 *
290
	 * @return bool
291
	 */
292 60
	public function change ($language) {
293
		/**
294
		 * Already set to specified language
295
		 */
296 60
		if ($language == $this->current_language && isset($this->translation[$language])) {
297 56
			return true;
298
		}
299 60
		$Config = Config::instance(true);
300
		/**
301
		 * @var string $language
302
		 */
303 60
		$language = $language ?: $Config->core['language'];
304 60
		if (!$this->can_be_changed_to($Config, $language)) {
305 4
			return false;
306
		}
307 60
		$Event = Event::instance();
308 60
		$Event->fire('System/Language/change/before');
309 60
		if (!isset($this->translation[$language])) {
310 60
			$this->translation[$language] = $this->get_translation($language);
311
		}
312
		/**
313
		 * Change current language to `$language`
314
		 */
315 60
		$this->current_language = $language;
316 60
		_include(LANGUAGES."/$language.php", false, false);
317 60
		$Request = Request::instance();
318 60
		if ($Request->regular_path && $Config->core['multilingual']) {
319 8
			Response::instance()->header('content-language', $this->content_language);
320
		}
321 60
		$Event->fire('System/Language/change/after');
322 60
		return true;
323
	}
324
	/**
325
	 * Check whether it is allowed to change to specified language according to configuration
326
	 *
327
	 * @param Config $Config
328
	 * @param string $language
329
	 *
330
	 * @return bool
331
	 */
332 60
	protected function can_be_changed_to ($Config, $language) {
333 60
		if (!$language) {
334
			return false;
335
		}
336
		return
337
			// Config not loaded yet
338 60
			!$Config->core ||
339
			// Set to language that is configured on system level
340 60
			$language == $Config->core['language'] ||
341
			// Set to active language
342
			(
343 22
				$Config->core['multilingual'] &&
344 60
				in_array($language, $Config->core['active_languages'])
345
			);
346
	}
347 60
	protected function get_translation ($language) {
348 60
		return Cache::instance()->get(
349 60
			"languages/$language",
350 60
			function () use ($language) {
351 10
				return $this->get_translation_internal($language);
352 60
			}
353
		);
354
	}
355
	/**
356
	 * Load translation from all over the system, set `$this->translation[$language]` and return it
357
	 *
358
	 * @param $language
359
	 *
360
	 * @return string[]
361
	 */
362 10
	protected function get_translation_internal ($language) {
363
		/**
364
		 * Get current system translations
365
		 */
366 10
		$translation = $this->get_translation_from_json(LANGUAGES."/$language.json");
367 10
		$translation = $this->fill_required_translation_keys($translation, $language);
368
		/**
369
		 * Set modules' translations
370
		 */
371 10
		foreach (get_files_list(MODULES, false, 'd', true) as $module_dir) {
372 10
			if (file_exists("$module_dir/languages/$language.json")) {
373 10
				$translation = $this->get_translation_from_json("$module_dir/languages/$language.json") + $translation;
374
			}
375
		}
376 10
		Event::instance()->fire(
377 10
			'System/Language/load',
378
			[
379 10
				'clanguage' => $language,
380 10
				'clang'     => $translation['clang'],
381 10
				'cregion'   => $translation['cregion']
382
			]
383
		);
384
		// TODO: Remove in 6.x
385 10
		Event::instance()->fire(
386 10
			'System/general/languages/load',
387
			[
388 10
				'clanguage' => $language,
389 10
				'clang'     => $translation['clang'],
390 10
				'cregion'   => $translation['cregion']
391
			]
392
		);
393
		/**
394
		 * Append translations from core language to fill potentially missing keys
395
		 */
396 10
		$core_language = Core::instance()->language;
397 10
		if ($language != $core_language) {
398 6
			$translation += $this->get_translation($core_language);
399
		}
400 10
		return $translation;
401
	}
402
	/**
403
	 * @param string $filename
404
	 *
405
	 * @return string[]
406
	 */
407 10
	protected function get_translation_from_json ($filename) {
408 10
		return $this->get_translation_from_json_internal(
409
			file_get_json_nocomments($filename)
410
		);
411
	}
412
	/**
413
	 * @param string[]|string[][] $translation
414
	 *
415
	 * @return string[]
416
	 */
417 10
	protected function get_translation_from_json_internal ($translation) {
418
		// Nested structure processing
419 10
		foreach ($translation as $item => $value) {
420 10
			if (is_array_assoc($value)) {
421 10
				unset($translation[$item]);
422 10
				foreach ($value as $sub_item => $sub_value) {
423 10
					$translation[$item.$sub_item] = $sub_value;
424
				}
425 10
				return $this->get_translation_from_json_internal($translation);
426
			}
427
		}
428 10
		return $translation;
429
	}
430
	/**
431
	 * Some required keys might be missing in translation, this functions tries to guess and fill them automatically
432
	 *
433
	 * @param string[] $translation
434
	 * @param string   $language
435
	 *
436
	 * @return string[]
437
	 */
438 10
	protected function fill_required_translation_keys ($translation, $language) {
439
		$translation += [
440 10
			'clanguage' => $language,
441 10
			'clang'     => mb_strtolower(mb_substr($language, 0, 2)),
442 10
			'clocale'   => $translation['clang'].'_'.mb_strtoupper($translation['cregion'])
443
		];
444
		$translation += [
445 10
			'content_language' => $translation['clang'],
446 10
			'cregion'          => $translation['clang']
447
		];
448 10
		return $translation;
449
	}
450
	/**
451
	 * Time formatting according to the current language (adding correct endings)
452
	 *
453
	 * @param int    $in          time (number)
454
	 * @param string $type        Type of formatting<br>
455
	 *                            s - seconds<br>m - minutes<br>h - hours<br>d - days<br>M - months<br>y - years
456
	 *
457
	 * @return string
458
	 */
459 6
	public function time ($in, $type) {
460 6
		if (is_callable($this->time)) {
461
			$time = $this->time;
462
			return $time($in, $type);
463
		} else {
464
			switch ($type) {
465 6
				case 's':
466 4
					return "$in $this->system_time_seconds";
467 2
				case 'm':
468
					return "$in $this->system_time_minutes";
469 2
				case 'h':
470
					return "$in $this->system_time_hours";
471 2
				case 'd':
472 2
					return "$in $this->system_time_days";
473
				case 'M':
474
					return "$in $this->system_time_months";
475
				case 'y':
476
					return "$in $this->system_time_years";
477
			}
478
		}
479
		return $in;
480
	}
481
	/**
482
	 * Allows to use formatted strings in translations
483
	 *
484
	 * @see format()
485
	 *
486
	 * @param string $item
487
	 * @param array  $arguments
488
	 *
489
	 * @return string
490
	 */
491 2
	public function __call ($item, $arguments) {
492 2
		return $this->format($item, $arguments);
493
	}
494
	/**
495
	 * Allows to use formatted strings in translations
496
	 *
497
	 * @param string       $item
498
	 * @param string[]     $arguments
499
	 * @param false|string $language If specified - translation for specified language will be returned, otherwise for current
500
	 * @param string       $prefix   Used by `\cs\Language\Prefix`, usually no need to use it directly
501
	 *
502
	 * @return string
503
	 */
504 8
	public function format ($item, $arguments, $language = false, $prefix = '') {
505 8
		return vsprintf($this->get($item, $language, $prefix), $arguments);
506
	}
507
	/**
508
	 * Formatting date according to language locale (translating months names, days of week, etc.)
509
	 *
510
	 * @param string|string[] $data
511
	 * @param bool            $short_may When in date() or similar functions "M" format option is used, third month "May" have the same short textual
512
	 *                                   representation as full, so, this option allows to specify, which exactly form of representation do you want
513
	 *
514
	 * @return string|string[]
515
	 */
516 2
	public function to_locale ($data, $short_may = false) {
517 2
		if (is_array($data)) {
518
			foreach ($data as &$item) {
519
				$item = $this->to_locale($item, $short_may);
520
			}
521
			return $data;
522
		}
523 2
		if ($short_may) {
524
			$data = str_replace('May', 'May_short', $data);
525
		}
526
		$from = [
527 2
			'January',
528
			'February',
529
			'March',
530
			'April',
531
			'May_short',
532
			'June',
533
			'July',
534
			'August',
535
			'September',
536
			'October',
537
			'November',
538
			'December',
539
			'Jan',
540
			'Feb',
541
			'Mar',
542
			'Apr',
543
			'May',
544
			'Jun',
545
			'Jul',
546
			'Aug',
547
			'Sep',
548
			'Oct',
549
			'Nov',
550
			'Dec',
551
			'Sunday',
552
			'Monday',
553
			'Tuesday',
554
			'Wednesday',
555
			'Thursday',
556
			'Friday',
557
			'Saturday',
558
			'Sun',
559
			'Mon',
560
			'Tue',
561
			'Wed',
562
			'Thu',
563
			'Fri',
564
			'Sat'
565
		];
566 2
		foreach ($from as $f) {
567 2
			$data = str_replace($f, $this->get("l_$f"), $data);
568
		}
569 2
		return $data;
570
	}
571
	/**
572
	 * Implementation of JsonSerializable interface
573
	 *
574
	 * @return string[]
575
	 */
576 4
	public function jsonSerialize () {
577 4
		return $this->translation[$this->current_language];
578
	}
579
}
580