Completed
Pull Request — master (#97)
by Filip
12:53 queued 09:18
created

Translator::logMissingTranslation()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 10
rs 9.4285
cc 3
eloc 6
nc 2
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 Nette;
16
use Nette\Utils\ObjectMixin;
17
use Psr\Log\LoggerInterface;
18
use Symfony\Component\Translation\Loader\LoaderInterface;
19
use Symfony\Component\Translation\MessageSelector;
20
use Symfony\Component\Translation\Translator as BaseTranslator;
21
22
23
24
/**
25
 * Translator.
26
 *
27
 * @author Fabien Potencier <[email protected]>
28
 * @author Filip Procházka <[email protected]>
29
 */
30
class Translator extends BaseTranslator implements ITranslator
31
{
32
33
	/**
34
	 * @var IUserLocaleResolver
35
	 */
36
	private $localeResolver;
37
38
	/**
39
	 * @var CatalogueCompiler
40
	 */
41
	private $catalogueCompiler;
42
43
	/**
44
	 * @var FallbackResolver
45
	 */
46
	private $fallbackResolver;
47
48
	/**
49
	 * @var IResourceLoader
50
	 */
51
	private $translationsLoader;
52
53
	/**
54
	 * @var LoggerInterface
55
	 */
56
	private $psrLogger;
57
58
	/**
59
	 * @var Panel
60
	 */
61
	private $panel;
62
63
	/**
64
	 * @var array
65
	 */
66
	private $availableResourceLocales = array();
67
68
	/**
69
	 * @var string
70
	 */
71
	private $defaultLocale;
72
73
	/**
74
	 * @var string
75
	 */
76
	private $localeWhitelist;
77
78
79
80
	/**
81
	 * @param IUserLocaleResolver $localeResolver
82
	 * @param MessageSelector $selector The message selector for pluralization
83
	 * @param CatalogueCompiler $catalogueCompiler
84
	 * @param FallbackResolver $fallbackResolver
85
	 * @param IResourceLoader $loader
86
	 */
87
	public function __construct(IUserLocaleResolver $localeResolver, MessageSelector $selector,
88
		CatalogueCompiler $catalogueCompiler, FallbackResolver $fallbackResolver, IResourceLoader $loader)
89
	{
90
		$this->localeResolver = $localeResolver;
91
		$this->catalogueCompiler = $catalogueCompiler;
92
		$this->fallbackResolver = $fallbackResolver;
93
		$this->translationsLoader = $loader;
94
95
		parent::__construct(NULL, $selector);
96
	}
97
98
99
100
	/**
101
	 * @internal
102
	 * @param Panel $panel
103
	 */
104
	public function injectPanel(Panel $panel)
105
	{
106
		$this->panel = $panel;
107
	}
108
109
110
111
	/**
112
	 * @param LoggerInterface|NULL $logger
113
	 */
114
	public function injectPsrLogger(LoggerInterface $logger = NULL)
115
	{
116
		$this->psrLogger = $logger;
117
	}
118
119
120
121
	/**
122
	 * Translates the given string.
123
	 *
124
	 * @param string  $message    The message id
125
	 * @param integer $count      The number to use to find the indice of the message
126
	 * @param array   $parameters An array of parameters for the message
127
	 * @param string  $domain     The domain for the message
128
	 * @param string  $locale     The locale
129
	 *
130
	 * @return string
131
	 */
132
	public function translate($message, $count = NULL, $parameters = array(), $domain = NULL, $locale = NULL)
133
	{
134
		if ($message instanceof Phrase) {
135
			return $message->translate($this);
136
		}
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;
140
			$domain = $parameters ?: NULL;
141
			$parameters = $count;
142
			$count = NULL;
143
		}
144
145
		if (empty($message)) {
146
			return $message;
147
148
		} elseif ($message instanceof Nette\Utils\Html) {
149
			if ($this->panel) {
150
				$this->panel->markUntranslated($message);
151
			}
152
			return $message; // todo: what now?
153
		}
154
155
		$tmp = array();
156
		foreach ($parameters as $key => $val) {
157
			$tmp['%' . trim($key, '%') . '%'] = $val;
158
		}
159
		$parameters = $tmp;
160
161
		if ($count !== NULL && is_scalar($count)) {
162
			return $this->transChoice($message, $count, $parameters + array('%count%' => $count), $domain, $locale);
0 ignored issues
show
Bug introduced by
It seems like $domain defined by $parameters ?: NULL on line 140 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...
163
		}
164
165
		return $this->trans($message, $parameters, $domain, $locale);
0 ignored issues
show
Bug introduced by
It seems like $domain defined by $parameters ?: NULL on line 140 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...
166
	}
167
168
169
170
	/**
171
	 * {@inheritdoc}
172
	 */
173
	public function trans($message, array $parameters = array(), $domain = NULL, $locale = NULL)
174
	{
175
		if ($message instanceof Phrase) {
176
			return $message->translate($this);
177
		}
178
179 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...
180
			list($domain, $id) = $this->extractMessageDomain($message);
181
182
		} else {
183
			$id = $message;
184
		}
185
186
		$result = parent::trans($id, $parameters, $domain, $locale);
187 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...
188
			$this->logMissingTranslation($message, $domain, $locale);
189
			if ($this->panel !== NULL) {
190
				$this->panel->markUntranslated($message);
191
			}
192
			$result = strtr($message, $parameters);
193
		}
