<?php
/**
* Internationalisation code.
+ * See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more information.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
public $dateFormatStrings = [];
public $mExtendedSpecialPageAliases;
- protected $namespaceNames, $mNamespaceIds, $namespaceAliases;
+ /** @var array|null */
+ protected $namespaceNames;
+ protected $mNamespaceIds, $namespaceAliases;
/**
* ReplacementArray object caches
*/
static private $fallbackLanguageCache = [];
+ /**
+ * Cache for grammar rules data
+ * @var MapCacheLRU|null
+ */
+ static private $grammarTransformations;
+
/**
* Cache for language names
* @var HashBagOStuff|null
# Since these are limited, this is safe even later changes to the registry --
# the only oddity is that it might change the type of the tag, and thus
# the results from the capturing groups.
- # http://www.iana.org/assignments/language-subtag-registry
+ # https://www.iana.org/assignments/language-subtag-registry
$grandfathered = "en{$s}GB{$s}oed"
. "|i{$s}(?:ami|bnn|default|enochian|hak|klingon|lux|mingo|navajo|pwn|tao|tay|tsu)"
// People think language codes are html safe, so enforce it.
// Ideally we should only allow a-zA-Z0-9-
// but, .+ and other chars are often used for {{int:}} hacks
- // see bugs 37564, 37587, 36938
+ // see bugs T39564, T39587, T38938
$cache[$code] =
// Protect against path traversal
strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
function __construct() {
$this->mConverter = new FakeConverter( $this );
// Set the code to the name of the descendant
- if ( get_class( $this ) == 'Language' ) {
+ if ( static::class === 'Language' ) {
$this->mCode = 'en';
} else {
- $this->mCode = str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) );
+ $this->mCode = str_replace( '_', '-', strtolower( substr( static::class, 8 ) ) );
}
self::getLocalisationCache();
}
if ( is_null( $this->namespaceNames ) ) {
global $wgMetaNamespace, $wgMetaNamespaceTalk, $wgExtraNamespaces;
- $this->namespaceNames = self::$dataCache->getItem( $this->mCode, 'namespaceNames' );
$validNamespaces = MWNamespace::getCanonicalNamespaces();
- $this->namespaceNames = $wgExtraNamespaces + $this->namespaceNames + $validNamespaces;
+ $this->namespaceNames = $wgExtraNamespaces +
+ self::$dataCache->getItem( $this->mCode, 'namespaceNames' );
+ $this->namespaceNames += $validNamespaces;
$this->namespaceNames[NS_PROJECT] = $wgMetaNamespace;
if ( $wgMetaNamespaceTalk ) {
*
* Based on a PHP-Nuke block by Sharjeel which is released under GNU/GPL license
*
- * @see http://phpnuke.org/modules.php?name=News&file=article&sid=8234&mode=thread&order=0&thold=0
+ * @see https://phpnuke.org/modules.php?name=News&file=article&sid=8234&mode=thread&order=0&thold=0
*
* @param string $ts
*
* Algorithm to convert Gregorian dates to Thai solar dates,
* Minguo dates or Minguo dates.
*
- * Link: http://en.wikipedia.org/wiki/Thai_solar_calendar
- * http://en.wikipedia.org/wiki/Minguo_calendar
- * http://en.wikipedia.org/wiki/Japanese_era_name
+ * Link: https://en.wikipedia.org/wiki/Thai_solar_calendar
+ * https://en.wikipedia.org/wiki/Minguo_calendar
+ * https://en.wikipedia.org/wiki/Japanese_era_name
*
* @param string $ts 14-character timestamp
* @param string $cName Calender name
$data = explode( '|', $tz, 3 );
if ( $data[0] == 'ZoneInfo' ) {
- MediaWiki\suppressWarnings();
- $userTZ = timezone_open( $data[2] );
- MediaWiki\restoreWarnings();
- if ( $userTZ !== false ) {
- $date = date_create( $ts, timezone_open( 'UTC' ) );
- date_timezone_set( $date, $userTZ );
- $date = date_format( $date, 'YmdHis' );
- return $date;
+ try {
+ $userTZ = new DateTimeZone( $data[2] );
+ $date = new DateTime( $ts, new DateTimeZone( 'UTC' ) );
+ $date->setTimezone( $userTZ );
+ return $date->format( 'YmdHis' );
+ } catch ( Exception $e ) {
+ // Unrecognized timezone, default to 'Offset' with the stored offset.
+ $data[0] = 'Offset';
}
- # Unrecognized timezone, default to 'Offset' with the stored offset.
- $data[0] = 'Offset';
}
if ( $data[0] == 'System' || $tz == '' ) {
* the date preference they're supposed to use, it should be used in
* all children.
*
- *<code>
- * function timeanddate([...], $format = true) {
- * $datePreference = $this->dateFormat($format);
- * [...]
- * }
- *</code>
+ * function timeanddate([...], $format = true) {
+ * $datePreference = $this->dateFormat($format);
+ * [...]
+ * }
*
* @param int|string|bool $usePrefs If true, the user's preference is used
* if false, the site/language default is used
/**
* Takes a number of seconds and returns an array with a set of corresponding intervals.
- * For example 65 will be turned into array( minutes => 1, seconds => 5 ).
+ * For example 65 will be turned into [ minutes => 1, seconds => 5 ].
*
* @since 1.20
*
/**
* @param string $key
- * @return array|null
+ * @return string|null
*/
public function getMessage( $key ) {
return self::$dataCache->getSubitem( $this->mCode, 'messages', $key );
public function iconv( $in, $out, $string ) {
# Even with //IGNORE iconv can whine about illegal characters in
# *input* string. We just ignore those too.
- # REF: http://bugs.php.net/bug.php?id=37166
+ # REF: https://bugs.php.net/bug.php?id=37166
# REF: https://phabricator.wikimedia.org/T18885
MediaWiki\suppressWarnings();
$text = iconv( $in, $out . '//IGNORE', $string );
public function parseFormattedNumber( $number ) {
$s = $this->digitTransformTable();
if ( $s ) {
- // eliminate empty array values such as ''. (bug 64347)
+ // eliminate empty array values such as ''. (T66347)
$s = array_filter( $s );
$number = strtr( $number, array_flip( $s ) );
}
$s = $this->separatorTransformTable();
if ( $s ) {
- // eliminate empty array values such as ''. (bug 64347)
+ // eliminate empty array values such as ''. (T66347)
$s = array_filter( $s );
$number = strtr( $number, array_flip( $s ) );
}
$string = $ellipsis . $string;
}
}
- # Do not truncate if the ellipsis makes the string longer/equal (bug 22181).
+ # Do not truncate if the ellipsis makes the string longer/equal (T24181).
# This check is *not* redundant if $adjustLength, due to the single case where
# LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string.
if ( strlen( $string ) < strlen( $stringOriginal ) ) {
return $wgGrammarForms[$this->getCode()][$case][$word];
}
+ $grammarTransformations = $this->getGrammarTransformations();
+
+ if ( isset( $grammarTransformations[$case] ) ) {
+ $forms = $grammarTransformations[$case];
+
+ // Some names of grammar rules are aliases for other rules.
+ // In such cases the value is a string rather than object,
+ // so load the actual rules.
+ if ( is_string( $forms ) ) {
+ $forms = $grammarTransformations[$forms];
+ }
+
+ foreach ( array_values( $forms ) as $rule ) {
+ $form = $rule[0];
+
+ if ( $form === '@metadata' ) {
+ continue;
+ }
+
+ $replacement = $rule[1];
+
+ $regex = '/' . addcslashes( $form, '/' ) . '/u';
+ $patternMatches = preg_match( $regex, $word );
+
+ if ( $patternMatches === false ) {
+ wfLogWarning(
+ 'An error occurred while processing grammar. ' .
+ "Word: '$word'. Regex: /$form/."
+ );
+ } elseif ( $patternMatches === 1 ) {
+ $word = preg_replace( $regex, $replacement, $word );
+
+ break;
+ }
+ }
+ }
+
return $word;
}
+
/**
* Get the grammar forms for the content language
* @return array Array of grammar forms
return [];
}
+
+ /**
+ * Get the grammar transformations data for the language.
+ * Used like grammar forms, with {{GRAMMAR}} and cases,
+ * but uses pairs of regexes and replacements instead of code.
+ *
+ * @return array[] Array of grammar transformations.
+ * @throws MWException
+ * @since 1.28
+ */
+ public function getGrammarTransformations() {
+ $languageCode = $this->getCode();
+
+ if ( self::$grammarTransformations === null ) {
+ self::$grammarTransformations = new MapCacheLRU( 10 );
+ }
+
+ if ( self::$grammarTransformations->has( $languageCode ) ) {
+ return self::$grammarTransformations->get( $languageCode );
+ }
+
+ $data = [];
+
+ $grammarDataFile = __DIR__ . "/data/grammarTransformations/$languageCode.json";
+ if ( is_readable( $grammarDataFile ) ) {
+ $data = FormatJson::decode(
+ file_get_contents( $grammarDataFile ),
+ true
+ );
+
+ if ( $data === null ) {
+ throw new MWException( "Invalid grammar data for \"$languageCode\"." );
+ }
+
+ self::$grammarTransformations->set( $languageCode, $data );
+ }
+
+ return $data;
+ }
+
/**
* Provides an alternative text depending on specified gender.
* Usage {{gender:username|masculine|feminine|unknown}}.
*
* @param string $str The validated block duration in English
* @param User $user User object to use timezone from or null for $wgUser
+ * @param int $now Current timestamp, for formatting relative block durations
* @return string Somehow translated block duration
* @see LanguageFi.php for example implementation
*/
- function translateBlockExpiry( $str, User $user = null ) {
+ function translateBlockExpiry( $str, User $user = null, $now = 0 ) {
$duration = SpecialBlock::getSuggestedDurations( $this );
foreach ( $duration as $show => $value ) {
if ( strcmp( $str, $value ) == 0 ) {
}
// If all else fails, return a standard duration or timestamp description.
- $time = strtotime( $str, 0 );
+ $time = strtotime( $str, $now );
if ( $time === false ) { // Unknown format. Return it as-is in case.
return $str;
- } elseif ( $time !== strtotime( $str, 1 ) ) { // It's a relative timestamp.
- // $time is relative to 0 so it's a duration length.
- return $this->formatDuration( $time );
+ } elseif ( $time !== strtotime( $str, $now + 1 ) ) { // It's a relative timestamp.
+ // The result differs based on current time, so the difference
+ // is a fixed duration length.
+ return $this->formatDuration( $time - $now );
} else { // It's an absolute timestamp.
if ( $time === 0 ) {
// wfTimestamp() handles 0 as current time instead of epoch.
# such as action=raw much more expensive than they need to be.
# This will hopefully cover most cases.
$talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
- [ &$this, 'replaceGrammarInNamespace' ], $talk );
+ [ $this, 'replaceGrammarInNamespace' ], $talk );
return str_replace( ' ', '_', $talk );
}
}
/**
- * @todo Document
+ * Formats a time given in seconds into a string representation of that time.
+ *
* @param int|float $seconds
- * @param array $format Optional
- * If $format['avoid'] === 'avoidseconds': don't mention seconds if $seconds >= 1 hour.
- * If $format['avoid'] === 'avoidminutes': don't mention seconds/minutes if $seconds > 48 hours.
+ * @param array $format An optional argument that formats the returned string in different ways:
+ * If $format['avoid'] === 'avoidseconds': don't show seconds if $seconds >= 1 hour,
+ * If $format['avoid'] === 'avoidminutes': don't show seconds/minutes if $seconds > 48 hours,
* If $format['noabbrevs'] is true: use 'seconds' and friends instead of 'seconds-abbrev'
* and friends.
- * For backwards compatibility, $format may also be one of the strings 'avoidseconds'
+ * @note For backwards compatibility, $format may also be one of the strings 'avoidseconds'
* or 'avoidminutes'.
* @return string
*/