Completed
Push — master ( c8301d...7dc261 )
by Nazar
13:51
created

Language   C

Complexity

Total Complexity 72

Size/Duplication

Total Lines 515
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 6

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 72
c 3
b 0
f 0
lcom 2
cbo 6
dl 0
loc 515
rs 5.5668

21 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 4 1
A get_translation_from_json_internal() 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
		return $this->get_translation_from_json_internal($translation);
366
	}
367
	/**
368
	 * @param string[]|string[][] $translation
369
	 *
370
	 * @return string[]
371
	 */
372
	protected function get_translation_from_json_internal ($translation) {
373
		// Nested structure processing
374
		foreach ($translation as $item => $value) {
375
			if (is_array_assoc($value)) {
376
				unset($translation[$item]);
377
				foreach ($value as $sub_item => $sub_value) {
1 ignored issue
show
Bug introduced by
The expression $value of type string|array<integer,string> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
378
					$translation[$item.$sub_item] = $sub_value;
379
				}
380
				return $this->get_translation_from_json_internal($translation);
381
			}
382
		}
383
		return $translation;
384
	}
385
	/**
386
	 * Some required keys might be missing in translation, this functions tries to guess and fill them automatically
387
	 *
388
	 * @param string[] $translation
389
	 * @param string   $language
390
	 *
391
	 * @return string[]
392
	 */
393
	protected function fill_required_translation_keys ($translation, $language) {
394
		$translation['clanguage'] = $language;
395
		if (!isset($translation['clang'])) {
396
			$translation['clang'] = mb_strtolower(mb_substr($language, 0, 2));
397
		}
398
		if (!isset($translation['content_language'])) {
399
			$translation['content_language'] = $translation['clang'];
400
		}
401
		if (!isset($translation['cregion'])) {
402
			$translation['cregion'] = $translation['clang'];
403
		}
404
		if (!isset($translation['clanguage_en'])) {
405
			$translation['clanguage_en'] = $language;
406
		}
407
		$translation['clocale'] = $translation['clang'].'_'.mb_strtoupper($translation['cregion']);
408
		return $translation;
409
	}
410
	/**
411
	 * Time formatting according to the current language (adding correct endings)
412
	 *
413
	 * @param int    $in          time (number)
414
	 * @param string $type        Type of formatting<br>
415
	 *                            s - seconds<br>m - minutes<br>h - hours<br>d - days<br>M - months<br>y - years
416
	 *
417
	 * @return string
418
	 */
419
	function time ($in, $type) {
420
		if (is_callable($this->time)) {
421
			$time = $this->time;
422
			return $time($in, $type);
423
		} else {
424
			switch ($type) {
425
				case 's':
426
					return "$in $this->seconds";
427
				case 'm':
428
					return "$in $this->minutes";
429
				case 'h':
430
					return "$in $this->hours";
431
				case 'd':
432
					return "$in $this->days";
433
				case 'M':
434
					return "$in $this->months";
435
				case 'y':
436
					return "$in $this->years";
437
			}
438
		}
439
		return $in;
440
	}
441
	/**
442
	 * Allows to use formatted strings in translations
443
	 *
444
	 * @see format()
445
	 *
446
	 * @param string $item
447
	 * @param array  $arguments
448
	 *
449
	 * @return string
450
	 */
451
	function __call ($item, $arguments) {
452
		return $this->format($item, $arguments);
453
	}
454
	/**
455
	 * Allows to use formatted strings in translations
456
	 *
457
	 * @param string       $item
458
	 * @param string[]     $arguments
459
	 * @param false|string $language If specified - translation for specified language will be returned, otherwise for current
460
	 * @param string       $prefix   Used by `\cs\Language\Prefix`, usually no need to use it directly
461
	 *
462
	 * @return string
463
	 */
464
	function format ($item, $arguments, $language = false, $prefix = '') {
465
		return vsprintf($this->get($item, $language, $prefix), $arguments);
466
	}
467
	/**
468
	 * Formatting date according to language locale (translating months names, days of week, etc.)
469
	 *
470
	 * @param string|string[] $data
471
	 * @param bool            $short_may When in date() or similar functions "M" format option is used, third month "May" have the same short textual
472
	 *                                   representation as full, so, this option allows to specify, which exactly form of representation do you want
473
	 *
474
	 * @return string|string[]
475
	 */
476
	function to_locale ($data, $short_may = false) {
477
		if (is_array($data)) {
478
			foreach ($data as &$item) {
479
				$item = $this->to_locale($item, $short_may);
480
			}
481
			return $data;
482
		}
483
		if ($short_may) {
484
			$data = str_replace('May', 'May_short', $data);
485
		}
486
		$from = [
487
			'January',
488
			'February',
489
			'March',
490
			'April',
491
			'May_short',
492
			'June',
493
			'July',
494
			'August',
495
			'September',
496
			'October',
497
			'November',
498
			'December',
499
			'Jan',
500
			'Feb',
501
			'Mar',
502
			'Apr',
503
			'May',
504
			'Jun',
505
			'Jul',
506
			'Aug',
507
			'Sep',
508
			'Oct',
509
			'Nov',
510
			'Dec',
511
			'Sunday',
512
			'Monday',
513
			'Tuesday',
514
			'Wednesday',
515
			'Thursday',
516
			'Friday',
517
			'Saturday',
518
			'Sun',
519
			'Mon',
520
			'Tue',
521
			'Wed',
522
			'Thu',
523
			'Fri',
524
			'Sat'
525
		];
526
		foreach ($from as $f) {
527
			$data = str_replace($f, $this->get("l_$f"), $data);
528
		}
529
		return $data;
530
	}
531
	/**
532
	 * Implementation of JsonSerializable interface
533
	 *
534
	 * @return string[]
535
	 */
536
	function jsonSerialize () {
537
		return $this->translation[$this->clanguage];
538
	}
539
}
540