Completed
Pull Request — master (#115)
by Lukáš
03:02
created

Translator   F

Complexity

Total Complexity 67

Size/Duplication

Total Lines 579
Duplicated Lines 5.53 %

Coupling/Cohesion

Components 4
Dependencies 12

Importance

Changes 23
Bugs 3 Features 4
Metric Value
wmc 67
c 23
b 3
f 4
lcom 4
cbo 12
dl 32
loc 579
rs 3.0612

32 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A injectPanel() 0 4 1
A injectPsrLogger() 0 4 1
A setLogger() 0 4 1
C translate() 6 35 11
B trans() 13 24 5
C transChoice() 13 33 7
A addLoader() 0 5 1
A getLoaders() 0 4 1
A setLocaleWhitelist() 0 4 1
B addResource() 0 17 5
A setFallbackLocales() 0 5 1
A getAvailableLocales() 0 6 1
A getLocale() 0 8 2
A getDefaultLocale() 0 4 1
A setDefaultLocale() 0 6 1
A domain() 0 4 1
A createTemplateHelpers() 0 4 1
A loadCatalogue() 0 12 3
A computeFallbackLocales() 0 4 1
A assertValidLocale() 0 6 2
A extractMessageDomain() 0 11 3
A logMissingTranslation() 0 10 3
A buildWhitelistRegexp() 0 4 2
A getReflection() 0 4 1
A __call() 0 4 1
A __callStatic() 0 4 1
A extensionMethod() 0 13 3
A __get() 0 4 1
A __set() 0 4 1
A __isset() 0 4 1
A __unset() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Translator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Translator, and based on these observations, apply Extract Interface, too.

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 Psr\Log\LoggerAwareInterface;
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, LoggerAwareInterface
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
81
	/**
82
	 * @param IUserLocaleResolver $localeResolver
83
	 * @param MessageSelector $selector The message selector for pluralization
84
	 * @param CatalogueCompiler $catalogueCompiler
85
	 * @param FallbackResolver $fallbackResolver
86
	 * @param IResourceLoader $loader
87
	 */
88
	public function __construct(IUserLocaleResolver $localeResolver, MessageSelector $selector,
89
		CatalogueCompiler $catalogueCompiler, FallbackResolver $fallbackResolver, IResourceLoader $loader)
90
	{
91
		$this->localeResolver = $localeResolver;
92
		$this->catalogueCompiler = $catalogueCompiler;
93
		$this->fallbackResolver = $fallbackResolver;
94
		$this->translationsLoader = $loader;
95
96
		parent::__construct(NULL, $selector);
97
	}
98
99
100
101
	/**
102
	 * @internal
103
	 * @param Panel $panel
104
	 */
105
	public function injectPanel(Panel $panel)
106
	{
107
		$this->panel = $panel;
108
	}
109
110
111
112
	/**
113
	 * @param LoggerInterface|NULL $logger
114
	 */
115
	public function injectPsrLogger(LoggerInterface $logger = NULL)
116
	{
117
		$this->psrLogger = $logger;
118
	}
119
120
121
122
	/**
123
	 * @param LoggerInterface $logger
124
	 */
125
	public function setLogger(LoggerInterface $logger)
126
	{
127
		$this->injectPsrLogger($logger);
128
	}
129
130
131
132
	/**
133
	 * Translates the given string.
134
	 *
135
	 * @param string  $message    The message id
136
	 * @param integer $count      The number to use to find the indice of the message
137
	 * @param array   $parameters An array of parameters for the message
138
	 * @param string  $domain     The domain for the message
139
	 * @param string  $locale     The locale
140
	 *
141
	 * @return string
142
	 */
143
	public function translate($message, $count = NULL, $parameters = [], $domain = NULL, $locale = NULL)
144
	{
145
		if ($message instanceof Phrase) {
146
			return $message->translate($this);
147
		}
148
149 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...
150
			$locale = $domain ?: NULL;
151
			$domain = $parameters ?: NULL;
152
			$parameters = $count;
153
			$count = NULL;
154
		}
155
156
		if (empty($message)) {
157
			return $message;
158
159
		} elseif ($message instanceof Nette\Utils\Html) {
160
			if ($this->panel) {
161
				$this->panel->markUntranslated($message, $domain);
0 ignored issues
show
Bug introduced by
It seems like $domain can also be of type array or null; however, Kdyby\Translation\Diagno...nel::markUntranslated() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
162
			}
163
			return $message; // todo: what now?
164
		}
165
166
		$tmp = [];
167
		foreach ($parameters as $key => $val) {
168
			$tmp['%' . trim($key, '%') . '%'] = $val;
169
		}
170
		$parameters = $tmp;
171
172
		if ($count !== NULL && is_scalar($count)) {
173
			return $this->transChoice($message, $count, $parameters + ['%count%' => $count], $domain, $locale);
0 ignored issues
show
Bug introduced by
It seems like $domain defined by $parameters ?: NULL on line 151 can also be of type array; however, Kdyby\Translation\Translator::transChoice() does only seem to accept string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
174
		}
175
176
		return $this->trans($message, $parameters, $domain, $locale);
0 ignored issues
show
Bug introduced by
It seems like $domain defined by $parameters ?: NULL on line 151 can also be of type array; however, Kdyby\Translation\Translator::trans() does only seem to accept string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
177
	}
