Merge "mediawiki.jqueryMsg: Use load urls instead of file urls for debug"
[lhc/web/wiklou.git] / languages / Language.php
1 <?php
2 /**
3 * Internationalisation code.
4 *
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.
9 *
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.
14 *
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
19 *
20 * @file
21 * @ingroup Language
22 */
23
24 /**
25 * @defgroup Language Language
26 */
27
28 if ( !defined( 'MEDIAWIKI' ) ) {
29 echo "This file is part of MediaWiki, it is not a valid entry point.\n";
30 exit( 1 );
31 }
32
33 if ( function_exists( 'mb_strtoupper' ) ) {
34 mb_internal_encoding( 'UTF-8' );
35 }
36
37 /**
38 * Internationalisation code
39 * @ingroup Language
40 */
41 class Language {
42 /**
43 * @var LanguageConverter
44 */
45 public $mConverter;
46
47 public $mVariants, $mCode, $mLoaded = false;
48 public $mMagicExtensions = array(), $mMagicHookDone = false;
49 private $mHtmlCode = null, $mParentLanguage = false;
50
51 public $dateFormatStrings = array();
52 public $mExtendedSpecialPageAliases;
53
54 protected $namespaceNames, $mNamespaceIds, $namespaceAliases;
55
56 /**
57 * ReplacementArray object caches
58 */
59 public $transformData = array();
60
61 /**
62 * @var LocalisationCache
63 */
64 static public $dataCache;
65
66 static public $mLangObjCache = array();
67
68 static public $mWeekdayMsgs = array(
69 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
70 'friday', 'saturday'
71 );
72
73 static public $mWeekdayAbbrevMsgs = array(
74 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
75 );
76
77 static public $mMonthMsgs = array(
78 'january', 'february', 'march', 'april', 'may_long', 'june',
79 'july', 'august', 'september', 'october', 'november',
80 'december'
81 );
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',
85 'december-gen'
86 );
87 static public $mMonthAbbrevMsgs = array(
88 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
89 'sep', 'oct', 'nov', 'dec'
90 );
91
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'
97 );
98
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'
105 );
106
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'
113 );
114
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'
120 );
121
122 /**
123 * @since 1.20
124 * @var array
125 */
126 static public $durationIntervals = array(
127 'millennia' => 31556952000,
128 'centuries' => 3155695200,
129 'decades' => 315569520,
130 'years' => 31556952, // 86400 * ( 365 + ( 24 * 3 + 25 ) / 400 )
131 'weeks' => 604800,
132 'days' => 86400,
133 'hours' => 3600,
134 'minutes' => 60,
135 'seconds' => 1,
136 );
137
138 /**
139 * Cache for language fallbacks.
140 * @see Language::getFallbacksIncludingSiteLanguage
141 * @since 1.21
142 * @var array
143 */
144 static private $fallbackLanguageCache = array();
145
146 /**
147 * Cache for language names
148 * @var MapCacheLRU|null
149 */
150 static private $languageNameCache;
151
152 /**
153 * Unicode directional formatting characters, for embedBidi()
154 */
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
158
159 /**
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.
164 *
165 * The form is '/(?:([strong ltr codepoint])|([strong rtl codepoint]))/u' .
166 *
167 * Generated by UnicodeJS (see tools/strongDir) from the UCD; see
168 * https://git.wikimedia.org/summary/unicodejs.git .
169 */
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
173
174 /**
175 * Get a cached or new language object for a given language code
176 * @param string $code
177 * @return Language
178 */
179 static function factory( $code ) {
180 global $wgDummyLanguageCodes, $wgLangObjCacheSize;
181
182 if ( isset( $wgDummyLanguageCodes[$code] ) ) {
183 $code = $wgDummyLanguageCodes[$code];
184 }
185
186 // get the language object to process
187 $langObj = isset( self::$mLangObjCache[$code] )
188 ? self::$mLangObjCache[$code]
189 : self::newFromCode( $code );
190
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 );
195
196 return $langObj;
197 }
198
199 /**
200 * Create a language object for a given language code
201 * @param string $code
202 * @throws MWException
203 * @return Language
204 */
205 protected static function newFromCode( $code ) {
206 // Protect against path traversal below
207 if ( !Language::isValidCode( $code )
208 || strcspn( $code, ":/\\\000" ) !== strlen( $code )
209 ) {
210 throw new MWException( "Invalid language code \"$code\"" );
211 }
212
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 );
218 return $lang;
219 }
220
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 ) ) {
225 $lang = new $class;
226 return $lang;
227 }
228
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'" );
234 }
235
236 $class = self::classFromCode( $fallbackCode );
237 self::preloadLanguageClass( $class );
238 if ( class_exists( $class ) ) {
239 $lang = Language::newFromCode( $fallbackCode );
240 $lang->setCode( $code );
241 return $lang;
242 }
243 }
244
245 throw new MWException( "Invalid fallback sequence for language '$code'" );
246 }
247
248 /**
249 * Checks whether any localisation is available for that language tag
250 * in MediaWiki (MessagesXx.php exists).
251 *
252 * @param string $code Language tag (in lower case)
253 * @return bool Whether language is supported
254 * @since 1.21
255 */
256 public static function isSupportedLanguage( $code ) {
257 return self::isValidBuiltInCode( $code )
258 && ( is_readable( self::getMessagesFileName( $code ) )
259 || is_readable( self::getJsonMessagesFileName( $code ) )
260 );
261 }
262
263 /**
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.
268 *
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
271 *
272 * @param string $code
273 * @param bool $lenient Whether to allow '_' as separator. The default is only '-'.
274 *
275 * @return bool
276 * @since 1.21
277 */
278 public static function isWellFormedLanguageTag( $code, $lenient = false ) {
279 $alpha = '[a-z]';
280 $digit = '[0-9]';
281 $alphanum = '[a-z0-9]';
282 $x = 'x'; # private use singleton
283 $singleton = '[a-wy-z]'; # other singleton
284 $s = $lenient ? '[-_]' : '-';
285
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})+";
292
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
298
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";
304
305 $variantList = "$variant(?:$s$variant)*";
306 $extensionList = "$extension(?:$s$extension)*";
307
308 $langtag = "(?:($language)"
309 . "(?:$s$script)?"
310 . "(?:$s$region)?"
311 . "(?:$s$variantList)?"
312 . "(?:$s$extensionList)?"
313 . "(?:$s$privateUse)?)";
314
315 # The final breakdown, with capturing groups for each of these components
316 # The variants, extensions, grandfathered, and private-use may have interior '-'
317
318 $root = "^(?:$langtag|$privateUse|$grandfathered)$";
319
320 return (bool)preg_match( "/$root/", strtolower( $code ) );
321 }
322
323 /**
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.
327 *
328 * @param string $code
329 *
330 * @return bool
331 */
332 public static function isValidCode( $code ) {
333 static $cache = array();
334 if ( isset( $cache[$code] ) ) {
335 return $cache[$code];
336 }
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
341 $cache[$code] =
342 strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
343 && !preg_match( MediaWikiTitleCodec::getTitleInvalidRegex(), $code );
344
345 return $cache[$code];
346 }
347
348 /**
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.
351 *
352 * @param string $code
353 *
354 * @throws MWException
355 * @since 1.18
356 * @return bool
357 */
358 public static function isValidBuiltInCode( $code ) {
359
360 if ( !is_string( $code ) ) {
361 if ( is_object( $code ) ) {
362 $addmsg = " of class " . get_class( $code );
363 } else {
364 $addmsg = '';
365 }
366 $type = gettype( $code );
367 throw new MWException( __METHOD__ . " must be passed a string, $type given$addmsg" );
368 }
369
370 return (bool)preg_match( '/^[a-z0-9-]{2,}$/', $code );
371 }
372
373 /**
374 * Returns true if a language code is an IETF tag known to MediaWiki.
375 *
376 * @param string $tag
377 *
378 * @since 1.21
379 * @return bool
380 */
381 public static function isKnownLanguageTag( $tag ) {
382 static $coreLanguageNames;
383
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 ) ) {
387 return false;
388 }
389
390 if ( $coreLanguageNames === null ) {
391 global $IP;
392 include "$IP/languages/Names.php";
393 }
394
395 if ( isset( $coreLanguageNames[$tag] )
396 || self::fetchLanguageName( $tag, $tag ) !== ''
397 ) {
398 return true;
399 }
400
401 return false;
402 }
403
404 /**
405 * @param string $code
406 * @return string Name of the language class
407 */
408 public static function classFromCode( $code ) {
409 if ( $code == 'en' ) {
410 return 'Language';
411 } else {
412 return 'Language' . str_replace( '-', '_', ucfirst( $code ) );
413 }
414 }
415
416 /**
417 * Includes language class files
418 *
419 * @param string $class Name of the language class
420 */
421 public static function preloadLanguageClass( $class ) {
422 global $IP;
423
424 if ( $class === 'Language' ) {
425 return;
426 }
427
428 if ( file_exists( "$IP/languages/classes/$class.php" ) ) {
429 include_once "$IP/languages/classes/$class.php";
430 }
431 }
432
433 /**
434 * Get the LocalisationCache instance
435 *
436 * @return LocalisationCache
437 */
438 public static function getLocalisationCache() {
439 if ( is_null( self::$dataCache ) ) {
440 global $wgLocalisationCacheConf;
441 $class = $wgLocalisationCacheConf['class'];
442 self::$dataCache = new $class( $wgLocalisationCacheConf );
443 }
444 return self::$dataCache;
445 }
446
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' ) {
451 $this->mCode = 'en';
452 } else {
453 $this->mCode = str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) );
454 }
455 self::getLocalisationCache();
456 }
457
458 /**
459 * Reduce memory usage
460 */
461 function __destruct() {
462 foreach ( $this as $name => $value ) {
463 unset( $this->$name );
464 }
465 }
466
467 /**
468 * Hook which will be called if this is the content language.
469 * Descendants can use this to register hook functions or modify globals
470 */
471 function initContLang() {
472 }
473
474 /**
475 * @return array
476 * @since 1.19
477 */
478 function getFallbackLanguages() {
479 return self::getFallbacksFor( $this->mCode );
480 }
481
482 /**
483 * Exports $wgBookstoreListEn
484 * @return array
485 */
486 function getBookstoreList() {
487 return self::$dataCache->getItem( $this->mCode, 'bookstoreList' );
488 }
489
490 /**
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.
493 *
494 * @return array
495 */
496 public function getNamespaces() {
497 if ( is_null( $this->namespaceNames ) ) {
498 global $wgMetaNamespace, $wgMetaNamespaceTalk, $wgExtraNamespaces;
499
500 $this->namespaceNames = self::$dataCache->getItem( $this->mCode, 'namespaceNames' );
501 $validNamespaces = MWNamespace::getCanonicalNamespaces();
502
503 $this->namespaceNames = $wgExtraNamespaces + $this->namespaceNames + $validNamespaces;
504
505 $this->namespaceNames[NS_PROJECT] = $wgMetaNamespace;
506 if ( $wgMetaNamespaceTalk ) {
507 $this->namespaceNames[NS_PROJECT_TALK] = $wgMetaNamespaceTalk;
508 } else {
509 $talk = $this->namespaceNames[NS_PROJECT_TALK];
510 $this->namespaceNames[NS_PROJECT_TALK] =
511 $this->fixVariableInNamespace( $talk );
512 }
513
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] );
518 }
519 }
520
521 # The above mixing may leave namespaces out of canonical order.
522 # Re-order by namespace ID number...
523 ksort( $this->namespaceNames );
524
525 Hooks::run( 'LanguageGetNamespaces', array( &$this->namespaceNames ) );
526 }
527
528 return $this->namespaceNames;
529 }
530
531 /**
532 * Arbitrarily set all of the namespace names at once. Mainly used for testing
533 * @param array $namespaces Array of namespaces (id => name)
534 */
535 public function setNamespaces( array $namespaces ) {
536 $this->namespaceNames = $namespaces;
537 $this->mNamespaceIds = null;
538 }
539
540 /**
541 * Resets all of the namespace caches. Mainly used for testing
542 */
543 public function resetNamespaces() {
544 $this->namespaceNames = null;
545 $this->mNamespaceIds = null;
546 $this->namespaceAliases = null;
547 }
548
549 /**
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.
554 *
555 * @return array
556 */
557 function getFormattedNamespaces() {
558 $ns = $this->getNamespaces();
559 foreach ( $ns as $k => $v ) {
560 $ns[$k] = strtr( $v, '_', ' ' );
561 }
562 return $ns;
563 }
564
565 /**
566 * Get a namespace value by key
567 * <code>
568 * $mw_ns = $wgContLang->getNsText( NS_MEDIAWIKI );
569 * echo $mw_ns; // prints 'MediaWiki'
570 * </code>
571 *
572 * @param int $index The array key of the namespace to return
573 * @return string|bool String if the namespace value exists, otherwise false
574 */
575 function getNsText( $index ) {
576 $ns = $this->getNamespaces();
577
578 return isset( $ns[$index] ) ? $ns[$index] : false;
579 }
580
581 /**
582 * A convenience function that returns the same thing as
583 * getNsText() except with '_' changed to ' ', useful for
584 * producing output.
585 *
586 * <code>
587 * $mw_ns = $wgContLang->getFormattedNsText( NS_MEDIAWIKI_TALK );
588 * echo $mw_ns; // prints 'MediaWiki talk'
589 * </code>
590 *
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)
593 */
594 function getFormattedNsText( $index ) {
595 $ns = $this->getNsText( $index );
596
597 return strtr( $ns, '_', ' ' );
598 }
599
600 /**
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... )
605 * @return string
606 * @since 1.18
607 */
608 function getGenderNsText( $index, $gender ) {
609 global $wgExtraGenderNamespaces;
610
611 $ns = $wgExtraGenderNamespaces +
612 (array)self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
613
614 return isset( $ns[$index][$gender] ) ? $ns[$index][$gender] : $this->getNsText( $index );
615 }
616
617 /**
618 * Whether this language uses gender-dependent namespace aliases.
619 * See https://www.mediawiki.org/wiki/Manual:$wgExtraGenderNamespaces
620 * @return bool
621 * @since 1.18
622 */
623 function needsGenderDistinction() {
624 global $wgExtraGenderNamespaces, $wgExtraNamespaces;
625 if ( count( $wgExtraGenderNamespaces ) > 0 ) {
626 // $wgExtraGenderNamespaces overrides everything
627 return true;
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
631 return false;
632 } else {
633 // Check what is in i18n files
634 $aliases = self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
635 return count( $aliases ) > 0;
636 }
637 }
638
639 /**
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.
643 *
644 * @param string $text
645 * @return int|bool An integer if $text is a valid value otherwise false
646 */
647 function getLocalNsIndex( $text ) {
648 $lctext = $this->lc( $text );
649 $ids = $this->getNamespaceIds();
650 return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
651 }
652
653 /**
654 * @return array
655 */
656 function getNamespaceAliases() {
657 if ( is_null( $this->namespaceAliases ) ) {
658 $aliases = self::$dataCache->getItem( $this->mCode, 'namespaceAliases' );
659 if ( !$aliases ) {
660 $aliases = array();
661 } else {
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;
667 }
668 }
669 }
670
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;
677 }
678 }
679
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 ) {
684 continue;
685 }
686 foreach ( $this->getNamespaces() as $ns => $_ ) {
687 $convertedNames[$this->getConverter()->convertNamespace( $ns, $variant )] = $ns;
688 }
689 }
690
691 $this->namespaceAliases = $aliases + $convertedNames;
692 }
693
694 return $this->namespaceAliases;
695 }
696
697 /**
698 * @return array
699 */
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;
710 }
711 foreach ( $this->getNamespaceAliases() as $name => $index ) {
712 $this->mNamespaceIds[$this->lc( $name )] = $index;
713 }
714 if ( $wgNamespaceAliases ) {
715 foreach ( $wgNamespaceAliases as $name => $index ) {
716 $this->mNamespaceIds[$this->lc( $name )] = $index;
717 }
718 }
719 }
720 return $this->mNamespaceIds;
721 }
722
723 /**
724 * Get a namespace key by value, case insensitive. Canonical namespace
725 * names override custom ones defined for the current language.
726 *
727 * @param string $text
728 * @return int|bool An integer if $text is a valid value otherwise false
729 */
730 function getNsIndex( $text ) {
731 $lctext = $this->lc( $text );
732 $ns = MWNamespace::getCanonicalIndex( $lctext );
733 if ( $ns !== null ) {
734 return $ns;
735 }
736 $ids = $this->getNamespaceIds();
737 return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
738 }
739
740 /**
741 * short names for language variants used for language conversion links.
742 *
743 * @param string $code
744 * @param bool $usemsg Use the "variantname-xyz" message if it exists
745 * @return string
746 */
747 function getVariantname( $code, $usemsg = true ) {
748 $msg = "variantname-$code";
749 if ( $usemsg && wfMessage( $msg )->exists() ) {
750 return $this->getMessageFromDB( $msg );
751 }
752 $name = self::fetchLanguageName( $code );
753 if ( $name ) {
754 return $name; # if it's defined as a language name, show that
755 } else {
756 # otherwise, output the language code
757 return $code;
758 }
759 }
760
761 /**
762 * @deprecated since 1.24, doesn't handle conflicting aliases. Use
763 * SpecialPageFactory::getLocalNameFor instead.
764 * @param string $name
765 * @return string
766 */
767 function specialPage( $name ) {
768 $aliases = $this->getSpecialPageAliases();
769 if ( isset( $aliases[$name][0] ) ) {
770 $name = $aliases[$name][0];
771 }
772 return $this->getNsText( NS_SPECIAL ) . ':' . $name;
773 }
774
775 /**
776 * @return array
777 */
778 function getDatePreferences() {
779 return self::$dataCache->getItem( $this->mCode, 'datePreferences' );
780 }
781
782 /**
783 * @return array
784 */
785 function getDateFormats() {
786 return self::$dataCache->getItem( $this->mCode, 'dateFormats' );
787 }
788
789 /**
790 * @return array|string
791 */
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';
797 } else {
798 return $df;
799 }
800 }
801
802 /**
803 * @return array
804 */
805 function getDatePreferenceMigrationMap() {
806 return self::$dataCache->getItem( $this->mCode, 'datePreferenceMigrationMap' );
807 }
808
809 /**
810 * @param string $image
811 * @return array|null
812 */
813 function getImageFile( $image ) {
814 return self::$dataCache->getSubitem( $this->mCode, 'imageFiles', $image );
815 }
816
817 /**
818 * @return array
819 * @since 1.24
820 */
821 function getImageFiles() {
822 return self::$dataCache->getItem( $this->mCode, 'imageFiles' );
823 }
824
825 /**
826 * @return array
827 */
828 function getExtraUserToggles() {
829 return (array)self::$dataCache->getItem( $this->mCode, 'extraUserToggles' );
830 }
831
832 /**
833 * @param string $tog
834 * @return string
835 */
836 function getUserToggle( $tog ) {
837 return $this->getMessageFromDB( "tog-$tog" );
838 }
839
840 /**
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
844 *
845 * @param bool $customisedOnly
846 *
847 * @return array
848 * @deprecated since 1.20, use fetchLanguageNames()
849 */
850 public static function getLanguageNames( $customisedOnly = false ) {
851 return self::fetchLanguageNames( null, $customisedOnly ? 'mwfile' : 'mw' );
852 }
853
854 /**
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
860 * @since 1.18.0
861 * @deprecated since 1.20, use fetchLanguageNames()
862 */
863 public static function getTranslatedLanguageNames( $code ) {
864 return self::fetchLanguageNames( $code, 'all' );
865 }
866
867 /**
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
876 * @since 1.20
877 */
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 );
883 }
884 if ( self::$languageNameCache->has( $cacheKey ) ) {
885 $ret = self::$languageNameCache->get( $cacheKey );
886 } else {
887 $ret = self::fetchLanguageNamesUncached( $inLanguage, $include );
888 self::$languageNameCache->set( $cacheKey, $ret );
889 }
890 return $ret;
891 }
892
893 /**
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
902 */
903 private static function fetchLanguageNamesUncached( $inLanguage = null, $include = 'mw' ) {
904 global $wgExtraLanguageNames;
905 static $coreLanguageNames;
906
907 if ( $coreLanguageNames === null ) {
908 global $IP;
909 include "$IP/languages/Names.php";
910 }
911
912 // If passed an invalid language code to use, fallback to en
913 if ( $inLanguage !== null && !Language::isValidCode( $inLanguage ) ) {
914 $inLanguage = 'en';
915 }
916
917 $names = array();
918
919 if ( $inLanguage ) {
920 # TODO: also include when $inLanguage is null, when this code is more efficient
921 Hooks::run( 'LanguageGetTranslatedLanguageNames', array( &$names, $inLanguage ) );
922 }
923
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;
930 }
931 }
932
933 if ( $include === 'all' ) {
934 ksort( $names );
935 return $names;
936 }
937
938 $returnMw = array();
939 $coreCodes = array_keys( $mwNames );
940 foreach ( $coreCodes as $coreCode ) {
941 $returnMw[$coreCode] = $names[$coreCode];
942 }
943
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 ) )
951 ) {
952 $namesMwFile[$code] = $names[$code];
953 }
954 }
955
956 ksort( $namesMwFile );
957 return $namesMwFile;
958 }
959
960 ksort( $returnMw );
961 # 'mw' option; default if it's not one of the other two options (all/mwfile)
962 return $returnMw;
963 }
964
965 /**
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
970 * @since 1.20
971 */
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];
976 }
977
978 /**
979 * Get a message from the MediaWiki namespace.
980 *
981 * @param string $msg Message name
982 * @return string
983 */
984 function getMessageFromDB( $msg ) {
985 return $this->msg( $msg )->text();
986 }
987
988 /**
989 * Get message object in this language. Only for use inside this class.
990 *
991 * @param string $msg Message name
992 * @return Message
993 */
994 protected function msg( $msg ) {
995 return wfMessage( $msg )->inLanguage( $this );
996 }
997
998 /**
999 * Get the native language name of $code.
1000 * Only if defined in MediaWiki, no other data like CLDR.
1001 * @param string $code
1002 * @return string
1003 * @deprecated since 1.20, use fetchLanguageName()
1004 */
1005 function getLanguageName( $code ) {
1006 return self::fetchLanguageName( $code );
1007 }
1008
1009 /**
1010 * @param string $key
1011 * @return string
1012 */
1013 function getMonthName( $key ) {
1014 return $this->getMessageFromDB( self::$mMonthMsgs[$key - 1] );
1015 }
1016
1017 /**
1018 * @return array
1019 */
1020 function getMonthNamesArray() {
1021 $monthNames = array( '' );
1022 for ( $i = 1; $i < 13; $i++ ) {
1023 $monthNames[] = $this->getMonthName( $i );
1024 }
1025 return $monthNames;
1026 }
1027
1028 /**
1029 * @param string $key
1030 * @return string
1031 */
1032 function getMonthNameGen( $key ) {
1033 return $this->getMessageFromDB( self::$mMonthGenMsgs[$key - 1] );
1034 }
1035
1036 /**
1037 * @param string $key
1038 * @return string
1039 */
1040 function getMonthAbbreviation( $key ) {
1041 return $this->getMessageFromDB( self::$mMonthAbbrevMsgs[$key - 1] );
1042 }
1043
1044 /**
1045 * @return array
1046 */
1047 function getMonthAbbreviationsArray() {
1048 $monthNames = array( '' );
1049 for ( $i = 1; $i < 13; $i++ ) {
1050 $monthNames[] = $this->getMonthAbbreviation( $i );
1051 }
1052 return $monthNames;
1053 }
1054
1055 /**
1056 * @param string $key
1057 * @return string
1058 */
1059 function getWeekdayName( $key ) {
1060 return $this->getMessageFromDB( self::$mWeekdayMsgs[$key - 1] );
1061 }
1062
1063 /**
1064 * @param string $key
1065 * @return string
1066 */
1067 function getWeekdayAbbreviation( $key ) {
1068 return $this->getMessageFromDB( self::$mWeekdayAbbrevMsgs[$key - 1] );
1069 }
1070
1071 /**
1072 * @param string $key
1073 * @return string
1074 */
1075 function getIranianCalendarMonthName( $key ) {
1076 return $this->getMessageFromDB( self::$mIranianCalendarMonthMsgs[$key - 1] );
1077 }
1078
1079 /**
1080 * @param string $key
1081 * @return string
1082 */
1083 function getHebrewCalendarMonthName( $key ) {
1084 return $this->getMessageFromDB( self::$mHebrewCalendarMonthMsgs[$key - 1] );
1085 }
1086
1087 /**
1088 * @param string $key
1089 * @return string
1090 */
1091 function getHebrewCalendarMonthNameGen( $key ) {
1092 return $this->getMessageFromDB( self::$mHebrewCalendarMonthGenMsgs[$key - 1] );
1093 }
1094
1095 /**
1096 * @param string $key
1097 * @return string
1098 */
1099 function getHijriCalendarMonthName( $key ) {
1100 return $this->getMessageFromDB( self::$mHijriCalendarMonthMsgs[$key - 1] );
1101 }
1102
1103 /**
1104 * Pass through result from $dateTimeObj->format()
1105 * @param DateTime|bool|null &$dateTimeObj
1106 * @param string $ts
1107 * @param DateTimeZone|bool|null $zone
1108 * @param string $code
1109 * @return string
1110 */
1111 private static function dateTimeObjFormat( &$dateTimeObj, $ts, $zone, $code ) {
1112 if ( !$dateTimeObj ) {
1113 $dateTimeObj = DateTime::createFromFormat(
1114 'YmdHis', $ts, $zone ?: new DateTimeZone( 'UTC' )
1115 );
1116 }
1117 return $dateTimeObj->format( $code );
1118 }
1119
1120 /**
1121 * This is a workalike of PHP's date() function, but with better
1122 * internationalisation, a reduced set of format characters, and a better
1123 * escaping format.
1124 *
1125 * Supported format characters are dDjlNwzWFmMntLoYyaAgGhHiscrUeIOPTZ. See
1126 * the PHP manual for definitions. There are a number of extensions, which
1127 * start with "x":
1128 *
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
1133 * xx Literal x
1134 * xg Genitive month name
1135 *
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
1141 *
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
1148 *
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
1153 *
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
1161 *
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:
1165 *
1166 * "The month is" F => The month is January
1167 * i's" => 20'11"
1168 *
1169 * Backslash escaping is also supported.
1170 *
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.
1174 *
1175 * @param string $format
1176 * @param string $ts 14-character timestamp
1177 * YYYYMMDDHHMMSS
1178 * 01234567890123
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?
1183 *
1184 * @throws MWException
1185 * @return string
1186 */
1187 function sprintfDate( $format, $ts, DateTimeZone $zone = null, &$ttl = null ) {
1188 $s = '';
1189 $raw = false;
1190 $roman = false;
1191 $hebrewNum = false;
1192 $dateTimeObj = false;
1193 $rawToggle = false;
1194 $iranian = false;
1195 $hebrew = false;
1196 $hijri = false;
1197 $thai = false;
1198 $minguo = false;
1199 $tenno = false;
1200
1201 $usedSecond = false;
1202 $usedMinute = false;
1203 $usedHour = false;
1204 $usedAMPM = false;
1205 $usedDay = false;
1206 $usedWeek = false;
1207 $usedMonth = false;
1208 $usedYear = false;
1209 $usedISOYear = false;
1210 $usedIsLeapYear = false;
1211
1212 $usedHebrewMonth = false;
1213 $usedIranianMonth = false;
1214 $usedHijriMonth = false;
1215 $usedHebrewYear = false;
1216 $usedIranianYear = false;
1217 $usedHijriYear = false;
1218 $usedTennoYear = false;
1219
1220 if ( strlen( $ts ) !== 14 ) {
1221 throw new MWException( __METHOD__ . ": The timestamp $ts should have 14 characters" );
1222 }
1223
1224 if ( !ctype_digit( $ts ) ) {
1225 throw new MWException( __METHOD__ . ": The timestamp $ts should be a number" );
1226 }
1227
1228 $formatLength = strlen( $format );
1229 for ( $p = 0; $p < $formatLength; $p++ ) {
1230 $num = false;
1231 $code = $format[$p];
1232 if ( $code == 'x' && $p < $formatLength - 1 ) {
1233 $code .= $format[++$p];
1234 }
1235
1236 if ( ( $code === 'xi'
1237 || $code === 'xj'
1238 || $code === 'xk'
1239 || $code === 'xm'
1240 || $code === 'xo'
1241 || $code === 'xt' )
1242 && $p < $formatLength - 1 ) {
1243 $code .= $format[++$p];
1244 }
1245
1246 switch ( $code ) {
1247 case 'xx':
1248 $s .= 'x';
1249 break;
1250 case 'xn':
1251 $raw = true;
1252 break;
1253 case 'xN':
1254 $rawToggle = !$rawToggle;
1255 break;
1256 case 'xr':
1257 $roman = true;
1258 break;
1259 case 'xh':
1260 $hebrewNum = true;
1261 break;
1262 case 'xg':
1263 $usedMonth = true;
1264 $s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) );
1265 break;
1266 case 'xjx':
1267 $usedHebrewMonth = true;
1268 if ( !$hebrew ) {
1269 $hebrew = self::tsToHebrew( $ts );
1270 }
1271 $s .= $this->getHebrewCalendarMonthNameGen( $hebrew[1] );
1272 break;
1273 case 'd':
1274 $usedDay = true;
1275 $num = substr( $ts, 6, 2 );
1276 break;
1277 case 'D':
1278 $usedDay = true;
1279 $s .= $this->getWeekdayAbbreviation(
1280 Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1
1281 );
1282 break;
1283 case 'j':
1284 $usedDay = true;
1285 $num = intval( substr( $ts, 6, 2 ) );
1286 break;
1287 case 'xij':
1288 $usedDay = true;
1289 if ( !$iranian ) {
1290 $iranian = self::tsToIranian( $ts );
1291 }
1292 $num = $iranian[2];
1293 break;
1294 case 'xmj':
1295 $usedDay = true;
1296 if ( !$hijri ) {
1297 $hijri = self::tsToHijri( $ts );
1298 }
1299 $num = $hijri[2];
1300 break;
1301 case 'xjj':
1302 $usedDay = true;
1303 if ( !$hebrew ) {
1304 $hebrew = self::tsToHebrew( $ts );
1305 }
1306 $num = $hebrew[2];
1307 break;
1308 case 'l':
1309 $usedDay = true;
1310 $s .= $this->getWeekdayName(
1311 Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1
1312 );
1313 break;
1314 case 'F':
1315 $usedMonth = true;
1316 $s .= $this->getMonthName( substr( $ts, 4, 2 ) );
1317 break;
1318 case 'xiF':
1319 $usedIranianMonth = true;
1320 if ( !$iranian ) {
1321 $iranian = self::tsToIranian( $ts );
1322 }
1323 $s .= $this->getIranianCalendarMonthName( $iranian[1] );
1324 break;
1325 case 'xmF':
1326 $usedHijriMonth = true;
1327 if ( !$hijri ) {
1328 $hijri = self::tsToHijri( $ts );
1329 }
1330 $s .= $this->getHijriCalendarMonthName( $hijri[1] );
1331 break;
1332 case 'xjF':
1333 $usedHebrewMonth = true;
1334 if ( !$hebrew ) {
1335 $hebrew = self::tsToHebrew( $ts );
1336 }
1337 $s .= $this->getHebrewCalendarMonthName( $hebrew[1] );
1338 break;
1339 case 'm':
1340 $usedMonth = true;
1341 $num = substr( $ts, 4, 2 );
1342 break;
1343 case 'M':
1344 $usedMonth = true;
1345 $s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) );
1346 break;
1347 case 'n':
1348 $usedMonth = true;
1349 $num = intval( substr( $ts, 4, 2 ) );
1350 break;
1351 case 'xin':
1352 $usedIranianMonth = true;
1353 if ( !$iranian ) {
1354 $iranian = self::tsToIranian( $ts );
1355 }
1356 $num = $iranian[1];
1357 break;
1358 case 'xmn':
1359 $usedHijriMonth = true;
1360 if ( !$hijri ) {
1361 $hijri = self::tsToHijri( $ts );
1362 }
1363 $num = $hijri[1];
1364 break;
1365 case 'xjn':
1366 $usedHebrewMonth = true;
1367 if ( !$hebrew ) {
1368 $hebrew = self::tsToHebrew( $ts );
1369 }
1370 $num = $hebrew[1];
1371 break;
1372 case 'xjt':
1373 $usedHebrewMonth = true;
1374 if ( !$hebrew ) {
1375 $hebrew = self::tsToHebrew( $ts );
1376 }
1377 $num = $hebrew[3];
1378 break;
1379 case 'Y':
1380 $usedYear = true;
1381 $num = substr( $ts, 0, 4 );
1382 break;
1383 case 'xiY':
1384 $usedIranianYear = true;
1385 if ( !$iranian ) {
1386 $iranian = self::tsToIranian( $ts );
1387 }
1388 $num = $iranian[0];
1389 break;
1390 case 'xmY':
1391 $usedHijriYear = true;
1392 if ( !$hijri ) {
1393 $hijri = self::tsToHijri( $ts );
1394 }
1395 $num = $hijri[0];
1396 break;
1397 case 'xjY':
1398 $usedHebrewYear = true;
1399 if ( !$hebrew ) {
1400 $hebrew = self::tsToHebrew( $ts );
1401 }
1402 $num = $hebrew[0];
1403 break;
1404 case 'xkY':
1405 $usedYear = true;
1406 if ( !$thai ) {
1407 $thai = self::tsToYear( $ts, 'thai' );
1408 }
1409 $num = $thai[0];
1410 break;
1411 case 'xoY':
1412 $usedYear = true;
1413 if ( !$minguo ) {
1414 $minguo = self::tsToYear( $ts, 'minguo' );
1415 }
1416 $num = $minguo[0];
1417 break;
1418 case 'xtY':
1419 $usedTennoYear = true;
1420 if ( !$tenno ) {
1421 $tenno = self::tsToYear( $ts, 'tenno' );
1422 }
1423 $num = $tenno[0];
1424 break;
1425 case 'y':
1426 $usedYear = true;
1427 $num = substr( $ts, 2, 2 );
1428 break;
1429 case 'xiy':
1430 $usedIranianYear = true;
1431 if ( !$iranian ) {
1432 $iranian = self::tsToIranian( $ts );
1433 }
1434 $num = substr( $iranian[0], -2 );
1435 break;
1436 case 'a':
1437 $usedAMPM = true;
1438 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm';
1439 break;
1440 case 'A':
1441 $usedAMPM = true;
1442 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'AM' : 'PM';
1443 break;
1444 case 'g':
1445 $usedHour = true;
1446 $h = substr( $ts, 8, 2 );
1447 $num = $h % 12 ? $h % 12 : 12;
1448 break;
1449 case 'G':
1450 $usedHour = true;
1451 $num = intval( substr( $ts, 8, 2 ) );
1452 break;
1453 case 'h':
1454 $usedHour = true;
1455 $h = substr( $ts, 8, 2 );
1456 $num = sprintf( '%02d', $h % 12 ? $h % 12 : 12 );
1457 break;
1458 case 'H':
1459 $usedHour = true;
1460 $num = substr( $ts, 8, 2 );
1461 break;
1462 case 'i':
1463 $usedMinute = true;
1464 $num = substr( $ts, 10, 2 );
1465 break;
1466 case 's':
1467 $usedSecond = true;
1468 $num = substr( $ts, 12, 2 );
1469 break;
1470 case 'c':
1471 case 'r':
1472 $usedSecond = true;
1473 // fall through
1474 case 'e':
1475 case 'O':
1476 case 'P':
1477 case 'T':
1478 $s .= Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1479 break;
1480 case 'w':
1481 case 'N':
1482 case 'z':
1483 $usedDay = true;
1484 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1485 break;
1486 case 'W':
1487 $usedWeek = true;
1488 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1489 break;
1490 case 't':
1491 $usedMonth = true;
1492 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1493 break;
1494 case 'L':
1495 $usedIsLeapYear = true;
1496 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1497 break;
1498 case 'o':
1499 $usedISOYear = true;
1500 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1501 break;
1502 case 'U':
1503 $usedSecond = true;
1504 // fall through
1505 case 'I':
1506 case 'Z':
1507 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1508 break;
1509 case '\\':
1510 # Backslash escaping
1511 if ( $p < $formatLength - 1 ) {
1512 $s .= $format[++$p];
1513 } else {
1514 $s .= '\\';
1515 }
1516 break;
1517 case '"':
1518 # Quoted literal
1519 if ( $p < $formatLength - 1 ) {
1520 $endQuote = strpos( $format, '"', $p + 1 );
1521 if ( $endQuote === false ) {
1522 # No terminating quote, assume literal "
1523 $s .= '"';
1524 } else {
1525 $s .= substr( $format, $p + 1, $endQuote - $p - 1 );
1526 $p = $endQuote;
1527 }
1528 } else {
1529 # Quote at end of string, assume literal "
1530 $s .= '"';
1531 }
1532 break;
1533 default:
1534 $s .= $format[$p];
1535 }
1536 if ( $num !== false ) {
1537 if ( $rawToggle || $raw ) {
1538 $s .= $num;
1539 $raw = false;
1540 } elseif ( $roman ) {
1541 $s .= Language::romanNumeral( $num );
1542 $roman = false;
1543 } elseif ( $hebrewNum ) {
1544 $s .= self::hebrewNumeral( $num );
1545 $hebrewNum = false;
1546 } else {
1547 $s .= $this->formatNum( $num, true );
1548 }
1549 }
1550 }
1551
1552 if ( $usedSecond ) {
1553 $ttl = 1;
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 );
1561 } elseif (
1562 $usedDay ||
1563 $usedHebrewMonth ||
1564 $usedIranianMonth ||
1565 $usedHijriMonth ||
1566 $usedHebrewYear ||
1567 $usedIranianYear ||
1568 $usedHijriYear ||
1569 $usedTennoYear
1570 ) {
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 );
1575 } else {
1576 $possibleTtls = array();
1577 $timeRemainingInDay = 86400 - substr( $ts, 8, 2 ) * 3600 -
1578 substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1579 if ( $usedWeek ) {
1580 $possibleTtls[] =
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(
1587 'Ymd',
1588 substr( $ts, 0, 4 ) . '1228',
1589 $zone ?: new DateTimeZone( 'UTC' )
1590 )->format( 'W' );
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;
1597 }
1598
1599 if ( $usedMonth ) {
1600 $possibleTtls[] =
1601 ( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 't' ) -
1602 substr( $ts, 6, 2 ) ) * 86400
1603 + $timeRemainingInDay;
1604 } elseif ( $usedYear ) {
1605 $possibleTtls[] =
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;
1615 $mod = $year % 4;
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;
1622 } else {
1623 $possibleTtls[] = ( $nextCandidate - $year + 3 ) * 365 * 86400 +
1624 $timeRemainingInYear;
1625 }
1626 } else {
1627 // this is a leap year, so the next year isn't
1628 $possibleTtls[] = $timeRemainingInYear;
1629 }
1630 }
1631
1632 if ( $possibleTtls ) {
1633 $ttl = min( $possibleTtls );
1634 }
1635 }
1636
1637 return $s;
1638 }
1639
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 );
1642
1643 /**
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.
1648 *
1649 * Link: http://www.farsiweb.info/jalali/jalali.c
1650 *
1651 * @param string $ts
1652 *
1653 * @return string
1654 */
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;
1659
1660 # Days passed from the beginning (including leap years)
1661 $gDayNo = 365 * $gy
1662 + floor( ( $gy + 3 ) / 4 )
1663 - floor( ( $gy + 99 ) / 100 )
1664 + floor( ( $gy + 399 ) / 400 );
1665
1666 // Add days of the past months of this year
1667 for ( $i = 0; $i < $gm; $i++ ) {
1668 $gDayNo += self::$GREG_DAYS[$i];
1669 }
1670
1671 // Leap years
1672 if ( $gm > 1 && ( ( $gy % 4 === 0 && $gy % 100 !== 0 || ( $gy % 400 == 0 ) ) ) ) {
1673 $gDayNo++;
1674 }
1675
1676 // Days passed in current month
1677 $gDayNo += (int)$gd;
1678
1679 $jDayNo = $gDayNo - 79;
1680
1681 $jNp = floor( $jDayNo / 12053 );
1682 $jDayNo %= 12053;
1683
1684 $jy = 979 + 33 * $jNp + 4 * floor( $jDayNo / 1461 );
1685 $jDayNo %= 1461;
1686
1687 if ( $jDayNo >= 366 ) {
1688 $jy += floor( ( $jDayNo - 1 ) / 365 );
1689 $jDayNo = floor( ( $jDayNo - 1 ) % 365 );
1690 }
1691
1692 for ( $i = 0; $i < 11 && $jDayNo >= self::$IRANIAN_DAYS[$i]; $i++ ) {
1693 $jDayNo -= self::$IRANIAN_DAYS[$i];
1694 }
1695
1696 $jm = $i + 1;
1697 $jd = $jDayNo + 1;
1698
1699 return array( $jy, $jm, $jd );
1700 }
1701
1702 /**
1703 * Converting Gregorian dates to Hijri dates.
1704 *
1705 * Based on a PHP-Nuke block by Sharjeel which is released under GNU/GPL license
1706 *
1707 * @see http://phpnuke.org/modules.php?name=News&file=article&sid=8234&mode=thread&order=0&thold=0
1708 *
1709 * @param string $ts
1710 *
1711 * @return string
1712 */
1713 private static function tsToHijri( $ts ) {
1714 $year = substr( $ts, 0, 4 );
1715 $month = substr( $ts, 4, 2 );
1716 $day = substr( $ts, 6, 2 );
1717
1718 $zyr = $year;
1719 $zd = $day;
1720 $zm = $month;
1721 $zy = $zyr;
1722
1723 if (
1724 ( $zy > 1582 ) || ( ( $zy == 1582 ) && ( $zm > 10 ) ) ||
1725 ( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) )
1726 ) {
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 ) +
1730 $zd - 32075;
1731 } else {
1732 $zjd = 367 * $zy - (int)( ( 7 * ( $zy + 5001 + (int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) +
1733 (int)( ( 275 * $zm ) / 9 ) + $zd + 1729777;
1734 }
1735
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;
1746
1747 return array( $zy, $zm, $zd );
1748 }
1749
1750 /**
1751 * Converting Gregorian dates to Hebrew dates.
1752 *
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
1756 * GNU GPL.
1757 *
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.
1760 *
1761 * @param string $ts
1762 *
1763 * @return string
1764 */
1765 private static function tsToHebrew( $ts ) {
1766 # Parse date
1767 $year = substr( $ts, 0, 4 );
1768 $month = substr( $ts, 4, 2 );
1769 $day = substr( $ts, 6, 2 );
1770
1771 # Calculate Hebrew year
1772 $hebrewYear = $year + 3760;
1773
1774 # Month number when September = 1, August = 12
1775 $month += 4;
1776 if ( $month > 12 ) {
1777 # Next year
1778 $month -= 12;
1779 $year++;
1780 $hebrewYear++;
1781 }
1782
1783 # Calculate day of year from 1 September
1784 $dayOfYear = $day;
1785 for ( $i = 1; $i < $month; $i++ ) {
1786 if ( $i == 6 ) {
1787 # February
1788 $dayOfYear += 28;
1789 # Check if the year is leap
1790 if ( $year % 400 == 0 || ( $year % 4 == 0 && $year % 100 > 0 ) ) {
1791 $dayOfYear++;
1792 }
1793 } elseif ( $i == 8 || $i == 10 || $i == 1 || $i == 3 ) {
1794 $dayOfYear += 30;
1795 } else {
1796 $dayOfYear += 31;
1797 }
1798 }
1799
1800 # Calculate the start of the Hebrew year
1801 $start = self::hebrewYearStart( $hebrewYear );
1802
1803 # Calculate next year's start
1804 if ( $dayOfYear <= $start ) {
1805 # Day is before the start of the year - it is the previous year
1806 # Next year's start
1807 $nextStart = $start;
1808 # Previous year
1809 $year--;
1810 $hebrewYear--;
1811 # Add days since previous year's 1 September
1812 $dayOfYear += 365;
1813 if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1814 # Leap year
1815 $dayOfYear++;
1816 }
1817 # Start of the new (previous) year
1818 $start = self::hebrewYearStart( $hebrewYear );
1819 } else {
1820 # Next year's start
1821 $nextStart = self::hebrewYearStart( $hebrewYear + 1 );
1822 }
1823
1824 # Calculate Hebrew day of year
1825 $hebrewDayOfYear = $dayOfYear - $start;
1826
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 ) ) {
1833 $diff += 13;
1834 } else {
1835 $diff += 12;
1836 }
1837
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;
1845
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;
1850 $hebrewMonth = 1;
1851 $days = 0;
1852 while ( $hebrewMonth <= 12 ) {
1853 # Calculate days in this month
1854 if ( $isLeap && $hebrewMonth == 6 ) {
1855 # Adar in a leap year
1856 if ( $isLeap ) {
1857 # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1858 $days = 30;
1859 if ( $hebrewDay <= $days ) {
1860 # Day in Adar I
1861 $hebrewMonth = 13;
1862 } else {
1863 # Subtract the days of Adar I
1864 $hebrewDay -= $days;
1865 # Try Adar II
1866 $days = 29;
1867 if ( $hebrewDay <= $days ) {
1868 # Day in Adar II
1869 $hebrewMonth = 14;
1870 }
1871 }
1872 }
1873 } elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
1874 # Cheshvan in a complete year (otherwise as the rule below)
1875 $days = 30;
1876 } elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) {
1877 # Kislev in an incomplete year (otherwise as the rule below)
1878 $days = 29;
1879 } else {
1880 # Odd months have 30 days, even have 29
1881 $days = 30 - ( $hebrewMonth - 1 ) % 2;
1882 }
1883 if ( $hebrewDay <= $days ) {
1884 # In the current month
1885 break;
1886 } else {
1887 # Subtract the days of the current month
1888 $hebrewDay -= $days;
1889 # Try in the next month
1890 $hebrewMonth++;
1891 }
1892 }
1893
1894 return array( $hebrewYear, $hebrewMonth, $hebrewDay, $days );
1895 }
1896
1897 /**
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.
1901 *
1902 * @param int $year
1903 *
1904 * @return string
1905 */
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 );
1910 if ( $m < 0 ) {
1911 $m--;
1912 }
1913 $Mar = intval( $m );
1914 if ( $m < 0 ) {
1915 $m++;
1916 }
1917 $m -= $Mar;
1918
1919 $c = intval( ( $Mar + 3 * ( $year - 1 ) + 5 * $b + 5 ) % 7 );
1920 if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1921 $Mar++;
1922 } elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1923 $Mar += 2;
1924 } elseif ( $c == 2 || $c == 4 || $c == 6 ) {
1925 $Mar++;
1926 }
1927
1928 $Mar += intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1929 return $Mar;
1930 }
1931
1932 /**
1933 * Algorithm to convert Gregorian dates to Thai solar dates,
1934 * Minguo dates or Minguo dates.
1935 *
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
1939 *
1940 * @param string $ts 14-character timestamp
1941 * @param string $cName Calender name
1942 * @return array Converted year, month, day
1943 */
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 );
1948
1949 if ( !strcmp( $cName, 'thai' ) ) {
1950 # Thai solar dates
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' ) ) {
1955 # Minguo dates
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
1964 if ( ( $gy < 1912 )
1965 || ( ( $gy == 1912 ) && ( $gm < 7 ) )
1966 || ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd < 31 ) )
1967 ) {
1968 # Meiji period
1969 $gy_gannen = $gy - 1868 + 1;
1970 $gy_offset = $gy_gannen;
1971 if ( $gy_gannen == 1 ) {
1972 $gy_offset = '元';
1973 }
1974 $gy_offset = '明治' . $gy_offset;
1975 } elseif (
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 ) )
1981 ) {
1982 # Taishō period
1983 $gy_gannen = $gy - 1912 + 1;
1984 $gy_offset = $gy_gannen;
1985 if ( $gy_gannen == 1 ) {
1986 $gy_offset = '元';
1987 }
1988 $gy_offset = '大正' . $gy_offset;
1989 } elseif (
1990 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd >= 26 ) ) ||
1991 ( ( $gy > 1926 ) && ( $gy < 1989 ) ) ||
1992 ( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd < 8 ) )
1993 ) {
1994 # Shōwa period
1995 $gy_gannen = $gy - 1926 + 1;
1996 $gy_offset = $gy_gannen;
1997 if ( $gy_gannen == 1 ) {
1998 $gy_offset = '元';
1999 }
2000 $gy_offset = '昭和' . $gy_offset;
2001 } else {
2002 # Heisei period
2003 $gy_gannen = $gy - 1989 + 1;
2004 $gy_offset = $gy_gannen;
2005 if ( $gy_gannen == 1 ) {
2006 $gy_offset = '元';
2007 }
2008 $gy_offset = '平成' . $gy_offset;
2009 }
2010 } else {
2011 $gy_offset = $gy;
2012 }
2013
2014 return array( $gy_offset, $gm, $gd );
2015 }
2016
2017 /**
2018 * Gets directionality of the first strongly directional codepoint, for embedBidi()
2019 *
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 ).
2023 *
2024 * TODO: Does not handle BIDI control characters inside the text.
2025 * TODO: Does not handle unallocated characters.
2026 *
2027 * @param string $text Text to test
2028 * @return null|string Directionality ('ltr' or 'rtl') or null
2029 */
2030 private static function strongDirFromContent( $text = '' ) {
2031 if ( !preg_match( self::$strongDirRegex, $text, $matches ) ) {
2032 return null;
2033 }
2034 if ( $matches[1] === '' ) {
2035 return 'rtl';
2036 }
2037 return 'ltr';
2038 }
2039
2040 /**
2041 * Roman number formatting up to 10000
2042 *
2043 * @param int $num
2044 *
2045 * @return string
2046 */
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' )
2054 );
2055
2056 $num = intval( $num );
2057 if ( $num > 10000 || $num <= 0 ) {
2058 return $num;
2059 }
2060
2061 $s = '';
2062 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
2063 if ( $num >= $pow10 ) {
2064 $s .= $table[$i][(int)floor( $num / $pow10 )];
2065 }
2066 $num = $num % $pow10;
2067 }
2068 return $s;
2069 }
2070
2071 /**
2072 * Hebrew Gematria number formatting up to 9999
2073 *
2074 * @param int $num
2075 *
2076 * @return string
2077 */
2078 static function hebrewNumeral( $num ) {
2079 static $table = array(
2080 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ),
2081 array( '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ),
2082 array( '',
2083 array( 'ק' ),
2084 array( 'ר' ),
2085 array( 'ש' ),
2086 array( 'ת' ),
2087 array( 'ת', 'ק' ),
2088 array( 'ת', 'ר' ),
2089 array( 'ת', 'ש' ),
2090 array( 'ת', 'ת' ),
2091 array( 'ת', 'ת', 'ק' ),
2092 array( 'ת', 'ת', 'ר' ),
2093 ),
2094 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' )
2095 );
2096
2097 $num = intval( $num );
2098 if ( $num > 9999 || $num <= 0 ) {
2099 return $num;
2100 }
2101
2102 // Round thousands have special notations
2103 if ( $num === 1000 ) {
2104 return "א' אלף";
2105 } elseif ( $num % 1000 === 0 ) {
2106 return $table[0][$num / 1000] . "' אלפים";
2107 }
2108
2109 $letters = array();
2110
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];
2116 $num = 0;
2117 } else {
2118 $letters = array_merge(
2119 $letters,
2120 (array)$table[$i][intval( $num / $pow10 )]
2121 );
2122
2123 if ( $pow10 === 1000 ) {
2124 $letters[] = "'";
2125 }
2126 }
2127 }
2128
2129 $num = $num % $pow10;
2130 }
2131
2132 $preTransformLength = count( $letters );
2133 if ( $preTransformLength === 1 ) {
2134 // Add geresh (single quote) to one-letter numbers
2135 $letters[] = "'";
2136 } else {
2137 $lastIndex = $preTransformLength - 1;
2138 $letters[$lastIndex] = str_replace(
2139 array( 'כ', 'מ', 'נ', 'פ', 'צ' ),
2140 array( 'ך', 'ם', 'ן', 'ף', 'ץ' ),
2141 $letters[$lastIndex]
2142 );
2143
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 ) {
2148 $letters[] = "'";
2149 } else {
2150 array_splice( $letters, -1, 0, '"' );
2151 }
2152 }
2153
2154 return implode( $letters );
2155 }
2156
2157 /**
2158 * Used by date() and time() to adjust the time output.
2159 *
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)
2163 * @return int
2164 */
2165 function userAdjust( $ts, $tz = false ) {
2166 global $wgUser, $wgLocalTZoffset;
2167
2168 if ( $tz === false ) {
2169 $tz = $wgUser->getOption( 'timecorrection' );
2170 }
2171
2172 $data = explode( '|', $tz, 3 );
2173
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' );
2182 return $date;
2183 }
2184 # Unrecognized timezone, default to 'Offset' with the stored offset.
2185 $data[0] = 'Offset';
2186 }
2187
2188 if ( $data[0] == 'System' || $tz == '' ) {
2189 # Global offset in minutes.
2190 $minDiff = $wgLocalTZoffset;
2191 } elseif ( $data[0] == 'Offset' ) {
2192 $minDiff = intval( $data[1] );
2193 } else {
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;
2201 }
2202 } else {
2203 $minDiff = intval( $data[0] ) * 60;
2204 }
2205 }
2206
2207 # No difference ? Return time unchanged
2208 if ( 0 == $minDiff ) {
2209 return $ts;
2210 }
2211
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.
2216 $t = mktime( (
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
2223
2224 $date = date( 'YmdHis', $t );
2225 MediaWiki\restoreWarnings();
2226
2227 return $date;
2228 }
2229
2230 /**
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
2233 * all children.
2234 *
2235 *<code>
2236 * function timeanddate([...], $format = true) {
2237 * $datePreference = $this->dateFormat($format);
2238 * [...]
2239 * }
2240 *</code>
2241 *
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.
2245 * @return string
2246 */
2247 function dateFormat( $usePrefs = true ) {
2248 global $wgUser;
2249
2250 if ( is_bool( $usePrefs ) ) {
2251 if ( $usePrefs ) {
2252 $datePreference = $wgUser->getDatePreference();
2253 } else {
2254 $datePreference = (string)User::getDefaultOption( 'date' );
2255 }
2256 } else {
2257 $datePreference = (string)$usePrefs;
2258 }
2259
2260 // return int
2261 if ( $datePreference == '' ) {
2262 return 'default';
2263 }
2264
2265 return $datePreference;
2266 }
2267
2268 /**
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
2272 *
2273 * @since 1.22 New type 'pretty' that provides a more readable timestamp format
2274 *
2275 * @return string
2276 */
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" );
2282 } else {
2283 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2284
2285 if ( $type === 'pretty' && $df === null ) {
2286 $df = $this->getDateFormatString( 'date', $pref );
2287 }
2288
2289 if ( $df === null ) {
2290 $pref = $this->getDefaultDateFormat();
2291 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2292 }
2293 }
2294 $this->dateFormatStrings[$type][$pref] = $df;
2295 }
2296 return $this->dateFormatStrings[$type][$pref];
2297 }
2298
2299 /**
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
2307 * @return string
2308 */
2309 function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
2310 $ts = wfTimestamp( TS_MW, $ts );
2311 if ( $adj ) {
2312 $ts = $this->userAdjust( $ts, $timecorrection );
2313 }
2314 $df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
2315 return $this->sprintfDate( $df, $ts );
2316 }
2317
2318 /**
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
2326 * @return string
2327 */
2328 function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
2329 $ts = wfTimestamp( TS_MW, $ts );
2330 if ( $adj ) {
2331 $ts = $this->userAdjust( $ts, $timecorrection );
2332 }
2333 $df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
2334 return $this->sprintfDate( $df, $ts );
2335 }
2336
2337 /**
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
2346 * @return string
2347 */
2348 function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false ) {
2349 $ts = wfTimestamp( TS_MW, $ts );
2350 if ( $adj ) {
2351 $ts = $this->userAdjust( $ts, $timecorrection );
2352 }
2353 $df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) );
2354 return $this->sprintfDate( $df, $ts );
2355 }
2356
2357 /**
2358 * Takes a number of seconds and turns it into a text using values such as hours and minutes.
2359 *
2360 * @since 1.20
2361 *
2362 * @param int $seconds The amount of seconds.
2363 * @param array $chosenIntervals The intervals to enable.
2364 *
2365 * @return string
2366 */
2367 public function formatDuration( $seconds, array $chosenIntervals = array() ) {
2368 $intervals = $this->getDurationIntervals( $seconds, $chosenIntervals );
2369
2370 $segments = array();
2371
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();
2377 }
2378
2379 return $this->listToText( $segments );
2380 }
2381
2382 /**
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 ).
2385 *
2386 * @since 1.20
2387 *
2388 * @param int $seconds The amount of seconds.
2389 * @param array $chosenIntervals The intervals to enable.
2390 *
2391 * @return array
2392 */
2393 public function getDurationIntervals( $seconds, array $chosenIntervals = array() ) {
2394 if ( empty( $chosenIntervals ) ) {
2395 $chosenIntervals = array(
2396 'millennia',
2397 'centuries',
2398 'decades',
2399 'years',
2400 'days',
2401 'hours',
2402 'minutes',
2403 'seconds'
2404 );
2405 }
2406
2407 $intervals = array_intersect_key( self::$durationIntervals, array_flip( $chosenIntervals ) );
2408 $sortedNames = array_keys( $intervals );
2409 $smallestInterval = array_pop( $sortedNames );
2410
2411 $segments = array();
2412
2413 foreach ( $intervals as $name => $length ) {
2414 $value = floor( $seconds / $length );
2415
2416 if ( $value > 0 || ( $name == $smallestInterval && empty( $segments ) ) ) {
2417 $seconds -= $value * $length;
2418 $segments[$name] = $value;
2419 }
2420 }
2421
2422 return $segments;
2423 }
2424
2425 /**
2426 * Internal helper function for userDate(), userTime() and userTimeAndDate()
2427 *
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
2441 * @since 1.19
2442 * @return string
2443 */
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' );
2450 } else {
2451 $offset = $options['timecorrection'];
2452 }
2453 $ts = $this->userAdjust( $ts, $offset );
2454 }
2455 if ( $options['format'] === true ) {
2456 $format = $user->getDatePreference();
2457 } else {
2458 $format = $options['format'];
2459 }
2460 $df = $this->getDateFormatString( $type, $this->dateFormat( $format ) );
2461 return $this->sprintfDate( $df, $ts );
2462 }
2463
2464 /**
2465 * Get the formatted date for the given timestamp and formatted for
2466 * the given user.
2467 *
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
2480 * @since 1.19
2481 * @return string
2482 */
2483 public function userDate( $ts, User $user, array $options = array() ) {
2484 return $this->internalUserTimeAndDate( 'date', $ts, $user, $options );
2485 }
2486
2487 /**
2488 * Get the formatted time for the given timestamp and formatted for
2489 * the given user.
2490 *
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
2503 * @since 1.19
2504 * @return string
2505 */
2506 public function userTime( $ts, User $user, array $options = array() ) {
2507 return $this->internalUserTimeAndDate( 'time', $ts, $user, $options );
2508 }
2509
2510 /**
2511 * Get the formatted date and time for the given timestamp and formatted for
2512 * the given user.
2513 *
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
2526 * @since 1.19
2527 * @return string
2528 */
2529 public function userTimeAndDate( $ts, User $user, array $options = array() ) {
2530 return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
2531 }
2532
2533 /**
2534 * Get the timestamp in a human-friendly relative format, e.g., "3 days ago".
2535 *
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.
2539 *
2540 * @since 1.26 (Prior to 1.26 method existed but was not meant to be used directly)
2541 *
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
2546 */
2547 public function getHumanTimestamp( MWTimestamp $time, MWTimestamp $relativeTo = null, User $user = null ) {
2548 if ( $relativeTo === null ) {
2549 $relativeTo = new MWTimestamp();
2550 }
2551 if ( $user === null ) {
2552 $user = RequestContext::getMain()->getUser();
2553 }
2554
2555 // Adjust for the user's timezone.
2556 $offsetThis = $time->offsetForUser( $user );
2557 $offsetRel = $relativeTo->offsetForUser( $user );
2558
2559 $ts = '';
2560 if ( Hooks::run( 'GetHumanTimestamp', array( &$ts, $time, $relativeTo, $user, $this ) ) ) {
2561 $ts = $this->getHumanTimestampInternal( $time, $relativeTo, $user );
2562 }
2563
2564 // Reset the timezone on the objects.
2565 $time->timestamp->sub( $offsetThis );
2566 $relativeTo->timestamp->sub( $offsetRel );
2567
2568 return $ts;
2569 }
2570
2571 /**
2572 * Convert an MWTimestamp into a pretty human-readable timestamp using
2573 * the given user preferences and relative base time.
2574 *
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
2580 * @since 1.26
2581 */
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' )
2589 ) {
2590 // Timestamps are in different years: use full timestamp
2591 // Also do full timestamp for future dates
2592 /**
2593 * @todo FIXME: Add better handling of future timestamps.
2594 */
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' )];
2605 // Messages:
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 ) ) )
2610 ->text();
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 ) ) )
2617 ->text();
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 ) ) )
2624 ->text();
2625
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();
2637 } else {
2638 // Less than 30 seconds ago.
2639 $ts = wfMessage( 'just-now' )->text();
2640 }
2641
2642 return $ts;
2643 }
2644
2645 /**
2646 * @param string $key
2647 * @return array|null
2648 */
2649 function getMessage( $key ) {
2650 return self::$dataCache->getSubitem( $this->mCode, 'messages', $key );
2651 }
2652
2653 /**
2654 * @return array
2655 */
2656 function getAllMessages() {
2657 return self::$dataCache->getItem( $this->mCode, 'messages' );
2658 }
2659
2660 /**
2661 * @param string $in
2662 * @param string $out
2663 * @param string $string
2664 * @return string
2665 */
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
2669
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();
2677 return $text;
2678 }
2679
2680 // callback functions for uc(), lc(), ucwords(), ucwordbreaks()
2681
2682 /**
2683 * @param array $matches
2684 * @return mixed|string
2685 */
2686 function ucwordbreaksCallbackAscii( $matches ) {
2687 return $this->ucfirst( $matches[1] );
2688 }
2689
2690 /**
2691 * @param array $matches
2692 * @return string
2693 */
2694 function ucwordbreaksCallbackMB( $matches ) {
2695 return mb_strtoupper( $matches[0] );
2696 }
2697
2698 /**
2699 * @param array $matches
2700 * @return string
2701 */
2702 function ucCallback( $matches ) {
2703 list( $wikiUpperChars ) = self::getCaseMaps();
2704 return strtr( $matches[1], $wikiUpperChars );
2705 }
2706
2707 /**
2708 * @param array $matches
2709 * @return string
2710 */
2711 function lcCallback( $matches ) {
2712 list( , $wikiLowerChars ) = self::getCaseMaps();
2713 return strtr( $matches[1], $wikiLowerChars );
2714 }
2715
2716 /**
2717 * @param array $matches
2718 * @return string
2719 */
2720 function ucwordsCallbackMB( $matches ) {
2721 return mb_strtoupper( $matches[0] );
2722 }
2723
2724 /**
2725 * @param array $matches
2726 * @return string
2727 */
2728 function ucwordsCallbackWiki( $matches ) {
2729 list( $wikiUpperChars ) = self::getCaseMaps();
2730 return strtr( $matches[0], $wikiUpperChars );
2731 }
2732
2733 /**
2734 * Make a string's first character uppercase
2735 *
2736 * @param string $str
2737 *
2738 * @return string
2739 */
2740 function ucfirst( $str ) {
2741 $o = ord( $str );
2742 if ( $o < 96 ) { // if already uppercase...
2743 return $str;
2744 } elseif ( $o < 128 ) {
2745 return ucfirst( $str ); // use PHP's ucfirst()
2746 } else {
2747 // fall back to more complex logic in case of multibyte strings
2748 return $this->uc( $str, true );
2749 }
2750 }
2751
2752 /**
2753 * Convert a string to uppercase
2754 *
2755 * @param string $str
2756 * @param bool $first
2757 *
2758 * @return string
2759 */
2760 function uc( $str, $first = false ) {
2761 if ( function_exists( 'mb_strtoupper' ) ) {
2762 if ( $first ) {
2763 if ( $this->isMultibyte( $str ) ) {
2764 return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2765 } else {
2766 return ucfirst( $str );
2767 }
2768 } else {
2769 return $this->isMultibyte( $str ) ? mb_strtoupper( $str ) : strtoupper( $str );
2770 }
2771 } else {
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' ),
2777 $str
2778 );
2779 } else {
2780 return $first ? ucfirst( $str ) : strtoupper( $str );
2781 }
2782 }
2783 }
2784
2785 /**
2786 * @param string $str
2787 * @return mixed|string
2788 */
2789 function lcfirst( $str ) {
2790 $o = ord( $str );
2791 if ( !$o ) {
2792 return strval( $str );
2793 } elseif ( $o >= 128 ) {
2794 return $this->lc( $str, true );
2795 } elseif ( $o > 96 ) {
2796 return $str;
2797 } else {
2798 $str[0] = strtolower( $str[0] );
2799 return $str;
2800 }
2801 }
2802
2803 /**
2804 * @param string $str
2805 * @param bool $first
2806 * @return mixed|string
2807 */
2808 function lc( $str, $first = false ) {
2809 if ( function_exists( 'mb_strtolower' ) ) {
2810 if ( $first ) {
2811 if ( $this->isMultibyte( $str ) ) {
2812 return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2813 } else {
2814 return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
2815 }
2816 } else {
2817 return $this->isMultibyte( $str ) ? mb_strtolower( $str ) : strtolower( $str );
2818 }
2819 } else {
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' ),
2825 $str
2826 );
2827 } else {
2828 return $first ? strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 ) : strtolower( $str );
2829 }
2830 }
2831 }
2832
2833 /**
2834 * @param string $str
2835 * @return bool
2836 */
2837 function isMultibyte( $str ) {
2838 return (bool)preg_match( '/[\x80-\xff]/', $str );
2839 }
2840
2841 /**
2842 * @param string $str
2843 * @return mixed|string
2844 */
2845 function ucwords( $str ) {
2846 if ( $this->isMultibyte( $str ) ) {
2847 $str = $this->lc( $str );
2848
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]*)/";
2851
2852 // function to use to capitalize a single char
2853 if ( function_exists( 'mb_strtoupper' ) ) {
2854 return preg_replace_callback(
2855 $replaceRegexp,
2856 array( $this, 'ucwordsCallbackMB' ),
2857 $str
2858 );
2859 } else {
2860 return preg_replace_callback(
2861 $replaceRegexp,
2862 array( $this, 'ucwordsCallbackWiki' ),
2863 $str
2864 );
2865 }
2866 } else {
2867 return ucwords( strtolower( $str ) );
2868 }
2869 }
2870
2871 /**
2872 * capitalize words at word breaks
2873 *
2874 * @param string $str
2875 * @return mixed
2876 */
2877 function ucwordbreaks( $str ) {
2878 if ( $this->isMultibyte( $str ) ) {
2879 $str = $this->lc( $str );
2880
2881 // since \b doesn't work for UTF-8, we explicitely define word break chars
2882 $breaks = "[ \-\(\)\}\{\.,\?!]";
2883
2884 // find first letter after word break
2885 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|" .
2886 "$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2887
2888 if ( function_exists( 'mb_strtoupper' ) ) {
2889 return preg_replace_callback(
2890 $replaceRegexp,
2891 array( $this, 'ucwordbreaksCallbackMB' ),
2892 $str
2893 );
2894 } else {
2895 return preg_replace_callback(
2896 $replaceRegexp,
2897 array( $this, 'ucwordsCallbackWiki' ),
2898 $str
2899 );
2900 }
2901 } else {
2902 return preg_replace_callback(
2903 '/\b([\w\x80-\xff]+)\b/',
2904 array( $this, 'ucwordbreaksCallbackAscii' ),
2905 $str
2906 );
2907 }
2908 }
2909
2910 /**
2911 * Return a case-folded representation of $s
2912 *
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.
2916 *
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.
2920 *
2921 * @param string $s
2922 *
2923 * @return string
2924 */
2925 function caseFold( $s ) {
2926 return $this->uc( $s );
2927 }
2928
2929 /**
2930 * @param string $s
2931 * @return string
2932 */
2933 function checkTitleEncoding( $s ) {
2934 if ( is_array( $s ) ) {
2935 throw new MWException( 'Given array to checkTitleEncoding.' );
2936 }
2937 if ( StringUtils::isUtf8( $s ) ) {
2938 return $s;
2939 }
2940
2941 return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s );
2942 }
2943
2944 /**
2945 * @return array
2946 */
2947 function fallback8bitEncoding() {
2948 return self::$dataCache->getItem( $this->mCode, 'fallback8bitEncoding' );
2949 }
2950
2951 /**
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
2955 * searching etc.
2956 *
2957 * @return bool
2958 */
2959 function hasWordBreaks() {
2960 return true;
2961 }
2962
2963 /**
2964 * Some languages such as Chinese require word segmentation,
2965 * Specify such segmentation when overridden in derived class.
2966 *
2967 * @param string $string
2968 * @return string
2969 */
2970 function segmentByWord( $string ) {
2971 return $string;
2972 }
2973
2974 /**
2975 * Some languages have special punctuation need to be normalized.
2976 * Make such changes here.
2977 *
2978 * @param string $string
2979 * @return string
2980 */
2981 function normalizeForSearch( $string ) {
2982 return self::convertDoubleWidth( $string );
2983 }
2984
2985 /**
2986 * convert double-width roman characters to single-width.
2987 * range: ff00-ff5f ~= 0020-007f
2988 *
2989 * @param string $string
2990 *
2991 * @return string
2992 */
2993 protected static function convertDoubleWidth( $string ) {
2994 static $full = null;
2995 static $half = null;
2996
2997 if ( $full === null ) {
2998 $fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2999 $halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
3000 $full = str_split( $fullWidth, 3 );
3001 $half = str_split( $halfWidth );
3002 }
3003
3004 $string = str_replace( $full, $half, $string );
3005 return $string;
3006 }
3007
3008 /**
3009 * @param string $string
3010 * @param string $pattern
3011 * @return string
3012 */
3013 protected static function insertSpace( $string, $pattern ) {
3014 $string = preg_replace( $pattern, " $1 ", $string );
3015 $string = preg_replace( '/ +/', ' ', $string );
3016 return $string;
3017 }
3018
3019 /**
3020 * @param array $termsArray
3021 * @return array
3022 */
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
3026 return $termsArray;
3027 }
3028
3029 /**
3030 * Get the first character of a string.
3031 *
3032 * @param string $s
3033 * @return string
3034 */
3035 function firstChar( $s ) {
3036 $matches = array();
3037 preg_match(
3038 '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
3039 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/',
3040 $s,
3041 $matches
3042 );
3043
3044 if ( isset( $matches[1] ) ) {
3045 if ( strlen( $matches[1] ) != 3 ) {
3046 return $matches[1];
3047 }
3048
3049 // Break down Hangul syllables to grab the first jamo
3050 $code = UtfNormal\Utils::utf8ToCodepoint( $matches[1] );
3051 if ( $code < 0xac00 || 0xd7a4 <= $code ) {
3052 return $matches[1];
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";
3079 } else {
3080 return "\xe3\x85\x8e";
3081 }
3082 } else {
3083 return '';
3084 }
3085 }
3086
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.
3092 }
3093
3094 /**
3095 * @param string $s
3096 * @return string
3097 */
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' ) {
3104 return $s;
3105 } else {
3106 return $this->iconv( 'UTF-8', $wgEditEncoding, $s );
3107 }
3108 }
3109
3110 /**
3111 * @param string $s
3112 * @return string
3113 */
3114 function recodeInput( $s ) {
3115 # Take the previous into account.
3116 global $wgEditEncoding;
3117 if ( $wgEditEncoding != '' ) {
3118 $enc = $wgEditEncoding;
3119 } else {
3120 $enc = 'UTF-8';
3121 }
3122 if ( $enc == 'UTF-8' ) {
3123 return $s;
3124 } else {
3125 return $this->iconv( $enc, 'UTF-8', $s );
3126 }
3127 }
3128
3129 /**
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.
3133 *
3134 * This is language-specific for performance reasons only.
3135 *
3136 * @param string $s
3137 *
3138 * @return string
3139 */
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 );
3146 }
3147
3148 return $s;
3149 }
3150
3151 /**
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.
3155 *
3156 * The data is cached in process memory. This will go faster if you have the
3157 * FastStringSearch extension.
3158 *
3159 * @param string $file
3160 * @param string $string
3161 *
3162 * @throws MWException
3163 * @return string
3164 */
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" );
3170 }
3171 $this->transformData[$file] = new ReplacementArray( $data );
3172 }
3173 return $this->transformData[$file]->replace( $string );
3174 }
3175
3176 /**
3177 * For right-to-left language support
3178 *
3179 * @return bool
3180 */
3181 function isRTL() {
3182 return self::$dataCache->getItem( $this->mCode, 'rtl' );
3183 }
3184
3185 /**
3186 * Return the correct HTML 'dir' attribute value for this language.
3187 * @return string
3188 */
3189 function getDir() {
3190 return $this->isRTL() ? 'rtl' : 'ltr';
3191 }
3192
3193 /**
3194 * Return 'left' or 'right' as appropriate alignment for line-start
3195 * for this language's text direction.
3196 *
3197 * Should be equivalent to CSS3 'start' text-align value....
3198 *
3199 * @return string
3200 */
3201 function alignStart() {
3202 return $this->isRTL() ? 'right' : 'left';
3203 }
3204
3205 /**
3206 * Return 'right' or 'left' as appropriate alignment for line-end
3207 * for this language's text direction.
3208 *
3209 * Should be equivalent to CSS3 'end' text-align value....
3210 *
3211 * @return string
3212 */
3213 function alignEnd() {
3214 return $this->isRTL() ? 'left' : 'right';
3215 }
3216
3217 /**
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.
3223 *
3224 * @param bool $opposite Get the direction mark opposite to your language
3225 * @return string
3226 * @since 1.20
3227 */
3228 function getDirMarkEntity( $opposite = false ) {
3229 if ( $opposite ) {
3230 return $this->isRTL() ? '&lrm;' : '&rlm;';
3231 }
3232 return $this->isRTL() ? '&rlm;' : '&lrm;';
3233 }
3234
3235 /**
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.
3241 *
3242 * @param bool $opposite Get the direction mark opposite to your language
3243 * @return string
3244 */
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
3248 if ( $opposite ) {
3249 return $this->isRTL() ? $lrm : $rlm;
3250 }
3251 return $this->isRTL() ? $rlm : $lrm;
3252 }
3253
3254 /**
3255 * @return array
3256 */
3257 function capitalizeAllNouns() {
3258 return self::$dataCache->getItem( $this->mCode, 'capitalizeAllNouns' );
3259 }
3260
3261 /**
3262 * An arrow, depending on the language direction.
3263 *
3264 * @param string $direction The direction of the arrow: forwards (default),
3265 * backwards, left, right, up, down.
3266 * @return string
3267 */
3268 function getArrow( $direction = 'forwards' ) {
3269 switch ( $direction ) {
3270 case 'forwards':
3271 return $this->isRTL() ? '←' : '→';
3272 case 'backwards':
3273 return $this->isRTL() ? '→' : '←';
3274 case 'left':
3275 return '←';
3276 case 'right':
3277 return '→';
3278 case 'up':
3279 return '↑';
3280 case 'down':
3281 return '↓';
3282 }
3283 }
3284
3285 /**
3286 * To allow "foo[[bar]]" to extend the link over the whole word "foobar"
3287 *
3288 * @return bool
3289 */
3290 function linkPrefixExtension() {
3291 return self::$dataCache->getItem( $this->mCode, 'linkPrefixExtension' );
3292 }
3293
3294 /**
3295 * Get all magic words from cache.
3296 * @return array
3297 */
3298 function getMagicWords() {
3299 return self::$dataCache->getItem( $this->mCode, 'magicWords' );
3300 }
3301
3302 /**
3303 * Run the LanguageGetMagic hook once.
3304 */
3305 protected function doMagicHook() {
3306 if ( $this->mMagicHookDone ) {
3307 return;
3308 }
3309 $this->mMagicHookDone = true;
3310 Hooks::run( 'LanguageGetMagic', array( &$this->mMagicExtensions, $this->getCode() ) );
3311 }
3312
3313 /**
3314 * Fill a MagicWord object with data from here
3315 *
3316 * @param MagicWord $mw
3317 */
3318 function getMagic( $mw ) {
3319 // Saves a function call
3320 if ( !$this->mMagicHookDone ) {
3321 $this->doMagicHook();
3322 }
3323
3324 if ( isset( $this->mMagicExtensions[$mw->mId] ) ) {
3325 $rawEntry = $this->mMagicExtensions[$mw->mId];
3326 } else {
3327 $rawEntry = self::$dataCache->getSubitem(
3328 $this->mCode, 'magicWords', $mw->mId );
3329 }
3330
3331 if ( !is_array( $rawEntry ) ) {
3332 wfWarn( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
3333 } else {
3334 $mw->mCaseSensitive = $rawEntry[0];
3335 $mw->mSynonyms = array_slice( $rawEntry, 1 );
3336 }
3337 }
3338
3339 /**
3340 * Add magic words to the extension array
3341 *
3342 * @param array $newWords
3343 */
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;
3350 }
3351 }
3352 }
3353
3354 /**
3355 * Get special page names, as an associative array
3356 * canonical name => array of valid names, including aliases
3357 * @return array
3358 */
3359 function getSpecialPageAliases() {
3360 // Cache aliases because it may be slow to load them
3361 if ( is_null( $this->mExtendedSpecialPageAliases ) ) {
3362 // Initialise array
3363 $this->mExtendedSpecialPageAliases =
3364 self::$dataCache->getItem( $this->mCode, 'specialPageAliases' );
3365 Hooks::run( 'LanguageGetSpecialPageAliases',
3366 array( &$this->mExtendedSpecialPageAliases, $this->getCode() ) );
3367 }
3368
3369 return $this->mExtendedSpecialPageAliases;
3370 }
3371
3372 /**
3373 * Italic is unsuitable for some languages
3374 *
3375 * @param string $text The text to be emphasized.
3376 * @return string
3377 */
3378 function emphasize( $text ) {
3379 return "<em>$text</em>";
3380 }
3381
3382 /**
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".
3389 *
3390 * An example of this function being called:
3391 * <code>
3392 * wfMessage( 'message' )->numParams( $num )->text()
3393 * </code>
3394 *
3395 * See $separatorTransformTable on MessageIs.php for
3396 * the , => . and . => , implementation.
3397 *
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
3402 * @return string
3403 */
3404 public function formatNum( $number, $nocommafy = false ) {
3405 global $wgTranslateNumerals;
3406 if ( !$nocommafy ) {
3407 $number = $this->commafy( $number );
3408 $s = $this->separatorTransformTable();
3409 if ( $s ) {
3410 $number = strtr( $number, $s );
3411 }
3412 }
3413
3414 if ( $wgTranslateNumerals ) {
3415 $s = $this->digitTransformTable();
3416 if ( $s ) {
3417 $number = strtr( $number, $s );
3418 }
3419 }
3420
3421 return $number;
3422 }
3423
3424 /**
3425 * Front-end for non-commafied formatNum
3426 *
3427 * @param int|float $number The string to be formatted, should be an integer
3428 * or a floating point number.
3429 * @since 1.21
3430 * @return string
3431 */
3432 public function formatNumNoSeparators( $number ) {
3433 return $this->formatNum( $number, true );
3434 }
3435
3436 /**
3437 * @param string $number
3438 * @return string
3439 */
3440 public function parseFormattedNumber( $number ) {
3441 $s = $this->digitTransformTable();
3442 if ( $s ) {
3443 // eliminate empty array values such as ''. (bug 64347)
3444 $s = array_filter( $s );
3445 $number = strtr( $number, array_flip( $s ) );
3446 }
3447
3448 $s = $this->separatorTransformTable();
3449 if ( $s ) {
3450 // eliminate empty array values such as ''. (bug 64347)
3451 $s = array_filter( $s );
3452 $number = strtr( $number, array_flip( $s ) );
3453 }
3454
3455 $number = strtr( $number, array( ',' => '' ) );
3456 return $number;
3457 }
3458
3459 /**
3460 * Adds commas to a given number
3461 * @since 1.19
3462 * @param mixed $number
3463 * @return string
3464 */
3465 function commafy( $number ) {
3466 $digitGroupingPattern = $this->digitGroupingPattern();
3467 if ( $number === null ) {
3468 return '';
3469 }
3470
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 ) ) );
3474 } else {
3475 // Ref: http://cldr.unicode.org/translation/number-patterns
3476 $sign = "";
3477 if ( intval( $number ) < 0 ) {
3478 // For negative numbers apply the algorithm like positive number and add sign.
3479 $sign = "-";
3480 $number = substr( $number, 1 );
3481 }
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;
3491 }
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;
3497 if ( $start < 0 ) {
3498 $start = 0;
3499 }
3500 $groupedNumber = substr( $number, $start, $end -$start ) . $groupedNumber;
3501 $end = $start;
3502 if ( $numMatches > 1 ) {
3503 // use the last pattern for the rest of the number
3504 $numMatches--;
3505 }
3506 if ( $start > 0 ) {
3507 $groupedNumber = "," . $groupedNumber;
3508 }
3509 }
3510 return $sign . $groupedNumber;
3511 }
3512 }
3513
3514 /**
3515 * @return string
3516 */
3517 function digitGroupingPattern() {
3518 return self::$dataCache->getItem( $this->mCode, 'digitGroupingPattern' );
3519 }
3520
3521 /**
3522 * @return array
3523 */
3524 function digitTransformTable() {
3525 return self::$dataCache->getItem( $this->mCode, 'digitTransformTable' );
3526 }
3527
3528 /**
3529 * @return array
3530 */
3531 function separatorTransformTable() {
3532 return self::$dataCache->getItem( $this->mCode, 'separatorTransformTable' );
3533 }
3534
3535 /**
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…)
3540 *
3541 * @param string[] $l
3542 * @return string
3543 */
3544 function listToText( array $l ) {
3545 $m = count( $l ) - 1;
3546 if ( $m < 0 ) {
3547 return '';
3548 }
3549 if ( $m > 0 ) {
3550 $and = $this->msg( 'and' )->escaped();
3551 $space = $this->msg( 'word-separator' )->escaped();
3552 if ( $m > 1 ) {
3553 $comma = $this->msg( 'comma-separator' )->escaped();
3554 }
3555 }
3556 $s = $l[$m];
3557 for ( $i = $m - 1; $i >= 0; $i-- ) {
3558 if ( $i == $m - 1 ) {
3559 $s = $l[$i] . $and . $space . $s;
3560 } else {
3561 $s = $l[$i] . $comma . $s;
3562 }
3563 }
3564 return $s;
3565 }
3566
3567 /**
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
3571 * @return string
3572 */
3573 function commaList( array $list ) {
3574 return implode(
3575 wfMessage( 'comma-separator' )->inLanguage( $this )->escaped(),
3576 $list
3577 );
3578 }
3579
3580 /**
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
3584 * @return string
3585 */
3586 function semicolonList( array $list ) {
3587 return implode(
3588 wfMessage( 'semicolon-separator' )->inLanguage( $this )->escaped(),
3589 $list
3590 );
3591 }
3592
3593 /**
3594 * Same as commaList, but separate it with the pipe instead.
3595 * @param string[] $list Array of strings to put in a pipe list
3596 * @return string
3597 */
3598 function pipeList( array $list ) {
3599 return implode(
3600 wfMessage( 'pipe-separator' )->inLanguage( $this )->escaped(),
3601 $list
3602 );
3603 }
3604
3605 /**
3606 * Truncate a string to a specified length in bytes, appending an optional
3607 * string (e.g. for ellipses)
3608 *
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
3612 *
3613 * If $length is negative, the string will be truncated from the beginning
3614 *
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.
3620 * @return string
3621 */
3622 function truncate( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3623 # Use the localized ellipsis character
3624 if ( $ellipsis == '...' ) {
3625 $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3626 }
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
3632 }
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...
3638 } else {
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;
3646 } else {
3647 $length += $eLength;
3648 $string = substr( $string, $length ); // ...xyz
3649 $string = $this->removeBadCharFirst( $string );
3650 $string = ltrim( $string );
3651 $string = $ellipsis . $string;
3652 }
3653 }
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 ) ) {
3658 return $string;
3659 } else {
3660 return $stringOriginal;
3661 }
3662 }
3663
3664 /**
3665 * Remove bytes that represent an incomplete Unicode character
3666 * at the end of string (e.g. bytes of the char are missing)
3667 *
3668 * @param string $string
3669 * @return string
3670 */
3671 protected function removeBadCharLast( $string ) {
3672 if ( $string != '' ) {
3673 $char = ord( $string[strlen( $string ) - 1] );
3674 $m = array();
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 )
3681 ) {
3682 # We chopped in the middle of a character; remove it
3683 $string = $m[1];
3684 }
3685 }
3686 return $string;
3687 }
3688
3689 /**
3690 * Remove bytes that represent an incomplete Unicode character
3691 * at the start of string (e.g. bytes of the char are missing)
3692 *
3693 * @param string $string
3694 * @return string
3695 */
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 );
3702 }
3703 }
3704 return $string;
3705 }
3706
3707 /**
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
3710 *
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.
3714 *
3715 * Note: since 1.18 you do not need to leave extra room in $length for ellipses.
3716 *
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
3720 * @return string
3721 */
3722 function truncateHtml( $text, $length, $ellipsis = '...' ) {
3723 # Use the localized ellipsis character
3724 if ( $ellipsis == '...' ) {
3725 $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3726 }
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)
3732 }
3733
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
3742
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
3756 }
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
3762 break;
3763 }
3764 }
3765 if ( $pos >= $textLen ) {
3766 break; // extra iteration just for above checks
3767 }
3768
3769 # Read the next char...
3770 $ch = $text[$pos];
3771 $lastCh = $pos ? $text[$pos - 1] : '';
3772 $ret .= $ch; // add to result string
3773 if ( $ch == '<' ) {
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 ) {
3782 if ( $ch == '/' ) {
3783 $tagType = 1; // close tag (e.g. "</span>")
3784 } else {
3785 $tagType = 0; // open tag (e.g. "<span>")
3786 $tag .= $ch;
3787 }
3788 $bracketState = 2; // building tag name
3789 } elseif ( $bracketState == 2 ) {
3790 if ( $ch != ' ' ) {
3791 $tag .= $ch;
3792 } else {
3793 // Name found (e.g. "<a href=..."), add on tag attributes...
3794 $pos += $this->truncate_skip( $ret, $text, "<>", $pos + 1 );
3795 }
3796 } elseif ( $bracketState == 0 ) {
3797 if ( $entityState ) {
3798 if ( $ch == ';' ) {
3799 $entityState = 0;
3800 $dispLen++; // entity is one displayed char
3801 }
3802 } else {
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 );
3807 }
3808 if ( $ch == '&' ) {
3809 $entityState = 1; // entity found, (e.g. "&#160;")
3810 } else {
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;
3816 $pos += $skipped;
3817 }
3818 }
3819 }
3820 }
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
3825 }
3826 return $ret;
3827 }
3828
3829 /**
3830 * truncateHtml() helper function
3831 * like strcspn() but adds the skipped chars to $ret
3832 *
3833 * @param string $ret
3834 * @param string $text
3835 * @param string $search
3836 * @param int $start
3837 * @param null|int $len
3838 * @return int
3839 */
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 ) {
3844 $len = 0; // sanity
3845 }
3846 $skipCount = 0;
3847 if ( $start < strlen( $text ) ) {
3848 $skipCount = strcspn( $text, $search, $start, $len );
3849 $ret .= substr( $text, $start, $skipCount );
3850 }
3851 return $skipCount;
3852 }
3853
3854 /**
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)
3862 */
3863 private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) {
3864 $tag = ltrim( $tag );
3865 if ( $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
3871 }
3872 }
3873 $tag = '';
3874 }
3875 }
3876
3877 /**
3878 * Grammatical transformations, needed for inflected languages
3879 * Invoked by putting {{grammar:case|word}} in a message
3880 *
3881 * @param string $word
3882 * @param string $case
3883 * @return string
3884 */
3885 function convertGrammar( $word, $case ) {
3886 global $wgGrammarForms;
3887 if ( isset( $wgGrammarForms[$this->getCode()][$case][$word] ) ) {
3888 return $wgGrammarForms[$this->getCode()][$case][$word];
3889 }
3890
3891 return $word;
3892 }
3893 /**
3894 * Get the grammar forms for the content language
3895 * @return array Array of grammar forms
3896 * @since 1.20
3897 */
3898 function getGrammarForms() {
3899 global $wgGrammarForms;
3900 if ( isset( $wgGrammarForms[$this->getCode()] )
3901 && is_array( $wgGrammarForms[$this->getCode()] )
3902 ) {
3903 return $wgGrammarForms[$this->getCode()];
3904 }
3905
3906 return array();
3907 }
3908 /**
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.
3913 *
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.
3917 *
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.
3921 *
3922 * @param string $gender
3923 * @param array $forms
3924 *
3925 * @return string
3926 */
3927 function gender( $gender, $forms ) {
3928 if ( !count( $forms ) ) {
3929 return '';
3930 }
3931 $forms = $this->preConvertPlural( $forms, 2 );
3932 if ( $gender === 'male' ) {
3933 return $forms[0];
3934 }
3935 if ( $gender === 'female' ) {
3936 return $forms[1];
3937 }
3938 return isset( $forms[2] ) ? $forms[2] : $forms[0];
3939 }
3940
3941 /**
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.
3946 *
3947 * Invoked by putting {{plural:count|wordform1|wordform2}}
3948 * or {{plural:count|wordform1|wordform2|wordform3}}
3949 *
3950 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
3951 *
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
3955 */
3956 function convertPlural( $count, $forms ) {
3957 // Handle explicit n=pluralform cases
3958 $forms = $this->handleExplicitPluralForms( $count, $forms );
3959 if ( is_string( $forms ) ) {
3960 return $forms;
3961 }
3962 if ( !count( $forms ) ) {
3963 return '';
3964 }
3965
3966 $pluralForm = $this->getPluralRuleIndexNumber( $count );
3967 $pluralForm = min( $pluralForm, count( $forms ) - 1 );
3968 return $forms[$pluralForm];
3969 }
3970
3971 /**
3972 * Handles explicit plural forms for Language::convertPlural()
3973 *
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().
3978 *
3979 * @since 1.23
3980 *
3981 * @param int $count Non-localized number
3982 * @param array $forms Different plural forms
3983 *
3984 * @return array|string
3985 */
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 );
3992 }
3993 unset( $forms[$index] );
3994 }
3995 }
3996 return array_values( $forms );
3997 }
3998
3999 /**
4000 * Checks that convertPlural was given an array and pads it to requested
4001 * amount of forms by copying the last one.
4002 *
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
4006 */
4007 protected function preConvertPlural( /* Array */ $forms, $count ) {
4008 while ( count( $forms ) < $count ) {
4009 $forms[] = $forms[count( $forms ) - 1];
4010 }
4011 return $forms;
4012 }
4013
4014 /**
4015 * Wraps argument with unicode control characters for directionality safety
4016 *
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".
4020 *
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.
4026 *
4027 * @param string $text Text to wrap
4028 * @return string Text, wrapped in LRE...PDF or RLE...PDF or nothing
4029 */
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;
4035 }
4036 if ( $dir === 'rtl' ) {
4037 // Wrap in RIGHT-TO-LEFT EMBEDDING ... POP DIRECTIONAL FORMATTING
4038 return self::$rle . $text . self::$pdf;
4039 }
4040 // No strong directionality: do not wrap
4041 return $text;
4042 }
4043
4044 /**
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
4049 * match up with it.
4050 *
4051 * @param string $str The validated block duration in English
4052 * @return string Somehow translated block duration
4053 * @see LanguageFi.php for example implementation
4054 */
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 ) );
4060 }
4061 }
4062
4063 if ( wfIsInfinity( $str ) ) {
4064 foreach ( $duration as $show => $value ) {
4065 if ( wfIsInfinity( $value ) ) {
4066 return htmlspecialchars( trim( $show ) );
4067 }
4068 }
4069 }
4070
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.
4074 return $str;
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' );
4082 } else {
4083 return $this->timeanddate( $time );
4084 }
4085 }
4086 }
4087
4088 /**
4089 * languages like Chinese need to be segmented in order for the diff
4090 * to be of any use
4091 *
4092 * @param string $text
4093 * @return string
4094 */
4095 public function segmentForDiff( $text ) {
4096 return $text;
4097 }
4098
4099 /**
4100 * and unsegment to show the result
4101 *
4102 * @param string $text
4103 * @return string
4104 */
4105 public function unsegmentForDiff( $text ) {
4106 return $text;
4107 }
4108
4109 /**
4110 * Return the LanguageConverter used in the Language
4111 *
4112 * @since 1.19
4113 * @return LanguageConverter
4114 */
4115 public function getConverter() {
4116 return $this->mConverter;
4117 }
4118
4119 /**
4120 * convert text to all supported variants
4121 *
4122 * @param string $text
4123 * @return array
4124 */
4125 public function autoConvertToAllVariants( $text ) {
4126 return $this->mConverter->autoConvertToAllVariants( $text );
4127 }
4128
4129 /**
4130 * convert text to different variants of a language.
4131 *
4132 * @param string $text
4133 * @return string
4134 */
4135 public function convert( $text ) {
4136 return $this->mConverter->convert( $text );
4137 }
4138
4139 /**
4140 * Convert a Title object to a string in the preferred variant
4141 *
4142 * @param Title $title
4143 * @return string
4144 */
4145 public function convertTitle( $title ) {
4146 return $this->mConverter->convertTitle( $title );
4147 }
4148
4149 /**
4150 * Convert a namespace index to a string in the preferred variant
4151 *
4152 * @param int $ns
4153 * @return string
4154 */
4155 public function convertNamespace( $ns ) {
4156 return $this->mConverter->convertNamespace( $ns );
4157 }
4158
4159 /**
4160 * Check if this is a language with variants
4161 *
4162 * @return bool
4163 */
4164 public function hasVariants() {
4165 return count( $this->getVariants() ) > 1;
4166 }
4167
4168 /**
4169 * Check if the language has the specific variant
4170 *
4171 * @since 1.19
4172 * @param string $variant
4173 * @return bool
4174 */
4175 public function hasVariant( $variant ) {
4176 return (bool)$this->mConverter->validateVariant( $variant );
4177 }
4178
4179 /**
4180 * Put custom tags (e.g. -{ }-) around math to prevent conversion
4181 *
4182 * @param string $text
4183 * @return string
4184 * @deprecated since 1.22 is no longer used
4185 */
4186 public function armourMath( $text ) {
4187 return $this->mConverter->armourMath( $text );
4188 }
4189
4190 /**
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
4194 * @return string
4195 * @todo this should get integrated somewhere sane
4196 */
4197 public function convertHtml( $text, $isTitle = false ) {
4198 return htmlspecialchars( $this->convert( $text, $isTitle ) );
4199 }
4200
4201 /**
4202 * @param string $key
4203 * @return string
4204 */
4205 public function convertCategoryKey( $key ) {
4206 return $this->mConverter->convertCategoryKey( $key );
4207 }
4208
4209 /**
4210 * Get the list of variants supported by this language
4211 * see sample implementation in LanguageZh.php
4212 *
4213 * @return array An array of language codes
4214 */
4215 public function getVariants() {
4216 return $this->mConverter->getVariants();
4217 }
4218
4219 /**
4220 * @return string
4221 */
4222 public function getPreferredVariant() {
4223 return $this->mConverter->getPreferredVariant();
4224 }
4225
4226 /**
4227 * @return string
4228 */
4229 public function getDefaultVariant() {
4230 return $this->mConverter->getDefaultVariant();
4231 }
4232
4233 /**
4234 * @return string
4235 */
4236 public function getURLVariant() {
4237 return $this->mConverter->getURLVariant();
4238 }
4239
4240 /**
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
4246 *
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
4251 */
4252 public function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
4253 $this->mConverter->findVariantLink( $link, $nt, $ignoreOtherCond );
4254 }
4255
4256 /**
4257 * returns language specific options used by User::getPageRenderHash()
4258 * for example, the preferred language variant
4259 *
4260 * @return string
4261 */
4262 function getExtraHashOptions() {
4263 return $this->mConverter->getExtraHashOptions();
4264 }
4265
4266 /**
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.
4270 *
4271 * @return string
4272 */
4273 public function getParsedTitle() {
4274 return $this->mConverter->getParsedTitle();
4275 }
4276
4277 /**
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.
4281 *
4282 * This function is called "markNoConversion" for historical
4283 * reasons.
4284 *
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
4288 */
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 );
4293 } else {
4294 return $text;
4295 }
4296 }
4297
4298 /**
4299 * A regular expression to match legal word-trailing characters
4300 * which should be merged onto a link of the form [[foo]]bar.
4301 *
4302 * @return string
4303 */
4304 public function linkTrail() {
4305 return self::$dataCache->getItem( $this->mCode, 'linkTrail' );
4306 }
4307
4308 /**
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]].
4311 *
4312 * @return string
4313 */
4314 public function linkPrefixCharset() {
4315 return self::$dataCache->getItem( $this->mCode, 'linkPrefixCharset' );
4316 }
4317
4318 /**
4319 * @deprecated since 1.24, will be removed in 1.25
4320 * @return Language
4321 */
4322 function getLangObj() {
4323 wfDeprecated( __METHOD__, '1.24' );
4324 return $this;
4325 }
4326
4327 /**
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).
4330 *
4331 * @return Language|null
4332 * @since 1.22
4333 */
4334 public function getParentLanguage() {
4335 if ( $this->mParentLanguage !== false ) {
4336 return $this->mParentLanguage;
4337 }
4338
4339 $pieces = explode( '-', $this->getCode() );
4340 $code = $pieces[0];
4341 if ( !in_array( $code, LanguageConverter::$languagesWithVariants ) ) {
4342 $this->mParentLanguage = null;
4343 return null;
4344 }
4345 $lang = Language::factory( $code );
4346 if ( !$lang->hasVariant( $this->getCode() ) ) {
4347 $this->mParentLanguage = null;
4348 return null;
4349 }
4350
4351 $this->mParentLanguage = $lang;
4352 return $lang;
4353 }
4354
4355 /**
4356 * Get the RFC 3066 code for this language object
4357 *
4358 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
4359 * htmlspecialchars() or similar
4360 *
4361 * @return string
4362 */
4363 public function getCode() {
4364 return $this->mCode;
4365 }
4366
4367 /**
4368 * Get the code in Bcp47 format which we can use
4369 * inside of html lang="" tags.
4370 *
4371 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
4372 * htmlspecialchars() or similar.
4373 *
4374 * @since 1.19
4375 * @return string
4376 */
4377 public function getHtmlCode() {
4378 if ( is_null( $this->mHtmlCode ) ) {
4379 $this->mHtmlCode = wfBCP47( $this->getCode() );
4380 }
4381 return $this->mHtmlCode;
4382 }
4383
4384 /**
4385 * @param string $code
4386 */
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;
4392 }
4393
4394 /**
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
4401 */
4402 public static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
4403 if ( !self::isValidBuiltInCode( $code ) ) {
4404 throw new MWException( "Invalid language code \"$code\"" );
4405 }
4406
4407 return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
4408 }
4409
4410 /**
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
4416 */
4417 public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
4418 $m = null;
4419 preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
4420 preg_quote( $suffix, '/' ) . '/', $filename, $m );
4421 if ( !count( $m ) ) {
4422 return false;
4423 }
4424 return str_replace( '_', '-', strtolower( $m[1] ) );
4425 }
4426
4427 /**
4428 * @param string $code
4429 * @return string
4430 */
4431 public static function getMessagesFileName( $code ) {
4432 global $IP;
4433 $file = self::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
4434 Hooks::run( 'Language::getMessagesFileName', array( $code, &$file ) );
4435 return $file;
4436 }
4437
4438 /**
4439 * @param string $code
4440 * @return string
4441 * @since 1.23
4442 */
4443 public static function getJsonMessagesFileName( $code ) {
4444 global $IP;
4445
4446 if ( !self::isValidBuiltInCode( $code ) ) {
4447 throw new MWException( "Invalid language code \"$code\"" );
4448 }
4449
4450 return "$IP/languages/i18n/$code.json";
4451 }
4452
4453 /**
4454 * @param string $code
4455 * @return string
4456 */
4457 public static function getClassFileName( $code ) {
4458 global $IP;
4459 return self::getFileName( "$IP/languages/classes/Language", $code, '.php' );
4460 }
4461
4462 /**
4463 * Get the first fallback for a given language.
4464 *
4465 * @param string $code
4466 *
4467 * @return bool|string
4468 */
4469 public static function getFallbackFor( $code ) {
4470 if ( $code === 'en' || !Language::isValidBuiltInCode( $code ) ) {
4471 return false;
4472 } else {
4473 $fallbacks = self::getFallbacksFor( $code );
4474 return $fallbacks[0];
4475 }
4476 }
4477
4478 /**
4479 * Get the ordered list of fallback languages.
4480 *
4481 * @since 1.19
4482 * @param string $code Language code
4483 * @return array Non-empty array, ending in "en"
4484 */
4485 public static function getFallbacksFor( $code ) {
4486 if ( $code === 'en' || !Language::isValidBuiltInCode( $code ) ) {
4487 return array();
4488 }
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' );
4492 }
4493
4494 /**
4495 * Get the ordered list of fallback languages, ending with the fallback
4496 * language chain for the site language.
4497 *
4498 * @since 1.22
4499 * @param string $code Language code
4500 * @return array Array( fallbacks, site fallbacks )
4501 */
4502 public static function getFallbacksIncludingSiteLanguage( $code ) {
4503 global $wgLanguageCode;
4504
4505 // Usually, we will only store a tiny number of fallback chains, so we
4506 // keep them in static memory.
4507 $cacheKey = "{$code}-{$wgLanguageCode}";
4508
4509 if ( !array_key_exists( $cacheKey, self::$fallbackLanguageCache ) ) {
4510 $fallbacks = self::getFallbacksFor( $code );
4511
4512 // Append the site's fallback chain, including the site language itself
4513 $siteFallbacks = self::getFallbacksFor( $wgLanguageCode );
4514 array_unshift( $siteFallbacks, $wgLanguageCode );
4515
4516 // Eliminate any languages already included in the chain
4517 $siteFallbacks = array_diff( $siteFallbacks, $fallbacks );
4518
4519 self::$fallbackLanguageCache[$cacheKey] = array( $fallbacks, $siteFallbacks );
4520 }
4521 return self::$fallbackLanguageCache[$cacheKey];
4522 }
4523
4524 /**
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().
4528 *
4529 * @param string $code
4530 *
4531 * @return array
4532 */
4533 public static function getMessagesFor( $code ) {
4534 return self::getLocalisationCache()->getItem( $code, 'messages' );
4535 }
4536
4537 /**
4538 * Get a message for a given language
4539 *
4540 * @param string $key
4541 * @param string $code
4542 *
4543 * @return string
4544 */
4545 public static function getMessageFor( $key, $code ) {
4546 return self::getLocalisationCache()->getSubitem( $code, 'messages', $key );
4547 }
4548
4549 /**
4550 * Get all message keys for a given language. This is a faster alternative to
4551 * array_keys( Language::getMessagesFor( $code ) )
4552 *
4553 * @since 1.19
4554 * @param string $code Language code
4555 * @return array Array of message keys (strings)
4556 */
4557 public static function getMessageKeysFor( $code ) {
4558 return self::getLocalisationCache()->getSubItemList( $code, 'messages' );
4559 }
4560
4561 /**
4562 * @param string $talk
4563 * @return mixed
4564 */
4565 function fixVariableInNamespace( $talk ) {
4566 if ( strpos( $talk, '$1' ) === false ) {
4567 return $talk;
4568 }
4569
4570 global $wgMetaNamespace;
4571 $talk = str_replace( '$1', $wgMetaNamespace, $talk );
4572
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 );
4580 }
4581
4582 /**
4583 * @param string $m
4584 * @return string
4585 */
4586 function replaceGrammarInNamespace( $m ) {
4587 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
4588 }
4589
4590 /**
4591 * @throws MWException
4592 * @return array
4593 */
4594 static function getCaseMaps() {
4595 static $wikiUpperChars, $wikiLowerChars;
4596 if ( isset( $wikiUpperChars ) ) {
4597 return array( $wikiUpperChars, $wikiLowerChars );
4598 }
4599
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" );
4604 }
4605 $wikiUpperChars = $arr['wikiUpperChars'];
4606 $wikiLowerChars = $arr['wikiLowerChars'];
4607 return array( $wikiUpperChars, $wikiLowerChars );
4608 }
4609
4610 /**
4611 * Decode an expiry (block, protection, etc) which has come from the DB
4612 *
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
4617 * @return string
4618 * @since 1.18
4619 */
4620 public function formatExpiry( $expiry, $format = true, $infinity = 'infinity' ) {
4621 static $dbInfinity;
4622 if ( $dbInfinity === null ) {
4623 $dbInfinity = wfGetDB( DB_SLAVE )->getInfinity();
4624 }
4625
4626 if ( $expiry == '' || $expiry === 'infinity' || $expiry == $dbInfinity ) {
4627 return $format === true
4628 ? $this->getMessageFromDB( 'infiniteblock' )
4629 : $infinity;
4630 } else {
4631 return $format === true
4632 ? $this->timeanddate( $expiry, /* User preference timezone */ true )
4633 : wfTimestamp( $format, $expiry );
4634 }
4635 }
4636
4637 /**
4638 * @todo Document
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'
4644 * and friends.
4645 * For backwards compatibility, $format may also be one of the strings 'avoidseconds'
4646 * or 'avoidminutes'.
4647 * @return string
4648 */
4649 function formatTimePeriod( $seconds, $format = array() ) {
4650 if ( !is_array( $format ) ) {
4651 $format = array( 'avoid' => $format ); // For backwards compatibility
4652 }
4653 if ( !isset( $format['avoid'] ) ) {
4654 $format['avoid'] = false;
4655 }
4656 if ( !isset( $format['noabbrevs'] ) ) {
4657 $format['noabbrevs'] = false;
4658 }
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 );
4667
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 ) {
4678 $secondsPart = 0;
4679 $minutes++;
4680 }
4681 $s = $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4682 $s .= ' ';
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 ) {
4689 $secondsPart = 0;
4690 $minutes++;
4691 }
4692 if ( $minutes == 60 ) {
4693 $minutes = 0;
4694 $hours++;
4695 }
4696 $s = $hoursMsg->params( $this->formatNum( $hours ) )->text();
4697 $s .= ' ';
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();
4701 }
4702 } else {
4703 $days = floor( $seconds / 86400 );
4704 if ( $format['avoid'] === 'avoidminutes' ) {
4705 $hours = round( ( $seconds - $days * 86400 ) / 3600 );
4706 if ( $hours == 24 ) {
4707 $hours = 0;
4708 $days++;
4709 }
4710 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4711 $s .= ' ';
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 ) {
4717 $minutes = 0;
4718 $hours++;
4719 }
4720 if ( $hours == 24 ) {
4721 $hours = 0;
4722 $days++;
4723 }
4724 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4725 $s .= ' ';
4726 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4727 $s .= ' ';
4728 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4729 } else {
4730 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4731 $s .= ' ';
4732 $s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
4733 }
4734 }
4735 return $s;
4736 }
4737
4738 /**
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.
4742 *
4743 * This use base 1000. For base 1024 use formatSize(), for another base
4744 * see formatComputingNumbers().
4745 *
4746 * @param int $bps
4747 * @return string
4748 */
4749 function formatBitrate( $bps ) {
4750 return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" );
4751 }
4752
4753 /**
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
4757 * @return string
4758 */
4759 function formatComputingNumbers( $size, $boundary, $messageKey ) {
4760 if ( $size <= 0 ) {
4761 return str_replace( '$1', $this->formatNum( $size ),
4762 $this->getMessageFromDB( str_replace( '$1', '', $messageKey ) )
4763 );
4764 }
4765 $sizes = array( '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zeta', 'yotta' );
4766 $index = 0;
4767
4768 $maxIndex = count( $sizes ) - 1;
4769 while ( $size >= $boundary && $index < $maxIndex ) {
4770 $index++;
4771 $size /= $boundary;
4772 }
4773
4774 // For small sizes no decimal places necessary
4775 $round = 0;
4776 if ( $index > 1 ) {
4777 // For MB and bigger two decimal places are smarter
4778 $round = 2;
4779 }
4780 $msg = str_replace( '$1', $sizes[$index], $messageKey );
4781
4782 $size = round( $size, $round );
4783 $text = $this->getMessageFromDB( $msg );
4784 return str_replace( '$1', $this->formatNum( $size ), $text );
4785 }
4786
4787 /**
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
4790 *
4791 * This method use base 1024. For base 1000 use formatBitrate(), for
4792 * another base see formatComputingNumbers()
4793 *
4794 * @param int $size Size to format
4795 * @return string Plain text (not HTML)
4796 */
4797 function formatSize( $size ) {
4798 return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" );
4799 }
4800
4801 /**
4802 * Make a list item, used by various special pages
4803 *
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
4809 */
4810 function specialList( $page, $details, $oppositedm = true ) {
4811 if ( !$details ) {
4812 return $page;
4813 }
4814
4815 $dirmark = ( $oppositedm ? $this->getDirMark( true ) : '' ) . $this->getDirMark();
4816 return
4817 $page .
4818 $dirmark .
4819 $this->msg( 'word-separator' )->escaped() .
4820 $this->msg( 'parentheses' )->rawParams( $details )->escaped();
4821 }
4822
4823 /**
4824 * Generate (prev x| next x) (20|50|100...) type links for paging
4825 *
4826 * @param Title $title Title object to link
4827 * @param int $offset
4828 * @param int $limit
4829 * @param array $query Optional URL query parameter string
4830 * @param bool $atend Optional param for specified if this is the last page
4831 * @return string
4832 */
4833 public function viewPrevNext( Title $title, $offset, $limit,
4834 array $query = array(), $atend = false
4835 ) {
4836 // @todo FIXME: Why on earth this needs one message for the text and another one for tooltip?
4837
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' );
4843 } else {
4844 $plink = htmlspecialchars( $prev );
4845 }
4846
4847 # Make 'next' link
4848 $next = wfMessage( 'nextn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4849 if ( $atend ) {
4850 $nlink = htmlspecialchars( $next );
4851 } else {
4852 $nlink = $this->numLink( $title, $offset + $limit, $limit,
4853 $query, $next, 'nextn-title', 'mw-nextlink' );
4854 }
4855
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' );
4861 }
4862
4863 return wfMessage( 'viewprevnext' )->inLanguage( $this )->title( $title
4864 )->rawParams( $plink, $nlink, $this->pipeList( $numLinks ) )->escaped();
4865 }
4866
4867 /**
4868 * Helper function for viewPrevNext() that generates links
4869 *
4870 * @param Title $title Title object to link
4871 * @param int $offset
4872 * @param int $limit
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
4878 */
4879 private function numLink( Title $title, $offset, $limit, array $query, $link,
4880 $tooltipMsg, $class
4881 ) {
4882 $query = array( 'limit' => $limit, 'offset' => $offset ) + $query;
4883 $tooltip = wfMessage( $tooltipMsg )->inLanguage( $this )->title( $title )
4884 ->numParams( $limit )->text();
4885
4886 return Html::element( 'a', array( 'href' => $title->getLocalURL( $query ),
4887 'title' => $tooltip, 'class' => $class ), $link );
4888 }
4889
4890 /**
4891 * Get the conversion rule title, if any.
4892 *
4893 * @return string
4894 */
4895 public function getConvRuleTitle() {
4896 return $this->mConverter->getConvRuleTitle();
4897 }
4898
4899 /**
4900 * Get the compiled plural rules for the language
4901 * @since 1.20
4902 * @return array Associative array with plural form, and plural rule as key-value pairs
4903 */
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 ) {
4911 break;
4912 }
4913 }
4914 }
4915 return $pluralRules;
4916 }
4917
4918 /**
4919 * Get the plural rules for the language
4920 * @since 1.20
4921 * @return array Associative array with plural form number and plural rule as key-value pairs
4922 */
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 ) {
4930 break;
4931 }
4932 }
4933 }
4934 return $pluralRules;
4935 }
4936
4937 /**
4938 * Get the plural rule types for the language
4939 * @since 1.22
4940 * @return array Associative array with plural form number and plural rule type as key-value pairs
4941 */
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 ) {
4949 break;
4950 }
4951 }
4952 }
4953 return $pluralRuleTypes;
4954 }
4955
4956 /**
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
4960 */
4961 public function getPluralRuleIndexNumber( $number ) {
4962 $pluralRules = $this->getCompiledPluralRules();
4963 $form = CLDRPluralRuleEvaluator::evaluateCompiled( $number, $pluralRules );
4964 return $form;
4965 }
4966
4967 /**
4968 * Find the plural rule type appropriate for the given number
4969 * For example, if the language is set to Arabic, getPluralType(5) should
4970 * return 'few'.
4971 * @since 1.22
4972 * @param int $number
4973 * @return string The name of the plural rule type, e.g. one, two, few, many
4974 */
4975 public function getPluralRuleType( $number ) {
4976 $index = $this->getPluralRuleIndexNumber( $number );
4977 $pluralRuleTypes = $this->getPluralRuleTypes();
4978 if ( isset( $pluralRuleTypes[$index] ) ) {
4979 return $pluralRuleTypes[$index];
4980 } else {
4981 return 'other';
4982 }
4983 }
4984 }