Completed
Pull Request — master (#154)
by Michal
02:01
created

Translator::translate()   D

Complexity

Conditions 16
Paths 199

Size

Total Lines 51

Duplication

Lines 6
Ratio 11.76 %

Importance

Changes 0
Metric Value
dl 6
loc 51
rs 4.7416
c 0
b 0
f 0
cc 16
nc 199
nop 5

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Translation\Diagnostics\Panel;
14
use Latte\Runtime\IHtmlString as LatteHtmlString;
15
use Nette\Utils\IHtmlString as NetteHtmlString;
16
use Nette\Utils\Strings;
17
use Psr\Log\LoggerInterface;
18
use Symfony\Component\Translation\Formatter\MessageFormatter;
19
use Symfony\Component\Translation\Loader\LoaderInterface;
20
21
class Translator extends \Symfony\Component\Translation\Translator implements \Kdyby\Translation\ITranslator
22
{
23
24
	use \Kdyby\StrictObjects\Scream;
25
26
	/**
27
	 * @var \Kdyby\Translation\IUserLocaleResolver
28
	 */
29
	private $localeResolver;
30
31
	/**
32
	 * @var \Kdyby\Translation\CatalogueCompiler
33
	 */
34
	private $catalogueCompiler;
35
36
	/**
37
	 * @var \Kdyby\Translation\FallbackResolver
38
	 */
39
	private $fallbackResolver;
40
41
	/**
42
	 * @var \Kdyby\Translation\IResourceLoader
43
	 */
44
	private $translationsLoader;
45
46
	/**
47
	 * @var \Psr\Log\LoggerInterface|NULL
48
	 */
49
	private $psrLogger;
50
51
	/**
52
	 * @var \Kdyby\Translation\Diagnostics\Panel|NULL
53
	 */
54
	private $panel;
55
56
	/**
57
	 * @var array
58
	 */
59
	private $availableResourceLocales = [];
60
61
	/**
62
	 * @var string
63
	 */
64
	private $defaultLocale;
65
66
	/**
67
	 * @var string|NULL
68
	 */
69
	private $localeWhitelist;
70
71
	/**
72
	 * @var \Symfony\Component\Translation\Formatter\MessageFormatter
73
	 */
74
	private $formatter;
75
76
	/**
77
	 * @param \Kdyby\Translation\IUserLocaleResolver $localeResolver
78
	 * @param \Symfony\Component\Translation\Formatter\MessageFormatter $formatter
79
	 * @param \Kdyby\Translation\CatalogueCompiler $catalogueCompiler
80
	 * @param \Kdyby\Translation\FallbackResolver $fallbackResolver
81
	 * @param \Kdyby\Translation\IResourceLoader $loader
82
	 * @throws \InvalidArgumentException
83
	 */
84
	public function __construct(
85
		IUserLocaleResolver $localeResolver,
86
		MessageFormatter $formatter,
87
		CatalogueCompiler $catalogueCompiler,
88
		FallbackResolver $fallbackResolver,
89
		IResourceLoader $loader
90
	)
91
	{
92
		$this->localeResolver = $localeResolver;
93
		$this->formatter = $formatter;
94
		$this->catalogueCompiler = $catalogueCompiler;
95
		$this->fallbackResolver = $fallbackResolver;
96
		$this->translationsLoader = $loader;
97
98
		parent::__construct('', $formatter);
99
		$this->setLocale(NULL);
100
	}
101
102
	/**
103
	 * @internal
104
	 * @param \Kdyby\Translation\Diagnostics\Panel $panel
105
	 */
106
	public function injectPanel(Panel $panel)
107
	{
108
		$this->panel = $panel;
109
	}
110
111
	/**
112
	 * @param \Psr\Log\LoggerInterface|NULL $logger
113
	 */
114
	public function injectPsrLogger(LoggerInterface $logger = NULL)
115
	{
116
		$this->psrLogger = $logger;
117
	}
118
119
	/**
120
	 * Translates the given string.
121
	 *
122
	 * @param string|\Kdyby\Translation\Phrase|mixed $message The message id
123
	 * @param int|array|NULL $count The number to use to find the indice of the message
124
	 * @param string|array|NULL $parameters An array of parameters for the message
125
	 * @param string|NULL $domain The domain for the message
126
	 * @param string|NULL $locale The locale
127
	 * @throws \InvalidArgumentException
128
	 * @return string|\Nette\Utils\IHtmlString|\Latte\Runtime\IHtmlString
129
	 */
130
	public function translate($message, $count = NULL, $parameters = [], $domain = NULL, $locale = NULL)
131
	{
132
		if ($message instanceof Phrase) {
133
			return $message->translate($this);
134
		}
135
136 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...
137
			$locale = ($domain !== NULL) ? (string) $domain : NULL;
138
			$domain = ($parameters !== NULL && !empty($parameters)) ? (string) $parameters : NULL;
139
			$parameters = $count;
140
			$count = NULL;
141
		}
142
143
		if (empty($message)) {
144
			return $message;
145
146
		} elseif ($message instanceof NetteHtmlString || $message instanceof LatteHtmlString) {
0 ignored issues
show
Bug introduced by
The class Nette\Utils\IHtmlString 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...
Bug introduced by
The class Latte\Runtime\IHtmlString 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...
147
			$this->logMissingTranslation($message->__toString(), $domain, $locale);
148
			return $message; // what now?
149
		} elseif (is_int($message)) {
150
			$message = (string) $message;
151
		}
152
153
		if (!is_string($message)) {
154
			throw new \Kdyby\Translation\InvalidArgumentException(sprintf('Message id must be a string, %s was given', gettype($message)));
155
		}
156
157
		if (Strings::startsWith($message, '//')) {
158
			if ($domain !== NULL) {
159
				throw new \Kdyby\Translation\InvalidArgumentException(sprintf(
160
					'Providing domain "%s" while also having the message "%s" absolute is not supported',
161
					$domain,
162
					$message
163
				));
164
			}
165
166
			$message = Strings::substring($message, 2);
167
		}
168
169
		$tmp = [];
170
		foreach ($parameters as $key => $val) {
0 ignored issues
show
Bug introduced by
The expression $parameters of type string|array|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
171
			$tmp['%' . trim($key, '%') . '%'] = $val;
172
		}
173
		$parameters = $tmp;
174
175
		if ($count !== NULL && is_scalar($count)) {
176
			return $this->transChoice($message, $count, $parameters + ['%count%' => $count], $domain, $locale);
177
		}
178
179
		return $this->trans($message, $parameters, $domain, $locale);
180
	}
181
182
	/**
183
	 * {@inheritdoc}
184
	 */
185
	public function trans($message, array $parameters = [], $domain = NULL, $locale = NULL)
186
	{
187
		if (is_int($message)) {
188
			$message = (string) $message;
189
		}
190
191
		if (!is_string($message)) {
192
			throw new \Kdyby\Translation\InvalidArgumentException(sprintf('Message id must be a string, %s was given', gettype($message)));
193
		}
194
195 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...
196
			list($domain, $id) = $this->extractMessageDomain($message);
197
198
		} else {
199
			$id = $message;
200
		}
201
202
		$result = parent::trans($id, $parameters, $domain, $locale);
203
		if ($result === "\x01") {
204
			$this->logMissingTranslation($message, $domain, $locale);
205
			$result = strtr($message, $parameters);
206
		}
207
208
		return $result;
209
	}
