MediaWiki REL1_33
Language.php
Go to the documentation of this file.
1<?php
29use CLDRPluralRuleParser\Evaluator;
30use Wikimedia\Assert\Assert;
31
36class Language {
41 const AS_AUTONYMS = null;
42
47 const ALL = 'all';
48
54 const SUPPORTED = 'mwfile';
55
60
61 public $mVariants, $mCode, $mLoaded = false;
62 public $mMagicExtensions = [];
63 private $mHtmlCode = null, $mParentLanguage = false;
64
65 public $dateFormatStrings = [];
67
69 protected $namespaceNames;
71
75 public $transformData = [];
76
80 public static $dataCache;
81
82 public static $mLangObjCache = [];
83
89
95
96 public static $mWeekdayMsgs = [
97 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
98 'friday', 'saturday'
99 ];
100
101 public static $mWeekdayAbbrevMsgs = [
102 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
103 ];
104
105 public static $mMonthMsgs = [
106 'january', 'february', 'march', 'april', 'may_long', 'june',
107 'july', 'august', 'september', 'october', 'november',
108 'december'
109 ];
110 public static $mMonthGenMsgs = [
111 'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
112 'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
113 'december-gen'
114 ];
115 public static $mMonthAbbrevMsgs = [
116 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
117 'sep', 'oct', 'nov', 'dec'
118 ];
119
121 'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
122 'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
123 'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
124 'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
125 ];
126
128 'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
129 'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
130 'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
131 'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
132 'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
133 ];
134
136 'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
137 'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
138 'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
139 'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
140 'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
141 ];
142
143 public static $mHijriCalendarMonthMsgs = [
144 'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
145 'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
146 'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
147 'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
148 ];
149
154 public static $durationIntervals = [
155 'millennia' => 31556952000,
156 'centuries' => 3155695200,
157 'decades' => 315569520,
158 'years' => 31556952, // 86400 * ( 365 + ( 24 * 3 + 25 ) / 400 )
159 'weeks' => 604800,
160 'days' => 86400,
161 'hours' => 3600,
162 'minutes' => 60,
163 'seconds' => 1,
164 ];
165
172 private static $fallbackLanguageCache = [];
173
179
184 private static $languageNameCache;
185
189 private static $lre = "\u{202A}"; // U+202A LEFT-TO-RIGHT EMBEDDING
190 private static $rle = "\u{202B}"; // U+202B RIGHT-TO-LEFT EMBEDDING
191 private static $pdf = "\u{202C}"; // U+202C POP DIRECTIONAL FORMATTING
192
204 // @codeCoverageIgnoreStart
205 // phpcs:ignore Generic.Files.LineLength
206 private static $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';
207 // @codeCoverageIgnoreEnd
208
215 static function factory( $code ) {
217
218 if ( isset( $wgDummyLanguageCodes[$code] ) ) {
220 }
221
222 // get the language object to process
223 $langObj = self::$mLangObjCache[$code] ?? self::newFromCode( $code );
224
225 // merge the language object in to get it up front in the cache
226 self::$mLangObjCache = array_merge( [ $code => $langObj ], self::$mLangObjCache );
227 // get rid of the oldest ones in case we have an overflow
228 self::$mLangObjCache = array_slice( self::$mLangObjCache, 0, $wgLangObjCacheSize, true );
229
230 return $langObj;
231 }
232
240 protected static function newFromCode( $code, $fallback = false ) {
241 if ( !self::isValidCode( $code ) ) {
242 throw new MWException( "Invalid language code \"$code\"" );
243 }
244
245 if ( !self::isValidBuiltInCode( $code ) ) {
246 // It's not possible to customise this code with class files, so
247 // just return a Language object. This is to support uselang= hacks.
248 $lang = new Language;
249 $lang->mCode = $code;
250 return $lang;
251 }
252
253 // Check if there is a language class for the code
254 $class = self::classFromCode( $code, $fallback );
255 // LanguageCode does not inherit Language
256 if ( class_exists( $class ) && is_a( $class, 'Language', true ) ) {
257 $lang = new $class;
258 return $lang;
259 }
260
261 // Keep trying the fallback list until we find an existing class
262 $fallbacks = self::getFallbacksFor( $code );
263 foreach ( $fallbacks as $fallbackCode ) {
264 if ( !self::isValidBuiltInCode( $fallbackCode ) ) {
265 throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" );
266 }
267
268 $class = self::classFromCode( $fallbackCode );
269 if ( class_exists( $class ) ) {
270 $lang = new $class;
271 $lang->mCode = $code;
272 return $lang;
273 }
274 }
275
276 throw new MWException( "Invalid fallback sequence for language '$code'" );
277 }
278
284 public static function clearCaches() {
285 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
286 throw new MWException( __METHOD__ . ' must not be used outside tests' );
287 }
288 self::$dataCache = null;
289 // Reinitialize $dataCache, since it's expected to always be available
290 self::getLocalisationCache();
291 self::$mLangObjCache = [];
292 self::$fallbackLanguageCache = [];
293 self::$grammarTransformations = null;
294 self::$languageNameCache = null;
295 }
296
305 public static function isSupportedLanguage( $code ) {
306 if ( !self::isValidBuiltInCode( $code ) ) {
307 return false;
308 }
309
310 if ( $code === 'qqq' ) {
311 return false;
312 }
313
314 return is_readable( self::getMessagesFileName( $code ) ) ||
315 is_readable( self::getJsonMessagesFileName( $code ) );
316 }
317
333 public static function isWellFormedLanguageTag( $code, $lenient = false ) {
334 $alpha = '[a-z]';
335 $digit = '[0-9]';
336 $alphanum = '[a-z0-9]';
337 $x = 'x'; # private use singleton
338 $singleton = '[a-wy-z]'; # other singleton
339 $s = $lenient ? '[-_]' : '-';
340
341 $language = "$alpha{2,8}|$alpha{2,3}$s$alpha{3}";
342 $script = "$alpha{4}"; # ISO 15924
343 $region = "(?:$alpha{2}|$digit{3})"; # ISO 3166-1 alpha-2 or UN M.49
344 $variant = "(?:$alphanum{5,8}|$digit$alphanum{3})";
345 $extension = "$singleton(?:$s$alphanum{2,8})+";
346 $privateUse = "$x(?:$s$alphanum{1,8})+";
347
348 # Define certain grandfathered codes, since otherwise the regex is pretty useless.
349 # Since these are limited, this is safe even later changes to the registry --
350 # the only oddity is that it might change the type of the tag, and thus
351 # the results from the capturing groups.
352 # https://www.iana.org/assignments/language-subtag-registry
353
354 $grandfathered = "en{$s}GB{$s}oed"
355 . "|i{$s}(?:ami|bnn|default|enochian|hak|klingon|lux|mingo|navajo|pwn|tao|tay|tsu)"
356 . "|no{$s}(?:bok|nyn)"
357 . "|sgn{$s}(?:BE{$s}(?:fr|nl)|CH{$s}de)"
358 . "|zh{$s}min{$s}nan";
359
360 $variantList = "$variant(?:$s$variant)*";
361 $extensionList = "$extension(?:$s$extension)*";
362
363 $langtag = "(?:($language)"
364 . "(?:$s$script)?"
365 . "(?:$s$region)?"
366 . "(?:$s$variantList)?"
367 . "(?:$s$extensionList)?"
368 . "(?:$s$privateUse)?)";
369
370 # The final breakdown, with capturing groups for each of these components
371 # The variants, extensions, grandfathered, and private-use may have interior '-'
372
373 $root = "^(?:$langtag|$privateUse|$grandfathered)$";
374
375 return (bool)preg_match( "/$root/", strtolower( $code ) );
376 }
377
387 public static function isValidCode( $code ) {
388 static $cache = [];
389 Assert::parameterType( 'string', $code, '$code' );
390 if ( !isset( $cache[$code] ) ) {
391 // People think language codes are html safe, so enforce it.
392 // Ideally we should only allow a-zA-Z0-9-
393 // but, .+ and other chars are often used for {{int:}} hacks
394 // see bugs T39564, T39587, T38938
395 $cache[$code] =
396 // Protect against path traversal
397 strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
399 }
400 return $cache[$code];
401 }
402
412 public static function isValidBuiltInCode( $code ) {
413 Assert::parameterType( 'string', $code, '$code' );
414
415 return (bool)preg_match( '/^[a-z0-9-]{2,}$/', $code );
416 }
417
426 public static function isKnownLanguageTag( $tag ) {
427 // Quick escape for invalid input to avoid exceptions down the line
428 // when code tries to process tags which are not valid at all.
429 if ( !self::isValidBuiltInCode( $tag ) ) {
430 return false;
431 }
432
433 if ( isset( MediaWiki\Languages\Data\Names::$names[$tag] )
434 || self::fetchLanguageName( $tag, $tag ) !== ''
435 ) {
436 return true;
437 }
438
439 return false;
440 }
441
447 public static function getLocalisationCache() {
448 if ( is_null( self::$dataCache ) ) {
450 $class = $wgLocalisationCacheConf['class'];
451 self::$dataCache = new $class( $wgLocalisationCacheConf );
452 }
453 return self::$dataCache;
454 }
455
456 function __construct() {
457 $this->mConverter = new FakeConverter( $this );
458 // Set the code to the name of the descendant
459 if ( static::class === 'Language' ) {
460 $this->mCode = 'en';
461 } else {
462 $this->mCode = str_replace( '_', '-', strtolower( substr( static::class, 8 ) ) );
463 }
464 self::getLocalisationCache();
465 }
466
470 function __destruct() {
471 foreach ( $this as $name => $value ) {
472 unset( $this->$name );
473 }
474 }
475
480 function initContLang() {
481 }
482
487 public function getFallbackLanguages() {
488 return self::getFallbacksFor( $this->mCode );
489 }
490
495 public function getBookstoreList() {
496 return self::$dataCache->getItem( $this->mCode, 'bookstoreList' );
497 }
498
505 public function getNamespaces() {
506 if ( is_null( $this->namespaceNames ) ) {
508
509 $validNamespaces = MWNamespace::getCanonicalNamespaces();
510
511 $this->namespaceNames = $wgExtraNamespaces +
512 self::$dataCache->getItem( $this->mCode, 'namespaceNames' );
513 $this->namespaceNames += $validNamespaces;
514
515 $this->namespaceNames[NS_PROJECT] = $wgMetaNamespace;
516 if ( $wgMetaNamespaceTalk ) {
517 $this->namespaceNames[NS_PROJECT_TALK] = $wgMetaNamespaceTalk;
518 } else {
519 $talk = $this->namespaceNames[NS_PROJECT_TALK];
520 $this->namespaceNames[NS_PROJECT_TALK] =
521 $this->fixVariableInNamespace( $talk );
522 }
523
524 # Sometimes a language will be localised but not actually exist on this wiki.
525 foreach ( $this->namespaceNames as $key => $text ) {
526 if ( !isset( $validNamespaces[$key] ) ) {
527 unset( $this->namespaceNames[$key] );
528 }
529 }
530
531 # The above mixing may leave namespaces out of canonical order.
532 # Re-order by namespace ID number...
533 ksort( $this->namespaceNames );
534
535 Hooks::run( 'LanguageGetNamespaces', [ &$this->namespaceNames ] );
536 }
537
539 }
540
545 public function setNamespaces( array $namespaces ) {
546 $this->namespaceNames = $namespaces;
547 $this->mNamespaceIds = null;
548 }
549
553 public function resetNamespaces() {
554 $this->namespaceNames = null;
555 $this->mNamespaceIds = null;
556 $this->namespaceAliases = null;
557 }
558
565 public function getFormattedNamespaces() {
566 $ns = $this->getNamespaces();
567 foreach ( $ns as $k => $v ) {
568 $ns[$k] = strtr( $v, '_', ' ' );
569 }
570 return $ns;
571 }
572
584 public function getNsText( $index ) {
585 $ns = $this->getNamespaces();
586 return $ns[$index] ?? false;
587 }
588
602 public function getFormattedNsText( $index ) {
603 $ns = $this->getNsText( $index );
604 return strtr( $ns, '_', ' ' );
605 }
606
615 public function getGenderNsText( $index, $gender ) {
617
619 (array)self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
620
621 return $ns[$index][$gender] ?? $this->getNsText( $index );
622 }
623
630 public function needsGenderDistinction() {
632 if ( count( $wgExtraGenderNamespaces ) > 0 ) {
633 // $wgExtraGenderNamespaces overrides everything
634 return true;
635 } elseif ( isset( $wgExtraNamespaces[NS_USER] ) && isset( $wgExtraNamespaces[NS_USER_TALK] ) ) {
637 // $wgExtraNamespaces overrides any gender aliases specified in i18n files
638 return false;
639 } else {
640 // Check what is in i18n files
641 $aliases = self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
642 return count( $aliases ) > 0;
643 }
644 }
645
654 function getLocalNsIndex( $text ) {
655 $lctext = $this->lc( $text );
656 $ids = $this->getNamespaceIds();
657 return $ids[$lctext] ?? false;
658 }
659
663 public function getNamespaceAliases() {
664 if ( is_null( $this->namespaceAliases ) ) {
665 $aliases = self::$dataCache->getItem( $this->mCode, 'namespaceAliases' );
666 if ( !$aliases ) {
667 $aliases = [];
668 } else {
669 foreach ( $aliases as $name => $index ) {
670 if ( $index === NS_PROJECT_TALK ) {
671 unset( $aliases[$name] );
673 $aliases[$name] = $index;
674 }
675 }
676 }
677
679 $genders = $wgExtraGenderNamespaces +
680 (array)self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
681 foreach ( $genders as $index => $forms ) {
682 foreach ( $forms as $alias ) {
683 $aliases[$alias] = $index;
684 }
685 }
686
687 # Also add converted namespace names as aliases, to avoid confusion.
688 $convertedNames = [];
689 foreach ( $this->getVariants() as $variant ) {
690 if ( $variant === $this->mCode ) {
691 continue;
692 }
693 foreach ( $this->getNamespaces() as $ns => $_ ) {
694 $convertedNames[$this->getConverter()->convertNamespace( $ns, $variant )] = $ns;
695 }
696 }
697
698 $this->namespaceAliases = $aliases + $convertedNames;
699
700 # Filter out aliases to namespaces that don't exist, e.g. from extensions
701 # that aren't loaded here but are included in the l10n cache.
702 # (array_intersect preserves keys from its first argument)
703 $this->namespaceAliases = array_intersect(
704 $this->namespaceAliases,
705 array_keys( $this->getNamespaces() )
706 );
707 }
708
710 }
711
715 public function getNamespaceIds() {
716 if ( is_null( $this->mNamespaceIds ) ) {
717 global $wgNamespaceAliases;
718 # Put namespace names and aliases into a hashtable.
719 # If this is too slow, then we should arrange it so that it is done
720 # before caching. The catch is that at pre-cache time, the above
721 # class-specific fixup hasn't been done.
722 $this->mNamespaceIds = [];
723 foreach ( $this->getNamespaces() as $index => $name ) {
724 $this->mNamespaceIds[$this->lc( $name )] = $index;
725 }
726 foreach ( $this->getNamespaceAliases() as $name => $index ) {
727 $this->mNamespaceIds[$this->lc( $name )] = $index;
728 }
729 if ( $wgNamespaceAliases ) {
730 foreach ( $wgNamespaceAliases as $name => $index ) {
731 $this->mNamespaceIds[$this->lc( $name )] = $index;
732 }
733 }
734 }
735 return $this->mNamespaceIds;
736 }
737
745 public function getNsIndex( $text ) {
746 $lctext = $this->lc( $text );
747 $ns = MWNamespace::getCanonicalIndex( $lctext );
748 if ( $ns !== null ) {
749 return $ns;
750 }
751 $ids = $this->getNamespaceIds();
752 return $ids[$lctext] ?? false;
753 }
754
762 public function getVariantname( $code, $usemsg = true ) {
763 $msg = "variantname-$code";
764 if ( $usemsg && wfMessage( $msg )->exists() ) {
765 return $this->getMessageFromDB( $msg );
766 }
767 $name = self::fetchLanguageName( $code );
768 if ( $name ) {
769 return $name; # if it's defined as a language name, show that
770 } else {
771 # otherwise, output the language code
772 return $code;
773 }
774 }
775
779 public function getDatePreferences() {
780 return self::$dataCache->getItem( $this->mCode, 'datePreferences' );
781 }
782
786 function getDateFormats() {
787 return self::$dataCache->getItem( $this->mCode, 'dateFormats' );
788 }
789
793 public function getDefaultDateFormat() {
794 $df = self::$dataCache->getItem( $this->mCode, 'defaultDateFormat' );
795 if ( $df === 'dmy or mdy' ) {
796 global $wgAmericanDates;
797 return $wgAmericanDates ? 'mdy' : 'dmy';
798 } else {
799 return $df;
800 }
801 }
802
806 public function getDatePreferenceMigrationMap() {
807 return self::$dataCache->getItem( $this->mCode, 'datePreferenceMigrationMap' );
808 }
809
813 public function getExtraUserToggles() {
814 return (array)self::$dataCache->getItem( $this->mCode, 'extraUserToggles' );
815 }
816
821 function getUserToggle( $tog ) {
822 return $this->getMessageFromDB( "tog-$tog" );
823 }
824
836 public static function fetchLanguageNames( $inLanguage = self::AS_AUTONYMS, $include = 'mw' ) {
837 $cacheKey = $inLanguage === self::AS_AUTONYMS ? 'null' : $inLanguage;
838 $cacheKey .= ":$include";
839 if ( self::$languageNameCache === null ) {
840 self::$languageNameCache = new HashBagOStuff( [ 'maxKeys' => 20 ] );
841 }
842
843 $ret = self::$languageNameCache->get( $cacheKey );
844 if ( !$ret ) {
845 $ret = self::fetchLanguageNamesUncached( $inLanguage, $include );
846 self::$languageNameCache->set( $cacheKey, $ret );
847 }
848 return $ret;
849 }
850
861 private static function fetchLanguageNamesUncached(
862 $inLanguage = self::AS_AUTONYMS,
863 $include = 'mw'
864 ) {
865 global $wgExtraLanguageNames, $wgUsePigLatinVariant;
866
867 // If passed an invalid language code to use, fallback to en
868 if ( $inLanguage !== self::AS_AUTONYMS && !self::isValidCode( $inLanguage ) ) {
869 $inLanguage = 'en';
870 }
871
872 $names = [];
873
874 if ( $inLanguage ) {
875 # TODO: also include when $inLanguage is null, when this code is more efficient
876 Hooks::run( 'LanguageGetTranslatedLanguageNames', [ &$names, $inLanguage ] );
877 }
878
879 $mwNames = $wgExtraLanguageNames + MediaWiki\Languages\Data\Names::$names;
880 if ( $wgUsePigLatinVariant ) {
881 // Pig Latin (for variant development)
882 $mwNames['en-x-piglatin'] = 'Igpay Atinlay';
883 }
884
885 foreach ( $mwNames as $mwCode => $mwName ) {
886 # - Prefer own MediaWiki native name when not using the hook
887 # - For other names just add if not added through the hook
888 if ( $mwCode === $inLanguage || !isset( $names[$mwCode] ) ) {
889 $names[$mwCode] = $mwName;
890 }
891 }
892
893 if ( $include === self::ALL ) {
894 ksort( $names );
895 return $names;
896 }
897
898 $returnMw = [];
899 $coreCodes = array_keys( $mwNames );
900 foreach ( $coreCodes as $coreCode ) {
901 $returnMw[$coreCode] = $names[$coreCode];
902 }
903
904 if ( $include === self::SUPPORTED ) {
905 $namesMwFile = [];
906 # We do this using a foreach over the codes instead of a directory
907 # loop so that messages files in extensions will work correctly.
908 foreach ( $returnMw as $code => $value ) {
909 if ( is_readable( self::getMessagesFileName( $code ) )
910 || is_readable( self::getJsonMessagesFileName( $code ) )
911 ) {
912 $namesMwFile[$code] = $names[$code];
913 }
914 }
915
916 ksort( $namesMwFile );
917 return $namesMwFile;
918 }
919
920 ksort( $returnMw );
921 # 'mw' option; default if it's not one of the other two options (all/mwfile)
922 return $returnMw;
923 }
924
933 public static function fetchLanguageName(
934 $code,
935 $inLanguage = self::AS_AUTONYMS,
936 $include = self::ALL
937 ) {
938 $code = strtolower( $code );
939 $array = self::fetchLanguageNames( $inLanguage, $include );
940 return !array_key_exists( $code, $array ) ? '' : $array[$code];
941 }
942
949 public function getMessageFromDB( $msg ) {
950 return $this->msg( $msg )->text();
951 }
952
959 protected function msg( $msg ) {
960 return wfMessage( $msg )->inLanguage( $this );
961 }
962
967 public function getMonthName( $key ) {
968 return $this->getMessageFromDB( self::$mMonthMsgs[$key - 1] );
969 }
970
974 public function getMonthNamesArray() {
975 $monthNames = [ '' ];
976 for ( $i = 1; $i < 13; $i++ ) {
977 $monthNames[] = $this->getMonthName( $i );
978 }
979 return $monthNames;
980 }
981
986 public function getMonthNameGen( $key ) {
987 return $this->getMessageFromDB( self::$mMonthGenMsgs[$key - 1] );
988 }
989
994 public function getMonthAbbreviation( $key ) {
995 return $this->getMessageFromDB( self::$mMonthAbbrevMsgs[$key - 1] );
996 }
997
1001 public function getMonthAbbreviationsArray() {
1002 $monthNames = [ '' ];
1003 for ( $i = 1; $i < 13; $i++ ) {
1004 $monthNames[] = $this->getMonthAbbreviation( $i );
1005 }
1006 return $monthNames;
1007 }
1008
1013 public function getWeekdayName( $key ) {
1014 return $this->getMessageFromDB( self::$mWeekdayMsgs[$key - 1] );
1015 }
1016
1021 function getWeekdayAbbreviation( $key ) {
1022 return $this->getMessageFromDB( self::$mWeekdayAbbrevMsgs[$key - 1] );
1023 }
1024
1030 return $this->getMessageFromDB( self::$mIranianCalendarMonthMsgs[$key - 1] );
1031 }
1032
1038 return $this->getMessageFromDB( self::$mHebrewCalendarMonthMsgs[$key - 1] );
1039 }
1040
1046 return $this->getMessageFromDB( self::$mHebrewCalendarMonthGenMsgs[$key - 1] );
1047 }
1048
1053 function getHijriCalendarMonthName( $key ) {
1054 return $this->getMessageFromDB( self::$mHijriCalendarMonthMsgs[$key - 1] );
1055 }
1056
1065 private static function dateTimeObjFormat( &$dateTimeObj, $ts, $zone, $code ) {
1066 if ( !$dateTimeObj ) {
1067 $dateTimeObj = DateTime::createFromFormat(
1068 'YmdHis', $ts, $zone ?: new DateTimeZone( 'UTC' )
1069 );
1070 }
1071 return $dateTimeObj->format( $code );
1072 }
1073
1143 public function sprintfDate( $format, $ts, DateTimeZone $zone = null, &$ttl = 'unused' ) {
1144 $s = '';
1145 $raw = false;
1146 $roman = false;
1147 $hebrewNum = false;
1148 $dateTimeObj = false;
1149 $rawToggle = false;
1150 $iranian = false;
1151 $hebrew = false;
1152 $hijri = false;
1153 $thai = false;
1154 $minguo = false;
1155 $tenno = false;
1156
1157 $usedSecond = false;
1158 $usedMinute = false;
1159 $usedHour = false;
1160 $usedAMPM = false;
1161 $usedDay = false;
1162 $usedWeek = false;
1163 $usedMonth = false;
1164 $usedYear = false;
1165 $usedISOYear = false;
1166 $usedIsLeapYear = false;
1167
1168 $usedHebrewMonth = false;
1169 $usedIranianMonth = false;
1170 $usedHijriMonth = false;
1171 $usedHebrewYear = false;
1172 $usedIranianYear = false;
1173 $usedHijriYear = false;
1174 $usedTennoYear = false;
1175
1176 if ( strlen( $ts ) !== 14 ) {
1177 throw new MWException( __METHOD__ . ": The timestamp $ts should have 14 characters" );
1178 }
1179
1180 if ( !ctype_digit( $ts ) ) {
1181 throw new MWException( __METHOD__ . ": The timestamp $ts should be a number" );
1182 }
1183
1184 $formatLength = strlen( $format );
1185 for ( $p = 0; $p < $formatLength; $p++ ) {
1186 $num = false;
1187 $code = $format[$p];
1188 if ( $code == 'x' && $p < $formatLength - 1 ) {
1189 $code .= $format[++$p];
1190 }
1191
1192 if ( ( $code === 'xi'
1193 || $code === 'xj'
1194 || $code === 'xk'
1195 || $code === 'xm'
1196 || $code === 'xo'
1197 || $code === 'xt' )
1198 && $p < $formatLength - 1 ) {
1199 $code .= $format[++$p];
1200 }
1201
1202 switch ( $code ) {
1203 case 'xx':
1204 $s .= 'x';
1205 break;
1206 case 'xn':
1207 $raw = true;
1208 break;
1209 case 'xN':
1210 $rawToggle = !$rawToggle;
1211 break;
1212 case 'xr':
1213 $roman = true;
1214 break;
1215 case 'xh':
1216 $hebrewNum = true;
1217 break;
1218 case 'xg':
1219 $usedMonth = true;
1220 $s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) );
1221 break;
1222 case 'xjx':
1223 $usedHebrewMonth = true;
1224 if ( !$hebrew ) {
1225 $hebrew = self::tsToHebrew( $ts );
1226 }
1227 $s .= $this->getHebrewCalendarMonthNameGen( $hebrew[1] );
1228 break;
1229 case 'd':
1230 $usedDay = true;
1231 $num = substr( $ts, 6, 2 );
1232 break;
1233 case 'D':
1234 $usedDay = true;
1235 $s .= $this->getWeekdayAbbreviation(
1236 self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1
1237 );
1238 break;
1239 case 'j':
1240 $usedDay = true;
1241 $num = intval( substr( $ts, 6, 2 ) );
1242 break;
1243 case 'xij':
1244 $usedDay = true;
1245 if ( !$iranian ) {
1246 $iranian = self::tsToIranian( $ts );
1247 }
1248 $num = $iranian[2];
1249 break;
1250 case 'xmj':
1251 $usedDay = true;
1252 if ( !$hijri ) {
1253 $hijri = self::tsToHijri( $ts );
1254 }
1255 $num = $hijri[2];
1256 break;
1257 case 'xjj':
1258 $usedDay = true;
1259 if ( !$hebrew ) {
1260 $hebrew = self::tsToHebrew( $ts );
1261 }
1262 $num = $hebrew[2];
1263 break;
1264 case 'l':
1265 $usedDay = true;
1266 $s .= $this->getWeekdayName(
1267 self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1
1268 );
1269 break;
1270 case 'F':
1271 $usedMonth = true;
1272 $s .= $this->getMonthName( substr( $ts, 4, 2 ) );
1273 break;
1274 case 'xiF':
1275 $usedIranianMonth = true;
1276 if ( !$iranian ) {
1277 $iranian = self::tsToIranian( $ts );
1278 }
1279 $s .= $this->getIranianCalendarMonthName( $iranian[1] );
1280 break;
1281 case 'xmF':
1282 $usedHijriMonth = true;
1283 if ( !$hijri ) {
1284 $hijri = self::tsToHijri( $ts );
1285 }
1286 $s .= $this->getHijriCalendarMonthName( $hijri[1] );
1287 break;
1288 case 'xjF':
1289 $usedHebrewMonth = true;
1290 if ( !$hebrew ) {
1291 $hebrew = self::tsToHebrew( $ts );
1292 }
1293 $s .= $this->getHebrewCalendarMonthName( $hebrew[1] );
1294 break;
1295 case 'm':
1296 $usedMonth = true;
1297 $num = substr( $ts, 4, 2 );
1298 break;
1299 case 'M':
1300 $usedMonth = true;
1301 $s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) );
1302 break;
1303 case 'n':
1304 $usedMonth = true;
1305 $num = intval( substr( $ts, 4, 2 ) );
1306 break;
1307 case 'xin':
1308 $usedIranianMonth = true;
1309 if ( !$iranian ) {
1310 $iranian = self::tsToIranian( $ts );
1311 }
1312 $num = $iranian[1];
1313 break;
1314 case 'xmn':
1315 $usedHijriMonth = true;
1316 if ( !$hijri ) {
1317 $hijri = self::tsToHijri( $ts );
1318 }
1319 $num = $hijri[1];
1320 break;
1321 case 'xjn':
1322 $usedHebrewMonth = true;
1323 if ( !$hebrew ) {
1324 $hebrew = self::tsToHebrew( $ts );
1325 }
1326 $num = $hebrew[1];
1327 break;
1328 case 'xjt':
1329 $usedHebrewMonth = true;
1330 if ( !$hebrew ) {
1331 $hebrew = self::tsToHebrew( $ts );
1332 }
1333 $num = $hebrew[3];
1334 break;
1335 case 'Y':
1336 $usedYear = true;
1337 $num = substr( $ts, 0, 4 );
1338 break;
1339 case 'xiY':
1340 $usedIranianYear = true;
1341 if ( !$iranian ) {
1342 $iranian = self::tsToIranian( $ts );
1343 }
1344 $num = $iranian[0];
1345 break;
1346 case 'xmY':
1347 $usedHijriYear = true;
1348 if ( !$hijri ) {
1349 $hijri = self::tsToHijri( $ts );
1350 }
1351 $num = $hijri[0];
1352 break;
1353 case 'xjY':
1354 $usedHebrewYear = true;
1355 if ( !$hebrew ) {
1356 $hebrew = self::tsToHebrew( $ts );
1357 }
1358 $num = $hebrew[0];
1359 break;
1360 case 'xkY':
1361 $usedYear = true;
1362 if ( !$thai ) {
1363 $thai = self::tsToYear( $ts, 'thai' );
1364 }
1365 $num = $thai[0];
1366 break;
1367 case 'xoY':
1368 $usedYear = true;
1369 if ( !$minguo ) {
1370 $minguo = self::tsToYear( $ts, 'minguo' );
1371 }
1372 $num = $minguo[0];
1373 break;
1374 case 'xtY':
1375 $usedTennoYear = true;
1376 if ( !$tenno ) {
1377 $tenno = self::tsToYear( $ts, 'tenno' );
1378 }
1379 $num = $tenno[0];
1380 break;
1381 case 'y':
1382 $usedYear = true;
1383 $num = substr( $ts, 2, 2 );
1384 break;
1385 case 'xiy':
1386 $usedIranianYear = true;
1387 if ( !$iranian ) {
1388 $iranian = self::tsToIranian( $ts );
1389 }
1390 $num = substr( $iranian[0], -2 );
1391 break;
1392 case 'xit':
1393 $usedIranianYear = true;
1394 if ( !$iranian ) {
1395 $iranian = self::tsToIranian( $ts );
1396 }
1397 $num = self::$IRANIAN_DAYS[$iranian[1] - 1];
1398 break;
1399 case 'xiz':
1400 $usedIranianYear = true;
1401 if ( !$iranian ) {
1402 $iranian = self::tsToIranian( $ts );
1403 }
1404 $num = $iranian[3];
1405 break;
1406 case 'a':
1407 $usedAMPM = true;
1408 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm';
1409 break;
1410 case 'A':
1411 $usedAMPM = true;
1412 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'AM' : 'PM';
1413 break;
1414 case 'g':
1415 $usedHour = true;
1416 $h = substr( $ts, 8, 2 );
1417 $num = $h % 12 ?: 12;
1418 break;
1419 case 'G':
1420 $usedHour = true;
1421 $num = intval( substr( $ts, 8, 2 ) );
1422 break;
1423 case 'h':
1424 $usedHour = true;
1425 $h = substr( $ts, 8, 2 );
1426 $num = sprintf( '%02d', $h % 12 ?: 12 );
1427 break;
1428 case 'H':
1429 $usedHour = true;
1430 $num = substr( $ts, 8, 2 );
1431 break;
1432 case 'i':
1433 $usedMinute = true;
1434 $num = substr( $ts, 10, 2 );
1435 break;
1436 case 's':
1437 $usedSecond = true;
1438 $num = substr( $ts, 12, 2 );
1439 break;
1440 case 'c':
1441 case 'r':
1442 $usedSecond = true;
1443 // fall through
1444 case 'e':
1445 case 'O':
1446 case 'P':
1447 case 'T':
1448 $s .= self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1449 break;
1450 case 'w':
1451 case 'N':
1452 case 'z':
1453 $usedDay = true;
1454 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1455 break;
1456 case 'W':
1457 $usedWeek = true;
1458 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1459 break;
1460 case 't':
1461 $usedMonth = true;
1462 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1463 break;
1464 case 'L':
1465 $usedIsLeapYear = true;
1466 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1467 break;
1468 case 'o':
1469 $usedISOYear = true;
1470 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1471 break;
1472 case 'U':
1473 $usedSecond = true;
1474 // fall through
1475 case 'I':
1476 case 'Z':
1477 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1478 break;
1479 case '\\':
1480 # Backslash escaping
1481 if ( $p < $formatLength - 1 ) {
1482 $s .= $format[++$p];
1483 } else {
1484 $s .= '\\';
1485 }
1486 break;
1487 case '"':
1488 # Quoted literal
1489 if ( $p < $formatLength - 1 ) {
1490 $endQuote = strpos( $format, '"', $p + 1 );
1491 if ( $endQuote === false ) {
1492 # No terminating quote, assume literal "
1493 $s .= '"';
1494 } else {
1495 $s .= substr( $format, $p + 1, $endQuote - $p - 1 );
1496 $p = $endQuote;
1497 }
1498 } else {
1499 # Quote at end of string, assume literal "
1500 $s .= '"';
1501 }
1502 break;
1503 default:
1504 $s .= $format[$p];
1505 }
1506 if ( $num !== false ) {
1507 if ( $rawToggle || $raw ) {
1508 $s .= $num;
1509 $raw = false;
1510 } elseif ( $roman ) {
1511 $s .= self::romanNumeral( $num );
1512 $roman = false;
1513 } elseif ( $hebrewNum ) {
1514 $s .= self::hebrewNumeral( $num );
1515 $hebrewNum = false;
1516 } else {
1517 $s .= $this->formatNum( $num, true );
1518 }
1519 }
1520 }
1521
1522 if ( $ttl === 'unused' ) {
1523 // No need to calculate the TTL, the caller wont use it anyway.
1524 } elseif ( $usedSecond ) {
1525 $ttl = 1;
1526 } elseif ( $usedMinute ) {
1527 $ttl = 60 - substr( $ts, 12, 2 );
1528 } elseif ( $usedHour ) {
1529 $ttl = 3600 - substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1530 } elseif ( $usedAMPM ) {
1531 $ttl = 43200 - ( substr( $ts, 8, 2 ) % 12 ) * 3600 -
1532 substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1533 } elseif (
1534 $usedDay ||
1535 $usedHebrewMonth ||
1536 $usedIranianMonth ||
1537 $usedHijriMonth ||
1538 $usedHebrewYear ||
1539 $usedIranianYear ||
1540 $usedHijriYear ||
1541 $usedTennoYear
1542 ) {
1543 // @todo Someone who understands the non-Gregorian calendars
1544 // should write proper logic for them so that they don't need purged every day.
1545 $ttl = 86400 - substr( $ts, 8, 2 ) * 3600 -
1546 substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1547 } else {
1548 $possibleTtls = [];
1549 $timeRemainingInDay = 86400 - substr( $ts, 8, 2 ) * 3600 -
1550 substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1551 if ( $usedWeek ) {
1552 $possibleTtls[] =
1553 ( 7 - self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400 +
1554 $timeRemainingInDay;
1555 } elseif ( $usedISOYear ) {
1556 // December 28th falls on the last ISO week of the year, every year.
1557 // The last ISO week of a year can be 52 or 53.
1558 $lastWeekOfISOYear = DateTime::createFromFormat(
1559 'Ymd',
1560 substr( $ts, 0, 4 ) . '1228',
1561 $zone ?: new DateTimeZone( 'UTC' )
1562 )->format( 'W' );
1563 $currentISOWeek = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'W' );
1564 $weeksRemaining = $lastWeekOfISOYear - $currentISOWeek;
1565 $timeRemainingInWeek =
1566 ( 7 - self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400
1567 + $timeRemainingInDay;
1568 $possibleTtls[] = $weeksRemaining * 604800 + $timeRemainingInWeek;
1569 }
1570
1571 if ( $usedMonth ) {
1572 $possibleTtls[] =
1573 ( self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 't' ) -
1574 substr( $ts, 6, 2 ) ) * 86400
1575 + $timeRemainingInDay;
1576 } elseif ( $usedYear ) {
1577 $possibleTtls[] =
1578 ( self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) + 364 -
1579 self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400
1580 + $timeRemainingInDay;
1581 } elseif ( $usedIsLeapYear ) {
1582 $year = substr( $ts, 0, 4 );
1583 $timeRemainingInYear =
1584 ( self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) + 364 -
1585 self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400
1586 + $timeRemainingInDay;
1587 $mod = $year % 4;
1588 if ( $mod || ( !( $year % 100 ) && $year % 400 ) ) {
1589 // this isn't a leap year. see when the next one starts
1590 $nextCandidate = $year - $mod + 4;
1591 if ( $nextCandidate % 100 || !( $nextCandidate % 400 ) ) {
1592 $possibleTtls[] = ( $nextCandidate - $year - 1 ) * 365 * 86400 +
1593 $timeRemainingInYear;
1594 } else {
1595 $possibleTtls[] = ( $nextCandidate - $year + 3 ) * 365 * 86400 +
1596 $timeRemainingInYear;
1597 }
1598 } else {
1599 // this is a leap year, so the next year isn't
1600 $possibleTtls[] = $timeRemainingInYear;
1601 }
1602 }
1603
1604 if ( $possibleTtls ) {
1605 $ttl = min( $possibleTtls );
1606 }
1607 }
1608
1609 return $s;
1610 }
1611
1612 private static $GREG_DAYS = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
1613 private static $IRANIAN_DAYS = [ 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 ];
1614
1627 private static function tsToIranian( $ts ) {
1628 $gy = substr( $ts, 0, 4 ) - 1600;
1629 $gm = substr( $ts, 4, 2 ) - 1;
1630 $gd = substr( $ts, 6, 2 ) - 1;
1631
1632 # Days passed from the beginning (including leap years)
1633 $gDayNo = 365 * $gy
1634 + floor( ( $gy + 3 ) / 4 )
1635 - floor( ( $gy + 99 ) / 100 )
1636 + floor( ( $gy + 399 ) / 400 );
1637
1638 // Add days of the past months of this year
1639 for ( $i = 0; $i < $gm; $i++ ) {
1640 $gDayNo += self::$GREG_DAYS[$i];
1641 }
1642
1643 // Leap years
1644 if ( $gm > 1 && ( ( $gy % 4 === 0 && $gy % 100 !== 0 || ( $gy % 400 == 0 ) ) ) ) {
1645 $gDayNo++;
1646 }
1647
1648 // Days passed in current month
1649 $gDayNo += (int)$gd;
1650
1651 $jDayNo = $gDayNo - 79;
1652
1653 $jNp = floor( $jDayNo / 12053 );
1654 $jDayNo %= 12053;
1655
1656 $jy = 979 + 33 * $jNp + 4 * floor( $jDayNo / 1461 );
1657 $jDayNo %= 1461;
1658
1659 if ( $jDayNo >= 366 ) {
1660 $jy += floor( ( $jDayNo - 1 ) / 365 );
1661 $jDayNo = floor( ( $jDayNo - 1 ) % 365 );
1662 }
1663
1664 $jz = $jDayNo;
1665
1666 for ( $i = 0; $i < 11 && $jDayNo >= self::$IRANIAN_DAYS[$i]; $i++ ) {
1667 $jDayNo -= self::$IRANIAN_DAYS[$i];
1668 }
1669
1670 $jm = $i + 1;
1671 $jd = $jDayNo + 1;
1672
1673 return [ $jy, $jm, $jd, $jz ];
1674 }
1675
1687 private static function tsToHijri( $ts ) {
1688 $year = substr( $ts, 0, 4 );
1689 $month = substr( $ts, 4, 2 );
1690 $day = substr( $ts, 6, 2 );
1691
1692 $zyr = $year;
1693 $zd = $day;
1694 $zm = $month;
1695 $zy = $zyr;
1696
1697 if (
1698 ( $zy > 1582 ) || ( ( $zy == 1582 ) && ( $zm > 10 ) ) ||
1699 ( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) )
1700 ) {
1701 $zjd = (int)( ( 1461 * ( $zy + 4800 + (int)( ( $zm - 14 ) / 12 ) ) ) / 4 ) +
1702 (int)( ( 367 * ( $zm - 2 - 12 * ( (int)( ( $zm - 14 ) / 12 ) ) ) ) / 12 ) -
1703 (int)( ( 3 * (int)( ( ( $zy + 4900 + (int)( ( $zm - 14 ) / 12 ) ) / 100 ) ) ) / 4 ) +
1704 $zd - 32075;
1705 } else {
1706 $zjd = 367 * $zy - (int)( ( 7 * ( $zy + 5001 + (int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) +
1707 (int)( ( 275 * $zm ) / 9 ) + $zd + 1729777;
1708 }
1709
1710 $zl = $zjd - 1948440 + 10632;
1711 $zn = (int)( ( $zl - 1 ) / 10631 );
1712 $zl = $zl - 10631 * $zn + 354;
1713 $zj = ( (int)( ( 10985 - $zl ) / 5316 ) ) * ( (int)( ( 50 * $zl ) / 17719 ) ) +
1714 ( (int)( $zl / 5670 ) ) * ( (int)( ( 43 * $zl ) / 15238 ) );
1715 $zl = $zl - ( (int)( ( 30 - $zj ) / 15 ) ) * ( (int)( ( 17719 * $zj ) / 50 ) ) -
1716 ( (int)( $zj / 16 ) ) * ( (int)( ( 15238 * $zj ) / 43 ) ) + 29;
1717 $zm = (int)( ( 24 * $zl ) / 709 );
1718 $zd = $zl - (int)( ( 709 * $zm ) / 24 );
1719 $zy = 30 * $zn + $zj - 30;
1720
1721 return [ $zy, $zm, $zd ];
1722 }
1723
1739 private static function tsToHebrew( $ts ) {
1740 # Parse date
1741 $year = substr( $ts, 0, 4 );
1742 $month = substr( $ts, 4, 2 );
1743 $day = substr( $ts, 6, 2 );
1744
1745 # Calculate Hebrew year
1746 $hebrewYear = $year + 3760;
1747
1748 # Month number when September = 1, August = 12
1749 $month += 4;
1750 if ( $month > 12 ) {
1751 # Next year
1752 $month -= 12;
1753 $year++;
1754 $hebrewYear++;
1755 }
1756
1757 # Calculate day of year from 1 September
1758 $dayOfYear = $day;
1759 for ( $i = 1; $i < $month; $i++ ) {
1760 if ( $i == 6 ) {
1761 # February
1762 $dayOfYear += 28;
1763 # Check if the year is leap
1764 if ( $year % 400 == 0 || ( $year % 4 == 0 && $year % 100 > 0 ) ) {
1765 $dayOfYear++;
1766 }
1767 } elseif ( $i == 8 || $i == 10 || $i == 1 || $i == 3 ) {
1768 $dayOfYear += 30;
1769 } else {
1770 $dayOfYear += 31;
1771 }
1772 }
1773
1774 # Calculate the start of the Hebrew year
1775 $start = self::hebrewYearStart( $hebrewYear );
1776
1777 # Calculate next year's start
1778 if ( $dayOfYear <= $start ) {
1779 # Day is before the start of the year - it is the previous year
1780 # Next year's start
1781 $nextStart = $start;
1782 # Previous year
1783 $year--;
1784 $hebrewYear--;
1785 # Add days since previous year's 1 September
1786 $dayOfYear += 365;
1787 if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1788 # Leap year
1789 $dayOfYear++;
1790 }
1791 # Start of the new (previous) year
1792 $start = self::hebrewYearStart( $hebrewYear );
1793 } else {
1794 # Next year's start
1795 $nextStart = self::hebrewYearStart( $hebrewYear + 1 );
1796 }
1797
1798 # Calculate Hebrew day of year
1799 $hebrewDayOfYear = $dayOfYear - $start;
1800
1801 # Difference between year's days
1802 $diff = $nextStart - $start;
1803 # Add 12 (or 13 for leap years) days to ignore the difference between
1804 # Hebrew and Gregorian year (353 at least vs. 365/6) - now the
1805 # difference is only about the year type
1806 if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1807 $diff += 13;
1808 } else {
1809 $diff += 12;
1810 }
1811
1812 # Check the year pattern, and is leap year
1813 # 0 means an incomplete year, 1 means a regular year, 2 means a complete year
1814 # This is mod 30, to work on both leap years (which add 30 days of Adar I)
1815 # and non-leap years
1816 $yearPattern = $diff % 30;
1817 # Check if leap year
1818 $isLeap = $diff >= 30;
1819
1820 # Calculate day in the month from number of day in the Hebrew year
1821 # Don't check Adar - if the day is not in Adar, we will stop before;
1822 # if it is in Adar, we will use it to check if it is Adar I or Adar II
1823 $hebrewDay = $hebrewDayOfYear;
1824 $hebrewMonth = 1;
1825 $days = 0;
1826 while ( $hebrewMonth <= 12 ) {
1827 # Calculate days in this month
1828 if ( $isLeap && $hebrewMonth == 6 ) {
1829 # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1830 $days = 30;
1831 if ( $hebrewDay <= $days ) {
1832 # Day in Adar I
1833 $hebrewMonth = 13;
1834 } else {
1835 # Subtract the days of Adar I
1836 $hebrewDay -= $days;
1837 # Try Adar II
1838 $days = 29;
1839 if ( $hebrewDay <= $days ) {
1840 # Day in Adar II
1841 $hebrewMonth = 14;
1842 }
1843 }
1844 } elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
1845 # Cheshvan in a complete year (otherwise as the rule below)
1846 $days = 30;
1847 } elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) {
1848 # Kislev in an incomplete year (otherwise as the rule below)
1849 $days = 29;
1850 } else {
1851 # Odd months have 30 days, even have 29
1852 $days = 30 - ( $hebrewMonth - 1 ) % 2;
1853 }
1854 if ( $hebrewDay <= $days ) {
1855 # In the current month
1856 break;
1857 } else {
1858 # Subtract the days of the current month
1859 $hebrewDay -= $days;
1860 # Try in the next month
1861 $hebrewMonth++;
1862 }
1863 }
1864
1865 return [ $hebrewYear, $hebrewMonth, $hebrewDay, $days ];
1866 }
1867
1877 private static function hebrewYearStart( $year ) {
1878 $a = intval( ( 12 * ( $year - 1 ) + 17 ) % 19 );
1879 $b = intval( ( $year - 1 ) % 4 );
1880 $m = 32.044093161144 + 1.5542417966212 * $a + $b / 4.0 - 0.0031777940220923 * ( $year - 1 );
1881 if ( $m < 0 ) {
1882 $m--;
1883 }
1884 $Mar = intval( $m );
1885 if ( $m < 0 ) {
1886 $m++;
1887 }
1888 $m -= $Mar;
1889
1890 $c = intval( ( $Mar + 3 * ( $year - 1 ) + 5 * $b + 5 ) % 7 );
1891 if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1892 $Mar++;
1893 } elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1894 $Mar += 2;
1895 } elseif ( $c == 2 || $c == 4 || $c == 6 ) {
1896 $Mar++;
1897 }
1898
1899 $Mar += intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1900 return $Mar;
1901 }
1902
1915 private static function tsToYear( $ts, $cName ) {
1916 $gy = substr( $ts, 0, 4 );
1917 $gm = substr( $ts, 4, 2 );
1918 $gd = substr( $ts, 6, 2 );
1919
1920 if ( !strcmp( $cName, 'thai' ) ) {
1921 # Thai solar dates
1922 # Add 543 years to the Gregorian calendar
1923 # Months and days are identical
1924 $gy_offset = $gy + 543;
1925 # fix for dates between 1912 and 1941
1926 # https://en.wikipedia.org/?oldid=836596673#New_year
1927 if ( $gy >= 1912 && $gy <= 1940 ) {
1928 if ( $gm <= 3 ) {
1929 $gy_offset--;
1930 }
1931 $gm = ( $gm - 3 ) % 12;
1932 }
1933 } elseif ( ( !strcmp( $cName, 'minguo' ) ) || !strcmp( $cName, 'juche' ) ) {
1934 # Minguo dates
1935 # Deduct 1911 years from the Gregorian calendar
1936 # Months and days are identical
1937 $gy_offset = $gy - 1911;
1938 } elseif ( !strcmp( $cName, 'tenno' ) ) {
1939 # Nengō dates up to Meiji period
1940 # Deduct years from the Gregorian calendar
1941 # depending on the nengo periods
1942 # Months and days are identical
1943 if ( ( $gy < 1912 )
1944 || ( ( $gy == 1912 ) && ( $gm < 7 ) )
1945 || ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd < 31 ) )
1946 ) {
1947 # Meiji period
1948 $gy_gannen = $gy - 1868 + 1;
1949 $gy_offset = $gy_gannen;
1950 if ( $gy_gannen == 1 ) {
1951 $gy_offset = '元';
1952 }
1953 $gy_offset = '明治' . $gy_offset;
1954 } elseif (
1955 ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd == 31 ) ) ||
1956 ( ( $gy == 1912 ) && ( $gm >= 8 ) ) ||
1957 ( ( $gy > 1912 ) && ( $gy < 1926 ) ) ||
1958 ( ( $gy == 1926 ) && ( $gm < 12 ) ) ||
1959 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd < 26 ) )
1960 ) {
1961 # Taishō period
1962 $gy_gannen = $gy - 1912 + 1;
1963 $gy_offset = $gy_gannen;
1964 if ( $gy_gannen == 1 ) {
1965 $gy_offset = '元';
1966 }
1967 $gy_offset = '大正' . $gy_offset;
1968 } elseif (
1969 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd >= 26 ) ) ||
1970 ( ( $gy > 1926 ) && ( $gy < 1989 ) ) ||
1971 ( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd < 8 ) )
1972 ) {
1973 # Shōwa period
1974 $gy_gannen = $gy - 1926 + 1;
1975 $gy_offset = $gy_gannen;
1976 if ( $gy_gannen == 1 ) {
1977 $gy_offset = '元';
1978 }
1979 $gy_offset = '昭和' . $gy_offset;
1980 } elseif (
1981 ( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd >= 8 ) ) ||
1982 ( ( $gy > 1989 ) && ( $gy < 2019 ) ) ||
1983 ( ( $gy == 2019 ) && ( $gm < 5 ) )
1984 ) {
1985 # Heisei period
1986 $gy_gannen = $gy - 1989 + 1;
1987 $gy_offset = $gy_gannen;
1988 if ( $gy_gannen == 1 ) {
1989 $gy_offset = '元';
1990 }
1991 $gy_offset = '平成' . $gy_offset;
1992 } else {
1993 # Reiwa period
1994 $gy_gannen = $gy - 2019 + 1;
1995 $gy_offset = $gy_gannen;
1996 if ( $gy_gannen == 1 ) {
1997 $gy_offset = '元';
1998 }
1999 $gy_offset = '令和' . $gy_offset;
2000 }
2001 } else {
2002 $gy_offset = $gy;
2003 }
2004
2005 return [ $gy_offset, $gm, $gd ];
2006 }
2007
2021 private static function strongDirFromContent( $text = '' ) {
2022 if ( !preg_match( self::$strongDirRegex, $text, $matches ) ) {
2023 return null;
2024 }
2025 if ( $matches[1] === '' ) {
2026 return 'rtl';
2027 }
2028 return 'ltr';
2029 }
2030
2038 static function romanNumeral( $num ) {
2039 static $table = [
2040 [ '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ],
2041 [ '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ],
2042 [ '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ],
2043 [ '', 'M', 'MM', 'MMM', 'MMMM', 'MMMMM', 'MMMMMM', 'MMMMMMM',
2044 'MMMMMMMM', 'MMMMMMMMM', 'MMMMMMMMMM' ]
2045 ];
2046
2047 $num = intval( $num );
2048 if ( $num > 10000 || $num <= 0 ) {
2049 return $num;
2050 }
2051
2052 $s = '';
2053 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
2054 if ( $num >= $pow10 ) {
2055 $s .= $table[$i][(int)floor( $num / $pow10 )];
2056 }
2057 $num = $num % $pow10;
2058 }
2059 return $s;
2060 }
2061
2069 static function hebrewNumeral( $num ) {
2070 static $table = [
2071 [ '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ],
2072 [ '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ],
2073 [ '',
2074 [ 'ק' ],
2075 [ 'ר' ],
2076 [ 'ש' ],
2077 [ 'ת' ],
2078 [ 'ת', 'ק' ],
2079 [ 'ת', 'ר' ],
2080 [ 'ת', 'ש' ],
2081 [ 'ת', 'ת' ],
2082 [ 'ת', 'ת', 'ק' ],
2083 [ 'ת', 'ת', 'ר' ],
2084 ],
2085 [ '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ]
2086 ];
2087
2088 $num = intval( $num );
2089 if ( $num > 9999 || $num <= 0 ) {
2090 return $num;
2091 }
2092
2093 // Round thousands have special notations
2094 if ( $num === 1000 ) {
2095 return "א' אלף";
2096 } elseif ( $num % 1000 === 0 ) {
2097 return $table[0][$num / 1000] . "' אלפים";
2098 }
2099
2100 $letters = [];
2101
2102 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
2103 if ( $num >= $pow10 ) {
2104 if ( $num === 15 || $num === 16 ) {
2105 $letters[] = $table[0][9];
2106 $letters[] = $table[0][$num - 9];
2107 $num = 0;
2108 } else {
2109 $letters = array_merge(
2110 $letters,
2111 (array)$table[$i][intval( $num / $pow10 )]
2112 );
2113
2114 if ( $pow10 === 1000 ) {
2115 $letters[] = "'";
2116 }
2117 }
2118 }
2119
2120 $num = $num % $pow10;
2121 }
2122
2123 $preTransformLength = count( $letters );
2124 if ( $preTransformLength === 1 ) {
2125 // Add geresh (single quote) to one-letter numbers
2126 $letters[] = "'";
2127 } else {
2128 $lastIndex = $preTransformLength - 1;
2129 $letters[$lastIndex] = str_replace(
2130 [ 'כ', 'מ', 'נ', 'פ', 'צ' ],
2131 [ 'ך', 'ם', 'ן', 'ף', 'ץ' ],
2132 $letters[$lastIndex]
2133 );
2134
2135 // Add gershayim (double quote) to multiple-letter numbers,
2136 // but exclude numbers with only one letter after the thousands
2137 // (1001-1009, 1020, 1030, 2001-2009, etc.)
2138 if ( $letters[1] === "'" && $preTransformLength === 3 ) {
2139 $letters[] = "'";
2140 } else {
2141 array_splice( $letters, -1, 0, '"' );
2142 }
2143 }
2144
2145 return implode( $letters );
2146 }
2147
2156 public function userAdjust( $ts, $tz = false ) {
2157 global $wgUser, $wgLocalTZoffset;
2158
2159 if ( $tz === false ) {
2160 $tz = $wgUser->getOption( 'timecorrection' );
2161 }
2162
2163 $data = explode( '|', $tz, 3 );
2164
2165 if ( $data[0] == 'ZoneInfo' ) {
2166 try {
2167 $userTZ = new DateTimeZone( $data[2] );
2168 $date = new DateTime( $ts, new DateTimeZone( 'UTC' ) );
2169 $date->setTimezone( $userTZ );
2170 return $date->format( 'YmdHis' );
2171 } catch ( Exception $e ) {
2172 // Unrecognized timezone, default to 'Offset' with the stored offset.
2173 $data[0] = 'Offset';
2174 }
2175 }
2176
2177 if ( $data[0] == 'System' || $tz == '' ) {
2178 # Global offset in minutes.
2179 $minDiff = $wgLocalTZoffset;
2180 } elseif ( $data[0] == 'Offset' ) {
2181 $minDiff = intval( $data[1] );
2182 } else {
2183 $data = explode( ':', $tz );
2184 if ( count( $data ) == 2 ) {
2185 $data[0] = intval( $data[0] );
2186 $data[1] = intval( $data[1] );
2187 $minDiff = abs( $data[0] ) * 60 + $data[1];
2188 if ( $data[0] < 0 ) {
2189 $minDiff = -$minDiff;
2190 }
2191 } else {
2192 $minDiff = intval( $data[0] ) * 60;
2193 }
2194 }
2195
2196 # No difference ? Return time unchanged
2197 if ( $minDiff == 0 ) {
2198 return $ts;
2199 }
2200
2201 Wikimedia\suppressWarnings(); // E_STRICT system time bitching
2202 # Generate an adjusted date; take advantage of the fact that mktime
2203 # will normalize out-of-range values so we don't have to split $minDiff
2204 # into hours and minutes.
2205 $t = mktime( (
2206 (int)substr( $ts, 8, 2 ) ), # Hours
2207 (int)substr( $ts, 10, 2 ) + $minDiff, # Minutes
2208 (int)substr( $ts, 12, 2 ), # Seconds
2209 (int)substr( $ts, 4, 2 ), # Month
2210 (int)substr( $ts, 6, 2 ), # Day
2211 (int)substr( $ts, 0, 4 ) ); # Year
2212
2213 $date = date( 'YmdHis', $t );
2214 Wikimedia\restoreWarnings();
2215
2216 return $date;
2217 }
2218
2234 function dateFormat( $usePrefs = true ) {
2235 global $wgUser;
2236
2237 if ( is_bool( $usePrefs ) ) {
2238 if ( $usePrefs ) {
2239 $datePreference = $wgUser->getDatePreference();
2240 } else {
2241 $datePreference = (string)User::getDefaultOption( 'date' );
2242 }
2243 } else {
2244 $datePreference = (string)$usePrefs;
2245 }
2246
2247 // return int
2248 if ( $datePreference == '' ) {
2249 return 'default';
2250 }
2251
2252 return $datePreference;
2253 }
2254
2265 function getDateFormatString( $type, $pref ) {
2266 $wasDefault = false;
2267 if ( $pref == 'default' ) {
2268 $wasDefault = true;
2269 $pref = $this->getDefaultDateFormat();
2270 }
2271
2272 if ( !isset( $this->dateFormatStrings[$type][$pref] ) ) {
2273 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2274
2275 if ( $type === 'pretty' && $df === null ) {
2276 $df = $this->getDateFormatString( 'date', $pref );
2277 }
2278
2279 if ( !$wasDefault && $df === null ) {
2280 $pref = $this->getDefaultDateFormat();
2281 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2282 }
2283
2284 $this->dateFormatStrings[$type][$pref] = $df;
2285 }
2286 return $this->dateFormatStrings[$type][$pref];
2287 }
2288
2299 public function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
2300 $ts = wfTimestamp( TS_MW, $ts );
2301 if ( $adj ) {
2302 $ts = $this->userAdjust( $ts, $timecorrection );
2303 }
2304 $df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
2305 return $this->sprintfDate( $df, $ts );
2306 }
2307
2318 public function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
2319 $ts = wfTimestamp( TS_MW, $ts );
2320 if ( $adj ) {
2321 $ts = $this->userAdjust( $ts, $timecorrection );
2322 }
2323 $df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
2324 return $this->sprintfDate( $df, $ts );
2325 }
2326
2338 public function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false ) {
2339 $ts = wfTimestamp( TS_MW, $ts );
2340 if ( $adj ) {
2341 $ts = $this->userAdjust( $ts, $timecorrection );
2342 }
2343 $df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) );
2344 return $this->sprintfDate( $df, $ts );
2345 }
2346
2357 public function formatDuration( $seconds, array $chosenIntervals = [] ) {
2358 $intervals = $this->getDurationIntervals( $seconds, $chosenIntervals );
2359
2360 $segments = [];
2361
2362 foreach ( $intervals as $intervalName => $intervalValue ) {
2363 // Messages: duration-seconds, duration-minutes, duration-hours, duration-days, duration-weeks,
2364 // duration-years, duration-decades, duration-centuries, duration-millennia
2365 $message = wfMessage( 'duration-' . $intervalName )->numParams( $intervalValue );
2366 $segments[] = $message->inLanguage( $this )->escaped();
2367 }
2368
2369 return $this->listToText( $segments );
2370 }
2371
2383 public function getDurationIntervals( $seconds, array $chosenIntervals = [] ) {
2384 if ( empty( $chosenIntervals ) ) {
2385 $chosenIntervals = [
2386 'millennia',
2387 'centuries',
2388 'decades',
2389 'years',
2390 'days',
2391 'hours',
2392 'minutes',
2393 'seconds'
2394 ];
2395 }
2396
2397 $intervals = array_intersect_key( self::$durationIntervals, array_flip( $chosenIntervals ) );
2398 $sortedNames = array_keys( $intervals );
2399 $smallestInterval = array_pop( $sortedNames );
2400
2401 $segments = [];
2402
2403 foreach ( $intervals as $name => $length ) {
2404 $value = floor( $seconds / $length );
2405
2406 if ( $value > 0 || ( $name == $smallestInterval && empty( $segments ) ) ) {
2407 $seconds -= $value * $length;
2408 $segments[$name] = $value;
2409 }
2410 }
2411
2412 return $segments;
2413 }
2414
2434 private function internalUserTimeAndDate( $type, $ts, User $user, array $options ) {
2435 $ts = wfTimestamp( TS_MW, $ts );
2436 $options += [ 'timecorrection' => true, 'format' => true ];
2437 if ( $options['timecorrection'] !== false ) {
2438 if ( $options['timecorrection'] === true ) {
2439 $offset = $user->getOption( 'timecorrection' );
2440 } else {
2441 $offset = $options['timecorrection'];
2442 }
2443 $ts = $this->userAdjust( $ts, $offset );
2444 }
2445 if ( $options['format'] === true ) {
2446 $format = $user->getDatePreference();
2447 } else {
2448 $format = $options['format'];
2449 }
2450 $df = $this->getDateFormatString( $type, $this->dateFormat( $format ) );
2451 return $this->sprintfDate( $df, $ts );
2452 }
2453
2473 public function userDate( $ts, User $user, array $options = [] ) {
2474 return $this->internalUserTimeAndDate( 'date', $ts, $user, $options );
2475 }
2476
2496 public function userTime( $ts, User $user, array $options = [] ) {
2497 return $this->internalUserTimeAndDate( 'time', $ts, $user, $options );
2498 }
2499
2519 public function userTimeAndDate( $ts, User $user, array $options = [] ) {
2520 return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
2521 }
2522
2538 public function getHumanTimestamp(
2539 MWTimestamp $time, MWTimestamp $relativeTo = null, User $user = null
2540 ) {
2541 if ( $relativeTo === null ) {
2542 $relativeTo = new MWTimestamp();
2543 }
2544 if ( $user === null ) {
2545 $user = RequestContext::getMain()->getUser();
2546 }
2547
2548 // Adjust for the user's timezone.
2549 $offsetThis = $time->offsetForUser( $user );
2550 $offsetRel = $relativeTo->offsetForUser( $user );
2551
2552 $ts = '';
2553 if ( Hooks::run( 'GetHumanTimestamp', [ &$ts, $time, $relativeTo, $user, $this ] ) ) {
2554 $ts = $this->getHumanTimestampInternal( $time, $relativeTo, $user );
2555 }
2556
2557 // Reset the timezone on the objects.
2558 $time->timestamp->sub( $offsetThis );
2559 $relativeTo->timestamp->sub( $offsetRel );
2560
2561 return $ts;
2562 }
2563
2576 MWTimestamp $ts, MWTimestamp $relativeTo, User $user
2577 ) {
2578 $diff = $ts->diff( $relativeTo );
2579 $diffDay = (bool)( (int)$ts->timestamp->format( 'w' ) -
2580 (int)$relativeTo->timestamp->format( 'w' ) );
2581 $days = $diff->days ?: (int)$diffDay;
2582 if ( $diff->invert || $days > 5
2583 && $ts->timestamp->format( 'Y' ) !== $relativeTo->timestamp->format( 'Y' )
2584 ) {
2585 // Timestamps are in different years: use full timestamp
2586 // Also do full timestamp for future dates
2590 $format = $this->getDateFormatString( 'both', $user->getDatePreference() ?: 'default' );
2591 $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
2592 } elseif ( $days > 5 ) {
2593 // Timestamps are in same year, but more than 5 days ago: show day and month only.
2594 $format = $this->getDateFormatString( 'pretty', $user->getDatePreference() ?: 'default' );
2595 $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
2596 } elseif ( $days > 1 ) {
2597 // Timestamp within the past week: show the day of the week and time
2598 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2599 $weekday = self::$mWeekdayMsgs[$ts->timestamp->format( 'w' )];
2600 // Messages:
2601 // sunday-at, monday-at, tuesday-at, wednesday-at, thursday-at, friday-at, saturday-at
2602 $ts = wfMessage( "$weekday-at" )
2603 ->inLanguage( $this )
2604 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2605 ->text();
2606 } elseif ( $days == 1 ) {
2607 // Timestamp was yesterday: say 'yesterday' and the time.
2608 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2609 $ts = wfMessage( 'yesterday-at' )
2610 ->inLanguage( $this )
2611 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2612 ->text();
2613 } elseif ( $diff->h > 1 || $diff->h == 1 && $diff->i > 30 ) {
2614 // Timestamp was today, but more than 90 minutes ago: say 'today' and the time.
2615 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2616 $ts = wfMessage( 'today-at' )
2617 ->inLanguage( $this )
2618 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2619 ->text();
2620
2621 // From here on in, the timestamp was soon enough ago so that we can simply say
2622 // XX units ago, e.g., "2 hours ago" or "5 minutes ago"
2623 } elseif ( $diff->h == 1 ) {
2624 // Less than 90 minutes, but more than an hour ago.
2625 $ts = wfMessage( 'hours-ago' )->inLanguage( $this )->numParams( 1 )->text();
2626 } elseif ( $diff->i >= 1 ) {
2627 // A few minutes ago.
2628 $ts = wfMessage( 'minutes-ago' )->inLanguage( $this )->numParams( $diff->i )->text();
2629 } elseif ( $diff->s >= 30 ) {
2630 // Less than a minute, but more than 30 sec ago.
2631 $ts = wfMessage( 'seconds-ago' )->inLanguage( $this )->numParams( $diff->s )->text();
2632 } else {
2633 // Less than 30 seconds ago.
2634 $ts = wfMessage( 'just-now' )->text();
2635 }
2636
2637 return $ts;
2638 }
2639
2644 public function getMessage( $key ) {
2645 return self::$dataCache->getSubitem( $this->mCode, 'messages', $key );
2646 }
2647
2651 function getAllMessages() {
2652 return self::$dataCache->getItem( $this->mCode, 'messages' );
2653 }
2654
2661 public function iconv( $in, $out, $string ) {
2662 # Even with //IGNORE iconv can whine about illegal characters in
2663 # *input* string. We just ignore those too.
2664 # REF: https://bugs.php.net/bug.php?id=37166
2665 # REF: https://phabricator.wikimedia.org/T18885
2666 Wikimedia\suppressWarnings();
2667 $text = iconv( $in, $out . '//IGNORE', $string );
2668 Wikimedia\restoreWarnings();
2669 return $text;
2670 }
2671
2672 // callback functions for ucwords(), ucwordbreaks()
2673
2679 return $this->ucfirst( $matches[1] );
2680 }
2681
2687 return mb_strtoupper( $matches[0] );
2688 }
2689
2695 return mb_strtoupper( $matches[0] );
2696 }
2697
2705 public function ucfirst( $str ) {
2706 $o = ord( $str );
2707 if ( $o < 96 ) { // if already uppercase...
2708 return $str;
2709 } elseif ( $o < 128 ) {
2710 return ucfirst( $str ); // use PHP's ucfirst()
2711 } else {
2712 // fall back to more complex logic in case of multibyte strings
2713 return $this->uc( $str, true );
2714 }
2715 }
2716
2725 public function uc( $str, $first = false ) {
2726 if ( $first ) {
2727 if ( $this->isMultibyte( $str ) ) {
2728 return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2729 } else {
2730 return ucfirst( $str );
2731 }
2732 } else {
2733 return $this->isMultibyte( $str ) ? mb_strtoupper( $str ) : strtoupper( $str );
2734 }
2735 }
2736
2741 function lcfirst( $str ) {
2742 $o = ord( $str );
2743 if ( !$o ) {
2744 return strval( $str );
2745 } elseif ( $o >= 128 ) {
2746 return $this->lc( $str, true );
2747 } elseif ( $o > 96 ) {
2748 return $str;
2749 } else {
2750 $str[0] = strtolower( $str[0] );
2751 return $str;
2752 }
2753 }
2754
2760 function lc( $str, $first = false ) {
2761 if ( $first ) {
2762 if ( $this->isMultibyte( $str ) ) {
2763 return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2764 } else {
2765 return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
2766 }
2767 } else {
2768 return $this->isMultibyte( $str ) ? mb_strtolower( $str ) : strtolower( $str );
2769 }
2770 }
2771
2776 function isMultibyte( $str ) {
2777 return strlen( $str ) !== mb_strlen( $str );
2778 }
2779
2784 function ucwords( $str ) {
2785 if ( $this->isMultibyte( $str ) ) {
2786 $str = $this->lc( $str );
2787
2788 // regexp to find first letter in each word (i.e. after each space)
2789 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2790
2791 // function to use to capitalize a single char
2792 return preg_replace_callback(
2793 $replaceRegexp,
2794 [ $this, 'ucwordsCallbackMB' ],
2795 $str
2796 );
2797 } else {
2798 return ucwords( strtolower( $str ) );
2799 }
2800 }
2801
2808 function ucwordbreaks( $str ) {
2809 if ( $this->isMultibyte( $str ) ) {
2810 $str = $this->lc( $str );
2811
2812 // since \b doesn't work for UTF-8, we explicitely define word break chars
2813 $breaks = "[ \-\‍(\‍)\}\{\.,\?!]";
2814
2815 // find first letter after word break
2816 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|" .
2817 "$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2818
2819 return preg_replace_callback(
2820 $replaceRegexp,
2821 [ $this, 'ucwordbreaksCallbackMB' ],
2822 $str
2823 );
2824 } else {
2825 return preg_replace_callback(
2826 '/\b([\w\x80-\xff]+)\b/',
2827 [ $this, 'ucwordbreaksCallbackAscii' ],
2828 $str
2829 );
2830 }
2831 }
2832
2848 function caseFold( $s ) {
2849 return $this->uc( $s );
2850 }
2851
2858 if ( is_array( $s ) ) {
2859 throw new MWException( 'Given array to checkTitleEncoding.' );
2860 }
2861 if ( StringUtils::isUtf8( $s ) ) {
2862 return $s;
2863 }
2864
2865 return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s );
2866 }
2867
2872 return self::$dataCache->getItem( $this->mCode, 'fallback8bitEncoding' );
2873 }
2874
2883 function hasWordBreaks() {
2884 return true;
2885 }
2886
2894 function segmentByWord( $string ) {
2895 return $string;
2896 }
2897
2905 function normalizeForSearch( $string ) {
2906 return self::convertDoubleWidth( $string );
2907 }
2908
2917 protected static function convertDoubleWidth( $string ) {
2918 static $full = null;
2919 static $half = null;
2920
2921 if ( $full === null ) {
2922 $fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2923 $halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2924 $full = str_split( $fullWidth, 3 );
2925 $half = str_split( $halfWidth );
2926 }
2927
2928 $string = str_replace( $full, $half, $string );
2929 return $string;
2930 }
2931
2937 protected static function insertSpace( $string, $pattern ) {
2938 $string = preg_replace( $pattern, " $1 ", $string );
2939 $string = preg_replace( '/ +/', ' ', $string );
2940 return $string;
2941 }
2942
2947 function convertForSearchResult( $termsArray ) {
2948 # some languages, e.g. Chinese, need to do a conversion
2949 # in order for search results to be displayed correctly
2950 return $termsArray;
2951 }
2952
2959 function firstChar( $s ) {
2960 $matches = [];
2961 preg_match(
2962 '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
2963 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/',
2964 $s,
2965 $matches
2966 );
2967
2968 if ( isset( $matches[1] ) ) {
2969 if ( strlen( $matches[1] ) != 3 ) {
2970 return $matches[1];
2971 }
2972
2973 // Break down Hangul syllables to grab the first jamo
2974 $code = UtfNormal\Utils::utf8ToCodepoint( $matches[1] );
2975 if ( $code < 0xac00 || 0xd7a4 <= $code ) {
2976 return $matches[1];
2977 } elseif ( $code < 0xb098 ) {
2978 return "\u{3131}";
2979 } elseif ( $code < 0xb2e4 ) {
2980 return "\u{3134}";
2981 } elseif ( $code < 0xb77c ) {
2982 return "\u{3137}";
2983 } elseif ( $code < 0xb9c8 ) {
2984 return "\u{3139}";
2985 } elseif ( $code < 0xbc14 ) {
2986 return "\u{3141}";
2987 } elseif ( $code < 0xc0ac ) {
2988 return "\u{3142}";
2989 } elseif ( $code < 0xc544 ) {
2990 return "\u{3145}";
2991 } elseif ( $code < 0xc790 ) {
2992 return "\u{3147}";
2993 } elseif ( $code < 0xcc28 ) {
2994 return "\u{3148}";
2995 } elseif ( $code < 0xce74 ) {
2996 return "\u{314A}";
2997 } elseif ( $code < 0xd0c0 ) {
2998 return "\u{314B}";
2999 } elseif ( $code < 0xd30c ) {
3000 return "\u{314C}";
3001 } elseif ( $code < 0xd558 ) {
3002 return "\u{314D}";
3003 } else {
3004 return "\u{314E}";
3005 }
3006 } else {
3007 return '';
3008 }
3009 }
3010
3014 function initEncoding() {
3015 wfDeprecated( __METHOD__, '1.28' );
3016 // No-op.
3017 }
3018
3024 function recodeForEdit( $s ) {
3025 wfDeprecated( __METHOD__, '1.28' );
3026 return $s;
3027 }
3028
3034 function recodeInput( $s ) {
3035 wfDeprecated( __METHOD__, '1.28' );
3036 return $s;
3037 }
3038
3050 public function normalize( $s ) {
3051 global $wgAllUnicodeFixes;
3052 $s = UtfNormal\Validator::cleanUp( $s );
3053 if ( $wgAllUnicodeFixes ) {
3054 $s = $this->transformUsingPairFile( 'normalize-ar.php', $s );
3055 $s = $this->transformUsingPairFile( 'normalize-ml.php', $s );
3056 }
3057
3058 return $s;
3059 }
3060
3075 protected function transformUsingPairFile( $file, $string ) {
3076 if ( !isset( $this->transformData[$file] ) ) {
3077 global $IP;
3078 $data = require "$IP/languages/data/{$file}";
3079 $this->transformData[$file] = new ReplacementArray( $data );
3080 }
3081 return $this->transformData[$file]->replace( $string );
3082 }
3083
3089 function isRTL() {
3090 return self::$dataCache->getItem( $this->mCode, 'rtl' );
3091 }
3092
3097 function getDir() {
3098 return $this->isRTL() ? 'rtl' : 'ltr';
3099 }
3100
3109 function alignStart() {
3110 return $this->isRTL() ? 'right' : 'left';
3111 }
3112
3121 function alignEnd() {
3122 return $this->isRTL() ? 'left' : 'right';
3123 }
3124
3136 function getDirMarkEntity( $opposite = false ) {
3137 if ( $opposite ) {
3138 return $this->isRTL() ? '&lrm;' : '&rlm;';
3139 }
3140 return $this->isRTL() ? '&rlm;' : '&lrm;';
3141 }
3142
3153 function getDirMark( $opposite = false ) {
3154 $lrm = "\u{200E}"; # LEFT-TO-RIGHT MARK, commonly abbreviated LRM
3155 $rlm = "\u{200F}"; # RIGHT-TO-LEFT MARK, commonly abbreviated RLM
3156 if ( $opposite ) {
3157 return $this->isRTL() ? $lrm : $rlm;
3158 }
3159 return $this->isRTL() ? $rlm : $lrm;
3160 }
3161
3166 return self::$dataCache->getItem( $this->mCode, 'capitalizeAllNouns' );
3167 }
3168
3176 function getArrow( $direction = 'forwards' ) {
3177 switch ( $direction ) {
3178 case 'forwards':
3179 return $this->isRTL() ? '←' : '→';
3180 case 'backwards':
3181 return $this->isRTL() ? '→' : '←';
3182 case 'left':
3183 return '←';
3184 case 'right':
3185 return '→';
3186 case 'up':
3187 return '↑';
3188 case 'down':
3189 return '↓';
3190 }
3191 }
3192
3199 return self::$dataCache->getItem( $this->mCode, 'linkPrefixExtension' );
3200 }
3201
3206 function getMagicWords() {
3207 return self::$dataCache->getItem( $this->mCode, 'magicWords' );
3208 }
3209
3215 function getMagic( $mw ) {
3216 $rawEntry = $this->mMagicExtensions[$mw->mId] ??
3217 self::$dataCache->getSubitem( $this->mCode, 'magicWords', $mw->mId );
3218
3219 if ( !is_array( $rawEntry ) ) {
3220 wfWarn( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
3221 } else {
3222 $mw->mCaseSensitive = $rawEntry[0];
3223 $mw->mSynonyms = array_slice( $rawEntry, 1 );
3224 }
3225 }
3226
3232 function addMagicWordsByLang( $newWords ) {
3233 $fallbackChain = $this->getFallbackLanguages();
3234 $fallbackChain = array_reverse( $fallbackChain );
3235 foreach ( $fallbackChain as $code ) {
3236 if ( isset( $newWords[$code] ) ) {
3237 $this->mMagicExtensions = $newWords[$code] + $this->mMagicExtensions;
3238 }
3239 }
3240 }
3241
3248 // Cache aliases because it may be slow to load them
3249 if ( is_null( $this->mExtendedSpecialPageAliases ) ) {
3250 // Initialise array
3251 $this->mExtendedSpecialPageAliases =
3252 self::$dataCache->getItem( $this->mCode, 'specialPageAliases' );
3253 }
3254
3255 return $this->mExtendedSpecialPageAliases;
3256 }
3257
3264 function emphasize( $text ) {
3265 return "<em>$text</em>";
3266 }
3267
3290 public function formatNum( $number, $nocommafy = false ) {
3291 global $wgTranslateNumerals;
3292 if ( !$nocommafy ) {
3293 $number = $this->commafy( $number );
3294 $s = $this->separatorTransformTable();
3295 if ( $s ) {
3296 $number = strtr( $number, $s );
3297 }
3298 }
3299
3300 if ( $wgTranslateNumerals ) {
3301 $s = $this->digitTransformTable();
3302 if ( $s ) {
3303 $number = strtr( $number, $s );
3304 }
3305 }
3306
3307 return (string)$number;
3308 }
3309
3318 public function formatNumNoSeparators( $number ) {
3319 return $this->formatNum( $number, true );
3320 }
3321
3326 public function parseFormattedNumber( $number ) {
3327 $s = $this->digitTransformTable();
3328 if ( $s ) {
3329 // eliminate empty array values such as ''. (T66347)
3330 $s = array_filter( $s );
3331 $number = strtr( $number, array_flip( $s ) );
3332 }
3333
3334 $s = $this->separatorTransformTable();
3335 if ( $s ) {
3336 // eliminate empty array values such as ''. (T66347)
3337 $s = array_filter( $s );
3338 $number = strtr( $number, array_flip( $s ) );
3339 }
3340
3341 $number = strtr( $number, [ ',' => '' ] );
3342 return $number;
3343 }
3344
3351 function commafy( $number ) {
3354 if ( $number === null ) {
3355 return '';
3356 }
3357
3358 if ( !$digitGroupingPattern || $digitGroupingPattern === "###,###,###" ) {
3359 // Default grouping is at thousands, use the same for ###,###,### pattern too.
3360 // In some languages it's conventional not to insert a thousands separator
3361 // in numbers that are four digits long (1000-9999).
3362 if ( $minimumGroupingDigits ) {
3363 // Number of '#' characters after last comma in the grouping pattern.
3364 // The pattern is hardcoded here, but this would vary for different patterns.
3365 $primaryGroupingSize = 3;
3366 // Maximum length of a number to suppress digit grouping for.
3367 $maximumLength = $minimumGroupingDigits + $primaryGroupingSize - 1;
3368 if ( preg_match( '/^\-?\d{1,' . $maximumLength . '}(\.\d+)?$/', $number ) ) {
3369 return $number;
3370 }
3371 }
3372 return strrev( (string)preg_replace( '/(\d{3})(?=\d)(?!\d*\.)/', '$1,', strrev( $number ) ) );
3373 } else {
3374 // Ref: http://cldr.unicode.org/translation/number-patterns
3375 $sign = "";
3376 if ( intval( $number ) < 0 ) {
3377 // For negative numbers apply the algorithm like positive number and add sign.
3378 $sign = "-";
3379 $number = substr( $number, 1 );
3380 }
3381 $integerPart = [];
3382 $decimalPart = [];
3383 $numMatches = preg_match_all( "/(#+)/", $digitGroupingPattern, $matches );
3384 preg_match( "/\d+/", $number, $integerPart );
3385 preg_match( "/\.\d*/", $number, $decimalPart );
3386 $groupedNumber = ( count( $decimalPart ) > 0 ) ? $decimalPart[0] : "";
3387 if ( $groupedNumber === $number ) {
3388 // the string does not have any number part. Eg: .12345
3389 return $sign . $groupedNumber;
3390 }
3391 $start = $end = ( $integerPart ) ? strlen( $integerPart[0] ) : 0;
3392 while ( $start > 0 ) {
3393 $match = $matches[0][$numMatches - 1];
3394 $matchLen = strlen( $match );
3395 $start = $end - $matchLen;
3396 if ( $start < 0 ) {
3397 $start = 0;
3398 }
3399 $groupedNumber = substr( $number, $start, $end - $start ) . $groupedNumber;
3400 $end = $start;
3401 if ( $numMatches > 1 ) {
3402 // use the last pattern for the rest of the number
3403 $numMatches--;
3404 }
3405 if ( $start > 0 ) {
3406 $groupedNumber = "," . $groupedNumber;
3407 }
3408 }
3409 return $sign . $groupedNumber;
3410 }
3411 }
3412
3417 return self::$dataCache->getItem( $this->mCode, 'digitGroupingPattern' );
3418 }
3419
3424 return self::$dataCache->getItem( $this->mCode, 'digitTransformTable' );
3425 }
3426
3431 return self::$dataCache->getItem( $this->mCode, 'separatorTransformTable' );
3432 }
3433
3438 return self::$dataCache->getItem( $this->mCode, 'minimumGroupingDigits' );
3439 }
3440
3449 public function listToText( array $list ) {
3450 $itemCount = count( $list );
3451 if ( $itemCount < 1 ) {
3452 return '';
3453 }
3454 $text = array_pop( $list );
3455 if ( $itemCount > 1 ) {
3456 $and = $this->msg( 'and' )->escaped();
3457 $space = $this->msg( 'word-separator' )->escaped();
3458 $comma = '';
3459 if ( $itemCount > 2 ) {
3460 $comma = $this->msg( 'comma-separator' )->escaped();
3461 }
3462 $text = implode( $comma, $list ) . $and . $space . $text;
3463 }
3464 return $text;
3465 }
3466
3473 function commaList( array $list ) {
3474 return implode(
3475 wfMessage( 'comma-separator' )->inLanguage( $this )->escaped(),
3476 $list
3477 );
3478 }
3479
3486 function semicolonList( array $list ) {
3487 return implode(
3488 wfMessage( 'semicolon-separator' )->inLanguage( $this )->escaped(),
3489 $list
3490 );
3491 }
3492
3498 function pipeList( array $list ) {
3499 return implode(
3500 wfMessage( 'pipe-separator' )->inLanguage( $this )->escaped(),
3501 $list
3502 );
3503 }
3504
3520 function truncateForDatabase( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3521 return $this->truncateInternal(
3522 $string, $length, $ellipsis, $adjustLength, 'strlen', 'substr'
3523 );
3524 }
3525
3544 function truncateForVisual( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3545 // Passing encoding to mb_strlen and mb_substr is optional.
3546 // Encoding defaults to mb_internal_encoding(), which is set to UTF-8 in Setup.php, so
3547 // explicit specification of encoding is skipped.
3548 // Note: Both multibyte methods are callables invoked in truncateInternal.
3549 return $this->truncateInternal(
3550 $string, $length, $ellipsis, $adjustLength, 'mb_strlen', 'mb_substr'
3551 );
3552 }
3553
3570 private function truncateInternal(
3571 $string, $length, $ellipsis, $adjustLength, callable $measureLength, callable $getSubstring
3572 ) {
3573 # Check if there is no need to truncate
3574 if ( $measureLength( $string ) <= abs( $length ) ) {
3575 return $string; // no need to truncate
3576 }
3577
3578 # Use the localized ellipsis character
3579 if ( $ellipsis == '...' ) {
3580 $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3581 }
3582 if ( $length == 0 ) {
3583 return $ellipsis; // convention
3584 }
3585
3586 $stringOriginal = $string;
3587 # If ellipsis length is >= $length then we can't apply $adjustLength
3588 if ( $adjustLength && $measureLength( $ellipsis ) >= abs( $length ) ) {
3589 $string = $ellipsis; // this can be slightly unexpected
3590 # Otherwise, truncate and add ellipsis...
3591 } else {
3592 $ellipsisLength = $adjustLength ? $measureLength( $ellipsis ) : 0;
3593 if ( $length > 0 ) {
3594 $length -= $ellipsisLength;
3595 $string = $getSubstring( $string, 0, $length ); // xyz...
3596 $string = $this->removeBadCharLast( $string );
3597 $string = rtrim( $string ) . $ellipsis;
3598 } else {
3599 $length += $ellipsisLength;
3600 $string = $getSubstring( $string, $length ); // ...xyz
3601 $string = $this->removeBadCharFirst( $string );
3602 $string = $ellipsis . ltrim( $string );
3603 }
3604 }
3605
3606 # Do not truncate if the ellipsis makes the string longer/equal (T24181).
3607 # This check is *not* redundant if $adjustLength, due to the single case where
3608 # LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string.
3609 if ( $measureLength( $string ) < $measureLength( $stringOriginal ) ) {
3610 return $string;
3611 } else {
3612 return $stringOriginal;
3613 }
3614 }
3615
3623 protected function removeBadCharLast( $string ) {
3624 if ( $string != '' ) {
3625 $char = ord( $string[strlen( $string ) - 1] );
3626 $m = [];
3627 if ( $char >= 0xc0 ) {
3628 # We got the first byte only of a multibyte char; remove it.
3629 $string = substr( $string, 0, -1 );
3630 } elseif ( $char >= 0x80 &&
3631 // Use the /s modifier (PCRE_DOTALL) so (.*) also matches newlines
3632 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
3633 '[\xf0-\xf7][\x80-\xbf]{1,2})$/s', $string, $m )
3634 ) {
3635 # We chopped in the middle of a character; remove it
3636 $string = $m[1];
3637 }
3638 }
3639 return $string;
3640 }
3641
3649 protected function removeBadCharFirst( $string ) {
3650 if ( $string != '' ) {
3651 $char = ord( $string[0] );
3652 if ( $char >= 0x80 && $char < 0xc0 ) {
3653 # We chopped in the middle of a character; remove the whole thing
3654 $string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
3655 }
3656 }
3657 return $string;
3658 }
3659
3675 function truncateHtml( $text, $length, $ellipsis = '...' ) {
3676 # Use the localized ellipsis character
3677 if ( $ellipsis == '...' ) {
3678 $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3679 }
3680 # Check if there is clearly no need to truncate
3681 if ( $length <= 0 ) {
3682 return $ellipsis; // no text shown, nothing to format (convention)
3683 } elseif ( strlen( $text ) <= $length ) {
3684 return $text; // string short enough even *with* HTML (short-circuit)
3685 }
3686
3687 $dispLen = 0; // innerHTML legth so far
3688 $testingEllipsis = false; // checking if ellipses will make string longer/equal?
3689 $tagType = 0; // 0-open, 1-close
3690 $bracketState = 0; // 1-tag start, 2-tag name, 0-neither
3691 $entityState = 0; // 0-not entity, 1-entity
3692 $tag = $ret = ''; // accumulated tag name, accumulated result string
3693 $openTags = []; // open tag stack
3694 $maybeState = null; // possible truncation state
3695
3696 $textLen = strlen( $text );
3697 $neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated
3698 for ( $pos = 0; true; ++$pos ) {
3699 # Consider truncation once the display length has reached the maximim.
3700 # We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
3701 # Check that we're not in the middle of a bracket/entity...
3702 if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) {
3703 if ( !$testingEllipsis ) {
3704 $testingEllipsis = true;
3705 # Save where we are; we will truncate here unless there turn out to
3706 # be so few remaining characters that truncation is not necessary.
3707 if ( !$maybeState ) { // already saved? ($neLength = 0 case)
3708 $maybeState = [ $ret, $openTags ]; // save state
3709 }
3710 } elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) {
3711 # String in fact does need truncation, the truncation point was OK.
3712 // @phan-suppress-next-line PhanTypeInvalidExpressionArrayDestructuring
3713 list( $ret, $openTags ) = $maybeState; // reload state
3714 $ret = $this->removeBadCharLast( $ret ); // multi-byte char fix
3715 $ret .= $ellipsis; // add ellipsis
3716 break;
3717 }
3718 }
3719 if ( $pos >= $textLen ) {
3720 break; // extra iteration just for above checks
3721 }
3722
3723 # Read the next char...
3724 $ch = $text[$pos];
3725 $lastCh = $pos ? $text[$pos - 1] : '';
3726 $ret .= $ch; // add to result string
3727 if ( $ch == '<' ) {
3728 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); // for bad HTML
3729 $entityState = 0; // for bad HTML
3730 $bracketState = 1; // tag started (checking for backslash)
3731 } elseif ( $ch == '>' ) {
3732 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags );
3733 $entityState = 0; // for bad HTML
3734 $bracketState = 0; // out of brackets
3735 } elseif ( $bracketState == 1 ) {
3736 if ( $ch == '/' ) {
3737 $tagType = 1; // close tag (e.g. "</span>")
3738 } else {
3739 $tagType = 0; // open tag (e.g. "<span>")
3740 $tag .= $ch;
3741 }
3742 $bracketState = 2; // building tag name
3743 } elseif ( $bracketState == 2 ) {
3744 if ( $ch != ' ' ) {
3745 $tag .= $ch;
3746 } else {
3747 // Name found (e.g. "<a href=..."), add on tag attributes...
3748 $pos += $this->truncate_skip( $ret, $text, "<>", $pos + 1 );
3749 }
3750 } elseif ( $bracketState == 0 ) {
3751 if ( $entityState ) {
3752 if ( $ch == ';' ) {
3753 $entityState = 0;
3754 $dispLen++; // entity is one displayed char
3755 }
3756 } else {
3757 if ( $neLength == 0 && !$maybeState ) {
3758 // Save state without $ch. We want to *hit* the first
3759 // display char (to get tags) but not *use* it if truncating.
3760 $maybeState = [ substr( $ret, 0, -1 ), $openTags ];
3761 }
3762 if ( $ch == '&' ) {
3763 $entityState = 1; // entity found, (e.g. "&#160;")
3764 } else {
3765 $dispLen++; // this char is displayed
3766 // Add the next $max display text chars after this in one swoop...
3767 $max = ( $testingEllipsis ? $length : $neLength ) - $dispLen;
3768 $skipped = $this->truncate_skip( $ret, $text, "<>&", $pos + 1, $max );
3769 $dispLen += $skipped;
3770 $pos += $skipped;
3771 }
3772 }
3773 }
3774 }
3775 // Close the last tag if left unclosed by bad HTML
3776 $this->truncate_endBracket( $tag, $text[$textLen - 1], $tagType, $openTags );
3777 while ( count( $openTags ) > 0 ) {
3778 $ret .= '</' . array_pop( $openTags ) . '>'; // close open tags
3779 }
3780 return $ret;
3781 }
3782
3794 private function truncate_skip( &$ret, $text, $search, $start, $len = null ) {
3795 if ( $len === null ) {
3796 $len = -1; // -1 means "no limit" for strcspn
3797 } elseif ( $len < 0 ) {
3798 $len = 0; // sanity
3799 }
3800 $skipCount = 0;
3801 if ( $start < strlen( $text ) ) {
3802 $skipCount = strcspn( $text, $search, $start, $len );
3803 $ret .= substr( $text, $start, $skipCount );
3804 }
3805 return $skipCount;
3806 }
3807
3817 private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) {
3818 $tag = ltrim( $tag );
3819 if ( $tag != '' ) {
3820 if ( $tagType == 0 && $lastCh != '/' ) {
3821 $openTags[] = $tag; // tag opened (didn't close itself)
3822 } elseif ( $tagType == 1 ) {
3823 if ( $openTags && $tag == $openTags[count( $openTags ) - 1] ) {
3824 array_pop( $openTags ); // tag closed
3825 }
3826 }
3827 $tag = '';
3828 }
3829 }
3830
3839 function convertGrammar( $word, $case ) {
3840 global $wgGrammarForms;
3841 if ( isset( $wgGrammarForms[$this->getCode()][$case][$word] ) ) {
3842 return $wgGrammarForms[$this->getCode()][$case][$word];
3843 }
3844
3846
3847 if ( isset( $grammarTransformations[$case] ) ) {
3848 $forms = $grammarTransformations[$case];
3849
3850 // Some names of grammar rules are aliases for other rules.
3851 // In such cases the value is a string rather than object,
3852 // so load the actual rules.
3853 if ( is_string( $forms ) ) {
3854 $forms = $grammarTransformations[$forms];
3855 }
3856
3857 foreach ( array_values( $forms ) as $rule ) {
3858 $form = $rule[0];
3859
3860 if ( $form === '@metadata' ) {
3861 continue;
3862 }
3863
3864 $replacement = $rule[1];
3865
3866 $regex = '/' . addcslashes( $form, '/' ) . '/u';
3867 $patternMatches = preg_match( $regex, $word );
3868
3869 if ( $patternMatches === false ) {
3871 'An error occurred while processing grammar. ' .
3872 "Word: '$word'. Regex: /$form/."
3873 );
3874 } elseif ( $patternMatches === 1 ) {
3875 $word = preg_replace( $regex, $replacement, $word );
3876
3877 break;
3878 }
3879 }
3880 }
3881
3882 return $word;
3883 }
3884
3890 function getGrammarForms() {
3891 global $wgGrammarForms;
3892 if ( isset( $wgGrammarForms[$this->getCode()] )
3893 && is_array( $wgGrammarForms[$this->getCode()] )
3894 ) {
3895 return $wgGrammarForms[$this->getCode()];
3896 }
3897
3898 return [];
3899 }
3900
3910 public function getGrammarTransformations() {
3911 $languageCode = $this->getCode();
3912
3913 if ( self::$grammarTransformations === null ) {
3914 self::$grammarTransformations = new MapCacheLRU( 10 );
3915 }
3916
3917 if ( self::$grammarTransformations->has( $languageCode ) ) {
3918 return self::$grammarTransformations->get( $languageCode );
3919 }
3920
3921 $data = [];
3922
3923 $grammarDataFile = __DIR__ . "/data/grammarTransformations/$languageCode.json";
3924 if ( is_readable( $grammarDataFile ) ) {
3925 $data = FormatJson::decode(
3926 file_get_contents( $grammarDataFile ),
3927 true
3928 );
3929
3930 if ( $data === null ) {
3931 throw new MWException( "Invalid grammar data for \"$languageCode\"." );
3932 }
3933
3934 self::$grammarTransformations->set( $languageCode, $data );
3935 }
3936
3937 return $data;
3938 }
3939
3959 function gender( $gender, $forms ) {
3960 if ( !count( $forms ) ) {
3961 return '';
3962 }
3963 $forms = $this->preConvertPlural( $forms, 2 );
3964 if ( $gender === 'male' ) {
3965 return $forms[0];
3966 }
3967 if ( $gender === 'female' ) {
3968 return $forms[1];
3969 }
3970 return $forms[2] ?? $forms[0];
3971 }
3972
3988 function convertPlural( $count, $forms ) {
3989 // Handle explicit n=pluralform cases
3990 $forms = $this->handleExplicitPluralForms( $count, $forms );
3991 if ( is_string( $forms ) ) {
3992 return $forms;
3993 }
3994 if ( !count( $forms ) ) {
3995 return '';
3996 }
3997
3998 $pluralForm = $this->getPluralRuleIndexNumber( $count );
3999 $pluralForm = min( $pluralForm, count( $forms ) - 1 );
4000 return $forms[$pluralForm];
4001 }
4002
4018 protected function handleExplicitPluralForms( $count, array $forms ) {
4019 foreach ( $forms as $index => $form ) {
4020 if ( preg_match( '/\d+=/i', $form ) ) {
4021 $pos = strpos( $form, '=' );
4022 if ( substr( $form, 0, $pos ) === (string)$count ) {
4023 return substr( $form, $pos + 1 );
4024 }
4025 unset( $forms[$index] );
4026 }
4027 }
4028 return array_values( $forms );
4029 }
4030
4039 protected function preConvertPlural( /* Array */ $forms, $count ) {
4040 return array_pad( $forms, $count, end( $forms ) );
4041 }
4042
4059 public function embedBidi( $text = '' ) {
4060 $dir = self::strongDirFromContent( $text );
4061 if ( $dir === 'ltr' ) {
4062 // Wrap in LEFT-TO-RIGHT EMBEDDING ... POP DIRECTIONAL FORMATTING
4063 return self::$lre . $text . self::$pdf;
4064 }
4065 if ( $dir === 'rtl' ) {
4066 // Wrap in RIGHT-TO-LEFT EMBEDDING ... POP DIRECTIONAL FORMATTING
4067 return self::$rle . $text . self::$pdf;
4068 }
4069 // No strong directionality: do not wrap
4070 return $text;
4071 }
4072
4086 function translateBlockExpiry( $str, User $user = null, $now = 0 ) {
4087 $duration = SpecialBlock::getSuggestedDurations( $this );
4088 foreach ( $duration as $show => $value ) {
4089 if ( strcmp( $str, $value ) == 0 ) {
4090 return htmlspecialchars( trim( $show ) );
4091 }
4092 }
4093
4094 if ( wfIsInfinity( $str ) ) {
4095 foreach ( $duration as $show => $value ) {
4096 if ( wfIsInfinity( $value ) ) {
4097 return htmlspecialchars( trim( $show ) );
4098 }
4099 }
4100 }
4101
4102 // If all else fails, return a standard duration or timestamp description.
4103 $time = strtotime( $str, $now );
4104 if ( $time === false ) { // Unknown format. Return it as-is in case.
4105 return $str;
4106 } elseif ( $time !== strtotime( $str, $now + 1 ) ) { // It's a relative timestamp.
4107 // The result differs based on current time, so the difference
4108 // is a fixed duration length.
4109 return $this->formatDuration( $time - $now );
4110 } else { // It's an absolute timestamp.
4111 if ( $time === 0 ) {
4112 // wfTimestamp() handles 0 as current time instead of epoch.
4113 $time = '19700101000000';
4114 }
4115 if ( $user ) {
4116 return $this->userTimeAndDate( $time, $user );
4117 }
4118 return $this->timeanddate( $time );
4119 }
4120 }
4121
4129 public function segmentForDiff( $text ) {
4130 return $text;
4131 }
4132
4139 public function unsegmentForDiff( $text ) {
4140 return $text;
4141 }
4142
4149 public function getConverter() {
4150 return $this->mConverter;
4151 }
4152
4161 public function autoConvert( $text, $variant = false ) {
4162 return $this->mConverter->autoConvert( $text, $variant );
4163 }
4164
4171 public function autoConvertToAllVariants( $text ) {
4172 return $this->mConverter->autoConvertToAllVariants( $text );
4173 }
4174
4186 public function convert( $text ) {
4187 return $this->mConverter->convert( $text );
4188 }
4189
4196 public function convertTitle( $title ) {
4197 return $this->mConverter->convertTitle( $title );
4198 }
4199
4208 public function convertNamespace( $ns, $variant = null ) {
4209 return $this->mConverter->convertNamespace( $ns, $variant );
4210 }
4211
4217 public function hasVariants() {
4218 return count( $this->getVariants() ) > 1;
4219 }
4220
4231 public function hasVariant( $variant ) {
4232 return $variant && ( $variant === $this->mConverter->validateVariant( $variant ) );
4233 }
4234
4241 public function convertHtml( $text ) {
4242 return htmlspecialchars( $this->convert( $text ) );
4243 }
4244
4249 public function convertCategoryKey( $key ) {
4250 return $this->mConverter->convertCategoryKey( $key );
4251 }
4252
4259 public function getVariants() {
4260 return $this->mConverter->getVariants();
4261 }
4262
4266 public function getPreferredVariant() {
4267 return $this->mConverter->getPreferredVariant();
4268 }
4269
4273 public function getDefaultVariant() {
4274 return $this->mConverter->getDefaultVariant();
4275 }
4276
4280 public function getURLVariant() {
4281 return $this->mConverter->getURLVariant();
4282 }
4283
4296 public function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
4297 $this->mConverter->findVariantLink( $link, $nt, $ignoreOtherCond );
4298 }
4299
4307 return $this->mConverter->getExtraHashOptions();
4308 }
4309
4317 public function getParsedTitle() {
4318 return $this->mConverter->getParsedTitle();
4319 }
4320
4327 public function updateConversionTable( Title $title ) {
4328 $this->mConverter->updateConversionTable( $title );
4329 }
4330
4337 public function linkTrail() {
4338 return self::$dataCache->getItem( $this->mCode, 'linkTrail' );
4339 }
4340
4347 public function linkPrefixCharset() {
4348 return self::$dataCache->getItem( $this->mCode, 'linkPrefixCharset' );
4349 }
4350
4358 public function getParentLanguage() {
4359 if ( $this->mParentLanguage !== false ) {
4360 return $this->mParentLanguage;
4361 }
4362
4363 $code = explode( '-', $this->getCode() )[0];
4364 if ( !in_array( $code, LanguageConverter::$languagesWithVariants ) ) {
4365 $this->mParentLanguage = null;
4366 return null;
4367 }
4368 $lang = self::factory( $code );
4369 if ( !$lang->hasVariant( $this->getCode() ) ) {
4370 $this->mParentLanguage = null;
4371 return null;
4372 }
4373
4374 $this->mParentLanguage = $lang;
4375 return $lang;
4376 }
4377
4385 public function equals( Language $lang ) {
4386 return $lang === $this || $lang->getCode() === $this->mCode;
4387 }
4388
4397 public function getCode() {
4398 return $this->mCode;
4399 }
4400
4411 public function getHtmlCode() {
4412 if ( is_null( $this->mHtmlCode ) ) {
4413 $this->mHtmlCode = LanguageCode::bcp47( $this->getCode() );
4414 }
4415 return $this->mHtmlCode;
4416 }
4417
4422 public function setCode( $code ) {
4423 wfDeprecated( __METHOD__, '1.32' );
4424 $this->mCode = $code;
4425 // Ensure we don't leave incorrect cached data lying around
4426 $this->mHtmlCode = null;
4427 $this->mParentLanguage = false;
4428 }
4429
4437 public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
4438 $m = null;
4439 preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
4440 preg_quote( $suffix, '/' ) . '/', $filename, $m );
4441 if ( !count( $m ) ) {
4442 return false;
4443 }
4444 return str_replace( '_', '-', strtolower( $m[1] ) );
4445 }
4446
4452 public static function classFromCode( $code, $fallback = true ) {
4453 if ( $fallback && $code == 'en' ) {
4454 return 'Language';
4455 } else {
4456 return 'Language' . str_replace( '-', '_', ucfirst( $code ) );
4457 }
4458 }
4459
4468 public static function getFileName( $prefix, $code, $suffix = '.php' ) {
4469 if ( !self::isValidBuiltInCode( $code ) ) {
4470 throw new MWException( "Invalid language code \"$code\"" );
4471 }
4472
4473 return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
4474 }
4475
4480 public static function getMessagesFileName( $code ) {
4481 global $IP;
4482 $file = self::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
4483 Hooks::run( 'Language::getMessagesFileName', [ $code, &$file ] );
4484 return $file;
4485 }
4486
4493 public static function getJsonMessagesFileName( $code ) {
4494 global $IP;
4495
4496 if ( !self::isValidBuiltInCode( $code ) ) {
4497 throw new MWException( "Invalid language code \"$code\"" );
4498 }
4499
4500 return "$IP/languages/i18n/$code.json";
4501 }
4502
4510 public static function getFallbackFor( $code ) {
4511 $fallbacks = self::getFallbacksFor( $code );
4512 if ( $fallbacks ) {
4513 return $fallbacks[0];
4514 }
4515 return false;
4516 }
4517
4528 public static function getFallbacksFor( $code, $mode = self::MESSAGES_FALLBACKS ) {
4529 if ( $code === 'en' || !self::isValidBuiltInCode( $code ) ) {
4530 return [];
4531 }
4532 switch ( $mode ) {
4533 case self::MESSAGES_FALLBACKS:
4534 // For unknown languages, fallbackSequence returns an empty array,
4535 // hardcode fallback to 'en' in that case as English messages are
4536 // always defined.
4537 return self::getLocalisationCache()->getItem( $code, 'fallbackSequence' ) ?: [ 'en' ];
4538 case self::STRICT_FALLBACKS:
4539 // Use this mode when you don't want to fallback to English unless
4540 // explicitly defined, for example when you have language-variant icons
4541 // and an international language-independent fallback.
4542 return self::getLocalisationCache()->getItem( $code, 'originalFallbackSequence' );
4543 default:
4544 throw new MWException( "Invalid fallback mode \"$mode\"" );
4545 }
4546 }
4547
4556 public static function getFallbacksIncludingSiteLanguage( $code ) {
4557 global $wgLanguageCode;
4558
4559 // Usually, we will only store a tiny number of fallback chains, so we
4560 // keep them in static memory.
4561 $cacheKey = "{$code}-{$wgLanguageCode}";
4562
4563 if ( !array_key_exists( $cacheKey, self::$fallbackLanguageCache ) ) {
4564 $fallbacks = self::getFallbacksFor( $code );
4565
4566 // Append the site's fallback chain, including the site language itself
4567 $siteFallbacks = self::getFallbacksFor( $wgLanguageCode );
4568 array_unshift( $siteFallbacks, $wgLanguageCode );
4569
4570 // Eliminate any languages already included in the chain
4571 $siteFallbacks = array_diff( $siteFallbacks, $fallbacks );
4572
4573 self::$fallbackLanguageCache[$cacheKey] = [ $fallbacks, $siteFallbacks ];
4574 }
4575 return self::$fallbackLanguageCache[$cacheKey];
4576 }
4577
4587 public static function getMessagesFor( $code ) {
4588 return self::getLocalisationCache()->getItem( $code, 'messages' );
4589 }
4590
4599 public static function getMessageFor( $key, $code ) {
4600 return self::getLocalisationCache()->getSubitem( $code, 'messages', $key );
4601 }
4602
4611 public static function getMessageKeysFor( $code ) {
4612 return self::getLocalisationCache()->getSubitemList( $code, 'messages' );
4613 }
4614
4619 function fixVariableInNamespace( $talk ) {
4620 if ( strpos( $talk, '$1' ) === false ) {
4621 return $talk;
4622 }
4623
4624 global $wgMetaNamespace;
4625 $talk = str_replace( '$1', $wgMetaNamespace, $talk );
4626
4627 # Allow grammar transformations
4628 # Allowing full message-style parsing would make simple requests
4629 # such as action=raw much more expensive than they need to be.
4630 # This will hopefully cover most cases.
4631 $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
4632 [ $this, 'replaceGrammarInNamespace' ], $talk );
4633 return str_replace( ' ', '_', $talk );
4634 }
4635
4641 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
4642 }
4643
4654 public function formatExpiry( $expiry, $format = true, $infinity = 'infinity' ) {
4655 static $dbInfinity;
4656 if ( $dbInfinity === null ) {
4657 $dbInfinity = wfGetDB( DB_REPLICA )->getInfinity();
4658 }
4659
4660 if ( $expiry == '' || $expiry === 'infinity' || $expiry == $dbInfinity ) {
4661 return $format === true
4662 ? $this->getMessageFromDB( 'infiniteblock' )
4663 : $infinity;
4664 } else {
4665 return $format === true
4666 ? $this->timeanddate( $expiry, /* User preference timezone */ true )
4667 : wfTimestamp( $format, $expiry );
4668 }
4669 }
4670
4684 function formatTimePeriod( $seconds, $format = [] ) {
4685 if ( !is_array( $format ) ) {
4686 $format = [ 'avoid' => $format ]; // For backwards compatibility
4687 }
4688 if ( !isset( $format['avoid'] ) ) {
4689 $format['avoid'] = false;
4690 }
4691 if ( !isset( $format['noabbrevs'] ) ) {
4692 $format['noabbrevs'] = false;
4693 }
4694 $secondsMsg = wfMessage(
4695 $format['noabbrevs'] ? 'seconds' : 'seconds-abbrev' )->inLanguage( $this );
4696 $minutesMsg = wfMessage(
4697 $format['noabbrevs'] ? 'minutes' : 'minutes-abbrev' )->inLanguage( $this );
4698 $hoursMsg = wfMessage(
4699 $format['noabbrevs'] ? 'hours' : 'hours-abbrev' )->inLanguage( $this );
4700 $daysMsg = wfMessage(
4701 $format['noabbrevs'] ? 'days' : 'days-abbrev' )->inLanguage( $this );
4702
4703 if ( round( $seconds * 10 ) < 100 ) {
4704 $s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
4705 $s = $secondsMsg->params( $s )->text();
4706 } elseif ( round( $seconds ) < 60 ) {
4707 $s = $this->formatNum( round( $seconds ) );
4708 $s = $secondsMsg->params( $s )->text();
4709 } elseif ( round( $seconds ) < 3600 ) {
4710 $minutes = floor( $seconds / 60 );
4711 $secondsPart = round( fmod( $seconds, 60 ) );
4712 if ( $secondsPart == 60 ) {
4713 $secondsPart = 0;
4714 $minutes++;
4715 }
4716 $s = $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4717 $s .= ' ';
4718 $s .= $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4719 } elseif ( round( $seconds ) <= 2 * 86400 ) {
4720 $hours = floor( $seconds / 3600 );
4721 $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
4722 $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
4723 if ( $secondsPart == 60 ) {
4724 $secondsPart = 0;
4725 $minutes++;
4726 }
4727 if ( $minutes == 60 ) {
4728 $minutes = 0;
4729 $hours++;
4730 }
4731 $s = $hoursMsg->params( $this->formatNum( $hours ) )->text();
4732 $s .= ' ';
4733 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4734 if ( !in_array( $format['avoid'], [ 'avoidseconds', 'avoidminutes' ] ) ) {
4735 $s .= ' ' . $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4736 }
4737 } else {
4738 $days = floor( $seconds / 86400 );
4739 if ( $format['avoid'] === 'avoidminutes' ) {
4740 $hours = round( ( $seconds - $days * 86400 ) / 3600 );
4741 if ( $hours == 24 ) {
4742 $hours = 0;
4743 $days++;
4744 }
4745 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4746 $s .= ' ';
4747 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4748 } elseif ( $format['avoid'] === 'avoidseconds' ) {
4749 $hours = floor( ( $seconds - $days * 86400 ) / 3600 );
4750 $minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
4751 if ( $minutes == 60 ) {
4752 $minutes = 0;
4753 $hours++;
4754 }
4755 if ( $hours == 24 ) {
4756 $hours = 0;
4757 $days++;
4758 }
4759 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4760 $s .= ' ';
4761 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4762 $s .= ' ';
4763 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4764 } else {
4765 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4766 $s .= ' ';
4767 $s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
4768 }
4769 }
4770 return $s;
4771 }
4772
4784 function formatBitrate( $bps ) {
4785 return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" );
4786 }
4787
4794 function formatComputingNumbers( $size, $boundary, $messageKey ) {
4795 if ( $size <= 0 ) {
4796 return str_replace( '$1', $this->formatNum( $size ),
4797 $this->getMessageFromDB( str_replace( '$1', '', $messageKey ) )
4798 );
4799 }
4800 $sizes = [ '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zeta', 'yotta' ];
4801 $index = 0;
4802
4803 $maxIndex = count( $sizes ) - 1;
4804 while ( $size >= $boundary && $index < $maxIndex ) {
4805 $index++;
4806 $size /= $boundary;
4807 }
4808
4809 // For small sizes no decimal places necessary
4810 $round = 0;
4811 if ( $index > 1 ) {
4812 // For MB and bigger two decimal places are smarter
4813 $round = 2;
4814 }
4815 $msg = str_replace( '$1', $sizes[$index], $messageKey );
4816
4817 $size = round( $size, $round );
4818 $text = $this->getMessageFromDB( $msg );
4819 return str_replace( '$1', $this->formatNum( $size ), $text );
4820 }
4821
4832 function formatSize( $size ) {
4833 return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" );
4834 }
4835
4845 function specialList( $page, $details, $oppositedm = true ) {
4846 if ( !$details ) {
4847 return $page;
4848 }
4849
4850 $dirmark = ( $oppositedm ? $this->getDirMark( true ) : '' ) . $this->getDirMark();
4851 return $page .
4852 $dirmark .
4853 $this->msg( 'word-separator' )->escaped() .
4854 $this->msg( 'parentheses' )->rawParams( $details )->escaped();
4855 }
4856
4869 public function viewPrevNext( Title $title, $offset, $limit,
4870 array $query = [], $atend = false
4871 ) {
4872 // @todo FIXME: Why on earth this needs one message for the text and another one for tooltip?
4873
4874 # Make 'previous' link
4875 $prev = wfMessage( 'prevn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4876 if ( $offset > 0 ) {
4877 $plink = $this->numLink( $title, max( $offset - $limit, 0 ), $limit,
4878 $query, $prev, 'prevn-title', 'mw-prevlink' );
4879 } else {
4880 $plink = htmlspecialchars( $prev );
4881 }
4882
4883 # Make 'next' link
4884 $next = wfMessage( 'nextn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4885 if ( $atend ) {
4886 $nlink = htmlspecialchars( $next );
4887 } else {
4888 $nlink = $this->numLink( $title, $offset + $limit, $limit,
4889 $query, $next, 'nextn-title', 'mw-nextlink' );
4890 }
4891
4892 # Make links to set number of items per page
4893 $numLinks = [];
4894 foreach ( [ 20, 50, 100, 250, 500 ] as $num ) {
4895 $numLinks[] = $this->numLink( $title, $offset, $num,
4896 $query, $this->formatNum( $num ), 'shown-title', 'mw-numlink' );
4897 }
4898
4899 return wfMessage( 'viewprevnext' )->inLanguage( $this )->title( $title
4900 )->rawParams( $plink, $nlink, $this->pipeList( $numLinks ) )->escaped();
4901 }
4902
4915 private function numLink( Title $title, $offset, $limit, array $query, $link,
4916 $tooltipMsg, $class
4917 ) {
4918 $query = [ 'limit' => $limit, 'offset' => $offset ] + $query;
4919 $tooltip = wfMessage( $tooltipMsg )->inLanguage( $this )->title( $title )
4920 ->numParams( $limit )->text();
4921
4922 return Html::element( 'a', [ 'href' => $title->getLocalURL( $query ),
4923 'title' => $tooltip, 'class' => $class ], $link );
4924 }
4925
4931 public function getConvRuleTitle() {
4932 return $this->mConverter->getConvRuleTitle();
4933 }
4934
4940 public function getCompiledPluralRules() {
4941 $pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'compiledPluralRules' );
4942 $fallbacks = self::getFallbacksFor( $this->mCode );
4943 if ( !$pluralRules ) {
4944 foreach ( $fallbacks as $fallbackCode ) {
4945 $pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
4946 if ( $pluralRules ) {
4947 break;
4948 }
4949 }
4950 }
4951 return $pluralRules;
4952 }
4953
4959 public function getPluralRules() {
4960 $pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRules' );
4961 $fallbacks = self::getFallbacksFor( $this->mCode );
4962 if ( !$pluralRules ) {
4963 foreach ( $fallbacks as $fallbackCode ) {
4964 $pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRules' );
4965 if ( $pluralRules ) {
4966 break;
4967 }
4968 }
4969 }
4970 return $pluralRules;
4971 }
4972
4978 public function getPluralRuleTypes() {
4979 $pluralRuleTypes = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRuleTypes' );
4980 $fallbacks = self::getFallbacksFor( $this->mCode );
4981 if ( !$pluralRuleTypes ) {
4982 foreach ( $fallbacks as $fallbackCode ) {
4983 $pluralRuleTypes = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRuleTypes' );
4984 if ( $pluralRuleTypes ) {
4985 break;
4986 }
4987 }
4988 }
4989 return $pluralRuleTypes;
4990 }
4991
4997 public function getPluralRuleIndexNumber( $number ) {
4998 $pluralRules = $this->getCompiledPluralRules();
4999 $form = Evaluator::evaluateCompiled( $number, $pluralRules );
5000 return $form;
5001 }
5002
5011 public function getPluralRuleType( $number ) {
5012 $index = $this->getPluralRuleIndexNumber( $number );
5013 $pluralRuleTypes = $this->getPluralRuleTypes();
5014 return $pluralRuleTypes[$index] ?? 'other';
5015 }
5016}
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
$wgLanguageCode
Site language code.
$wgExtraGenderNamespaces
Same as above, but for namespaces with gender distinction.
$wgTranslateNumerals
For Hindi and Arabic use local numerals instead of Western style (0-9) numerals in interface.
$wgGrammarForms
Some languages need different word forms, usually for different cases.
$wgExtraNamespaces
Additional namespaces.
$wgAllUnicodeFixes
Set this to always convert certain Unicode sequences to modern ones regardless of the content languag...
$wgDummyLanguageCodes
Functionally the same as $wgExtraLanguageCodes, but deprecated.
$wgNamespaceAliases
Namespace aliases.
$wgMetaNamespace
Name of the project namespace.
$wgMetaNamespaceTalk
Name of the project talk namespace.
$wgLocalTZoffset
Set an offset from UTC in minutes to use for the default timezone setting for anonymous users and new...
$wgLocalisationCacheConf
Localisation cache configuration.
$wgLangObjCacheSize
Language cache size, or really how many languages can we handle simultaneously without degrading to c...
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
wfIsInfinity( $str)
Determine input string is represents as infinity.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
$fallback
$namespaceAliases
$namespaceNames
$digitGroupingPattern
$minimumGroupingDigits
$IP
Definition WebStart.php:41
A fake language variant converter.
Simple store for keeping values in an associative array for the current process.
Base class for language conversion.
Internationalisation code.
Definition Language.php:36
const STRICT_FALLBACKS
Return a strict fallback chain in getFallbacksFor.
Definition Language.php:94
const MESSAGES_FALLBACKS
Return a fallback chain for messages in getFallbacksFor.
Definition Language.php:88
hasVariants()
Check if this is a language with variants.
initContLang()
Hook which will be called if this is the content language.
Definition Language.php:480
static $mWeekdayMsgs
Definition Language.php:96
getLocalNsIndex( $text)
Get a namespace key by value, case insensitive.
Definition Language.php:654
date( $ts, $adj=false, $format=true, $timecorrection=false)
linkTrail()
A regular expression to match legal word-trailing characters which should be merged onto a link of th...
static $strongDirRegex
Directionality test regex for embedBidi().
Definition Language.php:206
formatExpiry( $expiry, $format=true, $infinity='infinity')
Decode an expiry (block, protection, etc) which has come from the DB.
getPluralRuleTypes()
Get the plural rule types for the language.
static tsToIranian( $ts)
Algorithm by Roozbeh Pournader and Mohammad Toossi to convert Gregorian dates to Iranian dates.
getDefaultVariant()
convertCategoryKey( $key)
array null $namespaceNames
Definition Language.php:69
static getMessageKeysFor( $code)
Get all message keys for a given language.
static isWellFormedLanguageTag( $code, $lenient=false)
Returns true if a language code string is a well-formed language tag according to RFC 5646.
Definition Language.php:333
getVariantname( $code, $usemsg=true)
short names for language variants used for language conversion links.
Definition Language.php:762
static $mHebrewCalendarMonthGenMsgs
Definition Language.php:135
static isValidBuiltInCode( $code)
Returns true if a language code is of a valid form for the purposes of internal customisation of Medi...
Definition Language.php:412
recodeForEdit( $s)
$mParentLanguage
Definition Language.php:63
getWeekdayName( $key)
getHebrewCalendarMonthName( $key)
static getMessageFor( $key, $code)
Get a message for a given language.
gender( $gender, $forms)
Provides an alternative text depending on specified gender.
static tsToYear( $ts, $cName)
Algorithm to convert Gregorian dates to Thai solar dates, Minguo dates or Minguo dates.
truncateForVisual( $string, $length, $ellipsis='...', $adjustLength=true)
Truncate a string to a specified number of characters, appending an optional string (e....
ucwords( $str)
formatNum( $number, $nocommafy=false)
Normally we output all numbers in plain en_US style, that is 293,291.235 for twohundredninetythreetho...
static clearCaches()
Intended for tests that may change configuration in a way that invalidates caches.
Definition Language.php:284
static $pdf
Definition Language.php:191
digitGroupingPattern()
static array $fallbackLanguageCache
Cache for language fallbacks.
Definition Language.php:172
getMonthAbbreviation( $key)
Definition Language.php:994
getHebrewCalendarMonthNameGen( $key)
static $lre
Unicode directional formatting characters, for embedBidi()
Definition Language.php:189
getSpecialPageAliases()
Get special page names, as an associative array canonical name => array of valid names,...
getParsedTitle()
For languages that support multiple variants, the title of an article may be displayed differently in...
static isSupportedLanguage( $code)
Checks whether any localisation is available for that language tag in MediaWiki (MessagesXx....
Definition Language.php:305
hasVariant( $variant)
Strict check if the language has the specific variant.
setCode( $code)
static classFromCode( $code, $fallback=true)
transformUsingPairFile( $file, $string)
Transform a string using serialized data stored in the given file (which must be in the serialized su...
formatDuration( $seconds, array $chosenIntervals=[])
Takes a number of seconds and turns it into a text using values such as hours and minutes.
getMessage( $key)
getHijriCalendarMonthName( $key)
const ALL
Return all known languages in fetchLanguageName(s).
Definition Language.php:47
pipeList(array $list)
Same as commaList, but separate it with the pipe instead.
internalUserTimeAndDate( $type, $ts, User $user, array $options)
Internal helper function for userDate(), userTime() and userTimeAndDate()
ucwordbreaksCallbackAscii( $matches)
lc( $str, $first=false)
fallback8bitEncoding()
static LocalisationCache $dataCache
Definition Language.php:80
getPluralRuleType( $number)
Find the plural rule type appropriate for the given number For example, if the language is set to Ara...
normalize( $s)
Convert a UTF-8 string to normal form C.
userAdjust( $ts, $tz=false)
Used by date() and time() to adjust the time output.
getArrow( $direction='forwards')
An arrow, depending on the language direction.
equals(Language $lang)
Compare with an other language object.
ucwordbreaksCallbackMB( $matches)
convert( $text)
convert text to different variants of a language.
getParentLanguage()
Get the "parent" language which has a converter to convert a "compatible" language (in another varian...
getGenderNsText( $index, $gender)
Returns gender-dependent namespace alias if available.
Definition Language.php:615
translateBlockExpiry( $str, User $user=null, $now=0)
truncateHtml( $text, $length, $ellipsis='...')
Truncate a string of valid HTML to a specified length in bytes, appending an optional string (e....
getAllMessages()
getGrammarTransformations()
Get the grammar transformations data for the language.
__destruct()
Reduce memory usage.
Definition Language.php:470
getFormattedNsText( $index)
A convenience function that returns the same thing as getNsText() except with '_' changed to ' ',...
Definition Language.php:602
getURLVariant()
getMessageFromDB( $msg)
Get a message from the MediaWiki namespace.
Definition Language.php:949
capitalizeAllNouns()
getDirMark( $opposite=false)
A hidden direction mark (LRM or RLM), depending on the language direction.
truncate_skip(&$ret, $text, $search, $start, $len=null)
truncateHtml() helper function like strcspn() but adds the skipped chars to $ret
__construct()
Definition Language.php:456
getFormattedNamespaces()
A convenience function that returns getNamespaces() with spaces instead of underscores in values.
Definition Language.php:565
getExtraHashOptions()
returns language specific options used by User::getPageRenderHash() for example, the preferred langua...
$transformData
ReplacementArray object caches.
Definition Language.php:75
getMonthNamesArray()
Definition Language.php:974
userTimeAndDate( $ts, User $user, array $options=[])
Get the formatted date and time for the given timestamp and formatted for the given user.
getMonthNameGen( $key)
Definition Language.php:986
setNamespaces(array $namespaces)
Arbitrarily set all of the namespace names at once.
Definition Language.php:545
static $GREG_DAYS
commafy( $number)
Adds commas to a given number.
truncate_endBracket(&$tag, $tagType, $lastCh, &$openTags)
truncateHtml() helper function (a) push or pop $tag from $openTags as needed (b) clear $tag value
firstChar( $s)
Get the first character of a string.
$mExtendedSpecialPageAliases
Definition Language.php:66
getGrammarForms()
Get the grammar forms for the content language.
static getFallbacksFor( $code, $mode=self::MESSAGES_FALLBACKS)
Get the ordered list of fallback languages.
getNamespaces()
Returns an array of localised namespaces indexed by their numbers.
Definition Language.php:505
static convertDoubleWidth( $string)
convert double-width roman characters to single-width.
static tsToHebrew( $ts)
Converting Gregorian dates to Hebrew dates.
specialList( $page, $details, $oppositedm=true)
Make a list item, used by various special pages.
getHtmlCode()
Get the code in BCP 47 format which we can use inside of html lang="" tags.
needsGenderDistinction()
Whether this language uses gender-dependent namespace aliases.
Definition Language.php:630
static array $durationIntervals
Definition Language.php:154
getNsIndex( $text)
Get a namespace key by value, case insensitive.
Definition Language.php:745
linkPrefixCharset()
A regular expression character set to match legal word-prefixing characters which should be merged on...
static $IRANIAN_DAYS
listToText(array $list)
Take a list of strings and build a locale-friendly comma-separated list, using the local comma-separa...
LanguageConverter $mConverter
Definition Language.php:59
caseFold( $s)
Return a case-folded representation of $s.
truncateInternal( $string, $length, $ellipsis, $adjustLength, callable $measureLength, callable $getSubstring)
Internal method used for truncation.
static hebrewNumeral( $num)
Hebrew Gematria number formatting up to 9999.
getCode()
Get the internal language code for this language object.
static $mIranianCalendarMonthMsgs
Definition Language.php:120
uc( $str, $first=false)
Convert a string to uppercase.
static $mHebrewCalendarMonthMsgs
Definition Language.php:127
normalizeForSearch( $string)
Some languages have special punctuation need to be normalized.
truncateForDatabase( $string, $length, $ellipsis='...', $adjustLength=true)
Truncate a string to a specified length in bytes, appending an optional string (e....
static newFromCode( $code, $fallback=false)
Create a language object for a given language code.
Definition Language.php:240
embedBidi( $text='')
Wraps argument with unicode control characters for directionality safety.
getMonthAbbreviationsArray()
static fetchLanguageName( $code, $inLanguage=self::AS_AUTONYMS, $include=self::ALL)
Definition Language.php:933
getMagic( $mw)
Fill a MagicWord object with data from here.
viewPrevNext(Title $title, $offset, $limit, array $query=[], $atend=false)
Generate (prev x| next x) (20|50|100...) type links for paging.
resetNamespaces()
Resets all of the namespace caches.
Definition Language.php:553
getMagicWords()
Get all magic words from cache.
minimumGroupingDigits()
emphasize( $text)
Italic is unsuitable for some languages.
timeanddate( $ts, $adj=false, $format=true, $timecorrection=false)
static getFallbacksIncludingSiteLanguage( $code)
Get the ordered list of fallback languages, ending with the fallback language chain for the site lang...
convertForSearchResult( $termsArray)
sprintfDate( $format, $ts, DateTimeZone $zone=null, &$ttl='unused')
This is a workalike of PHP's date() function, but with better internationalisation,...
static $mMonthGenMsgs
Definition Language.php:110
getPreferredVariant()
getDirMarkEntity( $opposite=false)
A hidden direction mark (LRM or RLM), depending on the language direction.
convertGrammar( $word, $case)
Grammatical transformations, needed for inflected languages Invoked by putting {{grammar:case|word}} ...
static HashBagOStuff null $languageNameCache
Cache for language names.
Definition Language.php:184
autoConvert( $text, $variant=false)
convert text to a variant
getIranianCalendarMonthName( $key)
getHumanTimestampInternal(MWTimestamp $ts, MWTimestamp $relativeTo, User $user)
Convert an MWTimestamp into a pretty human-readable timestamp using the given user preferences and re...
getHumanTimestamp(MWTimestamp $time, MWTimestamp $relativeTo=null, User $user=null)
Get the timestamp in a human-friendly relative format, e.g., "3 days ago".
convertNamespace( $ns, $variant=null)
Convert a namespace index to a string in the preferred variant.
preConvertPlural($forms, $count)
Checks that convertPlural was given an array and pads it to requested amount of forms by copying the ...
formatTimePeriod( $seconds, $format=[])
Formats a time given in seconds into a string representation of that time.
static getFallbackFor( $code)
Get the first fallback for a given language.
getWeekdayAbbreviation( $key)
isRTL()
For right-to-left language support.
separatorTransformTable()
segmentForDiff( $text)
languages like Chinese need to be segmented in order for the diff to be of any use
static $mMonthAbbrevMsgs
Definition Language.php:115
replaceGrammarInNamespace( $m)
getBookstoreList()
Exports $wgBookstoreListEn.
Definition Language.php:495
getDir()
Return the correct HTML 'dir' attribute value for this language.
parseFormattedNumber( $number)
static factory( $code)
Get a cached or new language object for a given language code.
Definition Language.php:215
$mMagicExtensions
Definition Language.php:62
addMagicWordsByLang( $newWords)
Add magic words to the extension array.
linkPrefixExtension()
To allow "foo[[bar]]" to extend the link over the whole word "foobar".
getFallbackLanguages()
Definition Language.php:487
userDate( $ts, User $user, array $options=[])
Get the formatted date for the given timestamp and formatted for the given user.
recodeInput( $s)
getMonthName( $key)
Definition Language.php:967
getPluralRules()
Get the plural rules for the language.
autoConvertToAllVariants( $text)
convert text to all supported variants
const AS_AUTONYMS
Return autonyms in fetchLanguageName(s).
Definition Language.php:41
iconv( $in, $out, $string)
formatSize( $size)
Format a size in bytes for output, using an appropriate unit (B, KB, MB, GB, TB, PB,...
semicolonList(array $list)
Take a list of strings and build a locale-friendly semicolon-separated list, using the local semicolo...
fixVariableInNamespace( $talk)
static $rle
Definition Language.php:190
static insertSpace( $string, $pattern)
commaList(array $list)
Take a list of strings and build a locale-friendly comma-separated list, using the local comma-separa...
dateFormat( $usePrefs=true)
This is meant to be used by time(), date(), and timeanddate() to get the date preference they're supp...
static getMessagesFor( $code)
Get all messages for a given language WARNING: this may take a long time.
findVariantLink(&$link, &$nt, $ignoreOtherCond=false)
If a language supports multiple variants, it is possible that non-existing link in one variant actual...
const SUPPORTED
Return in fetchLanguageName(s) only the languages for which we have at least some localisation.
Definition Language.php:54
handleExplicitPluralForms( $count, array $forms)
Handles explicit plural forms for Language::convertPlural()
static dateTimeObjFormat(&$dateTimeObj, $ts, $zone, $code)
Pass through result from $dateTimeObj->format()
alignEnd()
Return 'right' or 'left' as appropriate alignment for line-end for this language's text direction.
getDateFormatString( $type, $pref)
Get a format string for a given type and preference.
convertPlural( $count, $forms)
Plural form transformations, needed for some languages.
getDurationIntervals( $seconds, array $chosenIntervals=[])
Takes a number of seconds and returns an array with a set of corresponding intervals.
static getLocalisationCache()
Get the LocalisationCache instance.
Definition Language.php:447
static $mLangObjCache
Definition Language.php:82
static getFileName( $prefix, $code, $suffix='.php')
Get the name of a file for a certain language code.
formatNumNoSeparators( $number)
Front-end for non-commafied formatNum.
static isKnownLanguageTag( $tag)
Returns true if a language code is an IETF tag known to MediaWiki.
Definition Language.php:426
msg( $msg)
Get message object in this language.
Definition Language.php:959
removeBadCharLast( $string)
Remove bytes that represent an incomplete Unicode character at the end of string (e....
static getJsonMessagesFileName( $code)
getNamespaceAliases()
Definition Language.php:663
ucwordbreaks( $str)
capitalize words at word breaks
ucfirst( $str)
Make a string's first character uppercase.
static strongDirFromContent( $text='')
Gets directionality of the first strongly directional codepoint, for embedBidi()
userTime( $ts, User $user, array $options=[])
Get the formatted time for the given timestamp and formatted for the given user.
getNsText( $index)
Get a namespace value by key.
Definition Language.php:584
hasWordBreaks()
Most writing systems use whitespace to break up words.
getConvRuleTitle()
Get the conversion rule title, if any.
formatBitrate( $bps)
Format a bitrate for output, using an appropriate unit (bps, kbps, Mbps, Gbps, Tbps,...
convertTitle( $title)
Convert a Title object to a string in the preferred variant.
numLink(Title $title, $offset, $limit, array $query, $link, $tooltipMsg, $class)
Helper function for viewPrevNext() that generates links.
static getMessagesFileName( $code)
getNamespaceIds()
Definition Language.php:715
static MapCacheLRU null $grammarTransformations
Cache for grammar rules data.
Definition Language.php:178
getVariants()
Get the list of variants supported by this language see sample implementation in LanguageZh....
time( $ts, $adj=false, $format=true, $timecorrection=false)
static getCodeFromFileName( $filename, $prefix='Language', $suffix='.php')
Get the language code from a file name.
getDefaultDateFormat()
Definition Language.php:793
removeBadCharFirst( $string)
Remove bytes that represent an incomplete Unicode character at the start of string (e....
static $mHijriCalendarMonthMsgs
Definition Language.php:143
static $mWeekdayAbbrevMsgs
Definition Language.php:101
unsegmentForDiff( $text)
and unsegment to show the result
formatComputingNumbers( $size, $boundary, $messageKey)
ucwordsCallbackMB( $matches)
isMultibyte( $str)
lcfirst( $str)
getPluralRuleIndexNumber( $number)
Find the index number of the plural rule appropriate for the given number.
updateConversionTable(Title $title)
Refresh the cache of conversion tables when MediaWiki:Conversiontable* is updated.
segmentByWord( $string)
Some languages such as Chinese require word segmentation, Specify such segmentation when overridden i...
checkTitleEncoding( $s)
TODO: $s is not always a string per T218883.
convertHtml( $text)
Perform output conversion on a string, and encode for safe HTML output.
getConverter()
Return the LanguageConverter used in the Language.
static tsToHijri( $ts)
Converting Gregorian dates to Hijri dates.
digitTransformTable()
static isValidCode( $code)
Returns true if a language code string is of a valid form, whether or not it exists.
Definition Language.php:387
static $mMonthMsgs
Definition Language.php:105
$dateFormatStrings
Definition Language.php:65
static romanNumeral( $num)
Roman number formatting up to 10000.
alignStart()
Return 'left' or 'right' as appropriate alignment for line-start for this language's text direction.
static hebrewYearStart( $year)
This calculates the Hebrew year start, as days since 1 September.
getCompiledPluralRules()
Get the compiled plural rules for the language.
Class for caching the contents of localisation files, Messages*.php and *.i18n.php.
MediaWiki exception.
Library for creating and parsing MW-style timestamps.
Handles a simple LRU key/value map with a maximum number of entries.
static getTitleInvalidRegex()
Returns a simple regex that will match on characters and sequences invalid in titles.
Wrapper around strtr() that holds replacements.
static getSuggestedDurations(Language $lang=null, $includeOther=true)
Get an array of suggested block durations from MediaWiki:Ipboptions.
static isUtf8( $value)
Test whether a string is valid UTF-8.
Represents a title within MediaWiki.
Definition Title.php:40
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:48
static getDefaultOption( $opt)
Get a given default option value.
Definition User.php:1818
=Architecture==Two class hierarchies are used to provide the functionality associated with the different content models:*Content interface(and AbstractContent base class) define functionality that acts on the concrete content of a page, and *ContentHandler base class provides functionality specific to a content model, but not acting on concrete content. The most important function of ContentHandler is to act as a factory for the appropriate implementation of Content. These Content objects are to be used by MediaWiki everywhere, instead of passing page content around as text. All manipulation and analysis of page content must be done via the appropriate methods of the Content object. For each content model, a subclass of ContentHandler has to be registered with $wgContentHandlers. The ContentHandler object for a given content model can be obtained using ContentHandler::getForModelID($id). Also Title, WikiPage and Revision now have getContentHandler() methods for convenience. ContentHandler objects are singletons that provide functionality specific to the content type, but not directly acting on the content of some page. ContentHandler::makeEmptyContent() and ContentHandler::unserializeContent() can be used to create a Content object of the appropriate type. However, it is recommended to instead use WikiPage::getContent() resp. Revision::getContent() to get a page 's content as a Content object. These two methods should be the ONLY way in which page content is accessed. Another important function of ContentHandler objects is to define custom action handlers for a content model, see ContentHandler::getActionOverrides(). This is similar to what WikiPage::getActionOverrides() was already doing.==Serialization==With the ContentHandler facility, page content no longer has to be text based. Objects implementing the Content interface are used to represent and handle the content internally. For storage and data exchange, each content model supports at least one serialization format via ContentHandler::serializeContent($content). The list of supported formats for a given content model can be accessed using ContentHandler::getSupportedFormats(). Content serialization formats are identified using MIME type like strings. The following formats are built in:*text/x-wiki - wikitext *text/javascript - for js pages *text/css - for css pages *text/plain - for future use, e.g. with plain text messages. *text/html - for future use, e.g. with plain html messages. *application/vnd.php.serialized - for future use with the api and for extensions *application/json - for future use with the api, and for use by extensions *application/xml - for future use with the api, and for use by extensions In PHP, use the corresponding CONTENT_FORMAT_XXX constant. Note that when using the API to access page content, especially action=edit, action=parse and action=query &prop=revisions, the model and format of the content should always be handled explicitly. Without that information, interpretation of the provided content is not reliable. The same applies to XML dumps generated via maintenance/dumpBackup.php or Special:Export. Also note that the API will provide encapsulated, serialized content - so if the API was called with format=json, and contentformat is also json(or rather, application/json), the page content is represented as a string containing an escaped json structure. Extensions that use JSON to serialize some types of page content may provide specialized API modules that allow access to that content in a more natural form.==Compatibility==The ContentHandler facility is introduced in a way that should allow all existing code to keep functioning at least for pages that contain wikitext or other text based content. However, a number of functions and hooks have been deprecated in favor of new versions that are aware of the page 's content model, and will now generate warnings when used. Most importantly, the following functions have been deprecated:*Revisions::getText() is deprecated in favor Revisions::getContent() *WikiPage::getText() is deprecated in favor WikiPage::getContent() Also, the old Article::getContent()(which returns text) is superceded by Article::getContentObject(). However, both methods should be avoided since they do not provide clean access to the page 's actual content. For instance, they may return a system message for non-existing pages. Use WikiPage::getContent() instead. Code that relies on a textual representation of the page content should eventually be rewritten. However, ContentHandler::getContentText() provides a stop-gap that can be used to get text for a page. Its behavior is controlled by $wgContentHandlerTextFallback it
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
globals txt Globals are evil The original MediaWiki code relied on globals for processing context far too often MediaWiki development since then has been a story of slowly moving context out of global variables and into objects Storing processing context in object member variables allows those objects to be reused in a much more flexible way Consider the elegance of
database rows
Definition globals.txt:10
const NS_USER
Definition Defines.php:75
const NS_PROJECT_TALK
Definition Defines.php:78
const NS_USER_TALK
Definition Defines.php:76
const NS_PROJECT
Definition Defines.php:77
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition hooks.txt:1802
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition hooks.txt:855
This code would result in ircNotify being run twice when an article is and once for brion Hooks can return three possible true was required This is the default since MediaWiki *some string
Definition hooks.txt:181
namespace and then decline to actually register it & $namespaces
Definition hooks.txt:925
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition hooks.txt:1999
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:955
Using a hook running we can avoid having all this option specific stuff in our mainline code Using the function We ve cleaned up the code here by removing clumps of infrequently used code and moving them off somewhere else It s much easier for someone working with this code to see what s _really_ going and make changes or fix bugs In we can take all the code that deals with the little used title reversing options(say) and put it in one place. Instead of having little title-reversing if-blocks spread all over the codebase in showAnArticle
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable & $code
Definition hooks.txt:856
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Definition hooks.txt:2004
if the prop value should be in the metadata multi language array format
Definition hooks.txt:1651
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition hooks.txt:2003
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt;div ...>$1&lt;/div>"). - flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException':Called before an exception(or PHP error) is logged. This is meant for integration with external error aggregation services
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition hooks.txt:3069
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:271
null for the local wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition hooks.txt:1617
return true to allow those checks to and false if checking is done & $user
Definition hooks.txt:1510
returning false will NOT prevent logging $e
Definition hooks.txt:2175
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring which defines all default service and specifies how they depend on each other("wiring"). When a new service is added to MediaWiki core
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition injection.txt:37
$cache
Definition mcc.php:33
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
A helper class for throttling authentication attempts.
const DB_REPLICA
Definition defines.php:25
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Definition router.php:42
if(!isset( $args[0])) $lang