Completed
Push — master ( 192b78...a3ceb8 )
by Michal
04:04
created

Translator::sanitizePluralExpression()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 12
nc 8
nop 1
dl 0
loc 20
ccs 14
cts 14
cp 1
crap 4
rs 9.2
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 $pluralequation = 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 SimpleMath
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);
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 10
        return $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 9
    public static function extractPluralCount($expr)
185
    {
186 9
        $parts = explode(';', $expr, 2);
187 9
        $nplurals = explode('=', trim($parts[0]), 2);
188 9
        if (strtolower(trim($nplurals[0])) != 'nplurals') {
189 2
            return 1;
190
        }
191 7
        return intval($nplurals[1]);
192
    }
193
194
    /**
195
     * Parse full PO header and extract only plural forms line.
196
     *
197
     * @param string $header Gettext header
198
     *
199
     * @return string verbatim plural form header field
200
     */
201 8
    public static function extractPluralsForms($header)
202
    {
203 8
        $headers = explode("\n", $header);
204 8
        $expr = 'nplurals=2; plural=n == 1 ? 0 : 1;';
205 8
        foreach ($headers as $header) {
206 8
            if (stripos($header, 'Plural-Forms:') === 0) {
207 6
                $expr = substr($header, 13);
208 6
            }
209 8
        }
210 8
        return $expr;
211
    }
212
213
    /**
214
     * Get possible plural forms from MO header
215
     *
216
     * @return string plural form header
217
     */
218 5
    private function getPluralForms()
219
    {
220
        // lets assume message number 0 is header
221
        // this is true, right?
222
223
        // cache header field for plural forms
224 5
        if (is_null($this->pluralequation)) {
225 4
            $header = $this->cache_translations[''];
226 4
            $expr = $this->extractPluralsForms($header);
227 4
            $this->pluralequation = new \SimpleMath\Math();
0 ignored issues
show
Documentation Bug introduced by
It seems like new \SimpleMath\Math() of type object<SimpleMath\Math> is incompatible with the declared type string|null of property $pluralequation.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
228 4
            $this->pluralequation->parse($this->sanitizePluralExpression($expr));
229 4
            $this->pluralcount = $this->extractPluralCount($expr);
230 4
        }
231 5
        return $this->pluralequation;
232
    }
233
234
    /**
235
     * Detects which plural form to take
236
     *
237
     * @param int $n count of objects
238
     *
239
     * @return int array index of the right plural form
240
     */
241 5
    private function selectString($n)
242
    {
243 5
        $equation = $this->getPluralForms();
244 5
        $equation->registerVariable('n', $n);
245
246 5
        $plural = $equation->run();
247
248 5
        if ($plural >= $this->pluralcount) {
249 1
            $plural = $this->pluralcount - 1;
250 1
        }
251 5
        return $plural;
252
    }
253
254
    /**
255
     * Plural version of gettext
256
     *
257
     * @param string $msgid       Single form
258
     * @param string $msgidPlural Plural form
259
     * @param string $number      Number of objects
260
     *
261
     * @return string translated plural form
262
     */
263 12
    public function ngettext($msgid, $msgidPlural, $number)
264
    {
265
        // this should contains all strings separated by NULLs
266 12
        $key = implode(chr(0), array($msgid, $msgidPlural));
267 12
        if (!array_key_exists($key, $this->cache_translations)) {
268 12
            return ($number != 1) ? $msgidPlural : $msgid;
269
        }
270
271
        // find out the appropriate form
272 5
        $select = $this->selectString($number);
273
274 5
        $result = $this->cache_translations[$key];
275 5
        $list = explode(chr(0), $result);
276 5
        return $list[$select];
277
    }
278
279
    /**
280
     * Translate with context
281
     *
282
     * @param string $msgctxt      Context
283
     * @param string $msgid        String to be translated
284
     *
285
     * @return string translated plural form
286
     */
287 10
    public function pgettext($msgctxt, $msgid)
288
    {
289 10
        $key = implode(chr(4), array($msgctxt, $msgid));
290 10
        $ret = $this->gettext($key);
291 10
        if (strpos($ret, chr(4)) !== false) {
292 5
            return $msgid;
293
        } else {
294 5
            return $ret;
295
        }
296
    }
297
298
    /**
299
     * Plural version of pgettext
300
     *
301
     * @param string $msgctxt     Context
302
     * @param string $msgid       Single form
303
     * @param string $msgidPlural Plural form
304
     * @param string $number      Number of objects
305
     *
306
     * @return string translated plural form
307
     */
308 4
    public function npgettext($msgctxt, $msgid, $msgidPlural, $number)
309
    {
310 4
        $key = implode(chr(4), array($msgctxt, $msgid));
311 4
        $ret = $this->ngettext($key, $msgidPlural, $number);
312 4
        if (strpos($ret, chr(4)) !== false) {
313 1
            return $msgid;
314
        } else {
315 3
            return $ret;
316
        }
317
    }
318
}
319