210
211
	/**
212
	 * {@inheritdoc}
213
	 */
214
	public function transChoice($message, $number, array $parameters = [], $domain = NULL, $locale = NULL)
215
	{
216
		if (is_int($message)) {
217
			$message = (string) $message;
218
		}
219
220
		if (!is_string($message)) {
221
			throw new \Kdyby\Translation\InvalidArgumentException(sprintf('Message id must be a string, %s was given', gettype($message)));
222
		}
223
224 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...
225
			list($domain, $id) = $this->extractMessageDomain($message);
226
227
		} else {
228
			$id = $message;
229
		}
230
231
		try {
232
			$result = parent::transChoice($id, $number, $parameters, $domain, $locale);
233
234
		} catch (\Exception $e) {
235
			$result = $id;
236
			if ($this->panel !== NULL) {
237
				$this->panel->choiceError($e, $domain);
238
			}
239
		}
240
241
		if ($result === "\x01") {
242
			$this->logMissingTranslation($message, $domain, $locale);
243
			if ($locale === NULL) {
244
				$locale = $this->getLocale();
245
			}
246
			if ($locale === NULL) {
247
				$result = strtr($message, $parameters);
248
249
			} else {
250
				$result = $this->formatter->choiceFormat($message, (int) $number, $locale, $parameters);
251
			}
252
		}
253
254
		return $result;
255
	}
256
257
	/**
258
	 * @param string $format
259
	 * @param \Symfony\Component\Translation\Loader\LoaderInterface $loader
260
	 */
261
	public function addLoader($format, LoaderInterface $loader)
262
	{
263
		parent::addLoader($format, $loader);
264
		$this->translationsLoader->addLoader($format, $loader);
265
	}
266
267
	/**
268
	 * @return \Symfony\Component\Translation\Loader\LoaderInterface[]
269
	 */
270
	protected function getLoaders()
271
	{
272
		return $this->translationsLoader->getLoaders();
273
	}
274
275
	/**
276
	 * @param array $whitelist
277
	 */
278
	public function setLocaleWhitelist(array $whitelist = NULL)
279
	{
280
		$this->localeWhitelist = self::buildWhitelistRegexp($whitelist);
281
	}
282
283
	/**
284
	 * {@inheritdoc}
285
	 */
286
	public function addResource($format, $resource, $locale, $domain = NULL)
