Merge "Use ImportStringSource for simple import sources"
[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 // Use the /s modifier (PCRE_DOTALL) so (.*) also matches newlines
3695 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
3696 '[\xf0-\xf7][\x80-\xbf]{1,2})$/s', $string, $m )
3697 ) {
3698 # We chopped in the middle of a character; remove it
3699 $string = $m[1];
3700 }
3701 }
3702 return $string;
3703 }
3704
3705 /**
3706 * Remove bytes that represent an incomplete Unicode character
3707 * at the start of string (e.g. bytes of the char are missing)
3708 *
3709 * @param string $string
3710 * @return string
3711 */
3712 protected function removeBadCharFirst( $string ) {
3713 if ( $string != '' ) {
3714 $char = ord( $string[0] );
3715 if ( $char >= 0x80 && $char < 0xc0 ) {
3716 # We chopped in the middle of a character; remove the whole thing
3717 $string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
3718 }
3719 }
3720 return $string;
3721 }
3722
3723 /**
3724 * Truncate a string of valid HTML to a specified length in bytes,
3725 * appending an optional string (e.g. for ellipses), and return valid HTML
3726 *
3727 * This is only intended for styled/linked text, such as HTML with
3728 * tags like <span> and <a>, were the tags are self-contained (valid HTML).
3729 * Also, this will not detect things like "display:none" CSS.
3730 *
3731 * Note: since 1.18 you do not need to leave extra room in $length for ellipses.
3732 *
3733 * @param string $text HTML string to truncate
3734 * @param int $length (zero/positive) Maximum length (including ellipses)
3735 * @param string $ellipsis String to append to the truncated text
3736 * @return string
3737 */
3738 function truncateHtml( $text, $length, $ellipsis = '...' ) {
3739 # Use the localized ellipsis character
3740 if ( $ellipsis == '...' ) {
3741 $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3742 }
3743 # Check if there is clearly no need to truncate
3744 if ( $length <= 0 ) {
3745 return $ellipsis; // no text shown, nothing to format (convention)
3746 } elseif ( strlen( $text ) <= $length ) {
3747 return $text; // string short enough even *with* HTML (short-circuit)
3748 }
3749
3750 $dispLen = 0; // innerHTML legth so far
3751 $testingEllipsis = false; // checking if ellipses will make string longer/equal?
3752 $tagType = 0; // 0-open, 1-close
3753 $bracketState = 0; // 1-tag start, 2-tag name, 0-neither
3754 $entityState = 0; // 0-not entity, 1-entity
3755 $tag = $ret = ''; // accumulated tag name, accumulated result string
3756 $openTags = array(); // open tag stack
3757 $maybeState = null; // possible truncation state
3758
3759 $textLen = strlen( $text );
3760 $neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated
3761 for ( $pos = 0; true; ++$pos ) {
3762 # Consider truncation once the display length has reached the maximim.
3763 # We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
3764 # Check that we're not in the middle of a bracket/entity...
3765 if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) {
3766 if ( !$testingEllipsis ) {
3767 $testingEllipsis = true;
3768 # Save where we are; we will truncate here unless there turn out to
3769 # be so few remaining characters that truncation is not necessary.
3770 if ( !$maybeState ) { // already saved? ($neLength = 0 case)
3771 $maybeState = array( $ret, $openTags ); // save state
3772 }
3773 } elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) {
3774 # String in fact does need truncation, the truncation point was OK.
3775 list( $ret, $openTags ) = $maybeState; // reload state
3776 $ret = $this->removeBadCharLast( $ret ); // multi-byte char fix
3777 $ret .= $ellipsis; // add ellipsis
3778 break;
3779 }
3780 }
3781 if ( $pos >= $textLen ) {
3782 break; // extra iteration just for above checks
3783 }
3784
3785 # Read the next char...
3786 $ch = $text[$pos];
3787 $lastCh = $pos ? $text[$pos - 1] : '';
3788 $ret .= $ch; // add to result string
3789 if ( $ch == '<' ) {
3790 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); // for bad HTML
3791 $entityState = 0; // for bad HTML
3792 $bracketState = 1; // tag started (checking for backslash)
3793 } elseif ( $ch == '>' ) {
3794 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags );
3795 $entityState = 0; // for bad HTML
3796 $bracketState = 0; // out of brackets
3797 } elseif ( $bracketState == 1 ) {
3798 if ( $ch == '/' ) {
3799 $tagType = 1; // close tag (e.g. "</span>")
3800 } else {
3801 $tagType = 0; // open tag (e.g. "<span>")
3802 $tag .= $ch;
3803 }
3804 $bracketState = 2; // building tag name
3805 } elseif ( $bracketState == 2 ) {
3806 if ( $ch != ' ' ) {
3807 $tag .= $ch;
3808 } else {
3809 // Name found (e.g. "<a href=..."), add on tag attributes...
3810 $pos += $this->truncate_skip( $ret, $text, "<>", $pos + 1 );
3811 }
3812 } elseif ( $bracketState == 0 ) {
3813 if ( $entityState ) {
3814 if ( $ch == ';' ) {
3815 $entityState = 0;
3816 $dispLen++; // entity is one displayed char
3817 }
3818 } else {
3819 if ( $neLength == 0 && !$maybeState ) {
3820 // Save state without $ch. We want to *hit* the first
3821 // display char (to get tags) but not *use* it if truncating.
3822 $maybeState = array( substr( $ret, 0, -1 ), $openTags );
3823 }
3824 if ( $ch == '&' ) {
3825 $entityState = 1; // entity found, (e.g. "&#160;")
3826 } else {
3827 $dispLen++; // this char is displayed
3828 // Add the next $max display text chars after this in one swoop...
3829 $max = ( $testingEllipsis ? $length : $neLength ) - $dispLen;
3830 $skipped = $this->truncate_skip( $ret, $text, "<>&", $pos + 1, $max );
3831 $dispLen += $skipped;
3832 $pos += $skipped;
3833 }
3834 }
3835 }
3836 }
3837 // Close the last tag if left unclosed by bad HTML
3838 $this->truncate_endBracket( $tag, $text[$textLen - 1], $tagType, $openTags );
3839 while ( count( $openTags ) > 0 ) {
3840 $ret .= '</' . array_pop( $openTags ) . '>'; // close open tags
3841 }
3842 return $ret;
3843 }
3844
3845 /**
3846 * truncateHtml() helper function
3847 * like strcspn() but adds the skipped chars to $ret
3848 *
3849 * @param string $ret
3850 * @param string $text
3851 * @param string $search
3852 * @param int $start
3853 * @param null|int $len
3854 * @return int
3855 */
3856 private function truncate_skip( &$ret, $text, $search, $start, $len = null ) {
3857 if ( $len === null ) {
3858 $len = -1; // -1 means "no limit" for strcspn
3859 } elseif ( $len < 0 ) {
3860 $len = 0; // sanity
3861 }
3862 $skipCount = 0;
3863 if ( $start < strlen( $text ) ) {
3864 $skipCount = strcspn( $text, $search, $start, $len );
3865 $ret .= substr( $text, $start, $skipCount );
3866 }
3867 return $skipCount;
3868 }
3869
3870 /**
3871 * truncateHtml() helper function
3872 * (a) push or pop $tag from $openTags as needed
3873 * (b) clear $tag value
3874 * @param string &$tag Current HTML tag name we are looking at
3875 * @param int $tagType (0-open tag, 1-close tag)
3876 * @param string $lastCh Character before the '>' that ended this tag
3877 * @param array &$openTags Open tag stack (not accounting for $tag)
3878 */
3879 private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) {
3880 $tag = ltrim( $tag );
3881 if ( $tag != '' ) {
3882 if ( $tagType == 0 && $lastCh != '/' ) {
3883 $openTags[] = $tag; // tag opened (didn't close itself)
3884 } elseif ( $tagType == 1 ) {
3885 if ( $openTags && $tag == $openTags[count( $openTags ) - 1] ) {
3886 array_pop( $openTags ); // tag closed
3887 }
3888 }
3889 $tag = '';
3890 }
3891 }
3892
3893 /**
3894 * Grammatical transformations, needed for inflected languages
3895 * Invoked by putting {{grammar:case|word}} in a message
3896 *
3897 * @param string $word
3898 * @param string $case
3899 * @return string
3900 */
3901 function convertGrammar( $word, $case ) {
3902 global $wgGrammarForms;
3903 if ( isset( $wgGrammarForms[$this->getCode()][$case][$word] ) ) {
3904 return $wgGrammarForms[$this->getCode()][$case][$word];
3905 }
3906
3907 return $word;
3908 }
3909 /**
3910 * Get the grammar forms for the content language
3911 * @return array Array of grammar forms
3912 * @since 1.20
3913 */
3914 function getGrammarForms() {
3915 global $wgGrammarForms;
3916 if ( isset( $wgGrammarForms[$this->getCode()] )
3917 && is_array( $wgGrammarForms[$this->getCode()] )
3918 ) {
3919 return $wgGrammarForms[$this->getCode()];
3920 }
3921
3922 return array();
3923 }
3924 /**
3925 * Provides an alternative text depending on specified gender.
3926 * Usage {{gender:username|masculine|feminine|unknown}}.
3927 * username is optional, in which case the gender of current user is used,
3928 * but only in (some) interface messages; otherwise default gender is used.
3929 *
3930 * If no forms are given, an empty string is returned. If only one form is
3931 * given, it will be returned unconditionally. These details are implied by
3932 * the caller and cannot be overridden in subclasses.
3933 *
3934 * If three forms are given, the default is to use the third (unknown) form.
3935 * If fewer than three forms are given, the default is to use the first (masculine) form.
3936 * These details can be overridden in subclasses.
3937 *
3938 * @param string $gender
3939 * @param array $forms
3940 *
3941 * @return string
3942 */
3943 function gender( $gender, $forms ) {
3944 if ( !count( $forms ) ) {
3945 return '';
3946 }
3947 $forms = $this->preConvertPlural( $forms, 2 );
3948 if ( $gender === 'male' ) {
3949 return $forms[0];
3950 }
3951 if ( $gender === 'female' ) {
3952 return $forms[1];
3953 }
3954 return isset( $forms[2] ) ? $forms[2] : $forms[0];
3955 }
3956
3957 /**
3958 * Plural form transformations, needed for some languages.
3959 * For example, there are 3 form of plural in Russian and Polish,
3960 * depending on "count mod 10". See [[w:Plural]]
3961 * For English it is pretty simple.
3962 *
3963 * Invoked by putting {{plural:count|wordform1|wordform2}}
3964 * or {{plural:count|wordform1|wordform2|wordform3}}
3965 *
3966 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
3967 *
3968 * @param int $count Non-localized number
3969 * @param array $forms Different plural forms
3970 * @return string Correct form of plural for $count in this language
3971 */
3972 function convertPlural( $count, $forms ) {
3973 // Handle explicit n=pluralform cases
3974 $forms = $this->handleExplicitPluralForms( $count, $forms );
3975 if ( is_string( $forms ) ) {
3976 return $forms;
3977 }
3978 if ( !count( $forms ) ) {
3979 return '';
3980 }
3981
3982 $pluralForm = $this->getPluralRuleIndexNumber( $count );
3983 $pluralForm = min( $pluralForm, count( $forms ) - 1 );
3984 return $forms[$pluralForm];
3985 }
3986
3987 /**
3988 * Handles explicit plural forms for Language::convertPlural()
3989 *
3990 * In {{PLURAL:$1|0=nothing|one|many}}, 0=nothing will be returned if $1 equals zero.
3991 * If an explicitly defined plural form matches the $count, then
3992 * string value returned, otherwise array returned for further consideration
3993 * by CLDR rules or overridden convertPlural().
3994 *
3995 * @since 1.23
3996 *
3997 * @param int $count Non-localized number
3998 * @param array $forms Different plural forms
3999 *
4000 * @return array|string
4001 */
4002 protected function handleExplicitPluralForms( $count, array $forms ) {
4003 foreach ( $forms as $index => $form ) {
4004 if ( preg_match( '/\d+=/i', $form ) ) {
4005 $pos = strpos( $form, '=' );
4006 if ( substr( $form, 0, $pos ) === (string)$count ) {
4007 return substr( $form, $pos + 1 );
4008 }
4009 unset( $forms[$index] );
4010 }
4011 }
4012 return array_values( $forms );
4013 }
4014
4015 /**
4016 * Checks that convertPlural was given an array and pads it to requested
4017 * amount of forms by copying the last one.
4018 *
4019 * @param array $forms Array of forms given to convertPlural
4020 * @param int $count How many forms should there be at least
4021 * @return array Padded array of forms or an exception if not an array
4022 */
4023 protected function preConvertPlural( /* Array */ $forms, $count ) {
4024 while ( count( $forms ) < $count ) {
4025 $forms[] = $forms[count( $forms ) - 1];
4026 }
4027 return $forms;
4028 }
4029
4030 /**
4031 * Wraps argument with unicode control characters for directionality safety
4032 *
4033 * This solves the problem where directionality-neutral characters at the edge of
4034 * the argument string get interpreted with the wrong directionality from the
4035 * enclosing context, giving renderings that look corrupted like "(Ben_(WMF".
4036 *
4037 * The wrapping is LRE...PDF or RLE...PDF, depending on the detected
4038 * directionality of the argument string, using the BIDI algorithm's own "First
4039 * strong directional codepoint" rule. Essentially, this works round the fact that
4040 * there is no embedding equivalent of U+2068 FSI (isolation with heuristic
4041 * direction inference). The latter is cleaner but still not widely supported.
4042 *
4043 * @param string $text Text to wrap
4044 * @return string Text, wrapped in LRE...PDF or RLE...PDF or nothing
4045 */
4046 public function embedBidi( $text = '' ) {
4047 $dir = Language::strongDirFromContent( $text );
4048 if ( $dir === 'ltr' ) {
4049 // Wrap in LEFT-TO-RIGHT EMBEDDING ... POP DIRECTIONAL FORMATTING
4050 return self::$lre . $text . self::$pdf;
4051 }
4052 if ( $dir === 'rtl' ) {
4053 // Wrap in RIGHT-TO-LEFT EMBEDDING ... POP DIRECTIONAL FORMATTING
4054 return self::$rle . $text . self::$pdf;
4055 }
4056 // No strong directionality: do not wrap
4057 return $text;
4058 }
4059
4060 /**
4061 * @todo Maybe translate block durations. Note that this function is somewhat misnamed: it
4062 * deals with translating the *duration* ("1 week", "4 days", etc), not the expiry time
4063 * (which is an absolute timestamp). Please note: do NOT add this blindly, as it is used
4064 * on old expiry lengths recorded in log entries. You'd need to provide the start date to
4065 * match up with it.
4066 *
4067 * @param string $str The validated block duration in English
4068 * @return string Somehow translated block duration
4069 * @see LanguageFi.php for example implementation
4070 */
4071 function translateBlockExpiry( $str ) {
4072 $duration = SpecialBlock::getSuggestedDurations( $this );
4073 foreach ( $duration as $show => $value ) {
4074 if ( strcmp( $str, $value ) == 0 ) {
4075 return htmlspecialchars( trim( $show ) );
4076 }
4077 }
4078
4079 if ( wfIsInfinity( $str ) ) {
4080 foreach ( $duration as $show => $value ) {
4081 if ( wfIsInfinity( $value ) ) {
4082 return htmlspecialchars( trim( $show ) );
4083 }
4084 }
4085 }
4086
4087 // If all else fails, return a standard duration or timestamp description.
4088 $time = strtotime( $str, 0 );
4089 if ( $time === false ) { // Unknown format. Return it as-is in case.
4090 return $str;
4091 } elseif ( $time !== strtotime( $str, 1 ) ) { // It's a relative timestamp.
4092 // $time is relative to 0 so it's a duration length.
4093 return $this->formatDuration( $time );
4094 } else { // It's an absolute timestamp.
4095 if ( $time === 0 ) {
4096 // wfTimestamp() handles 0 as current time instead of epoch.
4097 return $this->timeanddate( '19700101000000' );
4098 } else {
4099 return $this->timeanddate( $time );
4100 }
4101 }
4102 }
4103
4104 /**
4105 * languages like Chinese need to be segmented in order for the diff
4106 * to be of any use
4107 *
4108 * @param string $text
4109 * @return string
4110 */
4111 public function segmentForDiff( $text ) {
4112 return $text;
4113 }
4114
4115 /**
4116 * and unsegment to show the result
4117 *
4118 * @param string $text
4119 * @return string
4120 */
4121 public function unsegmentForDiff( $text ) {
4122 return $text;
4123 }
4124
4125 /**
4126 * Return the LanguageConverter used in the Language
4127 *
4128 * @since 1.19
4129 * @return LanguageConverter
4130 */
4131 public function getConverter() {
4132 return $this->mConverter;
4133 }
4134
4135 /**
4136 * convert text to all supported variants
4137 *
4138 * @param string $text
4139 * @return array
4140 */
4141 public function autoConvertToAllVariants( $text ) {
4142 return $this->mConverter->autoConvertToAllVariants( $text );
4143 }
4144
4145 /**
4146 * convert text to different variants of a language.
4147 *
4148 * @param string $text
4149 * @return string
4150 */
4151 public function convert( $text ) {
4152 return $this->mConverter->convert( $text );
4153 }
4154
4155 /**
4156 * Convert a Title object to a string in the preferred variant
4157 *
4158 * @param Title $title
4159 * @return string
4160 */
4161 public function convertTitle( $title ) {
4162 return $this->mConverter->convertTitle( $title );
4163 }
4164
4165 /**
4166 * Convert a namespace index to a string in the preferred variant
4167 *
4168 * @param int $ns
4169 * @return string
4170 */
4171 public function convertNamespace( $ns ) {
4172 return $this->mConverter->convertNamespace( $ns );
4173 }
4174
4175 /**
4176 * Check if this is a language with variants
4177 *
4178 * @return bool
4179 */
4180 public function hasVariants() {
4181 return count( $this->getVariants() ) > 1;
4182 }
4183
4184 /**
4185 * Check if the language has the specific variant
4186 *
4187 * @since 1.19
4188 * @param string $variant
4189 * @return bool
4190 */
4191 public function hasVariant( $variant ) {
4192 return (bool)$this->mConverter->validateVariant( $variant );
4193 }
4194
4195 /**
4196 * Put custom tags (e.g. -{ }-) around math to prevent conversion
4197 *
4198 * @param string $text
4199 * @return string
4200 * @deprecated since 1.22 is no longer used
4201 */
4202 public function armourMath( $text ) {
4203 return $this->mConverter->armourMath( $text );
4204 }
4205
4206 /**
4207 * Perform output conversion on a string, and encode for safe HTML output.
4208 * @param string $text Text to be converted
4209 * @param bool $isTitle Whether this conversion is for the article title
4210 * @return string
4211 * @todo this should get integrated somewhere sane
4212 */
4213 public function convertHtml( $text, $isTitle = false ) {
4214 return htmlspecialchars( $this->convert( $text, $isTitle ) );
4215 }
4216
4217 /**
4218 * @param string $key
4219 * @return string
4220 */
4221 public function convertCategoryKey( $key ) {
4222 return $this->mConverter->convertCategoryKey( $key );
4223 }
4224
4225 /**
4226 * Get the list of variants supported by this language
4227 * see sample implementation in LanguageZh.php
4228 *
4229 * @return array An array of language codes
4230 */
4231 public function getVariants() {
4232 return $this->mConverter->getVariants();
4233 }
4234
4235 /**
4236 * @return string
4237 */
4238 public function getPreferredVariant() {
4239 return $this->mConverter->getPreferredVariant();
4240 }
4241
4242 /**
4243 * @return string
4244 */
4245 public function getDefaultVariant() {
4246 return $this->mConverter->getDefaultVariant();
4247 }
4248
4249 /**
4250 * @return string
4251 */
4252 public function getURLVariant() {
4253 return $this->mConverter->getURLVariant();
4254 }
4255
4256 /**
4257 * If a language supports multiple variants, it is
4258 * possible that non-existing link in one variant
4259 * actually exists in another variant. this function
4260 * tries to find it. See e.g. LanguageZh.php
4261 * The input parameters may be modified upon return
4262 *
4263 * @param string &$link The name of the link
4264 * @param Title &$nt The title object of the link
4265 * @param bool $ignoreOtherCond To disable other conditions when
4266 * we need to transclude a template or update a category's link
4267 */
4268 public function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
4269 $this->mConverter->findVariantLink( $link, $nt, $ignoreOtherCond );
4270 }
4271
4272 /**
4273 * returns language specific options used by User::getPageRenderHash()
4274 * for example, the preferred language variant
4275 *
4276 * @return string
4277 */
4278 function getExtraHashOptions() {
4279 return $this->mConverter->getExtraHashOptions();
4280 }
4281
4282 /**
4283 * For languages that support multiple variants, the title of an
4284 * article may be displayed differently in different variants. this
4285 * function returns the apporiate title defined in the body of the article.
4286 *
4287 * @return string
4288 */
4289 public function getParsedTitle() {
4290 return $this->mConverter->getParsedTitle();
4291 }
4292
4293 /**
4294 * Prepare external link text for conversion. When the text is
4295 * a URL, it shouldn't be converted, and it'll be wrapped in
4296 * the "raw" tag (-{R| }-) to prevent conversion.
4297 *
4298 * This function is called "markNoConversion" for historical
4299 * reasons.
4300 *
4301 * @param string $text Text to be used for external link
4302 * @param bool $noParse Wrap it without confirming it's a real URL first
4303 * @return string The tagged text
4304 */
4305 public function markNoConversion( $text, $noParse = false ) {
4306 // Excluding protocal-relative URLs may avoid many false positives.
4307 if ( $noParse || preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
4308 return $this->mConverter->markNoConversion( $text );
4309 } else {
4310 return $text;
4311 }
4312 }
4313
4314 /**
4315 * A regular expression to match legal word-trailing characters
4316 * which should be merged onto a link of the form [[foo]]bar.
4317 *
4318 * @return string
4319 */
4320 public function linkTrail() {
4321 return self::$dataCache->getItem( $this->mCode, 'linkTrail' );
4322 }
4323
4324 /**
4325 * A regular expression character set to match legal word-prefixing
4326 * characters which should be merged onto a link of the form foo[[bar]].
4327 *
4328 * @return string
4329 */
4330 public function linkPrefixCharset() {
4331 return self::$dataCache->getItem( $this->mCode, 'linkPrefixCharset' );
4332 }
4333
4334 /**
4335 * @deprecated since 1.24, will be removed in 1.25
4336 * @return Language
4337 */
4338 function getLangObj() {
4339 wfDeprecated( __METHOD__, '1.24' );
4340 return $this;
4341 }
4342
4343 /**
4344 * Get the "parent" language which has a converter to convert a "compatible" language
4345 * (in another variant) to this language (eg. zh for zh-cn, but not en for en-gb).
4346 *
4347 * @return Language|null
4348 * @since 1.22
4349 */
4350 public function getParentLanguage() {
4351 if ( $this->mParentLanguage !== false ) {
4352 return $this->mParentLanguage;
4353 }
4354
4355 $pieces = explode( '-', $this->getCode() );
4356 $code = $pieces[0];
4357 if ( !in_array( $code, LanguageConverter::$languagesWithVariants ) ) {
4358 $this->mParentLanguage = null;
4359 return null;
4360 }
4361 $lang = Language::factory( $code );
4362 if ( !$lang->hasVariant( $this->getCode() ) ) {
4363 $this->mParentLanguage = null;
4364 return null;
4365 }
4366
4367 $this->mParentLanguage = $lang;
4368 return $lang;
4369 }
4370
4371 /**
4372 * Get the RFC 3066 code for this language object
4373 *
4374 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
4375 * htmlspecialchars() or similar
4376 *
4377 * @return string
4378 */
4379 public function getCode() {
4380 return $this->mCode;
4381 }
4382
4383 /**
4384 * Get the code in Bcp47 format which we can use
4385 * inside of html lang="" tags.
4386 *
4387 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
4388 * htmlspecialchars() or similar.
4389 *
4390 * @since 1.19
4391 * @return string
4392 */
4393 public function getHtmlCode() {
4394 if ( is_null( $this->mHtmlCode ) ) {
4395 $this->mHtmlCode = wfBCP47( $this->getCode() );
4396 }
4397 return $this->mHtmlCode;
4398 }
4399
4400 /**
4401 * @param string $code
4402 */
4403 public function setCode( $code ) {
4404 $this->mCode = $code;
4405 // Ensure we don't leave incorrect cached data lying around
4406 $this->mHtmlCode = null;
4407 $this->mParentLanguage = false;
4408 }
4409
4410 /**
4411 * Get the name of a file for a certain language code
4412 * @param string $prefix Prepend this to the filename
4413 * @param string $code Language code
4414 * @param string $suffix Append this to the filename
4415 * @throws MWException
4416 * @return string $prefix . $mangledCode . $suffix
4417 */
4418 public static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
4419 if ( !self::isValidBuiltInCode( $code ) ) {
4420 throw new MWException( "Invalid language code \"$code\"" );
4421 }
4422
4423 return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
4424 }
4425
4426 /**
4427 * Get the language code from a file name. Inverse of getFileName()
4428 * @param string $filename $prefix . $languageCode . $suffix
4429 * @param string $prefix Prefix before the language code
4430 * @param string $suffix Suffix after the language code
4431 * @return string Language code, or false if $prefix or $suffix isn't found
4432 */
4433 public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
4434 $m = null;
4435 preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
4436 preg_quote( $suffix, '/' ) . '/', $filename, $m );
4437 if ( !count( $m ) ) {
4438 return false;
4439 }
4440 return str_replace( '_', '-', strtolower( $m[1] ) );
4441 }
4442
4443 /**
4444 * @param string $code
4445 * @return string
4446 */
4447 public static function getMessagesFileName( $code ) {
4448 global $IP;
4449 $file = self::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
4450 Hooks::run( 'Language::getMessagesFileName', array( $code, &$file ) );
4451 return $file;
4452 }
4453
4454 /**
4455 * @param string $code
4456 * @return string
4457 * @since 1.23
4458 */
4459 public static function getJsonMessagesFileName( $code ) {
4460 global $IP;
4461
4462 if ( !self::isValidBuiltInCode( $code ) ) {
4463 throw new MWException( "Invalid language code \"$code\"" );
4464 }
4465
4466 return "$IP/languages/i18n/$code.json";
4467 }
4468
4469 /**
4470 * @param string $code
4471 * @return string
4472 */
4473 public static function getClassFileName( $code ) {
4474 global $IP;
4475 return self::getFileName( "$IP/languages/classes/Language", $code, '.php' );
4476 }
4477
4478 /**
4479 * Get the first fallback for a given language.
4480 *
4481 * @param string $code
4482 *
4483 * @return bool|string
4484 */
4485 public static function getFallbackFor( $code ) {
4486 if ( $code === 'en' || !Language::isValidBuiltInCode( $code ) ) {
4487 return false;
4488 } else {
4489 $fallbacks = self::getFallbacksFor( $code );
4490 return $fallbacks[0];
4491 }
4492 }
4493
4494 /**
4495 * Get the ordered list of fallback languages.
4496 *
4497 * @since 1.19
4498 * @param string $code Language code
4499 * @return array Non-empty array, ending in "en"
4500 */
4501 public static function getFallbacksFor( $code ) {
4502 if ( $code === 'en' || !Language::isValidBuiltInCode( $code ) ) {
4503 return array();
4504 }
4505 // For unknown languages, fallbackSequence returns an empty array,
4506 // hardcode fallback to 'en' in that case.
4507 return self::getLocalisationCache()->getItem( $code, 'fallbackSequence' ) ?: array( 'en' );
4508 }
4509
4510 /**
4511 * Get the ordered list of fallback languages, ending with the fallback
4512 * language chain for the site language.
4513 *
4514 * @since 1.22
4515 * @param string $code Language code
4516 * @return array Array( fallbacks, site fallbacks )
4517 */
4518 public static function getFallbacksIncludingSiteLanguage( $code ) {
4519 global $wgLanguageCode;
4520
4521 // Usually, we will only store a tiny number of fallback chains, so we
4522 // keep them in static memory.
4523 $cacheKey = "{$code}-{$wgLanguageCode}";
4524
4525 if ( !array_key_exists( $cacheKey, self::$fallbackLanguageCache ) ) {
4526 $fallbacks = self::getFallbacksFor( $code );
4527
4528 // Append the site's fallback chain, including the site language itself
4529 $siteFallbacks = self::getFallbacksFor( $wgLanguageCode );
4530 array_unshift( $siteFallbacks, $wgLanguageCode );
4531
4532 // Eliminate any languages already included in the chain
4533 $siteFallbacks = array_diff( $siteFallbacks, $fallbacks );
4534
4535 self::$fallbackLanguageCache[$cacheKey] = array( $fallbacks, $siteFallbacks );
4536 }
4537 return self::$fallbackLanguageCache[$cacheKey];
4538 }
4539
4540 /**
4541 * Get all messages for a given language
4542 * WARNING: this may take a long time. If you just need all message *keys*
4543 * but need the *contents* of only a few messages, consider using getMessageKeysFor().
4544 *
4545 * @param string $code
4546 *
4547 * @return array
4548 */
4549 public static function getMessagesFor( $code ) {
4550 return self::getLocalisationCache()->getItem( $code, 'messages' );
4551 }
4552
4553 /**
4554 * Get a message for a given language
4555 *
4556 * @param string $key
4557 * @param string $code
4558 *
4559 * @return string
4560 */
4561 public static function getMessageFor( $key, $code ) {
4562 return self::getLocalisationCache()->getSubitem( $code, 'messages', $key );
4563 }
4564
4565 /**
4566 * Get all message keys for a given language. This is a faster alternative to
4567 * array_keys( Language::getMessagesFor( $code ) )
4568 *
4569 * @since 1.19
4570 * @param string $code Language code
4571 * @return array Array of message keys (strings)
4572 */
4573 public static function getMessageKeysFor( $code ) {
4574 return self::getLocalisationCache()->getSubItemList( $code, 'messages' );
4575 }
4576
4577 /**
4578 * @param string $talk
4579 * @return mixed
4580 */
4581 function fixVariableInNamespace( $talk ) {
4582 if ( strpos( $talk, '$1' ) === false ) {
4583 return $talk;
4584 }
4585
4586 global $wgMetaNamespace;
4587 $talk = str_replace( '$1', $wgMetaNamespace, $talk );
4588
4589 # Allow grammar transformations
4590 # Allowing full message-style parsing would make simple requests
4591 # such as action=raw much more expensive than they need to be.
4592 # This will hopefully cover most cases.
4593 $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
4594 array( &$this, 'replaceGrammarInNamespace' ), $talk );
4595 return str_replace( ' ', '_', $talk );
4596 }
4597
4598 /**
4599 * @param string $m
4600 * @return string
4601 */
4602 function replaceGrammarInNamespace( $m ) {
4603 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
4604 }
4605
4606 /**
4607 * @throws MWException
4608 * @return array
4609 */
4610 static function getCaseMaps() {
4611 static $wikiUpperChars, $wikiLowerChars;
4612 if ( isset( $wikiUpperChars ) ) {
4613 return array( $wikiUpperChars, $wikiLowerChars );
4614 }
4615
4616 $arr = wfGetPrecompiledData( 'Utf8Case.ser' );
4617 if ( $arr === false ) {
4618 throw new MWException(
4619 "Utf8Case.ser is missing, please run \"make\" in the serialized directory\n" );
4620 }
4621 $wikiUpperChars = $arr['wikiUpperChars'];
4622 $wikiLowerChars = $arr['wikiLowerChars'];
4623 return array( $wikiUpperChars, $wikiLowerChars );
4624 }
4625
4626 /**
4627 * Decode an expiry (block, protection, etc) which has come from the DB
4628 *
4629 * @param string $expiry Database expiry String
4630 * @param bool|int $format True to process using language functions, or TS_ constant
4631 * to return the expiry in a given timestamp
4632 * @param string $inifinity If $format is not true, use this string for infinite expiry
4633 * @return string
4634 * @since 1.18
4635 */
4636 public function formatExpiry( $expiry, $format = true, $infinity = 'infinity' ) {
4637 static $dbInfinity;
4638 if ( $dbInfinity === null ) {
4639 $dbInfinity = wfGetDB( DB_SLAVE )->getInfinity();
4640 }
4641
4642 if ( $expiry == '' || $expiry === 'infinity' || $expiry == $dbInfinity ) {
4643 return $format === true
4644 ? $this->getMessageFromDB( 'infiniteblock' )
4645 : $infinity;
4646 } else {
4647 return $format === true
4648 ? $this->timeanddate( $expiry, /* User preference timezone */ true )
4649 : wfTimestamp( $format, $expiry );
4650 }
4651 }
4652
4653 /**
4654 * @todo Document
4655 * @param int|float $seconds
4656 * @param array $format Optional
4657 * If $format['avoid'] === 'avoidseconds': don't mention seconds if $seconds >= 1 hour.
4658 * If $format['avoid'] === 'avoidminutes': don't mention seconds/minutes if $seconds > 48 hours.
4659 * If $format['noabbrevs'] is true: use 'seconds' and friends instead of 'seconds-abbrev'
4660 * and friends.
4661 * For backwards compatibility, $format may also be one of the strings 'avoidseconds'
4662 * or 'avoidminutes'.
4663 * @return string
4664 */
4665 function formatTimePeriod( $seconds, $format = array() ) {
4666 if ( !is_array( $format ) ) {
4667 $format = array( 'avoid' => $format ); // For backwards compatibility
4668 }
4669 if ( !isset( $format['avoid'] ) ) {
4670 $format['avoid'] = false;
4671 }
4672 if ( !isset( $format['noabbrevs'] ) ) {
4673 $format['noabbrevs'] = false;
4674 }
4675 $secondsMsg = wfMessage(
4676 $format['noabbrevs'] ? 'seconds' : 'seconds-abbrev' )->inLanguage( $this );
4677 $minutesMsg = wfMessage(
4678 $format['noabbrevs'] ? 'minutes' : 'minutes-abbrev' )->inLanguage( $this );
4679 $hoursMsg = wfMessage(
4680 $format['noabbrevs'] ? 'hours' : 'hours-abbrev' )->inLanguage( $this );
4681 $daysMsg = wfMessage(
4682 $format['noabbrevs'] ? 'days' : 'days-abbrev' )->inLanguage( $this );
4683
4684 if ( round( $seconds * 10 ) < 100 ) {
4685 $s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
4686 $s = $secondsMsg->params( $s )->text();
4687 } elseif ( round( $seconds ) < 60 ) {
4688 $s = $this->formatNum( round( $seconds ) );
4689 $s = $secondsMsg->params( $s )->text();
4690 } elseif ( round( $seconds ) < 3600 ) {
4691 $minutes = floor( $seconds / 60 );
4692 $secondsPart = round( fmod( $seconds, 60 ) );
4693 if ( $secondsPart == 60 ) {
4694 $secondsPart = 0;
4695 $minutes++;
4696 }
4697 $s = $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4698 $s .= ' ';
4699 $s .= $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4700 } elseif ( round( $seconds ) <= 2 * 86400 ) {
4701 $hours = floor( $seconds / 3600 );
4702 $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
4703 $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
4704 if ( $secondsPart == 60 ) {
4705 $secondsPart = 0;
4706 $minutes++;
4707 }
4708 if ( $minutes == 60 ) {
4709 $minutes = 0;
4710 $hours++;
4711 }
4712 $s = $hoursMsg->params( $this->formatNum( $hours ) )->text();
4713 $s .= ' ';
4714 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4715 if ( !in_array( $format['avoid'], array( 'avoidseconds', 'avoidminutes' ) ) ) {
4716 $s .= ' ' . $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4717 }
4718 } else {
4719 $days = floor( $seconds / 86400 );
4720 if ( $format['avoid'] === 'avoidminutes' ) {
4721 $hours = round( ( $seconds - $days * 86400 ) / 3600 );
4722 if ( $hours == 24 ) {
4723 $hours = 0;
4724 $days++;
4725 }
4726 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4727 $s .= ' ';
4728 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4729 } elseif ( $format['avoid'] === 'avoidseconds' ) {
4730 $hours = floor( ( $seconds - $days * 86400 ) / 3600 );
4731 $minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
4732 if ( $minutes == 60 ) {
4733 $minutes = 0;
4734 $hours++;
4735 }
4736 if ( $hours == 24 ) {
4737 $hours = 0;
4738 $days++;
4739 }
4740 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4741 $s .= ' ';
4742 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4743 $s .= ' ';
4744 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4745 } else {
4746 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4747 $s .= ' ';
4748 $s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
4749 }
4750 }
4751 return $s;
4752 }
4753
4754 /**
4755 * Format a bitrate for output, using an appropriate
4756 * unit (bps, kbps, Mbps, Gbps, Tbps, Pbps, Ebps, Zbps or Ybps) according to
4757 * the magnitude in question.
4758 *
4759 * This use base 1000. For base 1024 use formatSize(), for another base
4760 * see formatComputingNumbers().
4761 *
4762 * @param int $bps
4763 * @return string
4764 */
4765 function formatBitrate( $bps ) {
4766 return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" );
4767 }
4768
4769 /**
4770 * @param int $size Size of the unit
4771 * @param int $boundary Size boundary (1000, or 1024 in most cases)
4772 * @param string $messageKey Message key to be uesd
4773 * @return string
4774 */
4775 function formatComputingNumbers( $size, $boundary, $messageKey ) {
4776 if ( $size <= 0 ) {
4777 return str_replace( '$1', $this->formatNum( $size ),
4778 $this->getMessageFromDB( str_replace( '$1', '', $messageKey ) )
4779 );
4780 }
4781 $sizes = array( '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zeta', 'yotta' );
4782 $index = 0;
4783
4784 $maxIndex = count( $sizes ) - 1;
4785 while ( $size >= $boundary && $index < $maxIndex ) {
4786 $index++;
4787 $size /= $boundary;
4788 }
4789
4790 // For small sizes no decimal places necessary
4791 $round = 0;
4792 if ( $index > 1 ) {
4793 // For MB and bigger two decimal places are smarter
4794 $round = 2;
4795 }
4796 $msg = str_replace( '$1', $sizes[$index], $messageKey );
4797
4798 $size = round( $size, $round );
4799 $text = $this->getMessageFromDB( $msg );
4800 return str_replace( '$1', $this->formatNum( $size ), $text );
4801 }
4802
4803 /**
4804 * Format a size in bytes for output, using an appropriate
4805 * unit (B, KB, MB, GB, TB, PB, EB, ZB or YB) according to the magnitude in question
4806 *
4807 * This method use base 1024. For base 1000 use formatBitrate(), for
4808 * another base see formatComputingNumbers()
4809 *
4810 * @param int $size Size to format
4811 * @return string Plain text (not HTML)
4812 */
4813 function formatSize( $size ) {
4814 return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" );
4815 }
4816
4817 /**
4818 * Make a list item, used by various special pages
4819 *
4820 * @param string $page Page link
4821 * @param string $details HTML safe text between brackets
4822 * @param bool $oppositedm Add the direction mark opposite to your
4823 * language, to display text properly
4824 * @return HTML escaped string
4825 */
4826 function specialList( $page, $details, $oppositedm = true ) {
4827 if ( !$details ) {
4828 return $page;
4829 }
4830
4831 $dirmark = ( $oppositedm ? $this->getDirMark( true ) : '' ) . $this->getDirMark();
4832 return
4833 $page .
4834 $dirmark .
4835 $this->msg( 'word-separator' )->escaped() .
4836 $this->msg( 'parentheses' )->rawParams( $details )->escaped();
4837 }
4838
4839 /**
4840 * Generate (prev x| next x) (20|50|100...) type links for paging
4841 *
4842 * @param Title $title Title object to link
4843 * @param int $offset
4844 * @param int $limit
4845 * @param array $query Optional URL query parameter string
4846 * @param bool $atend Optional param for specified if this is the last page
4847 * @return string
4848 */
4849 public function viewPrevNext( Title $title, $offset, $limit,
4850 array $query = array(), $atend = false
4851 ) {
4852 // @todo FIXME: Why on earth this needs one message for the text and another one for tooltip?
4853
4854 # Make 'previous' link
4855 $prev = wfMessage( 'prevn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4856 if ( $offset > 0 ) {
4857 $plink = $this->numLink( $title, max( $offset - $limit, 0 ), $limit,
4858 $query, $prev, 'prevn-title', 'mw-prevlink' );
4859 } else {
4860 $plink = htmlspecialchars( $prev );
4861 }
4862
4863 # Make 'next' link
4864 $next = wfMessage( 'nextn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4865 if ( $atend ) {
4866 $nlink = htmlspecialchars( $next );
4867 } else {
4868 $nlink = $this->numLink( $title, $offset + $limit, $limit,
4869 $query, $next, 'nextn-title', 'mw-nextlink' );
4870 }
4871
4872 # Make links to set number of items per page
4873 $numLinks = array();
4874 foreach ( array( 20, 50, 100, 250, 500 ) as $num ) {
4875 $numLinks[] = $this->numLink( $title, $offset, $num,
4876 $query, $this->formatNum( $num ), 'shown-title', 'mw-numlink' );
4877 }
4878
4879 return wfMessage( 'viewprevnext' )->inLanguage( $this )->title( $title
4880 )->rawParams( $plink, $nlink, $this->pipeList( $numLinks ) )->escaped();
4881 }
4882
4883 /**
4884 * Helper function for viewPrevNext() that generates links
4885 *
4886 * @param Title $title Title object to link
4887 * @param int $offset
4888 * @param int $limit
4889 * @param array $query Extra query parameters
4890 * @param string $link Text to use for the link; will be escaped
4891 * @param string $tooltipMsg Name of the message to use as tooltip
4892 * @param string $class Value of the "class" attribute of the link
4893 * @return string HTML fragment
4894 */
4895 private function numLink( Title $title, $offset, $limit, array $query, $link,
4896 $tooltipMsg, $class
4897 ) {
4898 $query = array( 'limit' => $limit, 'offset' => $offset ) + $query;
4899 $tooltip = wfMessage( $tooltipMsg )->inLanguage( $this )->title( $title )
4900 ->numParams( $limit )->text();
4901
4902 return Html::element( 'a', array( 'href' => $title->getLocalURL( $query ),
4903 'title' => $tooltip, 'class' => $class ), $link );
4904 }
4905
4906 /**
4907 * Get the conversion rule title, if any.
4908 *
4909 * @return string
4910 */
4911 public function getConvRuleTitle() {
4912 return $this->mConverter->getConvRuleTitle();
4913 }
4914
4915 /**
4916 * Get the compiled plural rules for the language
4917 * @since 1.20
4918 * @return array Associative array with plural form, and plural rule as key-value pairs
4919 */
4920 public function getCompiledPluralRules() {
4921 $pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'compiledPluralRules' );
4922 $fallbacks = Language::getFallbacksFor( $this->mCode );
4923 if ( !$pluralRules ) {
4924 foreach ( $fallbacks as $fallbackCode ) {
4925 $pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
4926 if ( $pluralRules ) {
4927 break;
4928 }
4929 }
4930 }
4931 return $pluralRules;
4932 }
4933
4934 /**
4935 * Get the plural rules for the language
4936 * @since 1.20
4937 * @return array Associative array with plural form number and plural rule as key-value pairs
4938 */
4939 public function getPluralRules() {
4940 $pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRules' );
4941 $fallbacks = Language::getFallbacksFor( $this->mCode );
4942 if ( !$pluralRules ) {
4943 foreach ( $fallbacks as $fallbackCode ) {
4944 $pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRules' );
4945 if ( $pluralRules ) {
4946 break;
4947 }
4948 }
4949 }
4950 return $pluralRules;
4951 }
4952
4953 /**
4954 * Get the plural rule types for the language
4955 * @since 1.22
4956 * @return array Associative array with plural form number and plural rule type as key-value pairs
4957 */
4958 public function getPluralRuleTypes() {
4959 $pluralRuleTypes = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRuleTypes' );
4960 $fallbacks = Language::getFallbacksFor( $this->mCode );
4961 if ( !$pluralRuleTypes ) {
4962 foreach ( $fallbacks as $fallbackCode ) {
4963 $pluralRuleTypes = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRuleTypes' );
4964 if ( $pluralRuleTypes ) {
4965 break;
4966 }
4967 }
4968 }
4969 return $pluralRuleTypes;
4970 }
4971
4972 /**
4973 * Find the index number of the plural rule appropriate for the given number
4974 * @param int $number
4975 * @return int The index number of the plural rule
4976 */
4977 public function getPluralRuleIndexNumber( $number ) {
4978 $pluralRules = $this->getCompiledPluralRules();
4979 $form = Evaluator::evaluateCompiled( $number, $pluralRules );
4980 return $form;
4981 }
4982
4983 /**
4984 * Find the plural rule type appropriate for the given number
4985 * For example, if the language is set to Arabic, getPluralType(5) should
4986 * return 'few'.
4987 * @since 1.22
4988 * @param int $number
4989 * @return string The name of the plural rule type, e.g. one, two, few, many
4990 */
4991 public function getPluralRuleType( $number ) {
4992 $index = $this->getPluralRuleIndexNumber( $number );
4993 $pluralRuleTypes = $this->getPluralRuleTypes();
4994 if ( isset( $pluralRuleTypes[$index] ) ) {
4995 return $pluralRuleTypes[$index];
4996 } else {
4997 return 'other';
4998 }
4999 }
5000 }