Completed
Push — master ( a4ebee...ff9168 )
by Nazar
07:28
created

Language::set()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 8
rs 9.4285
cc 3
eloc 6
nc 3
nop 2
1
<?php
2
/**
3
 * @package   CleverStyle CMS
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
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
class Language implements JsonSerializable {
24
	use Singleton;
25
	/**
26
	 * Current language
27
	 *
28
	 * @var string
29
	 */
30
	public $clanguage;
31
	/**
32
	 * callable for time processing
33
	 *
34
	 * @var callable
35
	 */
36
	public $time;
37
	/**
38
	 * For single initialization
39
	 *
40
	 * @var bool
41
	 */
42
	protected $init = false;
43
	/**
44
	 * Local cache of translations
45
	 *
46
	 * @var array
47
	 */
48
	protected $translation = [];
49
	/**
50
	 * Cache to optimize frequent calls
51
	 *
52
	 * @var array
53
	 */
54
	protected $localized_url = [];
55
	/**
56
	 * Set basic language
57
	 */
58
	protected function construct () {
59
		$Config = Config::instance(true);
60
		$Core   = Core::instance();
61
		$this->change($Core->language);
62
		/**
63
		 * We need Config for initialization
64
		 */
65
		if (!$Config) {
66
			Event::instance()->once(
0 ignored issues
show
Bug introduced by
It seems like once() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
67
				'System/Config/init/after',
68
				function () {
69
					$this->init();
70
				}
71
			);
72
		} else {
73
			$this->init();
74
		}
75
		Event::instance()->on(
0 ignored issues
show
Bug introduced by
It seems like on() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
76
			'System/Config/changed',
77
			function () {
78
				$Config = Config::instance();
79
				if ($Config->core['multilingual'] && User::instance(true)) {
80
					$this->change(User::instance()->language);
81
				} else {
82
					$this->change($Config->core['language']);
83
				}
84
			}
85
		);
86
	}
87
	/**
88
	 * Initialization: set default language based on system configuration and request-specific parameters
89
	 */
90
	protected function init () {
91
		$Config = Config::instance();
92
		/**
93
		 * @var _SERVER $_SERVER
94
		 */
95
		if ($Config->core['multilingual']) {
96
			/**
97
			 * Highest priority - `-Locale` header
98
			 */
99
			$language = $this->check_locale_header($Config->core['active_languages']);
100
			/**
101
			 * Second priority - URL
102
			 */
103
			$language = $language ?: $this->url_language($_SERVER->request_uri);
104
			/**
105
			 * Third - `Accept-Language` header
106
			 */
107
			$language = $language ?: $this->check_accept_header($Config->core['active_languages']);
108
		} else {
109
			$language = $Config->core['language'];
110
		}
111
		$this->change($language ?: '');
112
	}
113
	/**
114
	 * Does URL have language prefix
115
	 *
116
	 * @param false|string $url Relative url, `$_SERVER->request_uri` by default
117
	 *
118
	 * @return false|string If there is language prefix - language will be returned, `false` otherwise
119
	 */
120
	function url_language ($url = false) {
121
		$url = $url ?: $_SERVER->request_uri;
122
		if (isset($this->localized_url[$url])) {
123
			return $this->localized_url[$url];
124
		}
125
		$aliases = $this->get_aliases();
126
		$clang   = explode('?', $url, 2)[0];
127
		$clang   = explode('/', trim($clang, '/'), 2)[0];
128
		if (isset($aliases[$clang])) {
129
			return $this->localized_url[$url] = $aliases[$clang];
130
		}
131
		return false;
132
	}
133
	/**
134
	 * Checking Accept-Language header for languages that exists in configuration
135
	 *
136
	 * @param array $active_languages
137
	 *
138
	 * @return false|string
139
	 */
140
	protected function check_accept_header ($active_languages) {
141
		/**
142
		 * @var _SERVER $_SERVER
143
		 */
144
		$aliases          = $this->get_aliases();
145
		$accept_languages = array_filter(
146
			explode(
147
				',',
148
				strtolower(
149
					strtr($_SERVER->language, '-', '_')
150
				)
151
			)
152
		);
153
		foreach ($accept_languages as $language) {
154
			$language = explode(';', $language, 2)[0];
155
			if (@in_array($aliases[$language], $active_languages)) {
156
				return $aliases[$language];
157
			}
158
		}
159
		return false;
160
	}
161
	/**
162
	 * Check `*-Locale` header (for instance, `X-Facebook-Locale`) that exists in configuration
163
	 *
164
	 * @param string[] $active_languages
165
	 *
166
	 * @return false|string
167
	 */
168
	protected function check_locale_header ($active_languages) {
169
		/**
170
		 * @var _SERVER $_SERVER
171
		 */
172
		$aliases = $this->get_aliases();
173
		/**
174
		 * For `X-Facebook-Locale` and other similar
175
		 */
176
		foreach ($_SERVER as $i => $v) {
177
			if (stripos($i, '_LOCALE') !== false) {
178
				$language = strtolower($v);
179
				if (@in_array($aliases[$language], $active_languages)) {
180
					return $aliases[$language];
181
				}
182
				return false;
183
			}
184
		}
185
		return false;
186
	}
187
	/**
188
	 * Get languages aliases
189
	 *
190
	 * @return array|false
191
	 */
