Completed
Push — master ( 1cb4b6...6662e3 )
by Michal
02:59
created

MoTranslator::extract_plural_count()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2.0185

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 9
ccs 5
cts 6
cp 0.8333
rs 9.6666
cc 2
eloc 6
nc 2
nop 1
crap 2.0185
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 3
    private static function sanitize_plural_expression($expr)
120
    {
121
        // Get rid of disallowed characters.
122 3
        $expr = explode(';', $expr, 2)[1];
123 3
        $expr = preg_replace('@[^a-zA-Z0-9_:;\(\)\?\|\&=!<>+*/\%-]@', '', $expr);
124
125
        // Add parenthesis for tertiary '?' operator.
126 3
        $expr .= ';';
127 3
        $res = '';
128 3
        $p = 0;
129 3
        for ($i = 0; $i < strlen($expr); $i++) {
130 3
            $ch = $expr[$i];
131
            switch ($ch) {
132 3
                case '?':
133 3
                    $res .= ' ? (';
134 3
                    $p++;
135 3
                    break;
136 3
                case ':':
137 3
                    $res .= ') : (';
138 3
                    break;
139 3
                case ';':
140 3
                    $res .= str_repeat( ')', $p) . ';';
141 3
                    $p = 0;
142 3
                    break;
143 3
                default:
144 3
                    $res .= $ch;
145 3
            }
146 3
        }
147 3
        $res = str_replace('n','$n',$res);
148 3
        $res = str_replace('plural','$plural',$res);
149 3
        return $res;
150
    }
151
152
    /**
153
     * Extracts number of plurals from plurals form expression
154
     *
155
     * @param string $expr Expression to process
156
     *
157
     * @return int Total number of plurals
158
     */
159 3
    private static function extract_plural_count($expr)
160
    {
161 3
        $parts = explode(';', $expr, 2);
162 3
        $nplurals = explode('=', trim($parts[0]), 2);
163 3
        if (strtolower(trim($nplurals[0])) != 'nplurals') {
164
            return 1;
165
        }
166 3
        return intval($nplurals[1]);
167
    }
168
169
    /**
170
     * Parse full PO header and extract only plural forms line.
171
     *
172
     * @param string $header Gettext header
173
     *
174
     * @return string verbatim plural form header field
175
     */
176 4
    public static function extract_plural_forms_header_from_po_header($header)
177
    {
178 4
        $headers = explode("\n", $header);
179 4
        $expr = "nplurals=2; plural=n == 1 ? 0 : 1;";
180 4
        foreach ($headers as $header) {
181 4
            if (stripos($header, 'Plural-Forms:') === 0) {
182 4
                $expr = substr($header, 13);
183 4
            }
184 4
        }
185 4
        return $expr;
186
    }
187
188
    /**
189
     * Get possible plural forms from MO header
190
     *
191
     * @return string plural form header
192
     */
193 3
    private function get_plural_forms()
194
    {
195
        // lets assume message number 0 is header
196
        // this is true, right?
197
198
        // cache header field for plural forms
199 3
        if (is_null($this->pluralheader)) {
200 3
            $header = $this->cache_translations[""];
201 3
            $expr = $this->extract_plural_forms_header_from_po_header($header);
202 3
            $this->pluralheader = $this->sanitize_plural_expression($expr);
203 3
            $this->pluralcount = $this->extract_plural_count($expr);
204 3
        }
205 3
        return $this->pluralheader;
206
    }
207
208
    /**
209
     * Detects which plural form to take
210
     *
211
     * @param int $n count of objects
212
     *
213
     * @return int array index of the right plural form
214
     */
215 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...
216
    {
217 3
        $string = $this->get_plural_forms();
218
219 3
        $plural = 0;
220
221 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...
222 3
        if ($plural >= $this->pluralcount) {
223 1
            $plural = $this->pluralcount - 1;
224 1
        }
225 3
        return $plural;
226
    }
227
228
    /**
229
     * Plural version of gettext
230
     *
231
     * @param string $single Single form
232
     * @param string $plural Plural form
233
     * @param string $number Number of objects
234
     *
235
     * @return string translated plural form
236
     */
237 6
    public function ngettext($single, $plural, $number)
238
    {
239
        // this should contains all strings separated by NULLs
240 6
        $key = $single . chr(0) . $plural;
241 6
        if (! array_key_exists($key, $this->cache_translations)) {
242 6
            return ($number != 1) ? $plural : $single;
243
        }
244
245
        // find out the appropriate form
246 3
        $select = $this->select_string($number);
247
248 3
        $result = $this->cache_translations[$key];
249 3
        $list = explode(chr(0), $result);
250 3
        return $list[$select];
251
    }
252
253
    /**
254
     * Translate with context
255
     *
256
     * @param string $context Context
257
     * @param string $msgid   String to be translated
258
     *
259
     * @return string translated plural form
260
     */
261 4
    public function pgettext($context, $msgid)
262
    {
263 4
        $key = $context . chr(4) . $msgid;
264 4
        $ret = $this->gettext($key);
265 4
        if (strpos($ret, chr(4)) !== FALSE) {
266 1
            return $msgid;
267
        } else {
268 3
            return $ret;
269
        }
270
    }
271
272
    /**
273
     * Plural version of pgettext
274
     *
275
     * @param string $context Context
276
     * @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...
277
     * @param string $plural  Plural form
278
     * @param string $number  Number of objects
279
     *
280
     * @return string translated plural form
281
     */
282 2
    public function npgettext($context, $singular, $plural, $number)
283
    {
284 2
        $key = $context . chr(4) . $singular;
285 2
        $ret = $this->ngettext($key, $plural, $number);
286 2
        if (strpos($ret, chr(4)) !== FALSE) {
287 1
            return $singular;
288
        } else {
289 1
            return $ret;
290
        }
291
    }
292
}
293