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

src/Diagnostics/Panel.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\Diagnostics;
12
13
use Kdyby\Translation\Translator;
14
use Nette\Application\Application;
15
use Nette\Application\Request;
16
use Nette\Reflection\ClassType;
17
use Nette\Utils\Arrays;
18
use Nette\Utils\Html;
19
use Nette\Utils\Strings;
20
use Symfony\Component\Translation\Loader\YamlFileLoader;
21
use Tracy\BlueScreen;
22
use Tracy\Debugger;
23
use Tracy\Helpers;
24
25
class Panel implements \Tracy\IBarPanel
26
{
27
28
	use \Kdyby\StrictObjects\Scream;
29
30
	/**
31
	 * @var \Kdyby\Translation\Translator
32
	 */
33
	private $translator;
34
35
	/**
36
	 * @var array
37
	 */
38
	private $untranslated = [];
39
40
	/**
41
	 * @var array
42
	 */
43
	private $resources = [];
44
45
	/**
46
	 * @var array
47
	 */
48
	private $ignoredResources = [];
49
50
	/**
51
	 * @var array
52
	 */
53
	private $localeWhitelist = [];
54
55
	/**
56
	 * @var array|\Kdyby\Translation\IUserLocaleResolver[]
57
	 */
58
	private $localeResolvers = [];
59
60
	/**
61
	 * @var array
62
	 */
63
	private $onRequestLocaleSnapshot = [];
64
65
	/**
66
	 * @var string
67
	 */
68
	private $rootDir;
69
70
	/**
71
	 * @param string $rootDir
72
	 */
73
	public function __construct($rootDir)
74
	{
75
		$this->rootDir = $rootDir;
76
	}
77
78
	/**
79
	 * Renders HTML code for custom tab.
80
	 *
81
	 * @return string
82
	 */
83
	public function getTab()
84
	{
85
		return '<span title="Translation"><img width="16px" height="16px" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB90DExAuL9uIsPUAAAQ2SURBVFjD7Ze9bxxVFMV/583skjgOIXEaKCNFAgmQ+ExS8qEUCEHH30BBScG/QM1fEmgoAAkoAigSKKFASElHESEFbGcdez7uoXhvZteW7ewmQjRMsTs7s/Pufeece+4d+P/4jw8ddvHq1avv2b5mm02Lr9Y7/gpQBCFDD5YhgsAoRLhHMtHnRYNAAb3yt92//9y3v3x+MFY6LIGIuGYbKedngzAGsPK5Kb8BGRAu9xiuL+zQ6NphsQ5NYB7YhA1jQCObyOHG/3sMlpM0hmGNcmXh7w9PYL5wfsphbCMPaOR7BipEJahV0FHZvZ2D+1i2qY+gYERCZTGZzLdFkLNIht/v72AHGM5Pa85MqowUc4p81PaPo2CgIa9RxOZ8jowDZm0WXo2oBdttN8ihIABC5dPLI7BIQQA5prEGIeYdbnVtSSpD3WCaCCqpMB8jXSshEBFEROZ9TCTvepC/be63HWA2ppNRoJttP1flvkp5BArmNBQl5zSIMJtNRxJEwHqVOF1XGLHVdSQp+0CpAi1UytJVYHteBQ7cm7AgIKnAH7CWhBDrVRqfud91UERLFNPyihQMwaOUIAI5I9D0PU2Xz9frCjs4WVVUMhJstR2SwaUWgvx7VQqyG2b4IoIoO9xsWipllJ6sK1JB7Exd4whmbUcYlCVcRKrVq0DSqCIVRlPZ4SCqO7MHC0Ey1Epiu+k5XafMvbRaFXCgdBxBOIgw202/TyNBtmuHibJRYba6LjuAsgjjCA3UxwWPCEwanRCJ7bYd/eHspFootdwhdvqeJsyegyaCSVK271US2E9BNp8otbTd9STlB89NJvRFmJREJ4K7e0El2Op6NiY1x7GQlpoawihgq+lIZF2s1xWdu9GAcO6Yp6rcE4OcwGDGR7nhUglYRjL32pbOQdMH61VCzgiFc8eUs+2cTIkuoI2MmH20D9TLzEzuTafgwtp0dNXO5FlhsOvSh23zzBPTBfDm4ny0BBBWAInWpSmVmSDzqrEko0TpBwct9W+xGgKL41hWoMeJB/LQsa/JlOYkVDrmAHlpwnF0M6of2gdIC7vUvDUNk1HMbwVRbmo+jA3tOlaYByTYbRJdiG1E3UFFg2IvLxqm67qR1yg71kLflDXvwhHhsxtbSycw2xWffXiPP+/NmFTiGyX21t9m99RbWDCZ1Fy5dAlJeXyz6aOfz48Mzli6aPTp8rMXz/585UVeun7z4WW418Kll0/w9Pk1/t5JTHdh2otpElMl1PX8eP06fdus9ObhQ2g4VBrVxpvvdl/Ovmgacev2Dnf+2GXnxi6zGw/KMJrNJUXPS6fXmCaNw+d8GNV8KHXgp87xyg83tVQCALPvLlvCVUK/3tnh9t3mkw8+/u3Tx3kNu/H6C7z6063lnLBU4dd98NHzF9Z47eKpE4/7Hngw+LHHzveX3zmAyBv/xsvpP+li/lm3bxkuAAAAAElFTkSuQmCC" />'
86
			. $this->translator->getLocale() . ($this->untranslated ? ' <b>(' . count(array_unique($this->untranslated, SORT_REGULAR)) . ' errors)</b>' : '')
87
			. '</span>';
88
	}
89
90
	/**
91
	 * Renders HTML code for custom panel.
92
	 *
93
	 * @return string
94
	 */
95
	public function getPanel()
96
	{
97
		$h = 'htmlSpecialChars';
98
99
		$panel = [];
100
		if (!empty($this->untranslated)) {
101
			$panel[] = $this->renderUntranslated();
102
		}
103
104
		if (!empty($this->onRequestLocaleSnapshot)) {
105
			if (!empty($panel)) {
106
				$panel[] = '<br><br>';
107
			}
108
109
			$panel[] = '<h2>Locale resolution</h2>';
110
			$panel[] = '<p>Order of locale resolvers and final locale for each request</p>';
111
112
			foreach ($this->onRequestLocaleSnapshot as $i => $snapshot) {
113
				$s = $i > 0 ? '<br>' : '';
114
115
				/** @var \Nette\Application\Request[] $snapshot */
116
				$params = $snapshot['request']->getParameters();
117
				$s .= '<tr><th width="10px">&nbsp;</th>' .
118
					'<th>' . $h($snapshot['request']->getPresenterName() . (isset($params['action']) ? ':' . $params['action'] : '')) . '</th>' .
119
					'<th>' . $h($snapshot['locale']) . '</th></tr>';
120
121
				$l = 1;
122
				/** @var mixed[][] $snapshot */
123
				foreach ($snapshot['resolvers'] as $name => $resolvedLocale) {
124
					$s .= '<tr><td>' . ($l++) . '.</td><td>' . $h($name) . '</td><td>' . $h($resolvedLocale) . '</td></tr>';
125
				}
126
127
				$panel[] = '<table style="width:100%">' . $s . '</table>';
128
			}
129
		}
130
131
		if (!empty($this->resources)) {
132
			if (!empty($panel)) {
133
				$panel[] = '<br><br>';
134
			}
135
			$panel[] = '<h2>Loaded resources</h2>';
136
			$panel[] = $this->renderResources($this->resources);
137
		}
138
139
		if (!empty($this->ignoredResources)) {
140
			if (!empty($panel)) {
141
				$panel[] = '<br><br>';
142
			}
143
144
			$panel[] = '<h2>Ignored resources</h2>';
145
			$panel[] = '<p>Whitelist config: ' . implode(', ', array_map($h, $this->localeWhitelist)) . '</p>';
146
			$panel[] = $this->renderResources($this->ignoredResources);
147
		}
148
149
		return empty($panel) ? '' :
150
			'<h1>Missing translations: ' . count(array_unique($this->untranslated, SORT_REGULAR)) .
151
			', Resources: ' . count(Arrays::flatten($this->resources)) . '</h1>' .
152
			'<div class="nette-inner tracy-inner kdyby-TranslationPanel" style="min-width:500px">' . implode($panel) . '</div>' .
153
			'<style>
154
				#nette-debug .kdyby-TranslationPanel h2,
155
				#tracy-debug .kdyby-TranslationPanel h2 {font-size: 23px;}
156
			</style>';
157
	}
158
159
	private function renderUntranslated()
160
	{
161
		$s = '';
162
		$h = 'htmlSpecialChars';
163
164
		foreach ($unique = array_unique($this->untranslated, SORT_REGULAR) as $untranslated) {
165
			$message = $untranslated['message'];
166
167
			$s .= '<tr><td>';
168
169
			if ($message instanceof \Exception || $message instanceof \Throwable) {
170
				$s .= '<span style="color:red">' . $h($message->getMessage()) . '</span>';
171
172
			} elseif ($message instanceof Html) {
173
				$s .= '<span style="color:red">Nette\Utils\Html(' . $h((string) $message) . ')</span>';
174
175
			} else {
176
				$s .= $h($message);
177
			}
178
179
			$s .= '</td><td>' . $h($untranslated['domain']) . '</td></tr>';
180
		}
181
182
		return '<table style="width:100%"><tr><th>Untranslated message</th><th>Translation domain</th></tr>' . $s . '</table>';
183
	}
184
185
	private function renderResources($resourcesMap)
186
	{
187
		$s = '';
188
		$h = 'htmlSpecialChars';
189
190
		ksort($resourcesMap);
191
		foreach ($resourcesMap as $locale => $resources) {
192
			foreach ($resources as $resourcePath => $domain) {
193
				$s .= '<tr>';
194
				$s .= '<td>' . $h($locale) . '</td>';
195
				$s .= '<td>' . $h($domain) . '</td>';
196
197
				$relativePath = str_replace(rtrim($this->rootDir, '/') . '/', '', $resourcePath);
198
				if (Strings::startsWith($relativePath, 'vendor/')) {
199
					$parts = explode('/', $relativePath, 4);
200
					$left = array_pop($parts);
201
					$relativePath = $h(implode('/', $parts) . '/.../') . '<b>' . $h(basename($left)) . '</b>';
202
203
				} else {
204
					$relativePath = $h(dirname($relativePath)) . '/<b>' . $h(basename($relativePath)) . '</b>';
205
				}
206
207
				$s .= '<td>' . self::editorLink($resourcePath, 1, $relativePath) . '</td>';
208
				$s .= '</tr>';
209
			}
210
		}
211
212
		return '<table style="width:100%"><tr><th>Locale</th><th>Domain</th><th>Resource filename</th></tr>' . $s . '</table>';
213
	}
214
215
	/**
216
	 * @param string $id
217
	 * @param string|NULL $domain
218
	 */
219
	public function markUntranslated($id, $domain)
220
	{
221
		$this->untranslated[] = [
222
			'message' => $id,
223
			'domain' => $domain,
224
		];
225
	}
226
227
	/**
228
	 * @param \Exception|\Throwable $e
229
	 * @param string|NULL $domain
230
	 */
231
	public function choiceError($e, $domain)
232
	{
233
		$this->untranslated[] = [
234
			'message' => $e,
235
			'domain' => $domain,
236
		];
237
	}
238
239
	/**
240
	 * @param string|NULL $format
241
	 * @param string|array|NULL $resource
242
	 * @param string|NULL $locale
243
	 * @param string|NULL $domain
244
	 */
245 View Code Duplication
	public function addResource($format, $resource, $locale, $domain)
0 ignored issues
show
The parameter $format is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
246
	{
247
		if (is_array($resource)) {
248
			$resource = 'array ' . md5(serialize($resource));
249
		}
250
251
		$this->resources[$locale][$resource] = $domain;
252
	}
253
254
	public function setLocaleWhitelist($whitelist)
255
	{
256
		$this->localeWhitelist = $whitelist;
257
	}
258
259
	/**
260
	 * @param string|NULL $format
261
	 * @param string|array|NULL $resource
262
	 * @param string|NULL $locale
263
	 * @param string|NULL $domain
264
	 */
265 View Code Duplication
	public function addIgnoredResource($format, $resource, $locale, $domain)
0 ignored issues
show
The parameter $format is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
266
	{
267
		if (is_array($resource)) {
268
			$resource = 'array ' . md5(serialize($resource));
269
		}
270
271
		$this->ignoredResources[$locale][$resource] = $domain;
272
	}
273
274
	public function setLocaleResolvers(array $resolvers)
275
	{
276
		$this->localeResolvers = [];
277
		foreach ($resolvers as $resolver) {
278
			$this->localeResolvers[ClassType::from($resolver)->getShortName()] = $resolver;
279
		}
280
	}
281
282
	public function onRequest(Application $app, Request $request)
0 ignored issues
show
The parameter $app is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
283
	{
284
		if (!$this->translator) {
285
			return;
286
		}
287
288
		$snapshot = ['request' => $request, 'locale' => $this->translator->getLocale(), 'resolvers' => []];
289
		foreach ($this->localeResolvers as $name => $resolver) {
290
			$snapshot['resolvers'][$name] = $resolver->resolve($this->translator);
291
		}
292
293
		$this->onRequestLocaleSnapshot[] = $snapshot;
294
	}
295
296
	public function register(Translator $translator)
297
	{
298
		$this->translator = $translator;
299
		$translator->injectPanel($this);
300
301
		Debugger::getBar()->addPanel($this, 'kdyby.translation');
302
303
		return $this;
304
	}
305
306
	public static function registerBluescreen()
307
	{
308
		Debugger::getBlueScreen()->addPanel([get_called_class(), 'renderException']);
309
	}
310
311
	public static function renderException($e = NULL)
312
	{
313
		if (!$e instanceof \Kdyby\Translation\InvalidResourceException) {
314
			return NULL;
315
		}
316
		$previous = $e->getPrevious();
317
		if ($previous === NULL) {
318
			return NULL;
319
		}
320
321
		$previous = $previous->getPrevious();
322
		if (!$previous instanceof \Symfony\Component\Yaml\Exception\ParseException) {
323
			return NULL;
324
		}
325
326
		$method = YamlFileLoader::class . '::load';
327
		$call = Helpers::findTrace($e->getPrevious()->getTrace(), $method);
328
		if ($call !== NULL) {
329
			return [
330
				'tab' => 'YAML dictionary',
331
				'panel' => '<p><b>File:</b> ' . self::editorLink($call['args'][0], $previous->getParsedLine()) . '</p>'
332
					. ($previous->getParsedLine() ? BlueScreen::highlightFile($call['args'][0], $previous->getParsedLine()) : '')
333
					. '<p>' . $previous->getMessage() . ' </p>',
334
			];
335
		}
336
	}
337
338
	/**
339
	 * Returns link to editor.
340
	 *
341
	 * @return \Nette\Utils\Html|string
342
	 */
343
	private static function editorLink($file, $line, $text = NULL)
344
	{
345
		if (Debugger::$editor && is_file($file) && $text !== NULL) {
346
			return Html::el('a')
347
				->href(strtr(Debugger::$editor, ['%file' => rawurlencode($file), '%line' => $line]))
348
				->setAttribute('title', sprintf('%s:%s', $file, $line))
349
				->setHtml($text);
350
351
		} else {
352
			return Helpers::editorLink($file, $line);
353
		}
354
	}
355
356
}
357