Completed
Push — master ( 0619a6...a51eeb )
by Jáchym
21s queued 10s
created

Translator::translate()   F

Complexity

Conditions 16
Paths 353

Size

Total Lines 49

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 49
rs 2.8208
c 0
b 0
f 0
cc 16
nc 353
nop 2

How to fix   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\ChoiceMessageFormatterInterface;
19
use Symfony\Component\Translation\Formatter\MessageFormatterInterface;
20
use Symfony\Component\Translation\Loader\LoaderInterface;
21
22
class Translator extends \Symfony\Component\Translation\Translator implements \Kdyby\Translation\ITranslator
23
{
24
25
	use \Kdyby\StrictObjects\Scream;
26
27
	/**
28
	 * @var \Kdyby\Translation\IUserLocaleResolver
29
	 */
30
	private $localeResolver;
31
32
	/**
33
	 * @var \Kdyby\Translation\CatalogueCompiler
34
	 */
35
	private $catalogueCompiler;
36
37
	/**
38
	 * @var \Kdyby\Translation\FallbackResolver
39
	 */
40
	private $fallbackResolver;
41
42
	/**
43
	 * @var \Kdyby\Translation\IResourceLoader
44
	 */
45
	private $translationsLoader;
46
47
	/**
48
	 * @var \Psr\Log\LoggerInterface|NULL
49
	 */
50
	private $psrLogger;
51
52
	/**
53
	 * @var \Kdyby\Translation\Diagnostics\Panel|NULL
54
	 */
55
	private $panel;
56
57
	/**
58
	 * @var array
59
	 */
60
	private $availableResourceLocales = [];
61
62
	/**
63
	 * @var string
64
	 */
65
	private $defaultLocale;
66
67
	/**
68
	 * @var string|NULL
69
	 */
70
	private $localeWhitelist;
71
72
	/**
73
	 * @var \Symfony\Component\Translation\Formatter\MessageFormatterInterface
74
	 */
75
	private $formatter;
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
76
77
	/**
78
	 * @param \Kdyby\Translation\IUserLocaleResolver $localeResolver
79
	 * @param \Symfony\Component\Translation\Formatter\MessageFormatterInterface $formatter
80
	 * @param \Kdyby\Translation\CatalogueCompiler $catalogueCompiler
81
	 * @param \Kdyby\Translation\FallbackResolver $fallbackResolver
82
	 * @param \Kdyby\Translation\IResourceLoader $loader
83
	 * @throws \InvalidArgumentException
84
	 */
85
	public function __construct(
86
		IUserLocaleResolver $localeResolver,
87
		MessageFormatterInterface $formatter,
88
		CatalogueCompiler $catalogueCompiler,
89
		FallbackResolver $fallbackResolver,
90
		IResourceLoader $loader
91
	)
92
	{
93
		$this->localeResolver = $localeResolver;
94
		$this->formatter = $formatter;
95
		$this->catalogueCompiler = $catalogueCompiler;
96
		$this->fallbackResolver = $fallbackResolver;
97
		$this->translationsLoader = $loader;
98
99
		parent::__construct('', $formatter);
100
		$this->setLocale(NULL);
101
	}
102
103
	/**
104
	 * @internal
105
	 * @param \Kdyby\Translation\Diagnostics\Panel $panel
106
	 */
107
	public function injectPanel(Panel $panel)
108
	{
109
		$this->panel = $panel;
110
	}
111
112
	/**
113
	 * @param \Psr\Log\LoggerInterface|NULL $logger
114
	 */
115
	public function injectPsrLogger(LoggerInterface $logger = NULL)
116
	{
117
		$this->psrLogger = $logger;
118
	}
119
120
	/**
121
	 * Translates the given string.
122
	 *
123
	 * @param string|\Kdyby\Translation\Phrase|mixed $message The message id
124
	 * @param mixed ...$arg An array of parameters for the message
125
	 * @throws \InvalidArgumentException
126
	 * @return string
127
	 */
128
	public function translate($message, ...$arg): string
129
	{
130
		if ($message instanceof Phrase) {
131
			return $message->translate($this);
132
		}
133
134
		$count = isset($arg[0]) ? $arg[0] : NULL;
135
		$parameters = isset($arg[1]) ? $arg[1] : [];
136
		$domain = isset($arg[2]) ? $arg[2] : NULL;
137
		$locale = isset($arg[3]) ? $arg[3] : NULL;
138
139
		if (empty($message)) {
140
			return $message;
141
142
		} elseif ($message instanceof NetteHtmlString || $message instanceof LatteHtmlString) {
143
			$this->logMissingTranslation($message->__toString(), $domain, $locale);
144
			return (string) $message; // what now?
145
		} elseif (is_int($message)) {
146
			$message = (string) $message;
147
		}
148
149
		if (!is_string($message)) {
150
			throw new \Kdyby\Translation\InvalidArgumentException(sprintf('Message id must be a string, %s was given', gettype($message)));
151
		}
152
153
		if (Strings::startsWith($message, '//')) {
154
			if ($domain !== NULL) {
155
				throw new \Kdyby\Translation\InvalidArgumentException(sprintf(
156
					'Providing domain "%s" while also having the message "%s" absolute is not supported',
157
					$domain,
158
					$message
159
				));
160
			}
161
162
			$message = Strings::substring($message, 2);
163
		}
164
165
		$tmp = [];
166
		foreach ($parameters as $key => $val) {
167
			$tmp['%' . trim($key, '%') . '%'] = $val;
168
		}
169
		$parameters = $tmp;
170
171
		if ($count !== NULL && is_scalar($count)) {
172
			return $this->transChoice($message, $count, $parameters + ['%count%' => $count], $domain, $locale);
173
		}
174
175
		return $this->trans($message, $parameters, $domain, $locale);
176
	}
177
178
	/**
179
	 * {@inheritdoc}
180
	 */
181
	public function trans($message, array $parameters = [], $domain = NULL, $locale = NULL)
182
	{
183
		if (is_int($message)) {
184
			$message = (string) $message;
185
		}
186
187
		if (!is_string($message)) {
188
			throw new \Kdyby\Translation\InvalidArgumentException(sprintf('Message id must be a string, %s was given', gettype($message)));
189
		}
190
191 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...
192
			list($domain, $id) = $this->extractMessageDomain($message);
193
194
		} else {
195
			$id = $message;
196
		}
197
198
		$result = parent::trans($id, $parameters, $domain, $locale);
199
		if ($result === "\x01") {
200
			$this->logMissingTranslation($message, $domain, $locale);
201
			$result = strtr($message, $parameters);
202
		}
203
204
		return $result;
205
	}
