Completed
Pull Request — master (#181)
by Michal
01:25
created

Translator::trans()   B

Complexity

Conditions 6
Paths 18

Size

Total Lines 30

Duplication

Lines 6
Ratio 20 %

Importance

Changes 0
Metric Value
dl 6
loc 30
rs 8.8177
c 0
b 0
f 0
cc 6
nc 18
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\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;
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
	}
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 mixed ...$arg An array of parameters for the message
124
	 * @throws \InvalidArgumentException
125
	 * @return string
126
	 */
127
	public function translate($message, ...$arg): string
128
	{
129
		if ($message instanceof Phrase) {
130
			return $message->translate($this);
131
		}
132
133
		$count = isset($arg[0]) ? $arg[0] : NULL;
134
		$parameters = isset($arg[1]) ? $arg[1] : [];
135
		$domain = isset($arg[2]) ? $arg[2] : NULL;
136
		$locale = isset($arg[3]) ? $arg[3] : NULL;
137
138 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...
139
			$locale = ($domain !== NULL) ? (string) $domain : NULL;
140
			$domain = ($parameters !== NULL && !empty($parameters)) ? (string) $parameters : NULL;
141
			$parameters = $count;
142
			$count = NULL;
143
		}
144
145
		if (empty($message)) {
146
			return '';
147
		} 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...
148
			$this->logMissingTranslation($message->__toString(), $domain, $locale);
149
			return (string) $message; // what now?
150
		} elseif (is_int($message)) {
151
			$message = (string) $message;
152
		}
153
154
		if (!is_string($message)) {
155
			throw new \Kdyby\Translation\InvalidArgumentException(sprintf('Message id must be a string, %s was given', gettype($message)));
156
		}
157
158
		if (Strings::startsWith($message, '//')) {
159
			if ($domain !== NULL) {
160
				throw new \Kdyby\Translation\InvalidArgumentException(sprintf(
161
					'Providing domain "%s" while also having the message "%s" absolute is not supported',
162
					$domain,
163
					$message
164
				));
165
			}
166
167
			$message = Strings::substring($message, 2);
168
		}
169
170
		$tmp = [];
171
		foreach ($parameters as $key => $val) {
172
			$tmp['%' . trim($key, '%') . '%'] = $val;
173
		}
174
		$parameters = $tmp;
175
176
		if ($count !== NULL && is_scalar($count)) {
177
			return $this->transChoice($message, $count, $parameters + ['%count%' => $count], $domain, $locale);
178
		}
179
180
		return $this->trans($message, $parameters, $domain, $locale);
181
	}
182
183
	/**
184
	 * {@inheritdoc}
185
	 */
186
	public function trans($message, array $parameters = [], $domain = NULL, $locale = NULL)
187
	{
188
		if (is_int($message)) {
189
			$message = (string) $message;
190
		}
191
192
		if (!is_string($message)) {
193
			throw new \Kdyby\Translation\InvalidArgumentException(sprintf('Message id must be a string, %s was given', gettype($message)));
194
		}
195
196 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...
197
			list($domain, $id) = $this->extractMessageDomain($message);
198
199
		} else {
200
			$id = $message;
201
		}
202
203
		if ($id === '') {
204
			$id = $message;
205
			$domain = NULL;
206
		}
207
208
		$result = parent::trans($id, $parameters, $domain, $locale);
209
		if ($result === "\x01") {
210
			$this->logMissingTranslation($message, $domain, $locale);
211
			$result = strtr($message, $parameters);
212
		}
213
214
		return $result;
215
	}
216
217
	/**
218
	 * {@inheritdoc}
219
	 */
220
	public function transChoice($message, $number, array $parameters = [], $domain = NULL, $locale = NULL)
221
	{
222
		if (is_int($message)) {
223
			$message = (string) $message;
224
		}
225
226
		if (!is_string($message)) {
227
			throw new \Kdyby\Translation\InvalidArgumentException(sprintf('Message id must be a string, %s was given', gettype($message)));
228
		}
229
230 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...
231
			list($domain, $id) = $this->extractMessageDomain($message);
232
233
		} else {
234
			$id = $message;
235
		}
