Completed
Push — master ( 95db68...46a2db )
by Filip
02:55
created

Translator::trans()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 21
Code Lines 12

Duplication

Lines 6
Ratio 28.57 %

Importance

Changes 0
Metric Value
dl 6
loc 21
rs 9.0534
c 0
b 0
f 0
cc 4
eloc 12
nc 5
nop 4
1
<?php
2
3
/**
4
 * This file is part of the Kdyby (http://www.kdyby.org)
5
 *
6
 * Copyright (c) 2008 Filip Procházka ([email protected])
7
 *
8
 * For the full copyright and license information, please view the file license.txt that was distributed with this source code.
9
 */
10
11
namespace Kdyby\Translation;
12
13
use Kdyby;
14
use Kdyby\Translation\Diagnostics\Panel;
15
use Nette;
16
use Nette\Utils\ObjectMixin;
17
use Nette\Utils\Strings;
18
use Psr\Log\LoggerInterface;
19
use Symfony\Component\Translation\Loader\LoaderInterface;
20
use Symfony\Component\Translation\MessageSelector;
21
use Symfony\Component\Translation\Translator as BaseTranslator;
22
23
24
25
/**
26
 * Translator.
27
 *
28
 * @author Fabien Potencier <[email protected]>
29
 * @author Filip Procházka <[email protected]>
30
 */
31
class Translator extends BaseTranslator implements ITranslator
32
{
33
34
	/**
35
	 * @var IUserLocaleResolver
36
	 */
37
	private $localeResolver;
38
39
	/**
40
	 * @var CatalogueCompiler
41
	 */
42
	private $catalogueCompiler;
43
44
	/**
45
	 * @var FallbackResolver
46
	 */
47
	private $fallbackResolver;
48
49
	/**
50
	 * @var IResourceLoader
51
	 */
52
	private $translationsLoader;
53
54
	/**
55
	 * @var LoggerInterface
56
	 */
57
	private $psrLogger;
58
59
	/**
60
	 * @var Panel
61
	 */
62
	private $panel;
63
64
	/**
65
	 * @var array
66
	 */
67
	private $availableResourceLocales = [];
68
69
	/**
70
	 * @var string
71
	 */
72
	private $defaultLocale;
73
74
	/**
75
	 * @var string
76
	 */
77
	private $localeWhitelist;
78
79
	/**
80
	 * @var MessageSelector
81
	 */
82
	private $selector;
83
84
	/**
85
	 * @param IUserLocaleResolver $localeResolver
86
	 * @param MessageSelector $selector The message selector for pluralization
87
	 * @param CatalogueCompiler $catalogueCompiler
88
	 * @param FallbackResolver $fallbackResolver
89
	 * @param IResourceLoader $loader
90
	 */
91
	public function __construct(IUserLocaleResolver $localeResolver, MessageSelector $selector,
92
		CatalogueCompiler $catalogueCompiler, FallbackResolver $fallbackResolver, IResourceLoader $loader)
93
	{
94
		$this->localeResolver = $localeResolver;
95
		$this->selector = $selector;
96
		$this->catalogueCompiler = $catalogueCompiler;
97
		$this->fallbackResolver = $fallbackResolver;
98
		$this->translationsLoader = $loader;
99
100
		parent::__construct(NULL, $selector);
101
	}
102
103
104
105
	/**
106
	 * @internal
107
	 * @param Panel $panel
108
	 */
109
	public function injectPanel(Panel $panel)
110
	{
111
		$this->panel = $panel;
112
	}
113
114
115
116
	/**
117
	 * @param LoggerInterface|NULL $logger
118
	 */
119
	public function injectPsrLogger(LoggerInterface $logger = NULL)
120
	{
121
		$this->psrLogger = $logger;
122
	}
123
124
125
126
	/**
127
	 * Translates the given string.
128
	 *
129
	 * @param string  $message    The message id
130
	 * @param integer $count      The number to use to find the indice of the message
131
	 * @param array   $parameters An array of parameters for the message
132
	 * @param string  $domain     The domain for the message
133
	 * @param string  $locale     The locale
134
	 *
135
	 * @return string
136
	 */
137
	public function translate($message, $count = NULL, $parameters = [], $domain = NULL, $locale = NULL)
138
	{
139
		if ($message instanceof Phrase) {
140
			return $message->translate($this);
141
		}
142
143 View Code Duplication
		if (is_array($count)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
144
			$locale = $domain ?: NULL;
145
			$domain = $parameters ?: NULL;
146
			$parameters = $count;
147
			$count = NULL;
148
		}
149
150
		if (empty($message)) {
151
			return $message;
152
153
		} elseif ($message instanceof Nette\Utils\Html) {
0 ignored issues
show
Bug introduced by
The class Nette\Utils\Html does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
154
			if ($this->panel) {
155
				$this->panel->markUntranslated($message, $domain);
156
			}
157
			return $message; // todo: what now?
158
		}
159
160
		if (Strings::startsWith($message, '//')) {
161
			$message = Strings::substring($message, 2);
162
		}
163
164
		$tmp = [];
165
		foreach ($parameters as $key => $val) {
166
			$tmp['%' . trim($key, '%') . '%'] = $val;
167
		}
168
		$parameters = $tmp;
169
170
		if ($count !== NULL && is_scalar($count)) {
171
			return $this->transChoice($message, $count, $parameters + ['%count%' => $count], $domain, $locale);
172
		}
173
174
		return $this->trans($message, $parameters, $domain, $locale);
175
	}
176
177
178
179
	/**
180
	 * {@inheritdoc}
181
	 */
182
	public function trans($message, array $parameters = [], $domain = NULL, $locale = NULL)
183
	{
184
		if ($message instanceof Phrase) {
185
			return $message->translate($this);
186
		}
187
188 View Code Duplication
		if ($domain === NULL) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
189
			list($domain, $id) = $this->extractMessageDomain($message);
190
191
		} else {
192
			$id = $message;
193
		}
194
195
		$result = parent::trans($id, $parameters, $domain, $locale);
196
		if ($result === "\x01") {
197
			$this->logMissingTranslation($message, $domain, $locale);
198
			$result = strtr($message, $parameters);
199
		}
200
201
		return $result;
202
	}