178
179
180
181
	/**
182
	 * {@inheritdoc}
183
	 */
184
	public function trans($message, array $parameters = [], $domain = NULL, $locale = NULL)
185
	{
186
		if ($message instanceof Phrase) {
187
			return $message->translate($this);
188
		}
189
190 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...
191
			list($domain, $id) = $this->extractMessageDomain($message);
192
193
		} else {
194
			$id = $message;
195
		}
196
197
		$result = parent::trans($id, $parameters, $domain, $locale);
198 View Code Duplication
		if ($result === "\x01") {
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...
199
			$this->logMissingTranslation($message, $domain, $locale);
200
			if ($this->panel !== NULL) {
201
				$this->panel->markUntranslated($message, $domain);
202
			}
203
			$result = strtr($message, $parameters);
204
		}
205
206
		return $result;
207
	}
208
209
210
211
	/**
212
	 * {@inheritdoc}
213
	 */
214
	public function transChoice($message, $number, array $parameters = [], $domain = NULL, $locale = NULL)
215
	{
216
		if ($message instanceof Phrase) {
217
			return $message->translate($this);
218
		}
219
220 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...
221
			list($domain, $id) = $this->extractMessageDomain($message);
222
223
		} else {
224
			$id = $message;
225
		}
226
227
		try {
228
			$result = parent::transChoice($id, $number, $parameters, $domain, $locale);
229
230
		} catch (\Exception $e) {
231
			$result = $id;
232
			if ($this->panel !== NULL) {
233
				$this->panel->choiceError($e, $domain);
234
			}
235
		}
236
237 View Code Duplication
		if ($result === "\x01") {
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...
238
			$this->logMissingTranslation($message, $domain, $locale);
239
			if ($this->panel !== NULL) {
240
				$this->panel->markUntranslated($message, $domain);
241
			}
242
			$result = strtr($message, $parameters);
243
		}
244
245
		return $result;
246
	}
247
248
249
250
	/**
251
	 * @param string $format
252
	 * @param LoaderInterface $loader
253
	 */
254
	public function addLoader($format, LoaderInterface $loader)
255
	{
256
		parent::addLoader($format, $loader);
257
		$this->translationsLoader->addLoader($format, $loader);
258
	}
259
260
261
262
	/**
263
	 * @return \Symfony\Component\Translation\Loader\LoaderInterface[]
264
	 */
265
	protected function getLoaders()
266
	{
267
		return $this->translationsLoader->getLoaders();
268
	}
269
270
271
272
	/**
273
	 * @param array $whitelist
274
	 * @return Translator
275
	 */
276
	public function setLocaleWhitelist(array $whitelist = NULL)
277
	{
278
		$this->localeWhitelist = self::buildWhitelistRegexp($whitelist);
0 ignored issues
show
Bug introduced by
It seems like $whitelist defined by parameter $whitelist on line 276 can also be of type array; however, Kdyby\Translation\Transl...:buildWhitelistRegexp() does only seem to accept null|string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
279
	}
