]> CyberLeo.Net >> Repos - Github/YOURLS.git/blob - includes/pomo/mo.php
Translation API! zomigod. First pass. See Issue 52.
[Github/YOURLS.git] / includes / pomo / mo.php
1 <?php\r
2 /**\r
3  * Class for working with MO files\r
4  *\r
5  * @version $Id: mo.php 718 2012-10-31 00:32:02Z nbachiyski $\r
6  * @package pomo\r
7  * @subpackage mo\r
8  */\r
9 \r
10 require_once dirname(__FILE__) . '/translations.php';\r
11 require_once dirname(__FILE__) . '/streams.php';\r
12 \r
13 if ( !class_exists( 'MO' ) ):\r
14 class MO extends Gettext_Translations {\r
15 \r
16         var $_nplurals = 2;\r
17 \r
18         /**\r
19          * Fills up with the entries from MO file $filename\r
20          *\r
21          * @param string $filename MO file to load\r
22          */\r
23         function import_from_file($filename) {\r
24                 $reader = new POMO_FileReader($filename);\r
25                 if (!$reader->is_resource())\r
26                         return false;\r
27                 return $this->import_from_reader($reader);\r
28         }\r
29 \r
30         function export_to_file($filename) {\r
31                 $fh = fopen($filename, 'wb');\r
32                 if ( !$fh ) return false;\r
33                 $res = $this->export_to_file_handle( $fh );\r
34                 fclose($fh);\r
35                 return $res;\r
36         }\r
37 \r
38         function export() {\r
39                 $tmp_fh = fopen("php://temp", 'r+');\r
40                 if ( !$tmp_fh ) return false;\r
41                 $this->export_to_file_handle( $tmp_fh );\r
42                 rewind( $tmp_fh );\r
43                 return stream_get_contents( $tmp_fh );\r
44         }\r
45 \r
46         function is_entry_good_for_export( $entry ) {\r
47                 if ( empty( $entry->translations ) ) {\r
48                         return false;\r
49                 }\r
50 \r
51                 if ( !array_filter( $entry->translations ) ) {\r
52                         return false;\r
53                 }\r
54 \r
55                 return true;\r
56         }\r
57 \r
58         function export_to_file_handle($fh) {\r
59                 $entries = array_filter( $this->entries, array( $this, 'is_entry_good_for_export' ) );\r
60                 ksort($entries);\r
61                 $magic = 0x950412de;\r
62                 $revision = 0;\r
63                 $total = count($entries) + 1; // all the headers are one entry\r
64                 $originals_lenghts_addr = 28;\r
65                 $translations_lenghts_addr = $originals_lenghts_addr + 8 * $total;\r
66                 $size_of_hash = 0;\r
67                 $hash_addr = $translations_lenghts_addr + 8 * $total;\r
68                 $current_addr = $hash_addr;\r
69                 fwrite($fh, pack('V*', $magic, $revision, $total, $originals_lenghts_addr,\r
70                         $translations_lenghts_addr, $size_of_hash, $hash_addr));\r
71                 fseek($fh, $originals_lenghts_addr);\r
72 \r
73                 // headers' msgid is an empty string\r
74                 fwrite($fh, pack('VV', 0, $current_addr));\r
75                 $current_addr++;\r
76                 $originals_table = chr(0);\r
77 \r
78                 foreach($entries as $entry) {\r
79                         $originals_table .= $this->export_original($entry) . chr(0);\r
80                         $length = strlen($this->export_original($entry));\r
81                         fwrite($fh, pack('VV', $length, $current_addr));\r
82                         $current_addr += $length + 1; // account for the NULL byte after\r
83                 }\r
84 \r
85                 $exported_headers = $this->export_headers();\r
86                 fwrite($fh, pack('VV', strlen($exported_headers), $current_addr));\r
87                 $current_addr += strlen($exported_headers) + 1;\r
88                 $translations_table = $exported_headers . chr(0);\r
89 \r
90                 foreach($entries as $entry) {\r
91                         $translations_table .= $this->export_translations($entry) . chr(0);\r
92                         $length = strlen($this->export_translations($entry));\r
93                         fwrite($fh, pack('VV', $length, $current_addr));\r
94                         $current_addr += $length + 1;\r
95                 }\r
96 \r
97                 fwrite($fh, $originals_table);\r
98                 fwrite($fh, $translations_table);\r
99                 return true;\r
100         }\r
101 \r
102         function export_original($entry) {\r
103                 //TODO: warnings for control characters\r
104                 $exported = $entry->singular;\r
105                 if ($entry->is_plural) $exported .= chr(0).$entry->plural;\r
106                 if (!is_null($entry->context)) $exported = $entry->context . chr(4) . $exported;\r
107                 return $exported;\r
108         }\r
109 \r
110         function export_translations($entry) {\r
111                 //TODO: warnings for control characters\r
112                 return implode(chr(0), $entry->translations);\r
113         }\r
114 \r
115         function export_headers() {\r
116                 $exported = '';\r
117                 foreach($this->headers as $header => $value) {\r
118                         $exported.= "$header: $value\n";\r
119                 }\r
120                 return $exported;\r
121         }\r
122 \r
123         function get_byteorder($magic) {\r
124                 // The magic is 0x950412de\r
125 \r
126                 // bug in PHP 5.0.2, see https://savannah.nongnu.org/bugs/?func=detailitem&item_id=10565\r
127                 $magic_little = (int) - 1794895138;\r
128                 $magic_little_64 = (int) 2500072158;\r
129                 // 0xde120495\r
130                 $magic_big = ((int) - 569244523) & 0xFFFFFFFF;\r
131                 if ($magic_little == $magic || $magic_little_64 == $magic) {\r
132                         return 'little';\r
133                 } else if ($magic_big == $magic) {\r
134                         return 'big';\r
135                 } else {\r
136                         return false;\r
137                 }\r
138         }\r
139 \r
140         function import_from_reader($reader) {\r
141                 $endian_string = MO::get_byteorder($reader->readint32());\r
142                 if (false === $endian_string) {\r
143                         return false;\r
144                 }\r
145                 $reader->setEndian($endian_string);\r
146 \r
147                 $endian = ('big' == $endian_string)? 'N' : 'V';\r
148 \r
149                 $header = $reader->read(24);\r
150                 if ($reader->strlen($header) != 24)\r
151                         return false;\r
152 \r
153                 // parse header\r
154                 $header = unpack("{$endian}revision/{$endian}total/{$endian}originals_lenghts_addr/{$endian}translations_lenghts_addr/{$endian}hash_length/{$endian}hash_addr", $header);\r
155                 if (!is_array($header))\r
156                         return false;\r
157 \r
158                 extract( $header );\r
159 \r
160                 // support revision 0 of MO format specs, only\r
161                 if ($revision != 0)\r
162                         return false;\r
163 \r
164                 // seek to data blocks\r
165                 $reader->seekto($originals_lenghts_addr);\r
166 \r
167                 // read originals' indices\r
168                 $originals_lengths_length = $translations_lenghts_addr - $originals_lenghts_addr;\r
169                 if ( $originals_lengths_length != $total * 8 )\r
170                         return false;\r
171 \r
172                 $originals = $reader->read($originals_lengths_length);\r
173                 if ( $reader->strlen( $originals ) != $originals_lengths_length )\r
174                         return false;\r
175 \r
176                 // read translations' indices\r
177                 $translations_lenghts_length = $hash_addr - $translations_lenghts_addr;\r
178                 if ( $translations_lenghts_length != $total * 8 )\r
179                         return false;\r
180 \r
181                 $translations = $reader->read($translations_lenghts_length);\r
182                 if ( $reader->strlen( $translations ) != $translations_lenghts_length )\r
183                         return false;\r
184 \r
185                 // transform raw data into set of indices\r
186                 $originals    = $reader->str_split( $originals, 8 );\r
187                 $translations = $reader->str_split( $translations, 8 );\r
188 \r
189                 // skip hash table\r
190                 $strings_addr = $hash_addr + $hash_length * 4;\r
191 \r
192                 $reader->seekto($strings_addr);\r
193 \r
194                 $strings = $reader->read_all();\r
195                 $reader->close();\r
196 \r
197                 for ( $i = 0; $i < $total; $i++ ) {\r
198                         $o = unpack( "{$endian}length/{$endian}pos", $originals[$i] );\r
199                         $t = unpack( "{$endian}length/{$endian}pos", $translations[$i] );\r
200                         if ( !$o || !$t ) return false;\r
201 \r
202                         // adjust offset due to reading strings to separate space before\r
203                         $o['pos'] -= $strings_addr;\r
204                         $t['pos'] -= $strings_addr;\r
205 \r
206                         $original    = $reader->substr( $strings, $o['pos'], $o['length'] );\r
207                         $translation = $reader->substr( $strings, $t['pos'], $t['length'] );\r
208 \r
209                         if ('' === $original) {\r
210                                 $this->set_headers($this->make_headers($translation));\r
211                         } else {\r
212                                 $entry = &$this->make_entry($original, $translation);\r
213                                 $this->entries[$entry->key()] = &$entry;\r
214                         }\r
215                 }\r
216                 return true;\r
217         }\r
218 \r
219         /**\r
220          * Build a Translation_Entry from original string and translation strings,\r
221          * found in a MO file\r
222          *\r
223          * @static\r
224          * @param string $original original string to translate from MO file. Might contain\r
225          *      0x04 as context separator or 0x00 as singular/plural separator\r
226          * @param string $translation translation string from MO file. Might contain\r
227          *      0x00 as a plural translations separator\r
228          */\r
229         function &make_entry($original, $translation) {\r
230                 $entry = new Translation_Entry();\r
231                 // look for context\r
232                 $parts = explode(chr(4), $original);\r
233                 if (isset($parts[1])) {\r
234                         $original = $parts[1];\r
235                         $entry->context = $parts[0];\r
236                 }\r
237                 // look for plural original\r
238                 $parts = explode(chr(0), $original);\r
239                 $entry->singular = $parts[0];\r
240                 if (isset($parts[1])) {\r
241                         $entry->is_plural = true;\r
242                         $entry->plural = $parts[1];\r
243                 }\r
244                 // plural translations are also separated by \0\r
245                 $entry->translations = explode(chr(0), $translation);\r
246                 return $entry;\r
247         }\r
248 \r
249         function select_plural_form($count) {\r
250                 return $this->gettext_select_plural_form($count);\r
251         }\r
252 \r
253         function get_plural_forms_count() {\r
254                 return $this->_nplurals;\r
255         }\r
256 }\r
257 endif;