194
195
		return $result;
196
	}
197
198
199
200
	/**
201
	 * {@inheritdoc}
202
	 */
203
	public function transChoice($message, $number, array $parameters = array(), $domain = NULL, $locale = NULL)
204
	{
205
		if ($message instanceof Phrase) {
206
			return $message->translate($this);
207
		}
208
209 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...
210
			list($domain, $id) = $this->extractMessageDomain($message);
211
212
		} else {
213
			$id = $message;
214
		}
215
216
		try {
217
			$result = parent::transChoice($id, $number, $parameters, $domain, $locale);
218
219
		} catch (\Exception $e) {
220
			$result = $id;
221
			if ($this->panel !== NULL) {
222
				$this->panel->choiceError($e);
223
			}
224
		}
225
226 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...
227
			$this->logMissingTranslation($message, $domain, $locale);
228
			if ($this->panel !== NULL) {
229
				$this->panel->markUntranslated($message);
230
			}
231
			$result = strtr($message, $parameters);
232
		}
233
234
		return $result;
235
	}
236
237
238
239
	/**
240
	 * @param string $format
241
	 * @param LoaderInterface $loader
242
	 */
243
	public function addLoader($format, LoaderInterface $loader)
244
	{
245
		parent::addLoader($format, $loader);
246
		$this->translationsLoader->addLoader($format, $loader);
247
	}
248
249
250
251
	/**
252
	 * @return \Symfony\Component\Translation\Loader\LoaderInterface[]
253
	 */
254
	protected function getLoaders()
255
	{
256
		return $this->translationsLoader->getLoaders();
257
	}
258
259
260
261
	/**
262
	 * @param array $whitelist
263
	 * @return Translator
264
	 */
265
	public function setLocaleWhitelist(array $whitelist = NULL)
266
	{
267
		$this->localeWhitelist = self::buildWhitelistRegexp($whitelist);
0 ignored issues
show
Bug introduced by
It seems like $whitelist defined by parameter $whitelist on line 265 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...
268
	}
269
270
271
272
	/**
273
	 * {@inheritdoc}
274
	 */
275
	public function addResource($format, $resource, $locale, $domain = NULL)
276
	{
277
		if ($this->localeWhitelist && !preg_match($this->localeWhitelist, $locale)) {
278
			if ($this->panel) {
279
				$this->panel->addIgnoredResource($format, $resource, $locale, $domain);
280
			}
281
			return;
282
		}
283
284
		parent::addResource($format, $resource, $locale, $domain);
285
		$this->catalogueCompiler->addResource($format, $resource, $locale, $domain);
286
		$this->availableResourceLocales[$locale] = TRUE;
287
288
		if ($this->panel) {
289
			$this->panel->addResource($format, $resource, $locale, $domain);
290
		}
291
	}
292
293
294
295
	/**
296
	 * {@inheritdoc}
297
	 */
298
	public function setFallbackLocales(array $locales)
299
	{
300
		parent::setFallbackLocales($locales);
301
		$this->fallbackResolver->setFallbackLocales($locales);
302
	}
303
304
305
306
	/**
307
	 * Returns array of locales from given resources
308
	 *
309
	 * @return array
310
	 */
311
	public function getAvailableLocales()
312
	{
313
		$locales = array_keys($this->availableResourceLocales);
314
		sort($locales);
315
		return $locales;
316
	}
317
318
319
320
	/**
321
	 * {@inheritdoc}
322
	 */
323
	public function getLocale()
324
	{
325
		if ($this->locale === NULL) {
326
			$this->setLocale($this->localeResolver->resolve($this));
327
		}
328
329
		return $this->locale;
330
	}
331
332
333
334
	/**
335
	 * @return string
336
	 */
337
	public function getDefaultLocale()
338
	{
339
		return $this->defaultLocale;
340
	}
341
342
343
344
	/**
345
	 * @param string $locale
346
	 * @return Translator
347
	 */
348
	public function setDefaultLocale($locale)
349
	{
350
		$this->assertValidLocale($locale);
351
		$this->defaultLocale = $locale;
352
		return $this;
353
	}
354
355
356
357
	/**
358
	 * @param string $messagePrefix
359
	 * @return ITranslator
360
	 */
361
	public function domain($messagePrefix)
362
	{
363
		return new PrefixedTranslator($messagePrefix, $this);
364
	}
365
366
367
368
	/**
369
	 * @return TemplateHelpers
370
	 */
371
	public function createTemplateHelpers()
372
	{
373
		return new TemplateHelpers($this);
374
	}
375
376
377
378
	/**
379
	 * {@inheritdoc}
380
	 */
381
	protected function loadCatalogue($locale)
382
	{
383
		if (empty($locale)) {
384
			throw new InvalidArgumentException("Invalid locale.");
385
		}
386
387
		if (isset($this->catalogues[$locale])) {
388
			return;
389
		}
390
391
		$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...
392
	}
