MediaWiki REL1_31
Language.php
Go to the documentation of this file.
1<?php
29use CLDRPluralRuleParser\Evaluator;
30
35class Language {
40
41 public $mVariants, $mCode, $mLoaded = false;
42 public $mMagicExtensions = [], $mMagicHookDone = false;
43 private $mHtmlCode = null, $mParentLanguage = false;
44
45 public $dateFormatStrings = [];
47
49 protected $namespaceNames;
51
55 public $transformData = [];
56
60 static public $dataCache;
61
62 static public $mLangObjCache = [];
63
64 static public $mWeekdayMsgs = [
65 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
66 'friday', 'saturday'
67 ];
68
69 static public $mWeekdayAbbrevMsgs = [
70 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
71 ];
72
73 static public $mMonthMsgs = [
74 'january', 'february', 'march', 'april', 'may_long', 'june',
75 'july', 'august', 'september', 'october', 'november',
76 'december'
77 ];
78 static public $mMonthGenMsgs = [
79 'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
80 'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
81 'december-gen'
82 ];
83 static public $mMonthAbbrevMsgs = [
84 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
85 'sep', 'oct', 'nov', 'dec'
86 ];
87
89 'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
90 'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
91 'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
92 'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
93 ];
94
95 static public $mHebrewCalendarMonthMsgs = [
96 'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
97 'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
98 'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
99 'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
100 'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
101 ];
102
104 'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
105 'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
106 'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
107 'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
108 'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
109 ];
110
111 static public $mHijriCalendarMonthMsgs = [
112 'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
113 'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
114 'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
115 'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
116 ];
117
122 static public $durationIntervals = [
123 'millennia' => 31556952000,
124 'centuries' => 3155695200,
125 'decades' => 315569520,
126 'years' => 31556952, // 86400 * ( 365 + ( 24 * 3 + 25 ) / 400 )
127 'weeks' => 604800,
128 'days' => 86400,
129 'hours' => 3600,
130 'minutes' => 60,
131 'seconds' => 1,
132 ];
133
140 static private $fallbackLanguageCache = [];
141
147
152 static private $languageNameCache;
153
157 static private $lre = "\xE2\x80\xAA"; // U+202A LEFT-TO-RIGHT EMBEDDING
158 static private $rle = "\xE2\x80\xAB"; // U+202B RIGHT-TO-LEFT EMBEDDING
159 static private $pdf = "\xE2\x80\xAC"; // U+202C POP DIRECTIONAL FORMATTING
160
172 // @codeCoverageIgnoreStart
173 // phpcs:ignore Generic.Files.LineLength
174 static private $strongDirRegex = '/(?:([\x{41}-\x{5a}\x{61}-\x{7a}\x{aa}\x{b5}\x{ba}\x{c0}-\x{d6}\x{d8}-\x{f6}\x{f8}-\x{2b8}\x{2bb}-\x{2c1}\x{2d0}\x{2d1}\x{2e0}-\x{2e4}\x{2ee}\x{370}-\x{373}\x{376}\x{377}\x{37a}-\x{37d}\x{37f}\x{386}\x{388}-\x{38a}\x{38c}\x{38e}-\x{3a1}\x{3a3}-\x{3f5}\x{3f7}-\x{482}\x{48a}-\x{52f}\x{531}-\x{556}\x{559}-\x{55f}\x{561}-\x{587}\x{589}\x{903}-\x{939}\x{93b}\x{93d}-\x{940}\x{949}-\x{94c}\x{94e}-\x{950}\x{958}-\x{961}\x{964}-\x{980}\x{982}\x{983}\x{985}-\x{98c}\x{98f}\x{990}\x{993}-\x{9a8}\x{9aa}-\x{9b0}\x{9b2}\x{9b6}-\x{9b9}\x{9bd}-\x{9c0}\x{9c7}\x{9c8}\x{9cb}\x{9cc}\x{9ce}\x{9d7}\x{9dc}\x{9dd}\x{9df}-\x{9e1}\x{9e6}-\x{9f1}\x{9f4}-\x{9fa}\x{a03}\x{a05}-\x{a0a}\x{a0f}\x{a10}\x{a13}-\x{a28}\x{a2a}-\x{a30}\x{a32}\x{a33}\x{a35}\x{a36}\x{a38}\x{a39}\x{a3e}-\x{a40}\x{a59}-\x{a5c}\x{a5e}\x{a66}-\x{a6f}\x{a72}-\x{a74}\x{a83}\x{a85}-\x{a8d}\x{a8f}-\x{a91}\x{a93}-\x{aa8}\x{aaa}-\x{ab0}\x{ab2}\x{ab3}\x{ab5}-\x{ab9}\x{abd}-\x{ac0}\x{ac9}\x{acb}\x{acc}\x{ad0}\x{ae0}\x{ae1}\x{ae6}-\x{af0}\x{af9}\x{b02}\x{b03}\x{b05}-\x{b0c}\x{b0f}\x{b10}\x{b13}-\x{b28}\x{b2a}-\x{b30}\x{b32}\x{b33}\x{b35}-\x{b39}\x{b3d}\x{b3e}\x{b40}\x{b47}\x{b48}\x{b4b}\x{b4c}\x{b57}\x{b5c}\x{b5d}\x{b5f}-\x{b61}\x{b66}-\x{b77}\x{b83}\x{b85}-\x{b8a}\x{b8e}-\x{b90}\x{b92}-\x{b95}\x{b99}\x{b9a}\x{b9c}\x{b9e}\x{b9f}\x{ba3}\x{ba4}\x{ba8}-\x{baa}\x{bae}-\x{bb9}\x{bbe}\x{bbf}\x{bc1}\x{bc2}\x{bc6}-\x{bc8}\x{bca}-\x{bcc}\x{bd0}\x{bd7}\x{be6}-\x{bf2}\x{c01}-\x{c03}\x{c05}-\x{c0c}\x{c0e}-\x{c10}\x{c12}-\x{c28}\x{c2a}-\x{c39}\x{c3d}\x{c41}-\x{c44}\x{c58}-\x{c5a}\x{c60}\x{c61}\x{c66}-\x{c6f}\x{c7f}\x{c82}\x{c83}\x{c85}-\x{c8c}\x{c8e}-\x{c90}\x{c92}-\x{ca8}\x{caa}-\x{cb3}\x{cb5}-\x{cb9}\x{cbd}-\x{cc4}\x{cc6}-\x{cc8}\x{cca}\x{ccb}\x{cd5}\x{cd6}\x{cde}\x{ce0}\x{ce1}\x{ce6}-\x{cef}\x{cf1}\x{cf2}\x{d02}\x{d03}\x{d05}-\x{d0c}\x{d0e}-\x{d10}\x{d12}-\x{d3a}\x{d3d}-\x{d40}\x{d46}-\x{d48}\x{d4a}-\x{d4c}\x{d4e}\x{d57}\x{d5f}-\x{d61}\x{d66}-\x{d75}\x{d79}-\x{d7f}\x{d82}\x{d83}\x{d85}-\x{d96}\x{d9a}-\x{db1}\x{db3}-\x{dbb}\x{dbd}\x{dc0}-\x{dc6}\x{dcf}-\x{dd1}\x{dd8}-\x{ddf}\x{de6}-\x{def}\x{df2}-\x{df4}\x{e01}-\x{e30}\x{e32}\x{e33}\x{e40}-\x{e46}\x{e4f}-\x{e5b}\x{e81}\x{e82}\x{e84}\x{e87}\x{e88}\x{e8a}\x{e8d}\x{e94}-\x{e97}\x{e99}-\x{e9f}\x{ea1}-\x{ea3}\x{ea5}\x{ea7}\x{eaa}\x{eab}\x{ead}-\x{eb0}\x{eb2}\x{eb3}\x{ebd}\x{ec0}-\x{ec4}\x{ec6}\x{ed0}-\x{ed9}\x{edc}-\x{edf}\x{f00}-\x{f17}\x{f1a}-\x{f34}\x{f36}\x{f38}\x{f3e}-\x{f47}\x{f49}-\x{f6c}\x{f7f}\x{f85}\x{f88}-\x{f8c}\x{fbe}-\x{fc5}\x{fc7}-\x{fcc}\x{fce}-\x{fda}\x{1000}-\x{102c}\x{1031}\x{1038}\x{103b}\x{103c}\x{103f}-\x{1057}\x{105a}-\x{105d}\x{1061}-\x{1070}\x{1075}-\x{1081}\x{1083}\x{1084}\x{1087}-\x{108c}\x{108e}-\x{109c}\x{109e}-\x{10c5}\x{10c7}\x{10cd}\x{10d0}-\x{1248}\x{124a}-\x{124d}\x{1250}-\x{1256}\x{1258}\x{125a}-\x{125d}\x{1260}-\x{1288}\x{128a}-\x{128d}\x{1290}-\x{12b0}\x{12b2}-\x{12b5}\x{12b8}-\x{12be}\x{12c0}\x{12c2}-\x{12c5}\x{12c8}-\x{12d6}\x{12d8}-\x{1310}\x{1312}-\x{1315}\x{1318}-\x{135a}\x{1360}-\x{137c}\x{1380}-\x{138f}\x{13a0}-\x{13f5}\x{13f8}-\x{13fd}\x{1401}-\x{167f}\x{1681}-\x{169a}\x{16a0}-\x{16f8}\x{1700}-\x{170c}\x{170e}-\x{1711}\x{1720}-\x{1731}\x{1735}\x{1736}\x{1740}-\x{1751}\x{1760}-\x{176c}\x{176e}-\x{1770}\x{1780}-\x{17b3}\x{17b6}\x{17be}-\x{17c5}\x{17c7}\x{17c8}\x{17d4}-\x{17da}\x{17dc}\x{17e0}-\x{17e9}\x{1810}-\x{1819}\x{1820}-\x{1877}\x{1880}-\x{18a8}\x{18aa}\x{18b0}-\x{18f5}\x{1900}-\x{191e}\x{1923}-\x{1926}\x{1929}-\x{192b}\x{1930}\x{1931}\x{1933}-\x{1938}\x{1946}-\x{196d}\x{1970}-\x{1974}\x{1980}-\x{19ab}\x{19b0}-\x{19c9}\x{19d0}-\x{19da}\x{1a00}-\x{1a16}\x{1a19}\x{1a1a}\x{1a1e}-\x{1a55}\x{1a57}\x{1a61}\x{1a63}\x{1a64}\x{1a6d}-\x{1a72}\x{1a80}-\x{1a89}\x{1a90}-\x{1a99}\x{1aa0}-\x{1aad}\x{1b04}-\x{1b33}\x{1b35}\x{1b3b}\x{1b3d}-\x{1b41}\x{1b43}-\x{1b4b}\x{1b50}-\x{1b6a}\x{1b74}-\x{1b7c}\x{1b82}-\x{1ba1}\x{1ba6}\x{1ba7}\x{1baa}\x{1bae}-\x{1be5}\x{1be7}\x{1bea}-\x{1bec}\x{1bee}\x{1bf2}\x{1bf3}\x{1bfc}-\x{1c2b}\x{1c34}\x{1c35}\x{1c3b}-\x{1c49}\x{1c4d}-\x{1c7f}\x{1cc0}-\x{1cc7}\x{1cd3}\x{1ce1}\x{1ce9}-\x{1cec}\x{1cee}-\x{1cf3}\x{1cf5}\x{1cf6}\x{1d00}-\x{1dbf}\x{1e00}-\x{1f15}\x{1f18}-\x{1f1d}\x{1f20}-\x{1f45}\x{1f48}-\x{1f4d}\x{1f50}-\x{1f57}\x{1f59}\x{1f5b}\x{1f5d}\x{1f5f}-\x{1f7d}\x{1f80}-\x{1fb4}\x{1fb6}-\x{1fbc}\x{1fbe}\x{1fc2}-\x{1fc4}\x{1fc6}-\x{1fcc}\x{1fd0}-\x{1fd3}\x{1fd6}-\x{1fdb}\x{1fe0}-\x{1fec}\x{1ff2}-\x{1ff4}\x{1ff6}-\x{1ffc}\x{200e}\x{2071}\x{207f}\x{2090}-\x{209c}\x{2102}\x{2107}\x{210a}-\x{2113}\x{2115}\x{2119}-\x{211d}\x{2124}\x{2126}\x{2128}\x{212a}-\x{212d}\x{212f}-\x{2139}\x{213c}-\x{213f}\x{2145}-\x{2149}\x{214e}\x{214f}\x{2160}-\x{2188}\x{2336}-\x{237a}\x{2395}\x{249c}-\x{24e9}\x{26ac}\x{2800}-\x{28ff}\x{2c00}-\x{2c2e}\x{2c30}-\x{2c5e}\x{2c60}-\x{2ce4}\x{2ceb}-\x{2cee}\x{2cf2}\x{2cf3}\x{2d00}-\x{2d25}\x{2d27}\x{2d2d}\x{2d30}-\x{2d67}\x{2d6f}\x{2d70}\x{2d80}-\x{2d96}\x{2da0}-\x{2da6}\x{2da8}-\x{2dae}\x{2db0}-\x{2db6}\x{2db8}-\x{2dbe}\x{2dc0}-\x{2dc6}\x{2dc8}-\x{2dce}\x{2dd0}-\x{2dd6}\x{2dd8}-\x{2dde}\x{3005}-\x{3007}\x{3021}-\x{3029}\x{302e}\x{302f}\x{3031}-\x{3035}\x{3038}-\x{303c}\x{3041}-\x{3096}\x{309d}-\x{309f}\x{30a1}-\x{30fa}\x{30fc}-\x{30ff}\x{3105}-\x{312d}\x{3131}-\x{318e}\x{3190}-\x{31ba}\x{31f0}-\x{321c}\x{3220}-\x{324f}\x{3260}-\x{327b}\x{327f}-\x{32b0}\x{32c0}-\x{32cb}\x{32d0}-\x{32fe}\x{3300}-\x{3376}\x{337b}-\x{33dd}\x{33e0}-\x{33fe}\x{3400}-\x{4db5}\x{4e00}-\x{9fd5}\x{a000}-\x{a48c}\x{a4d0}-\x{a60c}\x{a610}-\x{a62b}\x{a640}-\x{a66e}\x{a680}-\x{a69d}\x{a6a0}-\x{a6ef}\x{a6f2}-\x{a6f7}\x{a722}-\x{a787}\x{a789}-\x{a7ad}\x{a7b0}-\x{a7b7}\x{a7f7}-\x{a801}\x{a803}-\x{a805}\x{a807}-\x{a80a}\x{a80c}-\x{a824}\x{a827}\x{a830}-\x{a837}\x{a840}-\x{a873}\x{a880}-\x{a8c3}\x{a8ce}-\x{a8d9}\x{a8f2}-\x{a8fd}\x{a900}-\x{a925}\x{a92e}-\x{a946}\x{a952}\x{a953}\x{a95f}-\x{a97c}\x{a983}-\x{a9b2}\x{a9b4}\x{a9b5}\x{a9ba}\x{a9bb}\x{a9bd}-\x{a9cd}\x{a9cf}-\x{a9d9}\x{a9de}-\x{a9e4}\x{a9e6}-\x{a9fe}\x{aa00}-\x{aa28}\x{aa2f}\x{aa30}\x{aa33}\x{aa34}\x{aa40}-\x{aa42}\x{aa44}-\x{aa4b}\x{aa4d}\x{aa50}-\x{aa59}\x{aa5c}-\x{aa7b}\x{aa7d}-\x{aaaf}\x{aab1}\x{aab5}\x{aab6}\x{aab9}-\x{aabd}\x{aac0}\x{aac2}\x{aadb}-\x{aaeb}\x{aaee}-\x{aaf5}\x{ab01}-\x{ab06}\x{ab09}-\x{ab0e}\x{ab11}-\x{ab16}\x{ab20}-\x{ab26}\x{ab28}-\x{ab2e}\x{ab30}-\x{ab65}\x{ab70}-\x{abe4}\x{abe6}\x{abe7}\x{abe9}-\x{abec}\x{abf0}-\x{abf9}\x{ac00}-\x{d7a3}\x{d7b0}-\x{d7c6}\x{d7cb}-\x{d7fb}\x{e000}-\x{fa6d}\x{fa70}-\x{fad9}\x{fb00}-\x{fb06}\x{fb13}-\x{fb17}\x{ff21}-\x{ff3a}\x{ff41}-\x{ff5a}\x{ff66}-\x{ffbe}\x{ffc2}-\x{ffc7}\x{ffca}-\x{ffcf}\x{ffd2}-\x{ffd7}\x{ffda}-\x{ffdc}\x{10000}-\x{1000b}\x{1000d}-\x{10026}\x{10028}-\x{1003a}\x{1003c}\x{1003d}\x{1003f}-\x{1004d}\x{10050}-\x{1005d}\x{10080}-\x{100fa}\x{10100}\x{10102}\x{10107}-\x{10133}\x{10137}-\x{1013f}\x{101d0}-\x{101fc}\x{10280}-\x{1029c}\x{102a0}-\x{102d0}\x{10300}-\x{10323}\x{10330}-\x{1034a}\x{10350}-\x{10375}\x{10380}-\x{1039d}\x{1039f}-\x{103c3}\x{103c8}-\x{103d5}\x{10400}-\x{1049d}\x{104a0}-\x{104a9}\x{10500}-\x{10527}\x{10530}-\x{10563}\x{1056f}\x{10600}-\x{10736}\x{10740}-\x{10755}\x{10760}-\x{10767}\x{11000}\x{11002}-\x{11037}\x{11047}-\x{1104d}\x{11066}-\x{1106f}\x{11082}-\x{110b2}\x{110b7}\x{110b8}\x{110bb}-\x{110c1}\x{110d0}-\x{110e8}\x{110f0}-\x{110f9}\x{11103}-\x{11126}\x{1112c}\x{11136}-\x{11143}\x{11150}-\x{11172}\x{11174}-\x{11176}\x{11182}-\x{111b5}\x{111bf}-\x{111c9}\x{111cd}\x{111d0}-\x{111df}\x{111e1}-\x{111f4}\x{11200}-\x{11211}\x{11213}-\x{1122e}\x{11232}\x{11233}\x{11235}\x{11238}-\x{1123d}\x{11280}-\x{11286}\x{11288}\x{1128a}-\x{1128d}\x{1128f}-\x{1129d}\x{1129f}-\x{112a9}\x{112b0}-\x{112de}\x{112e0}-\x{112e2}\x{112f0}-\x{112f9}\x{11302}\x{11303}\x{11305}-\x{1130c}\x{1130f}\x{11310}\x{11313}-\x{11328}\x{1132a}-\x{11330}\x{11332}\x{11333}\x{11335}-\x{11339}\x{1133d}-\x{1133f}\x{11341}-\x{11344}\x{11347}\x{11348}\x{1134b}-\x{1134d}\x{11350}\x{11357}\x{1135d}-\x{11363}\x{11480}-\x{114b2}\x{114b9}\x{114bb}-\x{114be}\x{114c1}\x{114c4}-\x{114c7}\x{114d0}-\x{114d9}\x{11580}-\x{115b1}\x{115b8}-\x{115bb}\x{115be}\x{115c1}-\x{115db}\x{11600}-\x{11632}\x{1163b}\x{1163c}\x{1163e}\x{11641}-\x{11644}\x{11650}-\x{11659}\x{11680}-\x{116aa}\x{116ac}\x{116ae}\x{116af}\x{116b6}\x{116c0}-\x{116c9}\x{11700}-\x{11719}\x{11720}\x{11721}\x{11726}\x{11730}-\x{1173f}\x{118a0}-\x{118f2}\x{118ff}\x{11ac0}-\x{11af8}\x{12000}-\x{12399}\x{12400}-\x{1246e}\x{12470}-\x{12474}\x{12480}-\x{12543}\x{13000}-\x{1342e}\x{14400}-\x{14646}\x{16800}-\x{16a38}\x{16a40}-\x{16a5e}\x{16a60}-\x{16a69}\x{16a6e}\x{16a6f}\x{16ad0}-\x{16aed}\x{16af5}\x{16b00}-\x{16b2f}\x{16b37}-\x{16b45}\x{16b50}-\x{16b59}\x{16b5b}-\x{16b61}\x{16b63}-\x{16b77}\x{16b7d}-\x{16b8f}\x{16f00}-\x{16f44}\x{16f50}-\x{16f7e}\x{16f93}-\x{16f9f}\x{1b000}\x{1b001}\x{1bc00}-\x{1bc6a}\x{1bc70}-\x{1bc7c}\x{1bc80}-\x{1bc88}\x{1bc90}-\x{1bc99}\x{1bc9c}\x{1bc9f}\x{1d000}-\x{1d0f5}\x{1d100}-\x{1d126}\x{1d129}-\x{1d166}\x{1d16a}-\x{1d172}\x{1d183}\x{1d184}\x{1d18c}-\x{1d1a9}\x{1d1ae}-\x{1d1e8}\x{1d360}-\x{1d371}\x{1d400}-\x{1d454}\x{1d456}-\x{1d49c}\x{1d49e}\x{1d49f}\x{1d4a2}\x{1d4a5}\x{1d4a6}\x{1d4a9}-\x{1d4ac}\x{1d4ae}-\x{1d4b9}\x{1d4bb}\x{1d4bd}-\x{1d4c3}\x{1d4c5}-\x{1d505}\x{1d507}-\x{1d50a}\x{1d50d}-\x{1d514}\x{1d516}-\x{1d51c}\x{1d51e}-\x{1d539}\x{1d53b}-\x{1d53e}\x{1d540}-\x{1d544}\x{1d546}\x{1d54a}-\x{1d550}\x{1d552}-\x{1d6a5}\x{1d6a8}-\x{1d6da}\x{1d6dc}-\x{1d714}\x{1d716}-\x{1d74e}\x{1d750}-\x{1d788}\x{1d78a}-\x{1d7c2}\x{1d7c4}-\x{1d7cb}\x{1d800}-\x{1d9ff}\x{1da37}-\x{1da3a}\x{1da6d}-\x{1da74}\x{1da76}-\x{1da83}\x{1da85}-\x{1da8b}\x{1f110}-\x{1f12e}\x{1f130}-\x{1f169}\x{1f170}-\x{1f19a}\x{1f1e6}-\x{1f202}\x{1f210}-\x{1f23a}\x{1f240}-\x{1f248}\x{1f250}\x{1f251}\x{20000}-\x{2a6d6}\x{2a700}-\x{2b734}\x{2b740}-\x{2b81d}\x{2b820}-\x{2cea1}\x{2f800}-\x{2fa1d}\x{f0000}-\x{ffffd}\x{100000}-\x{10fffd}])|([\x{590}\x{5be}\x{5c0}\x{5c3}\x{5c6}\x{5c8}-\x{5ff}\x{7c0}-\x{7ea}\x{7f4}\x{7f5}\x{7fa}-\x{815}\x{81a}\x{824}\x{828}\x{82e}-\x{858}\x{85c}-\x{89f}\x{200f}\x{fb1d}\x{fb1f}-\x{fb28}\x{fb2a}-\x{fb4f}\x{10800}-\x{1091e}\x{10920}-\x{10a00}\x{10a04}\x{10a07}-\x{10a0b}\x{10a10}-\x{10a37}\x{10a3b}-\x{10a3e}\x{10a40}-\x{10ae4}\x{10ae7}-\x{10b38}\x{10b40}-\x{10e5f}\x{10e7f}-\x{10fff}\x{1e800}-\x{1e8cf}\x{1e8d7}-\x{1edff}\x{1ef00}-\x{1efff}\x{608}\x{60b}\x{60d}\x{61b}-\x{64a}\x{66d}-\x{66f}\x{671}-\x{6d5}\x{6e5}\x{6e6}\x{6ee}\x{6ef}\x{6fa}-\x{710}\x{712}-\x{72f}\x{74b}-\x{7a5}\x{7b1}-\x{7bf}\x{8a0}-\x{8e2}\x{fb50}-\x{fd3d}\x{fd40}-\x{fdcf}\x{fdf0}-\x{fdfc}\x{fdfe}\x{fdff}\x{fe70}-\x{fefe}\x{1ee00}-\x{1eeef}\x{1eef2}-\x{1eeff}]))/u';
175 // @codeCoverageIgnoreEnd
176
183 static function factory( $code ) {
185
186 if ( isset( $wgDummyLanguageCodes[$code] ) ) {
188 }
189
190 // get the language object to process
191 $langObj = isset( self::$mLangObjCache[$code] )
192 ? self::$mLangObjCache[$code]
193 : self::newFromCode( $code );
194
195 // merge the language object in to get it up front in the cache
196 self::$mLangObjCache = array_merge( [ $code => $langObj ], self::$mLangObjCache );
197 // get rid of the oldest ones in case we have an overflow
198 self::$mLangObjCache = array_slice( self::$mLangObjCache, 0, $wgLangObjCacheSize, true );
199
200 return $langObj;
201 }
202
210 protected static function newFromCode( $code, $fallback = false ) {
211 if ( !self::isValidCode( $code ) ) {
212 throw new MWException( "Invalid language code \"$code\"" );
213 }
214
215 if ( !self::isValidBuiltInCode( $code ) ) {
216 // It's not possible to customise this code with class files, so
217 // just return a Language object. This is to support uselang= hacks.
218 $lang = new Language;
219 $lang->setCode( $code );
220 return $lang;
221 }
222
223 // Check if there is a language class for the code
224 $class = self::classFromCode( $code, $fallback );
225 if ( class_exists( $class ) ) {
226 $lang = new $class;
227 return $lang;
228 }
229
230 // Keep trying the fallback list until we find an existing class
231 $fallbacks = self::getFallbacksFor( $code );
232 foreach ( $fallbacks as $fallbackCode ) {
233 if ( !self::isValidBuiltInCode( $fallbackCode ) ) {
234 throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" );
235 }
236
237 $class = self::classFromCode( $fallbackCode );
238 if ( class_exists( $class ) ) {
239 $lang = new $class;
240 $lang->setCode( $code );
241 return $lang;
242 }
243 }
244
245 throw new MWException( "Invalid fallback sequence for language '$code'" );
246 }
247
256 public static function isSupportedLanguage( $code ) {
257 if ( !self::isValidBuiltInCode( $code ) ) {
258 return false;
259 }
260
261 if ( $code === 'qqq' ) {
262 return false;
263 }
264
265 return is_readable( self::getMessagesFileName( $code ) ) ||
266 is_readable( self::getJsonMessagesFileName( $code ) );
267 }
268
284 public static function isWellFormedLanguageTag( $code, $lenient = false ) {
285 $alpha = '[a-z]';
286 $digit = '[0-9]';
287 $alphanum = '[a-z0-9]';
288 $x = 'x'; # private use singleton
289 $singleton = '[a-wy-z]'; # other singleton
290 $s = $lenient ? '[-_]' : '-';
291
292 $language = "$alpha{2,8}|$alpha{2,3}$s$alpha{3}";
293 $script = "$alpha{4}"; # ISO 15924
294 $region = "(?:$alpha{2}|$digit{3})"; # ISO 3166-1 alpha-2 or UN M.49
295 $variant = "(?:$alphanum{5,8}|$digit$alphanum{3})";
296 $extension = "$singleton(?:$s$alphanum{2,8})+";
297 $privateUse = "$x(?:$s$alphanum{1,8})+";
298
299 # Define certain grandfathered codes, since otherwise the regex is pretty useless.
300 # Since these are limited, this is safe even later changes to the registry --
301 # the only oddity is that it might change the type of the tag, and thus
302 # the results from the capturing groups.
303 # https://www.iana.org/assignments/language-subtag-registry
304
305 $grandfathered = "en{$s}GB{$s}oed"
306 . "|i{$s}(?:ami|bnn|default|enochian|hak|klingon|lux|mingo|navajo|pwn|tao|tay|tsu)"
307 . "|no{$s}(?:bok|nyn)"
308 . "|sgn{$s}(?:BE{$s}(?:fr|nl)|CH{$s}de)"
309 . "|zh{$s}min{$s}nan";
310
311 $variantList = "$variant(?:$s$variant)*";
312 $extensionList = "$extension(?:$s$extension)*";
313
314 $langtag = "(?:($language)"
315 . "(?:$s$script)?"
316 . "(?:$s$region)?"
317 . "(?:$s$variantList)?"
318 . "(?:$s$extensionList)?"
319 . "(?:$s$privateUse)?)";
320
321 # The final breakdown, with capturing groups for each of these components
322 # The variants, extensions, grandfathered, and private-use may have interior '-'
323
324 $root = "^(?:$langtag|$privateUse|$grandfathered)$";
325
326 return (bool)preg_match( "/$root/", strtolower( $code ) );
327 }
328
338 public static function isValidCode( $code ) {
339 static $cache = [];
340 if ( !isset( $cache[$code] ) ) {
341 // People think language codes are html safe, so enforce it.
342 // Ideally we should only allow a-zA-Z0-9-
343 // but, .+ and other chars are often used for {{int:}} hacks
344 // see bugs T39564, T39587, T38938
345 $cache[$code] =
346 // Protect against path traversal
347 strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
349 }
350 return $cache[$code];
351 }
352
363 public static function isValidBuiltInCode( $code ) {
364 if ( !is_string( $code ) ) {
365 if ( is_object( $code ) ) {
366 $addmsg = " of class " . get_class( $code );
367 } else {
368 $addmsg = '';
369 }
370 $type = gettype( $code );
371 throw new MWException( __METHOD__ . " must be passed a string, $type given$addmsg" );
372 }
373
374 return (bool)preg_match( '/^[a-z0-9-]{2,}$/', $code );
375 }
376
385 public static function isKnownLanguageTag( $tag ) {
386 // Quick escape for invalid input to avoid exceptions down the line
387 // when code tries to process tags which are not valid at all.
388 if ( !self::isValidBuiltInCode( $tag ) ) {
389 return false;
390 }
391
392 if ( isset( MediaWiki\Languages\Data\Names::$names[$tag] )
393 || self::fetchLanguageName( $tag, $tag ) !== ''
394 ) {
395 return true;
396 }
397
398 return false;
399 }
400
406 public static function getLocalisationCache() {
407 if ( is_null( self::$dataCache ) ) {
409 $class = $wgLocalisationCacheConf['class'];
410 self::$dataCache = new $class( $wgLocalisationCacheConf );
411 }
412 return self::$dataCache;
413 }
414
415 function __construct() {
416 $this->mConverter = new FakeConverter( $this );
417 // Set the code to the name of the descendant
418 if ( static::class === 'Language' ) {
419 $this->mCode = 'en';
420 } else {
421 $this->mCode = str_replace( '_', '-', strtolower( substr( static::class, 8 ) ) );
422 }
423 self::getLocalisationCache();
424 }
425
429 function __destruct() {
430 foreach ( $this as $name => $value ) {
431 unset( $this->$name );
432 }
433 }
434
439 function initContLang() {
440 }
441
446 public function getFallbackLanguages() {
447 return self::getFallbacksFor( $this->mCode );
448 }
449
454 public function getBookstoreList() {
455 return self::$dataCache->getItem( $this->mCode, 'bookstoreList' );
456 }
457
464 public function getNamespaces() {
465 if ( is_null( $this->namespaceNames ) ) {
467
468 $validNamespaces = MWNamespace::getCanonicalNamespaces();
469
470 $this->namespaceNames = $wgExtraNamespaces +
471 self::$dataCache->getItem( $this->mCode, 'namespaceNames' );
472 $this->namespaceNames += $validNamespaces;
473
474 $this->namespaceNames[NS_PROJECT] = $wgMetaNamespace;
475 if ( $wgMetaNamespaceTalk ) {
476 $this->namespaceNames[NS_PROJECT_TALK] = $wgMetaNamespaceTalk;
477 } else {
478 $talk = $this->namespaceNames[NS_PROJECT_TALK];
479 $this->namespaceNames[NS_PROJECT_TALK] =
480 $this->fixVariableInNamespace( $talk );
481 }
482
483 # Sometimes a language will be localised but not actually exist on this wiki.
484 foreach ( $this->namespaceNames as $key => $text ) {
485 if ( !isset( $validNamespaces[$key] ) ) {
486 unset( $this->namespaceNames[$key] );
487 }
488 }
489
490 # The above mixing may leave namespaces out of canonical order.
491 # Re-order by namespace ID number...
492 ksort( $this->namespaceNames );
493
494 Hooks::run( 'LanguageGetNamespaces', [ &$this->namespaceNames ] );
495 }
496
498 }
499
504 public function setNamespaces( array $namespaces ) {
505 $this->namespaceNames = $namespaces;
506 $this->mNamespaceIds = null;
507 }
508
512 public function resetNamespaces() {
513 $this->namespaceNames = null;
514 $this->mNamespaceIds = null;
515 $this->namespaceAliases = null;
516 }
517
524 public function getFormattedNamespaces() {
525 $ns = $this->getNamespaces();
526 foreach ( $ns as $k => $v ) {
527 $ns[$k] = strtr( $v, '_', ' ' );
528 }
529 return $ns;
530 }
531
543 public function getNsText( $index ) {
544 $ns = $this->getNamespaces();
545 return isset( $ns[$index] ) ? $ns[$index] : false;
546 }
547
561 public function getFormattedNsText( $index ) {
562 $ns = $this->getNsText( $index );
563 return strtr( $ns, '_', ' ' );
564 }
565
574 public function getGenderNsText( $index, $gender ) {
576
578 (array)self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
579
580 return isset( $ns[$index][$gender] ) ? $ns[$index][$gender] : $this->getNsText( $index );
581 }
582
589 public function needsGenderDistinction() {
591 if ( count( $wgExtraGenderNamespaces ) > 0 ) {
592 // $wgExtraGenderNamespaces overrides everything
593 return true;
594 } elseif ( isset( $wgExtraNamespaces[NS_USER] ) && isset( $wgExtraNamespaces[NS_USER_TALK] ) ) {
596 // $wgExtraNamespaces overrides any gender aliases specified in i18n files
597 return false;
598 } else {
599 // Check what is in i18n files
600 $aliases = self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
601 return count( $aliases ) > 0;
602 }
603 }
604
613 function getLocalNsIndex( $text ) {
614 $lctext = $this->lc( $text );
615 $ids = $this->getNamespaceIds();
616 return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
617 }
618
622 public function getNamespaceAliases() {
623 if ( is_null( $this->namespaceAliases ) ) {
624 $aliases = self::$dataCache->getItem( $this->mCode, 'namespaceAliases' );
625 if ( !$aliases ) {
626 $aliases = [];
627 } else {
628 foreach ( $aliases as $name => $index ) {
629 if ( $index === NS_PROJECT_TALK ) {
630 unset( $aliases[$name] );
632 $aliases[$name] = $index;
633 }
634 }
635 }
636
638 $genders = $wgExtraGenderNamespaces +
639 (array)self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
640 foreach ( $genders as $index => $forms ) {
641 foreach ( $forms as $alias ) {
642 $aliases[$alias] = $index;
643 }
644 }
645
646 # Also add converted namespace names as aliases, to avoid confusion.
647 $convertedNames = [];
648 foreach ( $this->getVariants() as $variant ) {
649 if ( $variant === $this->mCode ) {
650 continue;
651 }
652 foreach ( $this->getNamespaces() as $ns => $_ ) {
653 $convertedNames[$this->getConverter()->convertNamespace( $ns, $variant )] = $ns;
654 }
655 }
656
657 $this->namespaceAliases = $aliases + $convertedNames;
658 }
659
661 }
662
666 public function getNamespaceIds() {
667 if ( is_null( $this->mNamespaceIds ) ) {
669 # Put namespace names and aliases into a hashtable.
670 # If this is too slow, then we should arrange it so that it is done
671 # before caching. The catch is that at pre-cache time, the above
672 # class-specific fixup hasn't been done.
673 $this->mNamespaceIds = [];
674 foreach ( $this->getNamespaces() as $index => $name ) {
675 $this->mNamespaceIds[$this->lc( $name )] = $index;
676 }
677 foreach ( $this->getNamespaceAliases() as $name => $index ) {
678 $this->mNamespaceIds[$this->lc( $name )] = $index;
679 }
680 if ( $wgNamespaceAliases ) {
681 foreach ( $wgNamespaceAliases as $name => $index ) {
682 $this->mNamespaceIds[$this->lc( $name )] = $index;
683 }
684 }
685 }
686 return $this->mNamespaceIds;
687 }
688
696 public function getNsIndex( $text ) {
697 $lctext = $this->lc( $text );
698 $ns = MWNamespace::getCanonicalIndex( $lctext );
699 if ( $ns !== null ) {
700 return $ns;
701 }
702 $ids = $this->getNamespaceIds();
703 return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
704 }
705
713 public function getVariantname( $code, $usemsg = true ) {
714 $msg = "variantname-$code";
715 if ( $usemsg && wfMessage( $msg )->exists() ) {
716 return $this->getMessageFromDB( $msg );
717 }
718 $name = self::fetchLanguageName( $code );
719 if ( $name ) {
720 return $name; # if it's defined as a language name, show that
721 } else {
722 # otherwise, output the language code
723 return $code;
724 }
725 }
726
730 public function getDatePreferences() {
731 return self::$dataCache->getItem( $this->mCode, 'datePreferences' );
732 }
733
737 function getDateFormats() {
738 return self::$dataCache->getItem( $this->mCode, 'dateFormats' );
739 }
740
744 public function getDefaultDateFormat() {
745 $df = self::$dataCache->getItem( $this->mCode, 'defaultDateFormat' );
746 if ( $df === 'dmy or mdy' ) {
747 global $wgAmericanDates;
748 return $wgAmericanDates ? 'mdy' : 'dmy';
749 } else {
750 return $df;
751 }
752 }
753
757 public function getDatePreferenceMigrationMap() {
758 return self::$dataCache->getItem( $this->mCode, 'datePreferenceMigrationMap' );
759 }
760
765 function getImageFile( $image ) {
766 return self::$dataCache->getSubitem( $this->mCode, 'imageFiles', $image );
767 }
768
773 public function getImageFiles() {
774 return self::$dataCache->getItem( $this->mCode, 'imageFiles' );
775 }
776
780 public function getExtraUserToggles() {
781 return (array)self::$dataCache->getItem( $this->mCode, 'extraUserToggles' );
782 }
783
788 function getUserToggle( $tog ) {
789 return $this->getMessageFromDB( "tog-$tog" );
790 }
791
803 public static function fetchLanguageNames( $inLanguage = null, $include = 'mw' ) {
804 $cacheKey = $inLanguage === null ? 'null' : $inLanguage;
805 $cacheKey .= ":$include";
806 if ( self::$languageNameCache === null ) {
807 self::$languageNameCache = new HashBagOStuff( [ 'maxKeys' => 20 ] );
808 }
809
810 $ret = self::$languageNameCache->get( $cacheKey );
811 if ( !$ret ) {
812 $ret = self::fetchLanguageNamesUncached( $inLanguage, $include );
813 self::$languageNameCache->set( $cacheKey, $ret );
814 }
815 return $ret;
816 }
817
828 private static function fetchLanguageNamesUncached( $inLanguage = null, $include = 'mw' ) {
829 global $wgExtraLanguageNames, $wgUsePigLatinVariant;
830
831 // If passed an invalid language code to use, fallback to en
832 if ( $inLanguage !== null && !self::isValidCode( $inLanguage ) ) {
833 $inLanguage = 'en';
834 }
835
836 $names = [];
837
838 if ( $inLanguage ) {
839 # TODO: also include when $inLanguage is null, when this code is more efficient
840 Hooks::run( 'LanguageGetTranslatedLanguageNames', [ &$names, $inLanguage ] );
841 }
842
843 $mwNames = $wgExtraLanguageNames + MediaWiki\Languages\Data\Names::$names;
844 if ( $wgUsePigLatinVariant ) {
845 // Pig Latin (for variant development)
846 $mwNames['en-x-piglatin'] = 'Igpay Atinlay';
847 }
848
849 foreach ( $mwNames as $mwCode => $mwName ) {
850 # - Prefer own MediaWiki native name when not using the hook
851 # - For other names just add if not added through the hook
852 if ( $mwCode === $inLanguage || !isset( $names[$mwCode] ) ) {
853 $names[$mwCode] = $mwName;
854 }
855 }
856
857 if ( $include === 'all' ) {
858 ksort( $names );
859 return $names;
860 }
861
862 $returnMw = [];
863 $coreCodes = array_keys( $mwNames );
864 foreach ( $coreCodes as $coreCode ) {
865 $returnMw[$coreCode] = $names[$coreCode];
866 }
867
868 if ( $include === 'mwfile' ) {
869 $namesMwFile = [];
870 # We do this using a foreach over the codes instead of a directory
871 # loop so that messages files in extensions will work correctly.
872 foreach ( $returnMw as $code => $value ) {
873 if ( is_readable( self::getMessagesFileName( $code ) )
874 || is_readable( self::getJsonMessagesFileName( $code ) )
875 ) {
876 $namesMwFile[$code] = $names[$code];
877 }
878 }
879
880 ksort( $namesMwFile );
881 return $namesMwFile;
882 }
883
884 ksort( $returnMw );
885 # 'mw' option; default if it's not one of the other two options (all/mwfile)
886 return $returnMw;
887 }
888
896 public static function fetchLanguageName( $code, $inLanguage = null, $include = 'all' ) {
897 $code = strtolower( $code );
898 $array = self::fetchLanguageNames( $inLanguage, $include );
899 return !array_key_exists( $code, $array ) ? '' : $array[$code];
900 }
901
908 public function getMessageFromDB( $msg ) {
909 return $this->msg( $msg )->text();
910 }
911
918 protected function msg( $msg ) {
919 return wfMessage( $msg )->inLanguage( $this );
920 }
921
926 public function getMonthName( $key ) {
927 return $this->getMessageFromDB( self::$mMonthMsgs[$key - 1] );
928 }
929
933 public function getMonthNamesArray() {
934 $monthNames = [ '' ];
935 for ( $i = 1; $i < 13; $i++ ) {
936 $monthNames[] = $this->getMonthName( $i );
937 }
938 return $monthNames;
939 }
940
945 public function getMonthNameGen( $key ) {
946 return $this->getMessageFromDB( self::$mMonthGenMsgs[$key - 1] );
947 }
948
953 public function getMonthAbbreviation( $key ) {
954 return $this->getMessageFromDB( self::$mMonthAbbrevMsgs[$key - 1] );
955 }
956
960 public function getMonthAbbreviationsArray() {
961 $monthNames = [ '' ];
962 for ( $i = 1; $i < 13; $i++ ) {
963 $monthNames[] = $this->getMonthAbbreviation( $i );
964 }
965 return $monthNames;
966 }
967
972 public function getWeekdayName( $key ) {
973 return $this->getMessageFromDB( self::$mWeekdayMsgs[$key - 1] );
974 }
975
980 function getWeekdayAbbreviation( $key ) {
981 return $this->getMessageFromDB( self::$mWeekdayAbbrevMsgs[$key - 1] );
982 }
983
989 return $this->getMessageFromDB( self::$mIranianCalendarMonthMsgs[$key - 1] );
990 }
991
996 function getHebrewCalendarMonthName( $key ) {
997 return $this->getMessageFromDB( self::$mHebrewCalendarMonthMsgs[$key - 1] );
998 }
999
1005 return $this->getMessageFromDB( self::$mHebrewCalendarMonthGenMsgs[$key - 1] );
1006 }
1007
1012 function getHijriCalendarMonthName( $key ) {
1013 return $this->getMessageFromDB( self::$mHijriCalendarMonthMsgs[$key - 1] );
1014 }
1015
1024 private static function dateTimeObjFormat( &$dateTimeObj, $ts, $zone, $code ) {
1025 if ( !$dateTimeObj ) {
1026 $dateTimeObj = DateTime::createFromFormat(
1027 'YmdHis', $ts, $zone ?: new DateTimeZone( 'UTC' )
1028 );
1029 }
1030 return $dateTimeObj->format( $code );
1031 }
1032
1102 public function sprintfDate( $format, $ts, DateTimeZone $zone = null, &$ttl = 'unused' ) {
1103 $s = '';
1104 $raw = false;
1105 $roman = false;
1106 $hebrewNum = false;
1107 $dateTimeObj = false;
1108 $rawToggle = false;
1109 $iranian = false;
1110 $hebrew = false;
1111 $hijri = false;
1112 $thai = false;
1113 $minguo = false;
1114 $tenno = false;
1115
1116 $usedSecond = false;
1117 $usedMinute = false;
1118 $usedHour = false;
1119 $usedAMPM = false;
1120 $usedDay = false;
1121 $usedWeek = false;
1122 $usedMonth = false;
1123 $usedYear = false;
1124 $usedISOYear = false;
1125 $usedIsLeapYear = false;
1126
1127 $usedHebrewMonth = false;
1128 $usedIranianMonth = false;
1129 $usedHijriMonth = false;
1130 $usedHebrewYear = false;
1131 $usedIranianYear = false;
1132 $usedHijriYear = false;
1133 $usedTennoYear = false;
1134
1135 if ( strlen( $ts ) !== 14 ) {
1136 throw new MWException( __METHOD__ . ": The timestamp $ts should have 14 characters" );
1137 }
1138
1139 if ( !ctype_digit( $ts ) ) {
1140 throw new MWException( __METHOD__ . ": The timestamp $ts should be a number" );
1141 }
1142
1143 $formatLength = strlen( $format );
1144 for ( $p = 0; $p < $formatLength; $p++ ) {
1145 $num = false;
1146 $code = $format[$p];
1147 if ( $code == 'x' && $p < $formatLength - 1 ) {
1148 $code .= $format[++$p];
1149 }
1150
1151 if ( ( $code === 'xi'
1152 || $code === 'xj'
1153 || $code === 'xk'
1154 || $code === 'xm'
1155 || $code === 'xo'
1156 || $code === 'xt' )
1157 && $p < $formatLength - 1 ) {
1158 $code .= $format[++$p];
1159 }
1160
1161 switch ( $code ) {
1162 case 'xx':
1163 $s .= 'x';
1164 break;
1165 case 'xn':
1166 $raw = true;
1167 break;
1168 case 'xN':
1169 $rawToggle = !$rawToggle;
1170 break;
1171 case 'xr':
1172 $roman = true;
1173 break;
1174 case 'xh':
1175 $hebrewNum = true;
1176 break;
1177 case 'xg':
1178 $usedMonth = true;
1179 $s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) );
1180 break;
1181 case 'xjx':
1182 $usedHebrewMonth = true;
1183 if ( !$hebrew ) {
1184 $hebrew = self::tsToHebrew( $ts );
1185 }
1186 $s .= $this->getHebrewCalendarMonthNameGen( $hebrew[1] );
1187 break;
1188 case 'd':
1189 $usedDay = true;
1190 $num = substr( $ts, 6, 2 );
1191 break;
1192 case 'D':
1193 $usedDay = true;
1194 $s .= $this->getWeekdayAbbreviation(
1195 self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1
1196 );
1197 break;
1198 case 'j':
1199 $usedDay = true;
1200 $num = intval( substr( $ts, 6, 2 ) );
1201 break;
1202 case 'xij':
1203 $usedDay = true;
1204 if ( !$iranian ) {
1205 $iranian = self::tsToIranian( $ts );
1206 }
1207 $num = $iranian[2];
1208 break;
1209 case 'xmj':
1210 $usedDay = true;
1211 if ( !$hijri ) {
1212 $hijri = self::tsToHijri( $ts );
1213 }
1214 $num = $hijri[2];
1215 break;
1216 case 'xjj':
1217 $usedDay = true;
1218 if ( !$hebrew ) {
1219 $hebrew = self::tsToHebrew( $ts );
1220 }
1221 $num = $hebrew[2];
1222 break;
1223 case 'l':
1224 $usedDay = true;
1225 $s .= $this->getWeekdayName(
1226 self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1
1227 );
1228 break;
1229 case 'F':
1230 $usedMonth = true;
1231 $s .= $this->getMonthName( substr( $ts, 4, 2 ) );
1232 break;
1233 case 'xiF':
1234 $usedIranianMonth = true;
1235 if ( !$iranian ) {
1236 $iranian = self::tsToIranian( $ts );
1237 }
1238 $s .= $this->getIranianCalendarMonthName( $iranian[1] );
1239 break;
1240 case 'xmF':
1241 $usedHijriMonth = true;
1242 if ( !$hijri ) {
1243 $hijri = self::tsToHijri( $ts );
1244 }
1245 $s .= $this->getHijriCalendarMonthName( $hijri[1] );
1246 break;
1247 case 'xjF':
1248 $usedHebrewMonth = true;
1249 if ( !$hebrew ) {
1250 $hebrew = self::tsToHebrew( $ts );
1251 }
1252 $s .= $this->getHebrewCalendarMonthName( $hebrew[1] );
1253 break;
1254 case 'm':
1255 $usedMonth = true;
1256 $num = substr( $ts, 4, 2 );
1257 break;
1258 case 'M':
1259 $usedMonth = true;
1260 $s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) );
1261 break;
1262 case 'n':
1263 $usedMonth = true;
1264 $num = intval( substr( $ts, 4, 2 ) );
1265 break;
1266 case 'xin':
1267 $usedIranianMonth = true;
1268 if ( !$iranian ) {
1269 $iranian = self::tsToIranian( $ts );
1270 }
1271 $num = $iranian[1];
1272 break;
1273 case 'xmn':
1274 $usedHijriMonth = true;
1275 if ( !$hijri ) {
1276 $hijri = self::tsToHijri( $ts );
1277 }
1278 $num = $hijri[1];
1279 break;
1280 case 'xjn':
1281 $usedHebrewMonth = true;
1282 if ( !$hebrew ) {
1283 $hebrew = self::tsToHebrew( $ts );
1284 }
1285 $num = $hebrew[1];
1286 break;
1287 case 'xjt':
1288 $usedHebrewMonth = true;
1289 if ( !$hebrew ) {
1290 $hebrew = self::tsToHebrew( $ts );
1291 }
1292 $num = $hebrew[3];
1293 break;
1294 case 'Y':
1295 $usedYear = true;
1296 $num = substr( $ts, 0, 4 );
1297 break;
1298 case 'xiY':
1299 $usedIranianYear = true;
1300 if ( !$iranian ) {
1301 $iranian = self::tsToIranian( $ts );
1302 }
1303 $num = $iranian[0];
1304 break;
1305 case 'xmY':
1306 $usedHijriYear = true;
1307 if ( !$hijri ) {
1308 $hijri = self::tsToHijri( $ts );
1309 }
1310 $num = $hijri[0];
1311 break;
1312 case 'xjY':
1313 $usedHebrewYear = true;
1314 if ( !$hebrew ) {
1315 $hebrew = self::tsToHebrew( $ts );
1316 }
1317 $num = $hebrew[0];
1318 break;
1319 case 'xkY':
1320 $usedYear = true;
1321 if ( !$thai ) {
1322 $thai = self::tsToYear( $ts, 'thai' );
1323 }
1324 $num = $thai[0];
1325 break;
1326 case 'xoY':
1327 $usedYear = true;
1328 if ( !$minguo ) {
1329 $minguo = self::tsToYear( $ts, 'minguo' );
1330 }
1331 $num = $minguo[0];
1332 break;
1333 case 'xtY':
1334 $usedTennoYear = true;
1335 if ( !$tenno ) {
1336 $tenno = self::tsToYear( $ts, 'tenno' );
1337 }
1338 $num = $tenno[0];
1339 break;
1340 case 'y':
1341 $usedYear = true;
1342 $num = substr( $ts, 2, 2 );
1343 break;
1344 case 'xiy':
1345 $usedIranianYear = true;
1346 if ( !$iranian ) {
1347 $iranian = self::tsToIranian( $ts );
1348 }
1349 $num = substr( $iranian[0], -2 );
1350 break;
1351 case 'xit':
1352 $usedIranianYear = true;
1353 if ( !$iranian ) {
1354 $iranian = self::tsToIranian( $ts );
1355 }
1356 $num = self::$IRANIAN_DAYS[$iranian[1] - 1];
1357 break;
1358 case 'xiz':
1359 $usedIranianYear = true;
1360 if ( !$iranian ) {
1361 $iranian = self::tsToIranian( $ts );
1362 }
1363 $num = $iranian[3];
1364 break;
1365 case 'a':
1366 $usedAMPM = true;
1367 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm';
1368 break;
1369 case 'A':
1370 $usedAMPM = true;
1371 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'AM' : 'PM';
1372 break;
1373 case 'g':
1374 $usedHour = true;
1375 $h = substr( $ts, 8, 2 );
1376 $num = $h % 12 ? $h % 12 : 12;
1377 break;
1378 case 'G':
1379 $usedHour = true;
1380 $num = intval( substr( $ts, 8, 2 ) );
1381 break;
1382 case 'h':
1383 $usedHour = true;
1384 $h = substr( $ts, 8, 2 );
1385 $num = sprintf( '%02d', $h % 12 ? $h % 12 : 12 );
1386 break;
1387 case 'H':
1388 $usedHour = true;
1389 $num = substr( $ts, 8, 2 );
1390 break;
1391 case 'i':
1392 $usedMinute = true;
1393 $num = substr( $ts, 10, 2 );
1394 break;
1395 case 's':
1396 $usedSecond = true;
1397 $num = substr( $ts, 12, 2 );
1398 break;
1399 case 'c':
1400 case 'r':
1401 $usedSecond = true;
1402 // fall through
1403 case 'e':
1404 case 'O':
1405 case 'P':
1406 case 'T':
1407 $s .= self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1408 break;
1409 case 'w':
1410 case 'N':
1411 case 'z':
1412 $usedDay = true;
1413 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1414 break;
1415 case 'W':
1416 $usedWeek = true;
1417 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1418 break;
1419 case 't':
1420 $usedMonth = true;
1421 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1422 break;
1423 case 'L':
1424 $usedIsLeapYear = true;
1425 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1426 break;
1427 case 'o':
1428 $usedISOYear = true;
1429 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1430 break;
1431 case 'U':
1432 $usedSecond = true;
1433 // fall through
1434 case 'I':
1435 case 'Z':
1436 $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1437 break;
1438 case '\\':
1439 # Backslash escaping
1440 if ( $p < $formatLength - 1 ) {
1441 $s .= $format[++$p];
1442 } else {
1443 $s .= '\\';
1444 }
1445 break;
1446 case '"':
1447 # Quoted literal
1448 if ( $p < $formatLength - 1 ) {
1449 $endQuote = strpos( $format, '"', $p + 1 );
1450 if ( $endQuote === false ) {
1451 # No terminating quote, assume literal "
1452 $s .= '"';
1453 } else {
1454 $s .= substr( $format, $p + 1, $endQuote - $p - 1 );
1455 $p = $endQuote;
1456 }
1457 } else {
1458 # Quote at end of string, assume literal "
1459 $s .= '"';
1460 }
1461 break;
1462 default:
1463 $s .= $format[$p];
1464 }
1465 if ( $num !== false ) {
1466 if ( $rawToggle || $raw ) {
1467 $s .= $num;
1468 $raw = false;
1469 } elseif ( $roman ) {
1470 $s .= self::romanNumeral( $num );
1471 $roman = false;
1472 } elseif ( $hebrewNum ) {
1473 $s .= self::hebrewNumeral( $num );
1474 $hebrewNum = false;
1475 } else {
1476 $s .= $this->formatNum( $num, true );
1477 }
1478 }
1479 }
1480
1481 if ( $ttl === 'unused' ) {
1482 // No need to calculate the TTL, the caller wont use it anyway.
1483 } elseif ( $usedSecond ) {
1484 $ttl = 1;
1485 } elseif ( $usedMinute ) {
1486 $ttl = 60 - substr( $ts, 12, 2 );
1487 } elseif ( $usedHour ) {
1488 $ttl = 3600 - substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1489 } elseif ( $usedAMPM ) {
1490 $ttl = 43200 - ( substr( $ts, 8, 2 ) % 12 ) * 3600 -
1491 substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1492 } elseif (
1493 $usedDay ||
1494 $usedHebrewMonth ||
1495 $usedIranianMonth ||
1496 $usedHijriMonth ||
1497 $usedHebrewYear ||
1498 $usedIranianYear ||
1499 $usedHijriYear ||
1500 $usedTennoYear
1501 ) {
1502 // @todo Someone who understands the non-Gregorian calendars
1503 // should write proper logic for them so that they don't need purged every day.
1504 $ttl = 86400 - substr( $ts, 8, 2 ) * 3600 -
1505 substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1506 } else {
1507 $possibleTtls = [];
1508 $timeRemainingInDay = 86400 - substr( $ts, 8, 2 ) * 3600 -
1509 substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1510 if ( $usedWeek ) {
1511 $possibleTtls[] =
1512 ( 7 - self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400 +
1513 $timeRemainingInDay;
1514 } elseif ( $usedISOYear ) {
1515 // December 28th falls on the last ISO week of the year, every year.
1516 // The last ISO week of a year can be 52 or 53.
1517 $lastWeekOfISOYear = DateTime::createFromFormat(
1518 'Ymd',
1519 substr( $ts, 0, 4 ) . '1228',
1520 $zone ?: new DateTimeZone( 'UTC' )
1521 )->format( 'W' );
1522 $currentISOWeek = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'W' );
1523 $weeksRemaining = $lastWeekOfISOYear - $currentISOWeek;
1524 $timeRemainingInWeek =
1525 ( 7 - self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400
1526 + $timeRemainingInDay;
1527 $possibleTtls[] = $weeksRemaining * 604800 + $timeRemainingInWeek;
1528 }
1529
1530 if ( $usedMonth ) {
1531 $possibleTtls[] =
1532 ( self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 't' ) -
1533 substr( $ts, 6, 2 ) ) * 86400
1534 + $timeRemainingInDay;
1535 } elseif ( $usedYear ) {
1536 $possibleTtls[] =
1537 ( self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) + 364 -
1538 self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400
1539 + $timeRemainingInDay;
1540 } elseif ( $usedIsLeapYear ) {
1541 $year = substr( $ts, 0, 4 );
1542 $timeRemainingInYear =
1543 ( self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) + 364 -
1544 self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400
1545 + $timeRemainingInDay;
1546 $mod = $year % 4;
1547 if ( $mod || ( !( $year % 100 ) && $year % 400 ) ) {
1548 // this isn't a leap year. see when the next one starts
1549 $nextCandidate = $year - $mod + 4;
1550 if ( $nextCandidate % 100 || !( $nextCandidate % 400 ) ) {
1551 $possibleTtls[] = ( $nextCandidate - $year - 1 ) * 365 * 86400 +
1552 $timeRemainingInYear;
1553 } else {
1554 $possibleTtls[] = ( $nextCandidate - $year + 3 ) * 365 * 86400 +
1555 $timeRemainingInYear;
1556 }
1557 } else {
1558 // this is a leap year, so the next year isn't
1559 $possibleTtls[] = $timeRemainingInYear;
1560 }
1561 }
1562
1563 if ( $possibleTtls ) {
1564 $ttl = min( $possibleTtls );
1565 }
1566 }
1567
1568 return $s;
1569 }
1570
1571 private static $GREG_DAYS = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
1572 private static $IRANIAN_DAYS = [ 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 ];
1573
1586 private static function tsToIranian( $ts ) {
1587 $gy = substr( $ts, 0, 4 ) - 1600;
1588 $gm = substr( $ts, 4, 2 ) - 1;
1589 $gd = substr( $ts, 6, 2 ) - 1;
1590
1591 # Days passed from the beginning (including leap years)
1592 $gDayNo = 365 * $gy
1593 + floor( ( $gy + 3 ) / 4 )
1594 - floor( ( $gy + 99 ) / 100 )
1595 + floor( ( $gy + 399 ) / 400 );
1596
1597 // Add days of the past months of this year
1598 for ( $i = 0; $i < $gm; $i++ ) {
1599 $gDayNo += self::$GREG_DAYS[$i];
1600 }
1601
1602 // Leap years
1603 if ( $gm > 1 && ( ( $gy % 4 === 0 && $gy % 100 !== 0 || ( $gy % 400 == 0 ) ) ) ) {
1604 $gDayNo++;
1605 }
1606
1607 // Days passed in current month
1608 $gDayNo += (int)$gd;
1609
1610 $jDayNo = $gDayNo - 79;
1611
1612 $jNp = floor( $jDayNo / 12053 );
1613 $jDayNo %= 12053;
1614
1615 $jy = 979 + 33 * $jNp + 4 * floor( $jDayNo / 1461 );
1616 $jDayNo %= 1461;
1617
1618 if ( $jDayNo >= 366 ) {
1619 $jy += floor( ( $jDayNo - 1 ) / 365 );
1620 $jDayNo = floor( ( $jDayNo - 1 ) % 365 );
1621 }
1622
1623 $jz = $jDayNo;
1624
1625 for ( $i = 0; $i < 11 && $jDayNo >= self::$IRANIAN_DAYS[$i]; $i++ ) {
1626 $jDayNo -= self::$IRANIAN_DAYS[$i];
1627 }
1628
1629 $jm = $i + 1;
1630 $jd = $jDayNo + 1;
1631
1632 return [ $jy, $jm, $jd, $jz ];
1633 }
1634
1646 private static function tsToHijri( $ts ) {
1647 $year = substr( $ts, 0, 4 );
1648 $month = substr( $ts, 4, 2 );
1649 $day = substr( $ts, 6, 2 );
1650
1651 $zyr = $year;
1652 $zd = $day;
1653 $zm = $month;
1654 $zy = $zyr;
1655
1656 if (
1657 ( $zy > 1582 ) || ( ( $zy == 1582 ) && ( $zm > 10 ) ) ||
1658 ( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) )
1659 ) {
1660 $zjd = (int)( ( 1461 * ( $zy + 4800 + (int)( ( $zm - 14 ) / 12 ) ) ) / 4 ) +
1661 (int)( ( 367 * ( $zm - 2 - 12 * ( (int)( ( $zm - 14 ) / 12 ) ) ) ) / 12 ) -
1662 (int)( ( 3 * (int)( ( ( $zy + 4900 + (int)( ( $zm - 14 ) / 12 ) ) / 100 ) ) ) / 4 ) +
1663 $zd - 32075;
1664 } else {
1665 $zjd = 367 * $zy - (int)( ( 7 * ( $zy + 5001 + (int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) +
1666 (int)( ( 275 * $zm ) / 9 ) + $zd + 1729777;
1667 }
1668
1669 $zl = $zjd - 1948440 + 10632;
1670 $zn = (int)( ( $zl - 1 ) / 10631 );
1671 $zl = $zl - 10631 * $zn + 354;
1672 $zj = ( (int)( ( 10985 - $zl ) / 5316 ) ) * ( (int)( ( 50 * $zl ) / 17719 ) ) +
1673 ( (int)( $zl / 5670 ) ) * ( (int)( ( 43 * $zl ) / 15238 ) );
1674 $zl = $zl - ( (int)( ( 30 - $zj ) / 15 ) ) * ( (int)( ( 17719 * $zj ) / 50 ) ) -
1675 ( (int)( $zj / 16 ) ) * ( (int)( ( 15238 * $zj ) / 43 ) ) + 29;
1676 $zm = (int)( ( 24 * $zl ) / 709 );
1677 $zd = $zl - (int)( ( 709 * $zm ) / 24 );
1678 $zy = 30 * $zn + $zj - 30;
1679
1680 return [ $zy, $zm, $zd ];
1681 }
1682
1698 private static function tsToHebrew( $ts ) {
1699 # Parse date
1700 $year = substr( $ts, 0, 4 );
1701 $month = substr( $ts, 4, 2 );
1702 $day = substr( $ts, 6, 2 );
1703
1704 # Calculate Hebrew year
1705 $hebrewYear = $year + 3760;
1706
1707 # Month number when September = 1, August = 12
1708 $month += 4;
1709 if ( $month > 12 ) {
1710 # Next year
1711 $month -= 12;
1712 $year++;
1713 $hebrewYear++;
1714 }
1715
1716 # Calculate day of year from 1 September
1717 $dayOfYear = $day;
1718 for ( $i = 1; $i < $month; $i++ ) {
1719 if ( $i == 6 ) {
1720 # February
1721 $dayOfYear += 28;
1722 # Check if the year is leap
1723 if ( $year % 400 == 0 || ( $year % 4 == 0 && $year % 100 > 0 ) ) {
1724 $dayOfYear++;
1725 }
1726 } elseif ( $i == 8 || $i == 10 || $i == 1 || $i == 3 ) {
1727 $dayOfYear += 30;
1728 } else {
1729 $dayOfYear += 31;
1730 }
1731 }
1732
1733 # Calculate the start of the Hebrew year
1734 $start = self::hebrewYearStart( $hebrewYear );
1735
1736 # Calculate next year's start
1737 if ( $dayOfYear <= $start ) {
1738 # Day is before the start of the year - it is the previous year
1739 # Next year's start
1740 $nextStart = $start;
1741 # Previous year
1742 $year--;
1743 $hebrewYear--;
1744 # Add days since previous year's 1 September
1745 $dayOfYear += 365;
1746 if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1747 # Leap year
1748 $dayOfYear++;
1749 }
1750 # Start of the new (previous) year
1751 $start = self::hebrewYearStart( $hebrewYear );
1752 } else {
1753 # Next year's start
1754 $nextStart = self::hebrewYearStart( $hebrewYear + 1 );
1755 }
1756
1757 # Calculate Hebrew day of year
1758 $hebrewDayOfYear = $dayOfYear - $start;
1759
1760 # Difference between year's days
1761 $diff = $nextStart - $start;
1762 # Add 12 (or 13 for leap years) days to ignore the difference between
1763 # Hebrew and Gregorian year (353 at least vs. 365/6) - now the
1764 # difference is only about the year type
1765 if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1766 $diff += 13;
1767 } else {
1768 $diff += 12;
1769 }
1770
1771 # Check the year pattern, and is leap year
1772 # 0 means an incomplete year, 1 means a regular year, 2 means a complete year
1773 # This is mod 30, to work on both leap years (which add 30 days of Adar I)
1774 # and non-leap years
1775 $yearPattern = $diff % 30;
1776 # Check if leap year
1777 $isLeap = $diff >= 30;
1778
1779 # Calculate day in the month from number of day in the Hebrew year
1780 # Don't check Adar - if the day is not in Adar, we will stop before;
1781 # if it is in Adar, we will use it to check if it is Adar I or Adar II
1782 $hebrewDay = $hebrewDayOfYear;
1783 $hebrewMonth = 1;
1784 $days = 0;
1785 while ( $hebrewMonth <= 12 ) {
1786 # Calculate days in this month
1787 if ( $isLeap && $hebrewMonth == 6 ) {
1788 # Adar in a leap year
1789 if ( $isLeap ) {
1790 # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1791 $days = 30;
1792 if ( $hebrewDay <= $days ) {
1793 # Day in Adar I
1794 $hebrewMonth = 13;
1795 } else {
1796 # Subtract the days of Adar I
1797 $hebrewDay -= $days;
1798 # Try Adar II
1799 $days = 29;
1800 if ( $hebrewDay <= $days ) {
1801 # Day in Adar II
1802 $hebrewMonth = 14;
1803 }
1804 }
1805 }
1806 } elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
1807 # Cheshvan in a complete year (otherwise as the rule below)
1808 $days = 30;
1809 } elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) {
1810 # Kislev in an incomplete year (otherwise as the rule below)
1811 $days = 29;
1812 } else {
1813 # Odd months have 30 days, even have 29
1814 $days = 30 - ( $hebrewMonth - 1 ) % 2;
1815 }
1816 if ( $hebrewDay <= $days ) {
1817 # In the current month
1818 break;
1819 } else {
1820 # Subtract the days of the current month
1821 $hebrewDay -= $days;
1822 # Try in the next month
1823 $hebrewMonth++;
1824 }
1825 }
1826
1827 return [ $hebrewYear, $hebrewMonth, $hebrewDay, $days ];
1828 }
1829
1839 private static function hebrewYearStart( $year ) {
1840 $a = intval( ( 12 * ( $year - 1 ) + 17 ) % 19 );
1841 $b = intval( ( $year - 1 ) % 4 );
1842 $m = 32.044093161144 + 1.5542417966212 * $a + $b / 4.0 - 0.0031777940220923 * ( $year - 1 );
1843 if ( $m < 0 ) {
1844 $m--;
1845 }
1846 $Mar = intval( $m );
1847 if ( $m < 0 ) {
1848 $m++;
1849 }
1850 $m -= $Mar;
1851
1852 $c = intval( ( $Mar + 3 * ( $year - 1 ) + 5 * $b + 5 ) % 7 );
1853 if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1854 $Mar++;
1855 } elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1856 $Mar += 2;
1857 } elseif ( $c == 2 || $c == 4 || $c == 6 ) {
1858 $Mar++;
1859 }
1860
1861 $Mar += intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1862 return $Mar;
1863 }
1864
1877 private static function tsToYear( $ts, $cName ) {
1878 $gy = substr( $ts, 0, 4 );
1879 $gm = substr( $ts, 4, 2 );
1880 $gd = substr( $ts, 6, 2 );
1881
1882 if ( !strcmp( $cName, 'thai' ) ) {
1883 # Thai solar dates
1884 # Add 543 years to the Gregorian calendar
1885 # Months and days are identical
1886 $gy_offset = $gy + 543;
1887 } elseif ( ( !strcmp( $cName, 'minguo' ) ) || !strcmp( $cName, 'juche' ) ) {
1888 # Minguo dates
1889 # Deduct 1911 years from the Gregorian calendar
1890 # Months and days are identical
1891 $gy_offset = $gy - 1911;
1892 } elseif ( !strcmp( $cName, 'tenno' ) ) {
1893 # Nengō dates up to Meiji period
1894 # Deduct years from the Gregorian calendar
1895 # depending on the nengo periods
1896 # Months and days are identical
1897 if ( ( $gy < 1912 )
1898 || ( ( $gy == 1912 ) && ( $gm < 7 ) )
1899 || ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd < 31 ) )
1900 ) {
1901 # Meiji period
1902 $gy_gannen = $gy - 1868 + 1;
1903 $gy_offset = $gy_gannen;
1904 if ( $gy_gannen == 1 ) {
1905 $gy_offset = '元';
1906 }
1907 $gy_offset = '明治' . $gy_offset;
1908 } elseif (
1909 ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd == 31 ) ) ||
1910 ( ( $gy == 1912 ) && ( $gm >= 8 ) ) ||
1911 ( ( $gy > 1912 ) && ( $gy < 1926 ) ) ||
1912 ( ( $gy == 1926 ) && ( $gm < 12 ) ) ||
1913 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd < 26 ) )
1914 ) {
1915 # Taishō period
1916 $gy_gannen = $gy - 1912 + 1;
1917 $gy_offset = $gy_gannen;
1918 if ( $gy_gannen == 1 ) {
1919 $gy_offset = '元';
1920 }
1921 $gy_offset = '大正' . $gy_offset;
1922 } elseif (
1923 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd >= 26 ) ) ||
1924 ( ( $gy > 1926 ) && ( $gy < 1989 ) ) ||
1925 ( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd < 8 ) )
1926 ) {
1927 # Shōwa period
1928 $gy_gannen = $gy - 1926 + 1;
1929 $gy_offset = $gy_gannen;
1930 if ( $gy_gannen == 1 ) {
1931 $gy_offset = '元';
1932 }
1933 $gy_offset = '昭和' . $gy_offset;
1934 } elseif (
1935 ( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd >= 8 ) ) ||
1936 ( ( $gy > 1989 ) && ( $gy < 2019 ) ) ||
1937 ( ( $gy == 2019 ) && ( $gm < 5 ) )
1938 ) {
1939 # Heisei period
1940 $gy_gannen = $gy - 1989 + 1;
1941 $gy_offset = $gy_gannen;
1942 if ( $gy_gannen == 1 ) {
1943 $gy_offset = '元';
1944 }
1945 $gy_offset = '平成' . $gy_offset;
1946 } else {
1947 # Reiwa period
1948 $gy_gannen = $gy - 2019 + 1;
1949 $gy_offset = $gy_gannen;
1950 if ( $gy_gannen == 1 ) {
1951 $gy_offset = '元';
1952 }
1953 $gy_offset = '令和' . $gy_offset;
1954 }
1955 } else {
1956 $gy_offset = $gy;
1957 }
1958
1959 return [ $gy_offset, $gm, $gd ];
1960 }
1961
1975 private static function strongDirFromContent( $text = '' ) {
1976 if ( !preg_match( self::$strongDirRegex, $text, $matches ) ) {
1977 return null;
1978 }
1979 if ( $matches[1] === '' ) {
1980 return 'rtl';
1981 }
1982 return 'ltr';
1983 }
1984
1992 static function romanNumeral( $num ) {
1993 static $table = [
1994 [ '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ],
1995 [ '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ],
1996 [ '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ],
1997 [ '', 'M', 'MM', 'MMM', 'MMMM', 'MMMMM', 'MMMMMM', 'MMMMMMM',
1998 'MMMMMMMM', 'MMMMMMMMM', 'MMMMMMMMMM' ]
1999 ];
2000
2001 $num = intval( $num );
2002 if ( $num > 10000 || $num <= 0 ) {
2003 return $num;
2004 }
2005
2006 $s = '';
2007 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
2008 if ( $num >= $pow10 ) {
2009 $s .= $table[$i][(int)floor( $num / $pow10 )];
2010 }
2011 $num = $num % $pow10;
2012 }
2013 return $s;
2014 }
2015
2023 static function hebrewNumeral( $num ) {
2024 static $table = [
2025 [ '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ],
2026 [ '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ],
2027 [ '',
2028 [ 'ק' ],
2029 [ 'ר' ],
2030 [ 'ש' ],
2031 [ 'ת' ],
2032 [ 'ת', 'ק' ],
2033 [ 'ת', 'ר' ],
2034 [ 'ת', 'ש' ],
2035 [ 'ת', 'ת' ],
2036 [ 'ת', 'ת', 'ק' ],
2037 [ 'ת', 'ת', 'ר' ],
2038 ],
2039 [ '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ]
2040 ];
2041
2042 $num = intval( $num );
2043 if ( $num > 9999 || $num <= 0 ) {
2044 return $num;
2045 }
2046
2047 // Round thousands have special notations
2048 if ( $num === 1000 ) {
2049 return "א' אלף";
2050 } elseif ( $num % 1000 === 0 ) {
2051 return $table[0][$num / 1000] . "' אלפים";
2052 }
2053
2054 $letters = [];
2055
2056 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
2057 if ( $num >= $pow10 ) {
2058 if ( $num === 15 || $num === 16 ) {
2059 $letters[] = $table[0][9];
2060 $letters[] = $table[0][$num - 9];
2061 $num = 0;
2062 } else {
2063 $letters = array_merge(
2064 $letters,
2065 (array)$table[$i][intval( $num / $pow10 )]
2066 );
2067
2068 if ( $pow10 === 1000 ) {
2069 $letters[] = "'";
2070 }
2071 }
2072 }
2073
2074 $num = $num % $pow10;
2075 }
2076
2077 $preTransformLength = count( $letters );
2078 if ( $preTransformLength === 1 ) {
2079 // Add geresh (single quote) to one-letter numbers
2080 $letters[] = "'";
2081 } else {
2082 $lastIndex = $preTransformLength - 1;
2083 $letters[$lastIndex] = str_replace(
2084 [ 'כ', 'מ', 'נ', 'פ', 'צ' ],
2085 [ 'ך', 'ם', 'ן', 'ף', 'ץ' ],
2086 $letters[$lastIndex]
2087 );
2088
2089 // Add gershayim (double quote) to multiple-letter numbers,
2090 // but exclude numbers with only one letter after the thousands
2091 // (1001-1009, 1020, 1030, 2001-2009, etc.)
2092 if ( $letters[1] === "'" && $preTransformLength === 3 ) {
2093 $letters[] = "'";
2094 } else {
2095 array_splice( $letters, -1, 0, '"' );
2096 }
2097 }
2098
2099 return implode( $letters );
2100 }
2101
2110 public function userAdjust( $ts, $tz = false ) {
2112
2113 if ( $tz === false ) {
2114 $tz = $wgUser->getOption( 'timecorrection' );
2115 }
2116
2117 $data = explode( '|', $tz, 3 );
2118
2119 if ( $data[0] == 'ZoneInfo' ) {
2120 try {
2121 $userTZ = new DateTimeZone( $data[2] );
2122 $date = new DateTime( $ts, new DateTimeZone( 'UTC' ) );
2123 $date->setTimezone( $userTZ );
2124 return $date->format( 'YmdHis' );
2125 } catch ( Exception $e ) {
2126 // Unrecognized timezone, default to 'Offset' with the stored offset.
2127 $data[0] = 'Offset';
2128 }
2129 }
2130
2131 if ( $data[0] == 'System' || $tz == '' ) {
2132 # Global offset in minutes.
2133 $minDiff = $wgLocalTZoffset;
2134 } elseif ( $data[0] == 'Offset' ) {
2135 $minDiff = intval( $data[1] );
2136 } else {
2137 $data = explode( ':', $tz );
2138 if ( count( $data ) == 2 ) {
2139 $data[0] = intval( $data[0] );
2140 $data[1] = intval( $data[1] );
2141 $minDiff = abs( $data[0] ) * 60 + $data[1];
2142 if ( $data[0] < 0 ) {
2143 $minDiff = -$minDiff;
2144 }
2145 } else {
2146 $minDiff = intval( $data[0] ) * 60;
2147 }
2148 }
2149
2150 # No difference ? Return time unchanged
2151 if ( 0 == $minDiff ) {
2152 return $ts;
2153 }
2154
2155 Wikimedia\suppressWarnings(); // E_STRICT system time bitching
2156 # Generate an adjusted date; take advantage of the fact that mktime
2157 # will normalize out-of-range values so we don't have to split $minDiff
2158 # into hours and minutes.
2159 $t = mktime( (
2160 (int)substr( $ts, 8, 2 ) ), # Hours
2161 (int)substr( $ts, 10, 2 ) + $minDiff, # Minutes
2162 (int)substr( $ts, 12, 2 ), # Seconds
2163 (int)substr( $ts, 4, 2 ), # Month
2164 (int)substr( $ts, 6, 2 ), # Day
2165 (int)substr( $ts, 0, 4 ) ); # Year
2166
2167 $date = date( 'YmdHis', $t );
2168 Wikimedia\restoreWarnings();
2169
2170 return $date;
2171 }
2172
2188 function dateFormat( $usePrefs = true ) {
2190
2191 if ( is_bool( $usePrefs ) ) {
2192 if ( $usePrefs ) {
2193 $datePreference = $wgUser->getDatePreference();
2194 } else {
2195 $datePreference = (string)User::getDefaultOption( 'date' );
2196 }
2197 } else {
2198 $datePreference = (string)$usePrefs;
2199 }
2200
2201 // return int
2202 if ( $datePreference == '' ) {
2203 return 'default';
2204 }
2205
2206 return $datePreference;
2207 }
2208
2219 function getDateFormatString( $type, $pref ) {
2220 $wasDefault = false;
2221 if ( $pref == 'default' ) {
2222 $wasDefault = true;
2223 $pref = $this->getDefaultDateFormat();
2224 }
2225
2226 if ( !isset( $this->dateFormatStrings[$type][$pref] ) ) {
2227 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2228
2229 if ( $type === 'pretty' && $df === null ) {
2230 $df = $this->getDateFormatString( 'date', $pref );
2231 }
2232
2233 if ( !$wasDefault && $df === null ) {
2234 $pref = $this->getDefaultDateFormat();
2235 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2236 }
2237
2238 $this->dateFormatStrings[$type][$pref] = $df;
2239 }
2240 return $this->dateFormatStrings[$type][$pref];
2241 }
2242
2253 public function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
2254 $ts = wfTimestamp( TS_MW, $ts );
2255 if ( $adj ) {
2256 $ts = $this->userAdjust( $ts, $timecorrection );
2257 }
2258 $df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
2259 return $this->sprintfDate( $df, $ts );
2260 }
2261
2272 public function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
2273 $ts = wfTimestamp( TS_MW, $ts );
2274 if ( $adj ) {
2275 $ts = $this->userAdjust( $ts, $timecorrection );
2276 }
2277 $df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
2278 return $this->sprintfDate( $df, $ts );
2279 }
2280
2292 public function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false ) {
2293 $ts = wfTimestamp( TS_MW, $ts );
2294 if ( $adj ) {
2295 $ts = $this->userAdjust( $ts, $timecorrection );
2296 }
2297 $df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) );
2298 return $this->sprintfDate( $df, $ts );
2299 }
2300
2311 public function formatDuration( $seconds, array $chosenIntervals = [] ) {
2312 $intervals = $this->getDurationIntervals( $seconds, $chosenIntervals );
2313
2314 $segments = [];
2315
2316 foreach ( $intervals as $intervalName => $intervalValue ) {
2317 // Messages: duration-seconds, duration-minutes, duration-hours, duration-days, duration-weeks,
2318 // duration-years, duration-decades, duration-centuries, duration-millennia
2319 $message = wfMessage( 'duration-' . $intervalName )->numParams( $intervalValue );
2320 $segments[] = $message->inLanguage( $this )->escaped();
2321 }
2322
2323 return $this->listToText( $segments );
2324 }
2325
2337 public function getDurationIntervals( $seconds, array $chosenIntervals = [] ) {
2338 if ( empty( $chosenIntervals ) ) {
2339 $chosenIntervals = [
2340 'millennia',
2341 'centuries',
2342 'decades',
2343 'years',
2344 'days',
2345 'hours',
2346 'minutes',
2347 'seconds'
2348 ];
2349 }
2350
2351 $intervals = array_intersect_key( self::$durationIntervals, array_flip( $chosenIntervals ) );
2352 $sortedNames = array_keys( $intervals );
2353 $smallestInterval = array_pop( $sortedNames );
2354
2355 $segments = [];
2356
2357 foreach ( $intervals as $name => $length ) {
2358 $value = floor( $seconds / $length );
2359
2360 if ( $value > 0 || ( $name == $smallestInterval && empty( $segments ) ) ) {
2361 $seconds -= $value * $length;
2362 $segments[$name] = $value;
2363 }
2364 }
2365
2366 return $segments;
2367 }
2368
2388 private function internalUserTimeAndDate( $type, $ts, User $user, array $options ) {
2389 $ts = wfTimestamp( TS_MW, $ts );
2390 $options += [ 'timecorrection' => true, 'format' => true ];
2391 if ( $options['timecorrection'] !== false ) {
2392 if ( $options['timecorrection'] === true ) {
2393 $offset = $user->getOption( 'timecorrection' );
2394 } else {
2395 $offset = $options['timecorrection'];
2396 }
2397 $ts = $this->userAdjust( $ts, $offset );
2398 }
2399 if ( $options['format'] === true ) {
2400 $format = $user->getDatePreference();
2401 } else {
2402 $format = $options['format'];
2403 }
2404 $df = $this->getDateFormatString( $type, $this->dateFormat( $format ) );
2405 return $this->sprintfDate( $df, $ts );
2406 }
2407
2427 public function userDate( $ts, User $user, array $options = [] ) {
2428 return $this->internalUserTimeAndDate( 'date', $ts, $user, $options );
2429 }
2430
2450 public function userTime( $ts, User $user, array $options = [] ) {
2451 return $this->internalUserTimeAndDate( 'time', $ts, $user, $options );
2452 }
2453
2473 public function userTimeAndDate( $ts, User $user, array $options = [] ) {
2474 return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
2475 }
2476
2492 public function getHumanTimestamp(
2493 MWTimestamp $time, MWTimestamp $relativeTo = null, User $user = null
2494 ) {
2495 if ( $relativeTo === null ) {
2496 $relativeTo = new MWTimestamp();
2497 }
2498 if ( $user === null ) {
2499 $user = RequestContext::getMain()->getUser();
2500 }
2501
2502 // Adjust for the user's timezone.
2503 $offsetThis = $time->offsetForUser( $user );
2504 $offsetRel = $relativeTo->offsetForUser( $user );
2505
2506 $ts = '';
2507 if ( Hooks::run( 'GetHumanTimestamp', [ &$ts, $time, $relativeTo, $user, $this ] ) ) {
2508 $ts = $this->getHumanTimestampInternal( $time, $relativeTo, $user );
2509 }
2510
2511 // Reset the timezone on the objects.
2512 $time->timestamp->sub( $offsetThis );
2513 $relativeTo->timestamp->sub( $offsetRel );
2514
2515 return $ts;
2516 }
2517
2530 MWTimestamp $ts, MWTimestamp $relativeTo, User $user
2531 ) {
2532 $diff = $ts->diff( $relativeTo );
2533 $diffDay = (bool)( (int)$ts->timestamp->format( 'w' ) -
2534 (int)$relativeTo->timestamp->format( 'w' ) );
2535 $days = $diff->days ?: (int)$diffDay;
2536 if ( $diff->invert || $days > 5
2537 && $ts->timestamp->format( 'Y' ) !== $relativeTo->timestamp->format( 'Y' )
2538 ) {
2539 // Timestamps are in different years: use full timestamp
2540 // Also do full timestamp for future dates
2544 $format = $this->getDateFormatString( 'both', $user->getDatePreference() ?: 'default' );
2545 $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
2546 } elseif ( $days > 5 ) {
2547 // Timestamps are in same year, but more than 5 days ago: show day and month only.
2548 $format = $this->getDateFormatString( 'pretty', $user->getDatePreference() ?: 'default' );
2549 $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
2550 } elseif ( $days > 1 ) {
2551 // Timestamp within the past week: show the day of the week and time
2552 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2553 $weekday = self::$mWeekdayMsgs[$ts->timestamp->format( 'w' )];
2554 // Messages:
2555 // sunday-at, monday-at, tuesday-at, wednesday-at, thursday-at, friday-at, saturday-at
2556 $ts = wfMessage( "$weekday-at" )
2557 ->inLanguage( $this )
2558 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2559 ->text();
2560 } elseif ( $days == 1 ) {
2561 // Timestamp was yesterday: say 'yesterday' and the time.
2562 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2563 $ts = wfMessage( 'yesterday-at' )
2564 ->inLanguage( $this )
2565 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2566 ->text();
2567 } elseif ( $diff->h > 1 || $diff->h == 1 && $diff->i > 30 ) {
2568 // Timestamp was today, but more than 90 minutes ago: say 'today' and the time.
2569 $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2570 $ts = wfMessage( 'today-at' )
2571 ->inLanguage( $this )
2572 ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2573 ->text();
2574
2575 // From here on in, the timestamp was soon enough ago so that we can simply say
2576 // XX units ago, e.g., "2 hours ago" or "5 minutes ago"
2577 } elseif ( $diff->h == 1 ) {
2578 // Less than 90 minutes, but more than an hour ago.
2579 $ts = wfMessage( 'hours-ago' )->inLanguage( $this )->numParams( 1 )->text();
2580 } elseif ( $diff->i >= 1 ) {
2581 // A few minutes ago.
2582 $ts = wfMessage( 'minutes-ago' )->inLanguage( $this )->numParams( $diff->i )->text();
2583 } elseif ( $diff->s >= 30 ) {
2584 // Less than a minute, but more than 30 sec ago.
2585 $ts = wfMessage( 'seconds-ago' )->inLanguage( $this )->numParams( $diff->s )->text();
2586 } else {
2587 // Less than 30 seconds ago.
2588 $ts = wfMessage( 'just-now' )->text();
2589 }
2590
2591 return $ts;
2592 }
2593
2598 public function getMessage( $key ) {
2599 return self::$dataCache->getSubitem( $this->mCode, 'messages', $key );
2600 }
2601
2605 function getAllMessages() {
2606 return self::$dataCache->getItem( $this->mCode, 'messages' );
2607 }
2608
2615 public function iconv( $in, $out, $string ) {
2616 # Even with //IGNORE iconv can whine about illegal characters in
2617 # *input* string. We just ignore those too.
2618 # REF: https://bugs.php.net/bug.php?id=37166
2619 # REF: https://phabricator.wikimedia.org/T18885
2620 Wikimedia\suppressWarnings();
2621 $text = iconv( $in, $out . '//IGNORE', $string );
2622 Wikimedia\restoreWarnings();
2623 return $text;
2624 }
2625
2626 // callback functions for ucwords(), ucwordbreaks()
2627
2633 return $this->ucfirst( $matches[1] );
2634 }
2635
2641 return mb_strtoupper( $matches[0] );
2642 }
2643
2649 return mb_strtoupper( $matches[0] );
2650 }
2651
2659 public function ucfirst( $str ) {
2660 $o = ord( $str );
2661 if ( $o < 96 ) { // if already uppercase...
2662 return $str;
2663 } elseif ( $o < 128 ) {
2664 return ucfirst( $str ); // use PHP's ucfirst()
2665 } else {
2666 // fall back to more complex logic in case of multibyte strings
2667 return $this->uc( $str, true );
2668 }
2669 }
2670
2679 public function uc( $str, $first = false ) {
2680 if ( $first ) {
2681 if ( $this->isMultibyte( $str ) ) {
2682 return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2683 } else {
2684 return ucfirst( $str );
2685 }
2686 } else {
2687 return $this->isMultibyte( $str ) ? mb_strtoupper( $str ) : strtoupper( $str );
2688 }
2689 }
2690
2695 function lcfirst( $str ) {
2696 $o = ord( $str );
2697 if ( !$o ) {
2698 return strval( $str );
2699 } elseif ( $o >= 128 ) {
2700 return $this->lc( $str, true );
2701 } elseif ( $o > 96 ) {
2702 return $str;
2703 } else {
2704 $str[0] = strtolower( $str[0] );
2705 return $str;
2706 }
2707 }
2708
2714 function lc( $str, $first = false ) {
2715 if ( $first ) {
2716 if ( $this->isMultibyte( $str ) ) {
2717 return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2718 } else {
2719 return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
2720 }
2721 } else {
2722 return $this->isMultibyte( $str ) ? mb_strtolower( $str ) : strtolower( $str );
2723 }
2724 }
2725
2730 function isMultibyte( $str ) {
2731 return strlen( $str ) !== mb_strlen( $str );
2732 }
2733
2738 function ucwords( $str ) {
2739 if ( $this->isMultibyte( $str ) ) {
2740 $str = $this->lc( $str );
2741
2742 // regexp to find first letter in each word (i.e. after each space)
2743 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2744
2745 // function to use to capitalize a single char
2746 return preg_replace_callback(
2747 $replaceRegexp,
2748 [ $this, 'ucwordsCallbackMB' ],
2749 $str
2750 );
2751 } else {
2752 return ucwords( strtolower( $str ) );
2753 }
2754 }
2755
2762 function ucwordbreaks( $str ) {
2763 if ( $this->isMultibyte( $str ) ) {
2764 $str = $this->lc( $str );
2765
2766 // since \b doesn't work for UTF-8, we explicitely define word break chars
2767 $breaks = "[ \-\‍(\‍)\}\{\.,\?!]";
2768
2769 // find first letter after word break
2770 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|" .
2771 "$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2772
2773 return preg_replace_callback(
2774 $replaceRegexp,
2775 [ $this, 'ucwordbreaksCallbackMB' ],
2776 $str
2777 );
2778 } else {
2779 return preg_replace_callback(
2780 '/\b([\w\x80-\xff]+)\b/',
2781 [ $this, 'ucwordbreaksCallbackAscii' ],
2782 $str
2783 );
2784 }
2785 }
2786
2802 function caseFold( $s ) {
2803 return $this->uc( $s );
2804 }
2805
2812 if ( is_array( $s ) ) {
2813 throw new MWException( 'Given array to checkTitleEncoding.' );
2814 }
2815 if ( StringUtils::isUtf8( $s ) ) {
2816 return $s;
2817 }
2818
2819 return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s );
2820 }
2821
2826 return self::$dataCache->getItem( $this->mCode, 'fallback8bitEncoding' );
2827 }
2828
2837 function hasWordBreaks() {
2838 return true;
2839 }
2840
2848 function segmentByWord( $string ) {
2849 return $string;
2850 }
2851
2859 function normalizeForSearch( $string ) {
2860 return self::convertDoubleWidth( $string );
2861 }
2862
2871 protected static function convertDoubleWidth( $string ) {
2872 static $full = null;
2873 static $half = null;
2874
2875 if ( $full === null ) {
2876 $fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2877 $halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2878 $full = str_split( $fullWidth, 3 );
2879 $half = str_split( $halfWidth );
2880 }
2881
2882 $string = str_replace( $full, $half, $string );
2883 return $string;
2884 }
2885
2891 protected static function insertSpace( $string, $pattern ) {
2892 $string = preg_replace( $pattern, " $1 ", $string );
2893 $string = preg_replace( '/ +/', ' ', $string );
2894 return $string;
2895 }
2896
2901 function convertForSearchResult( $termsArray ) {
2902 # some languages, e.g. Chinese, need to do a conversion
2903 # in order for search results to be displayed correctly
2904 return $termsArray;
2905 }
2906
2913 function firstChar( $s ) {
2914 $matches = [];
2915 preg_match(
2916 '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
2917 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/',
2918 $s,
2919 $matches
2920 );
2921
2922 if ( isset( $matches[1] ) ) {
2923 if ( strlen( $matches[1] ) != 3 ) {
2924 return $matches[1];
2925 }
2926
2927 // Break down Hangul syllables to grab the first jamo
2928 $code = UtfNormal\Utils::utf8ToCodepoint( $matches[1] );
2929 if ( $code < 0xac00 || 0xd7a4 <= $code ) {
2930 return $matches[1];
2931 } elseif ( $code < 0xb098 ) {
2932 return "\xe3\x84\xb1";
2933 } elseif ( $code < 0xb2e4 ) {
2934 return "\xe3\x84\xb4";
2935 } elseif ( $code < 0xb77c ) {
2936 return "\xe3\x84\xb7";
2937 } elseif ( $code < 0xb9c8 ) {
2938 return "\xe3\x84\xb9";
2939 } elseif ( $code < 0xbc14 ) {
2940 return "\xe3\x85\x81";
2941 } elseif ( $code < 0xc0ac ) {
2942 return "\xe3\x85\x82";
2943 } elseif ( $code < 0xc544 ) {
2944 return "\xe3\x85\x85";
2945 } elseif ( $code < 0xc790 ) {
2946 return "\xe3\x85\x87";
2947 } elseif ( $code < 0xcc28 ) {
2948 return "\xe3\x85\x88";
2949 } elseif ( $code < 0xce74 ) {
2950 return "\xe3\x85\x8a";
2951 } elseif ( $code < 0xd0c0 ) {
2952 return "\xe3\x85\x8b";
2953 } elseif ( $code < 0xd30c ) {
2954 return "\xe3\x85\x8c";
2955 } elseif ( $code < 0xd558 ) {
2956 return "\xe3\x85\x8d";
2957 } else {
2958 return "\xe3\x85\x8e";
2959 }
2960 } else {
2961 return '';
2962 }
2963 }
2964
2968 function initEncoding() {
2969 // No-op.
2970 }
2971
2977 function recodeForEdit( $s ) {
2978 return $s;
2979 }
2980
2986 function recodeInput( $s ) {
2987 return $s;
2988 }
2989
3001 function normalize( $s ) {
3003 $s = UtfNormal\Validator::cleanUp( $s );
3004 if ( $wgAllUnicodeFixes ) {
3005 $s = $this->transformUsingPairFile( 'normalize-ar.ser', $s );
3006 $s = $this->transformUsingPairFile( 'normalize-ml.ser', $s );
3007 }
3008
3009 return $s;
3010 }
3011
3026 function transformUsingPairFile( $file, $string ) {
3027 if ( !isset( $this->transformData[$file] ) ) {
3028 $data = wfGetPrecompiledData( $file );
3029 if ( $data === false ) {
3030 throw new MWException( __METHOD__ . ": The transformation file $file is missing" );
3031 }
3032 $this->transformData[$file] = new ReplacementArray( $data );
3033 }
3034 return $this->transformData[$file]->replace( $string );
3035 }
3036
3042 function isRTL() {
3043 return self::$dataCache->getItem( $this->mCode, 'rtl' );
3044 }
3045
3050 function getDir() {
3051 return $this->isRTL() ? 'rtl' : 'ltr';
3052 }
3053
3062 function alignStart() {
3063 return $this->isRTL() ? 'right' : 'left';
3064 }
3065
3074 function alignEnd() {
3075 return $this->isRTL() ? 'left' : 'right';
3076 }
3077
3089 function getDirMarkEntity( $opposite = false ) {
3090 if ( $opposite ) {
3091 return $this->isRTL() ? '&lrm;' : '&rlm;';
3092 }
3093 return $this->isRTL() ? '&rlm;' : '&lrm;';
3094 }
3095
3106 function getDirMark( $opposite = false ) {
3107 $lrm = "\xE2\x80\x8E"; # LEFT-TO-RIGHT MARK, commonly abbreviated LRM
3108 $rlm = "\xE2\x80\x8F"; # RIGHT-TO-LEFT MARK, commonly abbreviated RLM
3109 if ( $opposite ) {
3110 return $this->isRTL() ? $lrm : $rlm;
3111 }
3112 return $this->isRTL() ? $rlm : $lrm;
3113 }
3114
3119 return self::$dataCache->getItem( $this->mCode, 'capitalizeAllNouns' );
3120 }
3121
3129 function getArrow( $direction = 'forwards' ) {
3130 switch ( $direction ) {
3131 case 'forwards':
3132 return $this->isRTL() ? '←' : '→';
3133 case 'backwards':
3134 return $this->isRTL() ? '→' : '←';
3135 case 'left':
3136 return '←';
3137 case 'right':
3138 return '→';
3139 case 'up':
3140 return '↑';
3141 case 'down':
3142 return '↓';
3143 }
3144 }
3145
3152 return self::$dataCache->getItem( $this->mCode, 'linkPrefixExtension' );
3153 }
3154
3159 function getMagicWords() {
3160 return self::$dataCache->getItem( $this->mCode, 'magicWords' );
3161 }
3162
3166 protected function doMagicHook() {
3167 if ( $this->mMagicHookDone ) {
3168 return;
3169 }
3170 $this->mMagicHookDone = true;
3171 Hooks::run( 'LanguageGetMagic', [ &$this->mMagicExtensions, $this->getCode() ] );
3172 }
3173
3179 function getMagic( $mw ) {
3180 // Saves a function call
3181 if ( !$this->mMagicHookDone ) {
3182 $this->doMagicHook();
3183 }
3184
3185 if ( isset( $this->mMagicExtensions[$mw->mId] ) ) {
3186 $rawEntry = $this->mMagicExtensions[$mw->mId];
3187 } else {
3188 $rawEntry = self::$dataCache->getSubitem(
3189 $this->mCode, 'magicWords', $mw->mId );
3190 }
3191
3192 if ( !is_array( $rawEntry ) ) {
3193 wfWarn( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
3194 } else {
3195 $mw->mCaseSensitive = $rawEntry[0];
3196 $mw->mSynonyms = array_slice( $rawEntry, 1 );
3197 }
3198 }
3199
3205 function addMagicWordsByLang( $newWords ) {
3206 $fallbackChain = $this->getFallbackLanguages();
3207 $fallbackChain = array_reverse( $fallbackChain );
3208 foreach ( $fallbackChain as $code ) {
3209 if ( isset( $newWords[$code] ) ) {
3210 $this->mMagicExtensions = $newWords[$code] + $this->mMagicExtensions;
3211 }
3212 }
3213 }
3214
3221 // Cache aliases because it may be slow to load them
3222 if ( is_null( $this->mExtendedSpecialPageAliases ) ) {
3223 // Initialise array
3224 $this->mExtendedSpecialPageAliases =
3225 self::$dataCache->getItem( $this->mCode, 'specialPageAliases' );
3226 Hooks::run( 'LanguageGetSpecialPageAliases',
3227 [ &$this->mExtendedSpecialPageAliases, $this->getCode() ] );
3228 }
3229
3230 return $this->mExtendedSpecialPageAliases;
3231 }
3232
3239 function emphasize( $text ) {
3240 return "<em>$text</em>";
3241 }
3242
3265 public function formatNum( $number, $nocommafy = false ) {
3267 if ( !$nocommafy ) {
3268 $number = $this->commafy( $number );
3269 $s = $this->separatorTransformTable();
3270 if ( $s ) {
3271 $number = strtr( $number, $s );
3272 }
3273 }
3274
3275 if ( $wgTranslateNumerals ) {
3276 $s = $this->digitTransformTable();
3277 if ( $s ) {
3278 $number = strtr( $number, $s );
3279 }
3280 }
3281
3282 return (string)$number;
3283 }
3284
3293 public function formatNumNoSeparators( $number ) {
3294 return $this->formatNum( $number, true );
3295 }
3296
3301 public function parseFormattedNumber( $number ) {
3302 $s = $this->digitTransformTable();
3303 if ( $s ) {
3304 // eliminate empty array values such as ''. (T66347)
3305 $s = array_filter( $s );
3306 $number = strtr( $number, array_flip( $s ) );
3307 }
3308
3309 $s = $this->separatorTransformTable();
3310 if ( $s ) {
3311 // eliminate empty array values such as ''. (T66347)
3312 $s = array_filter( $s );
3313 $number = strtr( $number, array_flip( $s ) );
3314 }
3315
3316 $number = strtr( $number, [ ',' => '' ] );
3317 return $number;
3318 }
3319
3326 function commafy( $number ) {
3329 if ( $number === null ) {
3330 return '';
3331 }
3332
3333 if ( !$digitGroupingPattern || $digitGroupingPattern === "###,###,###" ) {
3334 // Default grouping is at thousands, use the same for ###,###,### pattern too.
3335 // In some languages it's conventional not to insert a thousands separator
3336 // in numbers that are four digits long (1000-9999).
3337 if ( $minimumGroupingDigits ) {
3338 // Number of '#' characters after last comma in the grouping pattern.
3339 // The pattern is hardcoded here, but this would vary for different patterns.
3340 $primaryGroupingSize = 3;
3341 // Maximum length of a number to suppress digit grouping for.
3342 $maximumLength = $minimumGroupingDigits + $primaryGroupingSize - 1;
3343 if ( preg_match( '/^\-?\d{1,' . $maximumLength . '}(\.\d+)?$/', $number ) ) {
3344 return $number;
3345 }
3346 }
3347 return strrev( (string)preg_replace( '/(\d{3})(?=\d)(?!\d*\.)/', '$1,', strrev( $number ) ) );
3348 } else {
3349 // Ref: http://cldr.unicode.org/translation/number-patterns
3350 $sign = "";
3351 if ( intval( $number ) < 0 ) {
3352 // For negative numbers apply the algorithm like positive number and add sign.
3353 $sign = "-";
3354 $number = substr( $number, 1 );
3355 }
3356 $integerPart = [];
3357 $decimalPart = [];
3358 $numMatches = preg_match_all( "/(#+)/", $digitGroupingPattern, $matches );
3359 preg_match( "/\d+/", $number, $integerPart );
3360 preg_match( "/\.\d*/", $number, $decimalPart );
3361 $groupedNumber = ( count( $decimalPart ) > 0 ) ? $decimalPart[0] : "";
3362 if ( $groupedNumber === $number ) {
3363 // the string does not have any number part. Eg: .12345
3364 return $sign . $groupedNumber;
3365 }
3366 $start = $end = ( $integerPart ) ? strlen( $integerPart[0] ) : 0;
3367 while ( $start > 0 ) {
3368 $match = $matches[0][$numMatches - 1];
3369 $matchLen = strlen( $match );
3370 $start = $end - $matchLen;
3371 if ( $start < 0 ) {
3372 $start = 0;
3373 }
3374 $groupedNumber = substr( $number, $start, $end - $start ) . $groupedNumber;
3375 $end = $start;
3376 if ( $numMatches > 1 ) {
3377 // use the last pattern for the rest of the number
3378 $numMatches--;
3379 }
3380 if ( $start > 0 ) {
3381 $groupedNumber = "," . $groupedNumber;
3382 }
3383 }
3384 return $sign . $groupedNumber;
3385 }
3386 }
3387
3392 return self::$dataCache->getItem( $this->mCode, 'digitGroupingPattern' );
3393 }
3394
3399 return self::$dataCache->getItem( $this->mCode, 'digitTransformTable' );
3400 }
3401
3406 return self::$dataCache->getItem( $this->mCode, 'separatorTransformTable' );
3407 }
3408
3413 return self::$dataCache->getItem( $this->mCode, 'minimumGroupingDigits' );
3414 }
3415
3425 function listToText( array $l ) {
3426 $m = count( $l ) - 1;
3427 if ( $m < 0 ) {
3428 return '';
3429 }
3430 if ( $m > 0 ) {
3431 $and = $this->msg( 'and' )->escaped();
3432 $space = $this->msg( 'word-separator' )->escaped();
3433 if ( $m > 1 ) {
3434 $comma = $this->msg( 'comma-separator' )->escaped();
3435 }
3436 }
3437 $s = $l[$m];
3438 for ( $i = $m - 1; $i >= 0; $i-- ) {
3439 if ( $i == $m - 1 ) {
3440 $s = $l[$i] . $and . $space . $s;
3441 } else {
3442 $s = $l[$i] . $comma . $s;
3443 }
3444 }
3445 return $s;
3446 }
3447
3454 function commaList( array $list ) {
3455 return implode(
3456 wfMessage( 'comma-separator' )->inLanguage( $this )->escaped(),
3457 $list
3458 );
3459 }
3460
3467 function semicolonList( array $list ) {
3468 return implode(
3469 wfMessage( 'semicolon-separator' )->inLanguage( $this )->escaped(),
3470 $list
3471 );
3472 }
3473
3479 function pipeList( array $list ) {
3480 return implode(
3481 wfMessage( 'pipe-separator' )->inLanguage( $this )->escaped(),
3482 $list
3483 );
3484 }
3485
3503 function truncate( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3504 return $this->truncateForDatabase( $string, $length, $ellipsis, $adjustLength );
3505 }
3506
3522 function truncateForDatabase( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3523 return $this->truncateInternal(
3524 $string, $length, $ellipsis, $adjustLength, 'strlen', 'substr'
3525 );
3526 }
3527
3546 function truncateForVisual( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3547 // Passing encoding to mb_strlen and mb_substr is optional.
3548 // Encoding defaults to mb_internal_encoding(), which is set to UTF-8 in Setup.php, so
3549 // explicit specification of encoding is skipped.
3550 // Note: Both multibyte methods are callables invoked in truncateInternal.
3551 return $this->truncateInternal(
3552 $string, $length, $ellipsis, $adjustLength, 'mb_strlen', 'mb_substr'
3553 );
3554 }
3555
3572 private function truncateInternal(
3573 $string, $length, $ellipsis = '...', $adjustLength = true, $measureLength, $getSubstring
3574 ) {
3575 if ( !is_callable( $measureLength ) || !is_callable( $getSubstring ) ) {
3576 throw new InvalidArgumentException( 'Invalid callback provided' );
3577 }
3578
3579 # Check if there is no need to truncate
3580 if ( $measureLength( $string ) <= abs( $length ) ) {
3581 return $string; // no need to truncate
3582 }
3583
3584 # Use the localized ellipsis character
3585 if ( $ellipsis == '...' ) {
3586 $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3587 }
3588 if ( $length == 0 ) {
3589 return $ellipsis; // convention
3590 }
3591
3592 $stringOriginal = $string;
3593 # If ellipsis length is >= $length then we can't apply $adjustLength
3594 if ( $adjustLength && $measureLength( $ellipsis ) >= abs( $length ) ) {
3595 $string = $ellipsis; // this can be slightly unexpected
3596 # Otherwise, truncate and add ellipsis...
3597 } else {
3598 $ellipsisLength = $adjustLength ? $measureLength( $ellipsis ) : 0;
3599 if ( $length > 0 ) {
3600 $length -= $ellipsisLength;
3601 $string = $getSubstring( $string, 0, $length ); // xyz...
3602 $string = $this->removeBadCharLast( $string );
3603 $string = rtrim( $string );
3604 $string = $string . $ellipsis;
3605 } else {
3606 $length += $ellipsisLength;
3607 $string = $getSubstring( $string, $length ); // ...xyz
3608 $string = $this->removeBadCharFirst( $string );
3609 $string = ltrim( $string );
3610 $string = $ellipsis . $string;
3611 }
3612 }
3613
3614 # Do not truncate if the ellipsis makes the string longer/equal (T24181).
3615 # This check is *not* redundant if $adjustLength, due to the single case where
3616 # LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string.
3617 if ( $measureLength( $string ) < $measureLength( $stringOriginal ) ) {
3618 return $string;
3619 } else {
3620 return $stringOriginal;
3621 }
3622 }
3623
3631 protected function removeBadCharLast( $string ) {
3632 if ( $string != '' ) {
3633 $char = ord( $string[strlen( $string ) - 1] );
3634 $m = [];
3635 if ( $char >= 0xc0 ) {
3636 # We got the first byte only of a multibyte char; remove it.
3637 $string = substr( $string, 0, -1 );
3638 } elseif ( $char >= 0x80 &&
3639 // Use the /s modifier (PCRE_DOTALL) so (.*) also matches newlines
3640 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
3641 '[\xf0-\xf7][\x80-\xbf]{1,2})$/s', $string, $m )
3642 ) {
3643 # We chopped in the middle of a character; remove it
3644 $string = $m[1];
3645 }
3646 }
3647 return $string;
3648 }
3649
3657 protected function removeBadCharFirst( $string ) {
3658 if ( $string != '' ) {
3659 $char = ord( $string[0] );
3660 if ( $char >= 0x80 && $char < 0xc0 ) {
3661 # We chopped in the middle of a character; remove the whole thing
3662 $string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
3663 }
3664 }
3665 return $string;
3666 }
3667
3683 function truncateHtml( $text, $length, $ellipsis = '...' ) {
3684 # Use the localized ellipsis character
3685 if ( $ellipsis == '...' ) {
3686 $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3687 }
3688 # Check if there is clearly no need to truncate
3689 if ( $length <= 0 ) {
3690 return $ellipsis; // no text shown, nothing to format (convention)
3691 } elseif ( strlen( $text ) <= $length ) {
3692 return $text; // string short enough even *with* HTML (short-circuit)
3693 }
3694
3695 $dispLen = 0; // innerHTML legth so far
3696 $testingEllipsis = false; // checking if ellipses will make string longer/equal?
3697 $tagType = 0; // 0-open, 1-close
3698 $bracketState = 0; // 1-tag start, 2-tag name, 0-neither
3699 $entityState = 0; // 0-not entity, 1-entity
3700 $tag = $ret = ''; // accumulated tag name, accumulated result string
3701 $openTags = []; // open tag stack
3702 $maybeState = null; // possible truncation state
3703
3704 $textLen = strlen( $text );
3705 $neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated
3706 for ( $pos = 0; true; ++$pos ) {
3707 # Consider truncation once the display length has reached the maximim.
3708 # We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
3709 # Check that we're not in the middle of a bracket/entity...
3710 if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) {
3711 if ( !$testingEllipsis ) {
3712 $testingEllipsis = true;
3713 # Save where we are; we will truncate here unless there turn out to
3714 # be so few remaining characters that truncation is not necessary.
3715 if ( !$maybeState ) { // already saved? ($neLength = 0 case)
3716 $maybeState = [ $ret, $openTags ]; // save state
3717 }
3718 } elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) {
3719 # String in fact does need truncation, the truncation point was OK.
3720 list( $ret, $openTags ) = $maybeState; // reload state
3721 $ret = $this->removeBadCharLast( $ret ); // multi-byte char fix
3722 $ret .= $ellipsis; // add ellipsis
3723 break;
3724 }
3725 }
3726 if ( $pos >= $textLen ) {
3727 break; // extra iteration just for above checks
3728 }
3729
3730 # Read the next char...
3731 $ch = $text[$pos];
3732 $lastCh = $pos ? $text[$pos - 1] : '';
3733 $ret .= $ch; // add to result string
3734 if ( $ch == '<' ) {
3735 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); // for bad HTML
3736 $entityState = 0; // for bad HTML
3737 $bracketState = 1; // tag started (checking for backslash)
3738 } elseif ( $ch == '>' ) {
3739 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags );
3740 $entityState = 0; // for bad HTML
3741 $bracketState = 0; // out of brackets
3742 } elseif ( $bracketState == 1 ) {
3743 if ( $ch == '/' ) {
3744 $tagType = 1; // close tag (e.g. "</span>")
3745 } else {
3746 $tagType = 0; // open tag (e.g. "<span>")
3747 $tag .= $ch;
3748 }
3749 $bracketState = 2; // building tag name
3750 } elseif ( $bracketState == 2 ) {
3751 if ( $ch != ' ' ) {
3752 $tag .= $ch;
3753 } else {
3754 // Name found (e.g. "<a href=..."), add on tag attributes...
3755 $pos += $this->truncate_skip( $ret, $text, "<>", $pos + 1 );
3756 }
3757 } elseif ( $bracketState == 0 ) {
3758 if ( $entityState ) {
3759 if ( $ch == ';' ) {
3760 $entityState = 0;
3761 $dispLen++; // entity is one displayed char
3762 }
3763 } else {
3764 if ( $neLength == 0 && !$maybeState ) {
3765 // Save state without $ch. We want to *hit* the first
3766 // display char (to get tags) but not *use* it if truncating.
3767 $maybeState = [ substr( $ret, 0, -1 ), $openTags ];
3768 }
3769 if ( $ch == '&' ) {
3770 $entityState = 1; // entity found, (e.g. "&#160;")
3771 } else {
3772 $dispLen++; // this char is displayed
3773 // Add the next $max display text chars after this in one swoop...
3774 $max = ( $testingEllipsis ? $length : $neLength ) - $dispLen;
3775 $skipped = $this->truncate_skip( $ret, $text, "<>&", $pos + 1, $max );
3776 $dispLen += $skipped;
3777 $pos += $skipped;
3778 }
3779 }
3780 }
3781 }
3782 // Close the last tag if left unclosed by bad HTML
3783 $this->truncate_endBracket( $tag, $text[$textLen - 1], $tagType, $openTags );
3784 while ( count( $openTags ) > 0 ) {
3785 $ret .= '</' . array_pop( $openTags ) . '>'; // close open tags
3786 }
3787 return $ret;
3788 }
3789
3801 private function truncate_skip( &$ret, $text, $search, $start, $len = null ) {
3802 if ( $len === null ) {
3803 $len = -1; // -1 means "no limit" for strcspn
3804 } elseif ( $len < 0 ) {
3805 $len = 0; // sanity
3806 }
3807 $skipCount = 0;
3808 if ( $start < strlen( $text ) ) {
3809 $skipCount = strcspn( $text, $search, $start, $len );
3810 $ret .= substr( $text, $start, $skipCount );
3811 }
3812 return $skipCount;
3813 }
3814
3824 private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) {
3825 $tag = ltrim( $tag );
3826 if ( $tag != '' ) {
3827 if ( $tagType == 0 && $lastCh != '/' ) {
3828 $openTags[] = $tag; // tag opened (didn't close itself)
3829 } elseif ( $tagType == 1 ) {
3830 if ( $openTags && $tag == $openTags[count( $openTags ) - 1] ) {
3831 array_pop( $openTags ); // tag closed
3832 }
3833 }
3834 $tag = '';
3835 }
3836 }
3837
3846 function convertGrammar( $word, $case ) {
3848 if ( isset( $wgGrammarForms[$this->getCode()][$case][$word] ) ) {
3849 return $wgGrammarForms[$this->getCode()][$case][$word];
3850 }
3851
3853
3854 if ( isset( $grammarTransformations[$case] ) ) {
3855 $forms = $grammarTransformations[$case];
3856
3857 // Some names of grammar rules are aliases for other rules.
3858 // In such cases the value is a string rather than object,
3859 // so load the actual rules.
3860 if ( is_string( $forms ) ) {
3861 $forms = $grammarTransformations[$forms];
3862 }
3863
3864 foreach ( array_values( $forms ) as $rule ) {
3865 $form = $rule[0];
3866
3867 if ( $form === '@metadata' ) {
3868 continue;
3869 }
3870
3871 $replacement = $rule[1];
3872
3873 $regex = '/' . addcslashes( $form, '/' ) . '/u';
3874 $patternMatches = preg_match( $regex, $word );
3875
3876 if ( $patternMatches === false ) {
3878 'An error occurred while processing grammar. ' .
3879 "Word: '$word'. Regex: /$form/."
3880 );
3881 } elseif ( $patternMatches === 1 ) {
3882 $word = preg_replace( $regex, $replacement, $word );
3883
3884 break;
3885 }
3886 }
3887 }
3888
3889 return $word;
3890 }
3891
3897 function getGrammarForms() {
3899 if ( isset( $wgGrammarForms[$this->getCode()] )
3900 && is_array( $wgGrammarForms[$this->getCode()] )
3901 ) {
3902 return $wgGrammarForms[$this->getCode()];
3903 }
3904
3905 return [];
3906 }
3907
3917 public function getGrammarTransformations() {
3918 $languageCode = $this->getCode();
3919
3920 if ( self::$grammarTransformations === null ) {
3921 self::$grammarTransformations = new MapCacheLRU( 10 );
3922 }
3923
3924 if ( self::$grammarTransformations->has( $languageCode ) ) {
3925 return self::$grammarTransformations->get( $languageCode );
3926 }
3927
3928 $data = [];
3929
3930 $grammarDataFile = __DIR__ . "/data/grammarTransformations/$languageCode.json";
3931 if ( is_readable( $grammarDataFile ) ) {
3932 $data = FormatJson::decode(
3933 file_get_contents( $grammarDataFile ),
3934 true
3935 );
3936
3937 if ( $data === null ) {
3938 throw new MWException( "Invalid grammar data for \"$languageCode\"." );
3939 }
3940
3941 self::$grammarTransformations->set( $languageCode, $data );
3942 }
3943
3944 return $data;
3945 }
3946
3966 function gender( $gender, $forms ) {
3967 if ( !count( $forms ) ) {
3968 return '';
3969 }
3970 $forms = $this->preConvertPlural( $forms, 2 );
3971 if ( $gender === 'male' ) {
3972 return $forms[0];
3973 }
3974 if ( $gender === 'female' ) {
3975 return $forms[1];
3976 }
3977 return isset( $forms[2] ) ? $forms[2] : $forms[0];
3978 }
3979
3995 function convertPlural( $count, $forms ) {
3996 // Handle explicit n=pluralform cases
3997 $forms = $this->handleExplicitPluralForms( $count, $forms );
3998 if ( is_string( $forms ) ) {
3999 return $forms;
4000 }
4001 if ( !count( $forms ) ) {
4002 return '';
4003 }
4004
4005 $pluralForm = $this->getPluralRuleIndexNumber( $count );
4006 $pluralForm = min( $pluralForm, count( $forms ) - 1 );
4007 return $forms[$pluralForm];
4008 }
4009
4025 protected function handleExplicitPluralForms( $count, array $forms ) {
4026 foreach ( $forms as $index => $form ) {
4027 if ( preg_match( '/\d+=/i', $form ) ) {
4028 $pos = strpos( $form, '=' );
4029 if ( substr( $form, 0, $pos ) === (string)$count ) {
4030 return substr( $form, $pos + 1 );
4031 }
4032 unset( $forms[$index] );
4033 }
4034 }
4035 return array_values( $forms );
4036 }
4037
4046 protected function preConvertPlural( /* Array */ $forms, $count ) {
4047 while ( count( $forms ) < $count ) {
4048 $forms[] = $forms[count( $forms ) - 1];
4049 }
4050 return $forms;
4051 }
4052
4069 public function embedBidi( $text = '' ) {
4070 $dir = self::strongDirFromContent( $text );
4071 if ( $dir === 'ltr' ) {
4072 // Wrap in LEFT-TO-RIGHT EMBEDDING ... POP DIRECTIONAL FORMATTING
4073 return self::$lre . $text . self::$pdf;
4074 }
4075 if ( $dir === 'rtl' ) {
4076 // Wrap in RIGHT-TO-LEFT EMBEDDING ... POP DIRECTIONAL FORMATTING
4077 return self::$rle . $text . self::$pdf;
4078 }
4079 // No strong directionality: do not wrap
4080 return $text;
4081 }
4082
4096 function translateBlockExpiry( $str, User $user = null, $now = 0 ) {
4097 $duration = SpecialBlock::getSuggestedDurations( $this );
4098 foreach ( $duration as $show => $value ) {
4099 if ( strcmp( $str, $value ) == 0 ) {
4100 return trim( $show );
4101 }
4102 }
4103
4104 if ( wfIsInfinity( $str ) ) {
4105 foreach ( $duration as $show => $value ) {
4106 if ( wfIsInfinity( $value ) ) {
4107 return trim( $show );
4108 }
4109 }
4110 }
4111
4112 // If all else fails, return a standard duration or timestamp description.
4113 $time = strtotime( $str, $now );
4114 if ( $time === false ) { // Unknown format. Return it as-is in case.
4115 return $str;
4116 } elseif ( $time !== strtotime( $str, $now + 1 ) ) { // It's a relative timestamp.
4117 // The result differs based on current time, so the difference
4118 // is a fixed duration length.
4119 return $this->formatDuration( $time - $now );
4120 } else { // It's an absolute timestamp.
4121 if ( $time === 0 ) {
4122 // wfTimestamp() handles 0 as current time instead of epoch.
4123 $time = '19700101000000';
4124 }
4125 if ( $user ) {
4126 return $this->userTimeAndDate( $time, $user );
4127 }
4128 return $this->timeanddate( $time );
4129 }
4130 }
4131
4139 public function segmentForDiff( $text ) {
4140 return $text;
4141 }
4142
4149 public function unsegmentForDiff( $text ) {
4150 return $text;
4151 }
4152
4159 public function getConverter() {
4160 return $this->mConverter;
4161 }
4162
4171 public function autoConvert( $text, $variant = false ) {
4172 return $this->mConverter->autoConvert( $text, $variant );
4173 }
4174
4181 public function autoConvertToAllVariants( $text ) {
4182 return $this->mConverter->autoConvertToAllVariants( $text );
4183 }
4184
4191 public function convert( $text ) {
4192 return $this->mConverter->convert( $text );
4193 }
4194
4201 public function convertTitle( $title ) {
4202 return $this->mConverter->convertTitle( $title );
4203 }
4204
4213 public function convertNamespace( $ns, $variant = null ) {
4214 return $this->mConverter->convertNamespace( $ns, $variant );
4215 }
4216
4222 public function hasVariants() {
4223 return count( $this->getVariants() ) > 1;
4224 }
4225
4233 public function hasVariant( $variant ) {
4234 return (bool)$this->mConverter->validateVariant( $variant );
4235 }
4236
4244 public function convertHtml( $text, $isTitle = false ) {
4245 return htmlspecialchars( $this->convert( $text, $isTitle ) );
4246 }
4247
4252 public function convertCategoryKey( $key ) {
4253 return $this->mConverter->convertCategoryKey( $key );
4254 }
4255
4262 public function getVariants() {
4263 return $this->mConverter->getVariants();
4264 }
4265
4269 public function getPreferredVariant() {
4270 return $this->mConverter->getPreferredVariant();
4271 }
4272
4276 public function getDefaultVariant() {
4277 return $this->mConverter->getDefaultVariant();
4278 }
4279
4283 public function getURLVariant() {
4284 return $this->mConverter->getURLVariant();
4285 }
4286
4299 public function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
4300 $this->mConverter->findVariantLink( $link, $nt, $ignoreOtherCond );
4301 }
4302
4310 return $this->mConverter->getExtraHashOptions();
4311 }
4312
4320 public function getParsedTitle() {
4321 return $this->mConverter->getParsedTitle();
4322 }
4323
4330 public function updateConversionTable( Title $title ) {
4331 $this->mConverter->updateConversionTable( $title );
4332 }
4333
4346 public function markNoConversion( $text, $noParse = false ) {
4347 // Excluding protocal-relative URLs may avoid many false positives.
4348 if ( $noParse || preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
4349 return $this->mConverter->markNoConversion( $text );
4350 } else {
4351 return $text;
4352 }
4353 }
4354
4361 public function linkTrail() {
4362 return self::$dataCache->getItem( $this->mCode, 'linkTrail' );
4363 }
4364
4371 public function linkPrefixCharset() {
4372 return self::$dataCache->getItem( $this->mCode, 'linkPrefixCharset' );
4373 }
4374
4382 public function getParentLanguage() {
4383 if ( $this->mParentLanguage !== false ) {
4384 return $this->mParentLanguage;
4385 }
4386
4387 $code = explode( '-', $this->getCode() )[0];
4388 if ( !in_array( $code, LanguageConverter::$languagesWithVariants ) ) {
4389 $this->mParentLanguage = null;
4390 return null;
4391 }
4392 $lang = self::factory( $code );
4393 if ( !$lang->hasVariant( $this->getCode() ) ) {
4394 $this->mParentLanguage = null;
4395 return null;
4396 }
4397
4398 $this->mParentLanguage = $lang;
4399 return $lang;
4400 }
4401
4409 public function equals( Language $lang ) {
4410 return $lang->getCode() === $this->mCode;
4411 }
4412
4421 public function getCode() {
4422 return $this->mCode;
4423 }
4424
4435 public function getHtmlCode() {
4436 if ( is_null( $this->mHtmlCode ) ) {
4437 $this->mHtmlCode = LanguageCode::bcp47( $this->getCode() );
4438 }
4439 return $this->mHtmlCode;
4440 }
4441
4445 public function setCode( $code ) {
4446 $this->mCode = $code;
4447 // Ensure we don't leave incorrect cached data lying around
4448 $this->mHtmlCode = null;
4449 $this->mParentLanguage = false;
4450 }
4451
4459 public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
4460 $m = null;
4461 preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
4462 preg_quote( $suffix, '/' ) . '/', $filename, $m );
4463 if ( !count( $m ) ) {
4464 return false;
4465 }
4466 return str_replace( '_', '-', strtolower( $m[1] ) );
4467 }
4468
4474 public static function classFromCode( $code, $fallback = true ) {
4475 if ( $fallback && $code == 'en' ) {
4476 return 'Language';
4477 } else {
4478 return 'Language' . str_replace( '-', '_', ucfirst( $code ) );
4479 }
4480 }
4481
4490 public static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
4491 if ( !self::isValidBuiltInCode( $code ) ) {
4492 throw new MWException( "Invalid language code \"$code\"" );
4493 }
4494
4495 return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
4496 }
4497
4502 public static function getMessagesFileName( $code ) {
4503 global $IP;
4504 $file = self::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
4505 Hooks::run( 'Language::getMessagesFileName', [ $code, &$file ] );
4506 return $file;
4507 }
4508
4515 public static function getJsonMessagesFileName( $code ) {
4516 global $IP;
4517
4518 if ( !self::isValidBuiltInCode( $code ) ) {
4519 throw new MWException( "Invalid language code \"$code\"" );
4520 }
4521
4522 return "$IP/languages/i18n/$code.json";
4523 }
4524
4532 public static function getFallbackFor( $code ) {
4533 $fallbacks = self::getFallbacksFor( $code );
4534 if ( $fallbacks ) {
4535 return $fallbacks[0];
4536 }
4537 return false;
4538 }
4539
4547 public static function getFallbacksFor( $code ) {
4548 if ( $code === 'en' || !self::isValidBuiltInCode( $code ) ) {
4549 return [];
4550 }
4551 // For unknown languages, fallbackSequence returns an empty array,
4552 // hardcode fallback to 'en' in that case.
4553 return self::getLocalisationCache()->getItem( $code, 'fallbackSequence' ) ?: [ 'en' ];
4554 }
4555
4564 public static function getFallbacksIncludingSiteLanguage( $code ) {
4566
4567 // Usually, we will only store a tiny number of fallback chains, so we
4568 // keep them in static memory.
4569 $cacheKey = "{$code}-{$wgLanguageCode}";
4570
4571 if ( !array_key_exists( $cacheKey, self::$fallbackLanguageCache ) ) {
4572 $fallbacks = self::getFallbacksFor( $code );
4573
4574 // Append the site's fallback chain, including the site language itself
4575 $siteFallbacks = self::getFallbacksFor( $wgLanguageCode );
4576 array_unshift( $siteFallbacks, $wgLanguageCode );
4577
4578 // Eliminate any languages already included in the chain
4579 $siteFallbacks = array_diff( $siteFallbacks, $fallbacks );
4580
4581 self::$fallbackLanguageCache[$cacheKey] = [ $fallbacks, $siteFallbacks ];
4582 }
4583 return self::$fallbackLanguageCache[$cacheKey];
4584 }
4585
4595 public static function getMessagesFor( $code ) {
4596 return self::getLocalisationCache()->getItem( $code, 'messages' );
4597 }
4598
4607 public static function getMessageFor( $key, $code ) {
4608 return self::getLocalisationCache()->getSubitem( $code, 'messages', $key );
4609 }
4610
4619 public static function getMessageKeysFor( $code ) {
4620 return self::getLocalisationCache()->getSubitemList( $code, 'messages' );
4621 }
4622
4627 function fixVariableInNamespace( $talk ) {
4628 if ( strpos( $talk, '$1' ) === false ) {
4629 return $talk;
4630 }
4631
4633 $talk = str_replace( '$1', $wgMetaNamespace, $talk );
4634
4635 # Allow grammar transformations
4636 # Allowing full message-style parsing would make simple requests
4637 # such as action=raw much more expensive than they need to be.
4638 # This will hopefully cover most cases.
4639 $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
4640 [ $this, 'replaceGrammarInNamespace' ], $talk );
4641 return str_replace( ' ', '_', $talk );
4642 }
4643
4649 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
4650 }
4651
4662 public function formatExpiry( $expiry, $format = true, $infinity = 'infinity' ) {
4663 static $dbInfinity;
4664 if ( $dbInfinity === null ) {
4665 $dbInfinity = wfGetDB( DB_REPLICA )->getInfinity();
4666 }
4667
4668 if ( $expiry == '' || $expiry === 'infinity' || $expiry == $dbInfinity ) {
4669 return $format === true
4670 ? $this->getMessageFromDB( 'infiniteblock' )
4671 : $infinity;
4672 } else {
4673 return $format === true
4674 ? $this->timeanddate( $expiry, /* User preference timezone */ true )
4675 : wfTimestamp( $format, $expiry );
4676 }
4677 }
4678
4692 function formatTimePeriod( $seconds, $format = [] ) {
4693 if ( !is_array( $format ) ) {
4694 $format = [ 'avoid' => $format ]; // For backwards compatibility
4695 }
4696 if ( !isset( $format['avoid'] ) ) {
4697 $format['avoid'] = false;
4698 }
4699 if ( !isset( $format['noabbrevs'] ) ) {
4700 $format['noabbrevs'] = false;
4701 }
4702 $secondsMsg = wfMessage(
4703 $format['noabbrevs'] ? 'seconds' : 'seconds-abbrev' )->inLanguage( $this );
4704 $minutesMsg = wfMessage(
4705 $format['noabbrevs'] ? 'minutes' : 'minutes-abbrev' )->inLanguage( $this );
4706 $hoursMsg = wfMessage(
4707 $format['noabbrevs'] ? 'hours' : 'hours-abbrev' )->inLanguage( $this );
4708 $daysMsg = wfMessage(
4709 $format['noabbrevs'] ? 'days' : 'days-abbrev' )->inLanguage( $this );
4710
4711 if ( round( $seconds * 10 ) < 100 ) {
4712 $s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
4713 $s = $secondsMsg->params( $s )->text();
4714 } elseif ( round( $seconds ) < 60 ) {
4715 $s = $this->formatNum( round( $seconds ) );
4716 $s = $secondsMsg->params( $s )->text();
4717 } elseif ( round( $seconds ) < 3600 ) {
4718 $minutes = floor( $seconds / 60 );
4719 $secondsPart = round( fmod( $seconds, 60 ) );
4720 if ( $secondsPart == 60 ) {
4721 $secondsPart = 0;
4722 $minutes++;
4723 }
4724 $s = $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4725 $s .= ' ';
4726 $s .= $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4727 } elseif ( round( $seconds ) <= 2 * 86400 ) {
4728 $hours = floor( $seconds / 3600 );
4729 $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
4730 $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
4731 if ( $secondsPart == 60 ) {
4732 $secondsPart = 0;
4733 $minutes++;
4734 }
4735 if ( $minutes == 60 ) {
4736 $minutes = 0;
4737 $hours++;
4738 }
4739 $s = $hoursMsg->params( $this->formatNum( $hours ) )->text();
4740 $s .= ' ';
4741 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4742 if ( !in_array( $format['avoid'], [ 'avoidseconds', 'avoidminutes' ] ) ) {
4743 $s .= ' ' . $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4744 }
4745 } else {
4746 $days = floor( $seconds / 86400 );
4747 if ( $format['avoid'] === 'avoidminutes' ) {
4748 $hours = round( ( $seconds - $days * 86400 ) / 3600 );
4749 if ( $hours == 24 ) {
4750 $hours = 0;
4751 $days++;
4752 }
4753 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4754 $s .= ' ';
4755 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4756 } elseif ( $format['avoid'] === 'avoidseconds' ) {
4757 $hours = floor( ( $seconds - $days * 86400 ) / 3600 );
4758 $minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
4759 if ( $minutes == 60 ) {
4760 $minutes = 0;
4761 $hours++;
4762 }
4763 if ( $hours == 24 ) {
4764 $hours = 0;
4765 $days++;
4766 }
4767 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4768 $s .= ' ';
4769 $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4770 $s .= ' ';
4771 $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4772 } else {
4773 $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4774 $s .= ' ';
4775 $s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
4776 }
4777 }
4778 return $s;
4779 }
4780
4792 function formatBitrate( $bps ) {
4793 return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" );
4794 }
4795
4802 function formatComputingNumbers( $size, $boundary, $messageKey ) {
4803 if ( $size <= 0 ) {
4804 return str_replace( '$1', $this->formatNum( $size ),
4805 $this->getMessageFromDB( str_replace( '$1', '', $messageKey ) )
4806 );
4807 }
4808 $sizes = [ '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zeta', 'yotta' ];
4809 $index = 0;
4810
4811 $maxIndex = count( $sizes ) - 1;
4812 while ( $size >= $boundary && $index < $maxIndex ) {
4813 $index++;
4814 $size /= $boundary;
4815 }
4816
4817 // For small sizes no decimal places necessary
4818 $round = 0;
4819 if ( $index > 1 ) {
4820 // For MB and bigger two decimal places are smarter
4821 $round = 2;
4822 }
4823 $msg = str_replace( '$1', $sizes[$index], $messageKey );
4824
4825 $size = round( $size, $round );
4826 $text = $this->getMessageFromDB( $msg );
4827 return str_replace( '$1', $this->formatNum( $size ), $text );
4828 }
4829
4840 function formatSize( $size ) {
4841 return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" );
4842 }
4843
4853 function specialList( $page, $details, $oppositedm = true ) {
4854 if ( !$details ) {
4855 return $page;
4856 }
4857
4858 $dirmark = ( $oppositedm ? $this->getDirMark( true ) : '' ) . $this->getDirMark();
4859 return $page .
4860 $dirmark .
4861 $this->msg( 'word-separator' )->escaped() .
4862 $this->msg( 'parentheses' )->rawParams( $details )->escaped();
4863 }
4864
4875 public function viewPrevNext( Title $title, $offset, $limit,
4876 array $query = [], $atend = false
4877 ) {
4878 // @todo FIXME: Why on earth this needs one message for the text and another one for tooltip?
4879
4880 # Make 'previous' link
4881 $prev = wfMessage( 'prevn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4882 if ( $offset > 0 ) {
4883 $plink = $this->numLink( $title, max( $offset - $limit, 0 ), $limit,
4884 $query, $prev, 'prevn-title', 'mw-prevlink' );
4885 } else {
4886 $plink = htmlspecialchars( $prev );
4887 }
4888
4889 # Make 'next' link
4890 $next = wfMessage( 'nextn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4891 if ( $atend ) {
4892 $nlink = htmlspecialchars( $next );
4893 } else {
4894 $nlink = $this->numLink( $title, $offset + $limit, $limit,
4895 $query, $next, 'nextn-title', 'mw-nextlink' );
4896 }
4897
4898 # Make links to set number of items per page
4899 $numLinks = [];
4900 foreach ( [ 20, 50, 100, 250, 500 ] as $num ) {
4901 $numLinks[] = $this->numLink( $title, $offset, $num,
4902 $query, $this->formatNum( $num ), 'shown-title', 'mw-numlink' );
4903 }
4904
4905 return wfMessage( 'viewprevnext' )->inLanguage( $this )->title( $title
4906 )->rawParams( $plink, $nlink, $this->pipeList( $numLinks ) )->escaped();
4907 }
4908
4921 private function numLink( Title $title, $offset, $limit, array $query, $link,
4922 $tooltipMsg, $class
4923 ) {
4924 $query = [ 'limit' => $limit, 'offset' => $offset ] + $query;
4925 $tooltip = wfMessage( $tooltipMsg )->inLanguage( $this )->title( $title )
4926 ->numParams( $limit )->text();
4927
4928 return Html::element( 'a', [ 'href' => $title->getLocalURL( $query ),
4929 'title' => $tooltip, 'class' => $class ], $link );
4930 }
4931
4937 public function getConvRuleTitle() {
4938 return $this->mConverter->getConvRuleTitle();
4939 }
4940
4946 public function getCompiledPluralRules() {
4947 $pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'compiledPluralRules' );
4948 $fallbacks = self::getFallbacksFor( $this->mCode );
4949 if ( !$pluralRules ) {
4950 foreach ( $fallbacks as $fallbackCode ) {
4951 $pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
4952 if ( $pluralRules ) {
4953 break;
4954 }
4955 }
4956 }
4957 return $pluralRules;
4958 }
4959
4965 public function getPluralRules() {
4966 $pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRules' );
4967 $fallbacks = self::getFallbacksFor( $this->mCode );
4968 if ( !$pluralRules ) {
4969 foreach ( $fallbacks as $fallbackCode ) {
4970 $pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRules' );
4971 if ( $pluralRules ) {
4972 break;
4973 }
4974 }
4975 }
4976 return $pluralRules;
4977 }
4978
4984 public function getPluralRuleTypes() {
4985 $pluralRuleTypes = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRuleTypes' );
4986 $fallbacks = self::getFallbacksFor( $this->mCode );
4987 if ( !$pluralRuleTypes ) {
4988 foreach ( $fallbacks as $fallbackCode ) {
4989 $pluralRuleTypes = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRuleTypes' );
4990 if ( $pluralRuleTypes ) {
4991 break;
4992 }
4993 }
4994 }
4995 return $pluralRuleTypes;
4996 }
4997
5003 public function getPluralRuleIndexNumber( $number ) {
5004 $pluralRules = $this->getCompiledPluralRules();
5005 $form = Evaluator::evaluateCompiled( $number, $pluralRules );
5006 return $form;
5007 }
5008
5017 public function getPluralRuleType( $number ) {
5018 $index = $this->getPluralRuleIndexNumber( $number );
5019 $pluralRuleTypes = $this->getPluralRuleTypes();
5020 if ( isset( $pluralRuleTypes[$index] ) ) {
5021 return $pluralRuleTypes[$index];
5022 } else {
5023 return 'other';
5024 }
5025 }
5026}
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.
wfUrlProtocolsWithoutProtRel()
Like wfUrlProtocols(), but excludes '//' from the protocol list.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfGetPrecompiledData( $name)
Get an object from the precompiled serialized directory.
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.
$fallback
$namespaceAliases
$namespaceNames
$digitGroupingPattern
$minimumGroupingDigits
$wgUser
Definition Setup.php:902
$IP
Definition WebStart.php:52
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:35
hasVariants()
Check if this is a language with variants.
initContLang()
Hook which will be called if this is the content language.
Definition Language.php:439
static $mWeekdayMsgs
Definition Language.php:64
getLocalNsIndex( $text)
Get a namespace key by value, case insensitive.
Definition Language.php:613
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:174
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:49
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:284
getVariantname( $code, $usemsg=true)
short names for language variants used for language conversion links.
Definition Language.php:713
static $mHebrewCalendarMonthGenMsgs
Definition Language.php:103
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:363
recodeForEdit( $s)
$mParentLanguage
Definition Language.php:43
getWeekdayName( $key)
Definition Language.php:972
getHebrewCalendarMonthName( $key)
Definition Language.php:996
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 getFallbacksFor( $code)
Get the ordered list of fallback languages.
static $pdf
Definition Language.php:159
digitGroupingPattern()
static array $fallbackLanguageCache
Cache for language fallbacks.
Definition Language.php:140
getMonthAbbreviation( $key)
Definition Language.php:953
getHebrewCalendarMonthNameGen( $key)
static $lre
Unicode directional formatting characters, for embedBidi()
Definition Language.php:157
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:256
hasVariant( $variant)
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)
static fetchLanguageName( $code, $inLanguage=null, $include='all')
Definition Language.php:896
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()
static getFileName( $prefix='Language', $code, $suffix='.php')
Get the name of a file for a certain language code.
ucwordbreaksCallbackAscii( $matches)
lc( $str, $first=false)
fallback8bitEncoding()
static LocalisationCache $dataCache
Definition Language.php:60
listToText(array $l)
Take a list of strings and build a locale-friendly comma-separated list, using the local comma-separa...
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:574
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:429
getFormattedNsText( $index)
A convenience function that returns the same thing as getNsText() except with '_' changed to ' ',...
Definition Language.php:561
getURLVariant()
getMessageFromDB( $msg)
Get a message from the MediaWiki namespace.
Definition Language.php:908
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:415
getFormattedNamespaces()
A convenience function that returns getNamespaces() with spaces instead of underscores in values.
Definition Language.php:524
getExtraHashOptions()
returns language specific options used by User::getPageRenderHash() for example, the preferred langua...
$transformData
ReplacementArray object caches.
Definition Language.php:55
getMonthNamesArray()
Definition Language.php:933
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:945
setNamespaces(array $namespaces)
Arbitrarily set all of the namespace names at once.
Definition Language.php:504
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.
doMagicHook()
Run the LanguageGetMagic hook once.
$mExtendedSpecialPageAliases
Definition Language.php:46
getGrammarForms()
Get the grammar forms for the content language.
getNamespaces()
Returns an array of localised namespaces indexed by their numbers.
Definition Language.php:464
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:589
static array $durationIntervals
Definition Language.php:122
getNsIndex( $text)
Get a namespace key by value, case insensitive.
Definition Language.php:696
linkPrefixCharset()
A regular expression character set to match legal word-prefixing characters which should be merged on...
static $IRANIAN_DAYS
LanguageConverter $mConverter
Definition Language.php:39
caseFold( $s)
Return a case-folded representation of $s.
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:88
uc( $str, $first=false)
Convert a string to uppercase.
static $mHebrewCalendarMonthMsgs
Definition Language.php:95
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:210
embedBidi( $text='')
Wraps argument with unicode control characters for directionality safety.
getMonthAbbreviationsArray()
Definition Language.php:960
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:512
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:78
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:152
autoConvert( $text, $variant=false)
convert text to a variant
getIranianCalendarMonthName( $key)
Definition Language.php:988
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 ...
truncateInternal( $string, $length, $ellipsis='...', $adjustLength=true, $measureLength, $getSubstring)
Internal method used for truncation.
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)
Definition Language.php:980
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
markNoConversion( $text, $noParse=false)
Prepare external link text for conversion.
static $mMonthAbbrevMsgs
Definition Language.php:83
replaceGrammarInNamespace( $m)
getBookstoreList()
Exports $wgBookstoreListEn.
Definition Language.php:454
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:183
$mMagicExtensions
Definition Language.php:42
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:446
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:926
getPluralRules()
Get the plural rules for the language.
autoConvertToAllVariants( $text)
convert text to all supported variants
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:158
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...
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:406
static $mLangObjCache
Definition Language.php:62
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:385
msg( $msg)
Get message object in this language.
Definition Language.php:918
removeBadCharLast( $string)
Remove bytes that represent an incomplete Unicode character at the end of string (e....
static getJsonMessagesFileName( $code)
getNamespaceAliases()
Definition Language.php:622
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:543
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:666
static MapCacheLRU null $grammarTransformations
Cache for grammar rules data.
Definition Language.php:146
getVariants()
Get the list of variants supported by this language see sample implementation in LanguageZh....
time( $ts, $adj=false, $format=true, $timecorrection=false)
truncate( $string, $length, $ellipsis='...', $adjustLength=true)
This method is deprecated since 1.31 and kept as alias for truncateForDatabase, which has replaced it...
static getCodeFromFileName( $filename, $prefix='Language', $suffix='.php')
Get the language code from a file name.
getDefaultDateFormat()
Definition Language.php:744
removeBadCharFirst( $string)
Remove bytes that represent an incomplete Unicode character at the start of string (e....
static $mHijriCalendarMonthMsgs
Definition Language.php:111
static $mWeekdayAbbrevMsgs
Definition Language.php:69
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.
$mMagicHookDone
Definition Language.php:42
segmentByWord( $string)
Some languages such as Chinese require word segmentation, Specify such segmentation when overridden i...
convertHtml( $text, $isTitle=false)
Perform output conversion on a string, and encode for safe HTML output.
checkTitleEncoding( $s)
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:338
static $mMonthMsgs
Definition Language.php:73
$dateFormatStrings
Definition Language.php:45
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 getMain()
Get the RequestContext object associated with the main request.
static getSuggestedDurations( $lang=null)
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:39
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:53
static getDefaultOption( $opt)
Get a given default option value.
Definition User.php:1762
=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
when a variable name is used in a it is silently declared as a new local masking the global
Definition design.txt:95
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
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:76
const NS_PROJECT_TALK
Definition Defines.php:79
const NS_USER_TALK
Definition Defines.php:77
const NS_PROJECT
Definition Defines.php:78
the array() calling protocol came about after MediaWiki 1.4rc1.
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition hooks.txt:1795
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 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:934
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:2001
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub 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:865
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:964
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 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
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:2006
if the prop value should be in the metadata multi language array format
Definition hooks.txt:1656
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:2005
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub 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:864
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition hooks.txt:3021
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:302
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:1620
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition hooks.txt:247
returning false will NOT prevent logging $e
Definition hooks.txt:2176
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
A helper class for throttling authentication attempts.
const DB_REPLICA
Definition defines.php:25
if(!isset( $args[0])) $lang