Completed
Push — master ( 543681...179964 )
by Nazar
03:56
created

Language::init()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 22
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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