192
	protected function get_aliases () {
193
		return Cache::instance()->get(
0 ignored issues
show
Bug introduced by
It seems like get() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
194
			'languages/aliases',
195
			function () {
196
				$aliases      = [];
197
				$aliases_list = _strtolower(get_files_list(LANGUAGES.'/aliases'));
198
				foreach ($aliases_list as $alias) {
199
					$aliases[$alias] = file_get_contents(LANGUAGES."/aliases/$alias");
200
				}
201
				return $aliases;
202
			}
203
		);
204
	}
205
	/**
206
	 * Get translation
207
	 *
208
	 * @param bool|string  $item
209
	 * @param false|string $language If specified - translation for specified language will be returned, otherwise for current
210
	 * @param string       $prefix   Used by `\cs\Language\Prefix`, usually no need to use it directly
211
	 *
212
	 * @return string
213
	 */
214
	function get ($item, $language = false, $prefix = '') {
215
		$language = $language ?: $this->clanguage;
216
		if (isset($this->translation[$language])) {
217
			$translation = $this->translation[$language];
218
			if (isset($translation[$prefix.$item])) {
219
				return $translation[$prefix.$item];
220
			} elseif (isset($translation[$item])) {
221
				return $translation[$item];
222
			}
223
			return ucfirst(str_replace('_', ' ', $item));
224
		}
225
		$current_language = $this->clanguage;
226
		$this->change($language);
227
		$return = $this->get($item, $language, $prefix);
228
		$this->change($current_language);
229
		return $return;
230
	}
231
	/**
232
	 * Set translation
233
	 *
234
	 * @param array|string $item Item string, or key-value array
235
	 * @param null|string  $value
236
	 *
237
	 * @return void
238
	 */
239
	function set ($item, $value = null) {
240
		$translate = &$this->translation[$this->clanguage];
241
		if (is_array($item)) {
242
			$translate = $item + ($translate ?: []);
243
		} else {
244
			$translate[$item] = $value;
245
		}
246
	}
247
	/**
248
	 * Get translation
249
	 *
250
	 * @param string $item
251
	 *
252
	 * @return string
253
	 */
254
	function __get ($item) {
255
		return $this->get($item);
256
	}
257
	/**
258
	 * Set translation
259
	 *
260
	 * @param array|string $item
261
	 * @param null|string  $value
262
	 *
263
	 * @return string
264
	 */
265
	function __set ($item, $value = null) {
266
		$this->set($item, $value);
267
	}
268
	/**
269
	 * Change language
270
	 *
271
	 * @param string $language
272
	 *
273
	 * @return bool
274
	 */
275
	function change ($language) {
276
		/**
277
		 * Already set to specified language
278
		 */
279
		if ($language == $this->clanguage) {
280
			return true;
281
		}
282
		$Config   = Config::instance(true);
283
		$language = $language ?: $Config->core['language'];
284
		if (
285
			!$language ||
286
			!$this->can_be_changed_to($Config, $language)
0 ignored issues
show
Documentation introduced by
$Config is of type object|object<cs\Singleton\Base>, but the function expects a object<cs\Config>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
287
		) {
288
			return false;
289
		}
290
		if (!isset($this->translation[$language])) {
291
			$Cache       = Cache::instance();
292
			$translation = $Cache->{"languages/$language"};
293
			if ($translation) {
294
				$this->translation[$language] = $translation;
295
			} else {
296
				/**
297
				 * `$this->get_translation()` will implicitly change `$this->translation`, so we do not need to assign new translation there manually
298
				 */
299
				$Cache->{"languages/$language"} = $this->get_translation($language);
300
			}
301
		}
302
		/**
303
		 * Change current language to `$language`
304
		 */
305
		$this->clanguage = $language;
306
		_include(LANGUAGES."/$language.php", false, false);
307
		_header("Content-Language: $this->content_language");
308
		return true;
309
	}
310
	/**
311
	 * Check whether it is allowed to change to specified language according to configuration
312
	 *
313
	 * @param Config $Config
314
	 * @param string $language
315
	 *
316
	 * @return bool
317
	 */
318
	protected function can_be_changed_to ($Config, $language) {
319
		return
320
			// Config not loaded yet
321
			!$Config->core ||
322
			// Set to language that is configured on system level
323
			$language == $Config->core['language'] ||
324
			// Set to active language
325
			(
326
				$Config->core['multilingual'] &&
327
				in_array($language, $Config->core['active_languages'])
328
			);
329
	}
330
	/**
331
	 * Load translation from all over the system, set `$this->translation[$language]` and return it
332
	 *
333
	 * @param $language
334
	 *
335
	 * @return string[]
336
	 */
337
	protected function get_translation ($language) {
338
		/**
339
		 * Get current system translations
340
		 */
341
		$translation = &$this->translation[$language];
342
		$translation = $this->get_translation_from_json(LANGUAGES."/$language.json");
343
		$translation = $this->fill_required_translation_keys($translation, $language);
344
		/**
345
		 * Set modules' translations
346
		 */
347
		foreach (get_files_list(MODULES, false, 'd', true) as $module_dir) {
348
			if (file_exists("$module_dir/languages/$language.json")) {
349
				$translation = $this->get_translation_from_json("$module_dir/languages/$language.json") + $translation;
350
			}
351
		}
352
		/**
353
		 * Set plugins' translations
354
		 */
355
		foreach (get_files_list(PLUGINS, false, 'd', true) as $plugin_dir) {
356
			if (file_exists("$plugin_dir/languages/$language.json")) {
357
				$translation = $this->get_translation_from_json("$plugin_dir/languages/$language.json") + $translation;
358
			}
359
		}
360
		Event::instance()->fire(
0 ignored issues
show
Bug introduced by
It seems like fire() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

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