287
	{
288
		if ($this->localeWhitelist !== NULL && !preg_match($this->localeWhitelist, $locale)) {
289
			if ($this->panel !== NULL) {
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 !== NULL) {
300
			$this->panel->addResource($format, $resource, $locale, $domain);
301
		}
302
	}
303
304
	/**
305
	 * {@inheritdoc}
306
	 */
307
	public function setFallbackLocales(array $locales)
308
	{
309
		parent::setFallbackLocales($locales);
310
		$this->fallbackResolver->setFallbackLocales($locales);
311
	}
312
313
	/**
314
	 * Returns array of locales from given resources
315
	 *
316
	 * @return array
317
	 */
318
	public function getAvailableLocales()
319
	{
320
		$locales = array_keys($this->availableResourceLocales);
321
		sort($locales);
322
		return $locales;
323
	}
324
325
	/**
326
	 * Sets the current locale.
327
	 *
328
	 * @param string|NULL $locale The locale
329
	 *
330
	 * @throws \InvalidArgumentException If the locale contains invalid characters
331
	 */
332
	public function setLocale($locale)
333
	{
334
		parent::setLocale($locale);
335
	}
336
337
	/**
338
	 * Returns the current locale.
339
	 *
340
	 * @return string|NULL The locale
341
	 */
342
	public function getLocale()
343
	{
344
		if (parent::getLocale() === NULL) {
345
			$this->setLocale($this->localeResolver->resolve($this));
346
		}
347
348
		return parent::getLocale();
349
	}
350
351
	/**
352
	 * @return string
353
	 */
354
	public function getDefaultLocale()
355
	{
356
		return $this->defaultLocale;
357
	}
358
359
	/**
360
	 * @param string $locale
361
	 * @return \Kdyby\Translation\Translator
362
	 */
363
	public function setDefaultLocale($locale)
364
	{
365
		$this->assertValidLocale($locale);
366
		$this->defaultLocale = $locale;
367
		return $this;
368
	}
369
370
	/**
371
	 * @param string $messagePrefix
372
	 * @return \Kdyby\Translation\ITranslator
373
	 */
374
	public function domain($messagePrefix)
375
	{
376
		return new PrefixedTranslator($messagePrefix, $this);
377
	}
378
379
	/**
380
	 * @return \Kdyby\Translation\TemplateHelpers
381
	 */
382
	public function createTemplateHelpers()
383
	{
384
		return new TemplateHelpers($this);
385
	}
386
387
	/**
388
	 * {@inheritdoc}
389
	 */
390
	protected function loadCatalogue($locale)
391
	{
392
		if (empty($locale)) {
393
			throw new \Kdyby\Translation\InvalidArgumentException('Invalid locale.');
394
		}
395
396
		if (isset($this->catalogues[$locale])) {
397
			return;
398
		}
399
400
		$this->catalogues = $this->catalogueCompiler->compile($this, $this->catalogues, $locale);
401
	}
402
403
	/**
404
	 * {@inheritdoc}
405
	 */
406
	protected function computeFallbackLocales($locale)
407
	{
408
		return $this->fallbackResolver->compute($this, $locale);
409
	}
410
411
	/**
412
	 * Asserts that the locale is valid, throws an Exception if not.
413
	 *
414
	 * @param string $locale Locale to tests
415
	 * @throws \InvalidArgumentException If the locale contains invalid characters
416
	 */
417
	protected function assertValidLocale($locale)
418
	{
419
		if (preg_match('~^[a-z0-9@_\\.\\-]*\z~i', $locale) !== 1) {
420
			throw new \InvalidArgumentException(sprintf('Invalid "%s" locale.', $locale));
421
		}
422
	}
423
424
	/**
425
	 * @param string $message
426
	 * @return array
427
	 */
428
	private function extractMessageDomain($message)
429
	{
430
		if (strpos($message, '.') !== FALSE && strpos($message, ' ') === FALSE) {
431
			list($domain, $message) = explode('.', $message, 2);
432
433
		} else {
434
			$domain = 'messages';
435
		}
436
437
		return [$domain, $message];
438
	}
439
440
	/**
441
	 * @param string|NULL $message
442
	 * @param string|NULL $domain
443
	 * @param string|NULL $locale
444
	 */
445
	protected function logMissingTranslation($message, $domain, $locale)
446
	{
447
		if ($message === NULL) {
448
			return;
449
		}
450
451
		if ($this->psrLogger !== NULL) {
452
			$this->psrLogger->notice('Missing translation', [
453
				'message' => $message,
454
				'domain' => $domain,
455
				'locale' => $locale ?: $this->getLocale(),
456
			]);
457
		}
458
459
		if ($this->panel !== NULL) {
460
			$this->panel->markUntranslated($message, $domain);
461
		}
462
	}
463
464
	/**
465
	 * @param array|NULL $whitelist
466
	 * @return null|string
467
	 */
468
	public static function buildWhitelistRegexp($whitelist)
469
	{
470
		return ($whitelist !== NULL) ? '~^(' . implode('|', $whitelist) . ')~i' : NULL;
471
	}
472
473
}
474