Make WordPress Core

Ticket #17268: 17268.3.diff

File 17268.3.diff, 36.5 KB (added by Mte90, 7 years ago)

improving code also for unit tests

  • src/wp-includes/l10n.php

    diff --git src/wp-includes/l10n.php src/wp-includes/l10n.php
    index 72bb406bc3..ff3856011d 100644
    function load_textdomain( $domain, $mofile ) { 
    581581         */
    582582        $mofile = apply_filters( 'load_textdomain_mofile', $mofile, $domain );
    583583
    584         if ( !is_readable( $mofile ) ) return false;
     584        if ( !is_readable( $mofile ) ) return false;
     585
     586        $use_mo_dynamic = apply_filters( 'load_mo_dynamic', !extension_loaded ('gettext'), $domain );
     587
     588        if( $use_mo_dynamic ) {
     589                if ( isset( $l10n[$domain] ) ) {
     590                        if ( $l10n[$domain] instanceof MO_dynamic && $l10n[$domain]->Mo_file_loaded( $mofile ) ) {
     591                                return true;
     592                        }
     593                }
     594
     595                $mo = new MO_dynamic ( $domain, $cache_mo_dynamic );
     596        } else {
     597                $mo = new MO();
     598                if(extension_loaded ('gettext') ){
     599                    $mo->set_domain( $domain );
     600                }
     601        }
    585602
    586         $mo = new MO();
    587603        if ( !$mo->import_from_file( $mofile ) ) return false;
    588604
    589         if ( isset( $l10n[$domain] ) )
     605        if ( isset( $l10n[$domain] ) && !extension_loaded ('gettext') ){
    590606                $mo->merge_with( $l10n[$domain] );
     607                if ( $use_mo_dynamic ) {
     608                        $l10n[$domain]->unhook_and_close();
     609                }
     610        }
    591611
    592612        unset( $l10n_unloaded[ $domain ] );
    593613
    function load_default_textdomain( $locale = null ) { 
    666686                $locale = is_admin() ? get_user_locale() : get_locale();
    667687        }
    668688
     689        if (extension_loaded ('gettext')) {
     690            putenv ('LC_ALL=' . $locale);
     691            setlocale (LC_ALL, $locale);
     692        }
     693
    669694        // Unload previously loaded strings so we can switch translations.
    670695        unload_textdomain( 'default' );
    671696
  • src/wp-includes/load.php

    diff --git src/wp-includes/load.php src/wp-includes/load.php
    index 63a4b0f64c..37b30639c4 100644
    function wp_load_translations_early() { 
    870870        require ABSPATH . WPINC . '/version.php';
    871871
    872872        // Translation and localization
     873        require_once ABSPATH . WPINC . '/pomo/native.php' ;
    873874        require_once ABSPATH . WPINC . '/pomo/mo.php';
     875        require_once ABSPATH . WPINC . '/pomo/mo-dynamic-loader.php';
    874876        require_once ABSPATH . WPINC . '/l10n.php';
    875877        require_once ABSPATH . WPINC . '/class-wp-locale.php';
    876878        require_once ABSPATH . WPINC . '/class-wp-locale-switcher.php';
  • 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..a436825f1a
    - +  
     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    /**
     42     * Given the number of items, returns the 0-based index of the plural form to use
     43     *
     44     * Here, in the base Translations class, the common logic for English is implemented:
     45     *      0 if there is one element, 1 otherwise
     46     *
     47     * This function should be overrided by the sub-classes. For example MO/PO can derive the logic
     48     * from their headers.
     49     *
     50     * @param integer $count number of items
     51     **/
     52    function select_plural_form ($count) {
     53      return (1 == $count? 0 : 1);
     54    }
     55
     56    function get_plural_forms_count () { return 2; }
     57
     58    /**
     59     * Merge this translation with another one, the other one takes precedence
     60     *
     61     * @param object $other
     62     *
     63     * @access public
     64     * @return void
     65     **/
     66    function merge_with (&$other) {
     67      $this->pOthers [] = $other;
     68    }
     69
     70    /**
     71     * Merge this translation with another one, this one takes precedence
     72     *
     73     * @param object $other
     74     *
     75     * @access public
     76     * @return void
     77     **/
     78    function merge_originals_with (&$other) {
     79      $this->sOthers [] = $Other;
     80   }
     81
     82    /**
     83     * Try to translate a given string
     84     *
     85     * @param string $singular
     86     * @param string $context (optional)
     87     *
     88     * @access public
     89     * @return string
     90     **/
     91    function translate ($singular, $context = null) {
     92      // Check for an empty string
     93      if (strlen ($singular) == 0)
     94        return $singular;
     95
     96      // Check other domains that take precedence
     97      foreach ($this->pOthers as $o)
     98        if (($t = $o->translate ($singular, $context)) != $singular)
     99          return $t;
     100
     101      // Make sure we have a domain assigned
     102      if ($this->Domain === null)
     103        return $singular;
     104
     105      // Translate without a context
     106      if ($context === null) {
     107        if (($t = dgettext ($this->Domain, $singular)) != $singular)
     108          return $t;
     109
     110      // Translate with a given context
     111      } else {
     112        $T = $context . "\x04" . $singular;
     113        $t = dgettext ($this->Domain, $T);
     114
     115        if ($T != $t)
     116          return $t;
     117      }
     118
     119      // Check for other domains
     120      foreach ($this->sOthers as $o)
     121        if (($t = $o->translate ($singular, $context)) != $singular)
     122          return $t;
     123
     124      return $singular;
     125    }
     126
     127    /**
     128     * Try to translate a plural string
     129     *
     130     * @param string $singular Singular version
     131     * @param string $plural Plural version
     132     * @param int $count Number of "items"
     133     * @param string $context (optional)
     134     *
     135     * @access public
     136     * @return string
     137     **/
     138    function translate_plural ($singular, $plural, $count, $context = null) {
     139      // Check for an empty string
     140      if (strlen ($singular) == 0)
     141        return $singular;
     142
     143      // Get the "default" return-value
     144      $default = ($count == 1 ? $singular : $plural);
     145
     146      // Check other domains that take precedence
     147      foreach ($this->pOthers as $o)
     148        if (($t = $o->translate_plural ($singular, $plural, $count, $context)) != $default)
     149          return $t;
     150
     151      // Make sure we have a domain assigned
     152      if ($this->Domain === null)
     153        return $default;
     154
     155      // Translate without context
     156      if ($context === null) {
     157        $t = dngettext ($this->Domain, $singular, $plural, $count);
     158
     159        if (($t != $singular) && ($t != $plural))
     160          return $t;
     161
     162      // Translate using a given context
     163      } else {
     164        $T = $context . "\x04" . $singular;
     165        $t = dngettext ($this->Domain, $T, $plural, $count);
     166
     167        if (($T != $t) && ($t != $plural))
     168          return $t;
     169      }
     170
     171      // Check other domains
     172      foreach ($this->sOthers as $o)
     173        if (($t = $o->translate_plural ($singular, $plural, $count, $context)) != $default)
     174          return $t;
     175
     176      return $default;
     177    }
     178
     179    /**
     180     * Fills up with the entries from MO file $filename
     181     *
     182     * @param string $filename MO file to load
     183     **/
     184    function import_from_file ($filename) {
     185      // Make sure that the locale is set correctly in environment
     186      global $locale;
     187
     188      $file_parts = pathinfo($filename);
     189      if(isset($file_parts['extension']) && $file_parts['extension'] !== 'mo') {
     190        return false;
     191      }
     192
     193      if (!is_file ($filename)) {
     194        return false;
     195      }
     196
     197      $this->set_domain($this->Domain);
     198
     199      // Setup the "domain" for gettext
     200      bindtextdomain ($this->Domain, $filename);
     201      bind_textdomain_codeset ($this->Domain, 'UTF-8');
     202
     203      return true;
     204    }
     205
     206    /**
     207     * Set the domain for the mo file
     208     */
     209    function set_domain( $domain ) {
     210      if($domain === null) {
     211        $this->Domain = 'no-domain';
     212        return $domain;
     213      }
     214
     215      if($domain === 'default') {
     216        textdomain($domain);
     217      }
     218
     219      $this->Domain = $domain;
     220      return $domain;
     221    }
     222
     223    /**
     224     * @param string $filename
     225     * @return bool
     226     */
     227    function export_to_file($filename) {
     228      $fh = fopen($filename, 'wb');
     229      if ( !$fh ) return false;
     230      $res = $this->export_to_file_handle( $fh );
     231      fclose($fh);
     232      return $res;
     233    }
     234
     235    /**
     236     * @param resource $fh
     237     * @return true
     238     */
     239    function export_to_file_handle($fh) {
     240      $entries = array_filter( $this->entries, array( $this, 'is_entry_good_for_export' ) );
     241      ksort($entries);
     242      $magic = 0x950412de;
     243      $revision = 0;
     244      $total = count($entries) + 1; // all the headers are one entry
     245      $originals_lenghts_addr = 28;
     246      $translations_lenghts_addr = $originals_lenghts_addr + 8 * $total;
     247      $size_of_hash = 0;
     248      $hash_addr = $translations_lenghts_addr + 8 * $total;
     249      $current_addr = $hash_addr;
     250      fwrite($fh, pack('V*', $magic, $revision, $total, $originals_lenghts_addr,
     251        $translations_lenghts_addr, $size_of_hash, $hash_addr));
     252      fseek($fh, $originals_lenghts_addr);
     253
     254      // headers' msgid is an empty string
     255      fwrite($fh, pack('VV', 0, $current_addr));
     256      $current_addr++;
     257      $originals_table = chr(0);
     258
     259      $reader = new POMO_Reader();
     260
     261      foreach($entries as $entry) {
     262        $originals_table .= $this->export_original($entry) . chr(0);
     263        $length = $reader->strlen($this->export_original($entry));
     264        fwrite($fh, pack('VV', $length, $current_addr));
     265        $current_addr += $length + 1; // account for the NULL byte after
     266      }
     267
     268      $exported_headers = $this->export_headers();
     269      fwrite($fh, pack('VV', $reader->strlen($exported_headers), $current_addr));
     270      $current_addr += strlen($exported_headers) + 1;
     271      $translations_table = $exported_headers . chr(0);
     272
     273      foreach($entries as $entry) {
     274        $translations_table .= $this->export_translations($entry) . chr(0);
     275        $length = $reader->strlen($this->export_translations($entry));
     276        fwrite($fh, pack('VV', $length, $current_addr));
     277        $current_addr += $length + 1;
     278      }
     279
     280      fwrite($fh, $originals_table);
     281      fwrite($fh, $translations_table);
     282      return true;
     283    }
     284
     285    /**
     286     * @param Translation_Entry $entry
     287     * @return bool
     288     */
     289    function is_entry_good_for_export( $entry ) {
     290      if ( empty( $entry->translations ) ) {
     291        return false;
     292      }
     293
     294      if ( !array_filter( $entry->translations ) ) {
     295        return false;
     296      }
     297
     298      return true;
     299    }
     300
     301      /**
     302       * @param Translation_Entry $entry
     303       * @return string
     304       */
     305      function export_original($entry) {
     306        //TODO: warnings for control characters
     307        $exported = $entry->singular;
     308        if ($entry->is_plural) $exported .= chr(0).$entry->plural;
     309        if ($entry->context) $exported = $entry->context . chr(4) . $exported;
     310        return $exported;
     311      }
     312
     313      /**
     314       * @return string
     315       */
     316      function export_headers() {
     317        $exported = '';
     318        foreach($this->headers as $header => $value) {
     319              $exported.= "$header: $value\n";
     320        }
     321        return $exported;
     322      }
     323      /**
     324       * @param Translation_Entry $entry
     325       * @return string
     326       */
     327      function export_translations($entry) {
     328        //TODO: warnings for control characters
     329        return $entry->is_plural ? implode(chr(0), $entry->translations) : $entry->translations[0];
     330      }
     331
     332  }
     333
     334  if (function_exists ('class_alias'))
     335    class_alias ('Translate_GetText_Native', 'MO');
     336  else {
     337    class MO extends Translate_GetText_Native { }
     338  }
  • src/wp-settings.php

    diff --git src/wp-settings.php src/wp-settings.php
    index 3d4c210338..f31f264c26 100644
    require( ABSPATH . WPINC . '/functions.php' ); 
    9999require( ABSPATH . WPINC . '/class-wp-matchesmapregex.php' );
    100100require( ABSPATH . WPINC . '/class-wp.php' );
    101101require( ABSPATH . WPINC . '/class-wp-error.php' );
     102require( ABSPATH . WPINC . '/pomo/native.php' );
    102103require( ABSPATH . WPINC . '/pomo/mo.php' );
     104require( ABSPATH . WPINC . '/pomo/mo-dynamic-loader.php' );
    103105
    104106// Include the wpdb class and, if present, a db.php database drop-in.
    105107global $wpdb;
  • tests/phpunit/tests/pomo/mo.php

    diff --git tests/phpunit/tests/pomo/mo.php tests/phpunit/tests/pomo/mo.php
    index 6ad6695103..e6c04712e7 100644
    class Tests_POMO_MO extends WP_UnitTestCase { 
    55        function test_mo_simple() {
    66                $mo = new MO();
    77                $mo->import_from_file(DIR_TESTDATA . '/pomo/simple.mo');
    8                 $this->assertEquals(array('Project-Id-Version' => 'WordPress 2.6-bleeding', 'Report-Msgid-Bugs-To' => 'wp-polyglots@lists.automattic.com'), $mo->headers);
    9                 $this->assertEquals(2, count($mo->entries));
    10                 $this->assertEquals(array('dyado'), $mo->entries['baba']->translations);
    11                 $this->assertEquals(array('yes'), $mo->entries["kuku\nruku"]->translations);
     8                if (!extension_loaded ('gettext')) {
     9                    // Native system doesn't support the parse of headers
     10                    $this->assertEquals(array('Project-Id-Version' => 'WordPress 2.6-bleeding', 'Report-Msgid-Bugs-To' => 'wp-polyglots@lists.automattic.com'), $mo->headers);
     11                    // Native system doesn't enable to get the list of all the entries
     12                    $this->assertEquals(2, count($mo->entries));
     13                    $this->assertEquals(array('dyado'), $mo->entries['baba']->translations);
     14                    $this->assertEquals(array('yes'), $mo->entries["kuku\nruku"]->translations);
     15                } else {
     16                    $this->assertEquals(array('dyado'), $mo->translate_plural('baba', '%d dragons', 1));
     17                }
    1218        }
    1319
    1420        function test_mo_plural() {
    1521                $mo = new MO();
    1622                $mo->import_from_file(DIR_TESTDATA . '/pomo/plural.mo');
    17                 $this->assertEquals(1, count($mo->entries));
    18                 $this->assertEquals(array("oney dragoney", "twoey dragoney", "manyey dragoney", "manyeyey dragoney", "manyeyeyey dragoney"), $mo->entries["one dragon"]->translations);
     23                if (!extension_loaded ('gettext')) {
     24                    $this->assertEquals(1, count($mo->entries));
     25                    $this->assertEquals(array("oney dragoney", "twoey dragoney", "manyey dragoney", "manyeyey dragoney", "manyeyeyey dragoney"), $mo->entries["one dragon"]->translations);
     26                }
    1927
    2028                $this->assertEquals('oney dragoney', $mo->translate_plural('one dragon', '%d dragons', 1));
    2129                $this->assertEquals('twoey dragoney', $mo->translate_plural('one dragon', '%d dragons', 2));
    class Tests_POMO_MO extends WP_UnitTestCase { 
    4149        function test_mo_context() {
    4250                $mo = new MO();
    4351                $mo->import_from_file(DIR_TESTDATA . '/pomo/context.mo');
    44                 $this->assertEquals(2, count($mo->entries));
     52                if (!extension_loaded ('gettext')) {
     53                    $this->assertEquals(2, count($mo->entries));
     54                }
    4555                $plural_entry = new Translation_Entry(array('singular' => 'one dragon', 'plural' => '%d dragons', 'translations' => array("oney dragoney", "twoey dragoney","manyey dragoney"), 'context' => 'dragonland'));
    46                 $this->assertEquals($plural_entry, $mo->entries[$plural_entry->key()]);
    47                 $this->assertEquals("dragonland", $mo->entries[$plural_entry->key()]->context);
    48 
     56                if (!extension_loaded ('gettext')) {
     57                    $this->assertEquals($plural_entry, $mo->entries[$plural_entry->key()]);
     58                    $this->assertEquals("dragonland", $mo->entries[$plural_entry->key()]->context);
    4959                $single_entry = new Translation_Entry(array('singular' => 'one dragon', 'translations' => array("oney dragoney"), 'context' => 'not so dragon'));
    5060                $this->assertEquals($single_entry, $mo->entries[$single_entry->key()]);
    5161                $this->assertEquals("not so dragon", $mo->entries[$single_entry->key()]->context);
    52 
     62                }
    5363        }
    5464
    5565        function test_translations_merge() {
    class Tests_POMO_MO extends WP_UnitTestCase { 
    6070                $guest->add_entry(new Translation_Entry(array('singular' => 'green',)));
    6171                $guest->add_entry(new Translation_Entry(array('singular' => 'red',)));
    6272                $host->merge_with($guest);
    63                 $this->assertEquals(3, count($host->entries));
     73                if (!extension_loaded ('gettext')) {
     74                    $this->assertEquals(3, count($host->entries));
     75                }
    6476                $this->assertEquals(array(), array_diff(array('pink', 'green', 'red'), array_keys($host->entries)));
    6577        }
    6678
    class Tests_POMO_MO extends WP_UnitTestCase { 
    91103                $again = new MO();
    92104                $again->import_from_file($temp_fn);
    93105
    94                 $this->assertEquals(count($entries), count($again->entries));
    95                 foreach($entries as $entry) {
    96                         $this->assertEquals($entry, $again->entries[$entry->key()]);
     106                if (!extension_loaded ('gettext')) {
     107                    $this->assertEquals(count($entries), count($again->entries));
     108                    foreach($entries as $entry) {
     109                            $this->assertEquals($entry, $again->entries[$entry->key()]);
     110                    }
    97111                }
    98112        }
    99113