Completed
Push — master ( 6ec166...f38c7d )
by Michal
03:01
created

MoTranslator::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
Metric Value
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 MoTranslator {
37
    /**
38
     * @var int Parse error code (0 if no error)
39
     */
40
    public $error = 0;
41
42
    /**
43
     * @var string|null cache header field for plural forms
44
     */
45
    private $pluralheader = NULL;
46
    /**
47
     * @var int|null number of plurals
48
     */
49
    private $pluralcount = NULL;
50
    /**
51
     * @var array Array with original -> translation mapping
52
     */
53
    private $cache_translations = array();
54
55
    /**
56
     * Constructor
57
     *
58
     * @param string $filename Name of mo file to load
59
     */
60 12
    public function __construct($filename)
61
    {
62 12
        if (! is_readable($filename)) {
63 2
            $this->error = 2; // file does not exist
64 2
            return;
65
        }
66
67 10
        $stream = new StringReader($filename);
68
69 10
        $magic = $stream->read(0, 4);
70 10
        if (strcmp($magic, MO_MAGIC_LE) == 0) {
71 6
            $unpack = 'V';
72 10
        } elseif (strcmp($magic, MO_MAGIC_BE) == 0) {
73 3
            $unpack = 'N';
74 3
        } else {
75 1
            $this->error = 1; // not MO file
76 1
            return;
77
        }
78
79
        /* Parse header */
80 9
        $total = $stream->readint($unpack, 8);
81 9
        $originals = $stream->readint($unpack, 12);
82 9
        $translations = $stream->readint($unpack, 16);
83
84
        /* get original and translations tables */
85 9
        $table_originals = $stream->readintarray($unpack, $originals, $total * 2);
86 9
        $table_translations = $stream->readintarray($unpack, $translations, $total * 2);
87
88
        /* read all strings to the cache */
89 9
        for ($i = 0; $i < $total; $i++) {
90 9
            $original = $stream->read($table_originals[$i * 2 + 2], $table_originals[$i * 2 + 1]);
91 9
            $translation = $stream->read($table_translations[$i * 2 + 2], $table_translations[$i * 2 + 1]);
92 9
            $this->cache_translations[$original] = $translation;
93 9
        }
94 9
    }
95
96
    /**
97
     * Translates a string
98
     *
99
     * @param string $msgid String to be translated
100
     *
101
     * @return string translated string (or original, if not found)
102
     */
103 7
    public function gettext($msgid)
104
    {
105 7
        if (array_key_exists($msgid, $this->cache_translations)) {
106 6
            return $this->cache_translations[$msgid];
107
        } else {
108 4
            return $msgid;
109
        }
110
    }
111
112
    /**
113
     * Sanitize plural form expression for use in PHP eval call.
114
     *
115
     * @param string $expr Expression to sanitize
116
     *
117
     * @return string sanitized plural form expression
118
     */
119 9
    public static function sanitize_plural_expression($expr)
120
    {
121
        // Get rid of disallowed characters.
122 9
        $expr = explode(';', $expr, 2);
123 9
        if (count($expr) == 2) {
124 7
            $expr = $expr[1];
125 7
        } else{
126 2
            $expr = $expr[0];
127
        }
128 9
        $expr = preg_replace('@[^a-zA-Z0-9_:;\(\)\?\|\&=!<>+*/\%-]@', '', $expr);
129
130
        // Add parenthesis for tertiary '?' operator.
131 9
        $expr .= ';';
132 9
        $res = '';
133 9
        $p = 0;
134 9
        for ($i = 0; $i < strlen($expr); $i++) {
135 9
            $ch = $expr[$i];
136
            switch ($ch) {
137 9
                case '?':
138 5
                    $res .= ' ? (';
139 5
                    $p++;
140 5
                    break;
141 9
                case ':':
142 5
                    $res .= ') : (';
143 5
                    break;
144 9
                case ';':
145 9
                    $res .= str_repeat( ')', $p) . ';';
146 9
                    $p = 0;
147 9
                    break;
148 8
                default:
149 8
                    $res .= $ch;
150 8
            }
151 9
        }
152 9
        $res = str_replace('n','$n',$res);
153 9
        $res = str_replace('plural','$plural',$res);
154 9
        return $res;
155
    }
156
157
    /**
158
     * Extracts number of plurals from plurals form expression
159
     *
160
     * @param string $expr Expression to process
161
     *
162
     * @return int Total number of plurals
163
     */
164 8
    public static function extract_plural_count($expr)
165
    {
166 8
        $parts = explode(';', $expr, 2);
167 8
        $nplurals = explode('=', trim($parts[0]), 2);
168 8
        if (strtolower(trim($nplurals[0])) != 'nplurals') {
169 2
            return 1;
170
        }
171 6
        return intval($nplurals[1]);
172
    }
173
174
    /**
175
     * Parse full PO header and extract only plural forms line.
176
     *
177
     * @param string $header Gettext header
178
     *
179
     * @return string verbatim plural form header field
180
     */
181 7
    public static function extract_plurals_forms($header)
182
    {
183 7
        $headers = explode("\n", $header);
184 7
        $expr = "nplurals=2; plural=n == 1 ? 0 : 1;";
185 7
        foreach ($headers as $header) {
186 7
            if (stripos($header, 'Plural-Forms:') === 0) {
187 5
                $expr = substr($header, 13);
188 5
            }
189 7
        }
190 7
        return $expr;
191
    }
192
193
    /**
194
     * Get possible plural forms from MO header
195
     *
196
     * @return string plural form header
197
     */
198 3
    private function get_plural_forms()
199
    {
200
        // lets assume message number 0 is header
201
        // this is true, right?
202
203
        // cache header field for plural forms
204 3
        if (is_null($this->pluralheader)) {
205 3
            $header = $this->cache_translations[""];
206 3
            $expr = $this->extract_plurals_forms($header);
207 3
            $this->pluralheader = $this->sanitize_plural_expression($expr);
208 3
            $this->pluralcount = $this->extract_plural_count($expr);
209 3
        }
210 3
        return $this->pluralheader;
211
    }
212
213
    /**
214
     * Detects which plural form to take
215
     *
216
     * @param int $n count of objects
217
     *
218
     * @return int array index of the right plural form
219
     */
220 3
    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...
221
    {
222 3
        $string = $this->get_plural_forms();
223
224 3
        $plural = 0;
225
226 3
        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...
227 3
        if ($plural >= $this->pluralcount) {
228 1
            $plural = $this->pluralcount - 1;
229 1
        }
230 3
        return $plural;
231
    }
232
233
    /**
234
     * Plural version of gettext
235
     *
236
     * @param string $single Single form
237
     * @param string $plural Plural form
238
     * @param string $number Number of objects
239
     *
240
     * @return string translated plural form
241
     */
242 6
    public function ngettext($single, $plural, $number)
243
    {
244
        // this should contains all strings separated by NULLs
245 6
        $key = implode(chr(0), array($single, $plural));
246 6
        if (! array_key_exists($key, $this->cache_translations)) {
247 6
            return ($number != 1) ? $plural : $single;
248
        }
249
250
        // find out the appropriate form
251 3
        $select = $this->select_string($number);
252
253 3
        $result = $this->cache_translations[$key];
254 3
        $list = explode(chr(0), $result);
255 3
        return $list[$select];
256
    }
257
258
    /**
259
     * Translate with context
260
     *
261
     * @param string $context Context
262
     * @param string $msgid   String to be translated
263
     *
264
     * @return string translated plural form
265
     */
266 4
    public function pgettext($context, $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...
267
    {
268 4
        $key = implode(chr(4), array($context, $msgid));
269 4
        $ret = $this->gettext($key);
270 4
        if (strpos($ret, chr(4)) !== false) {
271 1
            return $msgid;
272
        } else {
273 3
            return $ret;
274
        }
275
    }
276
277
    /**
278
     * Plural version of pgettext
279
     *
280
     * @param string $context Context
281
     * @param string $single  Single form
0 ignored issues
show
Bug introduced by
There is no parameter named $single. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
282
     * @param string $plural  Plural form
283
     * @param string $number  Number of objects
284
     *
285
     * @return string translated plural form
286
     */
287 2
    public function npgettext($context, $singular, $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...
288
    {
289 2
        $key = implode(chr(4), array($context, $singular));
290 2
        $ret = $this->ngettext($key, $plural, $number);
291 2
        if (strpos($ret, chr(4)) !== false) {
292 1
            return $singular;
293
        } else {
294 1
            return $ret;
295
        }
296
    }
297
}
298