Completed
Push — master ( c4c0b1...c8301d )
by Nazar
04:20
created

Language   C

Complexity

Total Complexity 71

Size/Duplication

Total Lines 506
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 6

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 71
c 2
b 0
f 0
lcom 2
cbo 6
dl 0
loc 506
rs 5.5906

20 Methods

Rating   Name   Duplication   Size   Complexity  
A construct() 0 4 1
B init() 0 25 5
A url_language() 0 13 4
A check_accept_header() 0 21 3
A check_locale_header() 0 19 4
A get_aliases() 0 13 2
B get() 0 17 5
A set() 0 8 3
A __get() 0 3 1
A __set() 0 3 1
C change() 0 35 7
A can_be_changed_to() 0 12 4
B get_translation() 0 40 6
A get_translation_from_json() 0 13 4
B fill_required_translation_keys() 0 17 5
C time() 0 22 8
A __call() 0 3 1
A format() 0 3 1
B to_locale() 0 55 5
A jsonSerialize() 0 3 1

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