WordPress.org

Make WordPress Core

Ticket #17268: 17268.2.diff

File 17268.2.diff, 29.3 KB (added by Mte90, 21 months ago)

working on unit tests

  • new file src/wp-includes/pomo/mo-dynamic-loader.php

    diff --git src/wp-includes/pomo/mo-dynamic-loader.php src/wp-includes/pomo/mo-dynamic-loader.php
    new file mode 100644
    index 0000000000..80e39985d0
    - +  
     1<?php
     2/**
     3 * Dynamic loading and parsing of MO files
     4 *
     5 * @author Björn Ahrens <bjoern@ahrens.net>
     6 * @package WP Performance Pack
     7 * @since 0.1
     8 */
     9
     10/**
     11 * Class holds information about a single MO file
     12 */
     13class MO_item {
     14        var $reader = NULL;
     15        var $mofile = '';
     16
     17        var $loaded = false;
     18        var $total = 0;
     19        var $originals = array();
     20        var $originals_table;
     21        var $translations_table;
     22        var $last_access;
     23
     24        var $hash_table;
     25        var $hash_length = 0;
     26
     27        function clear_reader () {
     28                if ( $this->reader !== NULL ) {
     29                        $this->reader->close();
     30                        $this->reader = NULL;
     31                }
     32        }
     33}
     34
     35/**
     36 * Class for working with MO files
     37 * Translation entries are created dynamically.
     38 * Due to this export and save functions are not implemented.
     39 */
     40class MO_dynamic extends Gettext_Translations {
     41        private $caching = false;
     42        private $modified = false;
     43
     44        protected $domain = '';
     45        protected $_nplurals = 2;
     46        protected $MOs = array();
     47
     48        protected $translations = NULL;
     49        protected $base_translations = NULL;
     50
     51        function __construct( $domain, $caching = false ) {
     52                $this->domain = $domain;
     53                $this->caching = $caching;
     54                if ( $caching ) {
     55                        add_action ( 'shutdown', array( $this, 'save_to_cache' ) );
     56                        add_action ( 'admin_init', array( $this, 'save_base_translations' ), 100 );
     57                }
     58                // Reader has to be destroyed befor any upgrades or else upgrade might fail, if a
     59                // reader is loaded (cannot delete old plugin/theme/etc. because a language file
     60                // is still opened).
     61                add_filter('upgrader_pre_install', array($this, 'clear_reader_before_upgrade'), 10, 2);
     62        }
     63
     64        static function get_byteorder($magic) {
     65                // The magic is 0x950412de
     66
     67                // bug in PHP 5.0.2, see https://savannah.nongnu.org/bugs/?func=detailitem&item_id=10565
     68                $magic_little = (int) - 1794895138;
     69                $magic_little_64 = (int) 2500072158;
     70                // 0xde120495
     71                $magic_big = ((int) - 569244523) & 0xFFFFFFFF;
     72                if ($magic_little == $magic || $magic_little_64 == $magic) {
     73                        return 'little';
     74                } else if ($magic_big == $magic) {
     75                        return 'big';
     76                } else {
     77                        return false;
     78                }
     79        }
     80
     81        function unhook_and_close () {
     82                remove_action ( 'shutdown', array( $this, 'save_to_cache' ) );
     83                remove_action ( 'admin_init', array( $this, 'save_base_translations' ), 100 );
     84                foreach ( $this->MOs as $moitem ) {
     85                        $moitem->clear_reader();
     86                }
     87                $this->MOs = array();
     88        }
     89
     90        function __destruct() {
     91                foreach ( $this->MOs as $moitem ) {
     92                        $moitem->clear_reader();
     93                }
     94        }
     95
     96        function clear_reader_before_upgrade($return, $plugin) {
     97                // stripped down copy of class-wp-upgrader.php Plugin_Upgrader::deactivate_plugin_before_upgrade
     98                if ( is_wp_error($return) ) //Bypass.
     99                        return $return;
     100
     101                foreach ( $this->MOs as $moitem ) {
     102                        $moitem->clear_reader();
     103                }
     104        }
     105
     106        function get_current_url () {
     107                $current_url = $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
     108                if ( isset($_SERVER['QUERY_STRING']) && ( $len = strlen( $_SERVER['QUERY_STRING'] ) ) > 0 ) {
     109                        $current_url = substr ( $current_url, 0, strlen($current_url) - $len - 1 );
     110                }
     111                if ( substr( $current_url, -10 ) === '/wp-admin/' ) {
     112                        $current_url .= 'index.php';
     113                }
     114                if ( isset( $_GET['page'] ) ) {
     115                        $current_url .= '?page=' . $_GET['page'];
     116                }
     117                return $current_url;
     118        }
     119
     120        function import_from_file( $filename ) {
     121                $moitem = new MO_item();
     122                $moitem->mofile = $filename;
     123                $this->MOs[] = $moitem;
     124
     125                // because only a reference to the MO file is created, at this point there is no information if $filename is a valid MO file, so the return value is always true
     126                return true;
     127        }
     128
     129        function save_base_translations () {
     130                if ( is_admin() && $this->translations !== NULL && $this->base_translations === NULL ) {
     131                        $this->base_translations = $this->translations;
     132                        $this->translations = array();
     133                }
     134        }
     135
     136        private function cache_get ( $key, $cache_time ) {
     137                $t = wp_cache_get( $key, 'dymoloader1.0' );
     138                if ( $t !== false && isset( $t['data'] ) ) {
     139                        // check soft expire
     140                        if ( $t['softexpire'] < time() ) {
     141                                // update cache with new soft expire time
     142                                $t['softexpire'] = time() + ( $cache_time - ( 5 * MINUTE_IN_SECONDS ) );
     143                                wp_cache_replace( $key, $t, 'dymoloader1.0', $cache_time );
     144                        }
     145                        return json_decode( gzuncompress( $t['data'] ), true );
     146                }
     147                return NULL;
     148        }
     149
     150        private function cache_set ( $key, $cache_time, $data ) {
     151                $t = array();
     152                $t['softexpire'] = time() + ( $cache_time - ( 5 * MINUTE_IN_SECONDS ) );
     153                $t['data'] = gzcompress( json_encode( $data ) );
     154                wp_cache_set( $key, $t, 'dymoloader1.0', $cache_time );
     155        }
     156
     157        function import_domain_from_cache () {
     158                // build cache key from domain and request uri
     159                if ( $this->caching ) {
     160                        if ( is_admin() ) {
     161                                $this->base_translations = $this->cache_get( 'backend_' . $this->domain, HOUR_IN_SECONDS );
     162                                $this->translations = $this->cache_get( 'backend_' . $this->domain . '_' . $this->get_current_url(), 30 * MINUTE_IN_SECONDS );
     163                        } else {
     164                                $this->translations = $this->cache_get( 'frontend_' . $this->domain, HOUR_IN_SECONDS );
     165                        }
     166                }
     167
     168                if ( $this->translations === NULL ) {
     169                        $this->translations = array();
     170                }
     171        }
     172
     173        function save_to_cache () {
     174                if ( $this->modified ) {
     175                        if ( is_admin() ) {
     176                                $this->cache_set( 'backend_' . $this->domain . '_' . $this->get_current_url(), 30 * MINUTE_IN_SECONDS, $this->translations ); // keep admin page cache for 30 minutes
     177                                if ( count( $this->base_translations ) > 0 ) {
     178                                        $this->cache_set( 'backend_'.$this->domain, HOUR_IN_SECONDS, $this->base_translations ); // keep admin base cache for 60 minutes
     179                                }
     180                        } else {
     181                                $this->cache_set( 'frontend_'.$this->domain, HOUR_IN_SECONDS, $this->translations ); // keep front end cache for 60 minutes
     182                        }
     183                }
     184        }
     185
     186        private function import_fail ( &$moitem ) {
     187                $moitem->reader->close();
     188                $moitem->reader = false;
     189                unset( $moitem->originals );
     190                unset( $moitem->originals_table );
     191                unset( $moitem->translations_table );
     192                unset( $moitem->hash_table );
     193
     194                return false;
     195        }
     196
     197        function import_from_reader( &$moitem ) {
     198                if ( $moitem->reader !== NULL) {
     199                        return ( $moitem->reader !== false );
     200                }
     201
     202                $file_size = filesize( $moitem->mofile );
     203                $moitem->reader=new POMO_FileReader( $moitem->mofile );
     204
     205                if ( $moitem->loaded === true ) {
     206                        return true;
     207                }
     208
     209                $endian_string = static::get_byteorder( $moitem->reader->readint32() );
     210                if ( false === $endian_string ) {
     211                        return $this->import_fail( $moitem );
     212                }
     213                $moitem->reader->setEndian( $endian_string );
     214                $endian = ( 'big' == $endian_string ) ? 'N' : 'V';
     215
     216                $header = $moitem->reader->read( 24 );
     217                if ( $moitem->reader->strlen( $header ) != 24 ) {
     218                        return $this->import_fail( $moitem );
     219                }
     220
     221                // parse header
     222                $header = unpack( "{$endian}revision/{$endian}total/{$endian}originals_lenghts_addr/{$endian}translations_lenghts_addr/{$endian}hash_length/{$endian}hash_addr", $header );
     223                if ( !is_array( $header ) ) {
     224                        return $this->import_fail( $moitem );
     225                }
     226                extract( $header );
     227
     228                // support revision 0 of MO format specs, only
     229                if ( $revision !== 0 ) {
     230                        return $this->import_fail( $moitem );
     231                }
     232
     233                $moitem->total = $total;
     234
     235                // read hashtable
     236                $moitem->hash_length = $hash_length;
     237                if ( $hash_length > 0 ) {
     238                        $moitem->reader->seekto ( $hash_addr );
     239                        $str = $moitem->reader->read( $hash_length * 4 );
     240                        if ( $moitem->reader->strlen( $str ) != $hash_length * 4 ) {
     241                                return $this->import_fail( $moitem );
     242                        }
     243                        if ( class_exists ( 'SplFixedArray' ) )
     244                                $moitem->hash_table = SplFixedArray::fromArray( unpack ( $endian.$hash_length, $str ), false );
     245                        else
     246                                $moitem->hash_table = array_slice( unpack ( $endian.$hash_length, $str ), 0 ); // force zero based index
     247                }
     248
     249                // read originals' indices
     250                $moitem->reader->seekto( $originals_lenghts_addr );
     251                $originals_lengths_length = $translations_lenghts_addr - $originals_lenghts_addr;
     252                if ( $originals_lengths_length != $total * 8 ) {
     253                        return $this->import_fail( $moitem );
     254                }
     255                $str = $moitem->reader->read( $originals_lengths_length );
     256                if ( $moitem->reader->strlen( $str ) != $originals_lengths_length ) {
     257                        return $this->import_fail( $moitem );
     258                }
     259                if ( class_exists ( 'SplFixedArray' ) )
     260                        $moitem->originals_table = SplFixedArray::fromArray( unpack ( $endian.($total * 2), $str ), false );
     261                else
     262                        $moitem->originals_table = array_slice( unpack ( $endian.($total * 2), $str ), 0 ); // force zero based index
     263
     264                // "sanity check" ( i.e. test for corrupted mo file )
     265                for ( $i = 0, $max = $total * 2; $i < $max; $i+=2 ) {
     266                        if ( $moitem->originals_table[ $i + 1 ] > $file_size
     267                                || $moitem->originals_table[ $i + 1 ] + $moitem->originals_table[ $i ] > $file_size ) {
     268                                return $this->import_fail( $moitem );
     269                        }
     270                }
     271
     272                // read translations' indices
     273                $translations_lenghts_length = $hash_addr - $translations_lenghts_addr;
     274                if ( $translations_lenghts_length != $total * 8 ) {
     275                        return $this->import_fail( $moitem );
     276                }
     277                $str = $moitem->reader->read( $translations_lenghts_length );
     278                if ( $moitem->reader->strlen( $str ) != $translations_lenghts_length ) {
     279                        return $this->import_fail( $moitem );
     280                }
     281                if ( class_exists ( 'SplFixedArray' ) )
     282                        $moitem->translations_table = SplFixedArray::fromArray( unpack ( $endian.($total * 2), $str ), false );
     283                else
     284                        $moitem->translations_table = array_slice( unpack ( $endian.($total * 2), $str ), 0 ); // force zero based index
     285
     286                // "sanity check" ( i.e. test for corrupted mo file )
     287                for ( $i = 0, $max = $total * 2; $i < $max; $i+=2 ) {
     288                        if ( $moitem->translations_table[ $i + 1 ] > $file_size
     289                                || $moitem->translations_table[ $i + 1 ] + $moitem->translations_table[ $i ] > $file_size ) {
     290                                return $this->import_fail( $moitem );
     291                        }
     292                }
     293
     294                $moitem->loaded = true; // read headers can fail, so set loaded to true
     295
     296                // read headers
     297                for ( $i = 0, $max = $total * 2; $i < $max; $i+=2 ) {
     298                        $original = '';
     299                        if ( $moitem->originals_table[$i] > 0 ) {
     300                                $moitem->reader->seekto( $moitem->originals_table[$i+1] );
     301                                $original = $moitem->reader->read( $moitem->originals_table[$i] );
     302
     303                                $j = strpos( $original, 0 );
     304                                if ( $j !== false )
     305                                        $original = substr( $original, 0, $i );
     306                        }
     307
     308                        if ( $original === '' ) {
     309                                $translation = '';
     310                                if ( $moitem->translations_table[$i] > 0 ) {
     311                                        $moitem->reader->seekto( $moitem->translations_table[$i+1] );
     312                                        $translation = $moitem->reader->read( $moitem->translations_table[$i] );
     313                                }
     314
     315                                $this->set_headers( $this->make_headers( $translation ) );
     316                        } else
     317                                return true;
     318                }
     319                return true;
     320        }
     321
     322        protected function search_translation ( $key ) {
     323                $hash_val = NULL;
     324                $key_len = strlen( $key );
     325
     326                for ( $j = 0, $max = count ( $this->MOs ); $j < $max; $j++ ) {
     327                        $moitem = $this->MOs[$j];
     328                        if ( $moitem->reader == NULL ) {
     329                                if ( !$this->import_from_reader( $moitem ) ) {
     330                                        // Error reading MO file, so delete it from MO list to prevent subsequent access
     331                                        unset( $this->MOs[$j] );
     332                                        return false; // return or continue?
     333                                }
     334                        }
     335
     336                        if ($moitem->hash_length>0) {
     337                                /* Use mo file hash table to search translation */
     338
     339                                // calculate hash value
     340                                // hashpjw function by P.J. Weinberger from gettext hash-string.c
     341                                // adapted to php and its quirkiness caused by missing unsigned ints and shift operators...
     342                                if ( $hash_val === NULL) {
     343                                        $hash_val = 0;
     344                                        $chars = unpack ( 'C*', $key ); // faster than accessing every single char by ord(char)
     345                                        foreach ( $chars as $char ) {
     346                                                $hash_val = ( $hash_val << 4 ) + $char;
     347                                                if( 0 !== ( $g = $hash_val & 0xF0000000 ) ){
     348                                                        if ( $g < 0 )
     349                                                                $hash_val ^= ( ( ($g & 0x7FFFFFFF) >> 24 ) | 0x80 ); // wordaround: php operator >> is arithmetic, not logic, so shifting negative values gives unexpected results. Cut sign bit, shift right, set sign bit again.
     350                                                                /*
     351                                                                workaround based on this function (adapted to actual used parameters):
     352
     353                                                                function shr($var,$amt) {
     354                                                                        $mask = 0x40000000;
     355                                                                        if($var < 0) {
     356                                                                                $var &= 0x7FFFFFFF;
     357                                                                                $mask = $mask >> ($amt-1);
     358                                                                                return ($var >> $amt) | $mask;
     359                                                                        }
     360                                                                        return $var >> $amt;
     361                                                                }
     362                                                                */
     363                                                        else
     364                                                                $hash_val ^= ( $g >> 24 );
     365                                                        $hash_val ^= $g;
     366                                                }
     367                                        }
     368                                }
     369
     370                                // calculate hash table index and increment
     371                                if ( $hash_val >= 0 ) {
     372                                        $idx = $hash_val % $moitem->hash_length;
     373                                        $incr = 1 + ($hash_val % ($moitem->hash_length - 2));
     374                                } else {
     375                                        $hash_val = (float) sprintf('%u', $hash_val); // workaround php not knowing unsigned int - %u outputs $hval as unsigned, then cast to float
     376                                        $idx = fmod( $hash_val, $moitem->hash_length);
     377                                        $incr = 1 + fmod ($hash_val, ($moitem->hash_length - 2));
     378                                }
     379
     380                                $orig_idx = $moitem->hash_table[$idx];
     381                                while ( $orig_idx != 0 ) {
     382                                        $orig_idx--; // index adjustment
     383
     384                                        $pos = $orig_idx * 2;
     385                                        if ( $orig_idx < $moitem->total // orig_idx must be in range
     386                                                 && $moitem->originals_table[$pos] >= $key_len ) { // and original length must be equal or greater as key length (original can contain plural forms)
     387
     388                                                // read original string
     389                                                $mo_original = '';
     390                                                if ( $moitem->originals_table[$pos] > 0 ) {
     391                                                        $moitem->reader->seekto( $moitem->originals_table[$pos+1] );
     392                                                        $mo_original = $moitem->reader->read( $moitem->originals_table[$pos] );
     393                                                }
     394
     395                                                if ( $moitem->originals_table[$pos] == $key_len
     396                                                         || ord( $mo_original{$key_len} ) == 0 ) {
     397                                                        // strings can only match if they have the same length, no need to inspect otherwise
     398
     399                                                        if ( false !== ( $i = strpos( $mo_original, 0 ) ) )
     400                                                                $cmpval = strncmp( $key, $mo_original, $i );
     401                                                        else
     402                                                                $cmpval = strcmp( $key, $mo_original );
     403
     404                                                        if ( $cmpval === 0 ) {
     405                                                                // key found, read translation string
     406                                                                $moitem->reader->seekto( $moitem->translations_table[$pos+1] );
     407                                                                $translation = $moitem->reader->read( $moitem->translations_table[$pos] );
     408                                                                if ( $j > 0 ) {
     409                                                                        // Assuming frequent subsequent translations from the same file resort MOs by access time to avoid unnecessary search in the wrong files.
     410                                                                        $moitem->last_access=time();
     411                                                                        usort( $this->MOs, function ($a, $b) {return ($b->last_access - $a->last_access);} );
     412                                                                }
     413                                                                return $translation;
     414                                                        }
     415                                                }
     416                                        }
     417
     418                                        if ($idx >= $moitem->hash_length - $incr)
     419                                                $idx -= ($moitem->hash_length - $incr);
     420                                        else
     421                                                $idx += $incr;
     422                                        $orig_idx = $moitem->hash_table[$idx];
     423                                }
     424                        } else {
     425                                /* No hash-table, do binary search for matching originals entry */
     426                                $left = 0;
     427                                $right = $moitem->total-1;
     428
     429                                while ( $left <= $right ) {
     430                                        $pivot = $left + (int) ( ( $right - $left ) / 2 );
     431                                        $pos = $pivot * 2;
     432
     433                                        if ( isset( $moitem->originals[$pivot] ) ) {
     434                                                $mo_original = $moitem->originals[$pivot];
     435                                        } else {
     436                                                // read and "cache" original string to improve performance of subsequent searches
     437                                                if ( $moitem->originals_table[$pos] > 0 ) {
     438                                                        $moitem->reader->seekto( $moitem->originals_table[$pos+1] );
     439                                                        $mo_original = $moitem->reader->read( $moitem->originals_table[$pos] );
     440                                                } else {
     441                                                        $mo_original = '';
     442                                                }
     443                                                $moitem->originals[$pivot] = $mo_original;
     444                                        }
     445
     446                                        if ( false !== ( $i = strpos( $mo_original, 0 ) ) )
     447                                                $cmpval = strncmp( $key, $mo_original, $i );
     448                                        else
     449                                                $cmpval = strcmp( $key, $mo_original );
     450
     451                                        if ( $cmpval === 0 ) {
     452                                                // key found read translation string
     453                                                $moitem->reader->seekto( $moitem->translations_table[$pos+1] );
     454                                                $translation = $moitem->reader->read( $moitem->translations_table[$pos] );
     455                                                if ( $j > 0 ) {
     456                                                        // Assuming frequent subsequent translations from the same file resort MOs by access time to avoid unnecessary search in the wrong files.
     457                                                        $moitem->last_access=time();
     458                                                        usort( $this->MOs, function ($a, $b) {return ($b->last_access - $a->last_access);} );
     459                                                }
     460                                                return $translation;
     461                                        } else if ( $cmpval < 0 ) {
     462                                                $right = $pivot - 1;
     463                                        } else { // if ($cmpval>0)
     464                                                $left = $pivot + 1;
     465                                        }
     466                                }
     467                        }
     468                }
     469                // key not found
     470                return false;
     471        }
     472
     473        function translate ($singular, $context = NULL) {
     474                if ( !isset ($singular{0} ) ) return $singular;
     475
     476                if ( $context == NULL ) {
     477                        $s = $singular;
     478                } else {
     479                        $s = $context . chr(4) . $singular;
     480                }
     481
     482                if ( $this->translations === NULL ) {
     483                        $this->import_domain_from_cache();
     484                }
     485
     486                if ( isset( $this->translations[$s] ) ) {
     487                        $t = $this->translations[$s];
     488                } elseif ( isset ($this->base_translations[$s] ) ) {
     489                        $t = $this->base_translations[$s];
     490                } else {
     491                        if ( false !== ( $t = $this->search_translation( $s ) ) ) {
     492                                $this->translations[$s] = $t;
     493                                $this->modified = true;
     494                        }
     495                }
     496
     497                if ( $t !== false ) {
     498                        if ( false !== ( $i = strpos( $t, 0 ) ) ) {
     499                                return substr( $t, 0, $i );
     500                        } else {
     501                                return $t;
     502                        }
     503                } else {
     504                        $this->translations[$s] = $singular;
     505                        $this->modified = true;
     506                        return $singular;
     507                }
     508        }
     509
     510        function translate_plural ($singular, $plural, $count, $context = null) {
     511                if ( !isset( $singular{0} ) ) return $singular;
     512
     513                // Get the "default" return-value
     514                $default = ($count == 1 ? $singular : $plural);
     515
     516                if ( $context == NULL ) {
     517                        $s = $singular;
     518                } else {
     519                        $s = $context . chr(4) . $singular;
     520                }
     521
     522                if ( $this->translations === NULL ) {
     523                        $this->import_domain_from_cache();
     524                }
     525
     526                if ( isset( $this->translations[$s] ) ) {
     527                        $t = $this->translations[$s];
     528                } elseif ( isset ($this->base_translations[$s] ) ) {
     529                        $t = $this->base_translations[$s];
     530                } else {
     531                        if ( false !== ( $t = $this->search_translation( $s ) ) ) {
     532                                $this->translations[$s] = $t;
     533                                $this->modified = true;
     534                        }
     535                }
     536
     537                if ( $t !== false ) {
     538                        if ( false !== ( $i = strpos( $t, 0 ) ) ) {
     539                                if ( $count == 1 ) {
     540                                        return substr ( $t, 0, $i );
     541                                } else {
     542                                        // only one plural form is assumed - needs improvement
     543                                        return substr( $t, $i+1 );
     544                                }
     545                        } else {
     546                                return $default;
     547                        }
     548                } else {
     549                        $this->translations[$s] = $singular . chr(0) . $plural;
     550                        $this->modified = true;
     551                        return $default;
     552                }
     553        }
     554
     555        function merge_with( &$other ) {
     556                if ( $other instanceof WPPP_MO_dynamic ) {
     557                        if ( $other->translations !== NULL ) {
     558                                foreach( $other->translations as $key => $translation ) {
     559                                        $this->translations[$key] = $translation;
     560                                }
     561                        }
     562                        if ( $other->base_translations !== NULL ) {
     563                                foreach( $other->base_translations as $key => $translation ) {
     564                                        $this->base_translations[$key] = $translation;
     565                                }
     566                        }
     567
     568                        foreach ( $other->MOs as $moitem ) {
     569                                $i = 0;
     570                                $c = count( $this->MOs );
     571                                $found = false;
     572                                while ( !$found && ( $i < $c ) ) {
     573                                        $found = $this->MOs[$i]->mofile == $moitem->mofile;
     574                                        $i++;
     575                                }
     576                                if ( !$found )
     577                                        $this->MOs[] = $moitem;
     578                        }
     579                }
     580        }
     581
     582        function MO_file_loaded ( $mofile ) {
     583                foreach ($this->MOs as $moitem) {
     584                        if ($moitem->mofile == $mofile) {
     585                                return true;
     586                        }
     587                }
     588                return false;
     589        }
     590}
  • new file src/wp-includes/pomo/native.php

    diff --git src/wp-includes/pomo/native.php src/wp-includes/pomo/native.php
    new file mode 100644
    index 0000000000..ca5ccb0ebc
    - +  
     1<?php
     2
     3  /**
     4   * Native GetText-Support for WordPress
     5   * ------------------------------------
     6   *
     7   * Copyright (C) 2012 Bernd Holzmueller <bernd@quarxconnect.de>
     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 3 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
     20   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
     21   *
     22   * @revision 02
     23   * @author Bernd Holzmueller <bernd@tiggerswelt.net>
     24   * @url http://oss.tiggerswelt.net/wordpress/3.3.1/
     25   **/
     26
     27  // Check if gettext-support is available
     28  if (!extension_loaded ('gettext'))
     29    return;
     30
     31  require_once dirname(__FILE__) . '/translations.php';
     32
     33  class Translate_GetText_Native extends Gettext_Translations {
     34    // Our default domain
     35    private $Domain = null;
     36
     37    // Merged domains
     38    private $pOthers = array ();
     39    private $sOthers = array ();
     40
     41    // {{{ select_plural_form
     42    /**
     43     * Given the number of items, returns the 0-based index of the plural form to use
     44     *
     45     * Here, in the base Translations class, the common logic for English is implemented:
     46     *      0 if there is one element, 1 otherwise
     47     *
     48     * This function should be overrided by the sub-classes. For example MO/PO can derive the logic
     49     * from their headers.
     50     *
     51     * @param integer $count number of items
     52     **/
     53    function select_plural_form ($count) {
     54      return (1 == $count? 0 : 1);
     55    }
     56    // }}}
     57
     58    function get_plural_forms_count () { return 2; }
     59
     60    // {{{ merge_with
     61    /**
     62     * Merge this translation with another one, the other one takes precedence
     63     *
     64     * @param object $other
     65     *
     66     * @access public
     67     * @return void
     68     **/
     69    function merge_with (&$other) {
     70      $this->pOthers [] = $other;
     71    }
     72    // }}}
     73
     74    // {{{ merge_originals_with
     75    /**
     76     * Merge this translation with another one, this one takes precedence
     77     *
     78     * @param object $other
     79     *
     80     * @access public
     81     * @return void
     82     **/
     83    function merge_originals_with (&$other) {
     84      $this->sOthers [] = $Other;
     85   }
     86    // }}}
     87
     88    // {{{ translate
     89    /**
     90     * Try to translate a given string
     91     *
     92     * @param string $singular
     93     * @param string $context (optional)
     94     *
     95     * @access public
     96     * @return string
     97     **/
     98    function translate ($singular, $context = null) {
     99      // Check for an empty string
     100      if (strlen ($singular) == 0)
     101        return $singular;
     102
     103      // Check other domains that take precedence
     104      foreach ($this->pOthers as $o)
     105        if (($t = $o->translate ($singular, $context)) != $singular)
     106          return $t;
     107
     108      // Make sure we have a domain assigned
     109      if ($this->Domain === null)
     110        return $singular;
     111
     112      // Translate without a context
     113      if ($context === null) {
     114        if (($t = dgettext ($this->Domain, $singular)) != $singular)
     115          return $t;
     116
     117      // Translate with a given context
     118      } else {
     119        $T = $context . "\x04" . $singular;
     120        $t = dgettext ($this->Domain, $T);
     121
     122        if ($T != $t)
     123          return $t;
     124      }
     125
     126      // Check for other domains
     127      foreach ($this->sOthers as $o)
     128        if (($t = $o->translate ($singular, $context)) != $singular)
     129          return $t;
     130
     131      return $singular;
     132    }
     133    // }}}
     134
     135    // {{{ translate_plural
     136    /**
     137     * Try to translate a plural string
     138     *
     139     * @param string $singular Singular version
     140     * @param string $plural Plural version
     141     * @param int $count Number of "items"
     142     * @param string $context (optional)
     143     *
     144     * @access public
     145     * @return string
     146     **/
     147    function translate_plural ($singular, $plural, $count, $context = null) {
     148      // Check for an empty string
     149      if (strlen ($singular) == 0)
     150        return $singular;
     151
     152      // Get the "default" return-value
     153      $default = ($count == 1 ? $singular : $plural);
     154
     155      // Check other domains that take precedence
     156      foreach ($this->pOthers as $o)
     157        if (($t = $o->translate_plural ($singular, $plural, $count, $context)) != $default)
     158          return $t;
     159
     160      // Make sure we have a domain assigned
     161      if ($this->Domain === null)
     162        return $default;
     163
     164      // Translate without context
     165      if ($context === null) {
     166        $t = dngettext ($this->Domain, $singular, $plural, $count);
     167
     168        if (($t != $singular) && ($t != $plural))
     169          return $t;
     170
     171      // Translate using a given context
     172      } else {
     173        $T = $context . "\x04" . $singular;
     174        $t = dngettext ($this->Domain, $T, $plural, $count);
     175
     176        if (($T != $t) && ($t != $plural))
     177          return $t;
     178      }
     179
     180      // Check other domains
     181      foreach ($this->sOthers as $o)
     182        if (($t = $o->translate_plural ($singular, $plural, $count, $context)) != $default)
     183          return $t;
     184
     185      return $default;
     186    }
     187    // }}}
     188
     189    // {{{ import_from_file
     190    /**
     191     * Fills up with the entries from MO file $filename
     192     *
     193     * @param string $filename MO file to load
     194     **/
     195    function import_from_file ($filename) {
     196      // Make sure that the locale is set correctly in environment
     197      global $locale;
     198
     199      $file_parts = pathinfo($filename);
     200      if($file_parts !== 'mo') {
     201        return false;
     202      }
     203
     204      putenv ('LC_ALL=' . $locale);
     205      setlocale (LC_ALL, $locale);
     206
     207      // Retrive MD5-hash of the file
     208      # DIRTY! But there is no other way at the moment to make this work
     209      if (!($Domain = md5_file ($filename)))
     210        return false;
     211
     212      // Make sure that the language-directory exists
     213      $Path = './wp-lang/' . $locale . '/LC_MESSAGES';
     214
     215      if (!wp_mkdir_p ($Path))
     216        return false;
     217
     218      // Make sure that the MO-File is existant at the destination
     219      $fn = $Path . '/' . $Domain . '.mo';
     220
     221      if (!is_file ($fn) && !@copy ($filename, $fn))
     222        return false;
     223
     224      // Setup the "domain" for gettext
     225      bindtextdomain ($Domain, './wp-lang/');
     226      bind_textdomain_codeset ($Domain, 'UTF-8');
     227
     228      // Do the final stuff and return success
     229      $this->Domain = $Domain;
     230
     231      return true;
     232    }
     233    // }}}
     234
     235    // {{{ export_to_file
     236    /**
     237     * @param string $filename
     238     * @return bool
     239     */
     240    function export_to_file($filename) {
     241        $fh = fopen($filename, 'wb');
     242        if ( !$fh ) return false;
     243        $res = $this->export_to_file_handle( $fh );
     244        fclose($fh);
     245        return $res;
     246    }
     247    // }}}
     248
     249    /**
     250     * @param resource $fh
     251     * @return true
     252     */
     253    function export_to_file_handle($fh) {
     254      $entries = array_filter( $this->entries, array( $this, 'is_entry_good_for_export' ) );
     255      ksort($entries);
     256      $magic = 0x950412de;
     257      $revision = 0;
     258      $total = count($entries) + 1; // all the headers are one entry
     259      $originals_lenghts_addr = 28;
     260      $translations_lenghts_addr = $originals_lenghts_addr + 8 * $total;
     261      $size_of_hash = 0;
     262      $hash_addr = $translations_lenghts_addr + 8 * $total;
     263      $current_addr = $hash_addr;
     264      fwrite($fh, pack('V*', $magic, $revision, $total, $originals_lenghts_addr,
     265        $translations_lenghts_addr, $size_of_hash, $hash_addr));
     266      fseek($fh, $originals_lenghts_addr);
     267
     268      // headers' msgid is an empty string
     269      fwrite($fh, pack('VV', 0, $current_addr));
     270      $current_addr++;
     271      $originals_table = chr(0);
     272
     273      $reader = new POMO_Reader();
     274
     275      foreach($entries as $entry) {
     276        $originals_table .= $this->export_original($entry) . chr(0);
     277        $length = $reader->strlen($this->export_original($entry));
     278        fwrite($fh, pack('VV', $length, $current_addr));
     279        $current_addr += $length + 1; // account for the NULL byte after
     280      }
     281
     282      $exported_headers = $this->export_headers();
     283      fwrite($fh, pack('VV', $reader->strlen($exported_headers), $current_addr));
     284      $current_addr += strlen($exported_headers) + 1;
     285      $translations_table = $exported_headers . chr(0);
     286
     287      foreach($entries as $entry) {
     288        $translations_table .= $this->export_translations($entry) . chr(0);
     289        $length = $reader->strlen($this->export_translations($entry));
     290        fwrite($fh, pack('VV', $length, $current_addr));
     291        $current_addr += $length + 1;
     292      }
     293
     294      fwrite($fh, $originals_table);
     295      fwrite($fh, $translations_table);
     296      return true;
     297    }
     298
     299    /**
     300     * @param Translation_Entry $entry
     301     * @return bool
     302     */
     303    function is_entry_good_for_export( $entry ) {
     304      if ( empty( $entry->translations ) ) {
     305        return false;
     306      }
     307
     308      if ( !array_filter( $entry->translations ) ) {
     309        return false;
     310      }
     311
     312      return true;
     313    }
     314
     315        /**
     316         * @param Translation_Entry $entry
     317         * @return string
     318         */
     319        function export_original($entry) {
     320                //TODO: warnings for control characters
     321                $exported = $entry->singular;
     322                if ($entry->is_plural) $exported .= chr(0).$entry->plural;
     323                if ($entry->context) $exported = $entry->context . chr(4) . $exported;
     324                return $exported;
     325        }
     326
     327        /**
     328         * @return string
     329         */
     330        function export_headers() {
     331                $exported = '';
     332                foreach($this->headers as $header => $value) {
     333                        $exported.= "$header: $value\n";
     334                }
     335                return $exported;
     336        }
     337        /**
     338         * @param Translation_Entry $entry
     339         * @return string
     340         */
     341        function export_translations($entry) {
     342                //TODO: warnings for control characters
     343                return $entry->is_plural ? implode(chr(0), $entry->translations) : $entry->translations[0];
     344        }
     345
     346  }
     347
     348  if (function_exists ('class_alias'))
     349    class_alias ('Translate_GetText_Native', 'MO');
     350  else {
     351    class MO extends Translate_GetText_Native { }
     352  }