Passed
Push — master ( 6f0438...e512d9 )
by Michal
02:06
created

Translator::setTranslation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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