Completed
Push — master ( 9a9839...8a0e23 )
by Michal
03:29
created

Translator   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 288
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 98.35%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
c 5
b 0
f 0
dl 0
loc 288
ccs 119
cts 121
cp 0.9835
rs 9.3999
wmc 33
lcom 1
cbo 1

10 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 40 6
A gettext() 0 8 2
C sanitize_plural_expression() 0 50 9
A extract_plural_count() 0 9 2
A extract_plurals_forms() 0 11 3
A get_plural_forms() 0 14 2
A select_string() 0 12 2
A ngettext() 0 15 3
A pgettext() 0 10 2
A npgettext() 0 10 2
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 22
    public function __construct($filename)
69
    {
70 22
        if (!is_readable($filename)) {
71 4
            $this->error = 2; // file does not exist
72 4
            return;
73
        }
74
75 18
        $stream = new StringReader($filename);
76
77
        try {
78 18
            $magic = $stream->read(0, 4);
79 17
            if (strcmp($magic, MO_MAGIC_LE) == 0) {
80 11
                $unpack = 'V';
81 17
            } elseif (strcmp($magic, MO_MAGIC_BE) == 0) {
82 6
                $unpack = 'N';
83 6
            } else {
84
                $this->error = 1; // not MO file
85
                return;
86
            }
87
88
            /* Parse header */
89 17
            $total = $stream->readint($unpack, 8);
90 14
            $originals = $stream->readint($unpack, 12);
91 14
            $translations = $stream->readint($unpack, 16);
92
93
            /* get original and translations tables */
94 14
            $table_originals = $stream->readintarray($unpack, $originals, $total * 2);
95 14
            $table_translations = $stream->readintarray($unpack, $translations, $total * 2);
96
97
            /* read all strings to the cache */
98 14
            for ($i = 0; $i < $total; $i++) {
99 14
                $original = $stream->read($table_originals[$i * 2 + 2], $table_originals[$i * 2 + 1]);
100 14
                $translation = $stream->read($table_translations[$i * 2 + 2], $table_translations[$i * 2 + 1]);
101 14
                $this->cache_translations[$original] = $translation;
102 14
            }
103 18
        } catch (ReaderException $e) {
104 4
            $this->error = 3; // error while reading
105 4
            return;
106
        }
107 14
    }
108
109
    /**
110
     * Translates a string
111
     *
112
     * @param string $msgid String to be translated
113
     *
114
     * @return string translated string (or original, if not found)
115
     */
116 19
    public function gettext($msgid)
117
    {
118 19
        if (array_key_exists($msgid, $this->cache_translations)) {
119 13
            return $this->cache_translations[$msgid];
120
        } else {
121 9
            return $msgid;
122
        }
123
    }
124
125
    /**
126
     * Sanitize plural form expression for use in PHP eval call.
127
     *
128
     * @param string $expr Expression to sanitize
129
     *
130
     * @return string sanitized plural form expression
131
     */
132 10
    public static function sanitize_plural_expression($expr)
133
    {
134
        // Parse equation
135 10
        $expr = explode(';', $expr, 2);
136 10
        if (count($expr) == 2) {
137 8
            $expr = $expr[1];
138 8
        } else {
139 2
            $expr = $expr[0];
140
        }
141 10
        $expr = trim(strtolower($expr));
142
        // Strip plural prefix
143 10
        if (substr($expr, 0, 6) === 'plural') {
144 9
            $expr = trim(substr($expr, 6));
145 9
        }
146
        // Strip equals
147 10
        if (substr($expr, 0, 1) === '=') {
148 9
            $expr = trim(substr($expr, 1));
149 9
        }
150
        // Get rid of disallowed characters.
151 10
        $expr = preg_replace('@[^n0-9:\(\)\?=!<>+*/&|%-]@', '', $expr);
152
153
        // Add parenthesis for tertiary '?' operator.
154 10
        $expr .= ';';
155 10
        $res = '';
156 10
        $p = 0;
157 10
        $len = strlen($expr);
158 10
        for ($i = 0; $i < $len; $i++) {
159 10
            $ch = $expr[$i];
160
            switch ($ch) {
161 10
                case '?':
162 6
                    $res .= ' ? (';
163 6
                    $p++;
164 6
                    break;
165 10
                case ':':
166 6
                    $res .= ') : (';
167 6
                    break;
168 10
                case ';':
169 10
                    $res .= str_repeat(')', $p) . ';';
170 10
                    $p = 0;
171 10
                    break;
172 9
                default:
173 9
                    $res .= $ch;
174 9
            }
175 10
        }
176 10
        $res = str_replace('n', '$n', $res);
177 10
        if ($res === ';') {
178 1
            return $res;
179
        }
180 9
        return '$plural = ' . $res;
181
    }
