1 | <?php |
||||||
2 | |||||||
3 | declare(strict_types=1); |
||||||
4 | |||||||
5 | /* |
||||||
6 | Copyright (c) 2003, 2009 Danilo Segan <[email protected]>. |
||||||
7 | Copyright (c) 2005 Nico Kaiser <[email protected]> |
||||||
8 | Copyright (c) 2016 Michal Čihař <[email protected]> |
||||||
9 | |||||||
10 | This file is part of MoTranslator. |
||||||
11 | |||||||
12 | This program is free software; you can redistribute it and/or modify |
||||||
13 | it under the terms of the GNU General Public License as published by |
||||||
14 | the Free Software Foundation; either version 2 of the License, or |
||||||
15 | (at your option) any later version. |
||||||
16 | |||||||
17 | This program is distributed in the hope that it will be useful, |
||||||
18 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
19 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
20 | GNU General Public License for more details. |
||||||
21 | |||||||
22 | You should have received a copy of the GNU General Public License along |
||||||
23 | with this program; if not, write to the Free Software Foundation, Inc., |
||||||
24 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
||||||
25 | */ |
||||||
26 | |||||||
27 | namespace PhpMyAdmin\MoTranslator; |
||||||
28 | |||||||
29 | use PhpMyAdmin\MoTranslator\Cache\CacheInterface; |
||||||
30 | use PhpMyAdmin\MoTranslator\Cache\GetAllInterface; |
||||||
31 | use PhpMyAdmin\MoTranslator\Cache\InMemoryCache; |
||||||
32 | use Symfony\Component\ExpressionLanguage\ExpressionLanguage; |
||||||
33 | use Throwable; |
||||||
34 | |||||||
35 | use function array_key_exists; |
||||||
36 | use function count; |
||||||
37 | use function explode; |
||||||
38 | use function is_numeric; |
||||||
39 | use function ltrim; |
||||||
40 | use function preg_replace; |
||||||
41 | use function rtrim; |
||||||
42 | use function sprintf; |
||||||
43 | use function str_contains; |
||||||
44 | use function str_starts_with; |
||||||
45 | use function stripos; |
||||||
46 | use function strtolower; |
||||||
47 | use function substr; |
||||||
48 | use function trim; |
||||||
49 | |||||||
50 | /** |
||||||
51 | * Provides a simple gettext replacement that works independently from |
||||||
52 | * the system's gettext abilities. |
||||||
53 | * It can read MO files and use them for translating strings. |
||||||
54 | * |
||||||
55 | * It caches ll strings and translations to speed up the string lookup. |
||||||
56 | */ |
||||||
57 | class Translator |
||||||
58 | { |
||||||
59 | /** |
||||||
60 | * None error. |
||||||
61 | */ |
||||||
62 | public const ERROR_NONE = 0; |
||||||
63 | |||||||
64 | /** |
||||||
65 | * File does not exist. |
||||||
66 | */ |
||||||
67 | public const ERROR_DOES_NOT_EXIST = 1; |
||||||
68 | |||||||
69 | /** |
||||||
70 | * File has bad magic number. |
||||||
71 | */ |
||||||
72 | public const ERROR_BAD_MAGIC = 2; |
||||||
73 | |||||||
74 | /** |
||||||
75 | * Error while reading file, probably too short. |
||||||
76 | */ |
||||||
77 | public const ERROR_READING = 3; |
||||||
78 | |||||||
79 | /** |
||||||
80 | * Big endian mo file magic bytes. |
||||||
81 | */ |
||||||
82 | public const MAGIC_BE = "\x95\x04\x12\xde"; |
||||||
83 | |||||||
84 | /** |
||||||
85 | * Little endian mo file magic bytes. |
||||||
86 | */ |
||||||
87 | public const MAGIC_LE = "\xde\x12\x04\x95"; |
||||||
88 | |||||||
89 | /** |
||||||
90 | * Parse error code (0 if no error). |
||||||
91 | */ |
||||||
92 | public int $error = self::ERROR_NONE; |
||||||
93 | |||||||
94 | /** |
||||||
95 | * Cache header field for plural forms. |
||||||
96 | */ |
||||||
97 | private string|null $pluralEquation = null; |
||||||
98 | |||||||
99 | /** |
||||||
100 | * Evaluator for plurals |
||||||
101 | */ |
||||||
102 | private ExpressionLanguage|null $pluralExpression = null; |
||||||
103 | |||||||
104 | /** |
||||||
105 | * number of plurals |
||||||
106 | */ |
||||||
107 | private int|null $pluralCount = null; |
||||||
108 | |||||||
109 | private CacheInterface $cache; |
||||||
110 | |||||||
111 | /** @param CacheInterface|string|null $cache Mo file to load (null for no file) or a CacheInterface implementation */ |
||||||
112 | 826 | public function __construct(CacheInterface|string|null $cache) |
|||||
113 | { |
||||||
114 | 826 | if (! $cache instanceof CacheInterface) { |
|||||
115 | 28 | $cache = new InMemoryCache(new MoParser($cache)); |
|||||
116 | } |
||||||
117 | |||||||
118 | 826 | $this->cache = $cache; |
|||||
119 | 118 | } |
|||||
120 | |||||||
121 | /** |
||||||
122 | * Translates a string. |
||||||
123 | * |
||||||
124 | * @param string $msgid String to be translated |
||||||
125 | * |
||||||
126 | * @return string translated string (or original, if not found) |
||||||
127 | */ |
||||||
128 | 476 | public function gettext(string $msgid): string |
|||||
129 | { |
||||||
130 | 476 | return $this->cache->get($msgid); |
|||||
131 | } |
||||||
132 | |||||||
133 | /** |
||||||
134 | * Check if a string is translated. |
||||||
135 | * |
||||||
136 | * @param string $msgid String to be checked |
||||||
137 | */ |
||||||
138 | 84 | public function exists(string $msgid): bool |
|||||
139 | { |
||||||
140 | 84 | return $this->cache->has($msgid); |
|||||
141 | } |
||||||
142 | |||||||
143 | /** |
||||||
144 | * Sanitize plural form expression for use in ExpressionLanguage. |
||||||
145 | * |
||||||
146 | * @param string $expr Expression to sanitize |
||||||
147 | * |
||||||
148 | * @return string sanitized plural form expression |
||||||
149 | */ |
||||||
150 | 322 | public static function sanitizePluralExpression(string $expr): string |
|||||
151 | { |
||||||
152 | // Parse equation |
||||||
153 | 322 | $expr = explode(';', $expr); |
|||||
154 | 322 | $expr = count($expr) >= 2 ? $expr[1] : $expr[0]; |
|||||
155 | 280 | ||||||
156 | $expr = trim(strtolower($expr)); |
||||||
157 | 42 | // Strip plural prefix |
|||||
158 | if (str_starts_with($expr, 'plural')) { |
||||||
159 | $expr = ltrim(substr($expr, 6)); |
||||||
160 | 322 | } |
|||||
161 | |||||||
162 | 322 | // Strip equals |
|||||
163 | 294 | if (str_starts_with($expr, '=')) { |
|||||
164 | $expr = ltrim(substr($expr, 1)); |
||||||
165 | } |
||||||
166 | |||||||
167 | 322 | // Cleanup from unwanted chars |
|||||
168 | 294 | $expr = preg_replace('@[^n0-9:\(\)\?=!<>/%&| ]@', '', $expr); |
|||||
169 | |||||||
170 | return (string) $expr; |
||||||
171 | } |
||||||
172 | 322 | ||||||
173 | /** |
||||||
174 | 322 | * Extracts number of plurals from plurals form expression. |
|||||
175 | * |
||||||
176 | * @param string $expr Expression to process |
||||||
177 | * |
||||||
178 | * @return int Total number of plurals |
||||||
179 | */ |
||||||
180 | public static function extractPluralCount(string $expr): int |
||||||
181 | { |
||||||
182 | $parts = explode(';', $expr, 2); |
||||||
183 | $nplurals = explode('=', trim($parts[0]), 2); |
||||||
184 | 308 | if (strtolower(rtrim($nplurals[0])) !== 'nplurals') { |
|||||
185 | return 1; |
||||||
186 | 308 | } |
|||||
187 | 308 | ||||||
188 | 308 | if (count($nplurals) === 1) { |
|||||
189 | 28 | return 1; |
|||||
190 | } |
||||||
191 | |||||||
192 | 280 | return (int) $nplurals[1]; |
|||||
193 | 14 | } |
|||||
194 | |||||||
195 | /** |
||||||
196 | 266 | * Parse full PO header and extract only plural forms line. |
|||||
197 | * |
||||||
198 | * @param string $header Gettext header |
||||||
199 | * |
||||||
200 | * @return string verbatim plural form header field |
||||||
201 | */ |
||||||
202 | public static function extractPluralsForms(string $header): string |
||||||
203 | { |
||||||
204 | $headers = explode("\n", $header); |
||||||
205 | $expr = 'nplurals=2; plural=n == 1 ? 0 : 1;'; |
||||||
206 | 280 | foreach ($headers as $header) { |
|||||
207 | if (stripos($header, 'Plural-Forms:') !== 0) { |
||||||
208 | 280 | continue; |
|||||
209 | 280 | } |
|||||
210 | 280 | ||||||
211 | 280 | $expr = substr($header, 13); |
|||||
212 | 280 | } |
|||||
213 | |||||||
214 | return $expr; |
||||||
215 | 210 | } |
|||||
216 | |||||||
217 | /** |
||||||
218 | 280 | * Get possible plural forms from MO header. |
|||||
219 | * |
||||||
220 | * @return string plural form header |
||||||
221 | */ |
||||||
222 | private function getPluralForms(): string |
||||||
223 | { |
||||||
224 | // lets assume message number 0 is header |
||||||
225 | // this is true, right? |
||||||
226 | 238 | ||||||
227 | // cache header field for plural forms |
||||||
228 | if ($this->pluralEquation === null) { |
||||||
229 | $header = $this->cache->get(''); |
||||||
230 | |||||||
231 | $expr = self::extractPluralsForms($header); |
||||||
232 | 238 | $this->pluralEquation = self::sanitizePluralExpression($expr); |
|||||
233 | 224 | $this->pluralCount = self::extractPluralCount($expr); |
|||||
234 | } |
||||||
235 | 224 | ||||||
236 | 224 | return $this->pluralEquation; |
|||||
0 ignored issues
–
show
Bug
Best Practice
introduced
by
![]() |
|||||||
237 | 224 | } |
|||||
238 | |||||||
239 | /** |
||||||
240 | 238 | * Detects which plural form to take. |
|||||
241 | * |
||||||
242 | * @param int $n count of objects |
||||||
243 | * |
||||||
244 | * @return int array index of the right plural form |
||||||
245 | */ |
||||||
246 | private function selectString(int $n): int |
||||||
247 | { |
||||||
248 | if ($this->pluralExpression === null) { |
||||||
249 | $this->pluralExpression = new ExpressionLanguage(); |
||||||
250 | 238 | } |
|||||
251 | |||||||
252 | 238 | try { |
|||||
253 | 224 | $evaluatedPlural = $this->pluralExpression->evaluate($this->getPluralForms(), ['n' => $n]); |
|||||
0 ignored issues
–
show
The method
evaluate() does not exist on null .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. ![]() |
|||||||
254 | $plural = is_numeric($evaluatedPlural) ? (int) $evaluatedPlural : 0; |
||||||
255 | } catch (Throwable) { |
||||||
256 | $plural = 0; |
||||||
257 | 238 | } |
|||||
258 | 238 | ||||||
259 | 34 | if ($plural >= $this->pluralCount) { |
|||||
260 | $plural = $this->pluralCount - 1; |
||||||
261 | 14 | } |
|||||
262 | 14 | ||||||
263 | return $plural; |
||||||
264 | } |
||||||
265 | 238 | ||||||
266 | 14 | /** |
|||||
267 | * Plural version of gettext. |
||||||
268 | * |
||||||
269 | 238 | * @param string $msgid Single form |
|||||
270 | * @param string $msgidPlural Plural form |
||||||
271 | * @param int $number Number of objects |
||||||
272 | * |
||||||
273 | * @return string translated plural form |
||||||
274 | */ |
||||||
275 | public function ngettext(string $msgid, string $msgidPlural, int $number): string |
||||||
276 | { |
||||||
277 | // this should contains all strings separated by NULLs |
||||||
278 | $key = $msgid . "\u{0}" . $msgidPlural; |
||||||
279 | if (! $this->cache->has($key)) { |
||||||
280 | return $number !== 1 ? $msgidPlural : $msgid; |
||||||
281 | 378 | } |
|||||
282 | |||||||
283 | $result = $this->cache->get($key); |
||||||
284 | 378 | ||||||
285 | 378 | // find out the appropriate form |
|||||
286 | 252 | $select = $this->selectString($number); |
|||||
287 | |||||||
288 | $list = explode("\u{0}", $result); |
||||||
289 | 238 | ||||||
290 | if (array_key_exists($select, $list)) { |
||||||
291 | return $list[$select]; |
||||||
292 | 238 | } |
|||||
293 | |||||||
294 | 238 | return $list[0]; |
|||||
295 | } |
||||||
296 | |||||||
297 | /** |
||||||
298 | * Translate with context. |
||||||
299 | * |
||||||
300 | * @param string $msgctxt Context |
||||||
301 | * @param string $msgid String to be translated |
||||||
302 | * |
||||||
303 | * @return string translated plural form |
||||||
304 | 238 | */ |
|||||
305 | 14 | public function pgettext(string $msgctxt, string $msgid): string |
|||||
306 | { |
||||||
307 | $key = $msgctxt . "\u{4}" . $msgid; |
||||||
308 | 238 | $ret = $this->gettext($key); |
|||||
309 | if ($ret === $key) { |
||||||
310 | return $msgid; |
||||||
311 | } |
||||||
312 | |||||||
313 | return $ret; |
||||||
314 | } |
||||||
315 | |||||||
316 | /** |
||||||
317 | * Plural version of pgettext. |
||||||
318 | * |
||||||
319 | 196 | * @param string $msgctxt Context |
|||||
320 | * @param string $msgid Single form |
||||||
321 | 196 | * @param string $msgidPlural Plural form |
|||||
322 | 196 | * @param int $number Number of objects |
|||||
323 | 196 | * |
|||||
324 | 84 | * @return string translated plural form |
|||||
325 | */ |
||||||
326 | public function npgettext(string $msgctxt, string $msgid, string $msgidPlural, int $number): string |
||||||
327 | 112 | { |
|||||
328 | $key = $msgctxt . "\u{4}" . $msgid; |
||||||
329 | $ret = $this->ngettext($key, $msgidPlural, $number); |
||||||
330 | if (str_contains($ret, "\u{4}")) { |
||||||
331 | return $msgid; |
||||||
332 | } |
||||||
333 | |||||||
334 | return $ret; |
||||||
335 | } |
||||||
336 | |||||||
337 | /** |
||||||
338 | * Set translation in place |
||||||
339 | * |
||||||
340 | 56 | * @param string $msgid String to be set |
|||||
341 | * @param string $msgstr Translation |
||||||
342 | 56 | */ |
|||||
343 | 56 | public function setTranslation(string $msgid, string $msgstr): void |
|||||
344 | 56 | { |
|||||
345 | 14 | $this->cache->set($msgid, $msgstr); |
|||||
346 | } |
||||||
347 | |||||||
348 | 42 | /** |
|||||
349 | * Set the translations |
||||||
350 | * |
||||||
351 | * @param array<string,string> $translations The translations "key => value" array |
||||||
352 | */ |
||||||
353 | public function setTranslations(array $translations): void |
||||||
354 | { |
||||||
355 | $this->cache->setAll($translations); |
||||||
356 | } |
||||||
357 | 140 | ||||||
358 | /** |
||||||
359 | 140 | * Get the translations |
|||||
360 | 20 | * |
|||||
361 | * @return array<string,string> The translations "key => value" array |
||||||
362 | */ |
||||||
363 | public function getTranslations(): array |
||||||
364 | { |
||||||
365 | if ($this->cache instanceof GetAllInterface) { |
||||||
366 | return $this->cache->getAll(); |
||||||
0 ignored issues
–
show
The method
getAll() does not exist on PhpMyAdmin\MoTranslator\Cache\CacheInterface . It seems like you code against a sub-type of PhpMyAdmin\MoTranslator\Cache\CacheInterface such as PhpMyAdmin\MoTranslator\Cache\InMemoryCache .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
367 | 14 | } |
|||||
368 | |||||||
369 | 14 | throw new CacheException(sprintf( |
|||||
370 | 2 | "Cache '%s' does not support getting translations", |
|||||
371 | $this->cache::class, |
||||||
372 | )); |
||||||
373 | } |
||||||
374 | } |
||||||
375 |