Completed
Push — master ( 94ba74...5b4930 )
by Filip
04:55
created

Translator::logMissingTranslation()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 18
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 10
nc 5
nop 3
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 Latte;
16
use Nette;
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
	use Kdyby\StrictObjects\Scream;
35
36
	/**
37
	 * @var IUserLocaleResolver
38
	 */
39
	private $localeResolver;
40
41
	/**
42
	 * @var CatalogueCompiler
43
	 */
44
	private $catalogueCompiler;
45
46
	/**
47
	 * @var FallbackResolver
48
	 */
49
	private $fallbackResolver;
50
51
	/**
52
	 * @var IResourceLoader
53
	 */
54
	private $translationsLoader;
55
56
	/**
57
	 * @var LoggerInterface|NULL
58
	 */
59
	private $psrLogger;
60
61
	/**
62
	 * @var Panel|NULL
63
	 */
64
	private $panel;
65
66
	/**
67
	 * @var array
68
	 */
69
	private $availableResourceLocales = [];
70
71
	/**
72
	 * @var string
73
	 */
74
	private $defaultLocale;
75
76
	/**
77
	 * @var string|NULL
78
	 */
79
	private $localeWhitelist;
80
81
	/**
82
	 * @var MessageSelector
83
	 */
84
	private $selector;
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...
85
86
	/**
87
	 * @param IUserLocaleResolver $localeResolver
88
	 * @param MessageSelector $selector The message selector for pluralization
89
	 * @param CatalogueCompiler $catalogueCompiler
90
	 * @param FallbackResolver $fallbackResolver
91
	 * @param IResourceLoader $loader
92
	 * @throws \InvalidArgumentException
93
	 */
94
	public function __construct(
95
		IUserLocaleResolver $localeResolver,
96
		MessageSelector $selector,
97
		CatalogueCompiler $catalogueCompiler,
98
		FallbackResolver $fallbackResolver,
99
		IResourceLoader $loader
100
	)
101
	{
102
		$this->localeResolver = $localeResolver;
103
		$this->selector = $selector;
104
		$this->catalogueCompiler = $catalogueCompiler;
105
		$this->fallbackResolver = $fallbackResolver;
106
		$this->translationsLoader = $loader;
107
108
		parent::__construct('', $selector);
109
		$this->setLocale(NULL);
110
	}
111
112
113
114
	/**
115
	 * @internal
116
	 * @param Panel $panel
117
	 */
118
	public function injectPanel(Panel $panel)
119
	{
120
		$this->panel = $panel;
121
	}
122
123
124
125
	/**
126
	 * @param LoggerInterface|NULL $logger
127
	 */
128
	public function injectPsrLogger(LoggerInterface $logger = NULL)
129
	{
130
		$this->psrLogger = $logger;
131
	}
132
133
134
135
	/**
136
	 * Translates the given string.
137
	 *
138
	 * @param string|\Kdyby\Translation\Phrase|mixed $message The message id
139
	 * @param int|array|NULL $count The number to use to find the indice of the message
140
	 * @param string|array|NULL $parameters An array of parameters for the message
141
	 * @param string|NULL $domain The domain for the message
142
	 * @param string|NULL $locale The locale
143
	 * @throws \InvalidArgumentException
144
	 * @return string|\Nette\Utils\IHtmlString|\Latte\Runtime\IHtmlString
145
	 */
146
	public function translate($message, $count = NULL, $parameters = [], $domain = NULL, $locale = NULL)
147
	{
148
		if ($message instanceof Phrase) {
149
			return $message->translate($this);
150
		}
151
152 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...
153
			$locale = ($domain !== NULL) ? (string) $domain : NULL;
154
			$domain = ($parameters !== NULL && !empty($parameters)) ? (string) $parameters : NULL;
155
			$parameters = $count;
156
			$count = NULL;
157
		}
158
159
		if (empty($message)) {
160
			return $message;
161
162
		} elseif ($message instanceof Nette\Utils\IHtmlString || $message instanceof Latte\Runtime\IHtmlString) {
163
			$this->logMissingTranslation($message->__toString(), $domain, $locale);
164
			return $message; // todo: what now?
165
		}
166
167
		if (!is_string($message)) {
168
			throw new InvalidArgumentException(sprintf('Message id must be a string, %s was given', gettype($message)));
169
		}
170
171
		if (Strings::startsWith($message, '//')) {
172
			if ($domain !== NULL) {
173
				throw new InvalidArgumentException(sprintf(
174
					'Providing domain "%s" while also having the message "%s" absolute is not supported',
175
					$domain,
176
					$message
177
				));
178
			}
179
180
			$message = Strings::substring($message, 2);
181
		}
