Completed
Push — master ( 01349c...a83e3f )
by Michal
03:39
created

Translator::pgettext()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 2
dl 0
loc 10
ccs 6
cts 6
cp 1
crap 2
rs 9.4285
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
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
27
28
/**
29
 * Provides a simple gettext replacement that works independently from
30
 * the system's gettext abilities.
31
 * It can read MO files and use them for translating strings.
32
 *
33
 * It caches ll strings and translations to speed up the string lookup.
34
 */
35
class Translator {
36
37
    /**
38
     * None error
39
     */
40
    const ERROR_NONE = 0;
41
    /**
42
     * File does not exist
43
     */
44
    const ERROR_DOES_NOT_EXIST = 1;
45
    /**
46
     * File has bad magic number
47
     */
48
    const ERROR_BAD_MAGIC = 2;
49
    /**
50
     * Error while reading file, probably too short
51
     */
52
    const ERROR_READING = 3;
53
54
    /**
55
     * Big endian mo file magic bytes.
56
     */
57
    const MAGIC_BE = "\x95\x04\x12\xde";
58
    /**
59
     * Little endian mo file magic bytes.
60
     */
61
    const MAGIC_LE = "\xde\x12\x04\x95";
62
63
    /**
64
     * Parse error code (0 if no error)
65
     *
66
     * @var int
67
     */
68
    public $error = Translator::ERROR_NONE;
69
70
    /**
71
     * Cache header field for plural forms
72
     *
73
     * @var string|null
74
     */
75
    private $pluralequation = NULL;
76
    /**
77
     *
78
     *
79
     * @var ExpressionLanguage|null Evaluator for plurals
80
     */
81
    private $pluralexpression = NULL;
82
    /**
83
     *
84
     *
85
     * @var int|null number of plurals
86
     */
87
    private $pluralcount = NULL;
88
    /**
89
     * Array with original -> translation mapping
90
     *
91
     * @var array
92
     */
93
    private $cache_translations = array();
94
95
    /**
96
     * Constructor
97
     *
98
     * @param string $filename Name of mo file to load
99
     */
100 23
    public function __construct($filename)
101
    {
102 23
        if (!is_readable($filename)) {
103 4
            $this->error = Translator::ERROR_DOES_NOT_EXIST;
104 4
            return;
105
        }
106
107 19
        $stream = new StringReader($filename);
108
109
        try {
110 19
            $magic = $stream->read(0, 4);
111 18
            if (strcmp($magic, Translator::MAGIC_LE) == 0) {
112 11
                $unpack = 'V';
113 18
            } elseif (strcmp($magic, Translator::MAGIC_BE) == 0) {
114 6
                $unpack = 'N';
115 6
            } else {
116 1
                $this->error = Translator::ERROR_BAD_MAGIC;
117 1
                return;
118
            }
119
120
            /* Parse header */
121 17
            $total = $stream->readint($unpack, 8);
122 14
            $originals = $stream->readint($unpack, 12);
123 14
            $translations = $stream->readint($unpack, 16);
124
125
            /* get original and translations tables */
126 14
            $table_originals = $stream->readintarray($unpack, $originals, $total * 2);
127 14
            $table_translations = $stream->readintarray($unpack, $translations, $total * 2);
128
129
            /* read all strings to the cache */
130 14
            for ($i = 0; $i < $total; $i++) {
131 14
                $original = $stream->read($table_originals[$i * 2 + 2], $table_originals[$i * 2 + 1]);
132 14
                $translation = $stream->read($table_translations[$i * 2 + 2], $table_translations[$i * 2 + 1]);
133 14
                $this->cache_translations[$original] = $translation;
134 14
            }
135 18
        } catch (ReaderException $e) {
136 4
            $this->error = Translator::ERROR_READING;
137 4
            return;
138
        }
139 14
    }
140
141
    /**
142
     * Translates a string
143
     *
144
     * @param string $msgid String to be translated
145
     *
146
     * @return string translated string (or original, if not found)
147
     */
148 20
    public function gettext($msgid)
149
    {
150 20
        if (array_key_exists($msgid, $this->cache_translations)) {
151 13
            return $this->cache_translations[$msgid];
152
        } else {
153 10
            return $msgid;
154
        }
155
    }
156
157
    /**
158
     * Sanitize plural form expression for use in ExpressionLanguage
159
     *
160
     * @param string $expr Expression to sanitize
161
     *
162
     * @return string sanitized plural form expression
163
     */
164 10
    public static function sanitizePluralExpression($expr)
165
    {
166
        // Parse equation
167 10
        $expr = explode(';', $expr);
168 10
        if (count($expr) >= 2) {
169 8
            $expr = $expr[1];
170 8
        } else {
171 2
            $expr = $expr[0];
172
        }
173 10
        $expr = trim(strtolower($expr));
174
        // Strip plural prefix
175 10
        if (substr($expr, 0, 6) === 'plural') {
176 9
            $expr = trim(substr($expr, 6));
177 9
        }
178
        // Strip equals
179 10
        if (substr($expr, 0, 1) === '=') {
180 9
            $expr = trim(substr($expr, 1));
181 9
        }
182 10
        return $expr;
183
    }
184
185
    /**
186
     * Extracts number of plurals from plurals form expression
187
     *
188
     * @param string $expr Expression to process
189
     *
190
     * @return int Total number of plurals
191
     */
192 9
    public static function extractPluralCount($expr)
193
    {
194 9
        $parts = explode(';', $expr, 2);
195 9
        $nplurals = explode('=', trim($parts[0]), 2);
196 9
        if (strtolower(trim($nplurals[0])) != 'nplurals') {
197 2
            return 1;
198
        }
199 7
        return intval($nplurals[1]);
200
    }
201
202
    /**
203
     * Parse full PO header and extract only plural forms line.
204
     *
205
     * @param string $header Gettext header
206
     *
207
     * @return string verbatim plural form header field
208
     */
209 8
    public static function extractPluralsForms($header)
210
    {
211 8
        $headers = explode("\n", $header);
212 8
        $expr = 'nplurals=2; plural=n == 1 ? 0 : 1;';
213 8
        foreach ($headers as $header) {
214 8
            if (stripos($header, 'Plural-Forms:') === 0) {
215 6
                $expr = substr($header, 13);
216 6
            }
217 8
        }
218 8
        return $expr;
219
    }
220
221
    /**
222
     * Get possible plural forms from MO header
223
     *
224
     * @return string plural form header
225
     */
226 5
    private function getPluralForms()
227
    {
228
        // lets assume message number 0 is header
229
        // this is true, right?
230
231
        // cache header field for plural forms
232 5
        if (is_null($this->pluralequation)) {
233 4
            $header = $this->cache_translations[''];
234 4
            $expr = $this->extractPluralsForms($header);
235 4
            $this->pluralequation = $this->sanitizePluralExpression($expr);
236 4
            $this->pluralcount = $this->extractPluralCount($expr);
237 4
        }
238 5
        return $this->pluralequation;
239
    }
240
241
    /**
242
     * Detects which plural form to take
243
     *
244
     * @param int $n count of objects
245
     *
246
     * @return int array index of the right plural form
247
     */
248 5
    private function selectString($n)
249
    {
250 5
        if (is_null($this->pluralexpression)) {
251 4
            $this->pluralexpression = new ExpressionLanguage();
252 4
        }
253 5
        $plural = $this->pluralexpression->evaluate(
254 5
            $this->getPluralForms(), array('n' => $n)
255 5
        );
256
257 5
        if ($plural >= $this->pluralcount) {
258 1
            $plural = $this->pluralcount - 1;
259 1
        }
260 5
        return $plural;
261
    }
262
263
    /**
264
     * Plural version of gettext
265
     *
266
     * @param string $msgid       Single form
267
     * @param string $msgidPlural Plural form
268
     * @param string $number      Number of objects
269
     *
270
     * @return string translated plural form
271
     */
272 12
    public function ngettext($msgid, $msgidPlural, $number)
273
    {
274
        // this should contains all strings separated by NULLs
275 12
        $key = implode(chr(0), array($msgid, $msgidPlural));
276 12
        if (!array_key_exists($key, $this->cache_translations)) {
277 12
            return ($number != 1) ? $msgidPlural : $msgid;
278
        }
279
280
        // find out the appropriate form
281 5
        $select = $this->selectString($number);
282
283 5
        $result = $this->cache_translations[$key];
284 5
        $list = explode(chr(0), $result);
285 5
        return $list[$select];
286
    }
287
288
    /**
289
     * Translate with context
290
     *
291
     * @param string $msgctxt      Context
292
     * @param string $msgid        String to be translated
293
     *
294
     * @return string translated plural form
295
     */
296 10
    public function pgettext($msgctxt, $msgid)
297
    {
298 10
        $key = implode(chr(4), array($msgctxt, $msgid));
299 10
        $ret = $this->gettext($key);
300 10
        if (strpos($ret, chr(4)) !== false) {
301 5
            return $msgid;
302
        } else {
303 5
            return $ret;
304
        }
305
    }
306
307
    /**
308
     * Plural version of pgettext
309
     *
310
     * @param string $msgctxt     Context
311
     * @param string $msgid       Single form
312
     * @param string $msgidPlural Plural form
313
     * @param string $number      Number of objects
314
     *
315
     * @return string translated plural form
316
     */
317 4
    public function npgettext($msgctxt, $msgid, $msgidPlural, $number)
318
    {
319 4
        $key = implode(chr(4), array($msgctxt, $msgid));
320 4
        $ret = $this->ngettext($key, $msgidPlural, $number);
321 4
        if (strpos($ret, chr(4)) !== false) {
322 1
            return $msgid;
323
        } else {
324 3
            return $ret;
325
        }
326
    }
327
}
328