Make WordPress Core

Ticket #10165: mo.3.php

File mo.3.php, 9.2 KB (added by soletan, 15 years ago)

Updated implementation using wp-cache API again, using different way of parsing MO-file header ...

Line 
1<?php
2
3
4/**
5 * Improved MO file reader for Wordpress.
6 *
7 * @author Thomas Urban <thomas.urban@toxa.de>
8 */
9
10
11include_once( dirname( __FILE__ ) . '/translations.php' );
12
13
14class MO extends Translations
15{
16
17        var $nplurals = 2;
18
19        var $pluralTerm = '';
20
21
22        var $strOverloaded = false;
23
24
25
26        function MO( $headers = array(), $hash = array() )
27        {
28
29                $this->headers = $headers;
30                $this->entries = $hash;
31
32                $this->strOverloaded = ( ini_get( 'mbstring.func_overload' ) & 2 ) &&
33                                                                is_callable( 'mb_substr' );
34
35                if ( $this->headers['Plural-Forms'] )
36                {
37
38                        $sep  = $this->_strpos( $this->headers['Plural-Forms'], ';' );
39                        $temp = $this->_substr( $this->headers['Plural-Forms'], 0, $sep );
40
41                        if ( preg_match( '/^nplurals\s*=\s*(\d+)$/i', trim( $temp ), $m ) )
42                        {
43                                $this->nplurals   = intval( $m[1] );
44                                $this->pluralTerm = trim( $this->_substr( $temp, $sep + 1 ) );
45                        }
46                }
47        }
48
49
50        function _strlen( $string )
51        {
52                return $this->strOverloaded ? mb_strlen( $string, 'ascii' )
53                                                                        : strlen( $string );
54        }
55
56        function _strpos( $haystack, $needle, $offset = null )
57        {
58                return $this->strOverloaded ? mb_strpos( $haystack, $needle, $offset, 'ascii' )
59                                                                        : strpos( $haystack, $needle, $offset );
60        }
61
62        function _substr( $string, $offset, $length = null )
63        {
64
65                if ( $this->strOverloaded )
66                        return mb_substr( $string, $offset, $length, 'ascii' );
67
68                if ( is_null( $length ) )
69                        return substr( $string, $offset );
70
71                return substr( $string, $offset, $length );
72
73        }
74
75        function _str_split( $string, $chunkSize )
76        {
77
78                if ( $this->strOverloaded )
79                {
80
81                        $length = mb_strlen( $string, 'ascii' );
82
83                        $out = array();
84
85                        for ( $i = 0; $i < $length; $i += $chunkSize )
86                                $out[] = mb_substr( $string, $i, $chunkSize, 'ascii' );
87
88                        return $out;
89
90                }
91                else
92                        return str_split( $string, $chunkSize );
93
94        }
95
96
97        /**
98         * Reads provided MO file.
99         *
100         * @param string $filename name of MO file to read
101         * @return MO
102         */
103
104        function import_from_file( $filename )
105        {
106
107                /*
108                 * check cache for existing record first
109                 */
110
111                $set = wp_cache_get( $filename, 'l10n' );
112                if ( is_array( $set ) )
113                {
114
115                        list( $this->headers, $this->entries ) = $set;
116
117                        return true;
118
119                }
120
121
122
123                /**
124                 * read header from file
125                 */
126
127                $file = fopen( $filename, 'r' );
128                if ( !$file )
129                        return false;
130
131                $header = fread( $file, 28 );
132                if ( $this->_strlen( $header ) != 28 )
133                        return false;
134
135                // detect endianess
136                $endian = unpack( 'Nendian', $this->_substr( $header, 0, 4 ) );
137                if ( $endian['endian'] == intval( hexdec( '950412de' ) ) )
138                        $endian = 'N';
139                else if ( $endian['endian'] == intval( hexdec( 'de120495' ) ) )
140                        $endian = 'V';
141                else
142                        return false;
143
144                // parse header
145                $header = unpack( "{$endian}Hrevision/{$endian}Hcount/{$endian}HposOriginals/{$endian}HposTranslations/{$endian}HsizeHash/{$endian}HposHash", $this->_substr( $header, 4 ) );
146                if ( !is_array( $header ) )
147                        return false;
148
149                extract( $header );
150
151                // support revision 0 of MO format specs, only
152                if ( $Hrevision != 0 )
153                        return false;
154
155
156
157                /*
158                 * read index tables on originals and translations
159                 */
160
161                // read originals' index
162                fseek( $file, $HposOriginals, SEEK_SET );
163
164                $originals = fread( $file, $Hcount * 8 );
165                if ( $this->_strlen( $originals ) != $Hcount * 8 )
166                        return false;
167
168                // read translations index
169                fseek( $file, $HposTranslations, SEEK_SET );
170
171                $translations = fread( $file, $Hcount * 8 );
172                if ( $this->_strlen( $translations ) != $Hcount * 8 )
173                        return false;
174
175                // transform raw data into set of indices
176                $originals    = $this->_str_split( $originals, 8 );
177                $translations = $this->_str_split( $translations, 8 );
178
179
180
181                /*
182                 * read set of strings to separate string
183                 */
184
185                // find position of first string in file
186                $HposStrings = 0x7FFFFFFF;
187
188                for ( $i = 0; $i < $Hcount; $i++ )
189                {
190
191                        // parse index records on original and related translation
192                        $o = unpack( "{$endian}length/{$endian}pos", $originals[$i] );
193                        $t = unpack( "{$endian}length/{$endian}pos", $translations[$i] );
194
195                        if ( !$o || !$t )
196                                return false;
197
198                        $originals[$i]    = $o;
199                        $translations[$i] = $t;
200
201                        $HposStrings = min( $HposStrings, $o['pos'], $t['pos'] );
202
203                }
204
205                // read strings expected in rest of file
206                fseek( $file, $HposStrings, SEEK_SET );
207
208                $strings = '';
209                while ( !feof( $file ) )
210                        $strings .= fread( $file, 4096 );
211
212                fclose( $file );
213
214
215
216                // collect hash records
217                $hash = $header = array();
218
219                for ( $i = 0; $i < $Hcount; $i++ )
220                {
221
222                        // adjust offset due to reading strings to separate space before
223                        $originals[$i]['pos']    -= $HposStrings;
224                        $translations[$i]['pos'] -= $HposStrings;
225
226                        // extract original and translations
227                        $original    = $this->_substr( $strings, $originals[$i]['pos'], $originals[$i]['length'] );
228                        $translation = $this->_substr( $strings, $translations[$i]['pos'], $translations[$i]['length'] );
229
230
231
232                        if ( $original === '' )
233                        {
234                                // got header --> store separately
235
236                                $header = array();
237
238                                foreach ( explode( "\n", $translation ) as $line )
239                                {
240
241                                        $sep = $this->_strpos( $line, ':' );
242                                        if ( $sep !== false )
243                                                $header[trim($this->_substr( $line, 0, $sep ))] = trim( $this->_substr( $line, $sep + 1 ));
244
245                                }
246                        }
247                        else
248                        {
249
250                                // detect context in original
251                                $sep = $this->_strpos( $original, "\04" );
252                                if ( $sep !== false )
253                                {
254                                        $context  = $this->_substr( $original, 0, $sep );
255                                        $original = $this->_substr( $original, $sep + 1 );
256                                }
257                                else
258                                        $context  = null;
259
260
261                                $original     = explode( "\00", $original );
262                                $translation  = explode( "\00", $translation );
263
264                                $singularFrom = array_shift( $original );
265                                $singularTo   = array_shift( $translation );
266
267                                $record = array(
268                                                                'C' => $context,                // context
269                                                                'S' => $singularFrom,   // singular original
270                                                                'X' => $original,               // plural orignal
271                                                                'T' => $singularTo,             // singular translation
272                                                                'P' => $translation,    // plural translations
273                                                                );
274
275                                $key = is_null( $context ) ? $singularFrom
276                                                                                   : "$context\04$singularFrom";
277
278
279                                $hash[$key] = $record;
280
281                        }
282                }
283
284
285                $this->headers = $header;
286                $this->entries = $hash;
287
288
289
290                /*
291                 * write result to cache
292                 */
293
294                wp_cache_set( $filename, array( $header, $hash ), 'l10n' );
295
296
297
298                return true;
299
300        }
301
302
303        /**
304         * Retrieves translation of single entry.
305         *
306         * The provided entry is constructed manually and thus providing proper
307         * key for lookup. The method returns a matching entry from internal pool
308         * of translations.
309         *
310         * NOTE! Passing by reference is required as long as parent class is using
311         *       it (obviously due to keeping things compatible with PHP4).
312         *
313         * @param Translate_Entry $entry entry to look up
314         * @return Translate_Entry resulting entry
315         */
316
317        function translate_entry( &$entry )
318        {
319
320                if ( !isset( $this->entries[$entry->key()] ) )
321                        return false;
322
323                if ( is_array( $this->entries[$entry->key()] ) )
324                {
325                        // convert entry to instance of Translate_Entry on demand
326
327                        // use internally managed record
328                        $args = $this->entries[$entry->key()];
329
330                        // add structures required by Translation_Entry
331                        $args['translations'] = array( $args['T'] );
332                        if ( count( $args['P'] ) )
333                                $args['translations'] = array_merge( $args['translations'], array_values( $args['P'] ) );
334
335                        if ( count( $args['P'] ) )
336                                $args['plural'] = array_shift( array_keys( $args['P'] ) );
337
338                        // temporarily transform entry
339                        return new Translation_Entry( $args );
340
341                }
342
343                if ( $this->entries[$entry->key()] instanceof Translation_Entry )
344                        return $this->entries[$entry->key()];
345
346                return false;
347
348        }
349
350
351        function translate( $singular, $context = null )
352        {
353
354                $key = $this->key( $singular, $context );
355
356                if ( is_array( $this->entries[$key] ) )
357                        if ( is_string( $this->entries[$key]['T'] ) )
358                                return $this->entries[$key]['T'];
359
360                return $singular;
361
362        }
363
364
365        function translate_plural( $singular, $plural, $count, $context = null )
366        {
367
368                $key = $this->key( $singular, $context );
369
370                $translated = $this->entries[$key];
371                if ( is_array( $translated ) )
372                {
373
374                        $index    = $this->select_plural_form( $count );
375                        $nplurals = $this->nplurals;
376
377                        if ( ( $index >= 0 ) && ( $index < $nplurals ) )
378                        {
379
380                                if ( ( $index == 0 ) && is_string( $translated['T'] ) )
381                                        // retrieve singular form
382                                        return $translated['T'];
383
384                                if ( is_string( $translated['P'][--$index] ) )
385                                        // retrieve selected plural form
386                                        return $translated['P'][$index];
387
388                        }
389                }
390
391
392                // retrieve one of the provided original forms depending on $count
393                return ( $count == 1 ) ? $singular : $plural;
394
395        }
396
397
398        function key( $singular, $context = null )
399        {
400                return is_null( $context ) ? $singular : "$context\04$singular";
401        }
402
403
404        function get_plural_forms_count()
405        {
406                return $this->nplurals;
407        }
408
409
410        function select_plural_form( $count )
411        {
412
413                $count = intval( $count );
414
415                if ( trim( $this->pluralTerm ) === '' )
416                        return ( $count === 1 ) ? 0 : 1;
417
418                return intval( eval( 'return (' . str_replace( 'n', '$count', $this->pluralTerm ) . ');' ) );
419
420        }
421}
422
423
424?>