236
237
		if ($id === '') {
238
			$id = $message;
239
			$domain = NULL;
240
		}
241
242
		try {
243
			$result = parent::transChoice($id, $number, $parameters, $domain, $locale);
244
245
		} catch (\Exception $e) {
246
			$result = $id;
247
			if ($this->panel !== NULL) {
248
				$this->panel->choiceError($e, $domain);
249
			}
250
		}
251
252
		if ($result === "\x01") {
253
			$this->logMissingTranslation($message, $domain, $locale);
254
			if ($locale === NULL) {
255
				$locale = $this->getLocale();
256
			}
257
			if ($locale === NULL) {
258
				$result = strtr($message, $parameters);
259
260
			} else {
261
				if (!$this->formatter instanceof ChoiceMessageFormatterInterface) {
0 ignored issues
show
Bug introduced by
The class Symfony\Component\Transl...ssageFormatterInterface 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...
262
					$result = $id;
263
					if ($this->panel !== NULL) {
264
						$this->panel->choiceError(new \Symfony\Component\Translation\Exception\LogicException(sprintf('The formatter "%s" does not support plural translations.', get_class($this->formatter))), $domain);
265
					}
266
				} else {
267
					$result = $this->formatter->choiceFormat($message, (int) $number, $locale, $parameters);
268
				}
269
			}
270
		}
271
272
		return $result;
273
	}
274
275
	/**
276
	 * @param string $format
277
	 * @param \Symfony\Component\Translation\Loader\LoaderInterface $loader
278
	 */
279
	public function addLoader($format, LoaderInterface $loader)
280
	{
281
		parent::addLoader($format, $loader);
282
		$this->translationsLoader->addLoader($format, $loader);
283
	}
284
285
	/**
286
	 * @return \Symfony\Component\Translation\Loader\LoaderInterface[]
287
	 */
288
	protected function getLoaders()
289
	{
290
		return $this->translationsLoader->getLoaders();
291
	}
292
293
	/**
294
	 * @param array $whitelist
295
	 */
296
	public function setLocaleWhitelist(array $whitelist = NULL)
297
	{
298
		$this->localeWhitelist = self::buildWhitelistRegexp($whitelist);
299
	}
300
301
	/**
302
	 * {@inheritdoc}
303
	 */
304
	public function addResource($format, $resource, $locale, $domain = NULL)
305
	{
306
		if ($this->localeWhitelist !== NULL && !preg_match($this->localeWhitelist, $locale)) {
307
			if ($this->panel !== NULL) {
308
				$this->panel->addIgnoredResource($format, $resource, $locale, $domain);
309
			}
310
			return;
311
		}
312
313
		parent::addResource($format, $resource, $locale, $domain);
314
		$this->catalogueCompiler->addResource($format, $resource, $locale, $domain);
315
		$this->availableResourceLocales[$locale] = TRUE;
316
317
		if ($this->panel !== NULL) {
318
			$this->panel->addResource($format, $resource, $locale, $domain);
319
		}
320
	}
321
322
	/**
323
	 * {@inheritdoc}
324
	 */
325
	public function setFallbackLocales(array $locales)
326
	{
327
		parent::setFallbackLocales($locales);
328
		$this->fallbackResolver->setFallbackLocales($locales);
329
	}
330
331
	/**
332
	 * Returns array of locales from given resources
333
	 *
334
	 * @return array
335
	 */
336
	public function getAvailableLocales()
337
	{
338
		$locales = array_keys($this->availableResourceLocales);
339
		sort($locales);
340
		return $locales;
341
	}
342
343
	/**
344
	 * Sets the current locale.
345
	 *
346
	 * @param string|NULL $locale The locale
347
	 *
348
	 * @throws \InvalidArgumentException If the locale contains invalid characters
349
	 */
350
	public function setLocale($locale)
