Completed
Push — master ( 185031...88e3a1 )
by Michal
05:19
created

Translator::extractPluralCount()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 1
dl 0
loc 9
ccs 6
cts 6
cp 1
crap 2
rs 9.6666
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 MoTranslator;
25
26
/**
27
 * Provides a simple gettext replacement that works independently from
28
 * the system's gettext abilities.
29
 * It can read MO files and use them for translating strings.
30
 *
31
 * It caches ll strings and translations to speed up the string lookup.
32
 */
33
class Translator {
34
35
    /**
36
     * None error
37
     */
38
    const ERROR_NONE = 0;
39
    /**
40
     * File does not exist
41
     */
42
    const ERROR_DOES_NOT_EXIST = 1;
43
    /**
44
     * File has bad magic number
45
     */
46
    const ERROR_BAD_MAGIC = 2;
47
    /**
48
     * Error while reading file, probably too short
49
     */
50
    const ERROR_READING = 3;
51
52
    /**
53
     * Big endian mo file magic bytes.
54
     */
55
    const MAGIC_BE = "\x95\x04\x12\xde";
56
    /**
57
     * Little endian mo file magic bytes.
58
     */
59
    const MAGIC_LE = "\xde\x12\x04\x95";
60
61
    /**
62
     * Parse error code (0 if no error)
63
     *
64
     * @var int
65
     */
66
    public $error = Translator::ERROR_NONE;
67
68
    /**
69
     * Cache header field for plural forms
70
     *
71
     * @var string|null
72
     */
73
    private $pluralheader = NULL;
74
    /**
75
     *
76
     *
77
     * @var int|null number of plurals
78
     */
79
    private $pluralcount = NULL;
80
    /**
81
     * Array with original -> translation mapping
82
     *
83
     * @var array
84
     */
85
    private $cache_translations = array();
86
87
    /**
88
     * Constructor
89
     *
90
     * @param string $filename Name of mo file to load
91
     */
92 23
    public function __construct($filename)
93
    {
94 23
        if (!is_readable($filename)) {
95 4
            $this->error = Translator::ERROR_DOES_NOT_EXIST;
96 4
            return;
97
        }
98
99 19
        $stream = new StringReader($filename);
100
101
        try {
102 19
            $magic = $stream->read(0, 4);
103 18
            if (strcmp($magic, Translator::MAGIC_LE) == 0) {
104 11
                $unpack = 'V';
105 18
            } elseif (strcmp($magic, Translator::MAGIC_BE) == 0) {
106 6
                $unpack = 'N';
107 6
            } else {
108 1
                $this->error = Translator::ERROR_BAD_MAGIC;
109 1
                return;
110
            }
111
112
            /* Parse header */
113 17
            $total = $stream->readint($unpack, 8);
114 14
            $originals = $stream->readint($unpack, 12);
115 14
            $translations = $stream->readint($unpack, 16);
116
117
            /* get original and translations tables */
118 14
            $table_originals = $stream->readintarray($unpack, $originals, $total * 2);
119 14
            $table_translations = $stream->readintarray($unpack, $translations, $total * 2);
120
121
            /* read all strings to the cache */
122 14
            for ($i = 0; $i < $total; $i++) {
123 14
                $original = $stream->read($table_originals[$i * 2 + 2], $table_originals[$i * 2 + 1]);
124 14
                $translation = $stream->read($table_translations[$i * 2 + 2], $table_translations[$i * 2 + 1]);
125 14
                $this->cache_translations[$original] = $translation;
126 14
            }
127 18
        } catch (ReaderException $e) {
128 4
            $this->error = Translator::ERROR_READING;
129 4
            return;
130
        }
131 14
    }
132
133
    /**
134
     * Translates a string
135
     *
136
     * @param string $msgid String to be translated
137
     *
138
     * @return string translated string (or original, if not found)
139
     */
140 20
    public function gettext($msgid)
141
    {
142 20
        if (array_key_exists($msgid, $this->cache_translations)) {
143 13
            return $this->cache_translations[$msgid];
144
        } else {
145 10
            return $msgid;
146
        }
147
    }
148
149
    /**
150
     * Sanitize plural form expression for use in PHP eval call.
151
     *
152
     * @param string $expr Expression to sanitize
153
     *
154
     * @return string sanitized plural form expression
155
     */
156 10
    public static function sanitizePluralExpression($expr)
157
    {
158
        // Parse equation
159 10
        $expr = explode(';', $expr, 2);
160 10
        if (count($expr) == 2) {
161 8
            $expr = $expr[1];
162 8
        } else {
163 2
            $expr = $expr[0];
164
        }
165 10
        $expr = trim(strtolower($expr));
166
        // Strip plural prefix
167 10
        if (substr($expr, 0, 6) === 'plural') {
168 9
            $expr = trim(substr($expr, 6));
169 9
        }
170
        // Strip equals
171 10
        if (substr($expr, 0, 1) === '=') {
172 9
            $expr = trim(substr($expr, 1));
173 9
        }
174
        // Get rid of disallowed characters.
175 10
        $expr = preg_replace('@[^n0-9:\(\)\?=!<>/%&|]@', '', $expr);
176
177
        // Add parenthesis for tertiary '?' operator.
178 10
        $expr .= ';';
179 10
        $res = '';
180 10
        $p = 0;
181 10
        $len = strlen($expr);
182 10
        for ($i = 0; $i < $len; $i++) {
183 10
            $ch = $expr[$i];
184
            switch ($ch) {
185 10
                case '?':
186 6
                    $res .= ' ? (';
187 6
                    $p++;
188 6
                    break;
189 10
                case ':':
190 6
                    $res .= ') : (';
191 6
                    break;
192 10
                case ';':
193 10
                    $res .= str_repeat(')', $p) . ';';
194 10
                    $p = 0;
195 10
                    break;
196 9
                default:
197 9
                    $res .= $ch;
198 9
            }
199 10
        }
200 10
        $res = str_replace('n', '$n', $res);
201 10
        if ($res === ';') {
202 1
            return $res;
203
        }
204 9
        return '$plural = ' . $res;
205
    }
206
207
    /**
208
     * Extracts number of plurals from plurals form expression
209
     *
210
     * @param string $expr Expression to process
211
     *
212
     * @return int Total number of plurals
213
     */
214 9
    public static function extractPluralCount($expr)
215
    {
216 9
        $parts = explode(';', $expr, 2);
217 9
        $nplurals = explode('=', trim($parts[0]), 2);
218 9
        if (strtolower(trim($nplurals[0])) != 'nplurals') {
219 2
            return 1;
220
        }
221 7
        return intval($nplurals[1]);
222
    }
223
224
    /**
225
     * Parse full PO header and extract only plural forms line.
226
     *
227
     * @param string $header Gettext header
228
     *
229
     * @return string verbatim plural form header field
230
     */
231 8
    public static function extractPluralsForms($header)
232
    {
233 8
        $headers = explode("\n", $header);
234 8
        $expr = 'nplurals=2; plural=n == 1 ? 0 : 1;';
235 8
        foreach ($headers as $header) {
236 8
            if (stripos($header, 'Plural-Forms:') === 0) {
237 6
                $expr = substr($header, 13);
238 6
            }
239 8
        }
240 8
        return $expr;
241
    }
242
243
    /**
244
     * Get possible plural forms from MO header
245
     *
246
     * @return string plural form header
247
     */
248 5
    private function getPluralForms()
249
    {
250
        // lets assume message number 0 is header
251
        // this is true, right?
252
253
        // cache header field for plural forms
254 5
        if (is_null($this->pluralheader)) {
255 4
            $header = $this->cache_translations[''];
256 4
            $expr = $this->extractPluralsForms($header);
257 4
            $this->pluralheader = $this->sanitizePluralExpression($expr);
258 4
            $this->pluralcount = $this->extractPluralCount($expr);
259 4
        }
260 5
        return $this->pluralheader;
261
    }
262
263
    /**
264
     * Detects which plural form to take
265
     *
266
     * @param int $n count of objects
267
     *
268
     * @return int array index of the right plural form
269
     */
270 5
    private function selectString($n)
271
    {
272 5
        $string = $this->getPluralForms();
273
274 5
        $plural = 0;
275
276 5
        eval($string);
0 ignored issues
show
Coding Style introduced by
It is generally not recommended to use eval unless absolutely required.

On one hand, eval might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM, eval prevents some optimization that they perform.

Loading history...
277 5
        if ($plural >= $this->pluralcount) {
278 1
            $plural = $this->pluralcount - 1;
279 1
        }
280 5
        return $plural;
281
    }
282
283
    /**
284
     * Plural version of gettext
285
     *
286
     * @param string $msgid        Single form
287
     * @param string $msgid_plural Plural form
288
     * @param string $number       Number of objects
289
     *
290
     * @return string translated plural form
291
     */
292 12
    public function ngettext($msgid, $msgid_plural, $number)
293
    {
294
        // this should contains all strings separated by NULLs
295 12
        $key = implode(chr(0), array($msgid, $msgid_plural));
296 12
        if (!array_key_exists($key, $this->cache_translations)) {
297 12
            return ($number != 1) ? $msgid_plural : $msgid;
298
        }
299
300
        // find out the appropriate form
301 5
        $select = $this->selectString($number);
302
303 5
        $result = $this->cache_translations[$key];
304 5
        $list = explode(chr(0), $result);
305 5
        return $list[$select];
306
    }
307
308
    /**
309
     * Translate with context
310
     *
311
     * @param string $msgctxt      Context
312
     * @param string $msgid        String to be translated
313
     *
314
     * @return string translated plural form
315
     */
316 10
    public function pgettext($msgctxt, $msgid)
317
    {
318 10
        $key = implode(chr(4), array($msgctxt, $msgid));
319 10
        $ret = $this->gettext($key);
320 10
        if (strpos($ret, chr(4)) !== false) {
321 5
            return $msgid;
322
        } else {
323 5
            return $ret;
324
        }
325
    }
326
327
    /**
328
     * Plural version of pgettext
329
     *
330
     * @param string $msgctxt      Context
331
     * @param string $msgid        Single form
332
     * @param string $msgid_plural Plural form
333
     * @param string $number       Number of objects
334
     *
335
     * @return string translated plural form
336
     */
337 4
    public function npgettext($msgctxt, $msgid, $msgid_plural, $number)
338
    {
339 4
        $key = implode(chr(4), array($msgctxt, $msgid));
340 4
        $ret = $this->ngettext($key, $msgid_plural, $number);
341 4
        if (strpos($ret, chr(4)) !== false) {
342 1
            return $msgid;
343
        } else {
344 3
            return $ret;
345
        }
346
    }
347
}
348