182
183
		$tmp = [];
184
		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...
185
			$tmp['%' . trim($key, '%') . '%'] = $val;
186
		}
187
		$parameters = $tmp;
188
189
		if ($count !== NULL && is_scalar($count)) {
190
			return $this->transChoice($message, $count, $parameters + ['%count%' => $count], $domain, $locale);
191
		}
192
193
		return $this->trans($message, $parameters, $domain, $locale);
194
	}
195
196
197
198
	/**
199
	 * {@inheritdoc}
200
	 */
201
	public function trans($message, array $parameters = [], $domain = NULL, $locale = NULL)
202
	{
203
		if (!is_string($message)) {
204
			throw new InvalidArgumentException(sprintf('Message id must be a string, %s was given', gettype($message)));
205
		}
206
207 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...
208
			list($domain, $id) = $this->extractMessageDomain($message);
209
210
		} else {
211
			$id = $message;
212
		}
213
214
		$result = parent::trans($id, $parameters, $domain, $locale);
215
		if ($result === "\x01") {
216
			$this->logMissingTranslation($message, $domain, $locale);
217
			$result = strtr($message, $parameters);
218
		}
219
220
		return $result;
221
	}
222
223
224
225
	/**
226
	 * {@inheritdoc}
227
	 */
228
	public function transChoice($message, $number, array $parameters = [], $domain = NULL, $locale = NULL)
229
	{
230
		if (!is_string($message)) {
231
			throw new InvalidArgumentException(sprintf('Message id must be a string, %s was given', gettype($message)));
232
		}
233
234 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...
235
			list($domain, $id) = $this->extractMessageDomain($message);
236
237
		} else {
238
			$id = $message;
239
		}
240
241
		try {
242
			$result = parent::transChoice($id, $number, $parameters, $domain, $locale);
243
244
		} catch (\Exception $e) {
245
			$result = $id;
246
			if ($this->panel !== NULL) {
247
				$this->panel->choiceError($e, $domain);
248
			}
249
		}
250
251
		if ($result === "\x01") {
252
			$this->logMissingTranslation($message, $domain, $locale);
253
			if ($locale === NULL) {
254
				$locale = $this->getLocale();
255
			}
256
			if ($locale === NULL) {
257
				$result = strtr($message, $parameters);
258
259
			} else {
260
				$result = strtr($this->selector->choose($message, (int) $number, $locale), $parameters);
261
			}
262
		}
263
264
		return $result;
265
	}
266
267
268
269
	/**
270
	 * @param string $format
271
	 * @param LoaderInterface $loader
272
	 */
273
	public function addLoader($format, LoaderInterface $loader)
274
	{
275
		parent::addLoader($format, $loader);
276
		$this->translationsLoader->addLoader($format, $loader);
277
	}
278
279
280
281
	/**
282
	 * @return \Symfony\Component\Translation\Loader\LoaderInterface[]
283
	 */
284
	protected function getLoaders()
285
	{
286
		return $this->translationsLoader->getLoaders();
287
	}
288
289
290
291
	/**
292
	 * @param array $whitelist
293
	 * @return Translator
294
	 */
295
	public function setLocaleWhitelist(array $whitelist = NULL)
296
	{
297
		$this->localeWhitelist = self::buildWhitelistRegexp($whitelist);
298
	}
299
300
301
302
	/**
303
	 * {@inheritdoc}
304
	 */
305
	public function addResource($format, $resource, $locale, $domain = NULL)
306
	{
307
		if ($this->localeWhitelist !== NULL && !preg_match($this->localeWhitelist, $locale)) {
308
			if ($this->panel !== NULL) {
309
				$this->panel->addIgnoredResource($format, $resource, $locale, $domain);
310
			}
311
			return;
312
		}
313
314
		parent::addResource($format, $resource, $locale, $domain);
315
		$this->catalogueCompiler->addResource($format, $resource, $locale, $domain);
316
		$this->availableResourceLocales[$locale] = TRUE;
317
318
		if ($this->panel !== NULL) {
319
			$this->panel->addResource($format, $resource, $locale, $domain);
320
		}
321
	}
322
323
324
325
	/**
326
	 * {@inheritdoc}
327
	 */
328
	public function setFallbackLocales(array $locales)
329
	{
330
		parent::setFallbackLocales($locales);
331
		$this->fallbackResolver->setFallbackLocales($locales);
332
	}