203
204
205
206
	/**
207
	 * {@inheritdoc}
208
	 */
209
	public function transChoice($message, $number, array $parameters = [], $domain = NULL, $locale = NULL)
210
	{
211
		if ($message instanceof Phrase) {
212
			return $message->translate($this);
213
		}
214
215 View Code Duplication
		if ($domain === NULL) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
216
			list($domain, $id) = $this->extractMessageDomain($message);
217
218
		} else {
219
			$id = $message;
220
		}
221
222
		try {
223
			$result = parent::transChoice($id, $number, $parameters, $domain, $locale);
224
225
		} catch (\Exception $e) {
226
			$result = $id;
227
			if ($this->panel !== NULL) {
228
				$this->panel->choiceError($e, $domain);
229
			}
230
		}
231
232
		if ($result === "\x01") {
233
			$this->logMissingTranslation($message, $domain, $locale);
234
			if ($locale === NULL) {
235
				$locale = $this->getLocale();
236
			}
237
			$result = strtr($this->selector->choose($message, (int) $number, $locale), $parameters);
238
		}
239
240
		return $result;
241
	}
242
243
244
245
	/**
246
	 * @param string $format
247
	 * @param LoaderInterface $loader
248
	 */
249
	public function addLoader($format, LoaderInterface $loader)
250
	{
251
		parent::addLoader($format, $loader);
252
		$this->translationsLoader->addLoader($format, $loader);
253
	}
254
255
256
257
	/**
258
	 * @return \Symfony\Component\Translation\Loader\LoaderInterface[]
259
	 */
260
	protected function getLoaders()
261
	{
262
		return $this->translationsLoader->getLoaders();
263
	}
264
265
266
267
	/**
268
	 * @param array $whitelist
269
	 * @return Translator
270
	 */
271
	public function setLocaleWhitelist(array $whitelist = NULL)
272
	{
273
		$this->localeWhitelist = self::buildWhitelistRegexp($whitelist);
274
	}
