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