333
334
335
336
	/**
337
	 * Returns array of locales from given resources
338
	 *
339
	 * @return array
340
	 */
341
	public function getAvailableLocales()
342
	{
343
		$locales = array_keys($this->availableResourceLocales);
344
		sort($locales);
345
		return $locales;
346
	}
347
348
349
350
	/**
351
	 * Sets the current locale.
352
	 *
353
	 * @param string|NULL $locale The locale
354
	 *
355
	 * @throws \InvalidArgumentException If the locale contains invalid characters
356
	 */
357
	public function setLocale($locale)
358
	{
359
		parent::setLocale($locale);
360
	}
361
362
363
364
	/**
365
	 * Returns the current locale.
366
	 *
367
	 * @return string|NULL The locale
368
	 */
369
	public function getLocale()
370
	{
371
		if (parent::getLocale() === NULL) {
372
			$this->setLocale($this->localeResolver->resolve($this));
373
		}
374
375
		return parent::getLocale();
376
	}
377
378
379
380
	/**
381
	 * @return string
382
	 */
383
	public function getDefaultLocale()
384
	{
385
		return $this->defaultLocale;
386
	}
387
388
389
390
	/**
391
	 * @param string $locale
392
	 * @return Translator
393
	 */
394
	public function setDefaultLocale($locale)
395
	{
396
		$this->assertValidLocale($locale);
397
		$this->defaultLocale = $locale;
398
		return $this;
399
	}
400
401
402
403
	/**
404
	 * @param string $messagePrefix
405
	 * @return ITranslator
406
	 */
407
	public function domain($messagePrefix)
408
	{
409
		return new PrefixedTranslator($messagePrefix, $this);
410
	}
411
412
413
414
	/**
415
	 * @return TemplateHelpers
416
	 */
417
	public function createTemplateHelpers()
418
	{
419
		return new TemplateHelpers($this);
420
	}
421
422
423
424
	/**
425
	 * {@inheritdoc}
426
	 */
427
	protected function loadCatalogue($locale)
428
	{
429
		if (empty($locale)) {
430
			throw new InvalidArgumentException("Invalid locale.");
431
		}
432
433
		if (isset($this->catalogues[$locale])) {
434
			return;
435
		}
436
437
		$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...
438
	}
439
440
441
442
	/**
443
	 * {@inheritdoc}
444
	 */
445
	protected function computeFallbackLocales($locale)
446
	{
447
		return $this->fallbackResolver->compute($this, $locale);
448
	}
449
450
451
452
	/**
453
	 * Asserts that the locale is valid, throws an Exception if not.
454
	 *
455
	 * @param string $locale Locale to tests
456
	 * @throws \InvalidArgumentException If the locale contains invalid characters
457
	 */
458
	protected function assertValidLocale($locale)
459
	{
460
		if (preg_match('~^[a-z0-9@_\\.\\-]*\z~i', $locale) !== 1) {
461
			throw new \InvalidArgumentException(sprintf('Invalid "%s" locale.', $locale));
462
		}
463
	}
464
465
466
467
	/**
468
	 * @param string $message
469
	 * @return array
470
	 */
471
	private function extractMessageDomain($message)
472
	{
473
		if (strpos($message, '.') !== FALSE && strpos($message, ' ') === FALSE) {
474
			list($domain, $message) = explode('.', $message, 2);
475
476
		} else {
477
			$domain = 'messages';
478
		}
479
480
		return [$domain, $message];
481
	}
482
483
484
485
	/**
486
	 * @param string|NULL $message
487
	 * @param string|NULL $domain
488
	 * @param string|NULL $locale
489
	 */
490
	protected function logMissingTranslation($message, $domain, $locale)
491
	{
492
		if ($message === NULL) {
493
			return;
494
		}
495
496
		if ($this->psrLogger !== NULL) {
497
			$this->psrLogger->notice('Missing translation', [
498
				'message' => $message,
499
				'domain' => $domain,
500
				'locale' => $locale ?: $this->getLocale(),
501
			]);
502
		}
503
504
		if ($this->panel !== NULL) {
505
			$this->panel->markUntranslated($message, $domain);
506
		}
507
	}
508
509
510
511
	/**
512
	 * @param array|NULL $whitelist
513
	 * @return null|string
514
	 */
515
	public static function buildWhitelistRegexp($whitelist)
516
	{
517
		return ($whitelist !== NULL) ? '~^(' . implode('|', $whitelist) . ')~i' : NULL;
518
	}
519
520
}
521