275
276
277
278
	/**
279
	 * {@inheritdoc}
280
	 */
281
	public function addResource($format, $resource, $locale, $domain = NULL)
282
	{
283
		if ($this->localeWhitelist && !preg_match($this->localeWhitelist, $locale)) {
284
			if ($this->panel) {
285
				$this->panel->addIgnoredResource($format, $resource, $locale, $domain);
286
			}
287
			return;
288
		}
289
290
		parent::addResource($format, $resource, $locale, $domain);
291
		$this->catalogueCompiler->addResource($format, $resource, $locale, $domain);
292
		$this->availableResourceLocales[$locale] = TRUE;
293
294
		if ($this->panel) {
295
			$this->panel->addResource($format, $resource, $locale, $domain);
296
		}
297
	}
298
299
300
301
	/**
302
	 * {@inheritdoc}
303
	 */
304
	public function setFallbackLocales(array $locales)
305
	{
306
		parent::setFallbackLocales($locales);
307
		$this->fallbackResolver->setFallbackLocales($locales);
308
	}
309
310
311
312
	/**
313
	 * Returns array of locales from given resources
314
	 *
315
	 * @return array
316
	 */
317
	public function getAvailableLocales()
318
	{
319
		$locales = array_keys($this->availableResourceLocales);
320
		sort($locales);
321
		return $locales;
322
	}
323
324
325
326
	/**
327
	 * {@inheritdoc}
328
	 */
329
	public function getLocale()
330
	{
331
		if (parent::getLocale() === NULL) {
332
			$this->setLocale($this->localeResolver->resolve($this));
333
		}
334
335
		return parent::getLocale();
336
	}
337
338
339
340
	/**
341
	 * @return string
342
	 */
343
	public function getDefaultLocale()
344
	{
345
		return $this->defaultLocale;
346
	}
347
348
349
350
	/**
351
	 * @param string $locale
352
	 * @return Translator
353
	 */
354
	public function setDefaultLocale($locale)
355
	{
356
		$this->assertValidLocale($locale);
357
		$this->defaultLocale = $locale;
358
		return $this;
359
	}
360
361
362
363
	/**
364
	 * @param string $messagePrefix
365
	 * @return ITranslator
366
	 */
367
	public function domain($messagePrefix)
368
	{
369
		return new PrefixedTranslator($messagePrefix, $this);
370
	}
371
372
373
374
	/**
375
	 * @return TemplateHelpers
376
	 */
377
	public function createTemplateHelpers()
378
	{
379
		return new TemplateHelpers($this);
380
	}
381
382
383
384
	/**
385
	 * {@inheritdoc}
386
	 */
387
	protected function loadCatalogue($locale)
388
	{
389
		if (empty($locale)) {
390
			throw new InvalidArgumentException("Invalid locale.");
391
		}
392
393
		if (isset($this->catalogues[$locale])) {
394
			return;
395
		}
396
397
		$this->catalogues = $this->catalogueCompiler->compile($this, $this->catalogues, $locale);
398
	}
399
400
401
402
	/**
403
	 * {@inheritdoc}
404
	 */
405
	protected function computeFallbackLocales($locale)
406
	{
407
		return $this->fallbackResolver->compute($this, $locale);
408
	}
409
410
411
412
	/**
413
	 * Asserts that the locale is valid, throws an Exception if not.
414
	 *
415
	 * @param string $locale Locale to tests
416
	 * @throws \InvalidArgumentException If the locale contains invalid characters
417
	 */
418
	protected function assertValidLocale($locale)
419
	{
420
		if (preg_match('~^[a-z0-9@_\\.\\-]*\z~i', $locale) !== 1) {
421
			throw new \InvalidArgumentException(sprintf('Invalid "%s" locale.', $locale));
422
		}
423
	}
424
425
426
427
	/**
428
	 * @param string $message
429
	 * @return array
430
	 */
431
	private function extractMessageDomain($message)
432
	{
433
		if (strpos($message, '.') !== FALSE && strpos($message, ' ') === FALSE) {
434
			list($domain, $message) = explode('.', $message, 2);
435
436
		} else {
437
			$domain = 'messages';
438
		}
439
440
		return [$domain, $message];
441
	}
