Documentation
[lhc/web/wiklou.git] / languages / Language.php
index 555c4a9..cf2c308 100644 (file)
@@ -148,9 +148,6 @@ class Language {
         * @return Language
         */
        protected static function newFromCode( $code ) {
-               global $IP;
-               static $recursionLevel = 0;
-
                // Protect against path traversal below
                if ( !Language::isValidCode( $code )
                        || strcspn( $code, ":/\\\000" ) !== strlen( $code ) )
@@ -166,35 +163,31 @@ class Language {
                        return $lang;
                }
 
-               if ( $code == 'en' ) {
-                       $class = 'Language';
-               } else {
-                       $class = 'Language' . str_replace( '-', '_', ucfirst( $code ) );
-                       if ( !defined( 'MW_COMPILED' ) ) {
-                               // Preload base classes to work around APC/PHP5 bug
-                               if ( file_exists( "$IP/languages/classes/$class.deps.php" ) ) {
-                                       include_once( "$IP/languages/classes/$class.deps.php" );
-                               }
-                               if ( file_exists( "$IP/languages/classes/$class.php" ) ) {
-                                       include_once( "$IP/languages/classes/$class.php" );
-                               }
-                       }
+               // Check if there is a language class for the code
+               $class = self::classFromCode( $code );
+               self::preloadLanguageClass( $class );
+               if ( MWInit::classExists( $class ) ) {
+                       $lang = new $class;
+                       return $lang;
                }
 
-               if ( $recursionLevel > 5 ) {
-                       throw new MWException( "Language fallback loop detected when creating class $class\n" );
-               }
+               // Keep trying the fallback list until we find an existing class
+               $fallbacks = Language::getFallbacksFor( $code );
+               foreach ( $fallbacks as $fallbackCode ) {
+                       if ( !Language::isValidBuiltInCode( $fallbackCode ) ) {
+                               throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" );
+                       }
 
-               if ( !MWInit::classExists( $class ) ) {
-                       $fallback = Language::getFallbackFor( $code );
-                       ++$recursionLevel;
-                       $lang = Language::newFromCode( $fallback );
-                       --$recursionLevel;
-                       $lang->setCode( $code );
-               } else {
-                       $lang = new $class;
+                       $class = self::classFromCode( $fallbackCode );
+                       self::preloadLanguageClass( $class );
+                       if ( MWInit::classExists( $class ) ) {
+                               $lang = Language::newFromCode( $fallbackCode );
+                               $lang->setCode( $code );
+                               return $lang;
+                       }
                }
-               return $lang;
+
+               throw new MWException( "Invalid fallback sequence for language '$code'" );
        }
 
        /**
@@ -218,10 +211,37 @@ class Language {
         *
         * @param $code string
         *
+        * @since 1.18
         * @return bool
         */
        public static function isValidBuiltInCode( $code ) {
-               return preg_match( '/^[a-z0-9-]*$/i', $code );
+               return preg_match( '/^[a-z0-9-]+$/i', $code );
+       }
+
+       public static function classFromCode( $code ) {
+               if ( $code == 'en' ) {
+                       return 'Language';
+               } else {
+                       return 'Language' . str_replace( '-', '_', ucfirst( $code ) );
+               }
+       }
+
+       public static function preloadLanguageClass( $class ) {
+               global $IP;
+
+               if ( $class === 'Language' ) {
+                       return;
+               }
+
+               if ( !defined( 'MW_COMPILED' ) ) {
+                       // Preload base classes to work around APC/PHP5 bug
+                       if ( file_exists( "$IP/languages/classes/$class.deps.php" ) ) {
+                               include_once( "$IP/languages/classes/$class.deps.php" );
+                       }
+                       if ( file_exists( "$IP/languages/classes/$class.php" ) ) {
+                               include_once( "$IP/languages/classes/$class.php" );
+                       }
+               }
        }
 
        /**
@@ -265,14 +285,21 @@ class Language {
        function initContLang() { }
 
        /**
+        * Same as getFallbacksFor for current language.
         * @return array|bool
+        * @deprecated in 1.19
         */
        function getFallbackLanguageCode() {
-               if ( $this->mCode === 'en' ) {
-                       return false;
-               } else {
-                       return self::$dataCache->getItem( $this->mCode, 'fallback' );
-               }
+               wfDeprecated( __METHOD__ );
+               return self::getFallbackFor( $this->mCode );
+       }
+
+       /**
+        * @return array
+        * @since 1.19
+        */
+       function getFallbackLanguages() {
+               return self::getFallbacksFor( $this->mCode );
        }
 
        /**
@@ -314,6 +341,8 @@ class Language {
                        # The above mixing may leave namespaces out of canonical order.
                        # Re-order by namespace ID number...
                        ksort( $this->namespaceNames );
+
+                       wfRunHooks( 'LanguageGetNamespaces', array( &$this->namespaceNames ) );
                }
                return $this->namespaceNames;
        }
@@ -504,7 +533,8 @@ class Language {
                        $this->getMessage( 'qbsettings-fixedleft' ),
                        $this->getMessage( 'qbsettings-fixedright' ),
                        $this->getMessage( 'qbsettings-floatingleft' ),
-                       $this->getMessage( 'qbsettings-floatingright' )
+                       $this->getMessage( 'qbsettings-floatingright' ),
+                       $this->getMessage( 'qbsettings-directionality' )
                );
        }
 
@@ -550,13 +580,6 @@ class Language {
                return self::$dataCache->getSubitem( $this->mCode, 'imageFiles', $image );
        }
 
-       /**
-        * @return array
-        */
-       function getDefaultUserOptionOverrides() {
-               return self::$dataCache->getItem( $this->mCode, 'defaultUserOptionOverrides' );
-       }
-
        /**
         * @return array
         */
@@ -2357,10 +2380,14 @@ class Language {
        /**
         * A hidden direction mark (LRM or RLM), depending on the language direction
         *
+        * @param $opposite Boolean Get the direction mark opposite to your language
         * @return string
         */
-       function getDirMark() {
-               return $this->isRTL() ? "\xE2\x80\x8F" : "\xE2\x80\x8E";
+       function getDirMark( $opposite = false ) {
+               $rtl = "\xE2\x80\x8F";
+               $ltr = "\xE2\x80\x8E";
+               if( $opposite ) { return $this->isRTL() ? $ltr : $rtl; }
+               return $this->isRTL() ? $rtl : $ltr;
        }
 
        /**
@@ -2438,15 +2465,7 @@ class Language {
         * @param $newWords array
         */
        function addMagicWordsByLang( $newWords ) {
-               $code = $this->getCode();
-               $fallbackChain = array();
-               while ( $code && !in_array( $code, $fallbackChain ) ) {
-                       $fallbackChain[] = $code;
-                       $code = self::getFallbackFor( $code );
-               }
-               if ( !in_array( 'en', $fallbackChain ) ) {
-                       $fallbackChain[] = 'en';
-               }
+               $fallbackChain = $this->getFallbackLanguages();
                $fallbackChain = array_reverse( $fallbackChain );
                foreach ( $fallbackChain as $code ) {
                        if ( isset( $newWords[$code] ) ) {
@@ -2770,31 +2789,33 @@ class Language {
                        return $text; // string short enough even *with* HTML (short-circuit)
                }
 
-               $displayLen = 0; // innerHTML legth so far
+               $dispLen = 0; // innerHTML legth so far
                $testingEllipsis = false; // checking if ellipses will make string longer/equal?
                $tagType = 0; // 0-open, 1-close
                $bracketState = 0; // 1-tag start, 2-tag name, 0-neither
                $entityState = 0; // 0-not entity, 1-entity
-               $tag = $ret = $pRet = ''; // accumulated tag name, accumulated result string
+               $tag = $ret = ''; // accumulated tag name, accumulated result string
                $openTags = array(); // open tag stack
-               $pOpenTags = array();
+               $maybeState = null; // possible truncation state
 
                $textLen = strlen( $text );
                $neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated
                for ( $pos = 0; true; ++$pos ) {
                        # Consider truncation once the display length has reached the maximim.
+                       # We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
                        # Check that we're not in the middle of a bracket/entity...
-                       if ( $displayLen >= $neLength && $bracketState == 0 && $entityState == 0 ) {
+                       if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) {
                                if ( !$testingEllipsis ) {
                                        $testingEllipsis = true;
                                        # Save where we are; we will truncate here unless there turn out to
                                        # be so few remaining characters that truncation is not necessary.
-                                       $pOpenTags = $openTags; // save state
-                                       $pRet = $ret; // save state
-                               } elseif ( $displayLen > $length && $displayLen > strlen( $ellipsis ) ) {
+                                       if ( !$maybeState ) { // already saved? ($neLength = 0 case)
+                                               $maybeState = array( $ret, $openTags ); // save state
+                                       }
+                               } elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) {
                                        # String in fact does need truncation, the truncation point was OK.
-                                       $openTags = $pOpenTags; // reload state
-                                       $ret = $this->removeBadCharLast( $pRet ); // reload state, multi-byte char fix
+                                       list( $ret, $openTags ) = $maybeState; // reload state
+                                       $ret = $this->removeBadCharLast( $ret ); // multi-byte char fix
                                        $ret .= $ellipsis; // add ellipsis
                                        break;
                                }
@@ -2832,25 +2853,27 @@ class Language {
                                if ( $entityState ) {
                                        if ( $ch == ';' ) {
                                                $entityState = 0;
-                                               $displayLen++; // entity is one displayed char
+                                               $dispLen++; // entity is one displayed char
                                        }
                                } else {
+                                       if ( $neLength == 0 && !$maybeState ) {
+                                               // Save state without $ch. We want to *hit* the first
+                                               // display char (to get tags) but not *use* it if truncating.
+                                               $maybeState = array( substr( $ret, 0, -1 ), $openTags );
+                                       }
                                        if ( $ch == '&' ) {
                                                $entityState = 1; // entity found, (e.g. " ")
                                        } else {
-                                               $displayLen++; // this char is displayed
+                                               $dispLen++; // this char is displayed
                                                // Add the next $max display text chars after this in one swoop...
-                                               $max = ( $testingEllipsis ? $length : $neLength ) - $displayLen;
+                                               $max = ( $testingEllipsis ? $length : $neLength ) - $dispLen;
                                                $skipped = $this->truncate_skip( $ret, $text, "<>&", $pos + 1, $max );
-                                               $displayLen += $skipped;
+                                               $dispLen += $skipped;
                                                $pos += $skipped;
                                        }
                                }
                        }
                }
-               if ( $displayLen == 0 ) {
-                       return ''; // no text shown, nothing to format
-               }
                // Close the last tag if left unclosed by bad HTML
                $this->truncate_endBracket( $tag, $text[$textLen - 1], $tagType, $openTags );
                while ( count( $openTags ) > 0 ) {
@@ -2990,9 +3013,8 @@ class Language {
        }
 
        /**
-        * Maybe translate block durations.  Note that this function is somewhat misnamed: it
-        * deals with translating the *duration* ("1 week", "4 days", etc), not the expiry time
-        * (which is an absolute timestamp).
+        * This translates the duration ("1 week", "4 days", etc)
+        * as well as the expiry time (which is an absolute timestamp).
         * @param $str String: the validated block duration in English
         * @return Somehow translated block duration
         * @see LanguageFi.php for example implementation
@@ -3016,8 +3038,8 @@ class Language {
                                }
                        }
                }
-               // If all else fails, return the original string.
-               return $str;
+               // If no duration is given, but a timestamp, display that
+               return ( strtotime( $str ) ? $this->timeanddate( strtotime( $str ) ) : $str );
        }
 
        /**
@@ -3292,18 +3314,39 @@ class Language {
        }
 
        /**
-        * Get the fallback for a given language
+        * Get the first fallback for a given language. 
         *
         * @param $code string
         *
         * @return false|string
         */
        static function getFallbackFor( $code ) {
-               if ( $code === 'en' ) {
-                       // Shortcut
+               if ( $code === 'en' || !Language::isValidBuiltInCode( $code ) ) {
                        return false;
                } else {
-                       return self::getLocalisationCache()->getItem( $code, 'fallback' );
+                       $fallbacks = self::getFallbacksFor( $code );
+                       $first = array_shift( $fallbacks );
+                       return $first;
+               }
+       }
+
+       /**
+        * Get the ordered list of fallback languages.
+        *
+        * @since 1.19
+        * @param $code string Language code
+        * @return array
+        */
+       static function getFallbacksFor( $code ) {
+               if ( $code === 'en' || !Language::isValidBuiltInCode( $code ) ) {
+                       return array();
+               } else {
+                       $v = self::getLocalisationCache()->getItem( $code, 'fallback' );
+                       $v = array_map( 'trim', explode( ',', $v ) );
+                       if ( $v[count( $v ) - 1] !== 'en' ) {
+                               $v[] = 'en';
+                       }
+                       return $v;
                }
        }
 
@@ -3412,17 +3455,17 @@ class Language {
         * @todo Document
         * @param $seconds int|float
         * @param $format String Optional, one of ("avoidseconds","avoidminutes"):
-        *              If "avoidminutes" don't mention minutes if $seconds >= 1 hour
-        *              If "avoidseconds" don't mention seconds/minutes if $seconds > 2 days
+        *              "avoidseconds" - don't mention seconds if $seconds >= 1 hour
+        *              "avoidminutes" - don't mention seconds/minutes if $seconds > 48 hours
         * @return string
         */
        function formatTimePeriod( $seconds, $format = false ) {
                if ( round( $seconds * 10 ) < 100 ) {
-                       return $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) ) .
-                               $this->getMessageFromDB( 'seconds-abbrev' );
+                       $s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
+                       $s .= $this->getMessageFromDB( 'seconds-abbrev' );
                } elseif ( round( $seconds ) < 60 ) {
-                       return $this->formatNum( round( $seconds ) ) .
-                               $this->getMessageFromDB( 'seconds-abbrev' );
+                       $s = $this->formatNum( round( $seconds ) );
+                       $s .= $this->getMessageFromDB( 'seconds-abbrev' );
                } elseif ( round( $seconds ) < 3600 ) {
                        $minutes = floor( $seconds / 60 );
                        $secondsPart = round( fmod( $seconds, 60 ) );
@@ -3430,32 +3473,62 @@ class Language {
                                $secondsPart = 0;
                                $minutes++;
                        }
-                       return $this->formatNum( $minutes ) . $this->getMessageFromDB( 'minutes-abbrev' ) .
-                               ' ' .
-                               $this->formatNum( $secondsPart ) . $this->getMessageFromDB( 'seconds-abbrev' );
+                       $s = $this->formatNum( $minutes ) . $this->getMessageFromDB( 'minutes-abbrev' );
+                       $s .= ' ';
+                       $s .= $this->formatNum( $secondsPart ) . $this->getMessageFromDB( 'seconds-abbrev' );
                } elseif ( round( $seconds ) <= 2*86400 ) {
                        $hours = floor( $seconds / 3600 );
                        $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
-                       $secondsPart = floor( $seconds - $hours * 3600 - $minutes * 60 );
-                       $s = $this->formatNum( $hours ) . $this->getMessageFromDB( 'hours-abbrev' ) .
-                               ' ' .
-                               $this->formatNum( $minutes ) . $this->getMessageFromDB( 'minutes-abbrev' );
-                       if ( $format !== 'avoidseconds' ) {
+                       $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
+                       if ( $secondsPart == 60 ) {
+                               $secondsPart = 0;
+                               $minutes++;
+                       }
+                       if ( $minutes == 60 ) {
+                               $minutes = 0;
+                               $hours++;
+                       }
+                       $s = $this->formatNum( $hours ) . $this->getMessageFromDB( 'hours-abbrev' );
+                       $s .= ' ';
+                       $s .= $this->formatNum( $minutes ) . $this->getMessageFromDB( 'minutes-abbrev' );
+                       if ( !in_array( $format, array( 'avoidseconds', 'avoidminutes' ) ) ) {
                                $s .= ' ' . $this->formatNum( $secondsPart ) .
                                        $this->getMessageFromDB( 'seconds-abbrev' );
                        }
-                       return $s;
                } else {
                        $days = floor( $seconds / 86400 );
-                       $s = $this->formatNum( $days ) . $this->getMessageFromDB( 'days-abbrev' ) . ' ';
                        if ( $format === 'avoidminutes' ) {
+                               $hours = round( ( $seconds - $days * 86400 ) / 3600 );
+                               if ( $hours == 24 ) {
+                                       $hours = 0;
+                                       $days++;
+                               }
+                               $s = $this->formatNum( $days ) . $this->getMessageFromDB( 'days-abbrev' );
+                               $s .= ' ';
+                               $s .= $this->formatNum( $hours ) . $this->getMessageFromDB( 'hours-abbrev' );
+                       } elseif ( $format === 'avoidseconds' ) {
                                $hours = floor( ( $seconds - $days * 86400 ) / 3600 );
+                               $minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
+                               if ( $minutes == 60 ) {
+                                       $minutes = 0;
+                                       $hours++;
+                               }
+                               if ( $hours == 24 ) {
+                                       $hours = 0;
+                                       $days++;
+                               }
+                               $s = $this->formatNum( $days ) . $this->getMessageFromDB( 'days-abbrev' );
+                               $s .= ' ';
                                $s .= $this->formatNum( $hours ) . $this->getMessageFromDB( 'hours-abbrev' );
+                               $s .= ' ';
+                               $s .= $this->formatNum( $minutes ) . $this->getMessageFromDB( 'minutes-abbrev' );
                        } else {
+                               $s = $this->formatNum( $days ) . $this->getMessageFromDB( 'days-abbrev' );
+                               $s .= ' ';
                                $s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
                        }
-                       return $s;
                }
+               return $s;
        }
 
        /**
@@ -3481,7 +3554,7 @@ class Language {
         * Format a size in bytes for output, using an appropriate
         * unit (B, KB, MB or GB) according to the magnitude in question
         *
-        * @param $size Size to format
+        * @param $size int Size to format
         * @return string Plain text (not HTML)
         */
        function formatSize( $size ) {