351
	{
352
		parent::setLocale($locale);
353
	}
354
355
	/**
356
	 * Returns the current locale.
357
	 *
358
	 * @return string|NULL The locale
359
	 */
360
	public function getLocale()
361
	{
362
		if (empty(parent::getLocale())) {
363
			$this->setLocale($this->localeResolver->resolve($this));
364
		}
365
366
		return parent::getLocale();
367
	}
368
369
	/**
370
	 * @return string
371
	 */
372
	public function getDefaultLocale()
373
	{
374
		return $this->defaultLocale;
375
	}
376
377
	/**
378
	 * @param string $locale
379
	 * @return \Kdyby\Translation\Translator
380
	 */
381
	public function setDefaultLocale($locale)
382
	{
383
		$this->assertValidLocale($locale);
384
		$this->defaultLocale = $locale;
385
		return $this;
386
	}
387
388
	/**
389
	 * @param string $messagePrefix
390
	 * @return \Kdyby\Translation\ITranslator
391
	 */
392
	public function domain($messagePrefix)
393
	{
394
		return new PrefixedTranslator($messagePrefix, $this);
395
	}
396
397
	/**
398
	 * @return \Kdyby\Translation\TemplateHelpers
399
	 */
400
	public function createTemplateHelpers()
401
	{
402
		return new TemplateHelpers($this);
403
	}
404
405
	/**
406
	 * {@inheritdoc}
407
	 */
408
	protected function loadCatalogue($locale)
409
	{
410
		if (empty($locale)) {
411
			throw new \Kdyby\Translation\InvalidArgumentException('Invalid locale.');
412
		}
413
414
		if (isset($this->catalogues[$locale])) {
415
			return;
416
		}
417
418
		$this->catalogues = $this->catalogueCompiler->compile($this, $this->catalogues, $locale);
419
	}
420
421
	/**
422
	 * {@inheritdoc}
423
	 */
424
	protected function computeFallbackLocales($locale)
425
	{
426
		return $this->fallbackResolver->compute($this, $locale);
427
	}
428
429
	/**
430
	 * Asserts that the locale is valid, throws an Exception if not.
431
	 *
432
	 * @param string $locale Locale to tests
433
	 * @throws \InvalidArgumentException If the locale contains invalid characters
434
	 */
435
	protected function assertValidLocale($locale)
436
	{
437
		if (preg_match('~^[a-z0-9@_\\.\\-]*\z~i', $locale) !== 1) {
438
			throw new \InvalidArgumentException(sprintf('Invalid "%s" locale.', $locale));
439
		}
440
	}
441
442
	/**
443
	 * @param string $message
444
	 * @return array
445
	 */
446
	private function extractMessageDomain($message)
447
	{
448
		if (strpos($message, '.') !== FALSE && strpos($message, ' ') === FALSE) {
449
			list($domain, $message) = explode('.', $message, 2);
450
451
		} else {
452
			$domain = 'messages';
453
		}
454
455
		return [$domain, $message];
456
	}
457
458
	/**
459
	 * @param string|NULL $message
460
	 * @param string|NULL $domain
461
	 * @param string|NULL $locale
462
	 */
463
	protected function logMissingTranslation($message, $domain, $locale)
464
	{
465
		if ($message === NULL) {
466
			return;
467
		}
468
469
		if ($this->psrLogger !== NULL) {
470
			$this->psrLogger->notice('Missing translation', [
471
				'message' => $message,
472
				'domain' => $domain,
473
				'locale' => $locale ?: $this->getLocale(),
474
			]);
475
		}
476
477
		if ($this->panel !== NULL) {
478
			$this->panel->markUntranslated($message, $domain);
479
		}
480
	}
481
482
	/**
483
	 * @param array|NULL $whitelist
484
	 * @return null|string
485
	 */
486
	public static function buildWhitelistRegexp($whitelist)
487
	{
488
		return ($whitelist !== NULL) ? '~^(' . implode('|', $whitelist) . ')~i' : NULL;
489
	}
490
491
}
492