280
281
282
283
	/**
284
	 * {@inheritdoc}
285
	 */
286
	public function addResource($format, $resource, $locale, $domain = NULL)
287
	{
288
		if ($this->localeWhitelist && !preg_match($this->localeWhitelist, $locale)) {
289
			if ($this->panel) {
290
				$this->panel->addIgnoredResource($format, $resource, $locale, $domain);
291
			}
292
			return;
293
		}
294
295
		parent::addResource($format, $resource, $locale, $domain);
296
		$this->catalogueCompiler->addResource($format, $resource, $locale, $domain);
297
		$this->availableResourceLocales[$locale] = TRUE;
298
299
		if ($this->panel) {
300
			$this->panel->addResource($format, $resource, $locale, $domain);
301
		}
302
	}
303
304
305
306
	/**
307
	 * {@inheritdoc}
308
	 */
309
	public function setFallbackLocales(array $locales)
310
	{
311
		parent::setFallbackLocales($locales);
312
		$this->fallbackResolver->setFallbackLocales($locales);
313
	}
314
315
316
317
	/**
318
	 * Returns array of locales from given resources
319
	 *
320
	 * @return array
321
	 */
322
	public function getAvailableLocales()
323
	{
324
		$locales = array_keys($this->availableResourceLocales);
325
		sort($locales);
326
		return $locales;
327
	}
328
329
330
331
	/**
332
	 * {@inheritdoc}
333
	 */
334
	public function getLocale()
335
	{
336
		if ($this->locale === NULL) {
337
			$this->setLocale($this->localeResolver->resolve($this));
338
		}
339
340
		return $this->locale;
341
	}
342
343
344
345
	/**
346
	 * @return string
347
	 */
348
	public function getDefaultLocale()
349
	{
350
		return $this->defaultLocale;
351
	}
352
353
354
355
	/**
356
	 * @param string $locale
357
	 * @return Translator
358
	 */
359
	public function setDefaultLocale($locale)
360
	{
361
		$this->assertValidLocale($locale);
362
		$this->defaultLocale = $locale;
363
		return $this;
364
	}
365
366
367
368
	/**
369
	 * @param string $messagePrefix
370
	 * @return ITranslator
371
	 */
372
	public function domain($messagePrefix)
373
	{
374
		return new PrefixedTranslator($messagePrefix, $this);
375
	}
376
377
378
379
	/**
380
	 * @return TemplateHelpers
381
	 */
382
	public function createTemplateHelpers()
383
	{
384
		return new TemplateHelpers($this);
385
	}
386
387
388
389
	/**
390
	 * {@inheritdoc}
391
	 */
392
	protected function loadCatalogue($locale)
393
	{
394
		if (empty($locale)) {
395
			throw new InvalidArgumentException("Invalid locale.");
396
		}
397
398
		if (isset($this->catalogues[$locale])) {
399
			return;
400
		}
401
402
		$this->catalogues = $this->catalogueCompiler->compile($this, $this->catalogues, $locale);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->catalogueCompiler...s->catalogues, $locale) of type array<integer|string,obj...ageCatalogueInterface>> is incompatible with the declared type array<integer,object<Sym...ageCatalogueInterface>> of property $catalogues.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
403
	}
404
405
406
407
	/**
408
	 * {@inheritdoc}
409
	 */
410
	protected function computeFallbackLocales($locale)
411
	{
412
		return $this->fallbackResolver->compute($this, $locale);
413
	}
414
415
416
417
	/**
418
	 * Asserts that the locale is valid, throws an Exception if not.
419
	 *
420
	 * @param string $locale Locale to tests
421
	 * @throws \InvalidArgumentException If the locale contains invalid characters
422
	 */
423
	protected function assertValidLocale($locale)
424
	{
425
		if (preg_match('~^[a-z0-9@_\\.\\-]*\z~i', $locale) !== 1) {
426
			throw new \InvalidArgumentException(sprintf('Invalid "%s" locale.', $locale));
427
		}
428
	}
429
430
431
432
	/**
433
	 * @param string $message
434
	 * @return array
435
	 */
436
	private function extractMessageDomain($message)