206
207
	/**
208
	 * {@inheritdoc}
209
	 */
210
	public function transChoice($message, $number, array $parameters = [], $domain = NULL, $locale = NULL)
211
	{
212
		if (is_int($message)) {
213
			$message = (string) $message;
214
		}
215
216
		if (!is_string($message)) {
217
			throw new \Kdyby\Translation\InvalidArgumentException(sprintf('Message id must be a string, %s was given', gettype($message)));
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);
0 ignored issues
show
Deprecated Code introduced by
The method Symfony\Component\Transl...anslator::transChoice() has been deprecated with message: since Symfony 4.2, use the trans() method instead with a %count% parameter

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
229
230
		} catch (\Exception $e) {
231
			$result = $id;
232
			if ($this->panel !== NULL) {
233
				$this->panel->choiceError($e, $domain);
234
			}
235
		}
236
237
		if ($result === "\x01") {
238
			$this->logMissingTranslation($message, $domain, $locale);
239
			if ($locale === NULL) {
240
				$locale = $this->getLocale();
241
			}
242
			if ($locale === NULL) {
243
				$result = strtr($message, $parameters);
244
245
			} else {
246
				if (!$this->formatter instanceof ChoiceMessageFormatterInterface) {
247
					$result = $id;
248
					if ($this->panel !== NULL) {
249
						$this->panel->choiceError(new \Symfony\Component\Translation\Exception\LogicException(sprintf('The formatter "%s" does not support plural translations.', get_class($this->formatter))), $domain);
250
					}
251
				} else {
252
					$result = $this->formatter->choiceFormat($message, (int) $number, $locale, $parameters);
253
				}
254
			}
255
		}
256
257
		return $result;
258
	}
259
260
	/**
261
	 * @param string $format
262
	 * @param \Symfony\Component\Translation\Loader\LoaderInterface $loader
263
	 */
264
	public function addLoader($format, LoaderInterface $loader)
265
	{
266
		parent::addLoader($format, $loader);
267
		$this->translationsLoader->addLoader($format, $loader);
268
	}
269
270
	/**
271
	 * @return \Symfony\Component\Translation\Loader\LoaderInterface[]
272
	 */
273
	protected function getLoaders()
274
	{
275
		return $this->translationsLoader->getLoaders();
276
	}
277
278
	/**
279
	 * @param array $whitelist
280
	 */
281
	public function setLocaleWhitelist(array $whitelist = NULL)
282
	{
283
		$this->localeWhitelist = self::buildWhitelistRegexp($whitelist);
284
	}
285
286
	/**
287
	 * {@inheritdoc}
288
	 */
289
	public function addResource($format, $resource, $locale, $domain = NULL)
