Completed
Push — master ( 195cac...2c4aac )
by Nazar
05:02
created

Language::get_aliases()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

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