437
	{
438
		if (strpos($message, '.') !== FALSE && strpos($message, ' ') === FALSE) {
439
			list($domain, $message) = explode('.', $message, 2);
440
441
		} else {
442
			$domain = 'messages';
443
		}
444
445
		return [$domain, $message];
446
	}
447
448
449
450
	/**
451
	 * @param string $message
452
	 * @param string $domain
453
	 * @param string $locale
454
	 */
455
	protected function logMissingTranslation($message, $domain, $locale)
456
	{
457
		if ($this->psrLogger) {
458
			$this->psrLogger->notice('Missing translation', [
459
				'message' => $message,
460
				'domain' => $domain,
461
				'locale' => $locale ?: $this->getLocale(),
462
			]);
463
		}
464
	}
465
466
467
468
	/**
469
	 * @param null|string $whitelist
470
	 * @return null|string
471
	 */
472
	public static function buildWhitelistRegexp($whitelist)
473
	{
474
		return $whitelist ? '~^(' . implode('|', $whitelist) . ')~i' : NULL;
475
	}
476
477
478
479
	/*************************** Nette\Object ***************************/
480
481
482
483
	/**
484
	 * Access to reflection.
485
	 * @return \Nette\Reflection\ClassType
486
	 */
487
	public static function getReflection()
488
	{
489
		return new Nette\Reflection\ClassType(get_called_class());
490
	}
491
492
493
494
	/**
495
	 * Call to undefined method.
496
	 *
497
	 * @param string $name
498
	 * @param array $args
499
	 *
500
	 * @throws \Nette\MemberAccessException
501
	 * @return mixed
502
	 */
503
	public function __call($name, $args)
504
	{
505
		return ObjectMixin::call($this, $name, $args);
506
	}
507
508
509
510
	/**
511
	 * Call to undefined static method.
512
	 *
513
	 * @param string $name
514
	 * @param array $args
515
	 *
516
	 * @throws \Nette\MemberAccessException
517
	 * @return mixed
518
	 */
519
	public static function __callStatic($name, $args)
520
	{
521
		return ObjectMixin::callStatic(get_called_class(), $name, $args);
522
	}
523
524
525
526
	/**
527
	 * Adding method to class.
528
	 *
529
	 * @param $name
530
	 * @param null $callback
531
	 *
532
	 * @throws \Nette\MemberAccessException
533
	 * @return callable|null
534
	 */
535
	public static function extensionMethod($name, $callback = NULL)
536
	{
537
		if (strpos($name, '::') === FALSE) {
538
			$class = get_called_class();
539
		} else {
540
			list($class, $name) = explode('::', $name);
541
		}
542
		if ($callback === NULL) {
543
			return ObjectMixin::getExtensionMethod($class, $name);
544
		} else {
545
			ObjectMixin::setExtensionMethod($class, $name, $callback);
546
		}
547
	}
548
549
550
551
	/**
552
	 * Returns property value. Do not call directly.
553
	 *
554
	 * @param string $name
555
	 *
556
	 * @throws \Nette\MemberAccessException
557
	 * @return mixed
558
	 */
559
	public function &__get($name)
560
	{
561
		return ObjectMixin::get($this, $name);
562
	}
563
564
565
566
	/**
567
	 * Sets value of a property. Do not call directly.
568
	 *
569
	 * @param string $name
570
	 * @param mixed $value
571
	 *
572
	 * @throws \Nette\MemberAccessException
573
	 * @return void
574
	 */
575
	public function __set($name, $value)
576
	{
577
		ObjectMixin::set($this, $name, $value);
578
	}
579
580
581
582
	/**
583
	 * Is property defined?
584
	 *
585
	 * @param string $name
586
	 *
587
	 * @return bool
588
	 */
589
	public function __isset($name)
590
	{
591
		return ObjectMixin::has($this, $name);
592
	}
593
594
595
596
	/**
597
	 * Access to undeclared property.
598
	 *
599
	 * @param string $name
600
	 *
601
	 * @throws \Nette\MemberAccessException
602
	 * @return void
603
	 */
604
	public function __unset($name)
605
	{
606
		ObjectMixin::remove($this, $name);
607
	}
608
609
}
610