182
183
    /**
184
     * Extracts number of plurals from plurals form expression
185
     *
186
     * @param string $expr Expression to process
187
     *
188
     * @return int Total number of plurals
189
     */
190 9
    public static function extract_plural_count($expr)
191
    {
192 9
        $parts = explode(';', $expr, 2);
193 9
        $nplurals = explode('=', trim($parts[0]), 2);
194 9
        if (strtolower(trim($nplurals[0])) != 'nplurals') {
195 2
            return 1;
196
        }
197 7
        return intval($nplurals[1]);
198
    }
199
200
    /**
201
     * Parse full PO header and extract only plural forms line.
202
     *
203
     * @param string $header Gettext header
204
     *
205
     * @return string verbatim plural form header field
206
     */
207 8
    public static function extract_plurals_forms($header)
208
    {
209 8
        $headers = explode("\n", $header);
210 8
        $expr = 'nplurals=2; plural=n == 1 ? 0 : 1;';
211 8
        foreach ($headers as $header) {
212 8
            if (stripos($header, 'Plural-Forms:') === 0) {
213 6
                $expr = substr($header, 13);
214 6
            }
215 8
        }
216 8
        return $expr;
217
    }
218
219
    /**
220
     * Get possible plural forms from MO header
221
     *
222
     * @return string plural form header
223
     */
224 5
    private function get_plural_forms()
225
    {
226
        // lets assume message number 0 is header
227
        // this is true, right?
228
229
        // cache header field for plural forms
230 5
        if (is_null($this->pluralheader)) {
231 4
            $header = $this->cache_translations[''];
232 4
            $expr = $this->extract_plurals_forms($header);
233 4
            $this->pluralheader = $this->sanitize_plural_expression($expr);
234 4
            $this->pluralcount = $this->extract_plural_count($expr);
235 4
        }
236 5
        return $this->pluralheader;
237
    }
238
239
    /**
240
     * Detects which plural form to take
241
     *
242
     * @param int $n count of objects
243
     *
244
     * @return int array index of the right plural form
245
     */
246 5
    private function select_string($n)
247
    {
248 5
        $string = $this->get_plural_forms();
249
250 5
        $plural = 0;
251
252 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...
253 5
        if ($plural >= $this->pluralcount) {
254 1
            $plural = $this->pluralcount - 1;
255 1
        }
256 5
        return $plural;
257
    }
258
259
    /**
260
     * Plural version of gettext
261
     *
262
     * @param string $msgid        Single form
263
     * @param string $msgid_plural Plural form
264
     * @param string $number       Number of objects
265
     *
266
     * @return string translated plural form
267
     */
268 11
    public function ngettext($msgid, $msgid_plural, $number)
269
    {
270
        // this should contains all strings separated by NULLs
271 11
        $key = implode(chr(0), array($msgid, $msgid_plural));
272 11
        if (!array_key_exists($key, $this->cache_translations)) {
273 11
            return ($number != 1) ? $msgid_plural : $msgid;
274
        }
275
276
        // find out the appropriate form
277 5
        $select = $this->select_string($number);
278
279 5
        $result = $this->cache_translations[$key];
280 5
        $list = explode(chr(0), $result);
281 5
        return $list[$select];
282
    }
283
284
    /**
285
     * Translate with context
286
     *
287
     * @param string $msgctxt      Context
288
     * @param string $msgid        String to be translated
289
     *
290
     * @return string translated plural form
291
     */
292 9
    public function pgettext($msgctxt, $msgid)
293
    {
294 9
        $key = implode(chr(4), array($msgctxt, $msgid));
295 9
        $ret = $this->gettext($key);
296 9
        if (strpos($ret, chr(4)) !== false) {
297 4
            return $msgid;
298
        } else {
299 5
            return $ret;
300
        }
301
    }
302
303
    /**
304
     * Plural version of pgettext
305
     *
306
     * @param string $msgctxt      Context
307
     * @param string $msgid        Single form
308
     * @param string $msgid_plural Plural form
309
     * @param string $number       Number of objects
310
     *
311
     * @return string translated plural form
312
     */
313 4
    public function npgettext($msgctxt, $msgid, $msgid_plural, $number)
314
    {
315 4
        $key = implode(chr(4), array($msgctxt, $msgid));
316 4
        $ret = $this->ngettext($key, $msgid_plural, $number);
317 4
        if (strpos($ret, chr(4)) !== false) {
318 1
            return $msgid;
319
        } else {
320 3
            return $ret;
321
        }
322
    }
323
}
324