Passed
Push — master ( 122983...91227d )
by William
02:34 queued 26s
created

Translator::loadTranslationsFromFile()   B

Complexity

Conditions 6
Paths 24

Size

Total Lines 45
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 29
c 1
b 0
f 0
dl 0
loc 45
ccs 28
cts 28
cp 1
rs 8.8337
cc 6
nc 24
nop 1
crap 6

1 Method

Rating   Name   Duplication   Size   Complexity  
A Translator::exists() 0 3 1
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 chr;
36
use function count;
37
use function explode;
38
use function get_class;
39
use function implode;
40
use function intval;
41
use function ltrim;
42
use function preg_replace;
43
use function rtrim;
44
use function sprintf;
45
use function stripos;
46
use function strpos;
47
use function strtolower;
48
use function substr;
49
use function trim;
50
51
/**
52
 * Provides a simple gettext replacement that works independently from
53
 * the system's gettext abilities.
54
 * It can read MO files and use them for translating strings.
55
 *
56
 * It caches ll strings and translations to speed up the string lookup.
57
 */
58
class Translator
59
{
60
    /**
61
     * None error.
62
     */
63
    public const ERROR_NONE = 0;
64
    /**
65
     * File does not exist.
66
     */
67
    public const ERROR_DOES_NOT_EXIST = 1;
68
    /**
69
     * File has bad magic number.
70
     */
71
    public const ERROR_BAD_MAGIC = 2;
72
    /**
73
     * Error while reading file, probably too short.
74
     */
75
    public const ERROR_READING = 3;
76
77
    /**
78
     * Big endian mo file magic bytes.
79
     */
80
    public const MAGIC_BE = "\x95\x04\x12\xde";
81
    /**
82
     * Little endian mo file magic bytes.
83
     */
84
    public const MAGIC_LE = "\xde\x12\x04\x95";
85
86
    /**
87
     * Parse error code (0 if no error).
88
     *
89
     * @var int
90
     */
91
    public $error = self::ERROR_NONE;
92
93
    /**
94
     * Cache header field for plural forms.
95
     *
96
     * @var string|null
97
     */
98
    private $pluralEquation = null;
99
100
    /** @var ExpressionLanguage|null Evaluator for plurals */
101
    private $pluralExpression = null;
102
103
    /** @var int|null number of plurals */
104
    private $pluralCount = null;
105
106
    /** @var CacheInterface */
107
    private $cache;
108
109
    /**
110
     * @param CacheInterface|string|null $cache Mo file to load (null for no file) or a CacheInterface implementation
111
     */
112 236
    public function __construct($cache)
113
    {
114 236
        if (! $cache instanceof CacheInterface) {
115 8
            $cache = new InMemoryCache(new MoParser($cache));
116
        }
117
118 236
        $this->cache = $cache;
119 59
    }
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 136
    public function gettext(string $msgid): string
129
    {
130 136
        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 24
    public function exists(string $msgid): bool
139
    {
140 24
        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 92
    public static function sanitizePluralExpression(string $expr): string
151
    {
152
        // Parse equation
153 92
        $expr = explode(';', $expr);
154 92
        if (count($expr) >= 2) {
155 80
            $expr = $expr[1];
156
        } else {
157 12
            $expr = $expr[0];
158
        }
159
160 92
        $expr = trim(strtolower($expr));
161
        // Strip plural prefix
162 92
        if (substr($expr, 0, 6) === 'plural') {
163 84
            $expr = ltrim(substr($expr, 6));
164
        }
165
166
        // Strip equals
167 92
        if (substr($expr, 0, 1) === '=') {
168 84
            $expr = ltrim(substr($expr, 1));
169
        }
170
171
        // Cleanup from unwanted chars
172 92
        $expr = preg_replace('@[^n0-9:\(\)\?=!<>/%&| ]@', '', $expr);
173
174 92
        return (string) $expr;
175
    }
176
177
    /**
178
     * Extracts number of plurals from plurals form expression.
179
     *
180
     * @param string $expr Expression to process
181
     *
182
     * @return int Total number of plurals
183
     */
184 88
    public static function extractPluralCount(string $expr): int
185
    {
186 88
        $parts = explode(';', $expr, 2);
187 88
        $nplurals = explode('=', trim($parts[0]), 2);
188 88
        if (strtolower(rtrim($nplurals[0])) !== 'nplurals') {
189 8
            return 1;
190
        }
191
192 80
        if (count($nplurals) === 1) {
193 4
            return 1;
194
        }
195
196 76
        return intval($nplurals[1]);
197
    }
198
199
    /**
200
     * Parse full PO header and extract only plural forms line.
201
     *
202
     * @param string $header Gettext header
203
     *
204
     * @return string verbatim plural form header field
205
     */
206 80
    public static function extractPluralsForms(string $header): string
207
    {
208 80
        $headers = explode("\n", $header);
209 80
        $expr = 'nplurals=2; plural=n == 1 ? 0 : 1;';
210 80
        foreach ($headers as $header) {
211 80
            if (stripos($header, 'Plural-Forms:') !== 0) {
212 80
                continue;
213
            }
214
215 60
            $expr = substr($header, 13);
216
        }
217
218 80
        return $expr;
219
    }
220
221
    /**
222
     * Get possible plural forms from MO header.
223
     *
224
     * @return string plural form header
225
     */
226 68
    private function getPluralForms(): string
227
    {
228
        // lets assume message number 0 is header
229
        // this is true, right?
230
231
        // cache header field for plural forms
232 68
        if ($this->pluralEquation === null) {
233 64
            $header = $this->cache->get('');
234
235 64
            $expr = $this->extractPluralsForms($header);
236 64
            $this->pluralEquation = $this->sanitizePluralExpression($expr);
237 64
            $this->pluralCount = $this->extractPluralCount($expr);
238
        }
239
240 68
        return $this->pluralEquation;
241
    }
242
243
    /**
244
     * Detects which plural form to take.
245
     *
246
     * @param int $n count of objects
247
     *
248
     * @return int array index of the right plural form
249
     */
250 68
    private function selectString(int $n): int
251
    {
252 68
        if ($this->pluralExpression === null) {
253 64
            $this->pluralExpression = new ExpressionLanguage();
254
        }
255
256
        try {
257 68
            $plural = (int) $this->pluralExpression->evaluate(
258 68
                $this->getPluralForms(),
259 68
                ['n' => $n]
260
            );
261 4
        } catch (Throwable $e) {
262 4
            $plural = 0;
263
        }
264
265 68
        if ($plural >= $this->pluralCount) {
266 4
            $plural = $this->pluralCount - 1;
267
        }
268
269 68
        return $plural;
270
    }
271
272
    /**
273
     * Plural version of gettext.
274
     *
275
     * @param string $msgid       Single form
276
     * @param string $msgidPlural Plural form
277
     * @param int    $number      Number of objects
278
     *
279
     * @return string translated plural form
280
     */
281 108
    public function ngettext(string $msgid, string $msgidPlural, int $number): string
282
    {
283
        // this should contains all strings separated by NULLs
284 108
        $key = implode(chr(0), [$msgid, $msgidPlural]);
285 108
        if (! $this->cache->has($key)) {
286 72
            return $number !== 1 ? $msgidPlural : $msgid;
287
        }
288
289 68
        $result = $this->cache->get($key);
290
291
        // find out the appropriate form
292 68
        $select = $this->selectString($number);
293
294 68
        $list = explode(chr(0), $result);
295
        // @codeCoverageIgnoreStart
296
        if ($list === false) {
297
            // This was added in 3ff2c63bcf85f81b3a205ce7222de11b33e2bf56 for phpstan
298
            // But according to the php manual it should never happen
299
            return '';
300
        }
301
302
        // @codeCoverageIgnoreEnd
303
304 68
        if (! isset($list[$select])) {
305 4
            return $list[0];
306
        }
307
308 68
        return $list[$select];
309
    }
310
311
    /**
312
     * Translate with context.
313
     *
314
     * @param string $msgctxt Context
315
     * @param string $msgid   String to be translated
316
     *
317
     * @return string translated plural form
318
     */
319 56
    public function pgettext(string $msgctxt, string $msgid): string
320
    {
321 56
        $key = implode(chr(4), [$msgctxt, $msgid]);
322 56
        $ret = $this->gettext($key);
323 56
        if (strpos($ret, chr(4)) !== false) {
324 24
            return $msgid;
325
        }
326
327 32
        return $ret;
328
    }
329
330
    /**
331
     * Plural version of pgettext.
332
     *
333
     * @param string $msgctxt     Context
334
     * @param string $msgid       Single form
335
     * @param string $msgidPlural Plural form
336
     * @param int    $number      Number of objects
337
     *
338
     * @return string translated plural form
339
     */
340 16
    public function npgettext(string $msgctxt, string $msgid, string $msgidPlural, int $number): string
341
    {
342 16
        $key = implode(chr(4), [$msgctxt, $msgid]);
343 16
        $ret = $this->ngettext($key, $msgidPlural, $number);
344 16
        if (strpos($ret, chr(4)) !== false) {
345 4
            return $msgid;
346
        }
347
348 12
        return $ret;
349
    }
350
351
    /**
352
     * Set translation in place
353
     *
354
     * @param string $msgid  String to be set
355
     * @param string $msgstr Translation
356
     */
357 40
    public function setTranslation(string $msgid, string $msgstr): void
358
    {
359 40
        $this->cache->set($msgid, $msgstr);
360 10
    }
361
362
    /**
363
     * Set the translations
364
     *
365
     * @param array<string,string> $translations The translations "key => value" array
366
     */
367 4
    public function setTranslations(array $translations): void
368
    {
369 4
        $this->cache->setAll($translations);
370 1
    }
371
372
    /**
373
     * Get the translations
374
     *
375
     * @return array<string,string> The translations "key => value" array
376
     */
377 8
    public function getTranslations(): array
378
    {
379 8
        if ($this->cache instanceof GetAllInterface) {
380 4
            return $this->cache->getAll();
381
        }
382
383 4
        throw new CacheException(sprintf(
384 1
            "Cache '%s' does not support getting translations",
385 4
            get_class($this->cache)
386
        ));
387
    }
388
}
389