Completed
Push — master ( d70e6c...215c97 )
by Nazar
04:09
created

Language::get_translation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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