1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* This file is part of the POMO package. |
4
|
|
|
* |
5
|
|
|
* @copyright 2014 POMO |
6
|
|
|
* @license GPL |
7
|
|
|
*/ |
8
|
|
|
|
9
|
|
|
namespace POMO; |
10
|
|
|
|
11
|
|
|
use POMO\Streams\Reader; |
12
|
|
|
use POMO\Streams\FileReader; |
13
|
|
|
use POMO\Translations\GettextTranslations; |
14
|
|
|
use POMO\Translations\EntryTranslations; |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* Class for working with MO files |
18
|
|
|
*/ |
19
|
|
|
class MO extends GettextTranslations |
20
|
|
|
{ |
21
|
|
|
public $_nplurals = 2; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* Fills up with the entries from MO file $filename |
25
|
|
|
* |
26
|
|
|
* @param string $filename MO file to load |
27
|
|
|
* @return bool Success |
28
|
|
|
*/ |
29
|
|
|
public function import_from_file($filename) |
30
|
|
|
{ |
31
|
|
|
$reader = new FileReader($filename); |
32
|
|
|
if (!$reader->is_resource()) { |
33
|
|
|
return false; |
34
|
|
|
} |
35
|
|
|
|
36
|
|
|
return $this->import_from_reader($reader); |
37
|
|
|
} |
38
|
|
|
|
39
|
|
|
public function export_to_file($filename) |
40
|
|
|
{ |
41
|
|
|
$fh = fopen($filename, 'wb'); |
42
|
|
|
if (!$fh) { |
43
|
|
|
return false; |
44
|
|
|
} |
45
|
|
|
$res = $this->export_to_file_handle($fh); |
46
|
|
|
fclose($fh); |
47
|
|
|
|
48
|
|
|
return $res; |
49
|
|
|
} |
50
|
|
|
|
51
|
|
|
public function export() |
52
|
|
|
{ |
53
|
|
|
$tmp_fh = fopen("php://temp", 'r+'); |
54
|
|
|
if (!$tmp_fh) { |
55
|
|
|
return false; |
56
|
|
|
} |
57
|
|
|
$this->export_to_file_handle($tmp_fh); |
58
|
|
|
rewind($tmp_fh); |
59
|
|
|
|
60
|
|
|
return stream_get_contents($tmp_fh); |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
public function is_entry_good_for_export($entry) |
64
|
|
|
{ |
65
|
|
|
if (empty($entry->translations)) { |
66
|
|
|
return false; |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
if (!array_filter($entry->translations)) { |
70
|
|
|
return false; |
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
return true; |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
public function export_to_file_handle($fh) |
77
|
|
|
{ |
78
|
|
|
$entries = array_filter( |
79
|
|
|
$this->entries, |
80
|
|
|
array($this, 'is_entry_good_for_export') |
81
|
|
|
); |
82
|
|
|
ksort($entries); |
83
|
|
|
$magic = 0x950412de; |
84
|
|
|
$revision = 0; |
85
|
|
|
$total = count($entries) + 1; // all the headers are one entry |
86
|
|
|
$originals_lenghts_addr = 28; |
87
|
|
|
$translations_lenghts_addr = $originals_lenghts_addr + 8 * $total; |
88
|
|
|
$size_of_hash = 0; |
89
|
|
|
$hash_addr = $translations_lenghts_addr + 8 * $total; |
90
|
|
|
$current_addr = $hash_addr; |
91
|
|
|
fwrite($fh, pack( |
92
|
|
|
'V*', |
93
|
|
|
$magic, |
94
|
|
|
$revision, |
95
|
|
|
$total, |
96
|
|
|
$originals_lenghts_addr, |
97
|
|
|
$translations_lenghts_addr, |
98
|
|
|
$size_of_hash, |
99
|
|
|
$hash_addr |
100
|
|
|
)); |
101
|
|
|
fseek($fh, $originals_lenghts_addr); |
102
|
|
|
|
103
|
|
|
// headers' msgid is an empty string |
104
|
|
|
fwrite($fh, pack('VV', 0, $current_addr)); |
105
|
|
|
$current_addr++; |
106
|
|
|
$originals_table = chr(0); |
107
|
|
|
|
108
|
|
|
$reader = new Reader(); |
109
|
|
|
|
110
|
|
View Code Duplication |
foreach ($entries as $entry) { |
|
|
|
|
111
|
|
|
$originals_table .= $this->export_original($entry) . chr(0); |
112
|
|
|
$length = $reader->strlen($this->export_original($entry)); |
113
|
|
|
fwrite($fh, pack('VV', $length, $current_addr)); |
114
|
|
|
$current_addr += $length + 1; // account for the NULL byte after |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
$exported_headers = $this->export_headers(); |
118
|
|
|
fwrite($fh, pack( |
119
|
|
|
'VV', |
120
|
|
|
$reader->strlen($exported_headers), |
121
|
|
|
$current_addr) |
122
|
|
|
); |
123
|
|
|
$current_addr += strlen($exported_headers) + 1; |
124
|
|
|
$translations_table = $exported_headers . chr(0); |
125
|
|
|
|
126
|
|
View Code Duplication |
foreach ($entries as $entry) { |
|
|
|
|
127
|
|
|
$translations_table .= $this->export_translations($entry).chr(0); |
128
|
|
|
$length = $reader->strlen($this->export_translations($entry)); |
129
|
|
|
fwrite($fh, pack('VV', $length, $current_addr)); |
130
|
|
|
$current_addr += $length + 1; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
fwrite($fh, $originals_table); |
134
|
|
|
fwrite($fh, $translations_table); |
135
|
|
|
|
136
|
|
|
return true; |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
public function export_original($entry) |
140
|
|
|
{ |
141
|
|
|
//TODO: warnings for control characters |
142
|
|
|
$exported = $entry->singular; |
143
|
|
|
if ($entry->is_plural) { |
144
|
|
|
$exported .= chr(0).$entry->plural; |
145
|
|
|
} |
146
|
|
|
if (!is_null($entry->context)) { |
147
|
|
|
$exported = $entry->context.chr(4).$exported; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
return $exported; |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
public function export_translations($entry) |
154
|
|
|
{ |
155
|
|
|
//TODO: warnings for control characters |
156
|
|
|
return implode(chr(0), $entry->translations); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
public function export_headers() |
160
|
|
|
{ |
161
|
|
|
$exported = ''; |
162
|
|
|
foreach ($this->headers as $header => $value) { |
163
|
|
|
$exported .= "$header: $value\n"; |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
return $exported; |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
public function get_byteorder($magic) |
170
|
|
|
{ |
171
|
|
|
// The magic is 0x950412de |
172
|
|
|
$magic_little = (int) - 1794895138; |
173
|
|
|
$magic_little_64 = (int) 2500072158; |
174
|
|
|
// 0xde120495 |
175
|
|
|
$magic_big = ((int) - 569244523) & 0xFFFFFFFF; |
176
|
|
|
if ($magic_little == $magic || $magic_little_64 == $magic) { |
177
|
|
|
return 'little'; |
178
|
|
|
} elseif ($magic_big == $magic) { |
179
|
|
|
return 'big'; |
180
|
|
|
} else { |
181
|
|
|
return false; |
182
|
|
|
} |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
public function import_from_reader($reader) |
186
|
|
|
{ |
187
|
|
|
$endian_string = $this->get_byteorder($reader->readint32()); |
188
|
|
|
if (false === $endian_string) { |
189
|
|
|
return false; |
190
|
|
|
} |
191
|
|
|
$reader->setEndian($endian_string); |
192
|
|
|
|
193
|
|
|
$endian = ('big' == $endian_string) ? 'N' : 'V'; |
194
|
|
|
|
195
|
|
|
$header = $reader->read(24); |
196
|
|
|
if ($reader->strlen($header) != 24) { |
197
|
|
|
return false; |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
// parse header |
201
|
|
|
$header = unpack("{$endian}revision/{$endian}total/{$endian}originals_lenghts_addr/{$endian}translations_lenghts_addr/{$endian}hash_length/{$endian}hash_addr", $header); |
202
|
|
|
if (!is_array($header)) { |
203
|
|
|
return false; |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
// support revision 0 of MO format specs, only |
207
|
|
|
if ($header['revision'] != 0) { |
208
|
|
|
return false; |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
// seek to data blocks |
212
|
|
|
$reader->seekto($header['originals_lenghts_addr']); |
213
|
|
|
|
214
|
|
|
// read originals' indices |
215
|
|
|
$originals_lengths_length = $header['translations_lenghts_addr'] - $header['originals_lenghts_addr']; |
216
|
|
|
if ($originals_lengths_length != $header['total'] * 8) { |
217
|
|
|
return false; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
$originals = $reader->read($originals_lengths_length); |
221
|
|
|
if ($reader->strlen($originals) != $originals_lengths_length) { |
222
|
|
|
return false; |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
// read translations' indices |
226
|
|
|
$translations_lenghts_length = $header['hash_addr'] - $header['translations_lenghts_addr']; |
227
|
|
|
if ($translations_lenghts_length != $header['total'] * 8) { |
228
|
|
|
return false; |
229
|
|
|
} |
230
|
|
|
|
231
|
|
|
$translations = $reader->read($translations_lenghts_length); |
232
|
|
|
if ($reader->strlen($translations) != $translations_lenghts_length) { |
233
|
|
|
return false; |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
// transform raw data into set of indices |
237
|
|
|
$originals = $reader->str_split($originals, 8); |
238
|
|
|
$translations = $reader->str_split($translations, 8); |
239
|
|
|
|
240
|
|
|
// skip hash table |
241
|
|
|
$strings_addr = $header['hash_addr'] + $header['hash_length'] * 4; |
242
|
|
|
|
243
|
|
|
$reader->seekto($strings_addr); |
244
|
|
|
|
245
|
|
|
$strings = $reader->read_all(); |
246
|
|
|
$reader->close(); |
247
|
|
|
|
248
|
|
|
for ($i = 0; $i < $header['total']; $i++) { |
249
|
|
|
$o = unpack("{$endian}length/{$endian}pos", $originals[$i]); |
250
|
|
|
$t = unpack("{$endian}length/{$endian}pos", $translations[$i]); |
251
|
|
|
if (!$o || !$t) { |
|
|
|
|
252
|
|
|
return false; |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
// adjust offset due to reading strings to separate space before |
256
|
|
|
$o['pos'] -= $strings_addr; |
257
|
|
|
$t['pos'] -= $strings_addr; |
258
|
|
|
|
259
|
|
|
$original = $reader->substr($strings, $o['pos'], $o['length']); |
260
|
|
|
$translation = $reader->substr($strings, $t['pos'], $t['length']); |
261
|
|
|
|
262
|
|
|
if ('' === $original) { |
263
|
|
|
$this->set_headers($this->make_headers($translation)); |
264
|
|
|
} else { |
265
|
|
|
$entry = &$this->make_entry($original, $translation); |
266
|
|
|
$this->entries[$entry->key()] = &$entry; |
267
|
|
|
} |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
return true; |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
/** |
274
|
|
|
* Build a EntryTranslations from original string and translation strings, |
275
|
|
|
* found in a MO file |
276
|
|
|
* |
277
|
|
|
* @param string $original original string to translate from MO file. |
278
|
|
|
* Might contain 0x04 as context separator or |
279
|
|
|
* 0x00 as singular/plural separator |
280
|
|
|
* @param string $translation translation string from MO file.Might contain |
281
|
|
|
* 0x00 as a plural translations separator |
282
|
|
|
* @retrun EntryTranslations New entry |
283
|
|
|
*/ |
284
|
|
|
public static function &make_entry($original, $translation) { |
285
|
|
|
$entry = new EntryTranslations(); |
286
|
|
|
// look for context |
287
|
|
|
$parts = explode(chr(4), $original); |
288
|
|
|
if (isset($parts[1])) { |
289
|
|
|
$original = $parts[1]; |
290
|
|
|
$entry->context = $parts[0]; |
291
|
|
|
} |
292
|
|
|
// look for plural original |
293
|
|
|
$parts = explode(chr(0), $original); |
294
|
|
|
$entry->singular = $parts[0]; |
295
|
|
|
if (isset($parts[1])) { |
296
|
|
|
$entry->is_plural = true; |
297
|
|
|
$entry->plural = $parts[1]; |
298
|
|
|
} |
299
|
|
|
// plural translations are also separated by \0 |
300
|
|
|
$entry->translations = explode(chr(0), $translation); |
301
|
|
|
|
302
|
|
|
return $entry; |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
public function select_plural_form($count) |
306
|
|
|
{ |
307
|
|
|
return $this->gettext_select_plural_form($count); |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
public function get_plural_forms_count() |
311
|
|
|
{ |
312
|
|
|
return $this->_nplurals; |
313
|
|
|
} |
314
|
|
|
} |
315
|
|
|
|
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.