442
443
444
445
	/**
446
	 * @param string $message
447
	 * @param string $domain
448
	 * @param string $locale
449
	 */
450
	protected function logMissingTranslation($message, $domain, $locale)
451
	{
452
		if ($this->psrLogger) {
453
			$this->psrLogger->notice('Missing translation', [
454
				'message' => $message,
455
				'domain' => $domain,
456
				'locale' => $locale ?: $this->getLocale(),
457
			]);
458
		}
459
460
		if ($this->panel !== NULL) {
461
			$this->panel->markUntranslated($message, $domain);
462
		}
463
	}
464
465
466
467
	/**
468
	 * @param array $whitelist
469
	 * @return null|string
470
	 */
471
	public static function buildWhitelistRegexp($whitelist)
472
	{
473
		return $whitelist ? '~^(' . implode('|', $whitelist) . ')~i' : NULL;
474
	}
475
476
477
478
	/*************************** Nette\Object ***************************/
479
480
481
482
	/**
483
	 * Access to reflection.
484
	 * @return \Nette\Reflection\ClassType
485
	 */
486
	public static function getReflection()
487
	{
488
		return new Nette\Reflection\ClassType(get_called_class());
489
	}
490
491
492
493
	/**
494
	 * Call to undefined method.
495
	 *
496
	 * @param string $name
497
	 * @param array $args
498
	 *
499
	 * @throws \Nette\MemberAccessException
500
	 * @return mixed
501
	 */
502
	public function __call($name, $args)
503
	{
504
		return ObjectMixin::call($this, $name, $args);
505
	}
506
507
508
509
	/**
510
	 * Call to undefined static method.
511
	 *
512
	 * @param string $name
513
	 * @param array $args
514
	 *
515
	 * @throws \Nette\MemberAccessException
516
	 * @return mixed
517
	 */
518
	public static function __callStatic($name, $args)
519
	{
520
		return ObjectMixin::callStatic(get_called_class(), $name, $args);
521
	}
522
523
524
525
	/**
526
	 * Adding method to class.
527
	 *
528
	 * @param $name
529
	 * @param null $callback
530
	 *
531
	 * @throws \Nette\MemberAccessException
532
	 * @return callable|null
533
	 */
534
	public static function extensionMethod($name, $callback = NULL)
535
	{
536
		if (strpos($name, '::') === FALSE) {
537
			$class = get_called_class();
538
		} else {
539
			list($class, $name) = explode('::', $name);
540
		}
541
		if ($callback === NULL) {
542
			return ObjectMixin::getExtensionMethod($class, $name);
543
		} else {
544
			ObjectMixin::setExtensionMethod($class, $name, $callback);
545
		}
546
	}
547
548
549
550
	/**
551
	 * Returns property value. Do not call directly.
552
	 *
553
	 * @param string $name
554
	 *
555
	 * @throws \Nette\MemberAccessException
556
	 * @return mixed
557
	 */
558
	public function &__get($name)
559
	{
560
		return ObjectMixin::get($this, $name);
561
	}
562
563
564
565
	/**
566
	 * Sets value of a property. Do not call directly.
567
	 *
568
	 * @param string $name
569
	 * @param mixed $value
570
	 *
571
	 * @throws \Nette\MemberAccessException
572
	 * @return void
573
	 */
574
	public function __set($name, $value)
575
	{
576
		ObjectMixin::set($this, $name, $value);
577
	}
578
579
580
581
	/**
582
	 * Is property defined?
583
	 *
584
	 * @param string $name
585
	 *
586
	 * @return bool
587
	 */
588
	public function __isset($name)
589
	{
590
		return ObjectMixin::has($this, $name);
591
	}
592
593
594
595
	/**
596
	 * Access to undeclared property.
597
	 *
598
	 * @param string $name
599
	 *
600
	 * @throws \Nette\MemberAccessException
601
	 * @return void
602
	 */
603
	public function __unset($name)
604
	{
605
		ObjectMixin::remove($this, $name);
606
	}
607
608
}
609