Make lines short to pass phpcs in Language.php
[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 use CLDRPluralRuleParser\Evaluator;
38
39 /**
40 * Internationalisation code
41 * @ingroup Language
42 */
43 class Language {
44 /**
45 * @var LanguageConverter
46 */
47 public $mConverter;
48
49 public $mVariants, $mCode, $mLoaded = false;
50 public $mMagicExtensions = array(), $mMagicHookDone = false;
51 private $mHtmlCode = null, $mParentLanguage = false;
52
53 public $dateFormatStrings = array();
54 public $mExtendedSpecialPageAliases;
55
56 protected $namespaceNames, $mNamespaceIds, $namespaceAliases;
57
58 /**
59 * ReplacementArray object caches
60 */
61 public $transformData = array();
62
63 /**
64 * @var LocalisationCache
65 */
66 static public $dataCache;
67
68 static public $mLangObjCache = array();
69
70 static public $mWeekdayMsgs = array(
71 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
72 'friday', 'saturday'
73 );
74
75 static public $mWeekdayAbbrevMsgs = array(
76 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
77 );
78
79 static public $mMonthMsgs = array(
80 'january', 'february', 'march', 'april', 'may_long', 'june',
81 'july', 'august', 'september', 'october', 'november',
82 'december'
83 );
84 static public $mMonthGenMsgs = array(
85 'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
86 'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
87 'december-gen'
88 );
89 static public $mMonthAbbrevMsgs = array(
90 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
91 'sep', 'oct', 'nov', 'dec'
92 );
93
94 static public $mIranianCalendarMonthMsgs = array(
95 'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
96 'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
97 'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
98 'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
99 );
100
101 static public $mHebrewCalendarMonthMsgs = array(
102 'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
103 'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
104 'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
105 'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
106 'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
107 );
108
109 static public $mHebrewCalendarMonthGenMsgs = array(
110 'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
111 'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
112 'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
113 'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
114 'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
115 );
116
117 static public $mHijriCalendarMonthMsgs = array(
118 'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
119 'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
120 'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
121 'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
122 );
123
124 /**
125 * @since 1.20
126 * @var array
127 */
128 static public $durationIntervals = array(
129 'millennia' => 31556952000,
130 'centuries' => 3155695200,
131 'decades' => 315569520,
132 'years' => 31556952, // 86400 * ( 365 + ( 24 * 3 + 25 ) / 400 )
133 'weeks' => 604800,
134 'days' => 86400,
135 'hours' => 3600,
136 'minutes' => 60,
137 'seconds' => 1,
138 );
139
140 /**
141 * Cache for language fallbacks.
142 * @see Language::getFallbacksIncludingSiteLanguage
143 * @since 1.21
144 * @var array
145 */
146 static private $fallbackLanguageCache = array();
147
148 /**
149 * Cache for language names
150 * @var MapCacheLRU|null
151 */
152 static private $languageNameCache;
153
154 /**
155 * Unicode directional formatting characters, for embedBidi()
156 */
157 static private $lre = "\xE2\x80\xAA"; // U+202A LEFT-TO-RIGHT EMBEDDING
158 static private $rle = "\xE2\x80\xAB"; // U+202B RIGHT-TO-LEFT EMBEDDING
159 static private $pdf = "\xE2\x80\xAC"; // U+202C POP DIRECTIONAL FORMATTING
160
161 /**
162 * Directionality test regex for embedBidi(). Matches the first strong directionality codepoint:
163 * - in group 1 if it is LTR
164 * - in group 2 if it is RTL
165 * Does not match if there is no strong directionality codepoint.
166 *
167 * The form is '/(?:([strong ltr codepoint])|([strong rtl codepoint]))/u' .
168 *
169 * Generated by UnicodeJS (see tools/strongDir) from the UCD; see
170 * https://git.wikimedia.org/summary/unicodejs.git .
171 */
172 // @codingStandardsIgnoreStart
173 // @codeCoverageIgnoreStart
174 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';
175 // @codeCoverageIgnoreEnd
176 // @codingStandardsIgnoreEnd
177
178 /**
179 * Get a cached or new language object for a given language code
180 * @param string $code
181 * @return Language
182 */
183 static function factory( $code ) {
184 global $wgDummyLanguageCodes, $wgLangObjCacheSize;
185
186 if ( isset( $wgDummyLanguageCodes[$code] ) ) {
187 $code = $wgDummyLanguageCodes[$code];
188 }
189
190 // get the language object to process
191 $langObj = isset( self::$mLangObjCache[$code] )
192 ? self::$mLangObjCache[$code]
193 : self::newFromCode( $code );
194
195 // merge the language object in to get it up front in the cache
196 self::$mLangObjCache = array_merge( array( $code => $langObj ), self::$mLangObjCache );
197 // get rid of the oldest ones in case we have an overflow
198 self::$mLangObjCache = array_slice( self::$mLangObjCache, 0, $wgLangObjCacheSize, true );
199
200 return $langObj;
201 }
202
203 /**
204 * Create a language object for a given language code
205 * @param string $code
206 * @throws MWException
207 * @return Language
208 */
209 protected static function newFromCode( $code ) {
210 // Protect against path traversal below
211 if ( !Language::isValidCode( $code )
212 || strcspn( $code, ":/\\\000" ) !== strlen( $code )
213 ) {
214 throw new MWException( "Invalid language code \"$code\"" );
215 }
216
217 if ( !Language::isValidBuiltInCode( $code ) ) {
218 // It's not possible to customise this code with class files, so
219 // just return a Language object. This is to support uselang= hacks.
220 $lang = new Language;
221 $lang->setCode( $code );
222 return $lang;
223 }
224
225 // Check if there is a language class for the code
226 $class = self::classFromCode( $code );
227 self::preloadLanguageClass( $class );
228 if ( class_exists( $class ) ) {
229 $lang = new $class;
230 return $lang;
231 }
232
233 // Keep trying the fallback list until we find an existing class
234 $fallbacks = Language::getFallbacksFor( $code );
235 foreach ( $fallbacks as $fallbackCode ) {
236 if ( !Language::isValidBuiltInCode( $fallbackCode ) ) {
237 throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" );
238 }
239
240 $class = self::classFromCode( $fallbackCode );
241 self::preloadLanguageClass( $class );
242 if ( class_exists( $class ) ) {
243 $lang = Language::newFromCode( $fallbackCode );
244 $lang->setCode( $code );
245 return $lang;
246 }
247 }
248
249 throw new MWException( "Invalid fallback sequence for language '$code'" );
250 }
251
252 /**
253 * Checks whether any localisation is available for that language tag
254 * in MediaWiki (MessagesXx.php exists).
255 *
256 * @param string $code Language tag (in lower case)
257 * @return bool Whether language is supported
258 * @since 1.21
259 */
260 public static function isSupportedLanguage( $code ) {
261 if ( !self::isValidBuiltInCode( $code ) ) {
262 return false;
263 }
264
265 if ( $code === 'qqq' ) {
266 return false;
267 }
268
269 return is_readable( self::getMessagesFileName( $code ) ) ||
270 is_readable( self::getJsonMessagesFileName( $code ) );
271 }
272
273 /**
274 * Returns true if a language code string is a well-formed language tag
275 * according to RFC 5646.
276 * This function only checks well-formedness; it doesn't check that
277 * language, script or variant codes actually exist in the repositories.
278 *
279 * Based on regexes by Mark Davis of the Unicode Consortium:
280 * http://unicode.org/repos/cldr/trunk/tools/java/org/unicode/cldr/util/data/langtagRegex.txt
281 *
282 * @param string $code
283 * @param bool $lenient Whether to allow '_' as separator. The default is only '-'.
284 *
285 * @return bool
286 * @since 1.21
287 */
288 public static function isWellFormedLanguageTag( $code, $lenient = false ) {
289 $alpha = '[a-z]';
290 $digit = '[0-9]';
291 $alphanum = '[a-z0-9]';
292 $x = 'x'; # private use singleton
293 $singleton = '[a-wy-z]'; # other singleton
294 $s = $lenient ? '[-_]' : '-';
295
296 $language = "$alpha{2,8}|$alpha{2,3}$s$alpha{3}";
297 $script = "$alpha{4}"; # ISO 15924
298 $region = "(?:$alpha{2}|$digit{3})"; # ISO 3166-1 alpha-2 or UN M.49
299 $variant = "(?:$alphanum{5,8}|$digit$alphanum{3})";
300 $extension = "$singleton(?:$s$alphanum{2,8})+";
301 $privateUse = "$x(?:$s$alphanum{1,8})+";
302
303 # Define certain grandfathered codes, since otherwise the regex is pretty useless.
304 # Since these are limited, this is safe even later changes to the registry --
305 # the only oddity is that it might change the type of the tag, and thus
306 # the results from the capturing groups.
307 # http://www.iana.org/assignments/language-subtag-registry
308
309 $grandfathered = "en{$s}GB{$s}oed"
310 . "|i{$s}(?:ami|bnn|default|enochian|hak|klingon|lux|mingo|navajo|pwn|tao|tay|tsu)"
311 . "|no{$s}(?:bok|nyn)"
312 . "|sgn{$s}(?:BE{$s}(?:fr|nl)|CH{$s}de)"
313 . "|zh{$s}min{$s}nan";
314
315 $variantList = "$variant(?:$s$variant)*";
316 $extensionList = "$extension(?:$s$extension)*";
317
318 $langtag = "(?:($language)"
319 . "(?:$s$script)?"
320 . "(?:$s$region)?"
321 . "(?:$s$variantList)?"
322 . "(?:$s$extensionList)?"
323 . "(?:$s$privateUse)?)";
324
325 # The final breakdown, with capturing groups for each of these components
326 # The variants, extensions, grandfathered, and private-use may have interior '-'
327
328 $root = "^(?:$langtag|$privateUse|$grandfathered)$";
329
330 return (bool)preg_match( "/$root/", strtolower( $code ) );
331 }
332
333 /**
334 * Returns true if a language code string is of a valid form, whether or
335 * not it exists. This includes codes which are used solely for
336 * customisation via the MediaWiki namespace.
337 *
338 * @param string $code
339 *
340 * @return bool
341 */
342 public static function isValidCode( $code ) {
343 static $cache = array();
344 if ( isset( $cache[$code] ) ) {
345 return $cache[$code];
346 }
347 // People think language codes are html safe, so enforce it.
348 // Ideally we should only allow a-zA-Z0-9-
349 // but, .+ and other chars are often used for {{int:}} hacks
350 // see bugs 37564, 37587, 36938
351 $cache[$code] =
352 strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
353 && !preg_match( MediaWikiTitleCodec::getTitleInvalidRegex(), $code );
354
355 return $cache[$code];
356 }
357
358 /**
359 * Returns true if a language code is of a valid form for the purposes of
360 * internal customisation of MediaWiki, via Messages*.php or *.json.
361 *
362 * @param string $code
363 *
364 * @throws MWException
365 * @since 1.18
366 * @return bool
367 */
368 public static function isValidBuiltInCode( $code ) {
369
370 if ( !is_string( $code ) ) {
371 if ( is_object( $code ) ) {
372 $addmsg = " of class " . get_class( $code );
373 } else {
374 $addmsg = '';
375 }
376 $type = gettype( $code );
377 throw new MWException( __METHOD__ . " must be passed a string, $type given$addmsg" );
378 }
379
380 return (bool)preg_match( '/^[a-z0-9-]{2,}$/', $code );
381 }
382
383 /**
384 * Returns true if a language code is an IETF tag known to MediaWiki.
385 *
386 * @param string $tag
387 *
388 * @since 1.21
389 * @return bool
390 */
391 public static function isKnownLanguageTag( $tag ) {
392 static $coreLanguageNames;
393
394 // Quick escape for invalid input to avoid exceptions down the line
395 // when code tries to process tags which are not valid at all.
396 if ( !self::isValidBuiltInCode( $tag ) ) {
397 return false;
398 }
399
400 if ( $coreLanguageNames === null ) {
401 global $IP;
402 include "$IP/languages/Names.php";
403 }
404
405 if ( isset( $coreLanguageNames[$tag] )
406 || self::fetchLanguageName( $tag, $tag ) !== ''
407 ) {
408 return true;
409 }
410
411 return false;
412 }
413
414 /**
415 * @param string $code
416 * @return string Name of the language class
417 */
418 public static function classFromCode( $code ) {
419 if ( $code == 'en' ) {
420 return 'Language';
421 } else {
422 return 'Language' . str_replace( '-', '_', ucfirst( $code ) );
423 }
424 }
425
426 /**
427 * Includes language class files
428 *
429 * @param string $class Name of the language class
430 */
431 public static function preloadLanguageClass( $class ) {
432 global $IP;
433
434 if ( $class === 'Language' ) {
435 return;
436 }
437
438 if ( file_exists( "$IP/languages/classes/$class.php" ) ) {
439 include_once "$IP/languages/classes/$class.php";
440 }
441 }
442
443 /**
444 * Get the LocalisationCache instance
445 *
446 * @return LocalisationCache
447 */
448 public static function getLocalisationCache() {
449 if ( is_null( self::$dataCache ) ) {
450 global $wgLocalisationCacheConf;
451 $class = $wgLocalisationCacheConf['class'];
452 self::$dataCache = new $class( $wgLocalisationCacheConf );
453 }
454 return self::$dataCache;
455 }
456
457 function __construct() {
458 $this->mConverter = new FakeConverter( $this );
459 // Set the code to the name of the descendant
460 if ( get_class( $this ) == 'Language' ) {
461 $this->mCode = 'en';
462 } else {
463 $this->mCode = str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) );
464 }
465 self::getLocalisationCache();
466 }
467
468 /**
469 * Reduce memory usage
470 */
471 function __destruct() {
472 foreach ( $this as $name => $value ) {
473 unset( $this->$name );
474 }
475 }
476
477 /**
478 * Hook which will be called if this is the content language.
479 * Descendants can use this to register hook functions or modify globals
480 */
481 function initContLang() {
482 }
483
484 /**
485 * @return array
486 * @since 1.19
487 */
488 function getFallbackLanguages() {
489 return self::getFallbacksFor( $this->mCode );
490 }
491
492 /**
493 * Exports $wgBookstoreListEn
494 * @return array
495 */
496 function getBookstoreList() {
497 return self::$dataCache->getItem( $this->mCode, 'bookstoreList' );
498 }
499
500 /**
501 * Returns an array of localised namespaces indexed by their numbers. If the namespace is not
502 * available in localised form, it will be included in English.
503 *
504 * @return array
505 */
506 public function getNamespaces() {
507 if ( is_null( $this->namespaceNames ) ) {
508 global $wgMetaNamespace, $wgMetaNamespaceTalk, $wgExtraNamespaces;
509
510 $this->namespaceNames = self::$dataCache->getItem( $this->mCode, 'namespaceNames' );
511 $validNamespaces = MWNamespace::getCanonicalNamespaces();
512
513 $this->namespaceNames = $wgExtraNamespaces + $this->namespaceNames + $validNamespaces;
514
515 $this->namespaceNames[NS_PROJECT] = $wgMetaNamespace;
516 if ( $wgMetaNamespaceTalk ) {
517 $this->namespaceNames[NS_PROJECT_TALK] = $wgMetaNamespaceTalk;
518 } else {
519 $talk = $this->namespaceNames[NS_PROJECT_TALK];
520 $this->namespaceNames[NS_PROJECT_TALK] =
521 $this->fixVariableInNamespace( $talk );
522 }
523
524 # Sometimes a language will be localised but not actually exist on this wiki.
525 foreach ( $this->namespaceNames as $key => $text ) {
526 if ( !isset( $validNamespaces[$key] ) ) {
527 unset( $this->namespaceNames[$key] );
528 }
529 }
530
531 # The above mixing may leave namespaces out of canonical order.
532 # Re-order by namespace ID number...
533 ksort( $this->namespaceNames );
534
535 Hooks::run( 'LanguageGetNamespaces', array( &$this->namespaceNames ) );
536 }
537
538 return $this->namespaceNames;
539 }
540
541 /**
542 * Arbitrarily set all of the namespace names at once. Mainly used for testing
543 * @param array $namespaces Array of namespaces (id => name)
544 */
545 public function setNamespaces( array $namespaces ) {
546 $this->namespaceNames = $namespaces;
547 $this->mNamespaceIds = null;
548 }
549
550 /**
551 * Resets all of the namespace caches. Mainly used for testing
552 */
553 public function resetNamespaces() {
554 $this->namespaceNames = null;
555 $this->mNamespaceIds = null;
556 $this->namespaceAliases = null;
557 }
558
559 /**
560 * A convenience function that returns getNamespaces() with spaces instead of underscores
561 * in values. Useful for producing output to be displayed e.g. in `<select>` forms.
562 *
563 * @return array
564 */
565 function getFormattedNamespaces() {
566 $ns = $this->getNamespaces();
567 foreach ( $ns as $k => $v ) {
568 $ns[$k] = strtr( $v, '_', ' ' );
569 }
570 return $ns;
571 }
572
573 /**
574 * Get a namespace value by key
575 *
576 * <code>
577 * $mw_ns = $wgContLang->getNsText( NS_MEDIAWIKI );
578 * echo $mw_ns; // prints 'MediaWiki'
579 * </code>
580 *
581 * @param int $index The array key of the namespace to return
582 * @return string|bool String if the namespace value exists, otherwise false
583 */
584 function getNsText( $index ) {
585 $ns = $this->getNamespaces();
586 return isset( $ns[$index] ) ? $ns[$index] : false;
587 }
588
589 /**
590 * A convenience function that returns the same thing as
591 * getNsText() except with '_' changed to ' ', useful for
592 * producing output.
593 *
594 * <code>
595 * $mw_ns = $wgContLang->getFormattedNsText( NS_MEDIAWIKI_TALK );
596 * echo $mw_ns; // prints 'MediaWiki talk'
597 * </code>
598 *
599 * @param int $index The array key of the namespace to return
600 * @return string Namespace name without underscores (empty string if namespace does not exist)
601 */
602 function getFormattedNsText( $index ) {
603 $ns = $this->getNsText( $index );
604 return strtr( $ns, '_', ' ' );
605 }
606
607 /**
608 * Returns gender-dependent namespace alias if available.
609 * See https://www.mediawiki.org/wiki/Manual:$wgExtraGenderNamespaces
610 * @param int $index Namespace index
611 * @param string $gender Gender key (male, female... )
612 * @return string
613 * @since 1.18
614 */
615 function getGenderNsText( $index, $gender ) {
616 global $wgExtraGenderNamespaces;
617
618 $ns = $wgExtraGenderNamespaces +
619 (array)self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
620
621 return isset( $ns[$index][$gender] ) ? $ns[$index][$gender] : $this->getNsText( $index );
622 }
623
624 /**
625 * Whether this language uses gender-dependent namespace aliases.
626 * See https://www.mediawiki.org/wiki/Manual:$wgExtraGenderNamespaces
627 * @return bool
628 * @since 1.18
629 */
630 function needsGenderDistinction() {
631 global $wgExtraGenderNamespaces, $wgExtraNamespaces;
632 if ( count( $wgExtraGenderNamespaces ) > 0 ) {
633 // $wgExtraGenderNamespaces overrides everything
634 return true;
635 } elseif ( isset( $wgExtraNamespaces[NS_USER] ) && isset( $wgExtraNamespaces[NS_USER_TALK] ) ) {
636 /// @todo There may be other gender namespace than NS_USER & NS_USER_TALK in the future
637 // $wgExtraNamespaces overrides any gender aliases specified in i18n files
638 return false;
639 } else {
640 // Check what is in i18n files
641 $aliases = self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
642 return count( $aliases ) > 0;
643 }
644 }
645
646 /**
647 * Get a namespace key by value, case insensitive.
648 * Only matches namespace names for the current language, not the
649 * canonical ones defined in Namespace.php.
650 *
651 * @param string $text
652 * @return int|bool An integer if $text is a valid value otherwise false
653 */
654 function getLocalNsIndex( $text ) {
655 $lctext = $this->lc( $text );
656 $ids = $this->getNamespaceIds();
657 return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
658 }
659
660 /**
661 * @return array
662 */
663 function getNamespaceAliases() {
664 if ( is_null( $this->namespaceAliases ) ) {
665 $aliases = self::$dataCache->getItem( $this->mCode, 'namespaceAliases' );
666 if ( !$aliases ) {
667 $aliases = array();
668 } else {
669 foreach ( $aliases as $name => $index ) {
670 if ( $index === NS_PROJECT_TALK ) {
671 unset( $aliases[$name] );
672 $name = $this->fixVariableInNamespace( $name );
673 $aliases[$name] = $index;
674 }
675 }
676 }
677
678 global $wgExtraGenderNamespaces;
679 $genders = $wgExtraGenderNamespaces +
680 (array)self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
681 foreach ( $genders as $index => $forms ) {
682 foreach ( $forms as $alias ) {
683 $aliases[$alias] = $index;
684 }
685 }
686
687 # Also add converted namespace names as aliases, to avoid confusion.
688 $convertedNames = array();
689 foreach ( $this->getVariants() as $variant ) {
690 if ( $variant === $this->mCode ) {
691 continue;
692 }
693 foreach ( $this->getNamespaces() as $ns => $_ ) {
694 $convertedNames[$this->getConverter()->convertNamespace( $ns, $variant )] = $ns;
695 }
696 }
697
698 $this->namespaceAliases = $aliases + $convertedNames;
699 }
700
701 return $this->namespaceAliases;
702 }
703
704 /**
705 * @return array
706 */
707 function getNamespaceIds() {
708 if ( is_null( $this->mNamespaceIds ) ) {
709 global $wgNamespaceAliases;
710 # Put namespace names and aliases into a hashtable.
711 # If this is too slow, then we should arrange it so that it is done
712 # before caching. The catch is that at pre-cache time, the above
713 # class-specific fixup hasn't been done.
714 $this->mNamespaceIds = array();
715 foreach ( $this->getNamespaces() as $index => $name ) {
716 $this->mNamespaceIds[$this->lc( $name )] = $index;
717 }
718 foreach ( $this->getNamespaceAliases() as $name => $index ) {
719 $this->mNamespaceIds[$this->lc( $name )] = $index;
720 }
721 if ( $wgNamespaceAliases ) {
722 foreach ( $wgNamespaceAliases as $name => $index ) {
723 $this->mNamespaceIds[$this->lc( $name )] = $index;
724 }
725 }
726 }
727 return $this->mNamespaceIds;
728 }
729
730 /**
731 * Get a namespace key by value, case insensitive. Canonical namespace
732 * names override custom ones defined for the current language.
733 *
734 * @param string $text
735 * @return int|bool An integer if $text is a valid value otherwise false
736 */
737 function getNsIndex( $text ) {
738 $lctext = $this->lc( $text );
739 $ns = MWNamespace::getCanonicalIndex( $lctext );
740 if ( $ns !== null ) {
741 return $ns;
742 }
743 $ids = $this->getNamespaceIds();
744 return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
745 }
746
747 /**
748 * short names for language variants used for language conversion links.
749 *
750 * @param string $code
751 * @param bool $usemsg Use the "variantname-xyz" message if it exists
752 * @return string
753 */
754 function getVariantname( $code, $usemsg = true ) {
755 $msg = "variantname-$code";
756 if ( $usemsg && wfMessage( $msg )->exists() ) {
757 return $this->getMessageFromDB( $msg );
758 }
759 $name = self::fetchLanguageName( $code );
760 if ( $name ) {
761 return $name; # if it's defined as a language name, show that
762 } else {
763 # otherwise, output the language code
764 return $code;
765 }
766 }
767
768 /**
769 * @deprecated since 1.24, doesn't handle conflicting aliases. Use
770 * SpecialPageFactory::getLocalNameFor instead.
771 * @param string $name
772 * @return string
773 */
774 function specialPage( $name ) {
775 $aliases = $this->getSpecialPageAliases();
776 if ( isset( $aliases[$name][0] ) ) {
777 $name = $aliases[$name][0];
778 }
779 return $this->getNsText( NS_SPECIAL ) . ':' . $name;
780 }
781
782 /**
783 * @return array
784 */
785 function getDatePreferences() {
786 return self::$dataCache->getItem( $this->mCode, 'datePreferences' );
787 }
788
789 /**
790 * @return array
791 */
792 function getDateFormats() {
793 return self::$dataCache->getItem( $this->mCode, 'dateFormats' );
794 }
795
796 /**
797 * @return array|string
798 */
799 function getDefaultDateFormat() {
800 $df = self::$dataCache->getItem( $this->mCode, 'defaultDateFormat' );
801 if ( $df === 'dmy or mdy' ) {
802 global $wgAmericanDates;
803 return $wgAmericanDates ? 'mdy' : 'dmy';
804 } else {
805 return $df;
806 }
807 }
808
809 /**
810 * @return array
811 */
812 function getDatePreferenceMigrationMap() {
813 return self::$dataCache->getItem( $this->mCode, 'datePreferenceMigrationMap' );
814 }
815
816 /**
817 * @param string $image
818 * @return array|null
819 */
820 function getImageFile( $image ) {
821 return self::$dataCache->getSubitem( $this->mCode, 'imageFiles', $image );
822 }
823
824 /**
825 * @return array
826 * @since 1.24
827 */
828 function getImageFiles() {
829 return self::$dataCache->getItem( $this->mCode, 'imageFiles' );
830 }
831
832 /**
833 * @return array
834 */
835 function getExtraUserToggles() {
836 return (array)self::$dataCache->getItem( $this->mCode, 'extraUserToggles' );
837 }
838
839 /**
840 * @param string $tog
841 * @return string
842 */
843 function getUserToggle( $tog ) {
844 return $this->getMessageFromDB( "tog-$tog" );
845 }
846
847 /**
848 * Get native language names, indexed by code.
849 * Only those defined in MediaWiki, no other data like CLDR.
850 * If $customisedOnly is true, only returns codes with a messages file
851 *
852 * @param bool $customisedOnly
853 *
854 * @return array
855 * @deprecated since 1.20, use fetchLanguageNames()
856 */
857 public static function getLanguageNames( $customisedOnly = false ) {
858 return self::fetchLanguageNames( null, $customisedOnly ? 'mwfile' : 'mw' );
859 }
860
861 /**
862 * Get translated language names. This is done on best effort and
863 * by default this is exactly the same as Language::getLanguageNames.
864 * The CLDR extension provides translated names.
865 * @param string $code Language code.
866 * @return array Language code => language name
867 * @since 1.18.0
868 * @deprecated since 1.20, use fetchLanguageNames()
869 */
870 public static function getTranslatedLanguageNames( $code ) {
871 return self::fetchLanguageNames( $code, 'all' );
872 }
873
874 /**
875 * Get an array of language names, indexed by code.
876 * @param null|string $inLanguage Code of language in which to return the names
877 * Use null for autonyms (native names)
878 * @param string $include One of:
879 * 'all' all available languages
880 * 'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
881 * 'mwfile' only if the language is in 'mw' *and* has a message file
882 * @return array Language code => language name
883 * @since 1.20
884 */
885 public static function fetchLanguageNames( $inLanguage = null, $include = 'mw' ) {
886 $cacheKey = $inLanguage === null ? 'null' : $inLanguage;
887 $cacheKey .= ":$include";
888 if ( self::$languageNameCache === null ) {
889 self::$languageNameCache = new MapCacheLRU( 20 );
890 }
891 if ( self::$languageNameCache->has( $cacheKey ) ) {
892 $ret = self::$languageNameCache->get( $cacheKey );
893 } else {
894 $ret = self::fetchLanguageNamesUncached( $inLanguage, $include );
895 self::$languageNameCache->set( $cacheKey, $ret );
896 }
897 return $ret;
898 }
899
900 /**
901 * Uncached helper for fetchLanguageNames
902 * @param null|string $inLanguage Code of language in which to return the names
903 * Use null for autonyms (native names)
904 * @param string $include One of:
905 * 'all' all available languages
906 * 'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
907 * 'mwfile' only if the language is in 'mw' *and* has a message file
908 * @return array Language code => language name
909 */
910 private static function fetchLanguageNamesUncached( $inLanguage = null, $include = 'mw' ) {
911 global $wgExtraLanguageNames;
912 static $coreLanguageNames;
913
914 if ( $coreLanguageNames === null ) {
915 global $IP;
916 include "$IP/languages/Names.php";
917 }
918
919 // If passed an invalid language code to use, fallback to en
920 if ( $inLanguage !== null && !Language::isValidCode( $inLanguage ) ) {
921 $inLanguage = 'en';
922 }
923
924 $names = array();
925
926 if ( $inLanguage ) {
927 # TODO: also include when $inLanguage is null, when this code is more efficient
928 Hooks::run( 'LanguageGetTranslatedLanguageNames', array( &$names, $inLanguage ) );
929 }
930
931 $mwNames = $wgExtraLanguageNames + $coreLanguageNames;
932 foreach ( $mwNames as $mwCode => $mwName ) {
933 # - Prefer own MediaWiki native name when not using the hook
934 # - For other names just add if not added through the hook
935 if ( $mwCode === $inLanguage || !isset( $names[$mwCode] ) ) {
936 $names[$mwCode] = $mwName;
937 }
938 }
939
940 if ( $include === 'all' ) {
941 ksort( $names );
942 return $names;
943 }
944
945 $returnMw = array();
946 $coreCodes = array_keys( $mwNames );
947 foreach ( $coreCodes as $coreCode ) {
948 $returnMw[$coreCode] = $names[$coreCode];
949 }
950
951 if ( $include === 'mwfile' ) {
952 $namesMwFile = array();
953 # We do this using a foreach over the codes instead of a directory
954 # loop so that messages files in extensions will work correctly.
955 foreach ( $returnMw as $code => $value ) {
956 if ( is_readable( self::getMessagesFileName( $code ) )
957 || is_readable( self::getJsonMessagesFileName( $code ) )
958 ) {
959 $namesMwFile[$code] = $names[$code];
960 }
961 }
962
963 ksort( $namesMwFile );
964 return $namesMwFile;
965 }
966
967 ksort( $returnMw );
968 # 'mw' option; default if it's not one of the other two options (all/mwfile)
969 return $returnMw;
970 }
971
972 /**
973 * @param string $code The code of the language for which to get the name
974 * @param null|string $inLanguage Code of language in which to return the name (null for autonyms)
975 * @param string $include 'all', 'mw' or 'mwfile'; see fetchLanguageNames()
976 * @return string Language name or empty
977 * @since 1.20
978 */
979 public static function fetchLanguageName( $code, $inLanguage = null, $include = 'all' ) {
980 $code = strtolower( $code );
981 $array = self::fetchLanguageNames( $inLanguage, $include );
982 return !array_key_exists( $code, $array ) ? '' : $array[$code];
983 }
984
985 /**
986 * Get a message from the MediaWiki namespace.
987 *
988 * @param string $msg Message name
989 * @return string
990 */
991 function getMessageFromDB( $msg ) {
992 return $this->msg( $msg )->text();
993 }
994
995 /**
996 * Get message object in this language. Only for use inside this class.
997 *
998 * @param string $msg Message name
999 * @return Message
1000 */
1001 protected function msg( $msg ) {
1002 return wfMessage( $msg )->inLanguage( $this );
1003 }
1004
1005 /**
1006 * Get the native language name of $code.
1007 * Only if defined in MediaWiki, no other data like CLDR.
1008 * @param string $code
1009 * @return string
1010 * @deprecated since 1.20, use fetchLanguageName()
1011 */
1012 function getLanguageName( $code ) {
1013 return self::fetchLanguageName( $code );
1014 }
1015
1016 /**
1017 * @param string $key
1018 * @return string
1019 */
1020 function getMonthName( $key ) {
1021 return $this->getMessageFromDB( self::$mMonthMsgs[$key - 1] );
1022 }
1023
1024 /**
1025 * @return array
1026 */
1027 function getMonthNamesArray() {
1028 $monthNames = array( '' );
1029 for ( $i = 1; $i < 13; $i++ ) {
1030 $monthNames[] = $this->getMonthName( $i );
1031 }
1032 return $monthNames;
1033 }
1034
1035 /**
1036 * @param string $key
1037 * @return string
1038 */
1039 function getMonthNameGen( $key ) {
1040 return $this->getMessageFromDB( self::$mMonthGenMsgs[$key - 1] );
1041 }
1042
1043 /**
1044 * @param string $key
1045 * @return string
1046 */
1047 function getMonthAbbreviation( $key ) {
1048 return $this->getMessageFromDB( self::$mMonthAbbrevMsgs[$key - 1] );
1049 }
1050
1051 /**
1052 * @return array
1053 */
1054 function getMonthAbbreviationsArray() {
1055 $monthNames = array( '' );
1056 for ( $i = 1; $i < 13; $i++ ) {
1057 $monthNames[] = $this->getMonthAbbreviation( $i );
1058 }
1059 return $monthNames;
1060 }
1061
1062 /**
1063 * @param string $key
1064 * @return string
1065 */
1066 function getWeekdayName( $key ) {
1067 return $this->getMessageFromDB( self::$mWeekdayMsgs[$key - 1] );
1068 }
1069
1070 /**
1071 * @param string $key
1072 * @return string
1073 */
1074 function getWeekdayAbbreviation( $key ) {
1075 return $this->getMessageFromDB( self::$mWeekdayAbbrevMsgs[$key - 1] );
1076 }
1077
1078 /**
1079 * @param string $key
1080 * @return string
1081 */
1082 function getIranianCalendarMonthName( $key ) {
1083 return $this->getMessageFromDB( self::$mIranianCalendarMonthMsgs[$key - 1] );
1084 }
1085
1086 /**
1087 * @param string $key
1088 * @return string
1089 */
1090 function getHebrewCalendarMonthName( $key ) {
1091 return $this->getMessageFromDB( self::$mHebrewCalendarMonthMsgs[$key - 1] );
1092 }
1093
1094 /**
1095 * @param string $key
1096 * @return string
1097 */
1098 function getHebrewCalendarMonthNameGen( $key ) {
1099 return $this->getMessageFromDB( self::$mHebrewCalendarMonthGenMsgs[$key - 1] );
1100 }
1101
1102 /**
1103 * @param string $key
1104 * @return string
1105 */
1106 function getHijriCalendarMonthName( $key ) {
1107 return $this->getMessageFromDB( self::$mHijriCalendarMonthMsgs[$key - 1] );
1108 }
1109
1110 /**
1111 * Pass through result from $dateTimeObj->format()
1112 * @param DateTime|bool|null &$dateTimeObj
1113 * @param string $ts
1114 * @param DateTimeZone|bool|null $zone
1115 * @param string $code
1116 * @return string
1117 */
1118 private static function dateTimeObjFormat( &$dateTimeObj, $ts, $zone, $code ) {
1119 if ( !$dateTimeObj ) {
1120 $dateTimeObj = DateTime::createFromFormat(
1121 'YmdHis', $ts, $zone ?: new DateTimeZone( 'UTC' )
1122 );
1123 }
1124 return $dateTimeObj->format( $code );
1125 }
1126
1127 /**
1128 * This is a workalike of PHP's date() function, but with better
1129 * internationalisation, a reduced set of format characters, and a better
1130 * escaping format.
1131 *
1132 * Supported format characters are dDjlNwzWFmMntLoYyaAgGhHiscrUeIOPTZ. See
1133 * the PHP manual for definitions. There are a number of extensions, which
1134 * start with "x":
1135 *
1136 * xn Do not translate digits of the next numeric format character
1137 * xN Toggle raw digit (xn) flag, stays set until explicitly unset
1138 * xr Use roman numerals for the next numeric format character
1139 * xh Use hebrew numerals for the next numeric format character
1140 * xx Literal x
1141 * xg Genitive month name
1142 *
1143 * xij j (day number) in Iranian calendar
1144 * xiF F (month name) in Iranian calendar
1145 * xin n (month number) in Iranian calendar
1146 * xiy y (two digit year) in Iranian calendar
1147 * xiY Y (full year) in Iranian calendar
1148 *
1149 * xjj j (day number) in Hebrew calendar
1150 * xjF F (month name) in Hebrew calendar
1151 * xjt t (days in month) in Hebrew calendar
1152 * xjx xg (genitive month name) in Hebrew calendar
1153 * xjn n (month number) in Hebrew calendar
1154 * xjY Y (full year) in Hebrew calendar
1155 *
1156 * xmj j (day number) in Hijri calendar
1157 * xmF F (month name) in Hijri calendar
1158 * xmn n (month number) in Hijri calendar
1159 * xmY Y (full year) in Hijri calendar
1160 *
1161 * xkY Y (full year) in Thai solar calendar. Months and days are
1162 * identical to the Gregorian calendar
1163 * xoY Y (full year) in Minguo calendar or Juche year.
1164 * Months and days are identical to the
1165 * Gregorian calendar
1166 * xtY Y (full year) in Japanese nengo. Months and days are
1167 * identical to the Gregorian calendar
1168 *
1169 * Characters enclosed in double quotes will be considered literal (with
1170 * the quotes themselves removed). Unmatched quotes will be considered
1171 * literal quotes. Example:
1172 *
1173 * "The month is" F => The month is January
1174 * i's" => 20'11"
1175 *
1176 * Backslash escaping is also supported.
1177 *
1178 * Input timestamp is assumed to be pre-normalized to the desired local
1179 * time zone, if any. Note that the format characters crUeIOPTZ will assume
1180 * $ts is UTC if $zone is not given.
1181 *
1182 * @param string $format
1183 * @param string $ts 14-character timestamp
1184 * YYYYMMDDHHMMSS
1185 * 01234567890123
1186 * @param DateTimeZone $zone Timezone of $ts
1187 * @param[out] int $ttl The amount of time (in seconds) the output may be cached for.
1188 * Only makes sense if $ts is the current time.
1189 * @todo handling of "o" format character for Iranian, Hebrew, Hijri & Thai?
1190 *
1191 * @throws MWException
1192 * @return string
1193 */
1194 function sprintfDate( $format, $ts, DateTimeZone $zone = null, &$ttl = null ) {
1195 $s = '';
1196 $raw = false;
1197 $roman = false;
1198 $hebrewNum = false;
1199 $dateTimeObj = false;
1200 $rawToggle = false;
1201 $iranian = false;
1202 $hebrew = false;
1203 $hijri = false;
1204 $thai = false;
1205 $minguo = false;
1206 $tenno = false;
1207
1208 $usedSecond = false;
1209 $usedMinute = false;
1210 $usedHour = false;
1211 $usedAMPM = false;
1212 $usedDay = false;
1213 $usedWeek = false;
1214 $usedMonth = false;
1215 $usedYear = false;
1216 $usedISOYear = false;
1217 $usedIsLeapYear = false;
1218
1219 $usedHebrewMonth = false;
1220 $usedIranianMonth = false;
1221 $usedHijriMonth = false;
1222 $usedHebrewYear = false;
1223 $usedIranianYear = false;
1224 $usedHijriYear = false;
1225 $usedTennoYear = false;
1226
1227 if ( strlen( $ts ) !== 14 ) {
1228 throw new MWException( __METHOD__ . ": The timestamp $ts should have 14 characters" );
1229 }
1230
1231 if ( !ctype_digit( $ts ) ) {
1232 throw new MWException( __METHOD__ . ": The timestamp $ts should be a number" );
1233 }
1234
1235 $formatLength = strlen( $format );
1236 for ( $p = 0; $p < $formatLength; $p++ ) {
1237 $num = false;
1238 $code = $format[$p];
1239 if ( $code == 'x' && $p < $formatLength - 1 ) {
1240 $code .= $format[++$p];
1241 }
1242
1243 if ( ( $code === 'xi'
1244 || $code === 'xj'
1245 || $code === 'xk'
1246 || $code === 'xm'
1247 || $code === 'xo'
1248 || $code === 'xt' )
1249 && $p < $formatLength - 1 ) {
1250 $code .= $format[++$p];
1251 }
1252
1253 switch ( $code ) {
1254 case 'xx':
1255 $s .= 'x';
1256 break;
1257 case 'xn':
1258 $raw = true;
1259 break;
1260 case 'xN':
1261 $rawToggle = !$rawToggle;
1262 break;
1263 case 'xr':
1264 $roman = true;
1265 break;
1266 case 'xh':
1267 $hebrewNum = true;
1268 break;
1269 case 'xg':
1270 $usedMonth = true;
1271 $s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) );
1272 break;
1273 case 'xjx':
1274 $usedHebrewMonth = true;
1275 if ( !$hebrew ) {
1276 $hebrew = self::tsToHebrew( $ts );
1277 }
1278 $s .= $this->getHebrewCalendarMonthNameGen( $hebrew[1] );
1279 break;
1280 case 'd':
1281 $usedDay = true;
1282 $num = substr( $ts, 6, 2 );
1283 break;
1284 case 'D':
1285 $usedDay = true;
1286 $s .= $this->getWeekdayAbbreviation(
1287 Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1
1288 );
1289 break;
1290 case 'j':
1291 $usedDay = true;
1292 $num = intval( substr( $ts, 6, 2 ) );
1293 break;
1294 case 'xij':
1295 $usedDay = true;
1296 if ( !$iranian ) {
1297 $iranian = self::tsToIranian( $ts );
1298 }
1299 $num = $iranian[2];
1300 break;
1301 case 'xmj':
1302 $usedDay = true;
1303 if ( !$hijri ) {
1304 $hijri = self::tsToHijri( $ts );
1305 }
1306 $num = $hijri[2];
1307 break;
1308 case 'xjj':
1309 $usedDay = true;
1310 if ( !$hebrew ) {
1311 $hebrew = self::tsToHebrew( $ts );
1312 }
1313 $num = $hebrew[2];
1314 break;
1315 case 'l':
1316 $usedDay = true;
1317 $s .= $this->getWeekdayName(
1318 Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1
1319 );
1320 break;
1321 case 'F':
1322 $usedMonth = true;
1323 $s .= $this->getMonthName( substr( $ts, 4, 2 ) );
1324 break;
1325 case 'xiF':
1326 $usedIranianMonth = true;
1327 if ( !$iranian ) {
1328 $iranian = self::tsToIranian( $ts );
1329 }
1330 $s .= $this->getIranianCalendarMonthName( $iranian[1] );
1331 break;
1332 case 'xmF':
1333 $usedHijriMonth = true;
1334 if ( !$hijri ) {
1335 $hijri = self::tsToHijri( $ts );
1336 }
1337 $s .= $this->getHijriCalendarMonthName( $hijri[1] );
1338 break;
1339 case 'xjF':
1340 $usedHebrewMonth = true;
1341 if ( !$hebrew ) {
1342 $hebrew = self::tsToHebrew( $ts );
1343 }
1344 $s .= $this->getHebrewCalendarMonthName( $hebrew[1] );
1345 break;
1346 case 'm':
1347 $usedMonth = true;
1348 $num = substr( $ts, 4, 2 );
1349 break;
1350 case 'M':
1351 $usedMonth = true;
1352 $s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) );
1353 break;
1354 case 'n':
1355 $usedMonth = true;
1356 $num = intval( substr( $ts, 4, 2 ) );
1357 break;
1358 case 'xin':
1359 $usedIranianMonth = true;
1360 if ( !$iranian ) {
1361 $iranian = self::tsToIranian( $ts );
1362 }
1363 $num = $iranian[1];
1364 break;
1365 case 'xmn':
1366 $usedHijriMonth = true;
1367 if ( !$hijri ) {
1368 $hijri = self::tsToHijri( $ts );
1369 }
1370 $num = $hijri[1];
1371 break;
1372 case 'xjn':
1373 $usedHebrewMonth = true;
1374 if ( !$hebrew ) {
1375 $hebrew = self::tsToHebrew( $ts );
1376 }
1377 $num = $hebrew[1];
1378 break;
1379 case 'xjt':
1380 $usedHebrewMonth = true;
1381 if ( !$hebrew ) {
1382 $hebrew = self::tsToHebrew( $ts );
1383 }
1384 $num = $hebrew[3];
1385 break;
1386 case 'Y':
1387 $usedYear = true;
1388 $num = substr( $ts, 0, 4 );
1389 break;
1390 case 'xiY':
1391 $usedIranianYear = true;
1392 if ( !$iranian ) {
1393 $iranian = self::tsToIranian( $ts );
1394 }
1395 $num = $iranian[0];
1396 break;
1397 case 'xmY':
1398 $usedHijriYear = true;
1399 if ( !$hijri ) {
1400 $hijri = self::tsToHijri( $ts );
1401 }
1402 $num = $hijri[0];
1403 break;
1404 case 'xjY':
1405 $usedHebrewYear = true;
1406 if ( !$hebrew ) {
1407 $hebrew = self::tsToHebrew( $ts );
1408 }
1409 $num = $hebrew[0];
1410 break;
1411 case 'xkY':
1412 $usedYear = true;
1413 if ( !$thai ) {
1414 $thai = self::tsToYear( $ts, 'thai' );
1415 }
1416 $num = $thai[0];
1417 break;
1418 case 'xoY':
1419 $usedYear = true;
1420 if ( !$minguo ) {
1421 $minguo = self::tsToYear( $ts, 'minguo' );
1422 }
1423 $num = $minguo[0];
1424 break;
1425 case 'xtY':
1426 $usedTennoYear = true;
1427 if ( !$tenno ) {
1428 $tenno = self::tsToYear( $ts, 'tenno' );
1429 }
1430 $num = $tenno[0];
1431 break;
1432 case 'y':
1433 $usedYear = true;
1434 $num = substr( $ts, 2, 2 );
1435 break;
1436 case 'xiy':
1437 $usedIranianYear = true;
1438 if ( !$iranian ) {
1439 $iranian = self::tsToIranian( $ts );
1440 }
1441 $num = substr( $iranian[0], -2 );
1442 break;
1443 case 'a':
1444 $usedAMPM = true;
1445 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm';
1446 break;
1447 case 'A':
1448 $usedAMPM = true;
1449 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'AM' : 'PM';
1450 break;
1451 case 'g':
1452 $usedHour = true;
1453 $h = substr( $ts, 8, 2 );
1454 $num = $h % 12 ? $h % 12 : 12;
1455 break;
1456 case 'G':
1457 $usedHour = true;
1458 $num = intval( substr( $ts, 8, 2 ) );
1459 break;
1460 case 'h':
1461 $usedHour = true;
1462 $h = substr( $ts, 8, 2 );
1463 $num = sprintf( '%02d', $h % 12 ? $h % 12 : 12 );
1464 break;
1465 case 'H':
1466 $usedHour = true;
1467 $num = substr( $ts, 8, 2 );
1468 break;
1469 case 'i':
1470 $usedMinute = true;
1471 $num = substr( $ts, 10, 2 );
1472 break;
1473 case 's':
1474 $usedSecond = true;
1475 $num = substr( $ts, 12, 2 );
1476 break;
1477 case 'c':
1478 case 'r':
1479 $usedSecond = true;
1480 // fall through
1481 case 'e':
1482 case 'O':
1483 case 'P':
1484 case 'T':
1485 $s .= Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1486 break;
1487 case 'w':
1488 case 'N':
1489 case 'z':
1490 $usedDay = true;
1491 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1492 break;
1493 case 'W':
1494 $usedWeek = true;
1495 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1496 break;
1497 case 't':
1498 $usedMonth = true;
1499 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1500 break;
1501 case 'L':
1502 $usedIsLeapYear = true;
1503 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1504 break;
1505 case 'o':
1506 $usedISOYear = true;
1507 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1508 break;
1509 case 'U':
1510 $usedSecond = true;
1511 // fall through
1512 case 'I':
1513 case 'Z':
1514 $num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1515 break;
1516 case '\\':
1517 # Backslash escaping
1518 if ( $p < $formatLength - 1 ) {
1519 $s .= $format[++$p];
1520 } else {
1521 $s .= '\\';
1522 }
1523 break;
1524 case '"':
1525 # Quoted literal
1526 if ( $p < $formatLength - 1 ) {
1527 $endQuote = strpos( $format, '"', $p + 1 );
1528 if ( $endQuote === false ) {
1529 # No terminating quote, assume literal "
1530 $s .= '"';
1531 } else {
1532 $s .= substr( $format, $p + 1, $endQuote - $p - 1 );
1533 $p = $endQuote;
1534 }
1535 } else {
1536 # Quote at end of string, assume literal "
1537 $s .= '"';
1538 }
1539 break;
1540 default:
1541 $s .= $format[$p];
1542 }
1543 if ( $num !== false ) {
1544 if ( $rawToggle || $raw ) {
1545 $s .= $num;
1546 $raw = false;
1547 } elseif ( $roman ) {
1548 $s .= Language::romanNumeral( $num );
1549 $roman = false;
1550 } elseif ( $hebrewNum ) {
1551 $s .= self::hebrewNumeral( $num );
1552 $hebrewNum = false;
1553 } else {
1554 $s .= $this->formatNum( $num, true );
1555 }
1556 }
1557 }
1558
1559 if ( $usedSecond ) {
1560 $ttl = 1;
1561 } elseif ( $usedMinute ) {
1562 $ttl = 60 - substr( $ts, 12, 2 );
1563 } elseif ( $usedHour ) {
1564 $ttl = 3600 - substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1565 } elseif ( $usedAMPM ) {
1566 $ttl = 43200 - ( substr( $ts, 8, 2 ) % 12 ) * 3600 -
1567 substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1568 } elseif (
1569 $usedDay ||
1570 $usedHebrewMonth ||
1571 $usedIranianMonth ||
1572 $usedHijriMonth ||
1573 $usedHebrewYear ||
1574 $usedIranianYear ||
1575 $usedHijriYear ||
1576 $usedTennoYear
1577 ) {
1578 // @todo Someone who understands the non-Gregorian calendars
1579 // should write proper logic for them so that they don't need purged every day.
1580 $ttl = 86400 - substr( $ts, 8, 2 ) * 3600 -
1581 substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1582 } else {
1583 $possibleTtls = array();
1584 $timeRemainingInDay = 86400 - substr( $ts, 8, 2 ) * 3600 -
1585 substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1586 if ( $usedWeek ) {
1587 $possibleTtls[] =
1588 ( 7 - Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400 +
1589 $timeRemainingInDay;
1590 } elseif ( $usedISOYear ) {
1591 // December 28th falls on the last ISO week of the year, every year.
1592 // The last ISO week of a year can be 52 or 53.
1593 $lastWeekOfISOYear = DateTime::createFromFormat(
1594 'Ymd',
1595 substr( $ts, 0, 4 ) . '1228',
1596 $zone ?: new DateTimeZone( 'UTC' )
1597 )->format( 'W' );
1598 $currentISOWeek = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'W' );
1599 $weeksRemaining = $lastWeekOfISOYear - $currentISOWeek;
1600 $timeRemainingInWeek =
1601 ( 7 - Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400
1602 + $timeRemainingInDay;
1603 $possibleTtls[] = $weeksRemaining * 604800 + $timeRemainingInWeek;
1604 }
1605
1606 if ( $usedMonth ) {
1607 $possibleTtls[] =
1608 ( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 't' ) -
1609 substr( $ts, 6, 2 ) ) * 86400
1610 + $timeRemainingInDay;
1611 } elseif ( $usedYear ) {
1612 $possibleTtls[] =
1613 ( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) + 364 -
1614 Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400
1615 + $timeRemainingInDay;
1616 } elseif ( $usedIsLeapYear ) {
1617 $year = substr( $ts, 0, 4 );
1618 $timeRemainingInYear =
1619 ( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) + 364 -
1620 Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400
1621 + $timeRemainingInDay;
1622 $mod = $year % 4;
1623 if ( $mod || ( !( $year % 100 ) && $year % 400 ) ) {
1624 // this isn't a leap year. see when the next one starts
1625 $nextCandidate = $year - $mod + 4;
1626 if ( $nextCandidate % 100 || !( $nextCandidate % 400 ) ) {
1627 $possibleTtls[] = ( $nextCandidate - $year - 1 ) * 365 * 86400 +
1628 $timeRemainingInYear;
1629 } else {
1630 $possibleTtls[] = ( $nextCandidate - $year + 3 ) * 365 * 86400 +
1631 $timeRemainingInYear;
1632 }
1633 } else {
1634 // this is a leap year, so the next year isn't
1635 $possibleTtls[] = $timeRemainingInYear;
1636 }
1637 }
1638
1639 if ( $possibleTtls ) {
1640 $ttl = min( $possibleTtls );
1641 }
1642 }
1643
1644 return $s;
1645 }
1646
1647 private static $GREG_DAYS = array( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
1648 private static $IRANIAN_DAYS = array( 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 );
1649
1650 /**
1651 * Algorithm by Roozbeh Pournader and Mohammad Toossi to convert
1652 * Gregorian dates to Iranian dates. Originally written in C, it
1653 * is released under the terms of GNU Lesser General Public
1654 * License. Conversion to PHP was performed by Niklas Laxström.
1655 *
1656 * Link: http://www.farsiweb.info/jalali/jalali.c
1657 *
1658 * @param string $ts
1659 *
1660 * @return string
1661 */
1662 private static function tsToIranian( $ts ) {
1663 $gy = substr( $ts, 0, 4 ) -1600;
1664 $gm = substr( $ts, 4, 2 ) -1;
1665 $gd = substr( $ts, 6, 2 ) -1;
1666
1667 # Days passed from the beginning (including leap years)
1668 $gDayNo = 365 * $gy
1669 + floor( ( $gy + 3 ) / 4 )
1670 - floor( ( $gy + 99 ) / 100 )
1671 + floor( ( $gy + 399 ) / 400 );
1672
1673 // Add days of the past months of this year
1674 for ( $i = 0; $i < $gm; $i++ ) {
1675 $gDayNo += self::$GREG_DAYS[$i];
1676 }
1677
1678 // Leap years
1679 if ( $gm > 1 && ( ( $gy % 4 === 0 && $gy % 100 !== 0 || ( $gy % 400 == 0 ) ) ) ) {
1680 $gDayNo++;
1681 }
1682
1683 // Days passed in current month
1684 $gDayNo += (int)$gd;
1685
1686 $jDayNo = $gDayNo - 79;
1687
1688 $jNp = floor( $jDayNo / 12053 );
1689 $jDayNo %= 12053;
1690
1691 $jy = 979 + 33 * $jNp + 4 * floor( $jDayNo / 1461 );
1692 $jDayNo %= 1461;
1693
1694 if ( $jDayNo >= 366 ) {
1695 $jy += floor( ( $jDayNo - 1 ) / 365 );
1696 $jDayNo = floor( ( $jDayNo - 1 ) % 365 );
1697 }
1698
1699 for ( $i = 0; $i < 11 && $jDayNo >= self::$IRANIAN_DAYS[$i]; $i++ ) {
1700 $jDayNo -= self::$IRANIAN_DAYS[$i];
1701 }
1702
1703 $jm = $i + 1;
1704 $jd = $jDayNo + 1;
1705
1706 return array( $jy, $jm, $jd );
1707 }
1708
1709 /**
1710 * Converting Gregorian dates to Hijri dates.
1711 *
1712 * Based on a PHP-Nuke block by Sharjeel which is released under GNU/GPL license
1713 *
1714 * @see http://phpnuke.org/modules.php?name=News&file=article&sid=8234&mode=thread&order=0&thold=0
1715 *
1716 * @param string $ts
1717 *
1718 * @return string
1719 */
1720 private static function tsToHijri( $ts ) {
1721 $year = substr( $ts, 0, 4 );
1722 $month = substr( $ts, 4, 2 );
1723 $day = substr( $ts, 6, 2 );
1724
1725 $zyr = $year;
1726 $zd = $day;
1727 $zm = $month;
1728 $zy = $zyr;
1729
1730 if (
1731 ( $zy > 1582 ) || ( ( $zy == 1582 ) && ( $zm > 10 ) ) ||
1732 ( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) )
1733 ) {
1734 $zjd = (int)( ( 1461 * ( $zy + 4800 + (int)( ( $zm - 14 ) / 12 ) ) ) / 4 ) +
1735 (int)( ( 367 * ( $zm - 2 - 12 * ( (int)( ( $zm - 14 ) / 12 ) ) ) ) / 12 ) -
1736 (int)( ( 3 * (int)( ( ( $zy + 4900 + (int)( ( $zm - 14 ) / 12 ) ) / 100 ) ) ) / 4 ) +
1737 $zd - 32075;
1738 } else {
1739 $zjd = 367 * $zy - (int)( ( 7 * ( $zy + 5001 + (int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) +
1740 (int)( ( 275 * $zm ) / 9 ) + $zd + 1729777;
1741 }
1742
1743 $zl = $zjd -1948440 + 10632;
1744 $zn = (int)( ( $zl - 1 ) / 10631 );
1745 $zl = $zl - 10631 * $zn + 354;
1746 $zj = ( (int)( ( 10985 - $zl ) / 5316 ) ) * ( (int)( ( 50 * $zl ) / 17719 ) ) +
1747 ( (int)( $zl / 5670 ) ) * ( (int)( ( 43 * $zl ) / 15238 ) );
1748 $zl = $zl - ( (int)( ( 30 - $zj ) / 15 ) ) * ( (int)( ( 17719 * $zj ) / 50 ) ) -
1749 ( (int)( $zj / 16 ) ) * ( (int)( ( 15238 * $zj ) / 43 ) ) + 29;
1750 $zm = (int)( ( 24 * $zl ) / 709 );
1751 $zd = $zl - (int)( ( 709 * $zm ) / 24 );
1752 $zy = 30 * $zn + $zj - 30;
1753
1754 return array( $zy, $zm, $zd );
1755 }
1756
1757 /**
1758 * Converting Gregorian dates to Hebrew dates.
1759 *
1760 * Based on a JavaScript code by Abu Mami and Yisrael Hersch
1761 * (abu-mami@kaluach.net, http://www.kaluach.net), who permitted
1762 * to translate the relevant functions into PHP and release them under
1763 * GNU GPL.
1764 *
1765 * The months are counted from Tishrei = 1. In a leap year, Adar I is 13
1766 * and Adar II is 14. In a non-leap year, Adar is 6.
1767 *
1768 * @param string $ts
1769 *
1770 * @return string
1771 */
1772 private static function tsToHebrew( $ts ) {
1773 # Parse date
1774 $year = substr( $ts, 0, 4 );
1775 $month = substr( $ts, 4, 2 );
1776 $day = substr( $ts, 6, 2 );
1777
1778 # Calculate Hebrew year
1779 $hebrewYear = $year + 3760;
1780
1781 # Month number when September = 1, August = 12
1782 $month += 4;
1783 if ( $month > 12 ) {
1784 # Next year
1785 $month -= 12;
1786 $year++;
1787 $hebrewYear++;
1788 }
1789
1790 # Calculate day of year from 1 September
1791 $dayOfYear = $day;
1792 for ( $i = 1; $i < $month; $i++ ) {
1793 if ( $i == 6 ) {
1794 # February
1795 $dayOfYear += 28;
1796 # Check if the year is leap
1797 if ( $year % 400 == 0 || ( $year % 4 == 0 && $year % 100 > 0 ) ) {
1798 $dayOfYear++;
1799 }
1800 } elseif ( $i == 8 || $i == 10 || $i == 1 || $i == 3 ) {
1801 $dayOfYear += 30;
1802 } else {
1803 $dayOfYear += 31;
1804 }
1805 }
1806
1807 # Calculate the start of the Hebrew year
1808 $start = self::hebrewYearStart( $hebrewYear );
1809
1810 # Calculate next year's start
1811 if ( $dayOfYear <= $start ) {
1812 # Day is before the start of the year - it is the previous year
1813 # Next year's start
1814 $nextStart = $start;
1815 # Previous year
1816 $year--;
1817 $hebrewYear--;
1818 # Add days since previous year's 1 September
1819 $dayOfYear += 365;
1820 if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1821 # Leap year
1822 $dayOfYear++;
1823 }
1824 # Start of the new (previous) year
1825 $start = self::hebrewYearStart( $hebrewYear );
1826 } else {
1827 # Next year's start
1828 $nextStart = self::hebrewYearStart( $hebrewYear + 1 );
1829 }
1830
1831 # Calculate Hebrew day of year
1832 $hebrewDayOfYear = $dayOfYear - $start;
1833
1834 # Difference between year's days
1835 $diff = $nextStart - $start;
1836 # Add 12 (or 13 for leap years) days to ignore the difference between
1837 # Hebrew and Gregorian year (353 at least vs. 365/6) - now the
1838 # difference is only about the year type
1839 if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1840 $diff += 13;
1841 } else {
1842 $diff += 12;
1843 }
1844
1845 # Check the year pattern, and is leap year
1846 # 0 means an incomplete year, 1 means a regular year, 2 means a complete year
1847 # This is mod 30, to work on both leap years (which add 30 days of Adar I)
1848 # and non-leap years
1849 $yearPattern = $diff % 30;
1850 # Check if leap year
1851 $isLeap = $diff >= 30;
1852
1853 # Calculate day in the month from number of day in the Hebrew year
1854 # Don't check Adar - if the day is not in Adar, we will stop before;
1855 # if it is in Adar, we will use it to check if it is Adar I or Adar II
1856 $hebrewDay = $hebrewDayOfYear;
1857 $hebrewMonth = 1;
1858 $days = 0;
1859 while ( $hebrewMonth <= 12 ) {
1860 # Calculate days in this month
1861 if ( $isLeap && $hebrewMonth == 6 ) {
1862 # Adar in a leap year
1863 if ( $isLeap ) {
1864 # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1865 $days = 30;
1866 if ( $hebrewDay <= $days ) {
1867 # Day in Adar I
1868 $hebrewMonth = 13;
1869 } else {
1870 # Subtract the days of Adar I
1871 $hebrewDay -= $days;
1872 # Try Adar II
1873 $days = 29;
1874 if ( $hebrewDay <= $days ) {
1875 # Day in Adar II
1876 $hebrewMonth = 14;
1877 }
1878 }
1879 }
1880 } elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
1881 # Cheshvan in a complete year (otherwise as the rule below)
1882 $days = 30;
1883 } elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) {
1884 # Kislev in an incomplete year (otherwise as the rule below)
1885 $days = 29;
1886 } else {
1887 # Odd months have 30 days, even have 29
1888 $days = 30 - ( $hebrewMonth - 1 ) % 2;
1889 }
1890 if ( $hebrewDay <= $days ) {
1891 # In the current month
1892 break;
1893 } else {
1894 # Subtract the days of the current month
1895 $hebrewDay -= $days;
1896 # Try in the next month
1897 $hebrewMonth++;
1898 }
1899 }
1900
1901 return array( $hebrewYear, $hebrewMonth, $hebrewDay, $days );
1902 }
1903
1904 /**
1905 * This calculates the Hebrew year start, as days since 1 September.
1906 * Based on Carl Friedrich Gauss algorithm for finding Easter date.
1907 * Used for Hebrew date.
1908 *
1909 * @param int $year
1910 *
1911 * @return string
1912 */
1913 private static function hebrewYearStart( $year ) {
1914 $a = intval( ( 12 * ( $year - 1 ) + 17 ) % 19 );
1915 $b = intval( ( $year - 1 ) % 4 );
1916 $m = 32.044093161144 + 1.5542417966212 * $a + $b / 4.0 - 0.0031777940220923 * ( $year - 1 );
1917 if ( $m < 0 ) {
1918 $m--;
1919 }
1920 $Mar = intval( $m );
1921 if ( $m < 0 ) {
1922 $m++;
1923 }
1924 $m -= $Mar;
1925
1926 $c = intval( ( $Mar + 3 * ( $year - 1 ) + 5 * $b + 5 ) % 7 );
1927 if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1928 $Mar++;
1929 } elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1930 $Mar += 2;
1931 } elseif ( $c == 2 || $c == 4 || $c == 6 ) {
1932 $Mar++;
1933 }
1934
1935 $Mar += intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1936 return $Mar;
1937 }
1938
1939 /**
1940 * Algorithm to convert Gregorian dates to Thai solar dates,
1941 * Minguo dates or Minguo dates.
1942 *
1943 * Link: http://en.wikipedia.org/wiki/Thai_solar_calendar
1944 * http://en.wikipedia.org/wiki/Minguo_calendar
1945 * http://en.wikipedia.org/wiki/Japanese_era_name
1946 *
1947 * @param string $ts 14-character timestamp
1948 * @param string $cName Calender name
1949 * @return array Converted year, month, day
1950 */
1951 private static function tsToYear( $ts, $cName ) {
1952 $gy = substr( $ts, 0, 4 );
1953 $gm = substr( $ts, 4, 2 );
1954 $gd = substr( $ts, 6, 2 );
1955
1956 if ( !strcmp( $cName, 'thai' ) ) {
1957 # Thai solar dates
1958 # Add 543 years to the Gregorian calendar
1959 # Months and days are identical
1960 $gy_offset = $gy + 543;
1961 } elseif ( ( !strcmp( $cName, 'minguo' ) ) || !strcmp( $cName, 'juche' ) ) {
1962 # Minguo dates
1963 # Deduct 1911 years from the Gregorian calendar
1964 # Months and days are identical
1965 $gy_offset = $gy - 1911;
1966 } elseif ( !strcmp( $cName, 'tenno' ) ) {
1967 # Nengō dates up to Meiji period
1968 # Deduct years from the Gregorian calendar
1969 # depending on the nengo periods
1970 # Months and days are identical
1971 if ( ( $gy < 1912 )
1972 || ( ( $gy == 1912 ) && ( $gm < 7 ) )
1973 || ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd < 31 ) )
1974 ) {
1975 # Meiji period
1976 $gy_gannen = $gy - 1868 + 1;
1977 $gy_offset = $gy_gannen;
1978 if ( $gy_gannen == 1 ) {
1979 $gy_offset = '元';
1980 }
1981 $gy_offset = '明治' . $gy_offset;
1982 } elseif (
1983 ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd == 31 ) ) ||
1984 ( ( $gy == 1912 ) && ( $gm >= 8 ) ) ||
1985 ( ( $gy > 1912 ) && ( $gy < 1926 ) ) ||
1986 ( ( $gy == 1926 ) && ( $gm < 12 ) ) ||
1987 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd < 26 ) )
1988 ) {
1989 # Taishō period
1990 $gy_gannen = $gy - 1912 + 1;
1991 $gy_offset = $gy_gannen;
1992 if ( $gy_gannen == 1 ) {
1993 $gy_offset = '元';
1994 }
1995 $gy_offset = '大正' . $gy_offset;
1996 } elseif (
1997 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd >= 26 ) ) ||
1998 ( ( $gy > 1926 ) && ( $gy < 1989 ) ) ||
1999 ( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd < 8 ) )
2000 ) {
2001 # Shōwa period
2002 $gy_gannen = $gy - 1926 + 1;
2003 $gy_offset = $gy_gannen;
2004 if ( $gy_gannen == 1 ) {
2005 $gy_offset = '元';
2006 }
2007 $gy_offset = '昭和' . $gy_offset;
2008 } else {
2009 # Heisei period
2010 $gy_gannen = $gy - 1989 + 1;
2011 $gy_offset = $gy_gannen;
2012 if ( $gy_gannen == 1 ) {
2013 $gy_offset = '元';
2014 }
2015 $gy_offset = '平成' . $gy_offset;
2016 }
2017 } else {
2018 $gy_offset = $gy;
2019 }
2020
2021 return array( $gy_offset, $gm, $gd );
2022 }
2023
2024 /**
2025 * Gets directionality of the first strongly directional codepoint, for embedBidi()
2026 *
2027 * This is the rule the BIDI algorithm uses to determine the directionality of
2028 * paragraphs ( http://unicode.org/reports/tr9/#The_Paragraph_Level ) and
2029 * FSI isolates ( http://unicode.org/reports/tr9/#Explicit_Directional_Isolates ).
2030 *
2031 * TODO: Does not handle BIDI control characters inside the text.
2032 * TODO: Does not handle unallocated characters.
2033 *
2034 * @param string $text Text to test
2035 * @return null|string Directionality ('ltr' or 'rtl') or null
2036 */
2037 private static function strongDirFromContent( $text = '' ) {
2038 if ( !preg_match( self::$strongDirRegex, $text, $matches ) ) {
2039 return null;
2040 }
2041 if ( $matches[1] === '' ) {
2042 return 'rtl';
2043 }
2044 return 'ltr';
2045 }
2046
2047 /**
2048 * Roman number formatting up to 10000
2049 *
2050 * @param int $num
2051 *
2052 * @return string
2053 */
2054 static function romanNumeral( $num ) {
2055 static $table = array(
2056 array( '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ),
2057 array( '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ),
2058 array( '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ),
2059 array( '', 'M', 'MM', 'MMM', 'MMMM', 'MMMMM', 'MMMMMM', 'MMMMMMM',
2060 'MMMMMMMM', 'MMMMMMMMM', 'MMMMMMMMMM' )
2061 );
2062
2063 $num = intval( $num );
2064 if ( $num > 10000 || $num <= 0 ) {
2065 return $num;
2066 }
2067
2068 $s = '';
2069 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
2070 if ( $num >= $pow10 ) {
2071 $s .= $table[$i][(int)floor( $num / $pow10 )];
2072 }
2073 $num = $num % $pow10;
2074 }
2075 return $s;
2076 }
2077
2078 /**
2079 * Hebrew Gematria number formatting up to 9999
2080 *
2081 * @param int $num
2082 *
2083 * @return string
2084 */
2085 static function hebrewNumeral( $num ) {
2086 static $table = array(
2087 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ),
2088 array( '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ),
2089 array( '',
2090 array( 'ק' ),
2091 array( 'ר' ),
2092 array( 'ש' ),
2093 array( 'ת' ),
2094 array( 'ת', 'ק' ),
2095 array( 'ת', 'ר' ),
2096 array( 'ת', 'ש' ),
2097 array( 'ת', 'ת' ),
2098 array( 'ת', 'ת', 'ק' ),
2099 array( 'ת', 'ת', 'ר' ),
2100 ),
2101 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' )
2102 );
2103
2104 $num = intval( $num );
2105 if ( $num > 9999 || $num <= 0 ) {
2106 return $num;
2107 }
2108
2109 // Round thousands have special notations
2110 if ( $num === 1000 ) {
2111 return "א' אלף";
2112 } elseif ( $num % 1000 === 0 ) {
2113 return $table[0][$num / 1000] . "' אלפים";
2114 }
2115
2116 $letters = array();
2117
2118 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
2119 if ( $num >= $pow10 ) {
2120 if ( $num === 15 || $num === 16 ) {
2121 $letters[] = $table[0][9];
2122 $letters[] = $table[0][$num - 9];
2123 $num = 0;
2124 } else {
2125 $letters = array_merge(
2126 $letters,
2127 (array)$table[$i][intval( $num / $pow10 )]
2128 );
2129
2130 if ( $pow10 === 1000 ) {
2131 $letters[] = "'";
2132 }
2133 }
2134 }
2135
2136 $num = $num % $pow10;
2137 }
2138
2139 $preTransformLength = count( $letters );
2140 if ( $preTransformLength === 1 ) {
2141 // Add geresh (single quote) to one-letter numbers
2142 $letters[] = "'";
2143 } else {
2144 $lastIndex = $preTransformLength - 1;
2145 $letters[$lastIndex] = str_replace(
2146 array( 'כ', 'מ', 'נ', 'פ', 'צ' ),
2147 array( 'ך', 'ם', 'ן', 'ף', 'ץ' ),
2148 $letters[$lastIndex]
2149 );
2150
2151 // Add gershayim (double quote) to multiple-letter numbers,
2152 // but exclude numbers with only one letter after the thousands
2153 // (1001-1009, 1020, 1030, 2001-2009, etc.)
2154 if ( $letters[1] === "'" && $preTransformLength === 3 ) {
2155 $letters[] = "'";
2156 } else {
2157 array_splice( $letters, -1, 0, '"' );
2158 }
2159 }
2160
2161 return implode( $letters );
2162 }
2163
2164 /**
2165 * Used by date() and time() to adjust the time output.
2166 *
2167 * @param string $ts The time in date('YmdHis') format
2168 * @param mixed $tz Adjust the time by this amount (default false, mean we
2169 * get user timecorrection setting)
2170 * @return int
2171 */
2172 function userAdjust( $ts, $tz = false ) {
2173 global $wgUser, $wgLocalTZoffset;
2174
2175 if ( $tz === false ) {
2176 $tz = $wgUser->getOption( 'timecorrection' );
2177 }
2178
2179 $data = explode( '|', $tz, 3 );
2180
2181 if ( $data[0] == 'ZoneInfo' ) {
2182 MediaWiki\suppressWarnings();
2183 $userTZ = timezone_open( $data[2] );
2184 MediaWiki\restoreWarnings();
2185 if ( $userTZ !== false ) {
2186 $date = date_create( $ts, timezone_open( 'UTC' ) );
2187 date_timezone_set( $date, $userTZ );
2188 $date = date_format( $date, 'YmdHis' );
2189 return $date;
2190 }
2191 # Unrecognized timezone, default to 'Offset' with the stored offset.
2192 $data[0] = 'Offset';
2193 }
2194
2195 if ( $data[0] == 'System' || $tz == '' ) {
2196 # Global offset in minutes.
2197 $minDiff = $wgLocalTZoffset;
2198 } elseif ( $data[0] == 'Offset' ) {
2199 $minDiff = intval( $data[1] );
2200 } else {
2201 $data = explode( ':', $tz );
2202 if ( count( $data ) == 2 ) {
2203 $data[0] = intval( $data[0] );
2204 $data[1] = intval( $data[1] );
2205 $minDiff = abs( $data[0] ) * 60 + $data[1];
2206 if ( $data[0] < 0 ) {
2207 $minDiff = -$minDiff;
2208 }
2209 } else {
2210 $minDiff = intval( $data[0] ) * 60;
2211 }
2212 }
2213
2214 # No difference ? Return time unchanged
2215 if ( 0 == $minDiff ) {
2216 return $ts;
2217 }
2218
2219 MediaWiki\suppressWarnings(); // E_STRICT system time bitching
2220 # Generate an adjusted date; take advantage of the fact that mktime
2221 # will normalize out-of-range values so we don't have to split $minDiff
2222 # into hours and minutes.
2223 $t = mktime( (
2224 (int)substr( $ts, 8, 2 ) ), # Hours
2225 (int)substr( $ts, 10, 2 ) + $minDiff, # Minutes
2226 (int)substr( $ts, 12, 2 ), # Seconds
2227 (int)substr( $ts, 4, 2 ), # Month
2228 (int)substr( $ts, 6, 2 ), # Day
2229 (int)substr( $ts, 0, 4 ) ); # Year
2230
2231 $date = date( 'YmdHis', $t );
2232 MediaWiki\restoreWarnings();
2233
2234 return $date;
2235 }
2236
2237 /**
2238 * This is meant to be used by time(), date(), and timeanddate() to get
2239 * the date preference they're supposed to use, it should be used in
2240 * all children.
2241 *
2242 *<code>
2243 * function timeanddate([...], $format = true) {
2244 * $datePreference = $this->dateFormat($format);
2245 * [...]
2246 * }
2247 *</code>
2248 *
2249 * @param int|string|bool $usePrefs If true, the user's preference is used
2250 * if false, the site/language default is used
2251 * if int/string, assumed to be a format.
2252 * @return string
2253 */
2254 function dateFormat( $usePrefs = true ) {
2255 global $wgUser;
2256
2257 if ( is_bool( $usePrefs ) ) {
2258 if ( $usePrefs ) {
2259 $datePreference = $wgUser->getDatePreference();
2260 } else {
2261 $datePreference = (string)User::getDefaultOption( 'date' );
2262 }
2263 } else {
2264 $datePreference = (string)$usePrefs;
2265 }
2266
2267 // return int
2268 if ( $datePreference == '' ) {
2269 return 'default';
2270 }
2271
2272 return $datePreference;
2273 }
2274
2275 /**
2276 * Get a format string for a given type and preference
2277 * @param string $type May be 'date', 'time', 'both', or 'pretty'.
2278 * @param string $pref The format name as it appears in Messages*.php under
2279 * $datePreferences.
2280 *
2281 * @since 1.22 New type 'pretty' that provides a more readable timestamp format
2282 *
2283 * @return string
2284 */
2285 function getDateFormatString( $type, $pref ) {
2286 $wasDefault = false;
2287 if ( $pref == 'default' ) {
2288 $wasDefault = true;
2289 $pref = $this->getDefaultDateFormat();
2290 }
2291
2292 if ( !isset( $this->dateFormatStrings[$type][$pref] ) ) {
2293 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2294
2295 if ( $type === 'pretty' && $df === null ) {
2296 $df = $this->getDateFormatString( 'date', $pref );
2297 }
2298
2299 if ( !$wasDefault && $df === null ) {
2300 $pref = $this->getDefaultDateFormat();
2301 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2302 }
2303
2304 $this->dateFormatStrings[$type][$pref] = $df;
2305 }
2306 return $this->dateFormatStrings[$type][$pref];
2307 }
2308
2309 /**
2310 * @param string $ts The time format which needs to be turned into a
2311 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2312 * @param bool $adj Whether to adjust the time output according to the
2313 * user configured offset ($timecorrection)
2314 * @param mixed $format True to use user's date format preference
2315 * @param string|bool $timecorrection The time offset as returned by
2316 * validateTimeZone() in Special:Preferences
2317 * @return string
2318 */
2319 function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
2320 $ts = wfTimestamp( TS_MW, $ts );
2321 if ( $adj ) {
2322 $ts = $this->userAdjust( $ts, $timecorrection );
2323 }
2324 $df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
2325 return $this->sprintfDate( $df, $ts );
2326 }
2327
2328 /**
2329 * @param string $ts The time format which needs to be turned into a
2330 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2331 * @param bool $adj Whether to adjust the time output according to the
2332 * user configured offset ($timecorrection)
2333 * @param mixed $format True to use user's date format preference
2334 * @param string|bool $timecorrection The time offset as returned by
2335 * validateTimeZone() in Special:Preferences
2336 * @return string
2337 */
2338 function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
2339 $ts = wfTimestamp( TS_MW, $ts );
2340 if ( $adj ) {
2341 $ts = $this->userAdjust( $ts, $timecorrection );
2342 }
2343 $df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
2344 return $this->sprintfDate( $df, $ts );
2345 }
2346
2347 /**
2348 * @param string $ts The time format which needs to be turned into a
2349 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2350 * @param bool $adj Whether to adjust the time output according to the
2351 * user configured offset ($timecorrection)
2352 * @param mixed $format What format to return, if it's false output the
2353 * default one (default true)
2354 * @param string|bool $timecorrection The time offset as returned by
2355 * validateTimeZone() in Special:Preferences
2356 * @return string
2357 */
2358 function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false ) {
2359 $ts = wfTimestamp( TS_MW, $ts );
2360 if ( $adj ) {
2361 $ts = $this->userAdjust( $ts, $timecorrection );
2362 }
2363 $df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) );
2364 return $this->sprintfDate( $df, $ts );
2365 }
2366
2367 /**
2368 * Takes a number of seconds and turns it into a text using values such as hours and minutes.
2369 *
2370 * @since 1.20
2371 *
2372 * @param int $seconds The amount of seconds.
2373 * @param array $chosenIntervals The intervals to enable.
2374 *
2375 * @return string
2376 */
2377 public function formatDuration( $seconds, array $chosenIntervals = array() ) {
2378 $intervals = $this->getDurationIntervals( $seconds, $chosenIntervals );
2379
2380 $segments = array();
2381
2382 foreach ( $intervals as $intervalName => $intervalValue ) {
2383 // Messages: duration-seconds, duration-minutes, duration-hours, duration-days, duration-weeks,
2384 // duration-years, duration-decades, duration-centuries, duration-millennia
2385 $message = wfMessage( 'duration-' . $intervalName )->numParams( $intervalValue );
2386 $segments[] = $message->inLanguage( $this )->escaped();
2387 }
2388
2389 return $this->listToText( $segments );
2390 }
2391
2392 /**
2393 * Takes a number of seconds and returns an array with a set of corresponding intervals.
2394 * For example 65 will be turned into array( minutes => 1, seconds => 5 ).
2395 *
2396 * @since 1.20
2397 *
2398 * @param int $seconds The amount of seconds.
2399 * @param array $chosenIntervals The intervals to enable.
2400 *
2401 * @return array
2402 */
2403 public function getDurationIntervals( $seconds, array $chosenIntervals = array() ) {
2404 if ( empty( $chosenIntervals ) ) {
2405 $chosenIntervals = array(
2406 'millennia',
2407 'centuries',
2408 'decades',
2409 'years',
2410 'days',
2411 'hours',
2412 'minutes',
2413 'seconds'
2414 );
2415 }
2416
2417 $intervals = array_intersect_key( self::$durationIntervals, array_flip( $chosenIntervals ) );
2418 $sortedNames = array_keys( $intervals );
2419 $smallestInterval = array_pop( $sortedNames );
2420
2421 $segments = array();
2422
2423 foreach ( $intervals as $name => $length ) {
2424 $value = floor( $seconds / $length );
2425
2426 if ( $value > 0 || ( $name == $smallestInterval && empty( $segments ) ) ) {
2427 $seconds -= $value * $length;
2428 $segments[$name] = $value;
2429 }
2430 }
2431
2432 return $segments;
2433 }
2434
2435 /**
2436 * Internal helper function for userDate(), userTime() and userTimeAndDate()
2437 *
2438 * @param string $type Can be 'date', 'time' or 'both'
2439 * @param string $ts The time format which needs to be turned into a
2440 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2441 * @param User $user User object used to get preferences for timezone and format
2442 * @param array $options Array, can contain the following keys:
2443 * - 'timecorrection': time correction, can have the following values:
2444 * - true: use user's preference
2445 * - false: don't use time correction
2446 * - int: value of time correction in minutes
2447 * - 'format': format to use, can have the following values:
2448 * - true: use user's preference
2449 * - false: use default preference
2450 * - string: format to use
2451 * @since 1.19
2452 * @return string
2453 */
2454 private function internalUserTimeAndDate( $type, $ts, User $user, array $options ) {
2455 $ts = wfTimestamp( TS_MW, $ts );
2456 $options += array( 'timecorrection' => true, 'format' => true );
2457 if ( $options['timecorrection'] !== false ) {
2458 if ( $options['timecorrection'] === true ) {
2459 $offset = $user->getOption( 'timecorrection' );
2460 } else {
2461 $offset = $options['timecorrection'];
2462 }
2463 $ts = $this->userAdjust( $ts, $offset );
2464 }
2465 if ( $options['format'] === true ) {
2466 $format = $user->getDatePreference();
2467 } else {
2468 $format = $options['format'];
2469 }
2470 $df = $this->getDateFormatString( $type, $this->dateFormat( $format ) );
2471 return $this->sprintfDate( $df, $ts );
2472 }
2473
2474 /**
2475 * Get the formatted date for the given timestamp and formatted for
2476 * the given user.
2477 *
2478 * @param mixed $ts Mixed: the time format which needs to be turned into a
2479 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2480 * @param User $user User object used to get preferences for timezone and format
2481 * @param array $options Array, can contain the following keys:
2482 * - 'timecorrection': time correction, can have the following values:
2483 * - true: use user's preference
2484 * - false: don't use time correction
2485 * - int: value of time correction in minutes
2486 * - 'format': format to use, can have the following values:
2487 * - true: use user's preference
2488 * - false: use default preference
2489 * - string: format to use
2490 * @since 1.19
2491 * @return string
2492 */
2493 public function userDate( $ts, User $user, array $options = array() ) {
2494 return $this->internalUserTimeAndDate( 'date', $ts, $user, $options );
2495 }
2496
2497 /**
2498 * Get the formatted time for the given timestamp and formatted for
2499 * the given user.
2500 *
2501 * @param mixed $ts The time format which needs to be turned into a
2502 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2503 * @param User $user User object used to get preferences for timezone and format
2504 * @param array $options Array, can contain the following keys:
2505 * - 'timecorrection': time correction, can have the following values:
2506 * - true: use user's preference
2507 * - false: don't use time correction
2508 * - int: value of time correction in minutes
2509 * - 'format': format to use, can have the following values:
2510 * - true: use user's preference
2511 * - false: use default preference
2512 * - string: format to use
2513 * @since 1.19
2514 * @return string
2515 */
2516 public function userTime( $ts, User $user, array $options = array() ) {
2517 return $this->internalUserTimeAndDate( 'time', $ts, $user, $options );
2518 }
2519
2520 /**
2521 * Get the formatted date and time for the given timestamp and formatted for
2522 * the given user.
2523 *
2524 * @param mixed $ts The time format which needs to be turned into a
2525 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2526 * @param User $user User object used to get preferences for timezone and format
2527 * @param array $options Array, can contain the following keys:
2528 * - 'timecorrection': time correction, can have the following values:
2529 * - true: use user's preference
2530 * - false: don't use time correction
2531 * - int: value of time correction in minutes
2532 * - 'format': format to use, can have the following values:
2533 * - true: use user's preference
2534 * - false: use default preference
2535 * - string: format to use
2536 * @since 1.19
2537 * @return string
2538 */
2539 public function userTimeAndDate( $ts, User $user, array $options = array() ) {
2540 return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
2541 }
2542
2543 /**
2544 * Get the timestamp in a human-friendly relative format, e.g., "3 days ago".
2545 *
2546 * Determine the difference between the timestamp and the current time, and
2547 * generate a readable timestamp by returning "<N> <units> ago", where the
2548 * largest possible unit is used.
2549 *
2550 * @since 1.26 (Prior to 1.26 method existed but was not meant to be used directly)
2551 *
2552 * @param MWTimestamp $time
2553 * @param MWTimestamp|null $relativeTo The base timestamp to compare to (defaults to now)
2554 * @param User|null $user User the timestamp is being generated for
2555 * (or null to use main context's user)
2556 * @return string Formatted timestamp
2557 */
2558 public function getHumanTimestamp(
2559 MWTimestamp $time, MWTimestamp $relativeTo = null, User $user = null
2560 ) {
2561 if ( $relativeTo === null ) {
2562 $relativeTo = new MWTimestamp();
2563 }
2564 if ( $user === null ) {
2565 $user = RequestContext::getMain()->getUser();
2566 }
2567
2568 // Adjust for the user's timezone.
2569 $offsetThis = $time->offsetForUser( $user );
2570 $offsetRel = $relativeTo->offsetForUser( $user );
2571
2572 $ts = '';
2573 if ( Hooks::run( 'GetHumanTimestamp', array( &$ts, $time, $relativeTo, $user, $this ) ) ) {
2574 $ts = $this->getHumanTimestampInternal( $time, $relativeTo, $user );
2575 }
2576
2577 // Reset the timezone on the objects.
2578 $time->timestamp->sub( $offsetThis );
2579 $relativeTo->timestamp->sub( $offsetRel );
2580
2581 return $ts;
2582 }
2583
2584 /**
2585 * Convert an MWTimestamp into a pretty human-readable timestamp using
2586 * the given user preferences and relative base time.
2587 *
2588 * @see Language::getHumanTimestamp
2589 * @param MWTimestamp $ts Timestamp to prettify
2590 * @param MWTimestamp $relativeTo Base timestamp
2591 * @param User $user User preferences to use
2592 * @return string Human timestamp
2593 * @since 1.26
2594 */
2595 private function getHumanTimestampInternal(
2596 MWTimestamp $ts, MWTimestamp $relativeTo, User $user
2597 ) {
2598 $diff = $ts->diff( $relativeTo );
2599 $diffDay = (bool)( (int)$ts->timestamp->format( 'w' ) -
2600 (int)$relativeTo->timestamp->format( 'w' ) );
2601 $days = $diff->days ?: (int)$diffDay;
2602 if ( $diff->invert || $days > 5
2603 && $ts->timestamp->format( 'Y' ) !== $relativeTo->timestamp->format( 'Y' )
2604 ) {
2605 // Timestamps are in different years: use full timestamp
2606 // Also do full timestamp for future dates
2607 /**
2608 * @todo FIXME: Add better handling of future timestamps.
2609 */
2610 $format = $this->getDateFormatString( 'both', $user->getDatePreference() ?: 'default' );
2611 $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
2612 } elseif ( $days > 5 ) {
2613 // Timestamps are in same year, but more than 5 days ago: show day and month only.
2614 $format = $this->getDateFormatString( 'pretty', $user->getDatePreference() ?: 'default' );
2615 $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
2616 } elseif ( $days > 1 ) {
2617 // Timestamp within the past week: show the day of the week and time
2618 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2619 $weekday = self::$mWeekdayMsgs[$ts->timestamp->format( 'w' )];
2620 // Messages:
2621 // sunday-at, monday-at, tuesday-at, wednesday-at, thursday-at, friday-at, saturday-at
2622 $ts = wfMessage( "$weekday-at" )
2623 ->inLanguage( $this )
2624 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2625 ->text();
2626 } elseif ( $days == 1 ) {
2627 // Timestamp was yesterday: say 'yesterday' and the time.
2628 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2629 $ts = wfMessage( 'yesterday-at' )
2630 ->inLanguage( $this )
2631 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2632 ->text();
2633 } elseif ( $diff->h > 1 || $diff->h == 1 && $diff->i > 30 ) {
2634 // Timestamp was today, but more than 90 minutes ago: say 'today' and the time.
2635 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2636 $ts = wfMessage( 'today-at' )
2637 ->inLanguage( $this )
2638 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2639 ->text();
2640
2641 // From here on in, the timestamp was soon enough ago so that we can simply say
2642 // XX units ago, e.g., "2 hours ago" or "5 minutes ago"
2643 } elseif ( $diff->h == 1 ) {
2644 // Less than 90 minutes, but more than an hour ago.
2645 $ts = wfMessage( 'hours-ago' )->inLanguage( $this )->numParams( 1 )->text();
2646 } elseif ( $diff->i >= 1 ) {
2647 // A few minutes ago.
2648 $ts = wfMessage( 'minutes-ago' )->inLanguage( $this )->numParams( $diff->i )->text();
2649 } elseif ( $diff->s >= 30 ) {
2650 // Less than a minute, but more than 30 sec ago.
2651 $ts = wfMessage( 'seconds-ago' )->inLanguage( $this )->numParams( $diff->s )->text();
2652 } else {
2653 // Less than 30 seconds ago.
2654 $ts = wfMessage( 'just-now' )->text();
2655 }
2656
2657 return $ts;
2658 }
2659
2660 /**
2661 * @param string $key
2662 * @return array|null
2663 */
2664 function getMessage( $key ) {
2665 return self::$dataCache->getSubitem( $this->mCode, 'messages', $key );
2666 }
2667
2668 /**
2669 * @return array
2670 */
2671 function getAllMessages() {
2672 return self::$dataCache->getItem( $this->mCode, 'messages' );
2673 }
2674
2675 /**
2676 * @param string $in
2677 * @param string $out
2678 * @param string $string
2679 * @return string
2680 */
2681 function iconv( $in, $out, $string ) {
2682 # This is a wrapper for iconv in all languages except esperanto,
2683 # which does some nasty x-conversions beforehand
2684
2685 # Even with //IGNORE iconv can whine about illegal characters in
2686 # *input* string. We just ignore those too.
2687 # REF: http://bugs.php.net/bug.php?id=37166
2688 # REF: https://phabricator.wikimedia.org/T18885
2689 MediaWiki\suppressWarnings();
2690 $text = iconv( $in, $out . '//IGNORE', $string );
2691 MediaWiki\restoreWarnings();
2692 return $text;
2693 }
2694
2695 // callback functions for uc(), lc(), ucwords(), ucwordbreaks()
2696
2697 /**
2698 * @param array $matches
2699 * @return mixed|string
2700 */
2701 function ucwordbreaksCallbackAscii( $matches ) {
2702 return $this->ucfirst( $matches[1] );
2703 }
2704
2705 /**
2706 * @param array $matches
2707 * @return string
2708 */
2709 function ucwordbreaksCallbackMB( $matches ) {
2710 return mb_strtoupper( $matches[0] );
2711 }
2712
2713 /**
2714 * @param array $matches
2715 * @return string
2716 */
2717 function ucCallback( $matches ) {
2718 list( $wikiUpperChars ) = self::getCaseMaps();
2719 return strtr( $matches[1], $wikiUpperChars );
2720 }
2721
2722 /**
2723 * @param array $matches
2724 * @return string
2725 */
2726 function lcCallback( $matches ) {
2727 list( , $wikiLowerChars ) = self::getCaseMaps();
2728 return strtr( $matches[1], $wikiLowerChars );
2729 }
2730
2731 /**
2732 * @param array $matches
2733 * @return string
2734 */
2735 function ucwordsCallbackMB( $matches ) {
2736 return mb_strtoupper( $matches[0] );
2737 }
2738
2739 /**
2740 * @param array $matches
2741 * @return string
2742 */
2743 function ucwordsCallbackWiki( $matches ) {
2744 list( $wikiUpperChars ) = self::getCaseMaps();
2745 return strtr( $matches[0], $wikiUpperChars );
2746 }
2747
2748 /**
2749 * Make a string's first character uppercase
2750 *
2751 * @param string $str
2752 *
2753 * @return string
2754 */
2755 function ucfirst( $str ) {
2756 $o = ord( $str );
2757 if ( $o < 96 ) { // if already uppercase...
2758 return $str;
2759 } elseif ( $o < 128 ) {
2760 return ucfirst( $str ); // use PHP's ucfirst()
2761 } else {
2762 // fall back to more complex logic in case of multibyte strings
2763 return $this->uc( $str, true );
2764 }
2765 }
2766
2767 /**
2768 * Convert a string to uppercase
2769 *
2770 * @param string $str
2771 * @param bool $first
2772 *
2773 * @return string
2774 */
2775 function uc( $str, $first = false ) {
2776 if ( function_exists( 'mb_strtoupper' ) ) {
2777 if ( $first ) {
2778 if ( $this->isMultibyte( $str ) ) {
2779 return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2780 } else {
2781 return ucfirst( $str );
2782 }
2783 } else {
2784 return $this->isMultibyte( $str ) ? mb_strtoupper( $str ) : strtoupper( $str );
2785 }
2786 } else {
2787 if ( $this->isMultibyte( $str ) ) {
2788 $x = $first ? '^' : '';
2789 return preg_replace_callback(
2790 "/$x([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
2791 array( $this, 'ucCallback' ),
2792 $str
2793 );
2794 } else {
2795 return $first ? ucfirst( $str ) : strtoupper( $str );
2796 }
2797 }
2798 }
2799
2800 /**
2801 * @param string $str
2802 * @return mixed|string
2803 */
2804 function lcfirst( $str ) {
2805 $o = ord( $str );
2806 if ( !$o ) {
2807 return strval( $str );
2808 } elseif ( $o >= 128 ) {
2809 return $this->lc( $str, true );
2810 } elseif ( $o > 96 ) {
2811 return $str;
2812 } else {
2813 $str[0] = strtolower( $str[0] );
2814 return $str;
2815 }
2816 }
2817
2818 /**
2819 * @param string $str
2820 * @param bool $first
2821 * @return mixed|string
2822 */
2823 function lc( $str, $first = false ) {
2824 if ( function_exists( 'mb_strtolower' ) ) {
2825 if ( $first ) {
2826 if ( $this->isMultibyte( $str ) ) {
2827 return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2828 } else {
2829 return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
2830 }
2831 } else {
2832 return $this->isMultibyte( $str ) ? mb_strtolower( $str ) : strtolower( $str );
2833 }
2834 } else {
2835 if ( $this->isMultibyte( $str ) ) {
2836 $x = $first ? '^' : '';
2837 return preg_replace_callback(
2838 "/$x([A-Z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
2839 array( $this, 'lcCallback' ),
2840 $str
2841 );
2842 } else {
2843 return $first ? strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 ) : strtolower( $str );
2844 }
2845 }
2846 }
2847
2848 /**
2849 * @param string $str
2850 * @return bool
2851 */
2852 function isMultibyte( $str ) {
2853 return strlen( $str ) !== mb_strlen( $str );
2854 }
2855
2856 /**
2857 * @param string $str
2858 * @return mixed|string
2859 */
2860 function ucwords( $str ) {
2861 if ( $this->isMultibyte( $str ) ) {
2862 $str = $this->lc( $str );
2863
2864 // regexp to find first letter in each word (i.e. after each space)
2865 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2866
2867 // function to use to capitalize a single char
2868 if ( function_exists( 'mb_strtoupper' ) ) {
2869 return preg_replace_callback(
2870 $replaceRegexp,
2871 array( $this, 'ucwordsCallbackMB' ),
2872 $str
2873 );
2874 } else {
2875 return preg_replace_callback(
2876 $replaceRegexp,
2877 array( $this, 'ucwordsCallbackWiki' ),
2878 $str
2879 );
2880 }
2881 } else {
2882 return ucwords( strtolower( $str ) );
2883 }
2884 }
2885
2886 /**
2887 * capitalize words at word breaks
2888 *
2889 * @param string $str
2890 * @return mixed
2891 */
2892 function ucwordbreaks( $str ) {
2893 if ( $this->isMultibyte( $str ) ) {
2894 $str = $this->lc( $str );
2895
2896 // since \b doesn't work for UTF-8, we explicitely define word break chars
2897 $breaks = "[ \-\(\)\}\{\.,\?!]";
2898
2899 // find first letter after word break
2900 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|" .
2901 "$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2902
2903 if ( function_exists( 'mb_strtoupper' ) ) {
2904 return preg_replace_callback(
2905 $replaceRegexp,
2906 array( $this, 'ucwordbreaksCallbackMB' ),
2907 $str
2908 );
2909 } else {
2910 return preg_replace_callback(
2911 $replaceRegexp,
2912 array( $this, 'ucwordsCallbackWiki' ),
2913 $str
2914 );
2915 }
2916 } else {
2917 return preg_replace_callback(
2918 '/\b([\w\x80-\xff]+)\b/',
2919 array( $this, 'ucwordbreaksCallbackAscii' ),
2920 $str
2921 );
2922 }
2923 }
2924
2925 /**
2926 * Return a case-folded representation of $s
2927 *
2928 * This is a representation such that caseFold($s1)==caseFold($s2) if $s1
2929 * and $s2 are the same except for the case of their characters. It is not
2930 * necessary for the value returned to make sense when displayed.
2931 *
2932 * Do *not* perform any other normalisation in this function. If a caller
2933 * uses this function when it should be using a more general normalisation
2934 * function, then fix the caller.
2935 *
2936 * @param string $s
2937 *
2938 * @return string
2939 */
2940 function caseFold( $s ) {
2941 return $this->uc( $s );
2942 }
2943
2944 /**
2945 * @param string $s
2946 * @return string
2947 */
2948 function checkTitleEncoding( $s ) {
2949 if ( is_array( $s ) ) {
2950 throw new MWException( 'Given array to checkTitleEncoding.' );
2951 }
2952 if ( StringUtils::isUtf8( $s ) ) {
2953 return $s;
2954 }
2955
2956 return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s );
2957 }
2958
2959 /**
2960 * @return array
2961 */
2962 function fallback8bitEncoding() {
2963 return self::$dataCache->getItem( $this->mCode, 'fallback8bitEncoding' );
2964 }
2965
2966 /**
2967 * Most writing systems use whitespace to break up words.
2968 * Some languages such as Chinese don't conventionally do this,
2969 * which requires special handling when breaking up words for
2970 * searching etc.
2971 *
2972 * @return bool
2973 */
2974 function hasWordBreaks() {
2975 return true;
2976 }
2977
2978 /**
2979 * Some languages such as Chinese require word segmentation,
2980 * Specify such segmentation when overridden in derived class.
2981 *
2982 * @param string $string
2983 * @return string
2984 */
2985 function segmentByWord( $string ) {
2986 return $string;
2987 }
2988
2989 /**
2990 * Some languages have special punctuation need to be normalized.
2991 * Make such changes here.
2992 *
2993 * @param string $string
2994 * @return string
2995 */
2996 function normalizeForSearch( $string ) {
2997 return self::convertDoubleWidth( $string );
2998 }
2999
3000 /**
3001 * convert double-width roman characters to single-width.
3002 * range: ff00-ff5f ~= 0020-007f
3003 *
3004 * @param string $string
3005 *
3006 * @return string
3007 */
3008 protected static function convertDoubleWidth( $string ) {
3009 static $full = null;
3010 static $half = null;
3011
3012 if ( $full === null ) {
3013 $fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
3014 $halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
3015 $full = str_split( $fullWidth, 3 );
3016 $half = str_split( $halfWidth );
3017 }
3018
3019 $string = str_replace( $full, $half, $string );
3020 return $string;
3021 }
3022
3023 /**
3024 * @param string $string
3025 * @param string $pattern
3026 * @return string
3027 */
3028 protected static function insertSpace( $string, $pattern ) {
3029 $string = preg_replace( $pattern, " $1 ", $string );
3030 $string = preg_replace( '/ +/', ' ', $string );
3031 return $string;
3032 }
3033
3034 /**
3035 * @param array $termsArray
3036 * @return array
3037 */
3038 function convertForSearchResult( $termsArray ) {
3039 # some languages, e.g. Chinese, need to do a conversion
3040 # in order for search results to be displayed correctly
3041 return $termsArray;
3042 }
3043
3044 /**
3045 * Get the first character of a string.
3046 *
3047 * @param string $s
3048 * @return string
3049 */
3050 function firstChar( $s ) {
3051 $matches = array();
3052 preg_match(
3053 '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
3054 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/',
3055 $s,
3056 $matches
3057 );
3058
3059 if ( isset( $matches[1] ) ) {
3060 if ( strlen( $matches[1] ) != 3 ) {
3061 return $matches[1];
3062 }
3063
3064 // Break down Hangul syllables to grab the first jamo
3065 $code = UtfNormal\Utils::utf8ToCodepoint( $matches[1] );
3066 if ( $code < 0xac00 || 0xd7a4 <= $code ) {
3067 return $matches[1];
3068 } elseif ( $code < 0xb098 ) {
3069 return "\xe3\x84\xb1";
3070 } elseif ( $code < 0xb2e4 ) {
3071 return "\xe3\x84\xb4";
3072 } elseif ( $code < 0xb77c ) {
3073 return "\xe3\x84\xb7";
3074 } elseif ( $code < 0xb9c8 ) {
3075 return "\xe3\x84\xb9";
3076 } elseif ( $code < 0xbc14 ) {
3077 return "\xe3\x85\x81";
3078 } elseif ( $code < 0xc0ac ) {
3079 return "\xe3\x85\x82";
3080 } elseif ( $code < 0xc544 ) {
3081 return "\xe3\x85\x85";
3082 } elseif ( $code < 0xc790 ) {
3083 return "\xe3\x85\x87";
3084 } elseif ( $code < 0xcc28 ) {
3085 return "\xe3\x85\x88";
3086 } elseif ( $code < 0xce74 ) {
3087 return "\xe3\x85\x8a";
3088 } elseif ( $code < 0xd0c0 ) {
3089 return "\xe3\x85\x8b";
3090 } elseif ( $code < 0xd30c ) {
3091 return "\xe3\x85\x8c";
3092 } elseif ( $code < 0xd558 ) {
3093 return "\xe3\x85\x8d";
3094 } else {
3095 return "\xe3\x85\x8e";
3096 }
3097 } else {
3098 return '';
3099 }
3100 }
3101
3102 function initEncoding() {
3103 # Some languages may have an alternate char encoding option
3104 # (Esperanto X-coding, Japanese furigana conversion, etc)
3105 # If this language is used as the primary content language,
3106 # an override to the defaults can be set here on startup.
3107 }
3108
3109 /**
3110 * @param string $s
3111 * @return string
3112 */
3113 function recodeForEdit( $s ) {
3114 # For some languages we'll want to explicitly specify
3115 # which characters make it into the edit box raw
3116 # or are converted in some way or another.
3117 global $wgEditEncoding;
3118 if ( $wgEditEncoding == '' || $wgEditEncoding == 'UTF-8' ) {
3119 return $s;
3120 } else {
3121 return $this->iconv( 'UTF-8', $wgEditEncoding, $s );
3122 }
3123 }
3124
3125 /**
3126 * @param string $s
3127 * @return string
3128 */
3129 function recodeInput( $s ) {
3130 # Take the previous into account.
3131 global $wgEditEncoding;
3132 if ( $wgEditEncoding != '' ) {
3133 $enc = $wgEditEncoding;
3134 } else {
3135 $enc = 'UTF-8';
3136 }
3137 if ( $enc == 'UTF-8' ) {
3138 return $s;
3139 } else {
3140 return $this->iconv( $enc, 'UTF-8', $s );
3141 }
3142 }
3143
3144 /**
3145 * Convert a UTF-8 string to normal form C. In Malayalam and Arabic, this
3146 * also cleans up certain backwards-compatible sequences, converting them
3147 * to the modern Unicode equivalent.
3148 *
3149 * This is language-specific for performance reasons only.
3150 *
3151 * @param string $s
3152 *
3153 * @return string
3154 */
3155 function normalize( $s ) {
3156 global $wgAllUnicodeFixes;
3157 $s = UtfNormal\Validator::cleanUp( $s );
3158 if ( $wgAllUnicodeFixes ) {
3159 $s = $this->transformUsingPairFile( 'normalize-ar.ser', $s );
3160 $s = $this->transformUsingPairFile( 'normalize-ml.ser', $s );
3161 }
3162
3163 return $s;
3164 }
3165
3166 /**
3167 * Transform a string using serialized data stored in the given file (which
3168 * must be in the serialized subdirectory of $IP). The file contains pairs
3169 * mapping source characters to destination characters.
3170 *
3171 * The data is cached in process memory. This will go faster if you have the
3172 * FastStringSearch extension.
3173 *
3174 * @param string $file
3175 * @param string $string
3176 *
3177 * @throws MWException
3178 * @return string
3179 */
3180 function transformUsingPairFile( $file, $string ) {
3181 if ( !isset( $this->transformData[$file] ) ) {
3182 $data = wfGetPrecompiledData( $file );
3183 if ( $data === false ) {
3184 throw new MWException( __METHOD__ . ": The transformation file $file is missing" );
3185 }
3186 $this->transformData[$file] = new ReplacementArray( $data );
3187 }
3188 return $this->transformData[$file]->replace( $string );
3189 }
3190
3191 /**
3192 * For right-to-left language support
3193 *
3194 * @return bool
3195 */
3196 function isRTL() {
3197 return self::$dataCache->getItem( $this->mCode, 'rtl' );
3198 }
3199
3200 /**
3201 * Return the correct HTML 'dir' attribute value for this language.
3202 * @return string
3203 */
3204 function getDir() {
3205 return $this->isRTL() ? 'rtl' : 'ltr';
3206 }
3207
3208 /**
3209 * Return 'left' or 'right' as appropriate alignment for line-start
3210 * for this language's text direction.
3211 *
3212 * Should be equivalent to CSS3 'start' text-align value....
3213 *
3214 * @return string
3215 */
3216 function alignStart() {
3217 return $this->isRTL() ? 'right' : 'left';
3218 }
3219
3220 /**
3221 * Return 'right' or 'left' as appropriate alignment for line-end
3222 * for this language's text direction.
3223 *
3224 * Should be equivalent to CSS3 'end' text-align value....
3225 *
3226 * @return string
3227 */
3228 function alignEnd() {
3229 return $this->isRTL() ? 'left' : 'right';
3230 }
3231
3232 /**
3233 * A hidden direction mark (LRM or RLM), depending on the language direction.
3234 * Unlike getDirMark(), this function returns the character as an HTML entity.
3235 * This function should be used when the output is guaranteed to be HTML,
3236 * because it makes the output HTML source code more readable. When
3237 * the output is plain text or can be escaped, getDirMark() should be used.
3238 *
3239 * @param bool $opposite Get the direction mark opposite to your language
3240 * @return string
3241 * @since 1.20
3242 */
3243 function getDirMarkEntity( $opposite = false ) {
3244 if ( $opposite ) {
3245 return $this->isRTL() ? '&lrm;' : '&rlm;';
3246 }
3247 return $this->isRTL() ? '&rlm;' : '&lrm;';
3248 }
3249
3250 /**
3251 * A hidden direction mark (LRM or RLM), depending on the language direction.
3252 * This function produces them as invisible Unicode characters and
3253 * the output may be hard to read and debug, so it should only be used
3254 * when the output is plain text or can be escaped. When the output is
3255 * HTML, use getDirMarkEntity() instead.
3256 *
3257 * @param bool $opposite Get the direction mark opposite to your language
3258 * @return string
3259 */
3260 function getDirMark( $opposite = false ) {
3261 $lrm = "\xE2\x80\x8E"; # LEFT-TO-RIGHT MARK, commonly abbreviated LRM
3262 $rlm = "\xE2\x80\x8F"; # RIGHT-TO-LEFT MARK, commonly abbreviated RLM
3263 if ( $opposite ) {
3264 return $this->isRTL() ? $lrm : $rlm;
3265 }
3266 return $this->isRTL() ? $rlm : $lrm;
3267 }
3268
3269 /**
3270 * @return array
3271 */
3272 function capitalizeAllNouns() {
3273 return self::$dataCache->getItem( $this->mCode, 'capitalizeAllNouns' );
3274 }
3275
3276 /**
3277 * An arrow, depending on the language direction.
3278 *
3279 * @param string $direction The direction of the arrow: forwards (default),
3280 * backwards, left, right, up, down.
3281 * @return string
3282 */
3283 function getArrow( $direction = 'forwards' ) {
3284 switch ( $direction ) {
3285 case 'forwards':
3286 return $this->isRTL() ? '←' : '→';
3287 case 'backwards':
3288 return $this->isRTL() ? '→' : '←';
3289 case 'left':
3290 return '←';
3291 case 'right':
3292 return '→';
3293 case 'up':
3294 return '↑';
3295 case 'down':
3296 return '↓';
3297 }
3298 }
3299
3300 /**
3301 * To allow "foo[[bar]]" to extend the link over the whole word "foobar"
3302 *
3303 * @return bool
3304 */
3305 function linkPrefixExtension() {
3306 return self::$dataCache->getItem( $this->mCode, 'linkPrefixExtension' );
3307 }
3308
3309 /**
3310 * Get all magic words from cache.
3311 * @return array
3312 */
3313 function getMagicWords() {
3314 return self::$dataCache->getItem( $this->mCode, 'magicWords' );
3315 }
3316
3317 /**
3318 * Run the LanguageGetMagic hook once.
3319 */
3320 protected function doMagicHook() {
3321 if ( $this->mMagicHookDone ) {
3322 return;
3323 }
3324 $this->mMagicHookDone = true;
3325 Hooks::run( 'LanguageGetMagic', array( &$this->mMagicExtensions, $this->getCode() ) );
3326 }
3327
3328 /**
3329 * Fill a MagicWord object with data from here
3330 *
3331 * @param MagicWord $mw
3332 */
3333 function getMagic( $mw ) {
3334 // Saves a function call
3335 if ( !$this->mMagicHookDone ) {
3336 $this->doMagicHook();
3337 }
3338
3339 if ( isset( $this->mMagicExtensions[$mw->mId] ) ) {
3340 $rawEntry = $this->mMagicExtensions[$mw->mId];
3341 } else {
3342 $rawEntry = self::$dataCache->getSubitem(
3343 $this->mCode, 'magicWords', $mw->mId );
3344 }
3345
3346 if ( !is_array( $rawEntry ) ) {
3347 wfWarn( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
3348 } else {
3349 $mw->mCaseSensitive = $rawEntry[0];
3350 $mw->mSynonyms = array_slice( $rawEntry, 1 );
3351 }
3352 }
3353
3354 /**
3355 * Add magic words to the extension array
3356 *
3357 * @param array $newWords
3358 */
3359 function addMagicWordsByLang( $newWords ) {
3360 $fallbackChain = $this->getFallbackLanguages();
3361 $fallbackChain = array_reverse( $fallbackChain );
3362 foreach ( $fallbackChain as $code ) {
3363 if ( isset( $newWords[$code] ) ) {
3364 $this->mMagicExtensions = $newWords[$code] + $this->mMagicExtensions;
3365 }
3366 }
3367 }
3368
3369 /**
3370 * Get special page names, as an associative array
3371 * canonical name => array of valid names, including aliases
3372 * @return array
3373 */
3374 function getSpecialPageAliases() {
3375 // Cache aliases because it may be slow to load them
3376 if ( is_null( $this->mExtendedSpecialPageAliases ) ) {
3377 // Initialise array
3378 $this->mExtendedSpecialPageAliases =
3379 self::$dataCache->getItem( $this->mCode, 'specialPageAliases' );
3380 Hooks::run( 'LanguageGetSpecialPageAliases',
3381 array( &$this->mExtendedSpecialPageAliases, $this->getCode() ) );
3382 }
3383
3384 return $this->mExtendedSpecialPageAliases;
3385 }
3386
3387 /**
3388 * Italic is unsuitable for some languages
3389 *
3390 * @param string $text The text to be emphasized.
3391 * @return string
3392 */
3393 function emphasize( $text ) {
3394 return "<em>$text</em>";
3395 }
3396
3397 /**
3398 * Normally we output all numbers in plain en_US style, that is
3399 * 293,291.235 for twohundredninetythreethousand-twohundredninetyone
3400 * point twohundredthirtyfive. However this is not suitable for all
3401 * languages, some such as Punjabi want ੨੯੩,੨੯੫.੨੩੫ and others such as
3402 * Icelandic just want to use commas instead of dots, and dots instead
3403 * of commas like "293.291,235".
3404 *
3405 * An example of this function being called:
3406 * <code>
3407 * wfMessage( 'message' )->numParams( $num )->text()
3408 * </code>
3409 *
3410 * See $separatorTransformTable on MessageIs.php for
3411 * the , => . and . => , implementation.
3412 *
3413 * @todo check if it's viable to use localeconv() for the decimal separator thing.
3414 * @param int|float $number The string to be formatted, should be an integer
3415 * or a floating point number.
3416 * @param bool $nocommafy Set to true for special numbers like dates
3417 * @return string
3418 */
3419 public function formatNum( $number, $nocommafy = false ) {
3420 global $wgTranslateNumerals;
3421 if ( !$nocommafy ) {
3422 $number = $this->commafy( $number );
3423 $s = $this->separatorTransformTable();
3424 if ( $s ) {
3425 $number = strtr( $number, $s );
3426 }
3427 }
3428
3429 if ( $wgTranslateNumerals ) {
3430 $s = $this->digitTransformTable();
3431 if ( $s ) {
3432 $number = strtr( $number, $s );
3433 }
3434 }
3435
3436 return $number;
3437 }
3438
3439 /**
3440 * Front-end for non-commafied formatNum
3441 *
3442 * @param int|float $number The string to be formatted, should be an integer
3443 * or a floating point number.
3444 * @since 1.21
3445 * @return string
3446 */
3447 public function formatNumNoSeparators( $number ) {
3448 return $this->formatNum( $number, true );
3449 }
3450
3451 /**
3452 * @param string $number
3453 * @return string
3454 */
3455 public function parseFormattedNumber( $number ) {
3456 $s = $this->digitTransformTable();
3457 if ( $s ) {
3458 // eliminate empty array values such as ''. (bug 64347)
3459 $s = array_filter( $s );
3460 $number = strtr( $number, array_flip( $s ) );
3461 }
3462
3463 $s = $this->separatorTransformTable();
3464 if ( $s ) {
3465 // eliminate empty array values such as ''. (bug 64347)
3466 $s = array_filter( $s );
3467 $number = strtr( $number, array_flip( $s ) );
3468 }
3469
3470 $number = strtr( $number, array( ',' => '' ) );
3471 return $number;
3472 }
3473
3474 /**
3475 * Adds commas to a given number
3476 * @since 1.19
3477 * @param mixed $number
3478 * @return string
3479 */
3480 function commafy( $number ) {
3481 $digitGroupingPattern = $this->digitGroupingPattern();
3482 if ( $number === null ) {
3483 return '';
3484 }
3485
3486 if ( !$digitGroupingPattern || $digitGroupingPattern === "###,###,###" ) {
3487 // default grouping is at thousands, use the same for ###,###,### pattern too.
3488 return strrev( (string)preg_replace( '/(\d{3})(?=\d)(?!\d*\.)/', '$1,', strrev( $number ) ) );
3489 } else {
3490 // Ref: http://cldr.unicode.org/translation/number-patterns
3491 $sign = "";
3492 if ( intval( $number ) < 0 ) {
3493 // For negative numbers apply the algorithm like positive number and add sign.
3494 $sign = "-";
3495 $number = substr( $number, 1 );
3496 }
3497 $integerPart = array();
3498 $decimalPart = array();
3499 $numMatches = preg_match_all( "/(#+)/", $digitGroupingPattern, $matches );
3500 preg_match( "/\d+/", $number, $integerPart );
3501 preg_match( "/\.\d*/", $number, $decimalPart );
3502 $groupedNumber = ( count( $decimalPart ) > 0 ) ? $decimalPart[0] : "";
3503 if ( $groupedNumber === $number ) {
3504 // the string does not have any number part. Eg: .12345
3505 return $sign . $groupedNumber;
3506 }
3507 $start = $end = ( $integerPart ) ? strlen( $integerPart[0] ) : 0;
3508 while ( $start > 0 ) {
3509 $match = $matches[0][$numMatches - 1];
3510 $matchLen = strlen( $match );
3511 $start = $end - $matchLen;
3512 if ( $start < 0 ) {
3513 $start = 0;
3514 }
3515 $groupedNumber = substr( $number, $start, $end -$start ) . $groupedNumber;
3516 $end = $start;
3517 if ( $numMatches > 1 ) {
3518 // use the last pattern for the rest of the number
3519 $numMatches--;
3520 }
3521 if ( $start > 0 ) {
3522 $groupedNumber = "," . $groupedNumber;
3523 }
3524 }
3525 return $sign . $groupedNumber;
3526 }
3527 }
3528
3529 /**
3530 * @return string
3531 */
3532 function digitGroupingPattern() {
3533 return self::$dataCache->getItem( $this->mCode, 'digitGroupingPattern' );
3534 }
3535
3536 /**
3537 * @return array
3538 */
3539 function digitTransformTable() {
3540 return self::$dataCache->getItem( $this->mCode, 'digitTransformTable' );
3541 }
3542
3543 /**
3544 * @return array
3545 */
3546 function separatorTransformTable() {
3547 return self::$dataCache->getItem( $this->mCode, 'separatorTransformTable' );
3548 }
3549
3550 /**
3551 * Take a list of strings and build a locale-friendly comma-separated
3552 * list, using the local comma-separator message.
3553 * The last two strings are chained with an "and".
3554 * NOTE: This function will only work with standard numeric array keys (0, 1, 2…)
3555 *
3556 * @param string[] $l
3557 * @return string
3558 */
3559 function listToText( array $l ) {
3560 $m = count( $l ) - 1;
3561 if ( $m < 0 ) {
3562 return '';
3563 }
3564 if ( $m > 0 ) {
3565 $and = $this->msg( 'and' )->escaped();
3566 $space = $this->msg( 'word-separator' )->escaped();
3567 if ( $m > 1 ) {
3568 $comma = $this->msg( 'comma-separator' )->escaped();
3569 }
3570 }
3571 $s = $l[$m];
3572 for ( $i = $m - 1; $i >= 0; $i-- ) {
3573 if ( $i == $m - 1 ) {
3574 $s = $l[$i] . $and . $space . $s;
3575 } else {
3576 $s = $l[$i] . $comma . $s;
3577 }
3578 }
3579 return $s;
3580 }
3581
3582 /**
3583 * Take a list of strings and build a locale-friendly comma-separated
3584 * list, using the local comma-separator message.
3585 * @param string[] $list Array of strings to put in a comma list
3586 * @return string
3587 */
3588 function commaList( array $list ) {
3589 return implode(
3590 wfMessage( 'comma-separator' )->inLanguage( $this )->escaped(),
3591 $list
3592 );
3593 }
3594
3595 /**
3596 * Take a list of strings and build a locale-friendly semicolon-separated
3597 * list, using the local semicolon-separator message.
3598 * @param string[] $list Array of strings to put in a semicolon list
3599 * @return string
3600 */
3601 function semicolonList( array $list ) {
3602 return implode(
3603 wfMessage( 'semicolon-separator' )->inLanguage( $this )->escaped(),
3604 $list
3605 );
3606 }
3607
3608 /**
3609 * Same as commaList, but separate it with the pipe instead.
3610 * @param string[] $list Array of strings to put in a pipe list
3611 * @return string
3612 */
3613 function pipeList( array $list ) {
3614 return implode(
3615 wfMessage( 'pipe-separator' )->inLanguage( $this )->escaped(),
3616 $list
3617 );
3618 }
3619
3620 /**
3621 * Truncate a string to a specified length in bytes, appending an optional
3622 * string (e.g. for ellipses)
3623 *
3624 * The database offers limited byte lengths for some columns in the database;
3625 * multi-byte character sets mean we need to ensure that only whole characters
3626 * are included, otherwise broken characters can be passed to the user
3627 *
3628 * If $length is negative, the string will be truncated from the beginning
3629 *
3630 * @param string $string String to truncate
3631 * @param int $length Maximum length (including ellipses)
3632 * @param string $ellipsis String to append to the truncated text
3633 * @param bool $adjustLength Subtract length of ellipsis from $length.
3634 * $adjustLength was introduced in 1.18, before that behaved as if false.
3635 * @return string
3636 */
3637 function truncate( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3638 # Use the localized ellipsis character
3639 if ( $ellipsis == '...' ) {
3640 $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3641 }
3642 # Check if there is no need to truncate
3643 if ( $length == 0 ) {
3644 return $ellipsis; // convention
3645 } elseif ( strlen( $string ) <= abs( $length ) ) {
3646 return $string; // no need to truncate
3647 }
3648 $stringOriginal = $string;
3649 # If ellipsis length is >= $length then we can't apply $adjustLength
3650 if ( $adjustLength && strlen( $ellipsis ) >= abs( $length ) ) {
3651 $string = $ellipsis; // this can be slightly unexpected
3652 # Otherwise, truncate and add ellipsis...
3653 } else {
3654 $eLength = $adjustLength ? strlen( $ellipsis ) : 0;
3655 if ( $length > 0 ) {
3656 $length -= $eLength;
3657 $string = substr( $string, 0, $length ); // xyz...
3658 $string = $this->removeBadCharLast( $string );
3659 $string = rtrim( $string );
3660 $string = $string . $ellipsis;
3661 } else {
3662 $length += $eLength;
3663 $string = substr( $string, $length ); // ...xyz
3664 $string = $this->removeBadCharFirst( $string );
3665 $string = ltrim( $string );
3666 $string = $ellipsis . $string;
3667 }
3668 }
3669 # Do not truncate if the ellipsis makes the string longer/equal (bug 22181).
3670 # This check is *not* redundant if $adjustLength, due to the single case where
3671 # LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string.
3672 if ( strlen( $string ) < strlen( $stringOriginal ) ) {
3673 return $string;
3674 } else {
3675 return $stringOriginal;
3676 }
3677 }
3678
3679 /**
3680 * Remove bytes that represent an incomplete Unicode character
3681 * at the end of string (e.g. bytes of the char are missing)
3682 *
3683 * @param string $string
3684 * @return string
3685 */
3686 protected function removeBadCharLast( $string ) {
3687 if ( $string != '' ) {
3688 $char = ord( $string[strlen( $string ) - 1] );
3689 $m = array();
3690 if ( $char >= 0xc0 ) {
3691 # We got the first byte only of a multibyte char; remove it.
3692 $string = substr( $string, 0, -1 );
3693 } elseif ( $char >= 0x80 &&
3694 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
3695 '[\xf0-\xf7][\x80-\xbf]{1,2})$/', $string, $m )
3696 ) {
3697 # We chopped in the middle of a character; remove it
3698 $string = $m[1];
3699 }
3700 }
3701 return $string;
3702 }
3703
3704 /**
3705 * Remove bytes that represent an incomplete Unicode character
3706 * at the start of string (e.g. bytes of the char are missing)
3707 *
3708 * @param string $string
3709 * @return string
3710 */
3711 protected function removeBadCharFirst( $string ) {
3712 if ( $string != '' ) {
3713 $char = ord( $string[0] );
3714 if ( $char >= 0x80 && $char < 0xc0 ) {
3715 # We chopped in the middle of a character; remove the whole thing
3716 $string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
3717 }
3718 }
3719 return $string;
3720 }
3721
3722 /**
3723 * Truncate a string of valid HTML to a specified length in bytes,
3724 * appending an optional string (e.g. for ellipses), and return valid HTML
3725 *
3726 * This is only intended for styled/linked text, such as HTML with
3727 * tags like <span> and <a>, were the tags are self-contained (valid HTML).
3728 * Also, this will not detect things like "display:none" CSS.
3729 *
3730 * Note: since 1.18 you do not need to leave extra room in $length for ellipses.
3731 *
3732 * @param string $text HTML string to truncate
3733 * @param int $length (zero/positive) Maximum length (including ellipses)
3734 * @param string $ellipsis String to append to the truncated text
3735 * @return string
3736 */
3737 function truncateHtml( $text, $length, $ellipsis = '...' ) {
3738 # Use the localized ellipsis character
3739 if ( $ellipsis == '...' ) {
3740 $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3741 }
3742 # Check if there is clearly no need to truncate
3743 if ( $length <= 0 ) {
3744 return $ellipsis; // no text shown, nothing to format (convention)
3745 } elseif ( strlen( $text ) <= $length ) {
3746 return $text; // string short enough even *with* HTML (short-circuit)
3747 }
3748
3749 $dispLen = 0; // innerHTML legth so far
3750 $testingEllipsis = false; // checking if ellipses will make string longer/equal?
3751 $tagType = 0; // 0-open, 1-close
3752 $bracketState = 0; // 1-tag start, 2-tag name, 0-neither
3753 $entityState = 0; // 0-not entity, 1-entity
3754 $tag = $ret = ''; // accumulated tag name, accumulated result string
3755 $openTags = array(); // open tag stack
3756 $maybeState = null; // possible truncation state
3757
3758 $textLen = strlen( $text );
3759 $neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated
3760 for ( $pos = 0; true; ++$pos ) {
3761 # Consider truncation once the display length has reached the maximim.
3762 # We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
3763 # Check that we're not in the middle of a bracket/entity...
3764 if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) {
3765 if ( !$testingEllipsis ) {
3766 $testingEllipsis = true;
3767 # Save where we are; we will truncate here unless there turn out to
3768 # be so few remaining characters that truncation is not necessary.
3769 if ( !$maybeState ) { // already saved? ($neLength = 0 case)
3770 $maybeState = array( $ret, $openTags ); // save state
3771 }
3772 } elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) {
3773 # String in fact does need truncation, the truncation point was OK.
3774 list( $ret, $openTags ) = $maybeState; // reload state
3775 $ret = $this->removeBadCharLast( $ret ); // multi-byte char fix
3776 $ret .= $ellipsis; // add ellipsis
3777 break;
3778 }
3779 }
3780 if ( $pos >= $textLen ) {
3781 break; // extra iteration just for above checks
3782 }
3783
3784 # Read the next char...
3785 $ch = $text[$pos];
3786 $lastCh = $pos ? $text[$pos - 1] : '';
3787 $ret .= $ch; // add to result string
3788 if ( $ch == '<' ) {
3789 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); // for bad HTML
3790 $entityState = 0; // for bad HTML
3791 $bracketState = 1; // tag started (checking for backslash)
3792 } elseif ( $ch == '>' ) {
3793 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags );
3794 $entityState = 0; // for bad HTML
3795 $bracketState = 0; // out of brackets
3796 } elseif ( $bracketState == 1 ) {
3797 if ( $ch == '/' ) {
3798 $tagType = 1; // close tag (e.g. "</span>")
3799 } else {
3800 $tagType = 0; // open tag (e.g. "<span>")
3801 $tag .= $ch;
3802 }
3803 $bracketState = 2; // building tag name
3804 } elseif ( $bracketState == 2 ) {
3805 if ( $ch != ' ' ) {
3806 $tag .= $ch;
3807 } else {
3808 // Name found (e.g. "<a href=..."), add on tag attributes...
3809 $pos += $this->truncate_skip( $ret, $text, "<>", $pos + 1 );
3810 }
3811 } elseif ( $bracketState == 0 ) {
3812 if ( $entityState ) {
3813 if ( $ch == ';' ) {
3814 $entityState = 0;
3815 $dispLen++; // entity is one displayed char
3816 }
3817 } else {
3818 if ( $neLength == 0 && !$maybeState ) {
3819 // Save state without $ch. We want to *hit* the first
3820 // display char (to get tags) but not *use* it if truncating.
3821 $maybeState = array( substr( $ret, 0, -1 ), $openTags );
3822 }
3823 if ( $ch == '&' ) {
3824 $entityState = 1; // entity found, (e.g. "&#160;")
3825 } else {
3826 $dispLen++; // this char is displayed
3827 // Add the next $max display text chars after this in one swoop...
3828 $max = ( $testingEllipsis ? $length : $neLength ) - $dispLen;
3829 $skipped = $this->truncate_skip( $ret, $text, "<>&", $pos + 1, $max );
3830 $dispLen += $skipped;
3831 $pos += $skipped;
3832 }
3833 }
3834 }
3835 }
3836 // Close the last tag if left unclosed by bad HTML
3837 $this->truncate_endBracket( $tag, $text[$textLen - 1], $tagType, $openTags );
3838 while ( count( $openTags ) > 0 ) {
3839 $ret .= '</' . array_pop( $openTags ) . '>'; // close open tags
3840 }
3841 return $ret;
3842 }
3843
3844 /**
3845 * truncateHtml() helper function
3846 * like strcspn() but adds the skipped chars to $ret
3847 *
3848 * @param string $ret
3849 * @param string $text
3850 * @param string $search
3851 * @param int $start
3852 * @param null|int $len
3853 * @return int
3854 */
3855 private function truncate_skip( &$ret, $text, $search, $start, $len = null ) {
3856 if ( $len === null ) {
3857 $len = -1; // -1 means "no limit" for strcspn
3858 } elseif ( $len < 0 ) {
3859 $len = 0; // sanity
3860 }
3861 $skipCount = 0;
3862 if ( $start < strlen( $text ) ) {
3863 $skipCount = strcspn( $text, $search, $start, $len );
3864 $ret .= substr( $text, $start, $skipCount );
3865 }
3866 return $skipCount;
3867 }
3868
3869 /**
3870 * truncateHtml() helper function
3871 * (a) push or pop $tag from $openTags as needed
3872 * (b) clear $tag value
3873 * @param string &$tag Current HTML tag name we are looking at
3874 * @param int $tagType (0-open tag, 1-close tag)
3875 * @param string $lastCh Character before the '>' that ended this tag
3876 * @param array &$openTags Open tag stack (not accounting for $tag)
3877 */
3878 private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) {
3879 $tag = ltrim( $tag );
3880 if ( $tag != '' ) {
3881 if ( $tagType == 0 && $lastCh != '/' ) {
3882 $openTags[] = $tag; // tag opened (didn't close itself)
3883 } elseif ( $tagType == 1 ) {
3884 if ( $openTags && $tag == $openTags[count( $openTags ) - 1] ) {
3885 array_pop( $openTags ); // tag closed
3886 }
3887 }
3888 $tag = '';
3889 }
3890 }
3891
3892 /**
3893 * Grammatical transformations, needed for inflected languages
3894 * Invoked by putting {{grammar:case|word}} in a message
3895 *
3896 * @param string $word
3897 * @param string $case
3898 * @return string
3899 */
3900 function convertGrammar( $word, $case ) {
3901 global $wgGrammarForms;
3902 if ( isset( $wgGrammarForms[$this->getCode()][$case][$word] ) ) {
3903 return $wgGrammarForms[$this->getCode()][$case][$word];
3904 }
3905
3906 return $word;
3907 }
3908 /**
3909 * Get the grammar forms for the content language
3910 * @return array Array of grammar forms
3911 * @since 1.20
3912 */
3913 function getGrammarForms() {
3914 global $wgGrammarForms;
3915 if ( isset( $wgGrammarForms[$this->getCode()] )
3916 && is_array( $wgGrammarForms[$this->getCode()] )
3917 ) {
3918 return $wgGrammarForms[$this->getCode()];
3919 }
3920
3921 return array();
3922 }
3923 /**
3924 * Provides an alternative text depending on specified gender.
3925 * Usage {{gender:username|masculine|feminine|unknown}}.
3926 * username is optional, in which case the gender of current user is used,
3927 * but only in (some) interface messages; otherwise default gender is used.
3928 *
3929 * If no forms are given, an empty string is returned. If only one form is
3930 * given, it will be returned unconditionally. These details are implied by
3931 * the caller and cannot be overridden in subclasses.
3932 *
3933 * If three forms are given, the default is to use the third (unknown) form.
3934 * If fewer than three forms are given, the default is to use the first (masculine) form.
3935 * These details can be overridden in subclasses.
3936 *
3937 * @param string $gender
3938 * @param array $forms
3939 *
3940 * @return string
3941 */
3942 function gender( $gender, $forms ) {
3943 if ( !count( $forms ) ) {
3944 return '';
3945 }
3946 $forms = $this->preConvertPlural( $forms, 2 );
3947 if ( $gender === 'male' ) {
3948 return $forms[0];
3949 }
3950 if ( $gender === 'female' ) {
3951 return $forms[1];
3952 }
3953 return isset( $forms[2] ) ? $forms[2] : $forms[0];
3954 }
3955
3956 /**
3957 * Plural form transformations, needed for some languages.
3958 * For example, there are 3 form of plural in Russian and Polish,
3959 * depending on "count mod 10". See [[w:Plural]]
3960 * For English it is pretty simple.
3961 *
3962 * Invoked by putting {{plural:count|wordform1|wordform2}}
3963 * or {{plural:count|wordform1|wordform2|wordform3}}
3964 *
3965 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
3966 *
3967 * @param int $count Non-localized number
3968 * @param array $forms Different plural forms
3969 * @return string Correct form of plural for $count in this language
3970 */
3971 function convertPlural( $count, $forms ) {
3972 // Handle explicit n=pluralform cases
3973 $forms = $this->handleExplicitPluralForms( $count, $forms );
3974 if ( is_string( $forms ) ) {
3975 return $forms;
3976 }
3977 if ( !count( $forms ) ) {
3978 return '';
3979 }
3980
3981 $pluralForm = $this->getPluralRuleIndexNumber( $count );
3982 $pluralForm = min( $pluralForm, count( $forms ) - 1 );
3983 return $forms[$pluralForm];
3984 }
3985
3986 /**
3987 * Handles explicit plural forms for Language::convertPlural()
3988 *
3989 * In {{PLURAL:$1|0=nothing|one|many}}, 0=nothing will be returned if $1 equals zero.
3990 * If an explicitly defined plural form matches the $count, then
3991 * string value returned, otherwise array returned for further consideration
3992 * by CLDR rules or overridden convertPlural().
3993 *
3994 * @since 1.23
3995 *
3996 * @param int $count Non-localized number
3997 * @param array $forms Different plural forms
3998 *
3999 * @return array|string
4000 */
4001 protected function handleExplicitPluralForms( $count, array $forms ) {
4002 foreach ( $forms as $index => $form ) {
4003 if ( preg_match( '/\d+=/i', $form ) ) {
4004 $pos = strpos( $form, '=' );
4005 if ( substr( $form, 0, $pos ) === (string)$count ) {
4006 return substr( $form, $pos + 1 );
4007 }
4008 unset( $forms[$index] );
4009 }
4010 }
4011 return array_values( $forms );
4012 }
4013
4014 /**
4015 * Checks that convertPlural was given an array and pads it to requested
4016 * amount of forms by copying the last one.
4017 *
4018 * @param array $forms Array of forms given to convertPlural
4019 * @param int $count How many forms should there be at least
4020 * @return array Padded array of forms or an exception if not an array
4021 */
4022 protected function preConvertPlural( /* Array */ $forms, $count ) {
4023 while ( count( $forms ) < $count ) {
4024 $forms[] = $forms[count( $forms ) - 1];
4025 }
4026 return $forms;
4027 }
4028
4029 /**
4030 * Wraps argument with unicode control characters for directionality safety
4031 *
4032 * This solves the problem where directionality-neutral characters at the edge of
4033 * the argument string get interpreted with the wrong directionality from the
4034 * enclosing context, giving renderings that look corrupted like "(Ben_(WMF".
4035 *
4036 * The wrapping is LRE...PDF or RLE...PDF, depending on the detected
4037 * directionality of the argument string, using the BIDI algorithm's own "First
4038 * strong directional codepoint" rule. Essentially, this works round the fact that
4039 * there is no embedding equivalent of U+2068 FSI (isolation with heuristic
4040 * direction inference). The latter is cleaner but still not widely supported.
4041 *
4042 * @param string $text Text to wrap
4043 * @return string Text, wrapped in LRE...PDF or RLE...PDF or nothing
4044 */
4045 public function embedBidi( $text = '' ) {
4046 $dir = Language::strongDirFromContent( $text );
4047 if ( $dir === 'ltr' ) {
4048 // Wrap in LEFT-TO-RIGHT EMBEDDING ... POP DIRECTIONAL FORMATTING
4049 return self::$lre . $text . self::$pdf;
4050 }
4051 if ( $dir === 'rtl' ) {
4052 // Wrap in RIGHT-TO-LEFT EMBEDDING ... POP DIRECTIONAL FORMATTING
4053 return self::$rle . $text . self::$pdf;
4054 }
4055 // No strong directionality: do not wrap
4056 return $text;
4057 }
4058
4059 /**
4060 * @todo Maybe translate block durations. Note that this function is somewhat misnamed: it
4061 * deals with translating the *duration* ("1 week", "4 days", etc), not the expiry time
4062 * (which is an absolute timestamp). Please note: do NOT add this blindly, as it is used
4063 * on old expiry lengths recorded in log entries. You'd need to provide the start date to
4064 * match up with it.
4065 *
4066 * @param string $str The validated block duration in English
4067 * @return string Somehow translated block duration
4068 * @see LanguageFi.php for example implementation
4069 */
4070 function translateBlockExpiry( $str ) {
4071 $duration = SpecialBlock::getSuggestedDurations( $this );
4072 foreach ( $duration as $show => $value ) {
4073 if ( strcmp( $str, $value ) == 0 ) {
4074 return htmlspecialchars( trim( $show ) );
4075 }
4076 }
4077
4078 if ( wfIsInfinity( $str ) ) {
4079 foreach ( $duration as $show => $value ) {
4080 if ( wfIsInfinity( $value ) ) {
4081 return htmlspecialchars( trim( $show ) );
4082 }
4083 }
4084 }
4085
4086 // If all else fails, return a standard duration or timestamp description.
4087 $time = strtotime( $str, 0 );
4088 if ( $time === false ) { // Unknown format. Return it as-is in case.
4089 return $str;
4090 } elseif ( $time !== strtotime( $str, 1 ) ) { // It's a relative timestamp.
4091 // $time is relative to 0 so it's a duration length.
4092 return $this->formatDuration( $time );
4093 } else { // It's an absolute timestamp.
4094 if ( $time === 0 ) {
4095 // wfTimestamp() handles 0 as current time instead of epoch.
4096 return $this->timeanddate( '19700101000000' );
4097 } else {
4098 return $this->timeanddate( $time );
4099 }
4100 }
4101 }
4102
4103 /**
4104 * languages like Chinese need to be segmented in order for the diff
4105 * to be of any use
4106 *
4107 * @param string $text
4108 * @return string
4109 */
4110 public function segmentForDiff( $text ) {
4111 return $text;
4112 }
4113
4114 /**
4115 * and unsegment to show the result
4116 *
4117 * @param string $text
4118 * @return string
4119 */
4120 public function unsegmentForDiff( $text ) {
4121 return $text;
4122 }
4123
4124 /**
4125 * Return the LanguageConverter used in the Language
4126 *
4127 * @since 1.19
4128 * @return LanguageConverter
4129 */
4130 public function getConverter() {
4131 return $this->mConverter;
4132 }
4133
4134 /**
4135 * convert text to all supported variants
4136 *
4137 * @param string $text
4138 * @return array
4139 */
4140 public function autoConvertToAllVariants( $text ) {
4141 return $this->mConverter->autoConvertToAllVariants( $text );
4142 }
4143
4144 /**
4145 * convert text to different variants of a language.
4146 *
4147 * @param string $text
4148 * @return string
4149 */
4150 public function convert( $text ) {
4151 return $this->mConverter->convert( $text );
4152 }
4153
4154 /**
4155 * Convert a Title object to a string in the preferred variant
4156 *
4157 * @param Title $title
4158 * @return string
4159 */
4160 public function convertTitle( $title ) {
4161 return $this->mConverter->convertTitle( $title );
4162 }
4163
4164 /**
4165 * Convert a namespace index to a string in the preferred variant
4166 *
4167 * @param int $ns
4168 * @return string
4169 */
4170 public function convertNamespace( $ns ) {
4171 return $this->mConverter->convertNamespace( $ns );
4172 }
4173
4174 /**
4175 * Check if this is a language with variants
4176 *
4177 * @return bool
4178 */
4179 public function hasVariants() {
4180 return count( $this->getVariants() ) > 1;
4181 }
4182
4183 /**
4184 * Check if the language has the specific variant
4185 *
4186 * @since 1.19
4187 * @param string $variant
4188 * @return bool
4189 */
4190 public function hasVariant( $variant ) {
4191 return (bool)$this->mConverter->validateVariant( $variant );
4192 }
4193
4194 /**
4195 * Put custom tags (e.g. -{ }-) around math to prevent conversion
4196 *
4197 * @param string $text
4198 * @return string
4199 * @deprecated since 1.22 is no longer used
4200 */
4201 public function armourMath( $text ) {
4202 return $this->mConverter->armourMath( $text );
4203 }
4204
4205 /**
4206 * Perform output conversion on a string, and encode for safe HTML output.
4207 * @param string $text Text to be converted
4208 * @param bool $isTitle Whether this conversion is for the article title
4209 * @return string
4210 * @todo this should get integrated somewhere sane
4211 */
4212 public function convertHtml( $text, $isTitle = false ) {
4213 return htmlspecialchars( $this->convert( $text, $isTitle ) );
4214 }
4215
4216 /**
4217 * @param string $key
4218 * @return string
4219 */
4220 public function convertCategoryKey( $key ) {
4221 return $this->mConverter->convertCategoryKey( $key );
4222 }
4223
4224 /**
4225 * Get the list of variants supported by this language
4226 * see sample implementation in LanguageZh.php
4227 *
4228 * @return array An array of language codes
4229 */
4230 public function getVariants() {
4231 return $this->mConverter->getVariants();
4232 }
4233
4234 /**
4235 * @return string
4236 */
4237 public function getPreferredVariant() {
4238 return $this->mConverter->getPreferredVariant();
4239 }
4240
4241 /**
4242 * @return string
4243 */
4244 public function getDefaultVariant() {
4245 return $this->mConverter->getDefaultVariant();
4246 }
4247
4248 /**
4249 * @return string
4250 */
4251 public function getURLVariant() {
4252 return $this->mConverter->getURLVariant();
4253 }
4254
4255 /**
4256 * If a language supports multiple variants, it is
4257 * possible that non-existing link in one variant
4258 * actually exists in another variant. this function
4259 * tries to find it. See e.g. LanguageZh.php
4260 * The input parameters may be modified upon return
4261 *
4262 * @param string &$link The name of the link
4263 * @param Title &$nt The title object of the link
4264 * @param bool $ignoreOtherCond To disable other conditions when
4265 * we need to transclude a template or update a category's link
4266 */
4267 public function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
4268 $this->mConverter->findVariantLink( $link, $nt, $ignoreOtherCond );
4269 }
4270
4271 /**
4272 * returns language specific options used by User::getPageRenderHash()
4273 * for example, the preferred language variant
4274 *
4275 * @return string
4276 */
4277 function getExtraHashOptions() {
4278 return $this->mConverter->getExtraHashOptions();
4279 }
4280
4281 /**
4282 * For languages that support multiple variants, the title of an
4283 * article may be displayed differently in different variants. this
4284 * function returns the apporiate title defined in the body of the article.
4285 *
4286 * @return string
4287 */
4288 public function getParsedTitle() {
4289 return $this->mConverter->getParsedTitle();
4290 }
4291
4292 /**
4293 * Prepare external link text for conversion. When the text is
4294 * a URL, it shouldn't be converted, and it'll be wrapped in
4295 * the "raw" tag (-{R| }-) to prevent conversion.
4296 *
4297 * This function is called "markNoConversion" for historical
4298 * reasons.
4299 *
4300 * @param string $text Text to be used for external link
4301 * @param bool $noParse Wrap it without confirming it's a real URL first
4302 * @return string The tagged text
4303 */
4304 public function markNoConversion( $text, $noParse = false ) {
4305 // Excluding protocal-relative URLs may avoid many false positives.
4306 if ( $noParse || preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
4307 return $this->mConverter->markNoConversion( $text );
4308 } else {
4309 return $text;
4310 }
4311 }
4312
4313 /**
4314 * A regular expression to match legal word-trailing characters
4315 * which should be merged onto a link of the form [[foo]]bar.
4316 *
4317 * @return string
4318 */
4319 public function linkTrail() {
4320 return self::$dataCache->getItem( $this->mCode, 'linkTrail' );
4321 }
4322
4323 /**
4324 * A regular expression character set to match legal word-prefixing
4325 * characters which should be merged onto a link of the form foo[[bar]].
4326 *
4327 * @return string
4328 */
4329 public function linkPrefixCharset() {
4330 return self::$dataCache->getItem( $this->mCode, 'linkPrefixCharset' );
4331 }
4332
4333 /**
4334 * @deprecated since 1.24, will be removed in 1.25
4335 * @return Language
4336 */
4337 function getLangObj() {
4338 wfDeprecated( __METHOD__, '1.24' );
4339 return $this;
4340 }
4341
4342 /**
4343 * Get the "parent" language which has a converter to convert a "compatible" language
4344 * (in another variant) to this language (eg. zh for zh-cn, but not en for en-gb).
4345 *
4346 * @return Language|null
4347 * @since 1.22
4348 */
4349 public function getParentLanguage() {
4350 if ( $this->mParentLanguage !== false ) {
4351 return $this->mParentLanguage;
4352 }
4353
4354 $pieces = explode( '-', $this->getCode() );
4355 $code = $pieces[0];
4356 if ( !in_array( $code, LanguageConverter::$languagesWithVariants ) ) {
4357 $this->mParentLanguage = null;
4358 return null;
4359 }
4360 $lang = Language::factory( $code );
4361 if ( !$lang->hasVariant( $this->getCode() ) ) {
4362 $this->mParentLanguage = null;
4363 return null;
4364 }
4365
4366 $this->mParentLanguage = $lang;
4367 return $lang;
4368 }
4369
4370 /**
4371 * Get the RFC 3066 code for this language object
4372 *
4373 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
4374 * htmlspecialchars() or similar
4375 *
4376 * @return string
4377 */
4378 public function getCode() {
4379 return $this->mCode;
4380 }
4381
4382 /**
4383 * Get the code in Bcp47 format which we can use
4384 * inside of html lang="" tags.
4385 *
4386 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
4387 * htmlspecialchars() or similar.
4388 *
4389 * @since 1.19
4390 * @return string
4391 */
4392 public function getHtmlCode() {
4393 if ( is_null( $this->mHtmlCode ) ) {
4394 $this->mHtmlCode = wfBCP47( $this->getCode() );
4395 }
4396 return $this->mHtmlCode;
4397 }
4398
4399 /**
4400 * @param string $code
4401 */
4402 public function setCode( $code ) {
4403 $this->mCode = $code;
4404 // Ensure we don't leave incorrect cached data lying around
4405 $this->mHtmlCode = null;
4406 $this->mParentLanguage = false;
4407 }
4408
4409 /**
4410 * Get the name of a file for a certain language code
4411 * @param string $prefix Prepend this to the filename
4412 * @param string $code Language code
4413 * @param string $suffix Append this to the filename
4414 * @throws MWException
4415 * @return string $prefix . $mangledCode . $suffix
4416 */
4417 public static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
4418 if ( !self::isValidBuiltInCode( $code ) ) {
4419 throw new MWException( "Invalid language code \"$code\"" );
4420 }
4421
4422 return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
4423 }
4424
4425 /**
4426 * Get the language code from a file name. Inverse of getFileName()
4427 * @param string $filename $prefix . $languageCode . $suffix
4428 * @param string $prefix Prefix before the language code
4429 * @param string $suffix Suffix after the language code
4430 * @return string Language code, or false if $prefix or $suffix isn't found
4431 */
4432 public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
4433 $m = null;
4434 preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
4435 preg_quote( $suffix, '/' ) . '/', $filename, $m );
4436 if ( !count( $m ) ) {
4437 return false;
4438 }
4439 return str_replace( '_', '-', strtolower( $m[1] ) );
4440 }
4441
4442 /**
4443 * @param string $code
4444 * @return string
4445 */
4446 public static function getMessagesFileName( $code ) {
4447 global $IP;
4448 $file = self::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
4449 Hooks::run( 'Language::getMessagesFileName', array( $code, &$file ) );
4450 return $file;
4451 }
4452
4453 /**
4454 * @param string $code
4455 * @return string
4456 * @since 1.23
4457 */
4458 public static function getJsonMessagesFileName( $code ) {
4459 global $IP;
4460
4461 if ( !self::isValidBuiltInCode( $code ) ) {
4462 throw new MWException( "Invalid language code \"$code\"" );
4463 }
4464
4465 return "$IP/languages/i18n/$code.json";
4466 }
4467
4468 /**
4469 * @param string $code
4470 * @return string
4471 */
4472 public static function getClassFileName( $code ) {
4473 global $IP;
4474 return self::getFileName( "$IP/languages/classes/Language", $code, '.php' );
4475 }
4476
4477 /**
4478 * Get the first fallback for a given language.
4479 *
4480 * @param string $code
4481 *
4482 * @return bool|string
4483 */
4484 public static function getFallbackFor( $code ) {
4485 if ( $code === 'en' || !Language::isValidBuiltInCode( $code ) ) {
4486 return false;
4487 } else {
4488 $fallbacks = self::getFallbacksFor( $code );
4489 return $fallbacks[0];
4490 }
4491 }
4492
4493 /**
4494 * Get the ordered list of fallback languages.
4495 *
4496 * @since 1.19
4497 * @param string $code Language code
4498 * @return array Non-empty array, ending in "en"
4499 */
4500 public static function getFallbacksFor( $code ) {
4501 if ( $code === 'en' || !Language::isValidBuiltInCode( $code ) ) {
4502 return array();
4503 }
4504 // For unknown languages, fallbackSequence returns an empty array,
4505 // hardcode fallback to 'en' in that case.
4506 return self::getLocalisationCache()->getItem( $code, 'fallbackSequence' ) ?: array( 'en' );
4507 }
4508
4509 /**
4510 * Get the ordered list of fallback languages, ending with the fallback
4511 * language chain for the site language.
4512 *
4513 * @since 1.22
4514 * @param string $code Language code
4515 * @return array Array( fallbacks, site fallbacks )
4516 */
4517 public static function getFallbacksIncludingSiteLanguage( $code ) {
4518 global $wgLanguageCode;
4519
4520 // Usually, we will only store a tiny number of fallback chains, so we
4521 // keep them in static memory.
4522 $cacheKey = "{$code}-{$wgLanguageCode}";
4523
4524 if ( !array_key_exists( $cacheKey, self::$fallbackLanguageCache ) ) {
4525 $fallbacks = self::getFallbacksFor( $code );
4526
4527 // Append the site's fallback chain, including the site language itself
4528 $siteFallbacks = self::getFallbacksFor( $wgLanguageCode );
4529 array_unshift( $siteFallbacks, $wgLanguageCode );
4530
4531 // Eliminate any languages already included in the chain
4532 $siteFallbacks = array_diff( $siteFallbacks, $fallbacks );
4533
4534 self::$fallbackLanguageCache[$cacheKey] = array( $fallbacks, $siteFallbacks );
4535 }
4536 return self::$fallbackLanguageCache[$cacheKey];
4537 }
4538
4539 /**
4540 * Get all messages for a given language
4541 * WARNING: this may take a long time. If you just need all message *keys*
4542 * but need the *contents* of only a few messages, consider using getMessageKeysFor().
4543 *
4544 * @param string $code
4545 *
4546 * @return array
4547 */
4548 public static function getMessagesFor( $code ) {
4549 return self::getLocalisationCache()->getItem( $code, 'messages' );
4550 }
4551
4552 /**
4553 * Get a message for a given language
4554 *
4555 * @param string $key
4556 * @param string $code
4557 *
4558 * @return string
4559 */
4560 public static function getMessageFor( $key, $code ) {
4561 return self::getLocalisationCache()->getSubitem( $code, 'messages', $key );
4562 }
4563
4564 /**
4565 * Get all message keys for a given language. This is a faster alternative to
4566 * array_keys( Language::getMessagesFor( $code ) )
4567 *
4568 * @since 1.19
4569 * @param string $code Language code
4570 * @return array Array of message keys (strings)
4571 */
4572 public static function getMessageKeysFor( $code ) {
4573 return self::getLocalisationCache()->getSubItemList( $code, 'messages' );
4574 }
4575
4576 /**
4577 * @param string $talk
4578 * @return mixed
4579 */
4580 function fixVariableInNamespace( $talk ) {
4581 if ( strpos( $talk, '$1' ) === false ) {
4582 return $talk;
4583 }
4584
4585 global $wgMetaNamespace;
4586 $talk = str_replace( '$1', $wgMetaNamespace, $talk );
4587
4588 # Allow grammar transformations
4589 # Allowing full message-style parsing would make simple requests
4590 # such as action=raw much more expensive than they need to be.
4591 # This will hopefully cover most cases.
4592 $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
4593 array( &$this, 'replaceGrammarInNamespace' ), $talk );
4594 return str_replace( ' ', '_', $talk );
4595 }
4596
4597 /**
4598 * @param string $m
4599 * @return string
4600 */
4601 function replaceGrammarInNamespace( $m ) {
4602 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
4603 }
4604
4605 /**
4606 * @throws MWException
4607 * @return array
4608 */
4609 static function getCaseMaps() {
4610 static $wikiUpperChars, $wikiLowerChars;
4611 if ( isset( $wikiUpperChars ) ) {
4612 return array( $wikiUpperChars, $wikiLowerChars );
4613 }
4614
4615 $arr = wfGetPrecompiledData( 'Utf8Case.ser' );
4616 if ( $arr === false ) {
4617 throw new MWException(
4618 "Utf8Case.ser is missing, please run \"make\" in the serialized directory\n" );
4619 }
4620 $wikiUpperChars = $arr['wikiUpperChars'];
4621 $wikiLowerChars = $arr['wikiLowerChars'];
4622 return array( $wikiUpperChars, $wikiLowerChars );
4623 }
4624
4625 /**
4626 * Decode an expiry (block, protection, etc) which has come from the DB
4627 *
4628 * @param string $expiry Database expiry String
4629 * @param bool|int $format True to process using language functions, or TS_ constant
4630 * to return the expiry in a given timestamp
4631 * @param string $inifinity If $format is not true, use this string for infinite expiry
4632 * @return string
4633 * @since 1.18
4634 */
4635 public function formatExpiry( $expiry, $format = true, $infinity = 'infinity' ) {
4636 static $dbInfinity;
4637 if ( $dbInfinity === null ) {
4638 $dbInfinity = wfGetDB( DB_SLAVE )->getInfinity();
4639 }
4640
4641 if ( $expiry == '' || $expiry === 'infinity' || $expiry == $dbInfinity ) {
4642 return $format === true
4643 ? $this->getMessageFromDB( 'infiniteblock' )
4644 : $infinity;
4645 } else {
4646 return $format === true
4647 ? $this->timeanddate( $expiry, /* User preference timezone */ true )
4648 : wfTimestamp( $format, $expiry );
4649 }
4650 }
4651
4652 /**
4653 * @todo Document
4654 * @param int|float $seconds
4655 * @param array $format Optional
4656 * If $format['avoid'] === 'avoidseconds': don't mention seconds if $seconds >= 1 hour.
4657 * If $format['avoid'] === 'avoidminutes': don't mention seconds/minutes if $seconds > 48 hours.
4658 * If $format['noabbrevs'] is true: use 'seconds' and friends instead of 'seconds-abbrev'
4659 * and friends.
4660 * For backwards compatibility, $format may also be one of the strings 'avoidseconds'
4661 * or 'avoidminutes'.
4662 * @return string
4663 */
4664 function formatTimePeriod( $seconds, $format = array() ) {
4665 if ( !is_array( $format ) ) {
4666 $format = array( 'avoid' => $format ); // For backwards compatibility
4667 }
4668 if ( !isset( $format['avoid'] ) ) {
4669 $format['avoid'] = false;
4670 }
4671 if ( !isset( $format['noabbrevs'] ) ) {
4672 $format['noabbrevs'] = false;
4673 }
4674 $secondsMsg = wfMessage(
4675 $format['noabbrevs'] ? 'seconds' : 'seconds-abbrev' )->inLanguage( $this );
4676 $minutesMsg = wfMessage(
4677 $format['noabbrevs'] ? 'minutes' : 'minutes-abbrev' )->inLanguage( $this );
4678 $hoursMsg = wfMessage(
4679 $format['noabbrevs'] ? 'hours' : 'hours-abbrev' )->inLanguage( $this );
4680 $daysMsg = wfMessage(
4681 $format['noabbrevs'] ? 'days' : 'days-abbrev' )->inLanguage( $this );
4682
4683 if ( round( $seconds * 10 ) < 100 ) {
4684 $s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
4685 $s = $secondsMsg->params( $s )->text();
4686 } elseif ( round( $seconds ) < 60 ) {
4687 $s = $this->formatNum( round( $seconds ) );
4688 $s = $secondsMsg->params( $s )->text();
4689 } elseif ( round( $seconds ) < 3600 ) {
4690 $minutes = floor( $seconds / 60 );
4691 $secondsPart = round( fmod( $seconds, 60 ) );
4692 if ( $secondsPart == 60 ) {
4693 $secondsPart = 0;
4694 $minutes++;
4695 }
4696 $s = $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4697 $s .= ' ';
4698 $s .= $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4699 } elseif ( round( $seconds ) <= 2 * 86400 ) {
4700 $hours = floor( $seconds / 3600 );
4701 $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
4702 $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
4703 if ( $secondsPart == 60 ) {
4704 $secondsPart = 0;
4705 $minutes++;
4706 }
4707 if ( $minutes == 60 ) {
4708 $minutes = 0;
4709 $hours++;
4710 }
4711 $s = $hoursMsg->params( $this->formatNum( $hours ) )->text();
4712 $s .= ' ';
4713 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4714 if ( !in_array( $format['avoid'], array( 'avoidseconds', 'avoidminutes' ) ) ) {
4715 $s .= ' ' . $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4716 }
4717 } else {
4718 $days = floor( $seconds / 86400 );
4719 if ( $format['avoid'] === 'avoidminutes' ) {
4720 $hours = round( ( $seconds - $days * 86400 ) / 3600 );
4721 if ( $hours == 24 ) {
4722 $hours = 0;
4723 $days++;
4724 }
4725 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4726 $s .= ' ';
4727 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4728 } elseif ( $format['avoid'] === 'avoidseconds' ) {
4729 $hours = floor( ( $seconds - $days * 86400 ) / 3600 );
4730 $minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
4731 if ( $minutes == 60 ) {
4732 $minutes = 0;
4733 $hours++;
4734 }
4735 if ( $hours == 24 ) {
4736 $hours = 0;
4737 $days++;
4738 }
4739 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4740 $s .= ' ';
4741 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4742 $s .= ' ';
4743 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4744 } else {
4745 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4746 $s .= ' ';
4747 $s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
4748 }
4749 }
4750 return $s;
4751 }
4752
4753 /**
4754 * Format a bitrate for output, using an appropriate
4755 * unit (bps, kbps, Mbps, Gbps, Tbps, Pbps, Ebps, Zbps or Ybps) according to
4756 * the magnitude in question.
4757 *
4758 * This use base 1000. For base 1024 use formatSize(), for another base
4759 * see formatComputingNumbers().
4760 *
4761 * @param int $bps
4762 * @return string
4763 */
4764 function formatBitrate( $bps ) {
4765 return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" );
4766 }
4767
4768 /**
4769 * @param int $size Size of the unit
4770 * @param int $boundary Size boundary (1000, or 1024 in most cases)
4771 * @param string $messageKey Message key to be uesd
4772 * @return string
4773 */
4774 function formatComputingNumbers( $size, $boundary, $messageKey ) {
4775 if ( $size <= 0 ) {
4776 return str_replace( '$1', $this->formatNum( $size ),
4777 $this->getMessageFromDB( str_replace( '$1', '', $messageKey ) )
4778 );
4779 }
4780 $sizes = array( '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zeta', 'yotta' );
4781 $index = 0;
4782
4783 $maxIndex = count( $sizes ) - 1;
4784 while ( $size >= $boundary && $index < $maxIndex ) {
4785 $index++;
4786 $size /= $boundary;
4787 }
4788
4789 // For small sizes no decimal places necessary
4790 $round = 0;
4791 if ( $index > 1 ) {
4792 // For MB and bigger two decimal places are smarter
4793 $round = 2;
4794 }
4795 $msg = str_replace( '$1', $sizes[$index], $messageKey );
4796
4797 $size = round( $size, $round );
4798 $text = $this->getMessageFromDB( $msg );
4799 return str_replace( '$1', $this->formatNum( $size ), $text );
4800 }
4801
4802 /**
4803 * Format a size in bytes for output, using an appropriate
4804 * unit (B, KB, MB, GB, TB, PB, EB, ZB or YB) according to the magnitude in question
4805 *
4806 * This method use base 1024. For base 1000 use formatBitrate(), for
4807 * another base see formatComputingNumbers()
4808 *
4809 * @param int $size Size to format
4810 * @return string Plain text (not HTML)
4811 */
4812 function formatSize( $size ) {
4813 return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" );
4814 }
4815
4816 /**
4817 * Make a list item, used by various special pages
4818 *
4819 * @param string $page Page link
4820 * @param string $details HTML safe text between brackets
4821 * @param bool $oppositedm Add the direction mark opposite to your
4822 * language, to display text properly
4823 * @return HTML escaped string
4824 */
4825 function specialList( $page, $details, $oppositedm = true ) {
4826 if ( !$details ) {
4827 return $page;
4828 }
4829
4830 $dirmark = ( $oppositedm ? $this->getDirMark( true ) : '' ) . $this->getDirMark();
4831 return
4832 $page .
4833 $dirmark .
4834 $this->msg( 'word-separator' )->escaped() .
4835 $this->msg( 'parentheses' )->rawParams( $details )->escaped();
4836 }
4837
4838 /**
4839 * Generate (prev x| next x) (20|50|100...) type links for paging
4840 *
4841 * @param Title $title Title object to link
4842 * @param int $offset
4843 * @param int $limit
4844 * @param array $query Optional URL query parameter string
4845 * @param bool $atend Optional param for specified if this is the last page
4846 * @return string
4847 */
4848 public function viewPrevNext( Title $title, $offset, $limit,
4849 array $query = array(), $atend = false
4850 ) {
4851 // @todo FIXME: Why on earth this needs one message for the text and another one for tooltip?
4852
4853 # Make 'previous' link
4854 $prev = wfMessage( 'prevn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4855 if ( $offset > 0 ) {
4856 $plink = $this->numLink( $title, max( $offset - $limit, 0 ), $limit,
4857 $query, $prev, 'prevn-title', 'mw-prevlink' );
4858 } else {
4859 $plink = htmlspecialchars( $prev );
4860 }
4861
4862 # Make 'next' link
4863 $next = wfMessage( 'nextn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4864 if ( $atend ) {
4865 $nlink = htmlspecialchars( $next );
4866 } else {
4867 $nlink = $this->numLink( $title, $offset + $limit, $limit,
4868 $query, $next, 'nextn-title', 'mw-nextlink' );
4869 }
4870
4871 # Make links to set number of items per page
4872 $numLinks = array();
4873 foreach ( array( 20, 50, 100, 250, 500 ) as $num ) {
4874 $numLinks[] = $this->numLink( $title, $offset, $num,
4875 $query, $this->formatNum( $num ), 'shown-title', 'mw-numlink' );
4876 }
4877
4878 return wfMessage( 'viewprevnext' )->inLanguage( $this )->title( $title
4879 )->rawParams( $plink, $nlink, $this->pipeList( $numLinks ) )->escaped();
4880 }
4881
4882 /**
4883 * Helper function for viewPrevNext() that generates links
4884 *
4885 * @param Title $title Title object to link
4886 * @param int $offset
4887 * @param int $limit
4888 * @param array $query Extra query parameters
4889 * @param string $link Text to use for the link; will be escaped
4890 * @param string $tooltipMsg Name of the message to use as tooltip
4891 * @param string $class Value of the "class" attribute of the link
4892 * @return string HTML fragment
4893 */
4894 private function numLink( Title $title, $offset, $limit, array $query, $link,
4895 $tooltipMsg, $class
4896 ) {
4897 $query = array( 'limit' => $limit, 'offset' => $offset ) + $query;
4898 $tooltip = wfMessage( $tooltipMsg )->inLanguage( $this )->title( $title )
4899 ->numParams( $limit )->text();
4900
4901 return Html::element( 'a', array( 'href' => $title->getLocalURL( $query ),
4902 'title' => $tooltip, 'class' => $class ), $link );
4903 }
4904
4905 /**
4906 * Get the conversion rule title, if any.
4907 *
4908 * @return string
4909 */
4910 public function getConvRuleTitle() {
4911 return $this->mConverter->getConvRuleTitle();
4912 }
4913
4914 /**
4915 * Get the compiled plural rules for the language
4916 * @since 1.20
4917 * @return array Associative array with plural form, and plural rule as key-value pairs
4918 */
4919 public function getCompiledPluralRules() {
4920 $pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'compiledPluralRules' );
4921 $fallbacks = Language::getFallbacksFor( $this->mCode );
4922 if ( !$pluralRules ) {
4923 foreach ( $fallbacks as $fallbackCode ) {
4924 $pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
4925 if ( $pluralRules ) {
4926 break;
4927 }
4928 }
4929 }
4930 return $pluralRules;
4931 }
4932
4933 /**
4934 * Get the plural rules for the language
4935 * @since 1.20
4936 * @return array Associative array with plural form number and plural rule as key-value pairs
4937 */
4938 public function getPluralRules() {
4939 $pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRules' );
4940 $fallbacks = Language::getFallbacksFor( $this->mCode );
4941 if ( !$pluralRules ) {
4942 foreach ( $fallbacks as $fallbackCode ) {
4943 $pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRules' );
4944 if ( $pluralRules ) {
4945 break;
4946 }
4947 }
4948 }
4949 return $pluralRules;
4950 }
4951
4952 /**
4953 * Get the plural rule types for the language
4954 * @since 1.22
4955 * @return array Associative array with plural form number and plural rule type as key-value pairs
4956 */
4957 public function getPluralRuleTypes() {
4958 $pluralRuleTypes = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRuleTypes' );
4959 $fallbacks = Language::getFallbacksFor( $this->mCode );
4960 if ( !$pluralRuleTypes ) {
4961 foreach ( $fallbacks as $fallbackCode ) {
4962 $pluralRuleTypes = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRuleTypes' );
4963 if ( $pluralRuleTypes ) {
4964 break;
4965 }
4966 }
4967 }
4968 return $pluralRuleTypes;
4969 }
4970
4971 /**
4972 * Find the index number of the plural rule appropriate for the given number
4973 * @param int $number
4974 * @return int The index number of the plural rule
4975 */
4976 public function getPluralRuleIndexNumber( $number ) {
4977 $pluralRules = $this->getCompiledPluralRules();
4978 $form = Evaluator::evaluateCompiled( $number, $pluralRules );
4979 return $form;
4980 }
4981
4982 /**
4983 * Find the plural rule type appropriate for the given number
4984 * For example, if the language is set to Arabic, getPluralType(5) should
4985 * return 'few'.
4986 * @since 1.22
4987 * @param int $number
4988 * @return string The name of the plural rule type, e.g. one, two, few, many
4989 */
4990 public function getPluralRuleType( $number ) {
4991 $index = $this->getPluralRuleIndexNumber( $number );
4992 $pluralRuleTypes = $this->getPluralRuleTypes();
4993 if ( isset( $pluralRuleTypes[$index] ) ) {
4994 return $pluralRuleTypes[$index];
4995 } else {
4996 return 'other';
4997 }
4998 }
4999 }