Completed
Push — master ( d631c6...eb2ea1 )
by Michal
03:22
created

Translator::extract_plurals_forms()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 11
ccs 9
cts 9
cp 1
rs 9.4285
cc 3
eloc 7
nc 3
nop 1
crap 3
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 1
define('MO_MAGIC_BE', "\x95\x04\x12\xde");
27 1
define('MO_MAGIC_LE', "\xde\x12\x04\x95");
28
29
/**
30
 * Provides a simple gettext replacement that works independently from
31
 * the system's gettext abilities.
32
 * It can read MO files and use them for translating strings.
33
 *
34
 * It caches ll strings and translations to speed up the string lookup.
35
 */
36
class Translator {
37
    /**
38
     * Parse error code (0 if no error)
39
     *
40
     * @var int
41
     */
42
    public $error = 0;
43
44
    /**
45
     * Cache header field for plural forms
46
     *
47
     * @var string|null
48
     */
49
    private $pluralheader = NULL;
50
    /**
51
     *
52
     *
53
     * @var int|null number of plurals
54
     */
55
    private $pluralcount = NULL;
56
    /**
57
     * Array with original -> translation mapping
58
     *
59
     * @var array
60
     */
61
    private $cache_translations = array();
62
63
    /**
64
     * Constructor
65
     *
66
     * @param string $filename Name of mo file to load
67
     */
68 19
    public function __construct($filename)
69
    {
70 19
        if (!is_readable($filename)) {
71 4
            $this->error = 2; // file does not exist
72 4
            return;
73
        }
74
75 15
        $stream = new StringReader($filename);
76
77 15
        $magic = $stream->read(0, 4);
78 15
        if (strcmp($magic, MO_MAGIC_LE) == 0) {
79 11
            $unpack = 'V';
80 15
        } elseif (strcmp($magic, MO_MAGIC_BE) == 0) {
81 3
            $unpack = 'N';
82 3
        } else {
83 1
            $this->error = 1; // not MO file
84 1
            return;
85
        }
86
87
        /* Parse header */
88 14
        $total = $stream->readint($unpack, 8);
89 14
        $originals = $stream->readint($unpack, 12);
90 14
        $translations = $stream->readint($unpack, 16);
91
92
        /* get original and translations tables */
93 14
        $table_originals = $stream->readintarray($unpack, $originals, $total * 2);
94 14
        $table_translations = $stream->readintarray($unpack, $translations, $total * 2);
95
96
        /* read all strings to the cache */
97 14
        for ($i = 0; $i < $total; $i++) {
98 14
            $original = $stream->read($table_originals[$i * 2 + 2], $table_originals[$i * 2 + 1]);
99 14
            $translation = $stream->read($table_translations[$i * 2 + 2], $table_translations[$i * 2 + 1]);
100 14
            $this->cache_translations[$original] = $translation;
101 14
        }
102 14
    }
103
104
    /**
105
     * Translates a string
106
     *
107
     * @param string $msgid String to be translated
108
     *
109
     * @return string translated string (or original, if not found)
110
     */
111 16
    public function gettext($msgid)
112
    {
113 16
        if (array_key_exists($msgid, $this->cache_translations)) {
114 13
            return $this->cache_translations[$msgid];
115
        } else {
116 6
            return $msgid;
117
        }
118
    }
119
120
    /**
121
     * Sanitize plural form expression for use in PHP eval call.
122
     *
123
     * @param string $expr Expression to sanitize
124
     *
125
     * @return string sanitized plural form expression
126
     */
127 10
    public static function sanitize_plural_expression($expr)
128
    {
129
        // Get rid of disallowed characters.
130 10
        $expr = explode(';', $expr, 2);
131 10
        if (count($expr) == 2) {
132 8
            $expr = $expr[1];
133 8
        } else {
134 2
            $expr = $expr[0];
135
        }
136 10
        $expr = preg_replace('@[^a-zA-Z0-9_:;\(\)\?\|\&=!<>+*/\%-]@', '', $expr);
137
138
        // Add parenthesis for tertiary '?' operator.
139 10
        $expr .= ';';
140 10
        $res = '';
141 10
        $p = 0;
142 10
        for ($i = 0; $i < strlen($expr); $i++) {
143 10
            $ch = $expr[$i];
144
            switch ($ch) {
145 10
                case '?':
146 6
                    $res .= ' ? (';
147 6
                    $p++;
148 6
                    break;
149 10
                case ':':
150 6
                    $res .= ') : (';
151 6
                    break;
152 10
                case ';':
153 10
                    $res .= str_repeat(')', $p) . ';';
154 10
                    $p = 0;
155 10
                    break;
156 9
                default:
157 9
                    $res .= $ch;
158 9
            }
159 10
        }
160 10
        $res = str_replace('n', '$n', $res);
161 10
        $res = str_replace('plural', '$plural', $res);
162 10
        return $res;
163
    }
164
165
    /**
166
     * Extracts number of plurals from plurals form expression
167
     *
168
     * @param string $expr Expression to process
169
     *
170
     * @return int Total number of plurals
171
     */
172 9
    public static function extract_plural_count($expr)
173
    {
174 9
        $parts = explode(';', $expr, 2);
175 9
        $nplurals = explode('=', trim($parts[0]), 2);
176 9
        if (strtolower(trim($nplurals[0])) != 'nplurals') {
177 2
            return 1;
178
        }
179 7
        return intval($nplurals[1]);
180
    }
181
182
    /**
183
     * Parse full PO header and extract only plural forms line.
184
     *
185
     * @param string $header Gettext header
186
     *
187
     * @return string verbatim plural form header field
188
     */
189 8
    public static function extract_plurals_forms($header)
190
    {
191 8
        $headers = explode("\n", $header);
192 8
        $expr = "nplurals=2; plural=n == 1 ? 0 : 1;";
193 8
        foreach ($headers as $header) {
194 8
            if (stripos($header, 'Plural-Forms:') === 0) {
195 6
                $expr = substr($header, 13);
196 6
            }
197 8
        }
198 8
        return $expr;
199
    }
200
201
    /**
202
     * Get possible plural forms from MO header
203
     *
204
     * @return string plural form header
205
     */
206 5
    private function get_plural_forms()
207
    {
208
        // lets assume message number 0 is header
209
        // this is true, right?
210
211
        // cache header field for plural forms
212 5
        if (is_null($this->pluralheader)) {
213 4
            $header = $this->cache_translations[""];
214 4
            $expr = $this->extract_plurals_forms($header);
215 4
            $this->pluralheader = $this->sanitize_plural_expression($expr);
216 4
            $this->pluralcount = $this->extract_plural_count($expr);
217 4
        }
218 5
        return $this->pluralheader;
219
    }
220
221
    /**
222
     * Detects which plural form to take
223
     *
224
     * @param int $n count of objects
225
     *
226
     * @return int array index of the right plural form
227
     */
228 5
    private function select_string($n)
1 ignored issue
show
Unused Code introduced by
The parameter $n is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
229
    {
230 5
        $string = $this->get_plural_forms();
231
232 5
        $plural = 0;
233
234 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...
235 5
        if ($plural >= $this->pluralcount) {
236 1
            $plural = $this->pluralcount - 1;
237 1
        }
238 5
        return $plural;
239
    }
240
241
    /**
242
     * Plural version of gettext
243
     *
244
     * @param string $msgid        Single form
245
     * @param string $msgid_plural Plural form
246
     * @param string $number       Number of objects
247
     *
248
     * @return string translated plural form
249
     */
250 8
    public function ngettext($msgid, $msgid_plural, $number)
251
    {
252
        // this should contains all strings separated by NULLs
253 8
        $key = implode(chr(0), array($msgid, $msgid_plural));
254 8
        if (!array_key_exists($key, $this->cache_translations)) {
255 8
            return ($number != 1) ? $msgid_plural : $msgid;
256
        }
257
258
        // find out the appropriate form
259 5
        $select = $this->select_string($number);
260
261 5
        $result = $this->cache_translations[$key];
262 5
        $list = explode(chr(0), $result);
263 5
        return $list[$select];
264
    }
265
266
    /**
267
     * Translate with context
268
     *
269
     * @param string $msgctxt      Context
270
     * @param string $msgid        String to be translated
271
     *
272
     * @return string translated plural form
273
     */
274 6
    public function pgettext($msgctxt, $msgid)
1 ignored issue
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...
275
    {
276 6
        $key = implode(chr(4), array($msgctxt, $msgid));
277 6
        $ret = $this->gettext($key);
278 6
        if (strpos($ret, chr(4)) !== false) {
279 1
            return $msgid;
280
        } else {
281 5
            return $ret;
282
        }
283
    }
284
285
    /**
286
     * Plural version of pgettext
287
     *
288
     * @param string $msgctxt      Context
289
     * @param string $msgid        Single form
290
     * @param string $msgid_plural Plural form
291
     * @param string $number       Number of objects
292
     *
293
     * @return string translated plural form
294
     */
295 4
    public function npgettext($msgctxt, $msgid, $msgid_plural, $number)
1 ignored issue
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...
296
    {
297 4
        $key = implode(chr(4), array($msgctxt, $msgid));
298 4
        $ret = $this->ngettext($key, $msgid_plural, $number);
299 4
        if (strpos($ret, chr(4)) !== false) {
300 1
            return $msgid;
301
        } else {
302 3
            return $ret;
303
        }
304
    }
305
}
306