3 * Internationalisation code.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
25 * @defgroup Language Language
28 if ( !defined( 'MEDIAWIKI' ) ) {
29 echo "This file is part of MediaWiki, it is not a valid entry point.\n";
33 if ( function_exists( 'mb_strtoupper' ) ) {
34 mb_internal_encoding( 'UTF-8' );
38 * Internationalisation code
43 * @var LanguageConverter
47 public $mVariants, $mCode, $mLoaded = false;
48 public $mMagicExtensions = array(), $mMagicHookDone = false;
49 private $mHtmlCode = null, $mParentLanguage = false;
51 public $dateFormatStrings = array();
52 public $mExtendedSpecialPageAliases;
54 protected $namespaceNames, $mNamespaceIds, $namespaceAliases;
57 * ReplacementArray object caches
59 public $transformData = array();
62 * @var LocalisationCache
64 static public $dataCache;
66 static public $mLangObjCache = array();
68 static public $mWeekdayMsgs = array(
69 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
73 static public $mWeekdayAbbrevMsgs = array(
74 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
77 static public $mMonthMsgs = array(
78 'january', 'february', 'march', 'april', 'may_long', 'june',
79 'july', 'august', 'september', 'october', 'november',
82 static public $mMonthGenMsgs = array(
83 'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
84 'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
87 static public $mMonthAbbrevMsgs = array(
88 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
89 'sep', 'oct', 'nov', 'dec'
92 static public $mIranianCalendarMonthMsgs = array(
93 'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
94 'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
95 'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
96 'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
99 static public $mHebrewCalendarMonthMsgs = array(
100 'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
101 'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
102 'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
103 'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
104 'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
107 static public $mHebrewCalendarMonthGenMsgs = array(
108 'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
109 'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
110 'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
111 'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
112 'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
115 static public $mHijriCalendarMonthMsgs = array(
116 'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
117 'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
118 'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
119 'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
126 static public $durationIntervals = array(
127 'millennia' => 31556952000,
128 'centuries' => 3155695200,
129 'decades' => 315569520,
130 'years' => 31556952, // 86400 * ( 365 + ( 24 * 3 + 25 ) / 400 )
139 * Cache for language fallbacks.
140 * @see Language::getFallbacksIncludingSiteLanguage
144 static private $fallbackLanguageCache = array();
147 * Cache for language names
148 * @var MapCacheLRU|null
150 static private $languageNameCache;
153 * Unicode directional formatting characters, for embedBidi()
155 static private $lre = "\xE2\x80\xAA"; // U+202A LEFT-TO-RIGHT EMBEDDING
156 static private $rle = "\xE2\x80\xAB"; // U+202B RIGHT-TO-LEFT EMBEDDING
157 static private $pdf = "\xE2\x80\xAC"; // U+202C POP DIRECTIONAL FORMATTING
160 * Directionality test regex for embedBidi(). Matches the first strong directionality codepoint:
161 * - in group 1 if it is LTR
162 * - in group 2 if it is RTL
163 * Does not match if there is no strong directionality codepoint.
165 * The form is '/(?:([strong ltr codepoint])|([strong rtl codepoint]))/u' .
167 * Generated by UnicodeJS (see tools/strongDir) from the UCD; see
168 * https://git.wikimedia.org/summary/unicodejs.git .
170 // @codeCoverageIgnoreStart
171 static private $strongDirRegex = '/(?:([\x{41}-\x{5a}\x{61}-\x{7a}\x{aa}\x{b5}\x{ba}\x{c0}-\x{d6}\x{d8}-\x{f6}\x{f8}-\x{2b8}\x{2bb}-\x{2c1}\x{2d0}\x{2d1}\x{2e0}-\x{2e4}\x{2ee}\x{370}-\x{373}\x{376}\x{377}\x{37a}-\x{37d}\x{37f}\x{386}\x{388}-\x{38a}\x{38c}\x{38e}-\x{3a1}\x{3a3}-\x{3f5}\x{3f7}-\x{482}\x{48a}-\x{52f}\x{531}-\x{556}\x{559}-\x{55f}\x{561}-\x{587}\x{589}\x{903}-\x{939}\x{93b}\x{93d}-\x{940}\x{949}-\x{94c}\x{94e}-\x{950}\x{958}-\x{961}\x{964}-\x{980}\x{982}\x{983}\x{985}-\x{98c}\x{98f}\x{990}\x{993}-\x{9a8}\x{9aa}-\x{9b0}\x{9b2}\x{9b6}-\x{9b9}\x{9bd}-\x{9c0}\x{9c7}\x{9c8}\x{9cb}\x{9cc}\x{9ce}\x{9d7}\x{9dc}\x{9dd}\x{9df}-\x{9e1}\x{9e6}-\x{9f1}\x{9f4}-\x{9fa}\x{a03}\x{a05}-\x{a0a}\x{a0f}\x{a10}\x{a13}-\x{a28}\x{a2a}-\x{a30}\x{a32}\x{a33}\x{a35}\x{a36}\x{a38}\x{a39}\x{a3e}-\x{a40}\x{a59}-\x{a5c}\x{a5e}\x{a66}-\x{a6f}\x{a72}-\x{a74}\x{a83}\x{a85}-\x{a8d}\x{a8f}-\x{a91}\x{a93}-\x{aa8}\x{aaa}-\x{ab0}\x{ab2}\x{ab3}\x{ab5}-\x{ab9}\x{abd}-\x{ac0}\x{ac9}\x{acb}\x{acc}\x{ad0}\x{ae0}\x{ae1}\x{ae6}-\x{af0}\x{af9}\x{b02}\x{b03}\x{b05}-\x{b0c}\x{b0f}\x{b10}\x{b13}-\x{b28}\x{b2a}-\x{b30}\x{b32}\x{b33}\x{b35}-\x{b39}\x{b3d}\x{b3e}\x{b40}\x{b47}\x{b48}\x{b4b}\x{b4c}\x{b57}\x{b5c}\x{b5d}\x{b5f}-\x{b61}\x{b66}-\x{b77}\x{b83}\x{b85}-\x{b8a}\x{b8e}-\x{b90}\x{b92}-\x{b95}\x{b99}\x{b9a}\x{b9c}\x{b9e}\x{b9f}\x{ba3}\x{ba4}\x{ba8}-\x{baa}\x{bae}-\x{bb9}\x{bbe}\x{bbf}\x{bc1}\x{bc2}\x{bc6}-\x{bc8}\x{bca}-\x{bcc}\x{bd0}\x{bd7}\x{be6}-\x{bf2}\x{c01}-\x{c03}\x{c05}-\x{c0c}\x{c0e}-\x{c10}\x{c12}-\x{c28}\x{c2a}-\x{c39}\x{c3d}\x{c41}-\x{c44}\x{c58}-\x{c5a}\x{c60}\x{c61}\x{c66}-\x{c6f}\x{c7f}\x{c82}\x{c83}\x{c85}-\x{c8c}\x{c8e}-\x{c90}\x{c92}-\x{ca8}\x{caa}-\x{cb3}\x{cb5}-\x{cb9}\x{cbd}-\x{cc4}\x{cc6}-\x{cc8}\x{cca}\x{ccb}\x{cd5}\x{cd6}\x{cde}\x{ce0}\x{ce1}\x{ce6}-\x{cef}\x{cf1}\x{cf2}\x{d02}\x{d03}\x{d05}-\x{d0c}\x{d0e}-\x{d10}\x{d12}-\x{d3a}\x{d3d}-\x{d40}\x{d46}-\x{d48}\x{d4a}-\x{d4c}\x{d4e}\x{d57}\x{d5f}-\x{d61}\x{d66}-\x{d75}\x{d79}-\x{d7f}\x{d82}\x{d83}\x{d85}-\x{d96}\x{d9a}-\x{db1}\x{db3}-\x{dbb}\x{dbd}\x{dc0}-\x{dc6}\x{dcf}-\x{dd1}\x{dd8}-\x{ddf}\x{de6}-\x{def}\x{df2}-\x{df4}\x{e01}-\x{e30}\x{e32}\x{e33}\x{e40}-\x{e46}\x{e4f}-\x{e5b}\x{e81}\x{e82}\x{e84}\x{e87}\x{e88}\x{e8a}\x{e8d}\x{e94}-\x{e97}\x{e99}-\x{e9f}\x{ea1}-\x{ea3}\x{ea5}\x{ea7}\x{eaa}\x{eab}\x{ead}-\x{eb0}\x{eb2}\x{eb3}\x{ebd}\x{ec0}-\x{ec4}\x{ec6}\x{ed0}-\x{ed9}\x{edc}-\x{edf}\x{f00}-\x{f17}\x{f1a}-\x{f34}\x{f36}\x{f38}\x{f3e}-\x{f47}\x{f49}-\x{f6c}\x{f7f}\x{f85}\x{f88}-\x{f8c}\x{fbe}-\x{fc5}\x{fc7}-\x{fcc}\x{fce}-\x{fda}\x{1000}-\x{102c}\x{1031}\x{1038}\x{103b}\x{103c}\x{103f}-\x{1057}\x{105a}-\x{105d}\x{1061}-\x{1070}\x{1075}-\x{1081}\x{1083}\x{1084}\x{1087}-\x{108c}\x{108e}-\x{109c}\x{109e}-\x{10c5}\x{10c7}\x{10cd}\x{10d0}-\x{1248}\x{124a}-\x{124d}\x{1250}-\x{1256}\x{1258}\x{125a}-\x{125d}\x{1260}-\x{1288}\x{128a}-\x{128d}\x{1290}-\x{12b0}\x{12b2}-\x{12b5}\x{12b8}-\x{12be}\x{12c0}\x{12c2}-\x{12c5}\x{12c8}-\x{12d6}\x{12d8}-\x{1310}\x{1312}-\x{1315}\x{1318}-\x{135a}\x{1360}-\x{137c}\x{1380}-\x{138f}\x{13a0}-\x{13f5}\x{13f8}-\x{13fd}\x{1401}-\x{167f}\x{1681}-\x{169a}\x{16a0}-\x{16f8}\x{1700}-\x{170c}\x{170e}-\x{1711}\x{1720}-\x{1731}\x{1735}\x{1736}\x{1740}-\x{1751}\x{1760}-\x{176c}\x{176e}-\x{1770}\x{1780}-\x{17b3}\x{17b6}\x{17be}-\x{17c5}\x{17c7}\x{17c8}\x{17d4}-\x{17da}\x{17dc}\x{17e0}-\x{17e9}\x{1810}-\x{1819}\x{1820}-\x{1877}\x{1880}-\x{18a8}\x{18aa}\x{18b0}-\x{18f5}\x{1900}-\x{191e}\x{1923}-\x{1926}\x{1929}-\x{192b}\x{1930}\x{1931}\x{1933}-\x{1938}\x{1946}-\x{196d}\x{1970}-\x{1974}\x{1980}-\x{19ab}\x{19b0}-\x{19c9}\x{19d0}-\x{19da}\x{1a00}-\x{1a16}\x{1a19}\x{1a1a}\x{1a1e}-\x{1a55}\x{1a57}\x{1a61}\x{1a63}\x{1a64}\x{1a6d}-\x{1a72}\x{1a80}-\x{1a89}\x{1a90}-\x{1a99}\x{1aa0}-\x{1aad}\x{1b04}-\x{1b33}\x{1b35}\x{1b3b}\x{1b3d}-\x{1b41}\x{1b43}-\x{1b4b}\x{1b50}-\x{1b6a}\x{1b74}-\x{1b7c}\x{1b82}-\x{1ba1}\x{1ba6}\x{1ba7}\x{1baa}\x{1bae}-\x{1be5}\x{1be7}\x{1bea}-\x{1bec}\x{1bee}\x{1bf2}\x{1bf3}\x{1bfc}-\x{1c2b}\x{1c34}\x{1c35}\x{1c3b}-\x{1c49}\x{1c4d}-\x{1c7f}\x{1cc0}-\x{1cc7}\x{1cd3}\x{1ce1}\x{1ce9}-\x{1cec}\x{1cee}-\x{1cf3}\x{1cf5}\x{1cf6}\x{1d00}-\x{1dbf}\x{1e00}-\x{1f15}\x{1f18}-\x{1f1d}\x{1f20}-\x{1f45}\x{1f48}-\x{1f4d}\x{1f50}-\x{1f57}\x{1f59}\x{1f5b}\x{1f5d}\x{1f5f}-\x{1f7d}\x{1f80}-\x{1fb4}\x{1fb6}-\x{1fbc}\x{1fbe}\x{1fc2}-\x{1fc4}\x{1fc6}-\x{1fcc}\x{1fd0}-\x{1fd3}\x{1fd6}-\x{1fdb}\x{1fe0}-\x{1fec}\x{1ff2}-\x{1ff4}\x{1ff6}-\x{1ffc}\x{200e}\x{2071}\x{207f}\x{2090}-\x{209c}\x{2102}\x{2107}\x{210a}-\x{2113}\x{2115}\x{2119}-\x{211d}\x{2124}\x{2126}\x{2128}\x{212a}-\x{212d}\x{212f}-\x{2139}\x{213c}-\x{213f}\x{2145}-\x{2149}\x{214e}\x{214f}\x{2160}-\x{2188}\x{2336}-\x{237a}\x{2395}\x{249c}-\x{24e9}\x{26ac}\x{2800}-\x{28ff}\x{2c00}-\x{2c2e}\x{2c30}-\x{2c5e}\x{2c60}-\x{2ce4}\x{2ceb}-\x{2cee}\x{2cf2}\x{2cf3}\x{2d00}-\x{2d25}\x{2d27}\x{2d2d}\x{2d30}-\x{2d67}\x{2d6f}\x{2d70}\x{2d80}-\x{2d96}\x{2da0}-\x{2da6}\x{2da8}-\x{2dae}\x{2db0}-\x{2db6}\x{2db8}-\x{2dbe}\x{2dc0}-\x{2dc6}\x{2dc8}-\x{2dce}\x{2dd0}-\x{2dd6}\x{2dd8}-\x{2dde}\x{3005}-\x{3007}\x{3021}-\x{3029}\x{302e}\x{302f}\x{3031}-\x{3035}\x{3038}-\x{303c}\x{3041}-\x{3096}\x{309d}-\x{309f}\x{30a1}-\x{30fa}\x{30fc}-\x{30ff}\x{3105}-\x{312d}\x{3131}-\x{318e}\x{3190}-\x{31ba}\x{31f0}-\x{321c}\x{3220}-\x{324f}\x{3260}-\x{327b}\x{327f}-\x{32b0}\x{32c0}-\x{32cb}\x{32d0}-\x{32fe}\x{3300}-\x{3376}\x{337b}-\x{33dd}\x{33e0}-\x{33fe}\x{3400}-\x{4db5}\x{4e00}-\x{9fd5}\x{a000}-\x{a48c}\x{a4d0}-\x{a60c}\x{a610}-\x{a62b}\x{a640}-\x{a66e}\x{a680}-\x{a69d}\x{a6a0}-\x{a6ef}\x{a6f2}-\x{a6f7}\x{a722}-\x{a787}\x{a789}-\x{a7ad}\x{a7b0}-\x{a7b7}\x{a7f7}-\x{a801}\x{a803}-\x{a805}\x{a807}-\x{a80a}\x{a80c}-\x{a824}\x{a827}\x{a830}-\x{a837}\x{a840}-\x{a873}\x{a880}-\x{a8c3}\x{a8ce}-\x{a8d9}\x{a8f2}-\x{a8fd}\x{a900}-\x{a925}\x{a92e}-\x{a946}\x{a952}\x{a953}\x{a95f}-\x{a97c}\x{a983}-\x{a9b2}\x{a9b4}\x{a9b5}\x{a9ba}\x{a9bb}\x{a9bd}-\x{a9cd}\x{a9cf}-\x{a9d9}\x{a9de}-\x{a9e4}\x{a9e6}-\x{a9fe}\x{aa00}-\x{aa28}\x{aa2f}\x{aa30}\x{aa33}\x{aa34}\x{aa40}-\x{aa42}\x{aa44}-\x{aa4b}\x{aa4d}\x{aa50}-\x{aa59}\x{aa5c}-\x{aa7b}\x{aa7d}-\x{aaaf}\x{aab1}\x{aab5}\x{aab6}\x{aab9}-\x{aabd}\x{aac0}\x{aac2}\x{aadb}-\x{aaeb}\x{aaee}-\x{aaf5}\x{ab01}-\x{ab06}\x{ab09}-\x{ab0e}\x{ab11}-\x{ab16}\x{ab20}-\x{ab26}\x{ab28}-\x{ab2e}\x{ab30}-\x{ab65}\x{ab70}-\x{abe4}\x{abe6}\x{abe7}\x{abe9}-\x{abec}\x{abf0}-\x{abf9}\x{ac00}-\x{d7a3}\x{d7b0}-\x{d7c6}\x{d7cb}-\x{d7fb}\x{e000}-\x{fa6d}\x{fa70}-\x{fad9}\x{fb00}-\x{fb06}\x{fb13}-\x{fb17}\x{ff21}-\x{ff3a}\x{ff41}-\x{ff5a}\x{ff66}-\x{ffbe}\x{ffc2}-\x{ffc7}\x{ffca}-\x{ffcf}\x{ffd2}-\x{ffd7}\x{ffda}-\x{ffdc}\x{10000}-\x{1000b}\x{1000d}-\x{10026}\x{10028}-\x{1003a}\x{1003c}\x{1003d}\x{1003f}-\x{1004d}\x{10050}-\x{1005d}\x{10080}-\x{100fa}\x{10100}\x{10102}\x{10107}-\x{10133}\x{10137}-\x{1013f}\x{101d0}-\x{101fc}\x{10280}-\x{1029c}\x{102a0}-\x{102d0}\x{10300}-\x{10323}\x{10330}-\x{1034a}\x{10350}-\x{10375}\x{10380}-\x{1039d}\x{1039f}-\x{103c3}\x{103c8}-\x{103d5}\x{10400}-\x{1049d}\x{104a0}-\x{104a9}\x{10500}-\x{10527}\x{10530}-\x{10563}\x{1056f}\x{10600}-\x{10736}\x{10740}-\x{10755}\x{10760}-\x{10767}\x{11000}\x{11002}-\x{11037}\x{11047}-\x{1104d}\x{11066}-\x{1106f}\x{11082}-\x{110b2}\x{110b7}\x{110b8}\x{110bb}-\x{110c1}\x{110d0}-\x{110e8}\x{110f0}-\x{110f9}\x{11103}-\x{11126}\x{1112c}\x{11136}-\x{11143}\x{11150}-\x{11172}\x{11174}-\x{11176}\x{11182}-\x{111b5}\x{111bf}-\x{111c9}\x{111cd}\x{111d0}-\x{111df}\x{111e1}-\x{111f4}\x{11200}-\x{11211}\x{11213}-\x{1122e}\x{11232}\x{11233}\x{11235}\x{11238}-\x{1123d}\x{11280}-\x{11286}\x{11288}\x{1128a}-\x{1128d}\x{1128f}-\x{1129d}\x{1129f}-\x{112a9}\x{112b0}-\x{112de}\x{112e0}-\x{112e2}\x{112f0}-\x{112f9}\x{11302}\x{11303}\x{11305}-\x{1130c}\x{1130f}\x{11310}\x{11313}-\x{11328}\x{1132a}-\x{11330}\x{11332}\x{11333}\x{11335}-\x{11339}\x{1133d}-\x{1133f}\x{11341}-\x{11344}\x{11347}\x{11348}\x{1134b}-\x{1134d}\x{11350}\x{11357}\x{1135d}-\x{11363}\x{11480}-\x{114b2}\x{114b9}\x{114bb}-\x{114be}\x{114c1}\x{114c4}-\x{114c7}\x{114d0}-\x{114d9}\x{11580}-\x{115b1}\x{115b8}-\x{115bb}\x{115be}\x{115c1}-\x{115db}\x{11600}-\x{11632}\x{1163b}\x{1163c}\x{1163e}\x{11641}-\x{11644}\x{11650}-\x{11659}\x{11680}-\x{116aa}\x{116ac}\x{116ae}\x{116af}\x{116b6}\x{116c0}-\x{116c9}\x{11700}-\x{11719}\x{11720}\x{11721}\x{11726}\x{11730}-\x{1173f}\x{118a0}-\x{118f2}\x{118ff}\x{11ac0}-\x{11af8}\x{12000}-\x{12399}\x{12400}-\x{1246e}\x{12470}-\x{12474}\x{12480}-\x{12543}\x{13000}-\x{1342e}\x{14400}-\x{14646}\x{16800}-\x{16a38}\x{16a40}-\x{16a5e}\x{16a60}-\x{16a69}\x{16a6e}\x{16a6f}\x{16ad0}-\x{16aed}\x{16af5}\x{16b00}-\x{16b2f}\x{16b37}-\x{16b45}\x{16b50}-\x{16b59}\x{16b5b}-\x{16b61}\x{16b63}-\x{16b77}\x{16b7d}-\x{16b8f}\x{16f00}-\x{16f44}\x{16f50}-\x{16f7e}\x{16f93}-\x{16f9f}\x{1b000}\x{1b001}\x{1bc00}-\x{1bc6a}\x{1bc70}-\x{1bc7c}\x{1bc80}-\x{1bc88}\x{1bc90}-\x{1bc99}\x{1bc9c}\x{1bc9f}\x{1d000}-\x{1d0f5}\x{1d100}-\x{1d126}\x{1d129}-\x{1d166}\x{1d16a}-\x{1d172}\x{1d183}\x{1d184}\x{1d18c}-\x{1d1a9}\x{1d1ae}-\x{1d1e8}\x{1d360}-\x{1d371}\x{1d400}-\x{1d454}\x{1d456}-\x{1d49c}\x{1d49e}\x{1d49f}\x{1d4a2}\x{1d4a5}\x{1d4a6}\x{1d4a9}-\x{1d4ac}\x{1d4ae}-\x{1d4b9}\x{1d4bb}\x{1d4bd}-\x{1d4c3}\x{1d4c5}-\x{1d505}\x{1d507}-\x{1d50a}\x{1d50d}-\x{1d514}\x{1d516}-\x{1d51c}\x{1d51e}-\x{1d539}\x{1d53b}-\x{1d53e}\x{1d540}-\x{1d544}\x{1d546}\x{1d54a}-\x{1d550}\x{1d552}-\x{1d6a5}\x{1d6a8}-\x{1d6da}\x{1d6dc}-\x{1d714}\x{1d716}-\x{1d74e}\x{1d750}-\x{1d788}\x{1d78a}-\x{1d7c2}\x{1d7c4}-\x{1d7cb}\x{1d800}-\x{1d9ff}\x{1da37}-\x{1da3a}\x{1da6d}-\x{1da74}\x{1da76}-\x{1da83}\x{1da85}-\x{1da8b}\x{1f110}-\x{1f12e}\x{1f130}-\x{1f169}\x{1f170}-\x{1f19a}\x{1f1e6}-\x{1f202}\x{1f210}-\x{1f23a}\x{1f240}-\x{1f248}\x{1f250}\x{1f251}\x{20000}-\x{2a6d6}\x{2a700}-\x{2b734}\x{2b740}-\x{2b81d}\x{2b820}-\x{2cea1}\x{2f800}-\x{2fa1d}\x{f0000}-\x{ffffd}\x{100000}-\x{10fffd}])|([\x{590}\x{5be}\x{5c0}\x{5c3}\x{5c6}\x{5c8}-\x{5ff}\x{7c0}-\x{7ea}\x{7f4}\x{7f5}\x{7fa}-\x{815}\x{81a}\x{824}\x{828}\x{82e}-\x{858}\x{85c}-\x{89f}\x{200f}\x{fb1d}\x{fb1f}-\x{fb28}\x{fb2a}-\x{fb4f}\x{10800}-\x{1091e}\x{10920}-\x{10a00}\x{10a04}\x{10a07}-\x{10a0b}\x{10a10}-\x{10a37}\x{10a3b}-\x{10a3e}\x{10a40}-\x{10ae4}\x{10ae7}-\x{10b38}\x{10b40}-\x{10e5f}\x{10e7f}-\x{10fff}\x{1e800}-\x{1e8cf}\x{1e8d7}-\x{1edff}\x{1ef00}-\x{1efff}\x{608}\x{60b}\x{60d}\x{61b}-\x{64a}\x{66d}-\x{66f}\x{671}-\x{6d5}\x{6e5}\x{6e6}\x{6ee}\x{6ef}\x{6fa}-\x{710}\x{712}-\x{72f}\x{74b}-\x{7a5}\x{7b1}-\x{7bf}\x{8a0}-\x{8e2}\x{fb50}-\x{fd3d}\x{fd40}-\x{fdcf}\x{fdf0}-\x{fdfc}\x{fdfe}\x{fdff}\x{fe70}-\x{fefe}\x{1ee00}-\x{1eeef}\x{1eef2}-\x{1eeff}]))/u';
172 // @codeCoverageIgnoreEnd
175 * Get a cached or new language object for a given language code
176 * @param string $code
179 static function factory( $code ) {
180 global $wgDummyLanguageCodes, $wgLangObjCacheSize;
182 if ( isset( $wgDummyLanguageCodes[$code] ) ) {
183 $code = $wgDummyLanguageCodes[$code];
186 // get the language object to process
187 $langObj = isset( self
::$mLangObjCache[$code] )
188 ? self
::$mLangObjCache[$code]
189 : self
::newFromCode( $code );
191 // merge the language object in to get it up front in the cache
192 self
::$mLangObjCache = array_merge( array( $code => $langObj ), self
::$mLangObjCache );
193 // get rid of the oldest ones in case we have an overflow
194 self
::$mLangObjCache = array_slice( self
::$mLangObjCache, 0, $wgLangObjCacheSize, true );
200 * Create a language object for a given language code
201 * @param string $code
202 * @throws MWException
205 protected static function newFromCode( $code ) {
206 // Protect against path traversal below
207 if ( !Language
::isValidCode( $code )
208 ||
strcspn( $code, ":/\\\000" ) !== strlen( $code )
210 throw new MWException( "Invalid language code \"$code\"" );
213 if ( !Language
::isValidBuiltInCode( $code ) ) {
214 // It's not possible to customise this code with class files, so
215 // just return a Language object. This is to support uselang= hacks.
216 $lang = new Language
;
217 $lang->setCode( $code );
221 // Check if there is a language class for the code
222 $class = self
::classFromCode( $code );
223 self
::preloadLanguageClass( $class );
224 if ( class_exists( $class ) ) {
229 // Keep trying the fallback list until we find an existing class
230 $fallbacks = Language
::getFallbacksFor( $code );
231 foreach ( $fallbacks as $fallbackCode ) {
232 if ( !Language
::isValidBuiltInCode( $fallbackCode ) ) {
233 throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" );
236 $class = self
::classFromCode( $fallbackCode );
237 self
::preloadLanguageClass( $class );
238 if ( class_exists( $class ) ) {
239 $lang = Language
::newFromCode( $fallbackCode );
240 $lang->setCode( $code );
245 throw new MWException( "Invalid fallback sequence for language '$code'" );
249 * Checks whether any localisation is available for that language tag
250 * in MediaWiki (MessagesXx.php exists).
252 * @param string $code Language tag (in lower case)
253 * @return bool Whether language is supported
256 public static function isSupportedLanguage( $code ) {
257 return self
::isValidBuiltInCode( $code )
258 && ( is_readable( self
::getMessagesFileName( $code ) )
259 ||
is_readable( self
::getJsonMessagesFileName( $code ) )
264 * Returns true if a language code string is a well-formed language tag
265 * according to RFC 5646.
266 * This function only checks well-formedness; it doesn't check that
267 * language, script or variant codes actually exist in the repositories.
269 * Based on regexes by Mark Davis of the Unicode Consortium:
270 * http://unicode.org/repos/cldr/trunk/tools/java/org/unicode/cldr/util/data/langtagRegex.txt
272 * @param string $code
273 * @param bool $lenient Whether to allow '_' as separator. The default is only '-'.
278 public static function isWellFormedLanguageTag( $code, $lenient = false ) {
281 $alphanum = '[a-z0-9]';
282 $x = 'x'; # private use singleton
283 $singleton = '[a-wy-z]'; # other singleton
284 $s = $lenient ?
'[-_]' : '-';
286 $language = "$alpha{2,8}|$alpha{2,3}$s$alpha{3}";
287 $script = "$alpha{4}"; # ISO 15924
288 $region = "(?:$alpha{2}|$digit{3})"; # ISO 3166-1 alpha-2 or UN M.49
289 $variant = "(?:$alphanum{5,8}|$digit$alphanum{3})";
290 $extension = "$singleton(?:$s$alphanum{2,8})+";
291 $privateUse = "$x(?:$s$alphanum{1,8})+";
293 # Define certain grandfathered codes, since otherwise the regex is pretty useless.
294 # Since these are limited, this is safe even later changes to the registry --
295 # the only oddity is that it might change the type of the tag, and thus
296 # the results from the capturing groups.
297 # http://www.iana.org/assignments/language-subtag-registry
299 $grandfathered = "en{$s}GB{$s}oed"
300 . "|i{$s}(?:ami|bnn|default|enochian|hak|klingon|lux|mingo|navajo|pwn|tao|tay|tsu)"
301 . "|no{$s}(?:bok|nyn)"
302 . "|sgn{$s}(?:BE{$s}(?:fr|nl)|CH{$s}de)"
303 . "|zh{$s}min{$s}nan";
305 $variantList = "$variant(?:$s$variant)*";
306 $extensionList = "$extension(?:$s$extension)*";
308 $langtag = "(?:($language)"
311 . "(?:$s$variantList)?"
312 . "(?:$s$extensionList)?"
313 . "(?:$s$privateUse)?)";
315 # The final breakdown, with capturing groups for each of these components
316 # The variants, extensions, grandfathered, and private-use may have interior '-'
318 $root = "^(?:$langtag|$privateUse|$grandfathered)$";
320 return (bool)preg_match( "/$root/", strtolower( $code ) );
324 * Returns true if a language code string is of a valid form, whether or
325 * not it exists. This includes codes which are used solely for
326 * customisation via the MediaWiki namespace.
328 * @param string $code
332 public static function isValidCode( $code ) {
333 static $cache = array();
334 if ( isset( $cache[$code] ) ) {
335 return $cache[$code];
337 // People think language codes are html safe, so enforce it.
338 // Ideally we should only allow a-zA-Z0-9-
339 // but, .+ and other chars are often used for {{int:}} hacks
340 // see bugs 37564, 37587, 36938
342 strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
343 && !preg_match( MediaWikiTitleCodec
::getTitleInvalidRegex(), $code );
345 return $cache[$code];
349 * Returns true if a language code is of a valid form for the purposes of
350 * internal customisation of MediaWiki, via Messages*.php or *.json.
352 * @param string $code
354 * @throws MWException
358 public static function isValidBuiltInCode( $code ) {
360 if ( !is_string( $code ) ) {
361 if ( is_object( $code ) ) {
362 $addmsg = " of class " . get_class( $code );
366 $type = gettype( $code );
367 throw new MWException( __METHOD__
. " must be passed a string, $type given$addmsg" );
370 return (bool)preg_match( '/^[a-z0-9-]{2,}$/', $code );
374 * Returns true if a language code is an IETF tag known to MediaWiki.
381 public static function isKnownLanguageTag( $tag ) {
382 static $coreLanguageNames;
384 // Quick escape for invalid input to avoid exceptions down the line
385 // when code tries to process tags which are not valid at all.
386 if ( !self
::isValidBuiltInCode( $tag ) ) {
390 if ( $coreLanguageNames === null ) {
392 include "$IP/languages/Names.php";
395 if ( isset( $coreLanguageNames[$tag] )
396 || self
::fetchLanguageName( $tag, $tag ) !== ''
405 * @param string $code
406 * @return string Name of the language class
408 public static function classFromCode( $code ) {
409 if ( $code == 'en' ) {
412 return 'Language' . str_replace( '-', '_', ucfirst( $code ) );
417 * Includes language class files
419 * @param string $class Name of the language class
421 public static function preloadLanguageClass( $class ) {
424 if ( $class === 'Language' ) {
428 if ( file_exists( "$IP/languages/classes/$class.php" ) ) {
429 include_once "$IP/languages/classes/$class.php";
434 * Get the LocalisationCache instance
436 * @return LocalisationCache
438 public static function getLocalisationCache() {
439 if ( is_null( self
::$dataCache ) ) {
440 global $wgLocalisationCacheConf;
441 $class = $wgLocalisationCacheConf['class'];
442 self
::$dataCache = new $class( $wgLocalisationCacheConf );
444 return self
::$dataCache;
447 function __construct() {
448 $this->mConverter
= new FakeConverter( $this );
449 // Set the code to the name of the descendant
450 if ( get_class( $this ) == 'Language' ) {
453 $this->mCode
= str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) );
455 self
::getLocalisationCache();
459 * Reduce memory usage
461 function __destruct() {
462 foreach ( $this as $name => $value ) {
463 unset( $this->$name );
468 * Hook which will be called if this is the content language.
469 * Descendants can use this to register hook functions or modify globals
471 function initContLang() {
478 function getFallbackLanguages() {
479 return self
::getFallbacksFor( $this->mCode
);
483 * Exports $wgBookstoreListEn
486 function getBookstoreList() {
487 return self
::$dataCache->getItem( $this->mCode
, 'bookstoreList' );
491 * Returns an array of localised namespaces indexed by their numbers. If the namespace is not
492 * available in localised form, it will be included in English.
496 public function getNamespaces() {
497 if ( is_null( $this->namespaceNames
) ) {
498 global $wgMetaNamespace, $wgMetaNamespaceTalk, $wgExtraNamespaces;
500 $this->namespaceNames
= self
::$dataCache->getItem( $this->mCode
, 'namespaceNames' );
501 $validNamespaces = MWNamespace
::getCanonicalNamespaces();
503 $this->namespaceNames
= $wgExtraNamespaces +
$this->namespaceNames +
$validNamespaces;
505 $this->namespaceNames
[NS_PROJECT
] = $wgMetaNamespace;
506 if ( $wgMetaNamespaceTalk ) {
507 $this->namespaceNames
[NS_PROJECT_TALK
] = $wgMetaNamespaceTalk;
509 $talk = $this->namespaceNames
[NS_PROJECT_TALK
];
510 $this->namespaceNames
[NS_PROJECT_TALK
] =
511 $this->fixVariableInNamespace( $talk );
514 # Sometimes a language will be localised but not actually exist on this wiki.
515 foreach ( $this->namespaceNames
as $key => $text ) {
516 if ( !isset( $validNamespaces[$key] ) ) {
517 unset( $this->namespaceNames
[$key] );
521 # The above mixing may leave namespaces out of canonical order.
522 # Re-order by namespace ID number...
523 ksort( $this->namespaceNames
);
525 Hooks
::run( 'LanguageGetNamespaces', array( &$this->namespaceNames
) );
528 return $this->namespaceNames
;
532 * Arbitrarily set all of the namespace names at once. Mainly used for testing
533 * @param array $namespaces Array of namespaces (id => name)
535 public function setNamespaces( array $namespaces ) {
536 $this->namespaceNames
= $namespaces;
537 $this->mNamespaceIds
= null;
541 * Resets all of the namespace caches. Mainly used for testing
543 public function resetNamespaces() {
544 $this->namespaceNames
= null;
545 $this->mNamespaceIds
= null;
546 $this->namespaceAliases
= null;
550 * A convenience function that returns the same thing as
551 * getNamespaces() except with the array values changed to ' '
552 * where it found '_', useful for producing output to be displayed
553 * e.g. in <select> forms.
557 function getFormattedNamespaces() {
558 $ns = $this->getNamespaces();
559 foreach ( $ns as $k => $v ) {
560 $ns[$k] = strtr( $v, '_', ' ' );
566 * Get a namespace value by key
568 * $mw_ns = $wgContLang->getNsText( NS_MEDIAWIKI );
569 * echo $mw_ns; // prints 'MediaWiki'
572 * @param int $index The array key of the namespace to return
573 * @return string|bool String if the namespace value exists, otherwise false
575 function getNsText( $index ) {
576 $ns = $this->getNamespaces();
578 return isset( $ns[$index] ) ?
$ns[$index] : false;
582 * A convenience function that returns the same thing as
583 * getNsText() except with '_' changed to ' ', useful for
587 * $mw_ns = $wgContLang->getFormattedNsText( NS_MEDIAWIKI_TALK );
588 * echo $mw_ns; // prints 'MediaWiki talk'
591 * @param int $index The array key of the namespace to return
592 * @return string Namespace name without underscores (empty string if namespace does not exist)
594 function getFormattedNsText( $index ) {
595 $ns = $this->getNsText( $index );
597 return strtr( $ns, '_', ' ' );
601 * Returns gender-dependent namespace alias if available.
602 * See https://www.mediawiki.org/wiki/Manual:$wgExtraGenderNamespaces
603 * @param int $index Namespace index
604 * @param string $gender Gender key (male, female... )
608 function getGenderNsText( $index, $gender ) {
609 global $wgExtraGenderNamespaces;
611 $ns = $wgExtraGenderNamespaces +
612 (array)self
::$dataCache->getItem( $this->mCode
, 'namespaceGenderAliases' );
614 return isset( $ns[$index][$gender] ) ?
$ns[$index][$gender] : $this->getNsText( $index );
618 * Whether this language uses gender-dependent namespace aliases.
619 * See https://www.mediawiki.org/wiki/Manual:$wgExtraGenderNamespaces
623 function needsGenderDistinction() {
624 global $wgExtraGenderNamespaces, $wgExtraNamespaces;
625 if ( count( $wgExtraGenderNamespaces ) > 0 ) {
626 // $wgExtraGenderNamespaces overrides everything
628 } elseif ( isset( $wgExtraNamespaces[NS_USER
] ) && isset( $wgExtraNamespaces[NS_USER_TALK
] ) ) {
629 /// @todo There may be other gender namespace than NS_USER & NS_USER_TALK in the future
630 // $wgExtraNamespaces overrides any gender aliases specified in i18n files
633 // Check what is in i18n files
634 $aliases = self
::$dataCache->getItem( $this->mCode
, 'namespaceGenderAliases' );
635 return count( $aliases ) > 0;
640 * Get a namespace key by value, case insensitive.
641 * Only matches namespace names for the current language, not the
642 * canonical ones defined in Namespace.php.
644 * @param string $text
645 * @return int|bool An integer if $text is a valid value otherwise false
647 function getLocalNsIndex( $text ) {
648 $lctext = $this->lc( $text );
649 $ids = $this->getNamespaceIds();
650 return isset( $ids[$lctext] ) ?
$ids[$lctext] : false;
656 function getNamespaceAliases() {
657 if ( is_null( $this->namespaceAliases
) ) {
658 $aliases = self
::$dataCache->getItem( $this->mCode
, 'namespaceAliases' );
662 foreach ( $aliases as $name => $index ) {
663 if ( $index === NS_PROJECT_TALK
) {
664 unset( $aliases[$name] );
665 $name = $this->fixVariableInNamespace( $name );
666 $aliases[$name] = $index;
671 global $wgExtraGenderNamespaces;
672 $genders = $wgExtraGenderNamespaces +
673 (array)self
::$dataCache->getItem( $this->mCode
, 'namespaceGenderAliases' );
674 foreach ( $genders as $index => $forms ) {
675 foreach ( $forms as $alias ) {
676 $aliases[$alias] = $index;
680 # Also add converted namespace names as aliases, to avoid confusion.
681 $convertedNames = array();
682 foreach ( $this->getVariants() as $variant ) {
683 if ( $variant === $this->mCode
) {
686 foreach ( $this->getNamespaces() as $ns => $_ ) {
687 $convertedNames[$this->getConverter()->convertNamespace( $ns, $variant )] = $ns;
691 $this->namespaceAliases
= $aliases +
$convertedNames;
694 return $this->namespaceAliases
;
700 function getNamespaceIds() {
701 if ( is_null( $this->mNamespaceIds
) ) {
702 global $wgNamespaceAliases;
703 # Put namespace names and aliases into a hashtable.
704 # If this is too slow, then we should arrange it so that it is done
705 # before caching. The catch is that at pre-cache time, the above
706 # class-specific fixup hasn't been done.
707 $this->mNamespaceIds
= array();
708 foreach ( $this->getNamespaces() as $index => $name ) {
709 $this->mNamespaceIds
[$this->lc( $name )] = $index;
711 foreach ( $this->getNamespaceAliases() as $name => $index ) {
712 $this->mNamespaceIds
[$this->lc( $name )] = $index;
714 if ( $wgNamespaceAliases ) {
715 foreach ( $wgNamespaceAliases as $name => $index ) {
716 $this->mNamespaceIds
[$this->lc( $name )] = $index;
720 return $this->mNamespaceIds
;
724 * Get a namespace key by value, case insensitive. Canonical namespace
725 * names override custom ones defined for the current language.
727 * @param string $text
728 * @return int|bool An integer if $text is a valid value otherwise false
730 function getNsIndex( $text ) {
731 $lctext = $this->lc( $text );
732 $ns = MWNamespace
::getCanonicalIndex( $lctext );
733 if ( $ns !== null ) {
736 $ids = $this->getNamespaceIds();
737 return isset( $ids[$lctext] ) ?
$ids[$lctext] : false;
741 * short names for language variants used for language conversion links.
743 * @param string $code
744 * @param bool $usemsg Use the "variantname-xyz" message if it exists
747 function getVariantname( $code, $usemsg = true ) {
748 $msg = "variantname-$code";
749 if ( $usemsg && wfMessage( $msg )->exists() ) {
750 return $this->getMessageFromDB( $msg );
752 $name = self
::fetchLanguageName( $code );
754 return $name; # if it's defined as a language name, show that
756 # otherwise, output the language code
762 * @deprecated since 1.24, doesn't handle conflicting aliases. Use
763 * SpecialPageFactory::getLocalNameFor instead.
764 * @param string $name
767 function specialPage( $name ) {
768 $aliases = $this->getSpecialPageAliases();
769 if ( isset( $aliases[$name][0] ) ) {
770 $name = $aliases[$name][0];
772 return $this->getNsText( NS_SPECIAL
) . ':' . $name;
778 function getDatePreferences() {
779 return self
::$dataCache->getItem( $this->mCode
, 'datePreferences' );
785 function getDateFormats() {
786 return self
::$dataCache->getItem( $this->mCode
, 'dateFormats' );
790 * @return array|string
792 function getDefaultDateFormat() {
793 $df = self
::$dataCache->getItem( $this->mCode
, 'defaultDateFormat' );
794 if ( $df === 'dmy or mdy' ) {
795 global $wgAmericanDates;
796 return $wgAmericanDates ?
'mdy' : 'dmy';
805 function getDatePreferenceMigrationMap() {
806 return self
::$dataCache->getItem( $this->mCode
, 'datePreferenceMigrationMap' );
810 * @param string $image
813 function getImageFile( $image ) {
814 return self
::$dataCache->getSubitem( $this->mCode
, 'imageFiles', $image );
821 function getImageFiles() {
822 return self
::$dataCache->getItem( $this->mCode
, 'imageFiles' );
828 function getExtraUserToggles() {
829 return (array)self
::$dataCache->getItem( $this->mCode
, 'extraUserToggles' );
836 function getUserToggle( $tog ) {
837 return $this->getMessageFromDB( "tog-$tog" );
841 * Get native language names, indexed by code.
842 * Only those defined in MediaWiki, no other data like CLDR.
843 * If $customisedOnly is true, only returns codes with a messages file
845 * @param bool $customisedOnly
848 * @deprecated since 1.20, use fetchLanguageNames()
850 public static function getLanguageNames( $customisedOnly = false ) {
851 return self
::fetchLanguageNames( null, $customisedOnly ?
'mwfile' : 'mw' );
855 * Get translated language names. This is done on best effort and
856 * by default this is exactly the same as Language::getLanguageNames.
857 * The CLDR extension provides translated names.
858 * @param string $code Language code.
859 * @return array Language code => language name
861 * @deprecated since 1.20, use fetchLanguageNames()
863 public static function getTranslatedLanguageNames( $code ) {
864 return self
::fetchLanguageNames( $code, 'all' );
868 * Get an array of language names, indexed by code.
869 * @param null|string $inLanguage Code of language in which to return the names
870 * Use null for autonyms (native names)
871 * @param string $include One of:
872 * 'all' all available languages
873 * 'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
874 * 'mwfile' only if the language is in 'mw' *and* has a message file
875 * @return array Language code => language name
878 public static function fetchLanguageNames( $inLanguage = null, $include = 'mw' ) {
879 $cacheKey = $inLanguage === null ?
'null' : $inLanguage;
880 $cacheKey .= ":$include";
881 if ( self
::$languageNameCache === null ) {
882 self
::$languageNameCache = new MapCacheLRU( 20 );
884 if ( self
::$languageNameCache->has( $cacheKey ) ) {
885 $ret = self
::$languageNameCache->get( $cacheKey );
887 $ret = self
::fetchLanguageNamesUncached( $inLanguage, $include );
888 self
::$languageNameCache->set( $cacheKey, $ret );
894 * Uncached helper for fetchLanguageNames
895 * @param null|string $inLanguage Code of language in which to return the names
896 * Use null for autonyms (native names)
897 * @param string $include One of:
898 * 'all' all available languages
899 * 'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
900 * 'mwfile' only if the language is in 'mw' *and* has a message file
901 * @return array Language code => language name
903 private static function fetchLanguageNamesUncached( $inLanguage = null, $include = 'mw' ) {
904 global $wgExtraLanguageNames;
905 static $coreLanguageNames;
907 if ( $coreLanguageNames === null ) {
909 include "$IP/languages/Names.php";
912 // If passed an invalid language code to use, fallback to en
913 if ( $inLanguage !== null && !Language
::isValidCode( $inLanguage ) ) {
920 # TODO: also include when $inLanguage is null, when this code is more efficient
921 Hooks
::run( 'LanguageGetTranslatedLanguageNames', array( &$names, $inLanguage ) );
924 $mwNames = $wgExtraLanguageNames +
$coreLanguageNames;
925 foreach ( $mwNames as $mwCode => $mwName ) {
926 # - Prefer own MediaWiki native name when not using the hook
927 # - For other names just add if not added through the hook
928 if ( $mwCode === $inLanguage ||
!isset( $names[$mwCode] ) ) {
929 $names[$mwCode] = $mwName;
933 if ( $include === 'all' ) {
939 $coreCodes = array_keys( $mwNames );
940 foreach ( $coreCodes as $coreCode ) {
941 $returnMw[$coreCode] = $names[$coreCode];
944 if ( $include === 'mwfile' ) {
945 $namesMwFile = array();
946 # We do this using a foreach over the codes instead of a directory
947 # loop so that messages files in extensions will work correctly.
948 foreach ( $returnMw as $code => $value ) {
949 if ( is_readable( self
::getMessagesFileName( $code ) )
950 ||
is_readable( self
::getJsonMessagesFileName( $code ) )
952 $namesMwFile[$code] = $names[$code];
956 ksort( $namesMwFile );
961 # 'mw' option; default if it's not one of the other two options (all/mwfile)
966 * @param string $code The code of the language for which to get the name
967 * @param null|string $inLanguage Code of language in which to return the name (null for autonyms)
968 * @param string $include 'all', 'mw' or 'mwfile'; see fetchLanguageNames()
969 * @return string Language name or empty
972 public static function fetchLanguageName( $code, $inLanguage = null, $include = 'all' ) {
973 $code = strtolower( $code );
974 $array = self
::fetchLanguageNames( $inLanguage, $include );
975 return !array_key_exists( $code, $array ) ?
'' : $array[$code];
979 * Get a message from the MediaWiki namespace.
981 * @param string $msg Message name
984 function getMessageFromDB( $msg ) {
985 return $this->msg( $msg )->text();
989 * Get message object in this language. Only for use inside this class.
991 * @param string $msg Message name
994 protected function msg( $msg ) {
995 return wfMessage( $msg )->inLanguage( $this );
999 * Get the native language name of $code.
1000 * Only if defined in MediaWiki, no other data like CLDR.
1001 * @param string $code
1003 * @deprecated since 1.20, use fetchLanguageName()
1005 function getLanguageName( $code ) {
1006 return self
::fetchLanguageName( $code );
1010 * @param string $key
1013 function getMonthName( $key ) {
1014 return $this->getMessageFromDB( self
::$mMonthMsgs[$key - 1] );
1020 function getMonthNamesArray() {
1021 $monthNames = array( '' );
1022 for ( $i = 1; $i < 13; $i++
) {
1023 $monthNames[] = $this->getMonthName( $i );
1029 * @param string $key
1032 function getMonthNameGen( $key ) {
1033 return $this->getMessageFromDB( self
::$mMonthGenMsgs[$key - 1] );
1037 * @param string $key
1040 function getMonthAbbreviation( $key ) {
1041 return $this->getMessageFromDB( self
::$mMonthAbbrevMsgs[$key - 1] );
1047 function getMonthAbbreviationsArray() {
1048 $monthNames = array( '' );
1049 for ( $i = 1; $i < 13; $i++
) {
1050 $monthNames[] = $this->getMonthAbbreviation( $i );
1056 * @param string $key
1059 function getWeekdayName( $key ) {
1060 return $this->getMessageFromDB( self
::$mWeekdayMsgs[$key - 1] );
1064 * @param string $key
1067 function getWeekdayAbbreviation( $key ) {
1068 return $this->getMessageFromDB( self
::$mWeekdayAbbrevMsgs[$key - 1] );
1072 * @param string $key
1075 function getIranianCalendarMonthName( $key ) {
1076 return $this->getMessageFromDB( self
::$mIranianCalendarMonthMsgs[$key - 1] );
1080 * @param string $key
1083 function getHebrewCalendarMonthName( $key ) {
1084 return $this->getMessageFromDB( self
::$mHebrewCalendarMonthMsgs[$key - 1] );
1088 * @param string $key
1091 function getHebrewCalendarMonthNameGen( $key ) {
1092 return $this->getMessageFromDB( self
::$mHebrewCalendarMonthGenMsgs[$key - 1] );
1096 * @param string $key
1099 function getHijriCalendarMonthName( $key ) {
1100 return $this->getMessageFromDB( self
::$mHijriCalendarMonthMsgs[$key - 1] );
1104 * Pass through result from $dateTimeObj->format()
1105 * @param DateTime|bool|null &$dateTimeObj
1107 * @param DateTimeZone|bool|null $zone
1108 * @param string $code
1111 private static function dateTimeObjFormat( &$dateTimeObj, $ts, $zone, $code ) {
1112 if ( !$dateTimeObj ) {
1113 $dateTimeObj = DateTime
::createFromFormat(
1114 'YmdHis', $ts, $zone ?
: new DateTimeZone( 'UTC' )
1117 return $dateTimeObj->format( $code );
1121 * This is a workalike of PHP's date() function, but with better
1122 * internationalisation, a reduced set of format characters, and a better
1125 * Supported format characters are dDjlNwzWFmMntLoYyaAgGhHiscrUeIOPTZ. See
1126 * the PHP manual for definitions. There are a number of extensions, which
1129 * xn Do not translate digits of the next numeric format character
1130 * xN Toggle raw digit (xn) flag, stays set until explicitly unset
1131 * xr Use roman numerals for the next numeric format character
1132 * xh Use hebrew numerals for the next numeric format character
1134 * xg Genitive month name
1136 * xij j (day number) in Iranian calendar
1137 * xiF F (month name) in Iranian calendar
1138 * xin n (month number) in Iranian calendar
1139 * xiy y (two digit year) in Iranian calendar
1140 * xiY Y (full year) in Iranian calendar
1142 * xjj j (day number) in Hebrew calendar
1143 * xjF F (month name) in Hebrew calendar
1144 * xjt t (days in month) in Hebrew calendar
1145 * xjx xg (genitive month name) in Hebrew calendar
1146 * xjn n (month number) in Hebrew calendar
1147 * xjY Y (full year) in Hebrew calendar
1149 * xmj j (day number) in Hijri calendar
1150 * xmF F (month name) in Hijri calendar
1151 * xmn n (month number) in Hijri calendar
1152 * xmY Y (full year) in Hijri calendar
1154 * xkY Y (full year) in Thai solar calendar. Months and days are
1155 * identical to the Gregorian calendar
1156 * xoY Y (full year) in Minguo calendar or Juche year.
1157 * Months and days are identical to the
1158 * Gregorian calendar
1159 * xtY Y (full year) in Japanese nengo. Months and days are
1160 * identical to the Gregorian calendar
1162 * Characters enclosed in double quotes will be considered literal (with
1163 * the quotes themselves removed). Unmatched quotes will be considered
1164 * literal quotes. Example:
1166 * "The month is" F => The month is January
1169 * Backslash escaping is also supported.
1171 * Input timestamp is assumed to be pre-normalized to the desired local
1172 * time zone, if any. Note that the format characters crUeIOPTZ will assume
1173 * $ts is UTC if $zone is not given.
1175 * @param string $format
1176 * @param string $ts 14-character timestamp
1179 * @param DateTimeZone $zone Timezone of $ts
1180 * @param[out] int $ttl The amount of time (in seconds) the output may be cached for.
1181 * Only makes sense if $ts is the current time.
1182 * @todo handling of "o" format character for Iranian, Hebrew, Hijri & Thai?
1184 * @throws MWException
1187 function sprintfDate( $format, $ts, DateTimeZone
$zone = null, &$ttl = null ) {
1192 $dateTimeObj = false;
1201 $usedSecond = false;
1202 $usedMinute = false;
1209 $usedISOYear = false;
1210 $usedIsLeapYear = false;
1212 $usedHebrewMonth = false;
1213 $usedIranianMonth = false;
1214 $usedHijriMonth = false;
1215 $usedHebrewYear = false;
1216 $usedIranianYear = false;
1217 $usedHijriYear = false;
1218 $usedTennoYear = false;
1220 if ( strlen( $ts ) !== 14 ) {
1221 throw new MWException( __METHOD__
. ": The timestamp $ts should have 14 characters" );
1224 if ( !ctype_digit( $ts ) ) {
1225 throw new MWException( __METHOD__
. ": The timestamp $ts should be a number" );
1228 $formatLength = strlen( $format );
1229 for ( $p = 0; $p < $formatLength; $p++
) {
1231 $code = $format[$p];
1232 if ( $code == 'x' && $p < $formatLength - 1 ) {
1233 $code .= $format[++
$p];
1236 if ( ( $code === 'xi'
1242 && $p < $formatLength - 1 ) {
1243 $code .= $format[++
$p];
1254 $rawToggle = !$rawToggle;
1264 $s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) );
1267 $usedHebrewMonth = true;
1269 $hebrew = self
::tsToHebrew( $ts );
1271 $s .= $this->getHebrewCalendarMonthNameGen( $hebrew[1] );
1275 $num = substr( $ts, 6, 2 );
1279 $s .= $this->getWeekdayAbbreviation(
1280 Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) +
1
1285 $num = intval( substr( $ts, 6, 2 ) );
1290 $iranian = self
::tsToIranian( $ts );
1297 $hijri = self
::tsToHijri( $ts );
1304 $hebrew = self
::tsToHebrew( $ts );
1310 $s .= $this->getWeekdayName(
1311 Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) +
1
1316 $s .= $this->getMonthName( substr( $ts, 4, 2 ) );
1319 $usedIranianMonth = true;
1321 $iranian = self
::tsToIranian( $ts );
1323 $s .= $this->getIranianCalendarMonthName( $iranian[1] );
1326 $usedHijriMonth = true;
1328 $hijri = self
::tsToHijri( $ts );
1330 $s .= $this->getHijriCalendarMonthName( $hijri[1] );
1333 $usedHebrewMonth = true;
1335 $hebrew = self
::tsToHebrew( $ts );
1337 $s .= $this->getHebrewCalendarMonthName( $hebrew[1] );
1341 $num = substr( $ts, 4, 2 );
1345 $s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) );
1349 $num = intval( substr( $ts, 4, 2 ) );
1352 $usedIranianMonth = true;
1354 $iranian = self
::tsToIranian( $ts );
1359 $usedHijriMonth = true;
1361 $hijri = self
::tsToHijri( $ts );
1366 $usedHebrewMonth = true;
1368 $hebrew = self
::tsToHebrew( $ts );
1373 $usedHebrewMonth = true;
1375 $hebrew = self
::tsToHebrew( $ts );
1381 $num = substr( $ts, 0, 4 );
1384 $usedIranianYear = true;
1386 $iranian = self
::tsToIranian( $ts );
1391 $usedHijriYear = true;
1393 $hijri = self
::tsToHijri( $ts );
1398 $usedHebrewYear = true;
1400 $hebrew = self
::tsToHebrew( $ts );
1407 $thai = self
::tsToYear( $ts, 'thai' );
1414 $minguo = self
::tsToYear( $ts, 'minguo' );
1419 $usedTennoYear = true;
1421 $tenno = self
::tsToYear( $ts, 'tenno' );
1427 $num = substr( $ts, 2, 2 );
1430 $usedIranianYear = true;
1432 $iranian = self
::tsToIranian( $ts );
1434 $num = substr( $iranian[0], -2 );
1438 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ?
'am' : 'pm';
1442 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ?
'AM' : 'PM';
1446 $h = substr( $ts, 8, 2 );
1447 $num = $h %
12 ?
$h %
12 : 12;
1451 $num = intval( substr( $ts, 8, 2 ) );
1455 $h = substr( $ts, 8, 2 );
1456 $num = sprintf( '%02d', $h %
12 ?
$h %
12 : 12 );
1460 $num = substr( $ts, 8, 2 );
1464 $num = substr( $ts, 10, 2 );
1468 $num = substr( $ts, 12, 2 );
1478 $s .= Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1484 $num = Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1488 $num = Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1492 $num = Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1495 $usedIsLeapYear = true;
1496 $num = Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1499 $usedISOYear = true;
1500 $num = Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1507 $num = Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1510 # Backslash escaping
1511 if ( $p < $formatLength - 1 ) {
1512 $s .= $format[++
$p];
1519 if ( $p < $formatLength - 1 ) {
1520 $endQuote = strpos( $format, '"', $p +
1 );
1521 if ( $endQuote === false ) {
1522 # No terminating quote, assume literal "
1525 $s .= substr( $format, $p +
1, $endQuote - $p - 1 );
1529 # Quote at end of string, assume literal "
1536 if ( $num !== false ) {
1537 if ( $rawToggle ||
$raw ) {
1540 } elseif ( $roman ) {
1541 $s .= Language
::romanNumeral( $num );
1543 } elseif ( $hebrewNum ) {
1544 $s .= self
::hebrewNumeral( $num );
1547 $s .= $this->formatNum( $num, true );
1552 if ( $usedSecond ) {
1554 } elseif ( $usedMinute ) {
1555 $ttl = 60 - substr( $ts, 12, 2 );
1556 } elseif ( $usedHour ) {
1557 $ttl = 3600 - substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1558 } elseif ( $usedAMPM ) {
1559 $ttl = 43200 - ( substr( $ts, 8, 2 ) %
12 ) * 3600 -
1560 substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1564 $usedIranianMonth ||
1571 // @todo Someone who understands the non-Gregorian calendars
1572 // should write proper logic for them so that they don't need purged every day.
1573 $ttl = 86400 - substr( $ts, 8, 2 ) * 3600 -
1574 substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1576 $possibleTtls = array();
1577 $timeRemainingInDay = 86400 - substr( $ts, 8, 2 ) * 3600 -
1578 substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1581 ( 7 - Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400 +
1582 $timeRemainingInDay;
1583 } elseif ( $usedISOYear ) {
1584 // December 28th falls on the last ISO week of the year, every year.
1585 // The last ISO week of a year can be 52 or 53.
1586 $lastWeekOfISOYear = DateTime
::createFromFormat(
1588 substr( $ts, 0, 4 ) . '1228',
1589 $zone ?
: new DateTimeZone( 'UTC' )
1591 $currentISOWeek = Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'W' );
1592 $weeksRemaining = $lastWeekOfISOYear - $currentISOWeek;
1593 $timeRemainingInWeek =
1594 ( 7 - Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400
1595 +
$timeRemainingInDay;
1596 $possibleTtls[] = $weeksRemaining * 604800 +
$timeRemainingInWeek;
1601 ( Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 't' ) -
1602 substr( $ts, 6, 2 ) ) * 86400
1603 +
$timeRemainingInDay;
1604 } elseif ( $usedYear ) {
1606 ( Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) +
364 -
1607 Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400
1608 +
$timeRemainingInDay;
1609 } elseif ( $usedIsLeapYear ) {
1610 $year = substr( $ts, 0, 4 );
1611 $timeRemainingInYear =
1612 ( Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) +
364 -
1613 Language
::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400
1614 +
$timeRemainingInDay;
1616 if ( $mod ||
( !( $year %
100 ) && $year %
400 ) ) {
1617 // this isn't a leap year. see when the next one starts
1618 $nextCandidate = $year - $mod +
4;
1619 if ( $nextCandidate %
100 ||
!( $nextCandidate %
400 ) ) {
1620 $possibleTtls[] = ( $nextCandidate - $year - 1 ) * 365 * 86400 +
1621 $timeRemainingInYear;
1623 $possibleTtls[] = ( $nextCandidate - $year +
3 ) * 365 * 86400 +
1624 $timeRemainingInYear;
1627 // this is a leap year, so the next year isn't
1628 $possibleTtls[] = $timeRemainingInYear;
1632 if ( $possibleTtls ) {
1633 $ttl = min( $possibleTtls );
1640 private static $GREG_DAYS = array( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
1641 private static $IRANIAN_DAYS = array( 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 );
1644 * Algorithm by Roozbeh Pournader and Mohammad Toossi to convert
1645 * Gregorian dates to Iranian dates. Originally written in C, it
1646 * is released under the terms of GNU Lesser General Public
1647 * License. Conversion to PHP was performed by Niklas Laxström.
1649 * Link: http://www.farsiweb.info/jalali/jalali.c
1655 private static function tsToIranian( $ts ) {
1656 $gy = substr( $ts, 0, 4 ) -1600;
1657 $gm = substr( $ts, 4, 2 ) -1;
1658 $gd = substr( $ts, 6, 2 ) -1;
1660 # Days passed from the beginning (including leap years)
1662 +
floor( ( $gy +
3 ) / 4 )
1663 - floor( ( $gy +
99 ) / 100 )
1664 +
floor( ( $gy +
399 ) / 400 );
1666 // Add days of the past months of this year
1667 for ( $i = 0; $i < $gm; $i++
) {
1668 $gDayNo +
= self
::$GREG_DAYS[$i];
1672 if ( $gm > 1 && ( ( $gy %
4 === 0 && $gy %
100 !== 0 ||
( $gy %
400 == 0 ) ) ) ) {
1676 // Days passed in current month
1677 $gDayNo +
= (int)$gd;
1679 $jDayNo = $gDayNo - 79;
1681 $jNp = floor( $jDayNo / 12053 );
1684 $jy = 979 +
33 * $jNp +
4 * floor( $jDayNo / 1461 );
1687 if ( $jDayNo >= 366 ) {
1688 $jy +
= floor( ( $jDayNo - 1 ) / 365 );
1689 $jDayNo = floor( ( $jDayNo - 1 ) %
365 );
1692 for ( $i = 0; $i < 11 && $jDayNo >= self
::$IRANIAN_DAYS[$i]; $i++
) {
1693 $jDayNo -= self
::$IRANIAN_DAYS[$i];
1699 return array( $jy, $jm, $jd );
1703 * Converting Gregorian dates to Hijri dates.
1705 * Based on a PHP-Nuke block by Sharjeel which is released under GNU/GPL license
1707 * @see http://phpnuke.org/modules.php?name=News&file=article&sid=8234&mode=thread&order=0&thold=0
1713 private static function tsToHijri( $ts ) {
1714 $year = substr( $ts, 0, 4 );
1715 $month = substr( $ts, 4, 2 );
1716 $day = substr( $ts, 6, 2 );
1724 ( $zy > 1582 ) ||
( ( $zy == 1582 ) && ( $zm > 10 ) ) ||
1725 ( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) )
1727 $zjd = (int)( ( 1461 * ( $zy +
4800 +
(int)( ( $zm - 14 ) / 12 ) ) ) / 4 ) +
1728 (int)( ( 367 * ( $zm - 2 - 12 * ( (int)( ( $zm - 14 ) / 12 ) ) ) ) / 12 ) -
1729 (int)( ( 3 * (int)( ( ( $zy +
4900 +
(int)( ( $zm - 14 ) / 12 ) ) / 100 ) ) ) / 4 ) +
1732 $zjd = 367 * $zy - (int)( ( 7 * ( $zy +
5001 +
(int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) +
1733 (int)( ( 275 * $zm ) / 9 ) +
$zd +
1729777;
1736 $zl = $zjd -1948440 +
10632;
1737 $zn = (int)( ( $zl - 1 ) / 10631 );
1738 $zl = $zl - 10631 * $zn +
354;
1739 $zj = ( (int)( ( 10985 - $zl ) / 5316 ) ) * ( (int)( ( 50 * $zl ) / 17719 ) ) +
1740 ( (int)( $zl / 5670 ) ) * ( (int)( ( 43 * $zl ) / 15238 ) );
1741 $zl = $zl - ( (int)( ( 30 - $zj ) / 15 ) ) * ( (int)( ( 17719 * $zj ) / 50 ) ) -
1742 ( (int)( $zj / 16 ) ) * ( (int)( ( 15238 * $zj ) / 43 ) ) +
29;
1743 $zm = (int)( ( 24 * $zl ) / 709 );
1744 $zd = $zl - (int)( ( 709 * $zm ) / 24 );
1745 $zy = 30 * $zn +
$zj - 30;
1747 return array( $zy, $zm, $zd );
1751 * Converting Gregorian dates to Hebrew dates.
1753 * Based on a JavaScript code by Abu Mami and Yisrael Hersch
1754 * (abu-mami@kaluach.net, http://www.kaluach.net), who permitted
1755 * to translate the relevant functions into PHP and release them under
1758 * The months are counted from Tishrei = 1. In a leap year, Adar I is 13
1759 * and Adar II is 14. In a non-leap year, Adar is 6.
1765 private static function tsToHebrew( $ts ) {
1767 $year = substr( $ts, 0, 4 );
1768 $month = substr( $ts, 4, 2 );
1769 $day = substr( $ts, 6, 2 );
1771 # Calculate Hebrew year
1772 $hebrewYear = $year +
3760;
1774 # Month number when September = 1, August = 12
1776 if ( $month > 12 ) {
1783 # Calculate day of year from 1 September
1785 for ( $i = 1; $i < $month; $i++
) {
1789 # Check if the year is leap
1790 if ( $year %
400 == 0 ||
( $year %
4 == 0 && $year %
100 > 0 ) ) {
1793 } elseif ( $i == 8 ||
$i == 10 ||
$i == 1 ||
$i == 3 ) {
1800 # Calculate the start of the Hebrew year
1801 $start = self
::hebrewYearStart( $hebrewYear );
1803 # Calculate next year's start
1804 if ( $dayOfYear <= $start ) {
1805 # Day is before the start of the year - it is the previous year
1807 $nextStart = $start;
1811 # Add days since previous year's 1 September
1813 if ( ( $year %
400 == 0 ) ||
( $year %
100 != 0 && $year %
4 == 0 ) ) {
1817 # Start of the new (previous) year
1818 $start = self
::hebrewYearStart( $hebrewYear );
1821 $nextStart = self
::hebrewYearStart( $hebrewYear +
1 );
1824 # Calculate Hebrew day of year
1825 $hebrewDayOfYear = $dayOfYear - $start;
1827 # Difference between year's days
1828 $diff = $nextStart - $start;
1829 # Add 12 (or 13 for leap years) days to ignore the difference between
1830 # Hebrew and Gregorian year (353 at least vs. 365/6) - now the
1831 # difference is only about the year type
1832 if ( ( $year %
400 == 0 ) ||
( $year %
100 != 0 && $year %
4 == 0 ) ) {
1838 # Check the year pattern, and is leap year
1839 # 0 means an incomplete year, 1 means a regular year, 2 means a complete year
1840 # This is mod 30, to work on both leap years (which add 30 days of Adar I)
1841 # and non-leap years
1842 $yearPattern = $diff %
30;
1843 # Check if leap year
1844 $isLeap = $diff >= 30;
1846 # Calculate day in the month from number of day in the Hebrew year
1847 # Don't check Adar - if the day is not in Adar, we will stop before;
1848 # if it is in Adar, we will use it to check if it is Adar I or Adar II
1849 $hebrewDay = $hebrewDayOfYear;
1852 while ( $hebrewMonth <= 12 ) {
1853 # Calculate days in this month
1854 if ( $isLeap && $hebrewMonth == 6 ) {
1855 # Adar in a leap year
1857 # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1859 if ( $hebrewDay <= $days ) {
1863 # Subtract the days of Adar I
1864 $hebrewDay -= $days;
1867 if ( $hebrewDay <= $days ) {
1873 } elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
1874 # Cheshvan in a complete year (otherwise as the rule below)
1876 } elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) {
1877 # Kislev in an incomplete year (otherwise as the rule below)
1880 # Odd months have 30 days, even have 29
1881 $days = 30 - ( $hebrewMonth - 1 ) %
2;
1883 if ( $hebrewDay <= $days ) {
1884 # In the current month
1887 # Subtract the days of the current month
1888 $hebrewDay -= $days;
1889 # Try in the next month
1894 return array( $hebrewYear, $hebrewMonth, $hebrewDay, $days );
1898 * This calculates the Hebrew year start, as days since 1 September.
1899 * Based on Carl Friedrich Gauss algorithm for finding Easter date.
1900 * Used for Hebrew date.
1906 private static function hebrewYearStart( $year ) {
1907 $a = intval( ( 12 * ( $year - 1 ) +
17 ) %
19 );
1908 $b = intval( ( $year - 1 ) %
4 );
1909 $m = 32.044093161144 +
1.5542417966212 * $a +
$b / 4.0 - 0.0031777940220923 * ( $year - 1 );
1913 $Mar = intval( $m );
1919 $c = intval( ( $Mar +
3 * ( $year - 1 ) +
5 * $b +
5 ) %
7 );
1920 if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1922 } elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1924 } elseif ( $c == 2 ||
$c == 4 ||
$c == 6 ) {
1928 $Mar +
= intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1933 * Algorithm to convert Gregorian dates to Thai solar dates,
1934 * Minguo dates or Minguo dates.
1936 * Link: http://en.wikipedia.org/wiki/Thai_solar_calendar
1937 * http://en.wikipedia.org/wiki/Minguo_calendar
1938 * http://en.wikipedia.org/wiki/Japanese_era_name
1940 * @param string $ts 14-character timestamp
1941 * @param string $cName Calender name
1942 * @return array Converted year, month, day
1944 private static function tsToYear( $ts, $cName ) {
1945 $gy = substr( $ts, 0, 4 );
1946 $gm = substr( $ts, 4, 2 );
1947 $gd = substr( $ts, 6, 2 );
1949 if ( !strcmp( $cName, 'thai' ) ) {
1951 # Add 543 years to the Gregorian calendar
1952 # Months and days are identical
1953 $gy_offset = $gy +
543;
1954 } elseif ( ( !strcmp( $cName, 'minguo' ) ) ||
!strcmp( $cName, 'juche' ) ) {
1956 # Deduct 1911 years from the Gregorian calendar
1957 # Months and days are identical
1958 $gy_offset = $gy - 1911;
1959 } elseif ( !strcmp( $cName, 'tenno' ) ) {
1960 # Nengō dates up to Meiji period
1961 # Deduct years from the Gregorian calendar
1962 # depending on the nengo periods
1963 # Months and days are identical
1965 ||
( ( $gy == 1912 ) && ( $gm < 7 ) )
1966 ||
( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd < 31 ) )
1969 $gy_gannen = $gy - 1868 +
1;
1970 $gy_offset = $gy_gannen;
1971 if ( $gy_gannen == 1 ) {
1974 $gy_offset = '明治' . $gy_offset;
1976 ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd == 31 ) ) ||
1977 ( ( $gy == 1912 ) && ( $gm >= 8 ) ) ||
1978 ( ( $gy > 1912 ) && ( $gy < 1926 ) ) ||
1979 ( ( $gy == 1926 ) && ( $gm < 12 ) ) ||
1980 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd < 26 ) )
1983 $gy_gannen = $gy - 1912 +
1;
1984 $gy_offset = $gy_gannen;
1985 if ( $gy_gannen == 1 ) {
1988 $gy_offset = '大正' . $gy_offset;
1990 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd >= 26 ) ) ||
1991 ( ( $gy > 1926 ) && ( $gy < 1989 ) ) ||
1992 ( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd < 8 ) )
1995 $gy_gannen = $gy - 1926 +
1;
1996 $gy_offset = $gy_gannen;
1997 if ( $gy_gannen == 1 ) {
2000 $gy_offset = '昭和' . $gy_offset;
2003 $gy_gannen = $gy - 1989 +
1;
2004 $gy_offset = $gy_gannen;
2005 if ( $gy_gannen == 1 ) {
2008 $gy_offset = '平成' . $gy_offset;
2014 return array( $gy_offset, $gm, $gd );
2018 * Gets directionality of the first strongly directional codepoint, for embedBidi()
2020 * This is the rule the BIDI algorithm uses to determine the directionality of
2021 * paragraphs ( http://unicode.org/reports/tr9/#The_Paragraph_Level ) and
2022 * FSI isolates ( http://unicode.org/reports/tr9/#Explicit_Directional_Isolates ).
2024 * TODO: Does not handle BIDI control characters inside the text.
2025 * TODO: Does not handle unallocated characters.
2027 * @param string $text Text to test
2028 * @return null|string Directionality ('ltr' or 'rtl') or null
2030 private static function strongDirFromContent( $text = '' ) {
2031 if ( !preg_match( self
::$strongDirRegex, $text, $matches ) ) {
2034 if ( $matches[1] === '' ) {
2041 * Roman number formatting up to 10000
2047 static function romanNumeral( $num ) {
2048 static $table = array(
2049 array( '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ),
2050 array( '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ),
2051 array( '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ),
2052 array( '', 'M', 'MM', 'MMM', 'MMMM', 'MMMMM', 'MMMMMM', 'MMMMMMM',
2053 'MMMMMMMM', 'MMMMMMMMM', 'MMMMMMMMMM' )
2056 $num = intval( $num );
2057 if ( $num > 10000 ||
$num <= 0 ) {
2062 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
2063 if ( $num >= $pow10 ) {
2064 $s .= $table[$i][(int)floor( $num / $pow10 )];
2066 $num = $num %
$pow10;
2072 * Hebrew Gematria number formatting up to 9999
2078 static function hebrewNumeral( $num ) {
2079 static $table = array(
2080 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ),
2081 array( '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ),
2091 array( 'ת', 'ת', 'ק' ),
2092 array( 'ת', 'ת', 'ר' ),
2094 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' )
2097 $num = intval( $num );
2098 if ( $num > 9999 ||
$num <= 0 ) {
2102 // Round thousands have special notations
2103 if ( $num === 1000 ) {
2105 } elseif ( $num %
1000 === 0 ) {
2106 return $table[0][$num / 1000] . "' אלפים";
2111 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
2112 if ( $num >= $pow10 ) {
2113 if ( $num === 15 ||
$num === 16 ) {
2114 $letters[] = $table[0][9];
2115 $letters[] = $table[0][$num - 9];
2118 $letters = array_merge(
2120 (array)$table[$i][intval( $num / $pow10 )]
2123 if ( $pow10 === 1000 ) {
2129 $num = $num %
$pow10;
2132 $preTransformLength = count( $letters );
2133 if ( $preTransformLength === 1 ) {
2134 // Add geresh (single quote) to one-letter numbers
2137 $lastIndex = $preTransformLength - 1;
2138 $letters[$lastIndex] = str_replace(
2139 array( 'כ', 'מ', 'נ', 'פ', 'צ' ),
2140 array( 'ך', 'ם', 'ן', 'ף', 'ץ' ),
2141 $letters[$lastIndex]
2144 // Add gershayim (double quote) to multiple-letter numbers,
2145 // but exclude numbers with only one letter after the thousands
2146 // (1001-1009, 1020, 1030, 2001-2009, etc.)
2147 if ( $letters[1] === "'" && $preTransformLength === 3 ) {
2150 array_splice( $letters, -1, 0, '"' );
2154 return implode( $letters );
2158 * Used by date() and time() to adjust the time output.
2160 * @param string $ts The time in date('YmdHis') format
2161 * @param mixed $tz Adjust the time by this amount (default false, mean we
2162 * get user timecorrection setting)
2165 function userAdjust( $ts, $tz = false ) {
2166 global $wgUser, $wgLocalTZoffset;
2168 if ( $tz === false ) {
2169 $tz = $wgUser->getOption( 'timecorrection' );
2172 $data = explode( '|', $tz, 3 );
2174 if ( $data[0] == 'ZoneInfo' ) {
2175 MediaWiki\
suppressWarnings();
2176 $userTZ = timezone_open( $data[2] );
2177 MediaWiki\restoreWarnings
();
2178 if ( $userTZ !== false ) {
2179 $date = date_create( $ts, timezone_open( 'UTC' ) );
2180 date_timezone_set( $date, $userTZ );
2181 $date = date_format( $date, 'YmdHis' );
2184 # Unrecognized timezone, default to 'Offset' with the stored offset.
2185 $data[0] = 'Offset';
2188 if ( $data[0] == 'System' ||
$tz == '' ) {
2189 # Global offset in minutes.
2190 $minDiff = $wgLocalTZoffset;
2191 } elseif ( $data[0] == 'Offset' ) {
2192 $minDiff = intval( $data[1] );
2194 $data = explode( ':', $tz );
2195 if ( count( $data ) == 2 ) {
2196 $data[0] = intval( $data[0] );
2197 $data[1] = intval( $data[1] );
2198 $minDiff = abs( $data[0] ) * 60 +
$data[1];
2199 if ( $data[0] < 0 ) {
2200 $minDiff = -$minDiff;
2203 $minDiff = intval( $data[0] ) * 60;
2207 # No difference ? Return time unchanged
2208 if ( 0 == $minDiff ) {
2212 MediaWiki\
suppressWarnings(); // E_STRICT system time bitching
2213 # Generate an adjusted date; take advantage of the fact that mktime
2214 # will normalize out-of-range values so we don't have to split $minDiff
2215 # into hours and minutes.
2217 (int)substr( $ts, 8, 2 ) ), # Hours
2218 (int)substr( $ts, 10, 2 ) +
$minDiff, # Minutes
2219 (int)substr( $ts, 12, 2 ), # Seconds
2220 (int)substr( $ts, 4, 2 ), # Month
2221 (int)substr( $ts, 6, 2 ), # Day
2222 (int)substr( $ts, 0, 4 ) ); # Year
2224 $date = date( 'YmdHis', $t );
2225 MediaWiki\restoreWarnings
();
2231 * This is meant to be used by time(), date(), and timeanddate() to get
2232 * the date preference they're supposed to use, it should be used in
2236 * function timeanddate([...], $format = true) {
2237 * $datePreference = $this->dateFormat($format);
2242 * @param int|string|bool $usePrefs If true, the user's preference is used
2243 * if false, the site/language default is used
2244 * if int/string, assumed to be a format.
2247 function dateFormat( $usePrefs = true ) {
2250 if ( is_bool( $usePrefs ) ) {
2252 $datePreference = $wgUser->getDatePreference();
2254 $datePreference = (string)User
::getDefaultOption( 'date' );
2257 $datePreference = (string)$usePrefs;
2261 if ( $datePreference == '' ) {
2265 return $datePreference;
2269 * Get a format string for a given type and preference
2270 * @param string $type May be date, time or both
2271 * @param string $pref The format name as it appears in Messages*.php
2273 * @since 1.22 New type 'pretty' that provides a more readable timestamp format
2277 function getDateFormatString( $type, $pref ) {
2278 if ( !isset( $this->dateFormatStrings
[$type][$pref] ) ) {
2279 if ( $pref == 'default' ) {
2280 $pref = $this->getDefaultDateFormat();
2281 $df = self
::$dataCache->getSubitem( $this->mCode
, 'dateFormats', "$pref $type" );
2283 $df = self
::$dataCache->getSubitem( $this->mCode
, 'dateFormats', "$pref $type" );
2285 if ( $type === 'pretty' && $df === null ) {
2286 $df = $this->getDateFormatString( 'date', $pref );
2289 if ( $df === null ) {
2290 $pref = $this->getDefaultDateFormat();
2291 $df = self
::$dataCache->getSubitem( $this->mCode
, 'dateFormats', "$pref $type" );
2294 $this->dateFormatStrings
[$type][$pref] = $df;
2296 return $this->dateFormatStrings
[$type][$pref];
2300 * @param string $ts The time format which needs to be turned into a
2301 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2302 * @param bool $adj Whether to adjust the time output according to the
2303 * user configured offset ($timecorrection)
2304 * @param mixed $format True to use user's date format preference
2305 * @param string|bool $timecorrection The time offset as returned by
2306 * validateTimeZone() in Special:Preferences
2309 function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
2310 $ts = wfTimestamp( TS_MW
, $ts );
2312 $ts = $this->userAdjust( $ts, $timecorrection );
2314 $df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
2315 return $this->sprintfDate( $df, $ts );
2319 * @param string $ts The time format which needs to be turned into a
2320 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2321 * @param bool $adj Whether to adjust the time output according to the
2322 * user configured offset ($timecorrection)
2323 * @param mixed $format True to use user's date format preference
2324 * @param string|bool $timecorrection The time offset as returned by
2325 * validateTimeZone() in Special:Preferences
2328 function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
2329 $ts = wfTimestamp( TS_MW
, $ts );
2331 $ts = $this->userAdjust( $ts, $timecorrection );
2333 $df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
2334 return $this->sprintfDate( $df, $ts );
2338 * @param string $ts The time format which needs to be turned into a
2339 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2340 * @param bool $adj Whether to adjust the time output according to the
2341 * user configured offset ($timecorrection)
2342 * @param mixed $format What format to return, if it's false output the
2343 * default one (default true)
2344 * @param string|bool $timecorrection The time offset as returned by
2345 * validateTimeZone() in Special:Preferences
2348 function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false ) {
2349 $ts = wfTimestamp( TS_MW
, $ts );
2351 $ts = $this->userAdjust( $ts, $timecorrection );
2353 $df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) );
2354 return $this->sprintfDate( $df, $ts );
2358 * Takes a number of seconds and turns it into a text using values such as hours and minutes.
2362 * @param int $seconds The amount of seconds.
2363 * @param array $chosenIntervals The intervals to enable.
2367 public function formatDuration( $seconds, array $chosenIntervals = array() ) {
2368 $intervals = $this->getDurationIntervals( $seconds, $chosenIntervals );
2370 $segments = array();
2372 foreach ( $intervals as $intervalName => $intervalValue ) {
2373 // Messages: duration-seconds, duration-minutes, duration-hours, duration-days, duration-weeks,
2374 // duration-years, duration-decades, duration-centuries, duration-millennia
2375 $message = wfMessage( 'duration-' . $intervalName )->numParams( $intervalValue );
2376 $segments[] = $message->inLanguage( $this )->escaped();
2379 return $this->listToText( $segments );
2383 * Takes a number of seconds and returns an array with a set of corresponding intervals.
2384 * For example 65 will be turned into array( minutes => 1, seconds => 5 ).
2388 * @param int $seconds The amount of seconds.
2389 * @param array $chosenIntervals The intervals to enable.
2393 public function getDurationIntervals( $seconds, array $chosenIntervals = array() ) {
2394 if ( empty( $chosenIntervals ) ) {
2395 $chosenIntervals = array(
2407 $intervals = array_intersect_key( self
::$durationIntervals, array_flip( $chosenIntervals ) );
2408 $sortedNames = array_keys( $intervals );
2409 $smallestInterval = array_pop( $sortedNames );
2411 $segments = array();
2413 foreach ( $intervals as $name => $length ) {
2414 $value = floor( $seconds / $length );
2416 if ( $value > 0 ||
( $name == $smallestInterval && empty( $segments ) ) ) {
2417 $seconds -= $value * $length;
2418 $segments[$name] = $value;
2426 * Internal helper function for userDate(), userTime() and userTimeAndDate()
2428 * @param string $type Can be 'date', 'time' or 'both'
2429 * @param string $ts The time format which needs to be turned into a
2430 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2431 * @param User $user User object used to get preferences for timezone and format
2432 * @param array $options Array, can contain the following keys:
2433 * - 'timecorrection': time correction, can have the following values:
2434 * - true: use user's preference
2435 * - false: don't use time correction
2436 * - int: value of time correction in minutes
2437 * - 'format': format to use, can have the following values:
2438 * - true: use user's preference
2439 * - false: use default preference
2440 * - string: format to use
2444 private function internalUserTimeAndDate( $type, $ts, User
$user, array $options ) {
2445 $ts = wfTimestamp( TS_MW
, $ts );
2446 $options +
= array( 'timecorrection' => true, 'format' => true );
2447 if ( $options['timecorrection'] !== false ) {
2448 if ( $options['timecorrection'] === true ) {
2449 $offset = $user->getOption( 'timecorrection' );
2451 $offset = $options['timecorrection'];
2453 $ts = $this->userAdjust( $ts, $offset );
2455 if ( $options['format'] === true ) {
2456 $format = $user->getDatePreference();
2458 $format = $options['format'];
2460 $df = $this->getDateFormatString( $type, $this->dateFormat( $format ) );
2461 return $this->sprintfDate( $df, $ts );
2465 * Get the formatted date for the given timestamp and formatted for
2468 * @param mixed $ts Mixed: the time format which needs to be turned into a
2469 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2470 * @param User $user User object used to get preferences for timezone and format
2471 * @param array $options Array, can contain the following keys:
2472 * - 'timecorrection': time correction, can have the following values:
2473 * - true: use user's preference
2474 * - false: don't use time correction
2475 * - int: value of time correction in minutes
2476 * - 'format': format to use, can have the following values:
2477 * - true: use user's preference
2478 * - false: use default preference
2479 * - string: format to use
2483 public function userDate( $ts, User
$user, array $options = array() ) {
2484 return $this->internalUserTimeAndDate( 'date', $ts, $user, $options );
2488 * Get the formatted time for the given timestamp and formatted for
2491 * @param mixed $ts The time format which needs to be turned into a
2492 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2493 * @param User $user User object used to get preferences for timezone and format
2494 * @param array $options Array, can contain the following keys:
2495 * - 'timecorrection': time correction, can have the following values:
2496 * - true: use user's preference
2497 * - false: don't use time correction
2498 * - int: value of time correction in minutes
2499 * - 'format': format to use, can have the following values:
2500 * - true: use user's preference
2501 * - false: use default preference
2502 * - string: format to use
2506 public function userTime( $ts, User
$user, array $options = array() ) {
2507 return $this->internalUserTimeAndDate( 'time', $ts, $user, $options );
2511 * Get the formatted date and time for the given timestamp and formatted for
2514 * @param mixed $ts The time format which needs to be turned into a
2515 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2516 * @param User $user User object used to get preferences for timezone and format
2517 * @param array $options Array, can contain the following keys:
2518 * - 'timecorrection': time correction, can have the following values:
2519 * - true: use user's preference
2520 * - false: don't use time correction
2521 * - int: value of time correction in minutes
2522 * - 'format': format to use, can have the following values:
2523 * - true: use user's preference
2524 * - false: use default preference
2525 * - string: format to use
2529 public function userTimeAndDate( $ts, User
$user, array $options = array() ) {
2530 return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
2534 * Get the timestamp in a human-friendly relative format, e.g., "3 days ago".
2536 * Determine the difference between the timestamp and the current time, and
2537 * generate a readable timestamp by returning "<N> <units> ago", where the
2538 * largest possible unit is used.
2540 * @since 1.26 (Prior to 1.26 method existed but was not meant to be used directly)
2542 * @param MWTimestamp $time
2543 * @param MWTimestamp|null $relativeTo The base timestamp to compare to (defaults to now)
2544 * @param User|null $user User the timestamp is being generated for (or null to use main context's user)
2545 * @return string Formatted timestamp
2547 public function getHumanTimestamp( MWTimestamp
$time, MWTimestamp
$relativeTo = null, User
$user = null ) {
2548 if ( $relativeTo === null ) {
2549 $relativeTo = new MWTimestamp();
2551 if ( $user === null ) {
2552 $user = RequestContext
::getMain()->getUser();
2555 // Adjust for the user's timezone.
2556 $offsetThis = $time->offsetForUser( $user );
2557 $offsetRel = $relativeTo->offsetForUser( $user );
2560 if ( Hooks
::run( 'GetHumanTimestamp', array( &$ts, $time, $relativeTo, $user, $this ) ) ) {
2561 $ts = $this->getHumanTimestampInternal( $time, $relativeTo, $user );
2564 // Reset the timezone on the objects.
2565 $time->timestamp
->sub( $offsetThis );
2566 $relativeTo->timestamp
->sub( $offsetRel );
2572 * Convert an MWTimestamp into a pretty human-readable timestamp using
2573 * the given user preferences and relative base time.
2575 * @see Language::getHumanTimestamp
2576 * @param MWTimestamp $ts Timestamp to prettify
2577 * @param MWTimestamp $relativeTo Base timestamp
2578 * @param User $user User preferences to use
2579 * @return string Human timestamp
2582 private function getHumanTimestampInternal( MWTimestamp
$ts, MWTimestamp
$relativeTo, User
$user ) {
2583 $diff = $ts->diff( $relativeTo );
2584 $diffDay = (bool)( (int)$ts->timestamp
->format( 'w' ) -
2585 (int)$relativeTo->timestamp
->format( 'w' ) );
2586 $days = $diff->days ?
: (int)$diffDay;
2587 if ( $diff->invert ||
$days > 5
2588 && $ts->timestamp
->format( 'Y' ) !== $relativeTo->timestamp
->format( 'Y' )
2590 // Timestamps are in different years: use full timestamp
2591 // Also do full timestamp for future dates
2593 * @todo FIXME: Add better handling of future timestamps.
2595 $format = $this->getDateFormatString( 'both', $user->getDatePreference() ?
: 'default' );
2596 $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW
) );
2597 } elseif ( $days > 5 ) {
2598 // Timestamps are in same year, but more than 5 days ago: show day and month only.
2599 $format = $this->getDateFormatString( 'pretty', $user->getDatePreference() ?
: 'default' );
2600 $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW
) );
2601 } elseif ( $days > 1 ) {
2602 // Timestamp within the past week: show the day of the week and time
2603 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?
: 'default' );
2604 $weekday = self
::$mWeekdayMsgs[$ts->timestamp
->format( 'w' )];
2606 // sunday-at, monday-at, tuesday-at, wednesday-at, thursday-at, friday-at, saturday-at
2607 $ts = wfMessage( "$weekday-at" )
2608 ->inLanguage( $this )
2609 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW
) ) )
2611 } elseif ( $days == 1 ) {
2612 // Timestamp was yesterday: say 'yesterday' and the time.
2613 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?
: 'default' );
2614 $ts = wfMessage( 'yesterday-at' )
2615 ->inLanguage( $this )
2616 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW
) ) )
2618 } elseif ( $diff->h
> 1 ||
$diff->h
== 1 && $diff->i
> 30 ) {
2619 // Timestamp was today, but more than 90 minutes ago: say 'today' and the time.
2620 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?
: 'default' );
2621 $ts = wfMessage( 'today-at' )
2622 ->inLanguage( $this )
2623 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW
) ) )
2626 // From here on in, the timestamp was soon enough ago so that we can simply say
2627 // XX units ago, e.g., "2 hours ago" or "5 minutes ago"
2628 } elseif ( $diff->h
== 1 ) {
2629 // Less than 90 minutes, but more than an hour ago.
2630 $ts = wfMessage( 'hours-ago' )->inLanguage( $this )->numParams( 1 )->text();
2631 } elseif ( $diff->i
>= 1 ) {
2632 // A few minutes ago.
2633 $ts = wfMessage( 'minutes-ago' )->inLanguage( $this )->numParams( $diff->i
)->text();
2634 } elseif ( $diff->s
>= 30 ) {
2635 // Less than a minute, but more than 30 sec ago.
2636 $ts = wfMessage( 'seconds-ago' )->inLanguage( $this )->numParams( $diff->s
)->text();
2638 // Less than 30 seconds ago.
2639 $ts = wfMessage( 'just-now' )->text();
2646 * @param string $key
2647 * @return array|null
2649 function getMessage( $key ) {
2650 return self
::$dataCache->getSubitem( $this->mCode
, 'messages', $key );
2656 function getAllMessages() {
2657 return self
::$dataCache->getItem( $this->mCode
, 'messages' );
2662 * @param string $out
2663 * @param string $string
2666 function iconv( $in, $out, $string ) {
2667 # This is a wrapper for iconv in all languages except esperanto,
2668 # which does some nasty x-conversions beforehand
2670 # Even with //IGNORE iconv can whine about illegal characters in
2671 # *input* string. We just ignore those too.
2672 # REF: http://bugs.php.net/bug.php?id=37166
2673 # REF: https://bugzilla.wikimedia.org/show_bug.cgi?id=16885
2674 MediaWiki\
suppressWarnings();
2675 $text = iconv( $in, $out . '//IGNORE', $string );
2676 MediaWiki\restoreWarnings
();
2680 // callback functions for uc(), lc(), ucwords(), ucwordbreaks()
2683 * @param array $matches
2684 * @return mixed|string
2686 function ucwordbreaksCallbackAscii( $matches ) {
2687 return $this->ucfirst( $matches[1] );
2691 * @param array $matches
2694 function ucwordbreaksCallbackMB( $matches ) {
2695 return mb_strtoupper( $matches[0] );
2699 * @param array $matches
2702 function ucCallback( $matches ) {
2703 list( $wikiUpperChars ) = self
::getCaseMaps();
2704 return strtr( $matches[1], $wikiUpperChars );
2708 * @param array $matches
2711 function lcCallback( $matches ) {
2712 list( , $wikiLowerChars ) = self
::getCaseMaps();
2713 return strtr( $matches[1], $wikiLowerChars );
2717 * @param array $matches
2720 function ucwordsCallbackMB( $matches ) {
2721 return mb_strtoupper( $matches[0] );
2725 * @param array $matches
2728 function ucwordsCallbackWiki( $matches ) {
2729 list( $wikiUpperChars ) = self
::getCaseMaps();
2730 return strtr( $matches[0], $wikiUpperChars );
2734 * Make a string's first character uppercase
2736 * @param string $str
2740 function ucfirst( $str ) {
2742 if ( $o < 96 ) { // if already uppercase...
2744 } elseif ( $o < 128 ) {
2745 return ucfirst( $str ); // use PHP's ucfirst()
2747 // fall back to more complex logic in case of multibyte strings
2748 return $this->uc( $str, true );
2753 * Convert a string to uppercase
2755 * @param string $str
2756 * @param bool $first
2760 function uc( $str, $first = false ) {
2761 if ( function_exists( 'mb_strtoupper' ) ) {
2763 if ( $this->isMultibyte( $str ) ) {
2764 return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2766 return ucfirst( $str );
2769 return $this->isMultibyte( $str ) ?
mb_strtoupper( $str ) : strtoupper( $str );
2772 if ( $this->isMultibyte( $str ) ) {
2773 $x = $first ?
'^' : '';
2774 return preg_replace_callback(
2775 "/$x([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
2776 array( $this, 'ucCallback' ),
2780 return $first ?
ucfirst( $str ) : strtoupper( $str );
2786 * @param string $str
2787 * @return mixed|string
2789 function lcfirst( $str ) {
2792 return strval( $str );
2793 } elseif ( $o >= 128 ) {
2794 return $this->lc( $str, true );
2795 } elseif ( $o > 96 ) {
2798 $str[0] = strtolower( $str[0] );
2804 * @param string $str
2805 * @param bool $first
2806 * @return mixed|string
2808 function lc( $str, $first = false ) {
2809 if ( function_exists( 'mb_strtolower' ) ) {
2811 if ( $this->isMultibyte( $str ) ) {
2812 return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2814 return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
2817 return $this->isMultibyte( $str ) ?
mb_strtolower( $str ) : strtolower( $str );
2820 if ( $this->isMultibyte( $str ) ) {
2821 $x = $first ?
'^' : '';
2822 return preg_replace_callback(
2823 "/$x([A-Z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
2824 array( $this, 'lcCallback' ),
2828 return $first ?
strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 ) : strtolower( $str );
2834 * @param string $str
2837 function isMultibyte( $str ) {
2838 return (bool)preg_match( '/[\x80-\xff]/', $str );
2842 * @param string $str
2843 * @return mixed|string
2845 function ucwords( $str ) {
2846 if ( $this->isMultibyte( $str ) ) {
2847 $str = $this->lc( $str );
2849 // regexp to find first letter in each word (i.e. after each space)
2850 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2852 // function to use to capitalize a single char
2853 if ( function_exists( 'mb_strtoupper' ) ) {
2854 return preg_replace_callback(
2856 array( $this, 'ucwordsCallbackMB' ),
2860 return preg_replace_callback(
2862 array( $this, 'ucwordsCallbackWiki' ),
2867 return ucwords( strtolower( $str ) );
2872 * capitalize words at word breaks
2874 * @param string $str
2877 function ucwordbreaks( $str ) {
2878 if ( $this->isMultibyte( $str ) ) {
2879 $str = $this->lc( $str );
2881 // since \b doesn't work for UTF-8, we explicitely define word break chars
2882 $breaks = "[ \-\(\)\}\{\.,\?!]";
2884 // find first letter after word break
2885 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|" .
2886 "$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2888 if ( function_exists( 'mb_strtoupper' ) ) {
2889 return preg_replace_callback(
2891 array( $this, 'ucwordbreaksCallbackMB' ),
2895 return preg_replace_callback(
2897 array( $this, 'ucwordsCallbackWiki' ),
2902 return preg_replace_callback(
2903 '/\b([\w\x80-\xff]+)\b/',
2904 array( $this, 'ucwordbreaksCallbackAscii' ),
2911 * Return a case-folded representation of $s
2913 * This is a representation such that caseFold($s1)==caseFold($s2) if $s1
2914 * and $s2 are the same except for the case of their characters. It is not
2915 * necessary for the value returned to make sense when displayed.
2917 * Do *not* perform any other normalisation in this function. If a caller
2918 * uses this function when it should be using a more general normalisation
2919 * function, then fix the caller.
2925 function caseFold( $s ) {
2926 return $this->uc( $s );
2933 function checkTitleEncoding( $s ) {
2934 if ( is_array( $s ) ) {
2935 throw new MWException( 'Given array to checkTitleEncoding.' );
2937 if ( StringUtils
::isUtf8( $s ) ) {
2941 return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s );
2947 function fallback8bitEncoding() {
2948 return self
::$dataCache->getItem( $this->mCode
, 'fallback8bitEncoding' );
2952 * Most writing systems use whitespace to break up words.
2953 * Some languages such as Chinese don't conventionally do this,
2954 * which requires special handling when breaking up words for
2959 function hasWordBreaks() {
2964 * Some languages such as Chinese require word segmentation,
2965 * Specify such segmentation when overridden in derived class.
2967 * @param string $string
2970 function segmentByWord( $string ) {
2975 * Some languages have special punctuation need to be normalized.
2976 * Make such changes here.
2978 * @param string $string
2981 function normalizeForSearch( $string ) {
2982 return self
::convertDoubleWidth( $string );
2986 * convert double-width roman characters to single-width.
2987 * range: ff00-ff5f ~= 0020-007f
2989 * @param string $string
2993 protected static function convertDoubleWidth( $string ) {
2994 static $full = null;
2995 static $half = null;
2997 if ( $full === null ) {
2998 $fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2999 $halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
3000 $full = str_split( $fullWidth, 3 );
3001 $half = str_split( $halfWidth );
3004 $string = str_replace( $full, $half, $string );
3009 * @param string $string
3010 * @param string $pattern
3013 protected static function insertSpace( $string, $pattern ) {
3014 $string = preg_replace( $pattern, " $1 ", $string );
3015 $string = preg_replace( '/ +/', ' ', $string );
3020 * @param array $termsArray
3023 function convertForSearchResult( $termsArray ) {
3024 # some languages, e.g. Chinese, need to do a conversion
3025 # in order for search results to be displayed correctly
3030 * Get the first character of a string.
3035 function firstChar( $s ) {
3038 '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
3039 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/',
3044 if ( isset( $matches[1] ) ) {
3045 if ( strlen( $matches[1] ) != 3 ) {
3049 // Break down Hangul syllables to grab the first jamo
3050 $code = UtfNormal\Utils
::utf8ToCodepoint( $matches[1] );
3051 if ( $code < 0xac00 ||
0xd7a4 <= $code ) {
3053 } elseif ( $code < 0xb098 ) {
3054 return "\xe3\x84\xb1";
3055 } elseif ( $code < 0xb2e4 ) {
3056 return "\xe3\x84\xb4";
3057 } elseif ( $code < 0xb77c ) {
3058 return "\xe3\x84\xb7";
3059 } elseif ( $code < 0xb9c8 ) {
3060 return "\xe3\x84\xb9";
3061 } elseif ( $code < 0xbc14 ) {
3062 return "\xe3\x85\x81";
3063 } elseif ( $code < 0xc0ac ) {
3064 return "\xe3\x85\x82";
3065 } elseif ( $code < 0xc544 ) {
3066 return "\xe3\x85\x85";
3067 } elseif ( $code < 0xc790 ) {
3068 return "\xe3\x85\x87";
3069 } elseif ( $code < 0xcc28 ) {
3070 return "\xe3\x85\x88";
3071 } elseif ( $code < 0xce74 ) {
3072 return "\xe3\x85\x8a";
3073 } elseif ( $code < 0xd0c0 ) {
3074 return "\xe3\x85\x8b";
3075 } elseif ( $code < 0xd30c ) {
3076 return "\xe3\x85\x8c";
3077 } elseif ( $code < 0xd558 ) {
3078 return "\xe3\x85\x8d";
3080 return "\xe3\x85\x8e";
3087 function initEncoding() {
3088 # Some languages may have an alternate char encoding option
3089 # (Esperanto X-coding, Japanese furigana conversion, etc)
3090 # If this language is used as the primary content language,
3091 # an override to the defaults can be set here on startup.
3098 function recodeForEdit( $s ) {
3099 # For some languages we'll want to explicitly specify
3100 # which characters make it into the edit box raw
3101 # or are converted in some way or another.
3102 global $wgEditEncoding;
3103 if ( $wgEditEncoding == '' ||
$wgEditEncoding == 'UTF-8' ) {
3106 return $this->iconv( 'UTF-8', $wgEditEncoding, $s );
3114 function recodeInput( $s ) {
3115 # Take the previous into account.
3116 global $wgEditEncoding;
3117 if ( $wgEditEncoding != '' ) {
3118 $enc = $wgEditEncoding;
3122 if ( $enc == 'UTF-8' ) {
3125 return $this->iconv( $enc, 'UTF-8', $s );
3130 * Convert a UTF-8 string to normal form C. In Malayalam and Arabic, this
3131 * also cleans up certain backwards-compatible sequences, converting them
3132 * to the modern Unicode equivalent.
3134 * This is language-specific for performance reasons only.
3140 function normalize( $s ) {
3141 global $wgAllUnicodeFixes;
3142 $s = UtfNormal\Validator
::cleanUp( $s );
3143 if ( $wgAllUnicodeFixes ) {
3144 $s = $this->transformUsingPairFile( 'normalize-ar.ser', $s );
3145 $s = $this->transformUsingPairFile( 'normalize-ml.ser', $s );
3152 * Transform a string using serialized data stored in the given file (which
3153 * must be in the serialized subdirectory of $IP). The file contains pairs
3154 * mapping source characters to destination characters.
3156 * The data is cached in process memory. This will go faster if you have the
3157 * FastStringSearch extension.
3159 * @param string $file
3160 * @param string $string
3162 * @throws MWException
3165 function transformUsingPairFile( $file, $string ) {
3166 if ( !isset( $this->transformData
[$file] ) ) {
3167 $data = wfGetPrecompiledData( $file );
3168 if ( $data === false ) {
3169 throw new MWException( __METHOD__
. ": The transformation file $file is missing" );
3171 $this->transformData
[$file] = new ReplacementArray( $data );
3173 return $this->transformData
[$file]->replace( $string );
3177 * For right-to-left language support
3182 return self
::$dataCache->getItem( $this->mCode
, 'rtl' );
3186 * Return the correct HTML 'dir' attribute value for this language.
3190 return $this->isRTL() ?
'rtl' : 'ltr';
3194 * Return 'left' or 'right' as appropriate alignment for line-start
3195 * for this language's text direction.
3197 * Should be equivalent to CSS3 'start' text-align value....
3201 function alignStart() {
3202 return $this->isRTL() ?
'right' : 'left';
3206 * Return 'right' or 'left' as appropriate alignment for line-end
3207 * for this language's text direction.
3209 * Should be equivalent to CSS3 'end' text-align value....
3213 function alignEnd() {
3214 return $this->isRTL() ?
'left' : 'right';
3218 * A hidden direction mark (LRM or RLM), depending on the language direction.
3219 * Unlike getDirMark(), this function returns the character as an HTML entity.
3220 * This function should be used when the output is guaranteed to be HTML,
3221 * because it makes the output HTML source code more readable. When
3222 * the output is plain text or can be escaped, getDirMark() should be used.
3224 * @param bool $opposite Get the direction mark opposite to your language
3228 function getDirMarkEntity( $opposite = false ) {
3230 return $this->isRTL() ?
'‎' : '‏';
3232 return $this->isRTL() ?
'‏' : '‎';
3236 * A hidden direction mark (LRM or RLM), depending on the language direction.
3237 * This function produces them as invisible Unicode characters and
3238 * the output may be hard to read and debug, so it should only be used
3239 * when the output is plain text or can be escaped. When the output is
3240 * HTML, use getDirMarkEntity() instead.
3242 * @param bool $opposite Get the direction mark opposite to your language
3245 function getDirMark( $opposite = false ) {
3246 $lrm = "\xE2\x80\x8E"; # LEFT-TO-RIGHT MARK, commonly abbreviated LRM
3247 $rlm = "\xE2\x80\x8F"; # RIGHT-TO-LEFT MARK, commonly abbreviated RLM
3249 return $this->isRTL() ?
$lrm : $rlm;
3251 return $this->isRTL() ?
$rlm : $lrm;
3257 function capitalizeAllNouns() {
3258 return self
::$dataCache->getItem( $this->mCode
, 'capitalizeAllNouns' );
3262 * An arrow, depending on the language direction.
3264 * @param string $direction The direction of the arrow: forwards (default),
3265 * backwards, left, right, up, down.
3268 function getArrow( $direction = 'forwards' ) {
3269 switch ( $direction ) {
3271 return $this->isRTL() ?
'←' : '→';
3273 return $this->isRTL() ?
'→' : '←';
3286 * To allow "foo[[bar]]" to extend the link over the whole word "foobar"
3290 function linkPrefixExtension() {
3291 return self
::$dataCache->getItem( $this->mCode
, 'linkPrefixExtension' );
3295 * Get all magic words from cache.
3298 function getMagicWords() {
3299 return self
::$dataCache->getItem( $this->mCode
, 'magicWords' );
3303 * Run the LanguageGetMagic hook once.
3305 protected function doMagicHook() {
3306 if ( $this->mMagicHookDone
) {
3309 $this->mMagicHookDone
= true;
3310 Hooks
::run( 'LanguageGetMagic', array( &$this->mMagicExtensions
, $this->getCode() ) );
3314 * Fill a MagicWord object with data from here
3316 * @param MagicWord $mw
3318 function getMagic( $mw ) {
3319 // Saves a function call
3320 if ( !$this->mMagicHookDone
) {
3321 $this->doMagicHook();
3324 if ( isset( $this->mMagicExtensions
[$mw->mId
] ) ) {
3325 $rawEntry = $this->mMagicExtensions
[$mw->mId
];
3327 $rawEntry = self
::$dataCache->getSubitem(
3328 $this->mCode
, 'magicWords', $mw->mId
);
3331 if ( !is_array( $rawEntry ) ) {
3332 wfWarn( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
3334 $mw->mCaseSensitive
= $rawEntry[0];
3335 $mw->mSynonyms
= array_slice( $rawEntry, 1 );
3340 * Add magic words to the extension array
3342 * @param array $newWords
3344 function addMagicWordsByLang( $newWords ) {
3345 $fallbackChain = $this->getFallbackLanguages();
3346 $fallbackChain = array_reverse( $fallbackChain );
3347 foreach ( $fallbackChain as $code ) {
3348 if ( isset( $newWords[$code] ) ) {
3349 $this->mMagicExtensions
= $newWords[$code] +
$this->mMagicExtensions
;
3355 * Get special page names, as an associative array
3356 * canonical name => array of valid names, including aliases
3359 function getSpecialPageAliases() {
3360 // Cache aliases because it may be slow to load them
3361 if ( is_null( $this->mExtendedSpecialPageAliases
) ) {
3363 $this->mExtendedSpecialPageAliases
=
3364 self
::$dataCache->getItem( $this->mCode
, 'specialPageAliases' );
3365 Hooks
::run( 'LanguageGetSpecialPageAliases',
3366 array( &$this->mExtendedSpecialPageAliases
, $this->getCode() ) );
3369 return $this->mExtendedSpecialPageAliases
;
3373 * Italic is unsuitable for some languages
3375 * @param string $text The text to be emphasized.
3378 function emphasize( $text ) {
3379 return "<em>$text</em>";
3383 * Normally we output all numbers in plain en_US style, that is
3384 * 293,291.235 for twohundredninetythreethousand-twohundredninetyone
3385 * point twohundredthirtyfive. However this is not suitable for all
3386 * languages, some such as Punjabi want ੨੯੩,੨੯੫.੨੩੫ and others such as
3387 * Icelandic just want to use commas instead of dots, and dots instead
3388 * of commas like "293.291,235".
3390 * An example of this function being called:
3392 * wfMessage( 'message' )->numParams( $num )->text()
3395 * See $separatorTransformTable on MessageIs.php for
3396 * the , => . and . => , implementation.
3398 * @todo check if it's viable to use localeconv() for the decimal separator thing.
3399 * @param int|float $number The string to be formatted, should be an integer
3400 * or a floating point number.
3401 * @param bool $nocommafy Set to true for special numbers like dates
3404 public function formatNum( $number, $nocommafy = false ) {
3405 global $wgTranslateNumerals;
3406 if ( !$nocommafy ) {
3407 $number = $this->commafy( $number );
3408 $s = $this->separatorTransformTable();
3410 $number = strtr( $number, $s );
3414 if ( $wgTranslateNumerals ) {
3415 $s = $this->digitTransformTable();
3417 $number = strtr( $number, $s );
3425 * Front-end for non-commafied formatNum
3427 * @param int|float $number The string to be formatted, should be an integer
3428 * or a floating point number.
3432 public function formatNumNoSeparators( $number ) {
3433 return $this->formatNum( $number, true );
3437 * @param string $number
3440 public function parseFormattedNumber( $number ) {
3441 $s = $this->digitTransformTable();
3443 // eliminate empty array values such as ''. (bug 64347)
3444 $s = array_filter( $s );
3445 $number = strtr( $number, array_flip( $s ) );
3448 $s = $this->separatorTransformTable();
3450 // eliminate empty array values such as ''. (bug 64347)
3451 $s = array_filter( $s );
3452 $number = strtr( $number, array_flip( $s ) );
3455 $number = strtr( $number, array( ',' => '' ) );
3460 * Adds commas to a given number
3462 * @param mixed $number
3465 function commafy( $number ) {
3466 $digitGroupingPattern = $this->digitGroupingPattern();
3467 if ( $number === null ) {
3471 if ( !$digitGroupingPattern ||
$digitGroupingPattern === "###,###,###" ) {
3472 // default grouping is at thousands, use the same for ###,###,### pattern too.
3473 return strrev( (string)preg_replace( '/(\d{3})(?=\d)(?!\d*\.)/', '$1,', strrev( $number ) ) );
3475 // Ref: http://cldr.unicode.org/translation/number-patterns
3477 if ( intval( $number ) < 0 ) {
3478 // For negative numbers apply the algorithm like positive number and add sign.
3480 $number = substr( $number, 1 );
3482 $integerPart = array();
3483 $decimalPart = array();
3484 $numMatches = preg_match_all( "/(#+)/", $digitGroupingPattern, $matches );
3485 preg_match( "/\d+/", $number, $integerPart );
3486 preg_match( "/\.\d*/", $number, $decimalPart );
3487 $groupedNumber = ( count( $decimalPart ) > 0 ) ?
$decimalPart[0] : "";
3488 if ( $groupedNumber === $number ) {
3489 // the string does not have any number part. Eg: .12345
3490 return $sign . $groupedNumber;
3492 $start = $end = ( $integerPart ) ?
strlen( $integerPart[0] ) : 0;
3493 while ( $start > 0 ) {
3494 $match = $matches[0][$numMatches - 1];
3495 $matchLen = strlen( $match );
3496 $start = $end - $matchLen;
3500 $groupedNumber = substr( $number, $start, $end -$start ) . $groupedNumber;
3502 if ( $numMatches > 1 ) {
3503 // use the last pattern for the rest of the number
3507 $groupedNumber = "," . $groupedNumber;
3510 return $sign . $groupedNumber;
3517 function digitGroupingPattern() {
3518 return self
::$dataCache->getItem( $this->mCode
, 'digitGroupingPattern' );
3524 function digitTransformTable() {
3525 return self
::$dataCache->getItem( $this->mCode
, 'digitTransformTable' );
3531 function separatorTransformTable() {
3532 return self
::$dataCache->getItem( $this->mCode
, 'separatorTransformTable' );
3536 * Take a list of strings and build a locale-friendly comma-separated
3537 * list, using the local comma-separator message.
3538 * The last two strings are chained with an "and".
3539 * NOTE: This function will only work with standard numeric array keys (0, 1, 2…)
3541 * @param string[] $l
3544 function listToText( array $l ) {
3545 $m = count( $l ) - 1;
3550 $and = $this->msg( 'and' )->escaped();
3551 $space = $this->msg( 'word-separator' )->escaped();
3553 $comma = $this->msg( 'comma-separator' )->escaped();
3557 for ( $i = $m - 1; $i >= 0; $i-- ) {
3558 if ( $i == $m - 1 ) {
3559 $s = $l[$i] . $and . $space . $s;
3561 $s = $l[$i] . $comma . $s;
3568 * Take a list of strings and build a locale-friendly comma-separated
3569 * list, using the local comma-separator message.
3570 * @param string[] $list Array of strings to put in a comma list
3573 function commaList( array $list ) {
3575 wfMessage( 'comma-separator' )->inLanguage( $this )->escaped(),
3581 * Take a list of strings and build a locale-friendly semicolon-separated
3582 * list, using the local semicolon-separator message.
3583 * @param string[] $list Array of strings to put in a semicolon list
3586 function semicolonList( array $list ) {
3588 wfMessage( 'semicolon-separator' )->inLanguage( $this )->escaped(),
3594 * Same as commaList, but separate it with the pipe instead.
3595 * @param string[] $list Array of strings to put in a pipe list
3598 function pipeList( array $list ) {
3600 wfMessage( 'pipe-separator' )->inLanguage( $this )->escaped(),
3606 * Truncate a string to a specified length in bytes, appending an optional
3607 * string (e.g. for ellipses)
3609 * The database offers limited byte lengths for some columns in the database;
3610 * multi-byte character sets mean we need to ensure that only whole characters
3611 * are included, otherwise broken characters can be passed to the user
3613 * If $length is negative, the string will be truncated from the beginning
3615 * @param string $string String to truncate
3616 * @param int $length Maximum length (including ellipses)
3617 * @param string $ellipsis String to append to the truncated text
3618 * @param bool $adjustLength Subtract length of ellipsis from $length.
3619 * $adjustLength was introduced in 1.18, before that behaved as if false.
3622 function truncate( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3623 # Use the localized ellipsis character
3624 if ( $ellipsis == '...' ) {
3625 $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3627 # Check if there is no need to truncate
3628 if ( $length == 0 ) {
3629 return $ellipsis; // convention
3630 } elseif ( strlen( $string ) <= abs( $length ) ) {
3631 return $string; // no need to truncate
3633 $stringOriginal = $string;
3634 # If ellipsis length is >= $length then we can't apply $adjustLength
3635 if ( $adjustLength && strlen( $ellipsis ) >= abs( $length ) ) {
3636 $string = $ellipsis; // this can be slightly unexpected
3637 # Otherwise, truncate and add ellipsis...
3639 $eLength = $adjustLength ?
strlen( $ellipsis ) : 0;
3640 if ( $length > 0 ) {
3641 $length -= $eLength;
3642 $string = substr( $string, 0, $length ); // xyz...
3643 $string = $this->removeBadCharLast( $string );
3644 $string = rtrim( $string );
3645 $string = $string . $ellipsis;
3647 $length +
= $eLength;
3648 $string = substr( $string, $length ); // ...xyz
3649 $string = $this->removeBadCharFirst( $string );
3650 $string = ltrim( $string );
3651 $string = $ellipsis . $string;
3654 # Do not truncate if the ellipsis makes the string longer/equal (bug 22181).
3655 # This check is *not* redundant if $adjustLength, due to the single case where
3656 # LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string.
3657 if ( strlen( $string ) < strlen( $stringOriginal ) ) {
3660 return $stringOriginal;
3665 * Remove bytes that represent an incomplete Unicode character
3666 * at the end of string (e.g. bytes of the char are missing)
3668 * @param string $string
3671 protected function removeBadCharLast( $string ) {
3672 if ( $string != '' ) {
3673 $char = ord( $string[strlen( $string ) - 1] );
3675 if ( $char >= 0xc0 ) {
3676 # We got the first byte only of a multibyte char; remove it.
3677 $string = substr( $string, 0, -1 );
3678 } elseif ( $char >= 0x80 &&
3679 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
3680 '[\xf0-\xf7][\x80-\xbf]{1,2})$/', $string, $m )
3682 # We chopped in the middle of a character; remove it
3690 * Remove bytes that represent an incomplete Unicode character
3691 * at the start of string (e.g. bytes of the char are missing)
3693 * @param string $string
3696 protected function removeBadCharFirst( $string ) {
3697 if ( $string != '' ) {
3698 $char = ord( $string[0] );
3699 if ( $char >= 0x80 && $char < 0xc0 ) {
3700 # We chopped in the middle of a character; remove the whole thing
3701 $string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
3708 * Truncate a string of valid HTML to a specified length in bytes,
3709 * appending an optional string (e.g. for ellipses), and return valid HTML
3711 * This is only intended for styled/linked text, such as HTML with
3712 * tags like <span> and <a>, were the tags are self-contained (valid HTML).
3713 * Also, this will not detect things like "display:none" CSS.
3715 * Note: since 1.18 you do not need to leave extra room in $length for ellipses.
3717 * @param string $text HTML string to truncate
3718 * @param int $length (zero/positive) Maximum length (including ellipses)
3719 * @param string $ellipsis String to append to the truncated text
3722 function truncateHtml( $text, $length, $ellipsis = '...' ) {
3723 # Use the localized ellipsis character
3724 if ( $ellipsis == '...' ) {
3725 $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3727 # Check if there is clearly no need to truncate
3728 if ( $length <= 0 ) {
3729 return $ellipsis; // no text shown, nothing to format (convention)
3730 } elseif ( strlen( $text ) <= $length ) {
3731 return $text; // string short enough even *with* HTML (short-circuit)
3734 $dispLen = 0; // innerHTML legth so far
3735 $testingEllipsis = false; // checking if ellipses will make string longer/equal?
3736 $tagType = 0; // 0-open, 1-close
3737 $bracketState = 0; // 1-tag start, 2-tag name, 0-neither
3738 $entityState = 0; // 0-not entity, 1-entity
3739 $tag = $ret = ''; // accumulated tag name, accumulated result string
3740 $openTags = array(); // open tag stack
3741 $maybeState = null; // possible truncation state
3743 $textLen = strlen( $text );
3744 $neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated
3745 for ( $pos = 0; true; ++
$pos ) {
3746 # Consider truncation once the display length has reached the maximim.
3747 # We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
3748 # Check that we're not in the middle of a bracket/entity...
3749 if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) {
3750 if ( !$testingEllipsis ) {
3751 $testingEllipsis = true;
3752 # Save where we are; we will truncate here unless there turn out to
3753 # be so few remaining characters that truncation is not necessary.
3754 if ( !$maybeState ) { // already saved? ($neLength = 0 case)
3755 $maybeState = array( $ret, $openTags ); // save state
3757 } elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) {
3758 # String in fact does need truncation, the truncation point was OK.
3759 list( $ret, $openTags ) = $maybeState; // reload state
3760 $ret = $this->removeBadCharLast( $ret ); // multi-byte char fix
3761 $ret .= $ellipsis; // add ellipsis
3765 if ( $pos >= $textLen ) {
3766 break; // extra iteration just for above checks
3769 # Read the next char...
3771 $lastCh = $pos ?
$text[$pos - 1] : '';
3772 $ret .= $ch; // add to result string
3774 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); // for bad HTML
3775 $entityState = 0; // for bad HTML
3776 $bracketState = 1; // tag started (checking for backslash)
3777 } elseif ( $ch == '>' ) {
3778 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags );
3779 $entityState = 0; // for bad HTML
3780 $bracketState = 0; // out of brackets
3781 } elseif ( $bracketState == 1 ) {
3783 $tagType = 1; // close tag (e.g. "</span>")
3785 $tagType = 0; // open tag (e.g. "<span>")
3788 $bracketState = 2; // building tag name
3789 } elseif ( $bracketState == 2 ) {
3793 // Name found (e.g. "<a href=..."), add on tag attributes...
3794 $pos +
= $this->truncate_skip( $ret, $text, "<>", $pos +
1 );
3796 } elseif ( $bracketState == 0 ) {
3797 if ( $entityState ) {
3800 $dispLen++
; // entity is one displayed char
3803 if ( $neLength == 0 && !$maybeState ) {
3804 // Save state without $ch. We want to *hit* the first
3805 // display char (to get tags) but not *use* it if truncating.
3806 $maybeState = array( substr( $ret, 0, -1 ), $openTags );
3809 $entityState = 1; // entity found, (e.g. " ")
3811 $dispLen++
; // this char is displayed
3812 // Add the next $max display text chars after this in one swoop...
3813 $max = ( $testingEllipsis ?
$length : $neLength ) - $dispLen;
3814 $skipped = $this->truncate_skip( $ret, $text, "<>&", $pos +
1, $max );
3815 $dispLen +
= $skipped;
3821 // Close the last tag if left unclosed by bad HTML
3822 $this->truncate_endBracket( $tag, $text[$textLen - 1], $tagType, $openTags );
3823 while ( count( $openTags ) > 0 ) {
3824 $ret .= '</' . array_pop( $openTags ) . '>'; // close open tags
3830 * truncateHtml() helper function
3831 * like strcspn() but adds the skipped chars to $ret
3833 * @param string $ret
3834 * @param string $text
3835 * @param string $search
3837 * @param null|int $len
3840 private function truncate_skip( &$ret, $text, $search, $start, $len = null ) {
3841 if ( $len === null ) {
3842 $len = -1; // -1 means "no limit" for strcspn
3843 } elseif ( $len < 0 ) {
3847 if ( $start < strlen( $text ) ) {
3848 $skipCount = strcspn( $text, $search, $start, $len );
3849 $ret .= substr( $text, $start, $skipCount );
3855 * truncateHtml() helper function
3856 * (a) push or pop $tag from $openTags as needed
3857 * (b) clear $tag value
3858 * @param string &$tag Current HTML tag name we are looking at
3859 * @param int $tagType (0-open tag, 1-close tag)
3860 * @param string $lastCh Character before the '>' that ended this tag
3861 * @param array &$openTags Open tag stack (not accounting for $tag)
3863 private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) {
3864 $tag = ltrim( $tag );
3866 if ( $tagType == 0 && $lastCh != '/' ) {
3867 $openTags[] = $tag; // tag opened (didn't close itself)
3868 } elseif ( $tagType == 1 ) {
3869 if ( $openTags && $tag == $openTags[count( $openTags ) - 1] ) {
3870 array_pop( $openTags ); // tag closed
3878 * Grammatical transformations, needed for inflected languages
3879 * Invoked by putting {{grammar:case|word}} in a message
3881 * @param string $word
3882 * @param string $case
3885 function convertGrammar( $word, $case ) {
3886 global $wgGrammarForms;
3887 if ( isset( $wgGrammarForms[$this->getCode()][$case][$word] ) ) {
3888 return $wgGrammarForms[$this->getCode()][$case][$word];
3894 * Get the grammar forms for the content language
3895 * @return array Array of grammar forms
3898 function getGrammarForms() {
3899 global $wgGrammarForms;
3900 if ( isset( $wgGrammarForms[$this->getCode()] )
3901 && is_array( $wgGrammarForms[$this->getCode()] )
3903 return $wgGrammarForms[$this->getCode()];
3909 * Provides an alternative text depending on specified gender.
3910 * Usage {{gender:username|masculine|feminine|unknown}}.
3911 * username is optional, in which case the gender of current user is used,
3912 * but only in (some) interface messages; otherwise default gender is used.
3914 * If no forms are given, an empty string is returned. If only one form is
3915 * given, it will be returned unconditionally. These details are implied by
3916 * the caller and cannot be overridden in subclasses.
3918 * If three forms are given, the default is to use the third (unknown) form.
3919 * If fewer than three forms are given, the default is to use the first (masculine) form.
3920 * These details can be overridden in subclasses.
3922 * @param string $gender
3923 * @param array $forms
3927 function gender( $gender, $forms ) {
3928 if ( !count( $forms ) ) {
3931 $forms = $this->preConvertPlural( $forms, 2 );
3932 if ( $gender === 'male' ) {
3935 if ( $gender === 'female' ) {
3938 return isset( $forms[2] ) ?
$forms[2] : $forms[0];
3942 * Plural form transformations, needed for some languages.
3943 * For example, there are 3 form of plural in Russian and Polish,
3944 * depending on "count mod 10". See [[w:Plural]]
3945 * For English it is pretty simple.
3947 * Invoked by putting {{plural:count|wordform1|wordform2}}
3948 * or {{plural:count|wordform1|wordform2|wordform3}}
3950 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
3952 * @param int $count Non-localized number
3953 * @param array $forms Different plural forms
3954 * @return string Correct form of plural for $count in this language
3956 function convertPlural( $count, $forms ) {
3957 // Handle explicit n=pluralform cases
3958 $forms = $this->handleExplicitPluralForms( $count, $forms );
3959 if ( is_string( $forms ) ) {
3962 if ( !count( $forms ) ) {
3966 $pluralForm = $this->getPluralRuleIndexNumber( $count );
3967 $pluralForm = min( $pluralForm, count( $forms ) - 1 );
3968 return $forms[$pluralForm];
3972 * Handles explicit plural forms for Language::convertPlural()
3974 * In {{PLURAL:$1|0=nothing|one|many}}, 0=nothing will be returned if $1 equals zero.
3975 * If an explicitly defined plural form matches the $count, then
3976 * string value returned, otherwise array returned for further consideration
3977 * by CLDR rules or overridden convertPlural().
3981 * @param int $count Non-localized number
3982 * @param array $forms Different plural forms
3984 * @return array|string
3986 protected function handleExplicitPluralForms( $count, array $forms ) {
3987 foreach ( $forms as $index => $form ) {
3988 if ( preg_match( '/\d+=/i', $form ) ) {
3989 $pos = strpos( $form, '=' );
3990 if ( substr( $form, 0, $pos ) === (string)$count ) {
3991 return substr( $form, $pos +
1 );
3993 unset( $forms[$index] );
3996 return array_values( $forms );
4000 * Checks that convertPlural was given an array and pads it to requested
4001 * amount of forms by copying the last one.
4003 * @param array $forms Array of forms given to convertPlural
4004 * @param int $count How many forms should there be at least
4005 * @return array Padded array of forms or an exception if not an array
4007 protected function preConvertPlural( /* Array */ $forms, $count ) {
4008 while ( count( $forms ) < $count ) {
4009 $forms[] = $forms[count( $forms ) - 1];
4015 * Wraps argument with unicode control characters for directionality safety
4017 * This solves the problem where directionality-neutral characters at the edge of
4018 * the argument string get interpreted with the wrong directionality from the
4019 * enclosing context, giving renderings that look corrupted like "(Ben_(WMF".
4021 * The wrapping is LRE...PDF or RLE...PDF, depending on the detected
4022 * directionality of the argument string, using the BIDI algorithm's own "First
4023 * strong directional codepoint" rule. Essentially, this works round the fact that
4024 * there is no embedding equivalent of U+2068 FSI (isolation with heuristic
4025 * direction inference). The latter is cleaner but still not widely supported.
4027 * @param string $text Text to wrap
4028 * @return string Text, wrapped in LRE...PDF or RLE...PDF or nothing
4030 public function embedBidi( $text = '' ) {
4031 $dir = Language
::strongDirFromContent( $text );
4032 if ( $dir === 'ltr' ) {
4033 // Wrap in LEFT-TO-RIGHT EMBEDDING ... POP DIRECTIONAL FORMATTING
4034 return self
::$lre . $text . self
::$pdf;
4036 if ( $dir === 'rtl' ) {
4037 // Wrap in RIGHT-TO-LEFT EMBEDDING ... POP DIRECTIONAL FORMATTING
4038 return self
::$rle . $text . self
::$pdf;
4040 // No strong directionality: do not wrap
4045 * @todo Maybe translate block durations. Note that this function is somewhat misnamed: it
4046 * deals with translating the *duration* ("1 week", "4 days", etc), not the expiry time
4047 * (which is an absolute timestamp). Please note: do NOT add this blindly, as it is used
4048 * on old expiry lengths recorded in log entries. You'd need to provide the start date to
4051 * @param string $str The validated block duration in English
4052 * @return string Somehow translated block duration
4053 * @see LanguageFi.php for example implementation
4055 function translateBlockExpiry( $str ) {
4056 $duration = SpecialBlock
::getSuggestedDurations( $this );
4057 foreach ( $duration as $show => $value ) {
4058 if ( strcmp( $str, $value ) == 0 ) {
4059 return htmlspecialchars( trim( $show ) );
4063 if ( wfIsInfinity( $str ) ) {
4064 foreach ( $duration as $show => $value ) {
4065 if ( wfIsInfinity( $value ) ) {
4066 return htmlspecialchars( trim( $show ) );
4071 // If all else fails, return a standard duration or timestamp description.
4072 $time = strtotime( $str, 0 );
4073 if ( $time === false ) { // Unknown format. Return it as-is in case.
4075 } elseif ( $time !== strtotime( $str, 1 ) ) { // It's a relative timestamp.
4076 // $time is relative to 0 so it's a duration length.
4077 return $this->formatDuration( $time );
4078 } else { // It's an absolute timestamp.
4079 if ( $time === 0 ) {
4080 // wfTimestamp() handles 0 as current time instead of epoch.
4081 return $this->timeanddate( '19700101000000' );
4083 return $this->timeanddate( $time );
4089 * languages like Chinese need to be segmented in order for the diff
4092 * @param string $text
4095 public function segmentForDiff( $text ) {
4100 * and unsegment to show the result
4102 * @param string $text
4105 public function unsegmentForDiff( $text ) {
4110 * Return the LanguageConverter used in the Language
4113 * @return LanguageConverter
4115 public function getConverter() {
4116 return $this->mConverter
;
4120 * convert text to all supported variants
4122 * @param string $text
4125 public function autoConvertToAllVariants( $text ) {
4126 return $this->mConverter
->autoConvertToAllVariants( $text );
4130 * convert text to different variants of a language.
4132 * @param string $text
4135 public function convert( $text ) {
4136 return $this->mConverter
->convert( $text );
4140 * Convert a Title object to a string in the preferred variant
4142 * @param Title $title
4145 public function convertTitle( $title ) {
4146 return $this->mConverter
->convertTitle( $title );
4150 * Convert a namespace index to a string in the preferred variant
4155 public function convertNamespace( $ns ) {
4156 return $this->mConverter
->convertNamespace( $ns );
4160 * Check if this is a language with variants
4164 public function hasVariants() {
4165 return count( $this->getVariants() ) > 1;
4169 * Check if the language has the specific variant
4172 * @param string $variant
4175 public function hasVariant( $variant ) {
4176 return (bool)$this->mConverter
->validateVariant( $variant );
4180 * Put custom tags (e.g. -{ }-) around math to prevent conversion
4182 * @param string $text
4184 * @deprecated since 1.22 is no longer used
4186 public function armourMath( $text ) {
4187 return $this->mConverter
->armourMath( $text );
4191 * Perform output conversion on a string, and encode for safe HTML output.
4192 * @param string $text Text to be converted
4193 * @param bool $isTitle Whether this conversion is for the article title
4195 * @todo this should get integrated somewhere sane
4197 public function convertHtml( $text, $isTitle = false ) {
4198 return htmlspecialchars( $this->convert( $text, $isTitle ) );
4202 * @param string $key
4205 public function convertCategoryKey( $key ) {
4206 return $this->mConverter
->convertCategoryKey( $key );
4210 * Get the list of variants supported by this language
4211 * see sample implementation in LanguageZh.php
4213 * @return array An array of language codes
4215 public function getVariants() {
4216 return $this->mConverter
->getVariants();
4222 public function getPreferredVariant() {
4223 return $this->mConverter
->getPreferredVariant();
4229 public function getDefaultVariant() {
4230 return $this->mConverter
->getDefaultVariant();
4236 public function getURLVariant() {
4237 return $this->mConverter
->getURLVariant();
4241 * If a language supports multiple variants, it is
4242 * possible that non-existing link in one variant
4243 * actually exists in another variant. this function
4244 * tries to find it. See e.g. LanguageZh.php
4245 * The input parameters may be modified upon return
4247 * @param string &$link The name of the link
4248 * @param Title &$nt The title object of the link
4249 * @param bool $ignoreOtherCond To disable other conditions when
4250 * we need to transclude a template or update a category's link
4252 public function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
4253 $this->mConverter
->findVariantLink( $link, $nt, $ignoreOtherCond );
4257 * returns language specific options used by User::getPageRenderHash()
4258 * for example, the preferred language variant
4262 function getExtraHashOptions() {
4263 return $this->mConverter
->getExtraHashOptions();
4267 * For languages that support multiple variants, the title of an
4268 * article may be displayed differently in different variants. this
4269 * function returns the apporiate title defined in the body of the article.
4273 public function getParsedTitle() {
4274 return $this->mConverter
->getParsedTitle();
4278 * Prepare external link text for conversion. When the text is
4279 * a URL, it shouldn't be converted, and it'll be wrapped in
4280 * the "raw" tag (-{R| }-) to prevent conversion.
4282 * This function is called "markNoConversion" for historical
4285 * @param string $text Text to be used for external link
4286 * @param bool $noParse Wrap it without confirming it's a real URL first
4287 * @return string The tagged text
4289 public function markNoConversion( $text, $noParse = false ) {
4290 // Excluding protocal-relative URLs may avoid many false positives.
4291 if ( $noParse ||
preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
4292 return $this->mConverter
->markNoConversion( $text );
4299 * A regular expression to match legal word-trailing characters
4300 * which should be merged onto a link of the form [[foo]]bar.
4304 public function linkTrail() {
4305 return self
::$dataCache->getItem( $this->mCode
, 'linkTrail' );
4309 * A regular expression character set to match legal word-prefixing
4310 * characters which should be merged onto a link of the form foo[[bar]].
4314 public function linkPrefixCharset() {
4315 return self
::$dataCache->getItem( $this->mCode
, 'linkPrefixCharset' );
4319 * @deprecated since 1.24, will be removed in 1.25
4322 function getLangObj() {
4323 wfDeprecated( __METHOD__
, '1.24' );
4328 * Get the "parent" language which has a converter to convert a "compatible" language
4329 * (in another variant) to this language (eg. zh for zh-cn, but not en for en-gb).
4331 * @return Language|null
4334 public function getParentLanguage() {
4335 if ( $this->mParentLanguage
!== false ) {
4336 return $this->mParentLanguage
;
4339 $pieces = explode( '-', $this->getCode() );
4341 if ( !in_array( $code, LanguageConverter
::$languagesWithVariants ) ) {
4342 $this->mParentLanguage
= null;
4345 $lang = Language
::factory( $code );
4346 if ( !$lang->hasVariant( $this->getCode() ) ) {
4347 $this->mParentLanguage
= null;
4351 $this->mParentLanguage
= $lang;
4356 * Get the RFC 3066 code for this language object
4358 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
4359 * htmlspecialchars() or similar
4363 public function getCode() {
4364 return $this->mCode
;
4368 * Get the code in Bcp47 format which we can use
4369 * inside of html lang="" tags.
4371 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
4372 * htmlspecialchars() or similar.
4377 public function getHtmlCode() {
4378 if ( is_null( $this->mHtmlCode
) ) {
4379 $this->mHtmlCode
= wfBCP47( $this->getCode() );
4381 return $this->mHtmlCode
;
4385 * @param string $code
4387 public function setCode( $code ) {
4388 $this->mCode
= $code;
4389 // Ensure we don't leave incorrect cached data lying around
4390 $this->mHtmlCode
= null;
4391 $this->mParentLanguage
= false;
4395 * Get the name of a file for a certain language code
4396 * @param string $prefix Prepend this to the filename
4397 * @param string $code Language code
4398 * @param string $suffix Append this to the filename
4399 * @throws MWException
4400 * @return string $prefix . $mangledCode . $suffix
4402 public static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
4403 if ( !self
::isValidBuiltInCode( $code ) ) {
4404 throw new MWException( "Invalid language code \"$code\"" );
4407 return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
4411 * Get the language code from a file name. Inverse of getFileName()
4412 * @param string $filename $prefix . $languageCode . $suffix
4413 * @param string $prefix Prefix before the language code
4414 * @param string $suffix Suffix after the language code
4415 * @return string Language code, or false if $prefix or $suffix isn't found
4417 public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
4419 preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
4420 preg_quote( $suffix, '/' ) . '/', $filename, $m );
4421 if ( !count( $m ) ) {
4424 return str_replace( '_', '-', strtolower( $m[1] ) );
4428 * @param string $code
4431 public static function getMessagesFileName( $code ) {
4433 $file = self
::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
4434 Hooks
::run( 'Language::getMessagesFileName', array( $code, &$file ) );
4439 * @param string $code
4443 public static function getJsonMessagesFileName( $code ) {
4446 if ( !self
::isValidBuiltInCode( $code ) ) {
4447 throw new MWException( "Invalid language code \"$code\"" );
4450 return "$IP/languages/i18n/$code.json";
4454 * @param string $code
4457 public static function getClassFileName( $code ) {
4459 return self
::getFileName( "$IP/languages/classes/Language", $code, '.php' );
4463 * Get the first fallback for a given language.
4465 * @param string $code
4467 * @return bool|string
4469 public static function getFallbackFor( $code ) {
4470 if ( $code === 'en' ||
!Language
::isValidBuiltInCode( $code ) ) {
4473 $fallbacks = self
::getFallbacksFor( $code );
4474 return $fallbacks[0];
4479 * Get the ordered list of fallback languages.
4482 * @param string $code Language code
4483 * @return array Non-empty array, ending in "en"
4485 public static function getFallbacksFor( $code ) {
4486 if ( $code === 'en' ||
!Language
::isValidBuiltInCode( $code ) ) {
4489 // For unknown languages, fallbackSequence returns an empty array,
4490 // hardcode fallback to 'en' in that case.
4491 return self
::getLocalisationCache()->getItem( $code, 'fallbackSequence' ) ?
: array( 'en' );
4495 * Get the ordered list of fallback languages, ending with the fallback
4496 * language chain for the site language.
4499 * @param string $code Language code
4500 * @return array Array( fallbacks, site fallbacks )
4502 public static function getFallbacksIncludingSiteLanguage( $code ) {
4503 global $wgLanguageCode;
4505 // Usually, we will only store a tiny number of fallback chains, so we
4506 // keep them in static memory.
4507 $cacheKey = "{$code}-{$wgLanguageCode}";
4509 if ( !array_key_exists( $cacheKey, self
::$fallbackLanguageCache ) ) {
4510 $fallbacks = self
::getFallbacksFor( $code );
4512 // Append the site's fallback chain, including the site language itself
4513 $siteFallbacks = self
::getFallbacksFor( $wgLanguageCode );
4514 array_unshift( $siteFallbacks, $wgLanguageCode );
4516 // Eliminate any languages already included in the chain
4517 $siteFallbacks = array_diff( $siteFallbacks, $fallbacks );
4519 self
::$fallbackLanguageCache[$cacheKey] = array( $fallbacks, $siteFallbacks );
4521 return self
::$fallbackLanguageCache[$cacheKey];
4525 * Get all messages for a given language
4526 * WARNING: this may take a long time. If you just need all message *keys*
4527 * but need the *contents* of only a few messages, consider using getMessageKeysFor().
4529 * @param string $code
4533 public static function getMessagesFor( $code ) {
4534 return self
::getLocalisationCache()->getItem( $code, 'messages' );
4538 * Get a message for a given language
4540 * @param string $key
4541 * @param string $code
4545 public static function getMessageFor( $key, $code ) {
4546 return self
::getLocalisationCache()->getSubitem( $code, 'messages', $key );
4550 * Get all message keys for a given language. This is a faster alternative to
4551 * array_keys( Language::getMessagesFor( $code ) )
4554 * @param string $code Language code
4555 * @return array Array of message keys (strings)
4557 public static function getMessageKeysFor( $code ) {
4558 return self
::getLocalisationCache()->getSubItemList( $code, 'messages' );
4562 * @param string $talk
4565 function fixVariableInNamespace( $talk ) {
4566 if ( strpos( $talk, '$1' ) === false ) {
4570 global $wgMetaNamespace;
4571 $talk = str_replace( '$1', $wgMetaNamespace, $talk );
4573 # Allow grammar transformations
4574 # Allowing full message-style parsing would make simple requests
4575 # such as action=raw much more expensive than they need to be.
4576 # This will hopefully cover most cases.
4577 $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
4578 array( &$this, 'replaceGrammarInNamespace' ), $talk );
4579 return str_replace( ' ', '_', $talk );
4586 function replaceGrammarInNamespace( $m ) {
4587 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
4591 * @throws MWException
4594 static function getCaseMaps() {
4595 static $wikiUpperChars, $wikiLowerChars;
4596 if ( isset( $wikiUpperChars ) ) {
4597 return array( $wikiUpperChars, $wikiLowerChars );
4600 $arr = wfGetPrecompiledData( 'Utf8Case.ser' );
4601 if ( $arr === false ) {
4602 throw new MWException(
4603 "Utf8Case.ser is missing, please run \"make\" in the serialized directory\n" );
4605 $wikiUpperChars = $arr['wikiUpperChars'];
4606 $wikiLowerChars = $arr['wikiLowerChars'];
4607 return array( $wikiUpperChars, $wikiLowerChars );
4611 * Decode an expiry (block, protection, etc) which has come from the DB
4613 * @param string $expiry Database expiry String
4614 * @param bool|int $format True to process using language functions, or TS_ constant
4615 * to return the expiry in a given timestamp
4616 * @param string $inifinity If $format is not true, use this string for infinite expiry
4620 public function formatExpiry( $expiry, $format = true, $infinity = 'infinity' ) {
4622 if ( $dbInfinity === null ) {
4623 $dbInfinity = wfGetDB( DB_SLAVE
)->getInfinity();
4626 if ( $expiry == '' ||
$expiry === 'infinity' ||
$expiry == $dbInfinity ) {
4627 return $format === true
4628 ?
$this->getMessageFromDB( 'infiniteblock' )
4631 return $format === true
4632 ?
$this->timeanddate( $expiry, /* User preference timezone */ true )
4633 : wfTimestamp( $format, $expiry );
4639 * @param int|float $seconds
4640 * @param array $format Optional
4641 * If $format['avoid'] === 'avoidseconds': don't mention seconds if $seconds >= 1 hour.
4642 * If $format['avoid'] === 'avoidminutes': don't mention seconds/minutes if $seconds > 48 hours.
4643 * If $format['noabbrevs'] is true: use 'seconds' and friends instead of 'seconds-abbrev'
4645 * For backwards compatibility, $format may also be one of the strings 'avoidseconds'
4646 * or 'avoidminutes'.
4649 function formatTimePeriod( $seconds, $format = array() ) {
4650 if ( !is_array( $format ) ) {
4651 $format = array( 'avoid' => $format ); // For backwards compatibility
4653 if ( !isset( $format['avoid'] ) ) {
4654 $format['avoid'] = false;
4656 if ( !isset( $format['noabbrevs'] ) ) {
4657 $format['noabbrevs'] = false;
4659 $secondsMsg = wfMessage(
4660 $format['noabbrevs'] ?
'seconds' : 'seconds-abbrev' )->inLanguage( $this );
4661 $minutesMsg = wfMessage(
4662 $format['noabbrevs'] ?
'minutes' : 'minutes-abbrev' )->inLanguage( $this );
4663 $hoursMsg = wfMessage(
4664 $format['noabbrevs'] ?
'hours' : 'hours-abbrev' )->inLanguage( $this );
4665 $daysMsg = wfMessage(
4666 $format['noabbrevs'] ?
'days' : 'days-abbrev' )->inLanguage( $this );
4668 if ( round( $seconds * 10 ) < 100 ) {
4669 $s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
4670 $s = $secondsMsg->params( $s )->text();
4671 } elseif ( round( $seconds ) < 60 ) {
4672 $s = $this->formatNum( round( $seconds ) );
4673 $s = $secondsMsg->params( $s )->text();
4674 } elseif ( round( $seconds ) < 3600 ) {
4675 $minutes = floor( $seconds / 60 );
4676 $secondsPart = round( fmod( $seconds, 60 ) );
4677 if ( $secondsPart == 60 ) {
4681 $s = $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4683 $s .= $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4684 } elseif ( round( $seconds ) <= 2 * 86400 ) {
4685 $hours = floor( $seconds / 3600 );
4686 $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
4687 $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
4688 if ( $secondsPart == 60 ) {
4692 if ( $minutes == 60 ) {
4696 $s = $hoursMsg->params( $this->formatNum( $hours ) )->text();
4698 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4699 if ( !in_array( $format['avoid'], array( 'avoidseconds', 'avoidminutes' ) ) ) {
4700 $s .= ' ' . $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4703 $days = floor( $seconds / 86400 );
4704 if ( $format['avoid'] === 'avoidminutes' ) {
4705 $hours = round( ( $seconds - $days * 86400 ) / 3600 );
4706 if ( $hours == 24 ) {
4710 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4712 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4713 } elseif ( $format['avoid'] === 'avoidseconds' ) {
4714 $hours = floor( ( $seconds - $days * 86400 ) / 3600 );
4715 $minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
4716 if ( $minutes == 60 ) {
4720 if ( $hours == 24 ) {
4724 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4726 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4728 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4730 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4732 $s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
4739 * Format a bitrate for output, using an appropriate
4740 * unit (bps, kbps, Mbps, Gbps, Tbps, Pbps, Ebps, Zbps or Ybps) according to
4741 * the magnitude in question.
4743 * This use base 1000. For base 1024 use formatSize(), for another base
4744 * see formatComputingNumbers().
4749 function formatBitrate( $bps ) {
4750 return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" );
4754 * @param int $size Size of the unit
4755 * @param int $boundary Size boundary (1000, or 1024 in most cases)
4756 * @param string $messageKey Message key to be uesd
4759 function formatComputingNumbers( $size, $boundary, $messageKey ) {
4761 return str_replace( '$1', $this->formatNum( $size ),
4762 $this->getMessageFromDB( str_replace( '$1', '', $messageKey ) )
4765 $sizes = array( '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zeta', 'yotta' );
4768 $maxIndex = count( $sizes ) - 1;
4769 while ( $size >= $boundary && $index < $maxIndex ) {
4774 // For small sizes no decimal places necessary
4777 // For MB and bigger two decimal places are smarter
4780 $msg = str_replace( '$1', $sizes[$index], $messageKey );
4782 $size = round( $size, $round );
4783 $text = $this->getMessageFromDB( $msg );
4784 return str_replace( '$1', $this->formatNum( $size ), $text );
4788 * Format a size in bytes for output, using an appropriate
4789 * unit (B, KB, MB, GB, TB, PB, EB, ZB or YB) according to the magnitude in question
4791 * This method use base 1024. For base 1000 use formatBitrate(), for
4792 * another base see formatComputingNumbers()
4794 * @param int $size Size to format
4795 * @return string Plain text (not HTML)
4797 function formatSize( $size ) {
4798 return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" );
4802 * Make a list item, used by various special pages
4804 * @param string $page Page link
4805 * @param string $details HTML safe text between brackets
4806 * @param bool $oppositedm Add the direction mark opposite to your
4807 * language, to display text properly
4808 * @return HTML escaped string
4810 function specialList( $page, $details, $oppositedm = true ) {
4815 $dirmark = ( $oppositedm ?
$this->getDirMark( true ) : '' ) . $this->getDirMark();
4819 $this->msg( 'word-separator' )->escaped() .
4820 $this->msg( 'parentheses' )->rawParams( $details )->escaped();
4824 * Generate (prev x| next x) (20|50|100...) type links for paging
4826 * @param Title $title Title object to link
4827 * @param int $offset
4829 * @param array $query Optional URL query parameter string
4830 * @param bool $atend Optional param for specified if this is the last page
4833 public function viewPrevNext( Title
$title, $offset, $limit,
4834 array $query = array(), $atend = false
4836 // @todo FIXME: Why on earth this needs one message for the text and another one for tooltip?
4838 # Make 'previous' link
4839 $prev = wfMessage( 'prevn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4840 if ( $offset > 0 ) {
4841 $plink = $this->numLink( $title, max( $offset - $limit, 0 ), $limit,
4842 $query, $prev, 'prevn-title', 'mw-prevlink' );
4844 $plink = htmlspecialchars( $prev );
4848 $next = wfMessage( 'nextn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4850 $nlink = htmlspecialchars( $next );
4852 $nlink = $this->numLink( $title, $offset +
$limit, $limit,
4853 $query, $next, 'nextn-title', 'mw-nextlink' );
4856 # Make links to set number of items per page
4857 $numLinks = array();
4858 foreach ( array( 20, 50, 100, 250, 500 ) as $num ) {
4859 $numLinks[] = $this->numLink( $title, $offset, $num,
4860 $query, $this->formatNum( $num ), 'shown-title', 'mw-numlink' );
4863 return wfMessage( 'viewprevnext' )->inLanguage( $this )->title( $title
4864 )->rawParams( $plink, $nlink, $this->pipeList( $numLinks ) )->escaped();
4868 * Helper function for viewPrevNext() that generates links
4870 * @param Title $title Title object to link
4871 * @param int $offset
4873 * @param array $query Extra query parameters
4874 * @param string $link Text to use for the link; will be escaped
4875 * @param string $tooltipMsg Name of the message to use as tooltip
4876 * @param string $class Value of the "class" attribute of the link
4877 * @return string HTML fragment
4879 private function numLink( Title
$title, $offset, $limit, array $query, $link,
4882 $query = array( 'limit' => $limit, 'offset' => $offset ) +
$query;
4883 $tooltip = wfMessage( $tooltipMsg )->inLanguage( $this )->title( $title )
4884 ->numParams( $limit )->text();
4886 return Html
::element( 'a', array( 'href' => $title->getLocalURL( $query ),
4887 'title' => $tooltip, 'class' => $class ), $link );
4891 * Get the conversion rule title, if any.
4895 public function getConvRuleTitle() {
4896 return $this->mConverter
->getConvRuleTitle();
4900 * Get the compiled plural rules for the language
4902 * @return array Associative array with plural form, and plural rule as key-value pairs
4904 public function getCompiledPluralRules() {
4905 $pluralRules = self
::$dataCache->getItem( strtolower( $this->mCode
), 'compiledPluralRules' );
4906 $fallbacks = Language
::getFallbacksFor( $this->mCode
);
4907 if ( !$pluralRules ) {
4908 foreach ( $fallbacks as $fallbackCode ) {
4909 $pluralRules = self
::$dataCache->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
4910 if ( $pluralRules ) {
4915 return $pluralRules;
4919 * Get the plural rules for the language
4921 * @return array Associative array with plural form number and plural rule as key-value pairs
4923 public function getPluralRules() {
4924 $pluralRules = self
::$dataCache->getItem( strtolower( $this->mCode
), 'pluralRules' );
4925 $fallbacks = Language
::getFallbacksFor( $this->mCode
);
4926 if ( !$pluralRules ) {
4927 foreach ( $fallbacks as $fallbackCode ) {
4928 $pluralRules = self
::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRules' );
4929 if ( $pluralRules ) {
4934 return $pluralRules;
4938 * Get the plural rule types for the language
4940 * @return array Associative array with plural form number and plural rule type as key-value pairs
4942 public function getPluralRuleTypes() {
4943 $pluralRuleTypes = self
::$dataCache->getItem( strtolower( $this->mCode
), 'pluralRuleTypes' );
4944 $fallbacks = Language
::getFallbacksFor( $this->mCode
);
4945 if ( !$pluralRuleTypes ) {
4946 foreach ( $fallbacks as $fallbackCode ) {
4947 $pluralRuleTypes = self
::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRuleTypes' );
4948 if ( $pluralRuleTypes ) {
4953 return $pluralRuleTypes;
4957 * Find the index number of the plural rule appropriate for the given number
4958 * @param int $number
4959 * @return int The index number of the plural rule
4961 public function getPluralRuleIndexNumber( $number ) {
4962 $pluralRules = $this->getCompiledPluralRules();
4963 $form = CLDRPluralRuleEvaluator
::evaluateCompiled( $number, $pluralRules );
4968 * Find the plural rule type appropriate for the given number
4969 * For example, if the language is set to Arabic, getPluralType(5) should
4972 * @param int $number
4973 * @return string The name of the plural rule type, e.g. one, two, few, many
4975 public function getPluralRuleType( $number ) {
4976 $index = $this->getPluralRuleIndexNumber( $number );
4977 $pluralRuleTypes = $this->getPluralRuleTypes();
4978 if ( isset( $pluralRuleTypes[$index] ) ) {
4979 return $pluralRuleTypes[$index];