290
	{
291
		if ($this->localeWhitelist !== NULL && !preg_match($this->localeWhitelist, $locale)) {
292
			if ($this->panel !== NULL) {
293
				$this->panel->addIgnoredResource($format, $resource, $locale, $domain);
294
			}
295
			return;
296
		}
297
298
		parent::addResource($format, $resource, $locale, $domain);
299
		$this->catalogueCompiler->addResource($format, $resource, $locale, $domain);
300
		$this->availableResourceLocales[$locale] = TRUE;
301
302
		if ($this->panel !== NULL) {
303
			$this->panel->addResource($format, $resource, $locale, $domain);
304
		}
305
	}
306
307
	/**
308
	 * {@inheritdoc}
309
	 */
310
	public function setFallbackLocales(array $locales)
311
	{
312
		parent::setFallbackLocales($locales);
313
		$this->fallbackResolver->setFallbackLocales($locales);
314
	}
315
316
	/**
317
	 * Returns array of locales from given resources
318
	 *
319
	 * @return array
320
	 */
321
	public function getAvailableLocales()
322
	{
323
		$locales = array_keys($this->availableResourceLocales);
324
		sort($locales);
325
		return $locales;
326
	}
327
328
	/**
329
	 * Sets the current locale.
330
	 *
331
	 * @param string|NULL $locale The locale
332
	 *
333
	 * @throws \InvalidArgumentException If the locale contains invalid characters
334
	 */
335
	public function setLocale($locale)
336
	{
337
		parent::setLocale($locale);
338
	}
339
340
	/**
341
	 * Returns the current locale.
342
	 *
343
	 * @return string|NULL The locale
344
	 */
345
	public function getLocale()
346
	{
347
		if (parent::getLocale() === NULL) {
348
			$this->setLocale($this->localeResolver->resolve($this));
349
		}
350
351
		return parent::getLocale();
352
	}
353
354
	/**
355
	 * @return string
356
	 */
357
	public function getDefaultLocale()
358
	{
359
		return $this->defaultLocale;
360
	}
361
362
	/**
363
	 * @param string $locale
364
	 * @return \Kdyby\Translation\Translator
365
	 */
366
	public function setDefaultLocale($locale)
367
	{
368
		$this->assertValidLocale($locale);
369
		$this->defaultLocale = $locale;
370
		return $this;
371
	}
372
373
	/**
374
	 * @param string $messagePrefix
375
	 * @return \Kdyby\Translation\ITranslator
376
	 */
377
	public function domain($messagePrefix)
378
	{
379
		return new PrefixedTranslator($messagePrefix, $this);
380
	}
381
382
	/**
383
	 * @return \Kdyby\Translation\TemplateHelpers
384
	 */
385
	public function createTemplateHelpers()
386
	{
387
		return new TemplateHelpers($this);
388
	}
389
390
	/**
391
	 * {@inheritdoc}
392
	 */
393
	protected function loadCatalogue($locale)
394
	{
395
		if (empty($locale)) {
396
			throw new \Kdyby\Translation\InvalidArgumentException('Invalid locale.');
397
		}
398
399
		if (isset($this->catalogues[$locale])) {
400
			return;
401
		}
402
403
		$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...
404
	}
405
406
	/**
407
	 * {@inheritdoc}
408
	 */
409
	protected function computeFallbackLocales($locale)
410
	{
411
		return $this->fallbackResolver->compute($this, $locale);
412
	}
413
414
	/**
415
	 * Asserts that the locale is valid, throws an Exception if not.
416
	 *
417
	 * @param string $locale Locale to tests
418
	 * @throws \InvalidArgumentException If the locale contains invalid characters
419
	 */
420
	protected function assertValidLocale($locale)
421
	{
422
		if (preg_match('~^[a-z0-9@_\\.\\-]*\z~i', $locale) !== 1) {
423
			throw new \InvalidArgumentException(sprintf('Invalid "%s" locale.', $locale));
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
	 * @param string|NULL $message
445
	 * @param string|NULL $domain
446
	 * @param string|NULL $locale
447
	 */
448
	protected function logMissingTranslation($message, $domain, $locale)
449
	{
450
		if ($message === NULL) {
451
			return;
452
		}
453
454
		if ($this->psrLogger !== NULL) {
455
			$this->psrLogger->notice('Missing translation', [
456
				'message' => $message,
457
				'domain' => $domain,
458
				'locale' => $locale ?: $this->getLocale(),
459
			]);
460
		}
461
462
		if ($this->panel !== NULL) {
463
			$this->panel->markUntranslated($message, $domain);
464
		}
465
	}
466
467
	/**
468
	 * @param array|NULL $whitelist
469
	 * @return null|string
470
	 */
471
	public static function buildWhitelistRegexp($whitelist)
472
	{
473
		return ($whitelist !== NULL) ? '~^(' . implode('|', $whitelist) . ')~i' : NULL;
474
	}
475
476
}
477