393
394
395
396
	/**
397
	 * {@inheritdoc}
398
	 */
399
	protected function computeFallbackLocales($locale)
400
	{
401
		return $this->fallbackResolver->compute($this, $locale);
402
	}
403
404
405
406
	/**
407
	 * Asserts that the locale is valid, throws an Exception if not.
408
	 *
409
	 * @param string $locale Locale to tests
410
	 * @throws \InvalidArgumentException If the locale contains invalid characters
411
	 */
412
	protected function assertValidLocale($locale)
413
	{
414
		if (preg_match('~^[a-z0-9@_\\.\\-]*\z~i', $locale) !== 1) {
415
			throw new \InvalidArgumentException(sprintf('Invalid "%s" locale.', $locale));
416
		}
417
	}
418
419
420
421
	/**
422
	 * @param $message
423
	 * @return array
424
	 */
425
	private function extractMessageDomain($message)
426
	{
427
		if (strpos($message, '.') !== FALSE && strpos($message, ' ') === FALSE) {
428
			list($domain, $message) = explode('.', $message, 2);
429
430
		} else {
431
			$domain = 'messages';
432
		}
433
434
		return array($domain, $message);
435
	}
436
437
438
439
	/**
440
	 * @param string $message
441
	 * @param string $domain
442
	 * @param string $locale
443
	 */
444
	protected function logMissingTranslation($message, $domain, $locale)
445
	{
446
		if ($this->psrLogger) {
447
			$this->psrLogger->notice('Missing translation', [
448
				'message' => $message,
449
				'domain' => $domain,
450
				'locale' => $locale ?: $this->getLocale(),
451
			]);
452
		}
453
	}
454
455
456
457
	/**
458
	 * @param null|string $whitelist
459
	 * @return null|string
460
	 */
461
	public static function buildWhitelistRegexp($whitelist)
462
	{
463
		return $whitelist ? '~^(' . implode('|', $whitelist) . ')~i' : NULL;
464
	}
465
466
467
468
	/*************************** Nette\Object ***************************/
469
470
471
472
	/**
473
	 * Access to reflection.
474
	 * @return \Nette\Reflection\ClassType
475
	 */
476
	public static function getReflection()
477
	{
478
		return new Nette\Reflection\ClassType(get_called_class());
479
	}
480
481
482
483
	/**
484
	 * Call to undefined method.
485
	 *
486
	 * @param string $name
487
	 * @param array $args
488
	 *
489
	 * @throws \Nette\MemberAccessException
490
	 * @return mixed
491
	 */
492
	public function __call($name, $args)
493
	{
494
		return ObjectMixin::call($this, $name, $args);
495
	}
496
497
498
499
	/**
500
	 * Call to undefined static method.
501
	 *
502
	 * @param string $name
503
	 * @param array $args
504
	 *
505
	 * @throws \Nette\MemberAccessException
506
	 * @return mixed
507
	 */
508
	public static function __callStatic($name, $args)
509
	{
510
		return ObjectMixin::callStatic(get_called_class(), $name, $args);
511
	}
512
513
514
515
	/**
516
	 * Adding method to class.
517
	 *
518
	 * @param $name
519
	 * @param null $callback
520
	 *
521
	 * @throws \Nette\MemberAccessException
522
	 * @return callable|null
523
	 */
524
	public static function extensionMethod($name, $callback = NULL)
525
	{
526
		if (strpos($name, '::') === FALSE) {
527
			$class = get_called_class();
528
		} else {
529
			list($class, $name) = explode('::', $name);
530
		}
531
		if ($callback === NULL) {
532
			return ObjectMixin::getExtensionMethod($class, $name);
533
		} else {
534
			ObjectMixin::setExtensionMethod($class, $name, $callback);
535
		}
536
	}
537
538
539
540
	/**
541
	 * Returns property value. Do not call directly.
542
	 *
543
	 * @param string $name
544
	 *
545
	 * @throws \Nette\MemberAccessException
546
	 * @return mixed
547
	 */
548
	public function &__get($name)
549
	{
550
		return ObjectMixin::get($this, $name);
551
	}
552
553
554
555
	/**
556
	 * Sets value of a property. Do not call directly.
557
	 *
558
	 * @param string $name
559
	 * @param mixed $value
560
	 *
561
	 * @throws \Nette\MemberAccessException
562
	 * @return void
563
	 */
564
	public function __set($name, $value)
565
	{
566
		ObjectMixin::set($this, $name, $value);
567
	}
568
569
570
571
	/**
572
	 * Is property defined?
573
	 *
574
	 * @param string $name
575
	 *
576
	 * @return bool
577
	 */
578
	public function __isset($name)
579
	{
580
		return ObjectMixin::has($this, $name);
581
	}
582
583
584
585
	/**
586
	 * Access to undeclared property.
587
	 *
588
	 * @param string $name
589
	 *
590
	 * @throws \Nette\MemberAccessException
591
	 * @return void
592
	 */
593
	public function __unset($name)
594
	{
595
		ObjectMixin::remove($this, $name);
596
	}
597
598
}
599