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 PhpMyAdmin\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 = self::ERROR_NONE; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* Cache header field for plural forms. |
72
|
|
|
* |
73
|
|
|
* @var string|null |
74
|
|
|
*/ |
75
|
|
|
private $pluralequation = null; |
76
|
|
|
/** |
77
|
|
|
* @var ExpressionLanguage|null Evaluator for plurals |
78
|
|
|
*/ |
79
|
|
|
private $pluralexpression = null; |
80
|
|
|
/** |
81
|
|
|
* @var int|null number of plurals |
82
|
|
|
*/ |
83
|
|
|
private $pluralcount = null; |
84
|
|
|
/** |
85
|
|
|
* Array with original -> translation mapping. |
86
|
|
|
* |
87
|
|
|
* @var array |
88
|
|
|
*/ |
89
|
|
|
private $cache_translations = array(); |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* Constructor. |
93
|
|
|
* |
94
|
|
|
* @param string $filename Name of mo file to load |
95
|
|
|
*/ |
96
|
45 |
|
public function __construct($filename) |
97
|
|
|
{ |
98
|
45 |
|
if (!is_readable($filename)) { |
99
|
6 |
|
$this->error = self::ERROR_DOES_NOT_EXIST; |
100
|
|
|
|
101
|
6 |
|
return; |
102
|
|
|
} |
103
|
|
|
|
104
|
39 |
|
$stream = new StringReader($filename); |
105
|
|
|
|
106
|
|
|
try { |
107
|
39 |
|
$magic = $stream->read(0, 4); |
108
|
38 |
|
if (strcmp($magic, self::MAGIC_LE) == 0) { |
109
|
30 |
|
$unpack = 'V'; |
110
|
8 |
|
} elseif (strcmp($magic, self::MAGIC_BE) == 0) { |
111
|
7 |
|
$unpack = 'N'; |
112
|
|
|
} else { |
113
|
1 |
|
$this->error = self::ERROR_BAD_MAGIC; |
114
|
|
|
|
115
|
1 |
|
return; |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
/* Parse header */ |
119
|
37 |
|
$total = $stream->readint($unpack, 8); |
120
|
37 |
|
$originals = $stream->readint($unpack, 12); |
121
|
34 |
|
$translations = $stream->readint($unpack, 16); |
122
|
|
|
|
123
|
|
|
/* get original and translations tables */ |
124
|
34 |
|
$table_originals = $stream->readintarray($unpack, $originals, $total * 2); |
125
|
33 |
|
$table_translations = $stream->readintarray($unpack, $translations, $total * 2); |
126
|
|
|
|
127
|
|
|
/* read all strings to the cache */ |
128
|
33 |
|
for ($i = 0; $i < $total; ++$i) { |
129
|
33 |
|
$original = $stream->read($table_originals[$i * 2 + 2], $table_originals[$i * 2 + 1]); |
130
|
33 |
|
$translation = $stream->read($table_translations[$i * 2 + 2], $table_translations[$i * 2 + 1]); |
131
|
33 |
|
$this->cache_translations[$original] = $translation; |
132
|
|
|
} |
133
|
5 |
|
} catch (ReaderException $e) { |
134
|
5 |
|
$this->error = self::ERROR_READING; |
135
|
|
|
|
136
|
5 |
|
return; |
137
|
|
|
} |
138
|
33 |
|
} |
139
|
|
|
|
140
|
|
|
/** |
141
|
|
|
* Translates a string. |
142
|
|
|
* |
143
|
|
|
* @param string $msgid String to be translated |
144
|
|
|
* |
145
|
|
|
* @return string translated string (or original, if not found) |
146
|
|
|
*/ |
147
|
30 |
|
public function gettext($msgid) |
148
|
|
|
{ |
149
|
30 |
|
if (array_key_exists($msgid, $this->cache_translations)) { |
150
|
21 |
|
return $this->cache_translations[$msgid]; |
151
|
|
|
} |
152
|
|
|
|
153
|
15 |
|
return $msgid; |
154
|
|
|
} |
155
|
|
|
|
156
|
|
|
/** |
157
|
|
|
* Check if a string is translated. |
158
|
|
|
* |
159
|
|
|
* @param string $msgid String to be checked |
160
|
|
|
* |
161
|
|
|
* @return bool |
162
|
|
|
*/ |
163
|
6 |
|
public function exists($msgid) |
164
|
|
|
{ |
165
|
6 |
|
return array_key_exists($msgid, $this->cache_translations); |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
/** |
169
|
|
|
* Sanitize plural form expression for use in ExpressionLanguage. |
170
|
|
|
* |
171
|
|
|
* @param string $expr Expression to sanitize |
172
|
|
|
* |
173
|
|
|
* @return string sanitized plural form expression |
174
|
|
|
*/ |
175
|
14 |
|
public static function sanitizePluralExpression($expr) |
176
|
|
|
{ |
177
|
|
|
// Parse equation |
178
|
14 |
|
$expr = explode(';', $expr); |
179
|
14 |
|
if (count($expr) >= 2) { |
180
|
11 |
|
$expr = $expr[1]; |
181
|
|
|
} else { |
182
|
3 |
|
$expr = $expr[0]; |
183
|
|
|
} |
184
|
14 |
|
$expr = trim(strtolower($expr)); |
185
|
|
|
// Strip plural prefix |
186
|
14 |
|
if (substr($expr, 0, 6) === 'plural') { |
187
|
12 |
|
$expr = ltrim(substr($expr, 6)); |
188
|
|
|
} |
189
|
|
|
// Strip equals |
190
|
14 |
|
if (substr($expr, 0, 1) === '=') { |
191
|
12 |
|
$expr = ltrim(substr($expr, 1)); |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
// Cleanup from unwanted chars |
195
|
14 |
|
$expr = preg_replace('@[^n0-9:\(\)\?=!<>/%&| ]@', '', $expr); |
196
|
|
|
|
197
|
14 |
|
return $expr; |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
/** |
201
|
|
|
* Extracts number of plurals from plurals form expression. |
202
|
|
|
* |
203
|
|
|
* @param string $expr Expression to process |
204
|
|
|
* |
205
|
|
|
* @return int Total number of plurals |
206
|
|
|
*/ |
207
|
13 |
|
public static function extractPluralCount($expr) |
208
|
|
|
{ |
209
|
13 |
|
$parts = explode(';', $expr, 2); |
210
|
13 |
|
$nplurals = explode('=', trim($parts[0]), 2); |
211
|
13 |
|
if (strtolower(rtrim($nplurals[0])) != 'nplurals') { |
212
|
2 |
|
return 1; |
213
|
|
|
} |
214
|
11 |
|
if (count($nplurals) == 1) { |
215
|
1 |
|
return 1; |
216
|
|
|
} |
217
|
|
|
|
218
|
10 |
|
return intval($nplurals[1]); |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
/** |
222
|
|
|
* Parse full PO header and extract only plural forms line. |
223
|
|
|
* |
224
|
|
|
* @param string $header Gettext header |
225
|
|
|
* |
226
|
|
|
* @return string verbatim plural form header field |
227
|
|
|
*/ |
228
|
11 |
|
public static function extractPluralsForms($header) |
229
|
|
|
{ |
230
|
11 |
|
$headers = explode("\n", $header); |
231
|
11 |
|
$expr = 'nplurals=2; plural=n == 1 ? 0 : 1;'; |
232
|
11 |
|
foreach ($headers as $header) { |
233
|
11 |
|
if (stripos($header, 'Plural-Forms:') === 0) { |
234
|
7 |
|
$expr = substr($header, 13); |
235
|
|
|
} |
236
|
|
|
} |
237
|
|
|
|
238
|
11 |
|
return $expr; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
/** |
242
|
|
|
* Get possible plural forms from MO header. |
243
|
|
|
* |
244
|
|
|
* @return string plural form header |
245
|
|
|
*/ |
246
|
8 |
|
private function getPluralForms() |
247
|
|
|
{ |
248
|
|
|
// lets assume message number 0 is header |
249
|
|
|
// this is true, right? |
250
|
|
|
|
251
|
|
|
// cache header field for plural forms |
252
|
8 |
|
if (is_null($this->pluralequation)) { |
253
|
7 |
|
if (isset($this->cache_translations[''])) { |
254
|
5 |
|
$header = $this->cache_translations['']; |
255
|
|
|
} else { |
256
|
2 |
|
$header = ''; |
257
|
|
|
} |
258
|
7 |
|
$expr = $this->extractPluralsForms($header); |
259
|
7 |
|
$this->pluralequation = $this->sanitizePluralExpression($expr); |
260
|
7 |
|
$this->pluralcount = $this->extractPluralCount($expr); |
261
|
|
|
} |
262
|
|
|
|
263
|
8 |
|
return $this->pluralequation; |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* Detects which plural form to take. |
268
|
|
|
* |
269
|
|
|
* @param int $n count of objects |
270
|
|
|
* |
271
|
|
|
* @return int array index of the right plural form |
272
|
|
|
*/ |
273
|
8 |
|
private function selectString($n) |
274
|
|
|
{ |
275
|
8 |
|
if (is_null($this->pluralexpression)) { |
276
|
7 |
|
$this->pluralexpression = new ExpressionLanguage(); |
277
|
|
|
} |
278
|
|
|
try { |
279
|
8 |
|
$plural = $this->pluralexpression->evaluate( |
280
|
8 |
|
$this->getPluralForms(), array('n' => $n) |
281
|
|
|
); |
282
|
1 |
|
} catch (\Exception $e) { |
283
|
1 |
|
$plural = 0; |
284
|
|
|
} |
285
|
|
|
|
286
|
8 |
|
if ($plural >= $this->pluralcount) { |
287
|
1 |
|
$plural = $this->pluralcount - 1; |
288
|
|
|
} |
289
|
|
|
|
290
|
8 |
|
return $plural; |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
/** |
294
|
|
|
* Plural version of gettext. |
295
|
|
|
* |
296
|
|
|
* @param string $msgid Single form |
297
|
|
|
* @param string $msgidPlural Plural form |
298
|
|
|
* @param int $number Number of objects |
299
|
|
|
* |
300
|
|
|
* @return string translated plural form |
301
|
|
|
*/ |
302
|
18 |
|
public function ngettext($msgid, $msgidPlural, $number) |
303
|
|
|
{ |
304
|
|
|
// this should contains all strings separated by NULLs |
305
|
18 |
|
$key = implode(chr(0), array($msgid, $msgidPlural)); |
306
|
18 |
|
if (!array_key_exists($key, $this->cache_translations)) { |
307
|
18 |
|
return ($number != 1) ? $msgidPlural : $msgid; |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
// find out the appropriate form |
311
|
8 |
|
$select = $this->selectString($number); |
312
|
|
|
|
313
|
8 |
|
$result = $this->cache_translations[$key]; |
314
|
8 |
|
$list = explode(chr(0), $result); |
315
|
|
|
|
316
|
8 |
|
if (!isset($list[$select])) { |
317
|
1 |
|
return $list[0]; |
318
|
|
|
} |
319
|
|
|
|
320
|
8 |
|
return $list[$select]; |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
/** |
324
|
|
|
* Translate with context. |
325
|
|
|
* |
326
|
|
|
* @param string $msgctxt Context |
327
|
|
|
* @param string $msgid String to be translated |
328
|
|
|
* |
329
|
|
|
* @return string translated plural form |
330
|
|
|
*/ |
331
|
14 |
View Code Duplication |
public function pgettext($msgctxt, $msgid) |
|
|
|
|
332
|
|
|
{ |
333
|
14 |
|
$key = implode(chr(4), array($msgctxt, $msgid)); |
334
|
14 |
|
$ret = $this->gettext($key); |
335
|
14 |
|
if (strpos($ret, chr(4)) !== false) { |
336
|
6 |
|
return $msgid; |
337
|
|
|
} |
338
|
|
|
|
339
|
8 |
|
return $ret; |
340
|
|
|
} |
341
|
|
|
|
342
|
|
|
/** |
343
|
|
|
* Plural version of pgettext. |
344
|
|
|
* |
345
|
|
|
* @param string $msgctxt Context |
346
|
|
|
* @param string $msgid Single form |
347
|
|
|
* @param string $msgidPlural Plural form |
348
|
|
|
* @param int $number Number of objects |
349
|
|
|
* |
350
|
|
|
* @return string translated plural form |
351
|
|
|
*/ |
352
|
4 |
View Code Duplication |
public function npgettext($msgctxt, $msgid, $msgidPlural, $number) |
|
|
|
|
353
|
|
|
{ |
354
|
4 |
|
$key = implode(chr(4), array($msgctxt, $msgid)); |
355
|
4 |
|
$ret = $this->ngettext($key, $msgidPlural, $number); |
356
|
4 |
|
if (strpos($ret, chr(4)) !== false) { |
357
|
1 |
|
return $msgid; |
358
|
|
|
} |
359
|
|
|
|
360
|
3 |
|
return $ret; |
361
|
|
|
} |
362
|
|
|
|
363
|
|
|
/** |
364
|
|
|
* Set translation in place |
365
|
|
|
* |
366
|
|
|
* @param string $msgid String to be set |
367
|
|
|
* @param string $msgstr Translation |
368
|
|
|
* |
369
|
|
|
* @return void |
370
|
|
|
*/ |
371
|
1 |
|
public function setTranslation($msgid, $msgstr) |
372
|
|
|
{ |
373
|
1 |
|
$this->cache_translations[$msgid] = $msgstr; |
374
|
1 |
|
} |
375
|
|
|
} |
376
|
|
|
|
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.