Make WordPress Core

Ticket #10165: mo.2.php

File mo.2.php, 10.7 KB (added by soletan, 17 years ago)

Updated re-implementation, using internal cache, supporting multi-byte overloading

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
20        var $strOverloaded = false;
21       
22       
23       
24        function MO( $headers = array(), $hash = array() )
25        {
26
27                $this->headers = $headers;
28                $this->entries = $hash;
29
30                $this->strOverloaded = ( ini_get( 'mbstring.func_overload' ) & 2 ) && 
31                                                                is_callable( 'mb_substr' ); 
32
33                if ( $this->headers['Plural-Forms'] )
34                {
35
36                        $sep  = $this->_strpos( $this->headers['Plural-Forms'], ';' );
37                        $temp = $this->_substr( $this->headers['Plural-Forms'], 0, $sep );
38
39                        if ( preg_match( '/^nplurals=(\d+)$/i', trim( $temp ), $matches ) )
40                                $this->nplurals = intval( $matches[1] );
41
42                }
43        }
44
45       
46        function _strlen( $string )
47        {
48                return $this->strOverloaded ? mb_strlen( $string, 'ascii' )
49                                                                        : strlen( $string );
50        }
51       
52        function _strpos( $haystack, $needle, $offset = null )
53        {
54                return $this->strOverloaded ? mb_strpos( $haystack, $needle, $offset, 'ascii' )
55                                                                        : strpos( $haystack, $needle, $offset );
56        }
57       
58        function _substr( $string, $offset, $length = null )
59        {
60
61                if ( $this->strOverloaded )
62                        return mb_substr( $string, $offset, $length, 'ascii' );
63
64                if ( is_null( $length ) )
65                        return substr( $string, $offset );
66
67                return substr( $string, $offset, $length );
68
69        }
70
71        function _str_split( $string, $chunkSize )
72        {
73
74                if ( $this->strOverloaded ) 
75                {
76
77                        $length = mb_strlen( $string, 'ascii' );
78                       
79                        $out = array();
80
81                        for ( $i = 0; $i < $length; $i += $chunkSize )
82                                $out[] = mb_substr( $string, $i, $chunkSize, 'ascii' );
83                               
84                        return $out;
85
86                }
87                else
88                        return str_split( $string, $chunkSize );
89
90        }
91       
92       
93        /**
94         * Reads provided MO file.
95         *
96         * @param string $filename name of MO file to read
97         * @return MO
98         */
99
100        function import_from_file( $filename )
101        {
102
103                /*
104                 * check cache for existing record first
105                 */
106
107                if ( $this->cache_is_newer( $filename ) )
108                {
109
110                        $set = $this->cache_read( $filename );
111                        if ( is_array( $set ) )
112                        {
113       
114                                list( $this->headers, $this->entries ) = $set;
115       
116                                return true;
117       
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                // seek to data blocks
162                fseek( $file, $HposOriginals, SEEK_SET );
163
164                // read originals' indices
165                $HsizeOriginals = $HposTranslations - $HposOriginals;
166                if ( $HsizeOriginals != $Hcount * 8 )
167                        return false;
168
169                $originals = fread( $file, $HsizeOriginals );
170                if ( $this->_strlen( $originals ) != $HsizeOriginals )
171                        return false;
172
173                // read translations' indices
174                $HsizeTranslations = $HposHash - $HposTranslations;
175                if ( $HsizeTranslations != $Hcount * 8 )
176                        return false;
177
178                $translations = fread( $file, $HsizeTranslations );
179                if ( $this->_strlen( $translations ) != $HsizeTranslations )
180                        return false;
181
182                // transform raw data into set of indices
183                $originals    = $this->_str_split( $originals, 8 );
184                $translations = $this->_str_split( $translations, 8 );
185
186
187
188                /*
189                 * read set of strings to separate string
190                 */
191
192                // skip hash table
193                $HposStrings = $HposHash + $HsizeHash * 4;
194
195                fseek( $file, $HposStrings, SEEK_SET );
196
197                // read strings expected in rest of file
198                $strings = '';
199                while ( !feof( $file ) )
200                        $strings .= fread( $file, 4096 );
201
202                fclose( $file );
203
204
205
206                // collect hash records
207                $hash = $header = array();
208
209                for ( $i = 0; $i < $Hcount; $i++ )
210                {
211
212                        // parse index records on original and related translation
213                        $o = unpack( "{$endian}length/{$endian}pos", $originals[$i] );
214                        $t = unpack( "{$endian}length/{$endian}pos", $translations[$i] );
215
216                        if ( !$o || !$t )
217                                return false;
218
219                        // adjust offset due to reading strings to separate space before
220                        $o['pos'] -= $HposStrings;
221                        $t['pos'] -= $HposStrings;
222
223                        // extract original and translations
224                        $original    = $this->_substr( $strings, $o['pos'], $o['length'] );
225                        $translation = $this->_substr( $strings, $t['pos'], $t['length'] );
226
227
228
229                        if ( $original === '' )
230                        {
231                                // got header --> store separately
232
233                                $header = array();
234
235                                foreach ( explode( "\n", $translation ) as $line )
236                                {
237
238                                        $sep = $this->_strpos( $line, ':' );
239                                        if ( $sep !== false )
240                                                $header[trim($this->_substr( $line, 0, $sep ))] = trim( $this->_substr( $line, $sep + 1 ));
241
242                                }
243                        }
244                        else
245                        {
246
247                                // detect context in original
248                                $sep = $this->_strpos( $original, "\04" );
249                                if ( $sep !== false )
250                                {
251                                        $context  = $this->_substr( $original, 0, $sep );
252                                        $original = $this->_substr( $original, $sep + 1 );
253                                }
254                                else
255                                        $context  = null;
256
257
258                                $original     = explode( "\00", $original );
259                                $translation  = explode( "\00", $translation );
260
261                                $singularFrom = array_shift( $original );
262                                $singularTo   = array_shift( $translation );
263
264                                if ( count( $original ) && ( count( $original ) == count( $translation ) ) )
265                                        $plurals = array_combine( $original, $translation );
266                                else
267                                        $plurals = array();
268
269
270                                $record = array(
271                                                                'context'     => $context,
272                                                                'singular'    => $singularFrom,
273                                                                'translation' => $singularTo,
274                                                                'plurals'     => $plurals,
275                                                                );
276
277                                $key = is_null( $context ) ? $singularFrom
278                                                                                   : "$context\04$singularFrom";
279
280
281                                $hash[$key] = $record;
282
283                        }
284                }
285
286
287                $this->headers = $header;
288                $this->entries = $hash;
289
290
291
292                /*
293                 * write result to cache
294                 */
295
296                $this->cache_write( $filename, array( $header, $hash ) );
297
298
299
300                return true;
301
302        }
303
304
305        /**
306         * Retrieves translation of single entry.
307         *
308         * The provided entry is constructed manually and thus providing proper
309         * key for lookup. The method returns a matching entry from internal pool
310         * of translations.
311         *
312         * NOTE! Passing by reference is required as long as parent class is using
313         *       it (obviously due to keeping things compatible with PHP4).
314         *
315         * @param Translate_Entry $entry entry to look up
316         * @return Translate_Entry resulting entry
317         */
318
319        function translate_entry( &$entry )
320        {
321
322                if ( !isset( $this->entries[$entry->key()] ) )
323                        return false;
324
325                if ( is_array( $this->entries[$entry->key()] ) )
326                {
327                        // convert entry to instance of Translate_Entry on demand
328
329                        // use internally managed record
330                        $args = $this->entries[$entry->key()];
331
332                        // add structures required by Translation_Entry
333                        $args['translations'] = array( $args['translation'] );
334                        if ( count( $args['plurals'] ) )
335                                $args['translations'] = array_merge( $args['translations'], array_values( $args['plurals'] ) );
336
337                        if ( count( $args['plurals'] ) )
338                                $args['plural'] = array_shift( array_keys( $args['plurals'] ) );
339
340                        // temporarily transform entry
341                        return new Translation_Entry( $args );
342
343                }
344
345                if ( $this->entries[$entry->key()] instanceof Translation_Entry )
346                        return $this->entries[$entry->key()];
347
348                return false;
349
350        }
351
352
353        function translate( $singular, $context = null )
354        {
355
356                $key = $this->key( $singular, $context );
357
358                if ( isset( $this->entries[$key] ) )
359                        if ( is_string( $this->entries[$key]['translation'] ) )
360                                return $this->entries[$key]['translation'];
361
362                return $singular;
363
364        }
365
366
367        function translate_plural( $singular, $plural, $count, $context = null )
368        {
369
370                $key = $this->key( $singular, $context );
371
372                $translated = $this->entries[$key];
373                if ( is_array( $translated ) )
374                {
375
376                        $index              = $this->select_plural_form( $count );
377                        $total_plural_forms = $this->get_plural_forms_count();
378
379                        if ( ( $index >= 0 ) && ( $index < $total_plural_forms ) )
380                        {
381
382                                if ( ( $index == 0 ) && is_string( $translated['translation'] ) )
383                                        return $translated['translation'];
384
385                                if ( count( $translated['plurals'] ) == $total_plural_forms - 1 )
386                                {
387                                        $plurals = array_values( $translated['plurals'] );
388                                        return $plurals[$index-1];
389                                }
390                        }
391
392
393                        return ( $count == 1 ) ? $singular : $plural;
394
395                }
396
397
398                // keep class compatible with old-style entry class
399                return parent::translate_plural( $singular, $plural, $count, $context );
400
401        }
402
403
404        function key( $singular, $context = null )
405        {
406                return is_null( $context ) ? $singular : "$context\04$singular";
407        }
408
409
410        function get_plural_forms_count()
411        {
412                return $this->nplurals;
413        }
414
415
416        function select_plural_form( $count )
417        {
418                return $this->gettext_select_plural_form( $count );
419        }
420       
421       
422       
423       
424       
425       
426        function cache_write( $filename, $data )
427        {
428
429                $cacheDir = ABSPATH . 'wp-content/cache';
430
431                if ( !is_writable( $cacheDir ) )
432                        return false;
433
434
435                $cacheDir .= '/mo';
436
437                if ( !is_dir( $cacheDir ) )
438                        if ( !mkdir( $cacheDir, 0750, true ) )
439                                return false;
440
441                if ( !is_writable( $cacheDir ) )
442                        return false;
443
444                       
445                $umask = umask( 0137 );
446
447                $handle = fopen( $cacheDir . '/' . sha1( $filename ), 'w' );
448                if ( $handle !== false )
449                {
450
451                        $data = gzcompress( serialize( $data ), 9 );
452                        $okay = ( fwrite( $handle, $data ) == $this->_strlen( $data ) );
453               
454                        fclose( $handle );
455
456                }
457                else
458                        $okay = false;
459                       
460                umask( $umask );
461               
462               
463                return $okay;
464
465        }
466       
467       
468        function cache_is_newer( $filename )
469        {
470
471                $cacheFile = ABSPATH . 'wp-content/cache/mo/' . sha1( $filename );
472
473                $cache = file_exists( $cacheFile ) ? stat( $cacheFile ) : 0;
474                $cache = is_array( $cache ) ? $cache['mtime'] : 0;
475
476                $file  = file_exists( $cacheFile ) ? stat( $filename ) : 0;
477                $file  = is_array( $file ) ? $file['mtime'] : 0;
478
479               
480                return ( $cache >= $file );
481
482        }
483       
484       
485        function cache_read( $filename )
486        {
487
488                $cacheFile = ABSPATH . 'wp-content/cache/mo/' . sha1( $filename );
489                if ( file_exists( $cacheFile ) )
490                {
491
492                        $data = file_get_contents( $cacheFile );
493                        if ( $data !== false )
494                        {
495                               
496                                $data = @unserialize( @gzuncompress( $data ) );
497                                if ( is_array( $data ) )
498                                        return $data;
499
500                        }
501                }
502               
503
504                return false;
505
506        }
507}
508
509
510?>