* @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 ) )
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'" );
}
/**
*
* @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" );
+ }
+ }
}
/**
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 );
}
/**
# 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;
}
$this->getMessage( 'qbsettings-fixedleft' ),
$this->getMessage( 'qbsettings-fixedright' ),
$this->getMessage( 'qbsettings-floatingleft' ),
- $this->getMessage( 'qbsettings-floatingright' )
+ $this->getMessage( 'qbsettings-floatingright' ),
+ $this->getMessage( 'qbsettings-directionality' )
);
}
return self::$dataCache->getSubitem( $this->mCode, 'imageFiles', $image );
}
- /**
- * @return array
- */
- function getDefaultUserOptionOverrides() {
- return self::$dataCache->getItem( $this->mCode, 'defaultUserOptionOverrides' );
- }
-
/**
* @return array
*/
/**
* 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;
}
/**
* @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] ) ) {
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;
}
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 ) {
}
/**
- * 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
}
}
}
- // 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 );
}
/**
}
/**
- * 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;
}
}
* @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 ) );
$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;
}
/**
* 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 ) {