MediaWiki  master
Language.php
Go to the documentation of this file.
1 <?php
32 
37 class Language {
42  const AS_AUTONYMS = null;
43 
48  const ALL = 'all';
49 
55  const SUPPORTED = 'mwfile';
56 
60  public $mConverter;
61 
62  public $mVariants, $mCode, $mLoaded = false;
63  public $mMagicExtensions = [];
64  private $mHtmlCode = null, $mParentLanguage = false;
65 
66  public $dateFormatStrings = [];
68 
70  protected $namespaceNames;
72 
76  public $transformData = [];
77 
80 
81  public static $mLangObjCache = [];
82 
87  const MESSAGES_FALLBACKS = 0;
88 
93  const STRICT_FALLBACKS = 1;
94 
95  public static $mWeekdayMsgs = [
96  'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
97  'friday', 'saturday'
98  ];
99 
100  public static $mWeekdayAbbrevMsgs = [
101  'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
102  ];
103 
104  public static $mMonthMsgs = [
105  'january', 'february', 'march', 'april', 'may_long', 'june',
106  'july', 'august', 'september', 'october', 'november',
107  'december'
108  ];
109  public static $mMonthGenMsgs = [
110  'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
111  'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
112  'december-gen'
113  ];
114  public static $mMonthAbbrevMsgs = [
115  'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
116  'sep', 'oct', 'nov', 'dec'
117  ];
118 
119  public static $mIranianCalendarMonthMsgs = [
120  'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
121  'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
122  'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
123  'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
124  ];
125 
126  public static $mHebrewCalendarMonthMsgs = [
127  'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
128  'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
129  'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
130  'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
131  'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
132  ];
133 
135  'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
136  'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
137  'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
138  'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
139  'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
140  ];
141 
142  public static $mHijriCalendarMonthMsgs = [
143  'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
144  'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
145  'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
146  'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
147  ];
148 
153  public static $durationIntervals = [
154  'millennia' => 31556952000,
155  'centuries' => 3155695200,
156  'decades' => 315569520,
157  'years' => 31556952, // 86400 * ( 365 + ( 24 * 3 + 25 ) / 400 )
158  'weeks' => 604800,
159  'days' => 86400,
160  'hours' => 3600,
161  'minutes' => 60,
162  'seconds' => 1,
163  ];
164 
171  private static $fallbackLanguageCache = [];
172 
177  private static $grammarTransformations;
178 
183  private static $languageNameCache;
184 
188  private static $lre = "\u{202A}"; // U+202A LEFT-TO-RIGHT EMBEDDING
189  private static $rle = "\u{202B}"; // U+202B RIGHT-TO-LEFT EMBEDDING
190  private static $pdf = "\u{202C}"; // U+202C POP DIRECTIONAL FORMATTING
191 
203  // @codeCoverageIgnoreStart
204  // phpcs:ignore Generic.Files.LineLength
205  private static $strongDirRegex = '/(?:([\x{41}-\x{5a}\x{61}-\x{7a}\x{aa}\x{b5}\x{ba}\x{c0}-\x{d6}\x{d8}-\x{f6}\x{f8}-\x{2b8}\x{2bb}-\x{2c1}\x{2d0}\x{2d1}\x{2e0}-\x{2e4}\x{2ee}\x{370}-\x{373}\x{376}\x{377}\x{37a}-\x{37d}\x{37f}\x{386}\x{388}-\x{38a}\x{38c}\x{38e}-\x{3a1}\x{3a3}-\x{3f5}\x{3f7}-\x{482}\x{48a}-\x{52f}\x{531}-\x{556}\x{559}-\x{55f}\x{561}-\x{587}\x{589}\x{903}-\x{939}\x{93b}\x{93d}-\x{940}\x{949}-\x{94c}\x{94e}-\x{950}\x{958}-\x{961}\x{964}-\x{980}\x{982}\x{983}\x{985}-\x{98c}\x{98f}\x{990}\x{993}-\x{9a8}\x{9aa}-\x{9b0}\x{9b2}\x{9b6}-\x{9b9}\x{9bd}-\x{9c0}\x{9c7}\x{9c8}\x{9cb}\x{9cc}\x{9ce}\x{9d7}\x{9dc}\x{9dd}\x{9df}-\x{9e1}\x{9e6}-\x{9f1}\x{9f4}-\x{9fa}\x{a03}\x{a05}-\x{a0a}\x{a0f}\x{a10}\x{a13}-\x{a28}\x{a2a}-\x{a30}\x{a32}\x{a33}\x{a35}\x{a36}\x{a38}\x{a39}\x{a3e}-\x{a40}\x{a59}-\x{a5c}\x{a5e}\x{a66}-\x{a6f}\x{a72}-\x{a74}\x{a83}\x{a85}-\x{a8d}\x{a8f}-\x{a91}\x{a93}-\x{aa8}\x{aaa}-\x{ab0}\x{ab2}\x{ab3}\x{ab5}-\x{ab9}\x{abd}-\x{ac0}\x{ac9}\x{acb}\x{acc}\x{ad0}\x{ae0}\x{ae1}\x{ae6}-\x{af0}\x{af9}\x{b02}\x{b03}\x{b05}-\x{b0c}\x{b0f}\x{b10}\x{b13}-\x{b28}\x{b2a}-\x{b30}\x{b32}\x{b33}\x{b35}-\x{b39}\x{b3d}\x{b3e}\x{b40}\x{b47}\x{b48}\x{b4b}\x{b4c}\x{b57}\x{b5c}\x{b5d}\x{b5f}-\x{b61}\x{b66}-\x{b77}\x{b83}\x{b85}-\x{b8a}\x{b8e}-\x{b90}\x{b92}-\x{b95}\x{b99}\x{b9a}\x{b9c}\x{b9e}\x{b9f}\x{ba3}\x{ba4}\x{ba8}-\x{baa}\x{bae}-\x{bb9}\x{bbe}\x{bbf}\x{bc1}\x{bc2}\x{bc6}-\x{bc8}\x{bca}-\x{bcc}\x{bd0}\x{bd7}\x{be6}-\x{bf2}\x{c01}-\x{c03}\x{c05}-\x{c0c}\x{c0e}-\x{c10}\x{c12}-\x{c28}\x{c2a}-\x{c39}\x{c3d}\x{c41}-\x{c44}\x{c58}-\x{c5a}\x{c60}\x{c61}\x{c66}-\x{c6f}\x{c7f}\x{c82}\x{c83}\x{c85}-\x{c8c}\x{c8e}-\x{c90}\x{c92}-\x{ca8}\x{caa}-\x{cb3}\x{cb5}-\x{cb9}\x{cbd}-\x{cc4}\x{cc6}-\x{cc8}\x{cca}\x{ccb}\x{cd5}\x{cd6}\x{cde}\x{ce0}\x{ce1}\x{ce6}-\x{cef}\x{cf1}\x{cf2}\x{d02}\x{d03}\x{d05}-\x{d0c}\x{d0e}-\x{d10}\x{d12}-\x{d3a}\x{d3d}-\x{d40}\x{d46}-\x{d48}\x{d4a}-\x{d4c}\x{d4e}\x{d57}\x{d5f}-\x{d61}\x{d66}-\x{d75}\x{d79}-\x{d7f}\x{d82}\x{d83}\x{d85}-\x{d96}\x{d9a}-\x{db1}\x{db3}-\x{dbb}\x{dbd}\x{dc0}-\x{dc6}\x{dcf}-\x{dd1}\x{dd8}-\x{ddf}\x{de6}-\x{def}\x{df2}-\x{df4}\x{e01}-\x{e30}\x{e32}\x{e33}\x{e40}-\x{e46}\x{e4f}-\x{e5b}\x{e81}\x{e82}\x{e84}\x{e87}\x{e88}\x{e8a}\x{e8d}\x{e94}-\x{e97}\x{e99}-\x{e9f}\x{ea1}-\x{ea3}\x{ea5}\x{ea7}\x{eaa}\x{eab}\x{ead}-\x{eb0}\x{eb2}\x{eb3}\x{ebd}\x{ec0}-\x{ec4}\x{ec6}\x{ed0}-\x{ed9}\x{edc}-\x{edf}\x{f00}-\x{f17}\x{f1a}-\x{f34}\x{f36}\x{f38}\x{f3e}-\x{f47}\x{f49}-\x{f6c}\x{f7f}\x{f85}\x{f88}-\x{f8c}\x{fbe}-\x{fc5}\x{fc7}-\x{fcc}\x{fce}-\x{fda}\x{1000}-\x{102c}\x{1031}\x{1038}\x{103b}\x{103c}\x{103f}-\x{1057}\x{105a}-\x{105d}\x{1061}-\x{1070}\x{1075}-\x{1081}\x{1083}\x{1084}\x{1087}-\x{108c}\x{108e}-\x{109c}\x{109e}-\x{10c5}\x{10c7}\x{10cd}\x{10d0}-\x{1248}\x{124a}-\x{124d}\x{1250}-\x{1256}\x{1258}\x{125a}-\x{125d}\x{1260}-\x{1288}\x{128a}-\x{128d}\x{1290}-\x{12b0}\x{12b2}-\x{12b5}\x{12b8}-\x{12be}\x{12c0}\x{12c2}-\x{12c5}\x{12c8}-\x{12d6}\x{12d8}-\x{1310}\x{1312}-\x{1315}\x{1318}-\x{135a}\x{1360}-\x{137c}\x{1380}-\x{138f}\x{13a0}-\x{13f5}\x{13f8}-\x{13fd}\x{1401}-\x{167f}\x{1681}-\x{169a}\x{16a0}-\x{16f8}\x{1700}-\x{170c}\x{170e}-\x{1711}\x{1720}-\x{1731}\x{1735}\x{1736}\x{1740}-\x{1751}\x{1760}-\x{176c}\x{176e}-\x{1770}\x{1780}-\x{17b3}\x{17b6}\x{17be}-\x{17c5}\x{17c7}\x{17c8}\x{17d4}-\x{17da}\x{17dc}\x{17e0}-\x{17e9}\x{1810}-\x{1819}\x{1820}-\x{1877}\x{1880}-\x{18a8}\x{18aa}\x{18b0}-\x{18f5}\x{1900}-\x{191e}\x{1923}-\x{1926}\x{1929}-\x{192b}\x{1930}\x{1931}\x{1933}-\x{1938}\x{1946}-\x{196d}\x{1970}-\x{1974}\x{1980}-\x{19ab}\x{19b0}-\x{19c9}\x{19d0}-\x{19da}\x{1a00}-\x{1a16}\x{1a19}\x{1a1a}\x{1a1e}-\x{1a55}\x{1a57}\x{1a61}\x{1a63}\x{1a64}\x{1a6d}-\x{1a72}\x{1a80}-\x{1a89}\x{1a90}-\x{1a99}\x{1aa0}-\x{1aad}\x{1b04}-\x{1b33}\x{1b35}\x{1b3b}\x{1b3d}-\x{1b41}\x{1b43}-\x{1b4b}\x{1b50}-\x{1b6a}\x{1b74}-\x{1b7c}\x{1b82}-\x{1ba1}\x{1ba6}\x{1ba7}\x{1baa}\x{1bae}-\x{1be5}\x{1be7}\x{1bea}-\x{1bec}\x{1bee}\x{1bf2}\x{1bf3}\x{1bfc}-\x{1c2b}\x{1c34}\x{1c35}\x{1c3b}-\x{1c49}\x{1c4d}-\x{1c7f}\x{1cc0}-\x{1cc7}\x{1cd3}\x{1ce1}\x{1ce9}-\x{1cec}\x{1cee}-\x{1cf3}\x{1cf5}\x{1cf6}\x{1d00}-\x{1dbf}\x{1e00}-\x{1f15}\x{1f18}-\x{1f1d}\x{1f20}-\x{1f45}\x{1f48}-\x{1f4d}\x{1f50}-\x{1f57}\x{1f59}\x{1f5b}\x{1f5d}\x{1f5f}-\x{1f7d}\x{1f80}-\x{1fb4}\x{1fb6}-\x{1fbc}\x{1fbe}\x{1fc2}-\x{1fc4}\x{1fc6}-\x{1fcc}\x{1fd0}-\x{1fd3}\x{1fd6}-\x{1fdb}\x{1fe0}-\x{1fec}\x{1ff2}-\x{1ff4}\x{1ff6}-\x{1ffc}\x{200e}\x{2071}\x{207f}\x{2090}-\x{209c}\x{2102}\x{2107}\x{210a}-\x{2113}\x{2115}\x{2119}-\x{211d}\x{2124}\x{2126}\x{2128}\x{212a}-\x{212d}\x{212f}-\x{2139}\x{213c}-\x{213f}\x{2145}-\x{2149}\x{214e}\x{214f}\x{2160}-\x{2188}\x{2336}-\x{237a}\x{2395}\x{249c}-\x{24e9}\x{26ac}\x{2800}-\x{28ff}\x{2c00}-\x{2c2e}\x{2c30}-\x{2c5e}\x{2c60}-\x{2ce4}\x{2ceb}-\x{2cee}\x{2cf2}\x{2cf3}\x{2d00}-\x{2d25}\x{2d27}\x{2d2d}\x{2d30}-\x{2d67}\x{2d6f}\x{2d70}\x{2d80}-\x{2d96}\x{2da0}-\x{2da6}\x{2da8}-\x{2dae}\x{2db0}-\x{2db6}\x{2db8}-\x{2dbe}\x{2dc0}-\x{2dc6}\x{2dc8}-\x{2dce}\x{2dd0}-\x{2dd6}\x{2dd8}-\x{2dde}\x{3005}-\x{3007}\x{3021}-\x{3029}\x{302e}\x{302f}\x{3031}-\x{3035}\x{3038}-\x{303c}\x{3041}-\x{3096}\x{309d}-\x{309f}\x{30a1}-\x{30fa}\x{30fc}-\x{30ff}\x{3105}-\x{312d}\x{3131}-\x{318e}\x{3190}-\x{31ba}\x{31f0}-\x{321c}\x{3220}-\x{324f}\x{3260}-\x{327b}\x{327f}-\x{32b0}\x{32c0}-\x{32cb}\x{32d0}-\x{32fe}\x{3300}-\x{3376}\x{337b}-\x{33dd}\x{33e0}-\x{33fe}\x{3400}-\x{4db5}\x{4e00}-\x{9fd5}\x{a000}-\x{a48c}\x{a4d0}-\x{a60c}\x{a610}-\x{a62b}\x{a640}-\x{a66e}\x{a680}-\x{a69d}\x{a6a0}-\x{a6ef}\x{a6f2}-\x{a6f7}\x{a722}-\x{a787}\x{a789}-\x{a7ad}\x{a7b0}-\x{a7b7}\x{a7f7}-\x{a801}\x{a803}-\x{a805}\x{a807}-\x{a80a}\x{a80c}-\x{a824}\x{a827}\x{a830}-\x{a837}\x{a840}-\x{a873}\x{a880}-\x{a8c3}\x{a8ce}-\x{a8d9}\x{a8f2}-\x{a8fd}\x{a900}-\x{a925}\x{a92e}-\x{a946}\x{a952}\x{a953}\x{a95f}-\x{a97c}\x{a983}-\x{a9b2}\x{a9b4}\x{a9b5}\x{a9ba}\x{a9bb}\x{a9bd}-\x{a9cd}\x{a9cf}-\x{a9d9}\x{a9de}-\x{a9e4}\x{a9e6}-\x{a9fe}\x{aa00}-\x{aa28}\x{aa2f}\x{aa30}\x{aa33}\x{aa34}\x{aa40}-\x{aa42}\x{aa44}-\x{aa4b}\x{aa4d}\x{aa50}-\x{aa59}\x{aa5c}-\x{aa7b}\x{aa7d}-\x{aaaf}\x{aab1}\x{aab5}\x{aab6}\x{aab9}-\x{aabd}\x{aac0}\x{aac2}\x{aadb}-\x{aaeb}\x{aaee}-\x{aaf5}\x{ab01}-\x{ab06}\x{ab09}-\x{ab0e}\x{ab11}-\x{ab16}\x{ab20}-\x{ab26}\x{ab28}-\x{ab2e}\x{ab30}-\x{ab65}\x{ab70}-\x{abe4}\x{abe6}\x{abe7}\x{abe9}-\x{abec}\x{abf0}-\x{abf9}\x{ac00}-\x{d7a3}\x{d7b0}-\x{d7c6}\x{d7cb}-\x{d7fb}\x{e000}-\x{fa6d}\x{fa70}-\x{fad9}\x{fb00}-\x{fb06}\x{fb13}-\x{fb17}\x{ff21}-\x{ff3a}\x{ff41}-\x{ff5a}\x{ff66}-\x{ffbe}\x{ffc2}-\x{ffc7}\x{ffca}-\x{ffcf}\x{ffd2}-\x{ffd7}\x{ffda}-\x{ffdc}\x{10000}-\x{1000b}\x{1000d}-\x{10026}\x{10028}-\x{1003a}\x{1003c}\x{1003d}\x{1003f}-\x{1004d}\x{10050}-\x{1005d}\x{10080}-\x{100fa}\x{10100}\x{10102}\x{10107}-\x{10133}\x{10137}-\x{1013f}\x{101d0}-\x{101fc}\x{10280}-\x{1029c}\x{102a0}-\x{102d0}\x{10300}-\x{10323}\x{10330}-\x{1034a}\x{10350}-\x{10375}\x{10380}-\x{1039d}\x{1039f}-\x{103c3}\x{103c8}-\x{103d5}\x{10400}-\x{1049d}\x{104a0}-\x{104a9}\x{10500}-\x{10527}\x{10530}-\x{10563}\x{1056f}\x{10600}-\x{10736}\x{10740}-\x{10755}\x{10760}-\x{10767}\x{11000}\x{11002}-\x{11037}\x{11047}-\x{1104d}\x{11066}-\x{1106f}\x{11082}-\x{110b2}\x{110b7}\x{110b8}\x{110bb}-\x{110c1}\x{110d0}-\x{110e8}\x{110f0}-\x{110f9}\x{11103}-\x{11126}\x{1112c}\x{11136}-\x{11143}\x{11150}-\x{11172}\x{11174}-\x{11176}\x{11182}-\x{111b5}\x{111bf}-\x{111c9}\x{111cd}\x{111d0}-\x{111df}\x{111e1}-\x{111f4}\x{11200}-\x{11211}\x{11213}-\x{1122e}\x{11232}\x{11233}\x{11235}\x{11238}-\x{1123d}\x{11280}-\x{11286}\x{11288}\x{1128a}-\x{1128d}\x{1128f}-\x{1129d}\x{1129f}-\x{112a9}\x{112b0}-\x{112de}\x{112e0}-\x{112e2}\x{112f0}-\x{112f9}\x{11302}\x{11303}\x{11305}-\x{1130c}\x{1130f}\x{11310}\x{11313}-\x{11328}\x{1132a}-\x{11330}\x{11332}\x{11333}\x{11335}-\x{11339}\x{1133d}-\x{1133f}\x{11341}-\x{11344}\x{11347}\x{11348}\x{1134b}-\x{1134d}\x{11350}\x{11357}\x{1135d}-\x{11363}\x{11480}-\x{114b2}\x{114b9}\x{114bb}-\x{114be}\x{114c1}\x{114c4}-\x{114c7}\x{114d0}-\x{114d9}\x{11580}-\x{115b1}\x{115b8}-\x{115bb}\x{115be}\x{115c1}-\x{115db}\x{11600}-\x{11632}\x{1163b}\x{1163c}\x{1163e}\x{11641}-\x{11644}\x{11650}-\x{11659}\x{11680}-\x{116aa}\x{116ac}\x{116ae}\x{116af}\x{116b6}\x{116c0}-\x{116c9}\x{11700}-\x{11719}\x{11720}\x{11721}\x{11726}\x{11730}-\x{1173f}\x{118a0}-\x{118f2}\x{118ff}\x{11ac0}-\x{11af8}\x{12000}-\x{12399}\x{12400}-\x{1246e}\x{12470}-\x{12474}\x{12480}-\x{12543}\x{13000}-\x{1342e}\x{14400}-\x{14646}\x{16800}-\x{16a38}\x{16a40}-\x{16a5e}\x{16a60}-\x{16a69}\x{16a6e}\x{16a6f}\x{16ad0}-\x{16aed}\x{16af5}\x{16b00}-\x{16b2f}\x{16b37}-\x{16b45}\x{16b50}-\x{16b59}\x{16b5b}-\x{16b61}\x{16b63}-\x{16b77}\x{16b7d}-\x{16b8f}\x{16f00}-\x{16f44}\x{16f50}-\x{16f7e}\x{16f93}-\x{16f9f}\x{1b000}\x{1b001}\x{1bc00}-\x{1bc6a}\x{1bc70}-\x{1bc7c}\x{1bc80}-\x{1bc88}\x{1bc90}-\x{1bc99}\x{1bc9c}\x{1bc9f}\x{1d000}-\x{1d0f5}\x{1d100}-\x{1d126}\x{1d129}-\x{1d166}\x{1d16a}-\x{1d172}\x{1d183}\x{1d184}\x{1d18c}-\x{1d1a9}\x{1d1ae}-\x{1d1e8}\x{1d360}-\x{1d371}\x{1d400}-\x{1d454}\x{1d456}-\x{1d49c}\x{1d49e}\x{1d49f}\x{1d4a2}\x{1d4a5}\x{1d4a6}\x{1d4a9}-\x{1d4ac}\x{1d4ae}-\x{1d4b9}\x{1d4bb}\x{1d4bd}-\x{1d4c3}\x{1d4c5}-\x{1d505}\x{1d507}-\x{1d50a}\x{1d50d}-\x{1d514}\x{1d516}-\x{1d51c}\x{1d51e}-\x{1d539}\x{1d53b}-\x{1d53e}\x{1d540}-\x{1d544}\x{1d546}\x{1d54a}-\x{1d550}\x{1d552}-\x{1d6a5}\x{1d6a8}-\x{1d6da}\x{1d6dc}-\x{1d714}\x{1d716}-\x{1d74e}\x{1d750}-\x{1d788}\x{1d78a}-\x{1d7c2}\x{1d7c4}-\x{1d7cb}\x{1d800}-\x{1d9ff}\x{1da37}-\x{1da3a}\x{1da6d}-\x{1da74}\x{1da76}-\x{1da83}\x{1da85}-\x{1da8b}\x{1f110}-\x{1f12e}\x{1f130}-\x{1f169}\x{1f170}-\x{1f19a}\x{1f1e6}-\x{1f202}\x{1f210}-\x{1f23a}\x{1f240}-\x{1f248}\x{1f250}\x{1f251}\x{20000}-\x{2a6d6}\x{2a700}-\x{2b734}\x{2b740}-\x{2b81d}\x{2b820}-\x{2cea1}\x{2f800}-\x{2fa1d}\x{f0000}-\x{ffffd}\x{100000}-\x{10fffd}])|([\x{590}\x{5be}\x{5c0}\x{5c3}\x{5c6}\x{5c8}-\x{5ff}\x{7c0}-\x{7ea}\x{7f4}\x{7f5}\x{7fa}-\x{815}\x{81a}\x{824}\x{828}\x{82e}-\x{858}\x{85c}-\x{89f}\x{200f}\x{fb1d}\x{fb1f}-\x{fb28}\x{fb2a}-\x{fb4f}\x{10800}-\x{1091e}\x{10920}-\x{10a00}\x{10a04}\x{10a07}-\x{10a0b}\x{10a10}-\x{10a37}\x{10a3b}-\x{10a3e}\x{10a40}-\x{10ae4}\x{10ae7}-\x{10b38}\x{10b40}-\x{10e5f}\x{10e7f}-\x{10fff}\x{1e800}-\x{1e8cf}\x{1e8d7}-\x{1edff}\x{1ef00}-\x{1efff}\x{608}\x{60b}\x{60d}\x{61b}-\x{64a}\x{66d}-\x{66f}\x{671}-\x{6d5}\x{6e5}\x{6e6}\x{6ee}\x{6ef}\x{6fa}-\x{710}\x{712}-\x{72f}\x{74b}-\x{7a5}\x{7b1}-\x{7bf}\x{8a0}-\x{8e2}\x{fb50}-\x{fd3d}\x{fd40}-\x{fdcf}\x{fdf0}-\x{fdfc}\x{fdfe}\x{fdff}\x{fe70}-\x{fefe}\x{1ee00}-\x{1eeef}\x{1eef2}-\x{1eeff}]))/u';
206  // @codeCoverageIgnoreEnd
207 
214  static function factory( $code ) {
216 
217  if ( isset( $wgDummyLanguageCodes[$code] ) ) {
218  $code = $wgDummyLanguageCodes[$code];
219  }
220 
221  // get the language object to process
222  $langObj = self::$mLangObjCache[$code] ?? self::newFromCode( $code );
223 
224  // merge the language object in to get it up front in the cache
225  self::$mLangObjCache = array_merge( [ $code => $langObj ], self::$mLangObjCache );
226  // get rid of the oldest ones in case we have an overflow
227  self::$mLangObjCache = array_slice( self::$mLangObjCache, 0, $wgLangObjCacheSize, true );
228 
229  return $langObj;
230  }
231 
239  protected static function newFromCode( $code, $fallback = false ) {
240  if ( !self::isValidCode( $code ) ) {
241  throw new MWException( "Invalid language code \"$code\"" );
242  }
243 
244  if ( !self::isValidBuiltInCode( $code ) ) {
245  // It's not possible to customise this code with class files, so
246  // just return a Language object. This is to support uselang= hacks.
247  $lang = new Language;
248  $lang->mCode = $code;
249  return $lang;
250  }
251 
252  // Check if there is a language class for the code
253  $class = self::classFromCode( $code, $fallback );
254  // LanguageCode does not inherit Language
255  if ( class_exists( $class ) && is_a( $class, 'Language', true ) ) {
256  $lang = new $class;
257  return $lang;
258  }
259 
260  // Keep trying the fallback list until we find an existing class
261  $fallbacks = self::getFallbacksFor( $code );
262  foreach ( $fallbacks as $fallbackCode ) {
263  if ( !self::isValidBuiltInCode( $fallbackCode ) ) {
264  throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" );
265  }
266 
267  $class = self::classFromCode( $fallbackCode );
268  if ( class_exists( $class ) ) {
269  $lang = new $class;
270  $lang->mCode = $code;
271  return $lang;
272  }
273  }
274 
275  throw new MWException( "Invalid fallback sequence for language '$code'" );
276  }
277 
283  public static function clearCaches() {
284  if ( !defined( 'MW_PHPUNIT_TEST' ) && !defined( 'MEDIAWIKI_INSTALL' ) ) {
285  throw new MWException( __METHOD__ . ' must not be used outside tests/installer' );
286  }
287  if ( !defined( 'MEDIAWIKI_INSTALL' ) ) {
288  MediaWikiServices::getInstance()->resetServiceForTesting( 'LocalisationCache' );
289  }
290  self::$mLangObjCache = [];
291  self::$fallbackLanguageCache = [];
292  self::$grammarTransformations = null;
293  self::$languageNameCache = null;
294  }
295 
304  public static function isSupportedLanguage( $code ) {
305  if ( !self::isValidBuiltInCode( $code ) ) {
306  return false;
307  }
308 
309  if ( $code === 'qqq' ) {
310  return false;
311  }
312 
313  return is_readable( self::getMessagesFileName( $code ) ) ||
314  is_readable( self::getJsonMessagesFileName( $code ) );
315  }
316 
332  public static function isWellFormedLanguageTag( $code, $lenient = false ) {
333  $alpha = '[a-z]';
334  $digit = '[0-9]';
335  $alphanum = '[a-z0-9]';
336  $x = 'x'; # private use singleton
337  $singleton = '[a-wy-z]'; # other singleton
338  $s = $lenient ? '[-_]' : '-';
339 
340  $language = "$alpha{2,8}|$alpha{2,3}$s$alpha{3}";
341  $script = "$alpha{4}"; # ISO 15924
342  $region = "(?:$alpha{2}|$digit{3})"; # ISO 3166-1 alpha-2 or UN M.49
343  $variant = "(?:$alphanum{5,8}|$digit$alphanum{3})";
344  $extension = "$singleton(?:$s$alphanum{2,8})+";
345  $privateUse = "$x(?:$s$alphanum{1,8})+";
346 
347  # Define certain grandfathered codes, since otherwise the regex is pretty useless.
348  # Since these are limited, this is safe even later changes to the registry --
349  # the only oddity is that it might change the type of the tag, and thus
350  # the results from the capturing groups.
351  # https://www.iana.org/assignments/language-subtag-registry
352 
353  $grandfathered = "en{$s}GB{$s}oed"
354  . "|i{$s}(?:ami|bnn|default|enochian|hak|klingon|lux|mingo|navajo|pwn|tao|tay|tsu)"
355  . "|no{$s}(?:bok|nyn)"
356  . "|sgn{$s}(?:BE{$s}(?:fr|nl)|CH{$s}de)"
357  . "|zh{$s}min{$s}nan";
358 
359  $variantList = "$variant(?:$s$variant)*";
360  $extensionList = "$extension(?:$s$extension)*";
361 
362  $langtag = "(?:($language)"
363  . "(?:$s$script)?"
364  . "(?:$s$region)?"
365  . "(?:$s$variantList)?"
366  . "(?:$s$extensionList)?"
367  . "(?:$s$privateUse)?)";
368 
369  # The final breakdown, with capturing groups for each of these components
370  # The variants, extensions, grandfathered, and private-use may have interior '-'
371 
372  $root = "^(?:$langtag|$privateUse|$grandfathered)$";
373 
374  return (bool)preg_match( "/$root/", strtolower( $code ) );
375  }
376 
386  public static function isValidCode( $code ) {
387  static $cache = [];
388  Assert::parameterType( 'string', $code, '$code' );
389  if ( !isset( $cache[$code] ) ) {
390  // People think language codes are html safe, so enforce it.
391  // Ideally we should only allow a-zA-Z0-9-
392  // but, .+ and other chars are often used for {{int:}} hacks
393  // see bugs T39564, T39587, T38938
394  $cache[$code] =
395  // Protect against path traversal
396  strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
397  && !preg_match( MediaWikiTitleCodec::getTitleInvalidRegex(), $code );
398  }
399  return $cache[$code];
400  }
401 
411  public static function isValidBuiltInCode( $code ) {
412  Assert::parameterType( 'string', $code, '$code' );
413 
414  return (bool)preg_match( '/^[a-z0-9-]{2,}$/', $code );
415  }
416 
425  public static function isKnownLanguageTag( $tag ) {
426  // Quick escape for invalid input to avoid exceptions down the line
427  // when code tries to process tags which are not valid at all.
428  if ( !self::isValidBuiltInCode( $tag ) ) {
429  return false;
430  }
431 
432  if ( isset( MediaWiki\Languages\Data\Names::$names[$tag] )
433  || self::fetchLanguageName( $tag, $tag ) !== ''
434  ) {
435  return true;
436  }
437 
438  return false;
439  }
440 
447  public static function getLocalisationCache() {
448  return MediaWikiServices::getInstance()->getLocalisationCache();
449  }
450 
451  function __construct() {
452  $this->mConverter = new FakeConverter( $this );
453  // Set the code to the name of the descendant
454  if ( static::class === 'Language' ) {
455  $this->mCode = 'en';
456  } else {
457  $this->mCode = str_replace( '_', '-', strtolower( substr( static::class, 8 ) ) );
458  }
459  $this->localisationCache = MediaWikiServices::getInstance()->getLocalisationCache();
460  }
461 
465  function __destruct() {
466  foreach ( $this as $name => $value ) {
467  unset( $this->$name );
468  }
469  }
470 
475  function initContLang() {
476  }
477 
482  public function getFallbackLanguages() {
483  return self::getFallbacksFor( $this->mCode );
484  }
485 
490  public function getBookstoreList() {
491  return $this->localisationCache->getItem( $this->mCode, 'bookstoreList' );
492  }
493 
500  public function getNamespaces() {
501  if ( is_null( $this->namespaceNames ) ) {
503 
504  $validNamespaces = MediaWikiServices::getInstance()->getNamespaceInfo()->
505  getCanonicalNamespaces();
506 
507  $this->namespaceNames = $wgExtraNamespaces +
508  $this->localisationCache->getItem( $this->mCode, 'namespaceNames' );
509  $this->namespaceNames += $validNamespaces;
510 
511  $this->namespaceNames[NS_PROJECT] = $wgMetaNamespace;
512  if ( $wgMetaNamespaceTalk ) {
513  $this->namespaceNames[NS_PROJECT_TALK] = $wgMetaNamespaceTalk;
514  } else {
515  $talk = $this->namespaceNames[NS_PROJECT_TALK];
516  $this->namespaceNames[NS_PROJECT_TALK] =
517  $this->fixVariableInNamespace( $talk );
518  }
519 
520  # Sometimes a language will be localised but not actually exist on this wiki.
521  foreach ( $this->namespaceNames as $key => $text ) {
522  if ( !isset( $validNamespaces[$key] ) ) {
523  unset( $this->namespaceNames[$key] );
524  }
525  }
526 
527  # The above mixing may leave namespaces out of canonical order.
528  # Re-order by namespace ID number...
529  ksort( $this->namespaceNames );
530 
531  Hooks::run( 'LanguageGetNamespaces', [ &$this->namespaceNames ] );
532  }
533 
534  return $this->namespaceNames;
535  }
536 
541  public function setNamespaces( array $namespaces ) {
542  $this->namespaceNames = $namespaces;
543  $this->mNamespaceIds = null;
544  }
545 
549  public function resetNamespaces() {
550  $this->namespaceNames = null;
551  $this->mNamespaceIds = null;
552  $this->namespaceAliases = null;
553  }
554 
561  public function getFormattedNamespaces() {
562  $ns = $this->getNamespaces();
563  foreach ( $ns as $k => $v ) {
564  $ns[$k] = strtr( $v, '_', ' ' );
565  }
566  return $ns;
567  }
568 
580  public function getNsText( $index ) {
581  $ns = $this->getNamespaces();
582  return $ns[$index] ?? false;
583  }
584 
598  public function getFormattedNsText( $index ) {
599  $ns = $this->getNsText( $index );
600  return strtr( $ns, '_', ' ' );
601  }
602 
611  public function getGenderNsText( $index, $gender ) {
613 
614  $ns = $wgExtraGenderNamespaces +
615  (array)$this->localisationCache->getItem( $this->mCode, 'namespaceGenderAliases' );
616 
617  return $ns[$index][$gender] ?? $this->getNsText( $index );
618  }
619 
626  public function needsGenderDistinction() {
628  if ( count( $wgExtraGenderNamespaces ) > 0 ) {
629  // $wgExtraGenderNamespaces overrides everything
630  return true;
631  } elseif ( isset( $wgExtraNamespaces[NS_USER] ) && isset( $wgExtraNamespaces[NS_USER_TALK] ) ) {
633  // $wgExtraNamespaces overrides any gender aliases specified in i18n files
634  return false;
635  } else {
636  // Check what is in i18n files
637  $aliases = $this->localisationCache->getItem( $this->mCode, 'namespaceGenderAliases' );
638  return count( $aliases ) > 0;
639  }
640  }
641 
650  function getLocalNsIndex( $text ) {
651  $lctext = $this->lc( $text );
652  $ids = $this->getNamespaceIds();
653  return $ids[$lctext] ?? false;
654  }
655 
659  public function getNamespaceAliases() {
660  if ( is_null( $this->namespaceAliases ) ) {
661  $aliases = $this->localisationCache->getItem( $this->mCode, 'namespaceAliases' );
662  if ( !$aliases ) {
663  $aliases = [];
664  } else {
665  foreach ( $aliases as $name => $index ) {
666  if ( $index === NS_PROJECT_TALK ) {
667  unset( $aliases[$name] );
668  $name = $this->fixVariableInNamespace( $name );
669  $aliases[$name] = $index;
670  }
671  }
672  }
673 
675  $genders = $wgExtraGenderNamespaces + (array)$this->localisationCache
676  ->getItem( $this->mCode, 'namespaceGenderAliases' );
677  foreach ( $genders as $index => $forms ) {
678  foreach ( $forms as $alias ) {
679  $aliases[$alias] = $index;
680  }
681  }
682 
683  # Also add converted namespace names as aliases, to avoid confusion.
684  $convertedNames = [];
685  foreach ( $this->getVariants() as $variant ) {
686  if ( $variant === $this->mCode ) {
687  continue;
688  }
689  foreach ( $this->getNamespaces() as $ns => $_ ) {
690  $convertedNames[$this->getConverter()->convertNamespace( $ns, $variant )] = $ns;
691  }
692  }
693 
694  $this->namespaceAliases = $aliases + $convertedNames;
695 
696  # Filter out aliases to namespaces that don't exist, e.g. from extensions
697  # that aren't loaded here but are included in the l10n cache.
698  # (array_intersect preserves keys from its first argument)
699  $this->namespaceAliases = array_intersect(
700  $this->namespaceAliases,
701  array_keys( $this->getNamespaces() )
702  );
703  }
704 
706  }
707 
711  public function getNamespaceIds() {
712  if ( is_null( $this->mNamespaceIds ) ) {
713  global $wgNamespaceAliases;
714  # Put namespace names and aliases into a hashtable.
715  # If this is too slow, then we should arrange it so that it is done
716  # before caching. The catch is that at pre-cache time, the above
717  # class-specific fixup hasn't been done.
718  $this->mNamespaceIds = [];
719  foreach ( $this->getNamespaces() as $index => $name ) {
720  $this->mNamespaceIds[$this->lc( $name )] = $index;
721  }
722  foreach ( $this->getNamespaceAliases() as $name => $index ) {
723  $this->mNamespaceIds[$this->lc( $name )] = $index;
724  }
725  if ( $wgNamespaceAliases ) {
726  foreach ( $wgNamespaceAliases as $name => $index ) {
727  $this->mNamespaceIds[$this->lc( $name )] = $index;
728  }
729  }
730  }
731  return $this->mNamespaceIds;
732  }
733 
741  public function getNsIndex( $text ) {
742  $lctext = $this->lc( $text );
743  $ns = MediaWikiServices::getInstance()->getNamespaceInfo()->
744  getCanonicalIndex( $lctext );
745  if ( $ns !== null ) {
746  return $ns;
747  }
748  $ids = $this->getNamespaceIds();
749  return $ids[$lctext] ?? false;
750  }
751 
759  public function getVariantname( $code, $usemsg = true ) {
760  $msg = "variantname-$code";
761  if ( $usemsg && wfMessage( $msg )->exists() ) {
762  return $this->getMessageFromDB( $msg );
763  }
764  $name = self::fetchLanguageName( $code );
765  if ( $name ) {
766  return $name; # if it's defined as a language name, show that
767  } else {
768  # otherwise, output the language code
769  return $code;
770  }
771  }
772 
776  public function getDatePreferences() {
777  return $this->localisationCache->getItem( $this->mCode, 'datePreferences' );
778  }
779 
783  function getDateFormats() {
784  return $this->localisationCache->getItem( $this->mCode, 'dateFormats' );
785  }
786 
790  public function getDefaultDateFormat() {
791  $df = $this->localisationCache->getItem( $this->mCode, 'defaultDateFormat' );
792  if ( $df === 'dmy or mdy' ) {
793  global $wgAmericanDates;
794  return $wgAmericanDates ? 'mdy' : 'dmy';
795  } else {
796  return $df;
797  }
798  }
799 
803  public function getDatePreferenceMigrationMap() {
804  return $this->localisationCache->getItem( $this->mCode, 'datePreferenceMigrationMap' );
805  }
806 
810  public function getExtraUserToggles() {
811  wfDeprecated( __METHOD__, '1.34' );
812  return [];
813  }
814 
819  function getUserToggle( $tog ) {
820  return $this->getMessageFromDB( "tog-$tog" );
821  }
822 
834  public static function fetchLanguageNames( $inLanguage = self::AS_AUTONYMS, $include = 'mw' ) {
835  $cacheKey = $inLanguage === self::AS_AUTONYMS ? 'null' : $inLanguage;
836  $cacheKey .= ":$include";
837  if ( self::$languageNameCache === null ) {
838  self::$languageNameCache = new HashBagOStuff( [ 'maxKeys' => 20 ] );
839  }
840 
841  $ret = self::$languageNameCache->get( $cacheKey );
842  if ( !$ret ) {
843  $ret = self::fetchLanguageNamesUncached( $inLanguage, $include );
844  self::$languageNameCache->set( $cacheKey, $ret );
845  }
846  return $ret;
847  }
848 
859  private static function fetchLanguageNamesUncached(
860  $inLanguage = self::AS_AUTONYMS,
861  $include = 'mw'
862  ) {
863  global $wgExtraLanguageNames, $wgUsePigLatinVariant;
864 
865  // If passed an invalid language code to use, fallback to en
866  if ( $inLanguage !== self::AS_AUTONYMS && !self::isValidCode( $inLanguage ) ) {
867  $inLanguage = 'en';
868  }
869 
870  $names = [];
871 
872  if ( $inLanguage ) {
873  # TODO: also include when $inLanguage is null, when this code is more efficient
874  Hooks::run( 'LanguageGetTranslatedLanguageNames', [ &$names, $inLanguage ] );
875  }
876 
877  $mwNames = $wgExtraLanguageNames + MediaWiki\Languages\Data\Names::$names;
878  if ( $wgUsePigLatinVariant ) {
879  // Pig Latin (for variant development)
880  $mwNames['en-x-piglatin'] = 'Igpay Atinlay';
881  }
882 
883  foreach ( $mwNames as $mwCode => $mwName ) {
884  # - Prefer own MediaWiki native name when not using the hook
885  # - For other names just add if not added through the hook
886  if ( $mwCode === $inLanguage || !isset( $names[$mwCode] ) ) {
887  $names[$mwCode] = $mwName;
888  }
889  }
890 
891  if ( $include === self::ALL ) {
892  ksort( $names );
893  return $names;
894  }
895 
896  $returnMw = [];
897  $coreCodes = array_keys( $mwNames );
898  foreach ( $coreCodes as $coreCode ) {
899  $returnMw[$coreCode] = $names[$coreCode];
900  }
901 
902  if ( $include === self::SUPPORTED ) {
903  $namesMwFile = [];
904  # We do this using a foreach over the codes instead of a directory
905  # loop so that messages files in extensions will work correctly.
906  foreach ( $returnMw as $code => $value ) {
907  if ( is_readable( self::getMessagesFileName( $code ) )
908  || is_readable( self::getJsonMessagesFileName( $code ) )
909  ) {
910  $namesMwFile[$code] = $names[$code];
911  }
912  }
913 
914  ksort( $namesMwFile );
915  return $namesMwFile;
916  }
917 
918  ksort( $returnMw );
919  # 'mw' option; default if it's not one of the other two options (all/mwfile)
920  return $returnMw;
921  }
922 
931  public static function fetchLanguageName(
932  $code,
933  $inLanguage = self::AS_AUTONYMS,
934  $include = self::ALL
935  ) {
936  $code = strtolower( $code );
937  $array = self::fetchLanguageNames( $inLanguage, $include );
938  return !array_key_exists( $code, $array ) ? '' : $array[$code];
939  }
940 
947  public function getMessageFromDB( $msg ) {
948  return $this->msg( $msg )->text();
949  }
950 
957  protected function msg( $msg ) {
958  return wfMessage( $msg )->inLanguage( $this );
959  }
960 
965  public function getMonthName( $key ) {
966  return $this->getMessageFromDB( self::$mMonthMsgs[$key - 1] );
967  }
968 
972  public function getMonthNamesArray() {
973  $monthNames = [ '' ];
974  for ( $i = 1; $i < 13; $i++ ) {
975  $monthNames[] = $this->getMonthName( $i );
976  }
977  return $monthNames;
978  }
979 
984  public function getMonthNameGen( $key ) {
985  return $this->getMessageFromDB( self::$mMonthGenMsgs[$key - 1] );
986  }
987 
992  public function getMonthAbbreviation( $key ) {
993  return $this->getMessageFromDB( self::$mMonthAbbrevMsgs[$key - 1] );
994  }
995 
999  public function getMonthAbbreviationsArray() {
1000  $monthNames = [ '' ];
1001  for ( $i = 1; $i < 13; $i++ ) {
1002  $monthNames[] = $this->getMonthAbbreviation( $i );
1003  }
1004  return $monthNames;
1005  }
1006 
1011  public function getWeekdayName( $key ) {
1012  return $this->getMessageFromDB( self::$mWeekdayMsgs[$key - 1] );
1013  }
1014 
1019  function getWeekdayAbbreviation( $key ) {
1020  return $this->getMessageFromDB( self::$mWeekdayAbbrevMsgs[$key - 1] );
1021  }
1022 
1027  function getIranianCalendarMonthName( $key ) {
1028  return $this->getMessageFromDB( self::$mIranianCalendarMonthMsgs[$key - 1] );
1029  }
1030 
1035  function getHebrewCalendarMonthName( $key ) {
1036  return $this->getMessageFromDB( self::$mHebrewCalendarMonthMsgs[$key - 1] );
1037  }
1038 
1044  return $this->getMessageFromDB( self::$mHebrewCalendarMonthGenMsgs[$key - 1] );
1045  }
1046 
1051  function getHijriCalendarMonthName( $key ) {
1052  return $this->getMessageFromDB( self::$mHijriCalendarMonthMsgs[$key - 1] );
1053  }
1054 
1063  private static function dateTimeObjFormat( &$dateTimeObj, $ts, $zone, $code ) {
1064  if ( !$dateTimeObj ) {
1065  $dateTimeObj = DateTime::createFromFormat(
1066  'YmdHis', $ts, $zone ?: new DateTimeZone( 'UTC' )
1067  );
1068  }
1069  return $dateTimeObj->format( $code );
1070  }
1071 
1141  public function sprintfDate( $format, $ts, DateTimeZone $zone = null, &$ttl = 'unused' ) {
1142  $s = '';
1143  $raw = false;
1144  $roman = false;
1145  $hebrewNum = false;
1146  $dateTimeObj = false;
1147  $rawToggle = false;
1148  $iranian = false;
1149  $hebrew = false;
1150  $hijri = false;
1151  $thai = false;
1152  $minguo = false;
1153  $tenno = false;
1154 
1155  $usedSecond = false;
1156  $usedMinute = false;
1157  $usedHour = false;
1158  $usedAMPM = false;
1159  $usedDay = false;
1160  $usedWeek = false;
1161  $usedMonth = false;
1162  $usedYear = false;
1163  $usedISOYear = false;
1164  $usedIsLeapYear = false;
1165 
1166  $usedHebrewMonth = false;
1167  $usedIranianMonth = false;
1168  $usedHijriMonth = false;
1169  $usedHebrewYear = false;
1170  $usedIranianYear = false;
1171  $usedHijriYear = false;
1172  $usedTennoYear = false;
1173 
1174  if ( strlen( $ts ) !== 14 ) {
1175  throw new MWException( __METHOD__ . ": The timestamp $ts should have 14 characters" );
1176  }
1177 
1178  if ( !ctype_digit( $ts ) ) {
1179  throw new MWException( __METHOD__ . ": The timestamp $ts should be a number" );
1180  }
1181 
1182  $formatLength = strlen( $format );
1183  for ( $p = 0; $p < $formatLength; $p++ ) {
1184  $num = false;
1185  $code = $format[$p];
1186  if ( $code == 'x' && $p < $formatLength - 1 ) {
1187  $code .= $format[++$p];
1188  }
1189 
1190  if ( ( $code === 'xi'
1191  || $code === 'xj'
1192  || $code === 'xk'
1193  || $code === 'xm'
1194  || $code === 'xo'
1195  || $code === 'xt' )
1196  && $p < $formatLength - 1 ) {
1197  $code .= $format[++$p];
1198  }
1199 
1200  switch ( $code ) {
1201  case 'xx':
1202  $s .= 'x';
1203  break;
1204  case 'xn':
1205  $raw = true;
1206  break;
1207  case 'xN':
1208  $rawToggle = !$rawToggle;
1209  break;
1210  case 'xr':
1211  $roman = true;
1212  break;
1213  case 'xh':
1214  $hebrewNum = true;
1215  break;
1216  case 'xg':
1217  $usedMonth = true;
1218  $s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) );
1219  break;
1220  case 'xjx':
1221  $usedHebrewMonth = true;
1222  if ( !$hebrew ) {
1223  $hebrew = self::tsToHebrew( $ts );
1224  }
1225  $s .= $this->getHebrewCalendarMonthNameGen( $hebrew[1] );
1226  break;
1227  case 'd':
1228  $usedDay = true;
1229  $num = substr( $ts, 6, 2 );
1230  break;
1231  case 'D':
1232  $usedDay = true;
1233  $s .= $this->getWeekdayAbbreviation(
1234  self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1
1235  );
1236  break;
1237  case 'j':
1238  $usedDay = true;
1239  $num = intval( substr( $ts, 6, 2 ) );
1240  break;
1241  case 'xij':
1242  $usedDay = true;
1243  if ( !$iranian ) {
1244  $iranian = self::tsToIranian( $ts );
1245  }
1246  $num = $iranian[2];
1247  break;
1248  case 'xmj':
1249  $usedDay = true;
1250  if ( !$hijri ) {
1251  $hijri = self::tsToHijri( $ts );
1252  }
1253  $num = $hijri[2];
1254  break;
1255  case 'xjj':
1256  $usedDay = true;
1257  if ( !$hebrew ) {
1258  $hebrew = self::tsToHebrew( $ts );
1259  }
1260  $num = $hebrew[2];
1261  break;
1262  case 'l':
1263  $usedDay = true;
1264  $s .= $this->getWeekdayName(
1265  self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1
1266  );
1267  break;
1268  case 'F':
1269  $usedMonth = true;
1270  $s .= $this->getMonthName( substr( $ts, 4, 2 ) );
1271  break;
1272  case 'xiF':
1273  $usedIranianMonth = true;
1274  if ( !$iranian ) {
1275  $iranian = self::tsToIranian( $ts );
1276  }
1277  $s .= $this->getIranianCalendarMonthName( $iranian[1] );
1278  break;
1279  case 'xmF':
1280  $usedHijriMonth = true;
1281  if ( !$hijri ) {
1282  $hijri = self::tsToHijri( $ts );
1283  }
1284  $s .= $this->getHijriCalendarMonthName( $hijri[1] );
1285  break;
1286  case 'xjF':
1287  $usedHebrewMonth = true;
1288  if ( !$hebrew ) {
1289  $hebrew = self::tsToHebrew( $ts );
1290  }
1291  $s .= $this->getHebrewCalendarMonthName( $hebrew[1] );
1292  break;
1293  case 'm':
1294  $usedMonth = true;
1295  $num = substr( $ts, 4, 2 );
1296  break;
1297  case 'M':
1298  $usedMonth = true;
1299  $s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) );
1300  break;
1301  case 'n':
1302  $usedMonth = true;
1303  $num = intval( substr( $ts, 4, 2 ) );
1304  break;
1305  case 'xin':
1306  $usedIranianMonth = true;
1307  if ( !$iranian ) {
1308  $iranian = self::tsToIranian( $ts );
1309  }
1310  $num = $iranian[1];
1311  break;
1312  case 'xmn':
1313  $usedHijriMonth = true;
1314  if ( !$hijri ) {
1315  $hijri = self::tsToHijri( $ts );
1316  }
1317  $num = $hijri[1];
1318  break;
1319  case 'xjn':
1320  $usedHebrewMonth = true;
1321  if ( !$hebrew ) {
1322  $hebrew = self::tsToHebrew( $ts );
1323  }
1324  $num = $hebrew[1];
1325  break;
1326  case 'xjt':
1327  $usedHebrewMonth = true;
1328  if ( !$hebrew ) {
1329  $hebrew = self::tsToHebrew( $ts );
1330  }
1331  $num = $hebrew[3];
1332  break;
1333  case 'Y':
1334  $usedYear = true;
1335  $num = substr( $ts, 0, 4 );
1336  break;
1337  case 'xiY':
1338  $usedIranianYear = true;
1339  if ( !$iranian ) {
1340  $iranian = self::tsToIranian( $ts );
1341  }
1342  $num = $iranian[0];
1343  break;
1344  case 'xmY':
1345  $usedHijriYear = true;
1346  if ( !$hijri ) {
1347  $hijri = self::tsToHijri( $ts );
1348  }
1349  $num = $hijri[0];
1350  break;
1351  case 'xjY':
1352  $usedHebrewYear = true;
1353  if ( !$hebrew ) {
1354  $hebrew = self::tsToHebrew( $ts );
1355  }
1356  $num = $hebrew[0];
1357  break;
1358  case 'xkY':
1359  $usedYear = true;
1360  if ( !$thai ) {
1361  $thai = self::tsToYear( $ts, 'thai' );
1362  }
1363  $num = $thai[0];
1364  break;
1365  case 'xoY':
1366  $usedYear = true;
1367  if ( !$minguo ) {
1368  $minguo = self::tsToYear( $ts, 'minguo' );
1369  }
1370  $num = $minguo[0];
1371  break;
1372  case 'xtY':
1373  $usedTennoYear = true;
1374  if ( !$tenno ) {
1375  $tenno = self::tsToYear( $ts, 'tenno' );
1376  }
1377  $num = $tenno[0];
1378  break;
1379  case 'y':
1380  $usedYear = true;
1381  $num = substr( $ts, 2, 2 );
1382  break;
1383  case 'xiy':
1384  $usedIranianYear = true;
1385  if ( !$iranian ) {
1386  $iranian = self::tsToIranian( $ts );
1387  }
1388  $num = substr( $iranian[0], -2 );
1389  break;
1390  case 'xit':
1391  $usedIranianYear = true;
1392  if ( !$iranian ) {
1393  $iranian = self::tsToIranian( $ts );
1394  }
1395  $num = self::$IRANIAN_DAYS[$iranian[1] - 1];
1396  break;
1397  case 'xiz':
1398  $usedIranianYear = true;
1399  if ( !$iranian ) {
1400  $iranian = self::tsToIranian( $ts );
1401  }
1402  $num = $iranian[3];
1403  break;
1404  case 'a':
1405  $usedAMPM = true;
1406  $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm';
1407  break;
1408  case 'A':
1409  $usedAMPM = true;
1410  $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'AM' : 'PM';
1411  break;
1412  case 'g':
1413  $usedHour = true;
1414  $h = substr( $ts, 8, 2 );
1415  $num = $h % 12 ?: 12;
1416  break;
1417  case 'G':
1418  $usedHour = true;
1419  $num = intval( substr( $ts, 8, 2 ) );
1420  break;
1421  case 'h':
1422  $usedHour = true;
1423  $h = substr( $ts, 8, 2 );
1424  $num = sprintf( '%02d', $h % 12 ?: 12 );
1425  break;
1426  case 'H':
1427  $usedHour = true;
1428  $num = substr( $ts, 8, 2 );
1429  break;
1430  case 'i':
1431  $usedMinute = true;
1432  $num = substr( $ts, 10, 2 );
1433  break;
1434  case 's':
1435  $usedSecond = true;
1436  $num = substr( $ts, 12, 2 );
1437  break;
1438  case 'c':
1439  case 'r':
1440  $usedSecond = true;
1441  // fall through
1442  case 'e':
1443  case 'O':
1444  case 'P':
1445  case 'T':
1446  $s .= self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1447  break;
1448  case 'w':
1449  case 'N':
1450  case 'z':
1451  $usedDay = true;
1452  $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1453  break;
1454  case 'W':
1455  $usedWeek = true;
1456  $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1457  break;
1458  case 't':
1459  $usedMonth = true;
1460  $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1461  break;
1462  case 'L':
1463  $usedIsLeapYear = true;
1464  $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1465  break;
1466  case 'o':
1467  $usedISOYear = true;
1468  $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1469  break;
1470  case 'U':
1471  $usedSecond = true;
1472  // fall through
1473  case 'I':
1474  case 'Z':
1475  $num = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1476  break;
1477  case '\\':
1478  # Backslash escaping
1479  if ( $p < $formatLength - 1 ) {
1480  $s .= $format[++$p];
1481  } else {
1482  $s .= '\\';
1483  }
1484  break;
1485  case '"':
1486  # Quoted literal
1487  if ( $p < $formatLength - 1 ) {
1488  $endQuote = strpos( $format, '"', $p + 1 );
1489  if ( $endQuote === false ) {
1490  # No terminating quote, assume literal "
1491  $s .= '"';
1492  } else {
1493  $s .= substr( $format, $p + 1, $endQuote - $p - 1 );
1494  $p = $endQuote;
1495  }
1496  } else {
1497  # Quote at end of string, assume literal "
1498  $s .= '"';
1499  }
1500  break;
1501  default:
1502  $s .= $format[$p];
1503  }
1504  if ( $num !== false ) {
1505  if ( $rawToggle || $raw ) {
1506  $s .= $num;
1507  $raw = false;
1508  } elseif ( $roman ) {
1509  $s .= self::romanNumeral( $num );
1510  $roman = false;
1511  } elseif ( $hebrewNum ) {
1512  $s .= self::hebrewNumeral( $num );
1513  $hebrewNum = false;
1514  } else {
1515  $s .= $this->formatNum( $num, true );
1516  }
1517  }
1518  }
1519 
1520  if ( $ttl === 'unused' ) {
1521  // No need to calculate the TTL, the caller wont use it anyway.
1522  } elseif ( $usedSecond ) {
1523  $ttl = 1;
1524  } elseif ( $usedMinute ) {
1525  $ttl = 60 - substr( $ts, 12, 2 );
1526  } elseif ( $usedHour ) {
1527  $ttl = 3600 - substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1528  } elseif ( $usedAMPM ) {
1529  $ttl = 43200 - ( substr( $ts, 8, 2 ) % 12 ) * 3600 -
1530  substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1531  } elseif (
1532  $usedDay ||
1533  $usedHebrewMonth ||
1534  $usedIranianMonth ||
1535  $usedHijriMonth ||
1536  $usedHebrewYear ||
1537  $usedIranianYear ||
1538  $usedHijriYear ||
1539  $usedTennoYear
1540  ) {
1541  // @todo Someone who understands the non-Gregorian calendars
1542  // should write proper logic for them so that they don't need purged every day.
1543  $ttl = 86400 - substr( $ts, 8, 2 ) * 3600 -
1544  substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1545  } else {
1546  $possibleTtls = [];
1547  $timeRemainingInDay = 86400 - substr( $ts, 8, 2 ) * 3600 -
1548  substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1549  if ( $usedWeek ) {
1550  $possibleTtls[] =
1551  ( 7 - self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400 +
1552  $timeRemainingInDay;
1553  } elseif ( $usedISOYear ) {
1554  // December 28th falls on the last ISO week of the year, every year.
1555  // The last ISO week of a year can be 52 or 53.
1556  $lastWeekOfISOYear = DateTime::createFromFormat(
1557  'Ymd',
1558  substr( $ts, 0, 4 ) . '1228',
1559  $zone ?: new DateTimeZone( 'UTC' )
1560  )->format( 'W' );
1561  $currentISOWeek = self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'W' );
1562  $weeksRemaining = $lastWeekOfISOYear - $currentISOWeek;
1563  $timeRemainingInWeek =
1564  ( 7 - self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400
1565  + $timeRemainingInDay;
1566  $possibleTtls[] = $weeksRemaining * 604800 + $timeRemainingInWeek;
1567  }
1568 
1569  if ( $usedMonth ) {
1570  $possibleTtls[] =
1571  ( self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 't' ) -
1572  substr( $ts, 6, 2 ) ) * 86400
1573  + $timeRemainingInDay;
1574  } elseif ( $usedYear ) {
1575  $possibleTtls[] =
1576  ( self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) + 364 -
1577  self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400
1578  + $timeRemainingInDay;
1579  } elseif ( $usedIsLeapYear ) {
1580  $year = substr( $ts, 0, 4 );
1581  $timeRemainingInYear =
1582  ( self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) + 364 -
1583  self::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400
1584  + $timeRemainingInDay;
1585  $mod = $year % 4;
1586  if ( $mod || ( !( $year % 100 ) && $year % 400 ) ) {
1587  // this isn't a leap year. see when the next one starts
1588  $nextCandidate = $year - $mod + 4;
1589  if ( $nextCandidate % 100 || !( $nextCandidate % 400 ) ) {
1590  $possibleTtls[] = ( $nextCandidate - $year - 1 ) * 365 * 86400 +
1591  $timeRemainingInYear;
1592  } else {
1593  $possibleTtls[] = ( $nextCandidate - $year + 3 ) * 365 * 86400 +
1594  $timeRemainingInYear;
1595  }
1596  } else {
1597  // this is a leap year, so the next year isn't
1598  $possibleTtls[] = $timeRemainingInYear;
1599  }
1600  }
1601 
1602  if ( $possibleTtls ) {
1603  $ttl = min( $possibleTtls );
1604  }
1605  }
1606 
1607  return $s;
1608  }
1609 
1610  private static $GREG_DAYS = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
1611  private static $IRANIAN_DAYS = [ 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 ];
1612 
1625  private static function tsToIranian( $ts ) {
1626  $gy = substr( $ts, 0, 4 ) - 1600;
1627  $gm = substr( $ts, 4, 2 ) - 1;
1628  $gd = substr( $ts, 6, 2 ) - 1;
1629 
1630  # Days passed from the beginning (including leap years)
1631  $gDayNo = 365 * $gy
1632  + floor( ( $gy + 3 ) / 4 )
1633  - floor( ( $gy + 99 ) / 100 )
1634  + floor( ( $gy + 399 ) / 400 );
1635 
1636  // Add days of the past months of this year
1637  for ( $i = 0; $i < $gm; $i++ ) {
1638  $gDayNo += self::$GREG_DAYS[$i];
1639  }
1640 
1641  // Leap years
1642  if ( $gm > 1 && ( ( $gy % 4 === 0 && $gy % 100 !== 0 || ( $gy % 400 == 0 ) ) ) ) {
1643  $gDayNo++;
1644  }
1645 
1646  // Days passed in current month
1647  $gDayNo += (int)$gd;
1648 
1649  $jDayNo = $gDayNo - 79;
1650 
1651  $jNp = floor( $jDayNo / 12053 );
1652  $jDayNo %= 12053;
1653 
1654  $jy = 979 + 33 * $jNp + 4 * floor( $jDayNo / 1461 );
1655  $jDayNo %= 1461;
1656 
1657  if ( $jDayNo >= 366 ) {
1658  $jy += floor( ( $jDayNo - 1 ) / 365 );
1659  $jDayNo = floor( ( $jDayNo - 1 ) % 365 );
1660  }
1661 
1662  $jz = $jDayNo;
1663 
1664  for ( $i = 0; $i < 11 && $jDayNo >= self::$IRANIAN_DAYS[$i]; $i++ ) {
1665  $jDayNo -= self::$IRANIAN_DAYS[$i];
1666  }
1667 
1668  $jm = $i + 1;
1669  $jd = $jDayNo + 1;
1670 
1671  return [ $jy, $jm, $jd, $jz ];
1672  }
1673 
1685  private static function tsToHijri( $ts ) {
1686  $year = substr( $ts, 0, 4 );
1687  $month = substr( $ts, 4, 2 );
1688  $day = substr( $ts, 6, 2 );
1689 
1690  $zyr = $year;
1691  $zd = $day;
1692  $zm = $month;
1693  $zy = $zyr;
1694 
1695  if (
1696  ( $zy > 1582 ) || ( ( $zy == 1582 ) && ( $zm > 10 ) ) ||
1697  ( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) )
1698  ) {
1699  $zjd = (int)( ( 1461 * ( $zy + 4800 + (int)( ( $zm - 14 ) / 12 ) ) ) / 4 ) +
1700  (int)( ( 367 * ( $zm - 2 - 12 * ( (int)( ( $zm - 14 ) / 12 ) ) ) ) / 12 ) -
1701  (int)( ( 3 * (int)( ( ( $zy + 4900 + (int)( ( $zm - 14 ) / 12 ) ) / 100 ) ) ) / 4 ) +
1702  $zd - 32075;
1703  } else {
1704  $zjd = 367 * $zy - (int)( ( 7 * ( $zy + 5001 + (int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) +
1705  (int)( ( 275 * $zm ) / 9 ) + $zd + 1729777;
1706  }
1707 
1708  $zl = $zjd - 1948440 + 10632;
1709  $zn = (int)( ( $zl - 1 ) / 10631 );
1710  $zl = $zl - 10631 * $zn + 354;
1711  $zj = ( (int)( ( 10985 - $zl ) / 5316 ) ) * ( (int)( ( 50 * $zl ) / 17719 ) ) +
1712  ( (int)( $zl / 5670 ) ) * ( (int)( ( 43 * $zl ) / 15238 ) );
1713  $zl = $zl - ( (int)( ( 30 - $zj ) / 15 ) ) * ( (int)( ( 17719 * $zj ) / 50 ) ) -
1714  ( (int)( $zj / 16 ) ) * ( (int)( ( 15238 * $zj ) / 43 ) ) + 29;
1715  $zm = (int)( ( 24 * $zl ) / 709 );
1716  $zd = $zl - (int)( ( 709 * $zm ) / 24 );
1717  $zy = 30 * $zn + $zj - 30;
1718 
1719  return [ $zy, $zm, $zd ];
1720  }
1721 
1737  private static function tsToHebrew( $ts ) {
1738  # Parse date
1739  $year = substr( $ts, 0, 4 );
1740  $month = substr( $ts, 4, 2 );
1741  $day = substr( $ts, 6, 2 );
1742 
1743  # Calculate Hebrew year
1744  $hebrewYear = $year + 3760;
1745 
1746  # Month number when September = 1, August = 12
1747  $month += 4;
1748  if ( $month > 12 ) {
1749  # Next year
1750  $month -= 12;
1751  $year++;
1752  $hebrewYear++;
1753  }
1754 
1755  # Calculate day of year from 1 September
1756  $dayOfYear = $day;
1757  for ( $i = 1; $i < $month; $i++ ) {
1758  if ( $i == 6 ) {
1759  # February
1760  $dayOfYear += 28;
1761  # Check if the year is leap
1762  if ( $year % 400 == 0 || ( $year % 4 == 0 && $year % 100 > 0 ) ) {
1763  $dayOfYear++;
1764  }
1765  } elseif ( $i == 8 || $i == 10 || $i == 1 || $i == 3 ) {
1766  $dayOfYear += 30;
1767  } else {
1768  $dayOfYear += 31;
1769  }
1770  }
1771 
1772  # Calculate the start of the Hebrew year
1773  $start = self::hebrewYearStart( $hebrewYear );
1774 
1775  # Calculate next year's start
1776  if ( $dayOfYear <= $start ) {
1777  # Day is before the start of the year - it is the previous year
1778  # Next year's start
1779  $nextStart = $start;
1780  # Previous year
1781  $year--;
1782  $hebrewYear--;
1783  # Add days since previous year's 1 September
1784  $dayOfYear += 365;
1785  if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1786  # Leap year
1787  $dayOfYear++;
1788  }
1789  # Start of the new (previous) year
1790  $start = self::hebrewYearStart( $hebrewYear );
1791  } else {
1792  # Next year's start
1793  $nextStart = self::hebrewYearStart( $hebrewYear + 1 );
1794  }
1795 
1796  # Calculate Hebrew day of year
1797  $hebrewDayOfYear = $dayOfYear - $start;
1798 
1799  # Difference between year's days
1800  $diff = $nextStart - $start;
1801  # Add 12 (or 13 for leap years) days to ignore the difference between
1802  # Hebrew and Gregorian year (353 at least vs. 365/6) - now the
1803  # difference is only about the year type
1804  if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1805  $diff += 13;
1806  } else {
1807  $diff += 12;
1808  }
1809 
1810  # Check the year pattern, and is leap year
1811  # 0 means an incomplete year, 1 means a regular year, 2 means a complete year
1812  # This is mod 30, to work on both leap years (which add 30 days of Adar I)
1813  # and non-leap years
1814  $yearPattern = $diff % 30;
1815  # Check if leap year
1816  $isLeap = $diff >= 30;
1817 
1818  # Calculate day in the month from number of day in the Hebrew year
1819  # Don't check Adar - if the day is not in Adar, we will stop before;
1820  # if it is in Adar, we will use it to check if it is Adar I or Adar II
1821  $hebrewDay = $hebrewDayOfYear;
1822  $hebrewMonth = 1;
1823  $days = 0;
1824  while ( $hebrewMonth <= 12 ) {
1825  # Calculate days in this month
1826  if ( $isLeap && $hebrewMonth == 6 ) {
1827  # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1828  $days = 30;
1829  if ( $hebrewDay <= $days ) {
1830  # Day in Adar I
1831  $hebrewMonth = 13;
1832  } else {
1833  # Subtract the days of Adar I
1834  $hebrewDay -= $days;
1835  # Try Adar II
1836  $days = 29;
1837  if ( $hebrewDay <= $days ) {
1838  # Day in Adar II
1839  $hebrewMonth = 14;
1840  }
1841  }
1842  } elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
1843  # Cheshvan in a complete year (otherwise as the rule below)
1844  $days = 30;
1845  } elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) {
1846  # Kislev in an incomplete year (otherwise as the rule below)
1847  $days = 29;
1848  } else {
1849  # Odd months have 30 days, even have 29
1850  $days = 30 - ( $hebrewMonth - 1 ) % 2;
1851  }
1852  if ( $hebrewDay <= $days ) {
1853  # In the current month
1854  break;
1855  } else {
1856  # Subtract the days of the current month
1857  $hebrewDay -= $days;
1858  # Try in the next month
1859  $hebrewMonth++;
1860  }
1861  }
1862 
1863  return [ $hebrewYear, $hebrewMonth, $hebrewDay, $days ];
1864  }
1865 
1875  private static function hebrewYearStart( $year ) {
1876  $a = intval( ( 12 * ( $year - 1 ) + 17 ) % 19 );
1877  $b = intval( ( $year - 1 ) % 4 );
1878  $m = 32.044093161144 + 1.5542417966212 * $a + $b / 4.0 - 0.0031777940220923 * ( $year - 1 );
1879  if ( $m < 0 ) {
1880  $m--;
1881  }
1882  $Mar = intval( $m );
1883  if ( $m < 0 ) {
1884  $m++;
1885  }
1886  $m -= $Mar;
1887 
1888  $c = intval( ( $Mar + 3 * ( $year - 1 ) + 5 * $b + 5 ) % 7 );
1889  if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1890  $Mar++;
1891  } elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1892  $Mar += 2;
1893  } elseif ( $c == 2 || $c == 4 || $c == 6 ) {
1894  $Mar++;
1895  }
1896 
1897  $Mar += intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1898  return $Mar;
1899  }
1900 
1913  private static function tsToYear( $ts, $cName ) {
1914  $gy = substr( $ts, 0, 4 );
1915  $gm = substr( $ts, 4, 2 );
1916  $gd = substr( $ts, 6, 2 );
1917 
1918  if ( !strcmp( $cName, 'thai' ) ) {
1919  # Thai solar dates
1920  # Add 543 years to the Gregorian calendar
1921  # Months and days are identical
1922  $gy_offset = $gy + 543;
1923  # fix for dates between 1912 and 1941
1924  # https://en.wikipedia.org/?oldid=836596673#New_year
1925  if ( $gy >= 1912 && $gy <= 1940 ) {
1926  if ( $gm <= 3 ) {
1927  $gy_offset--;
1928  }
1929  $gm = ( $gm - 3 ) % 12;
1930  }
1931  } elseif ( ( !strcmp( $cName, 'minguo' ) ) || !strcmp( $cName, 'juche' ) ) {
1932  # Minguo dates
1933  # Deduct 1911 years from the Gregorian calendar
1934  # Months and days are identical
1935  $gy_offset = $gy - 1911;
1936  } elseif ( !strcmp( $cName, 'tenno' ) ) {
1937  # Nengō dates up to Meiji period
1938  # Deduct years from the Gregorian calendar
1939  # depending on the nengo periods
1940  # Months and days are identical
1941  if ( ( $gy < 1912 )
1942  || ( ( $gy == 1912 ) && ( $gm < 7 ) )
1943  || ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd < 31 ) )
1944  ) {
1945  # Meiji period
1946  $gy_gannen = $gy - 1868 + 1;
1947  $gy_offset = $gy_gannen;
1948  if ( $gy_gannen == 1 ) {
1949  $gy_offset = '元';
1950  }
1951  $gy_offset = '明治' . $gy_offset;
1952  } elseif (
1953  ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd == 31 ) ) ||
1954  ( ( $gy == 1912 ) && ( $gm >= 8 ) ) ||
1955  ( ( $gy > 1912 ) && ( $gy < 1926 ) ) ||
1956  ( ( $gy == 1926 ) && ( $gm < 12 ) ) ||
1957  ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd < 26 ) )
1958  ) {
1959  # Taishō period
1960  $gy_gannen = $gy - 1912 + 1;
1961  $gy_offset = $gy_gannen;
1962  if ( $gy_gannen == 1 ) {
1963  $gy_offset = '元';
1964  }
1965  $gy_offset = '大正' . $gy_offset;
1966  } elseif (
1967  ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd >= 26 ) ) ||
1968  ( ( $gy > 1926 ) && ( $gy < 1989 ) ) ||
1969  ( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd < 8 ) )
1970  ) {
1971  # Shōwa period
1972  $gy_gannen = $gy - 1926 + 1;
1973  $gy_offset = $gy_gannen;
1974  if ( $gy_gannen == 1 ) {
1975  $gy_offset = '元';
1976  }
1977  $gy_offset = '昭和' . $gy_offset;
1978  } elseif (
1979  ( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd >= 8 ) ) ||
1980  ( ( $gy > 1989 ) && ( $gy < 2019 ) ) ||
1981  ( ( $gy == 2019 ) && ( $gm < 5 ) )
1982  ) {
1983  # Heisei period
1984  $gy_gannen = $gy - 1989 + 1;
1985  $gy_offset = $gy_gannen;
1986  if ( $gy_gannen == 1 ) {
1987  $gy_offset = '元';
1988  }
1989  $gy_offset = '平成' . $gy_offset;
1990  } else {
1991  # Reiwa period
1992  $gy_gannen = $gy - 2019 + 1;
1993  $gy_offset = $gy_gannen;
1994  if ( $gy_gannen == 1 ) {
1995  $gy_offset = '元';
1996  }
1997  $gy_offset = '令和' . $gy_offset;
1998  }
1999  } else {
2000  $gy_offset = $gy;
2001  }
2002 
2003  return [ $gy_offset, $gm, $gd ];
2004  }
2005 
2019  private static function strongDirFromContent( $text = '' ) {
2020  if ( !preg_match( self::$strongDirRegex, $text, $matches ) ) {
2021  return null;
2022  }
2023  if ( $matches[1] === '' ) {
2024  return 'rtl';
2025  }
2026  return 'ltr';
2027  }
2028 
2036  static function romanNumeral( $num ) {
2037  static $table = [
2038  [ '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ],
2039  [ '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ],
2040  [ '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ],
2041  [ '', 'M', 'MM', 'MMM', 'MMMM', 'MMMMM', 'MMMMMM', 'MMMMMMM',
2042  'MMMMMMMM', 'MMMMMMMMM', 'MMMMMMMMMM' ]
2043  ];
2044 
2045  $num = intval( $num );
2046  if ( $num > 10000 || $num <= 0 ) {
2047  return $num;
2048  }
2049 
2050  $s = '';
2051  for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
2052  if ( $num >= $pow10 ) {
2053  $s .= $table[$i][(int)floor( $num / $pow10 )];
2054  }
2055  $num = $num % $pow10;
2056  }
2057  return $s;
2058  }
2059 
2067  static function hebrewNumeral( $num ) {
2068  static $table = [
2069  [ '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ],
2070  [ '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ],
2071  [ '',
2072  [ 'ק' ],
2073  [ 'ר' ],
2074  [ 'ש' ],
2075  [ 'ת' ],
2076  [ 'ת', 'ק' ],
2077  [ 'ת', 'ר' ],
2078  [ 'ת', 'ש' ],
2079  [ 'ת', 'ת' ],
2080  [ 'ת', 'ת', 'ק' ],
2081  [ 'ת', 'ת', 'ר' ],
2082  ],
2083  [ '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ]
2084  ];
2085 
2086  $num = intval( $num );
2087  if ( $num > 9999 || $num <= 0 ) {
2088  return $num;
2089  }
2090 
2091  // Round thousands have special notations
2092  if ( $num === 1000 ) {
2093  return "א' אלף";
2094  } elseif ( $num % 1000 === 0 ) {
2095  return $table[0][$num / 1000] . "' אלפים";
2096  }
2097 
2098  $letters = [];
2099 
2100  for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
2101  if ( $num >= $pow10 ) {
2102  if ( $num === 15 || $num === 16 ) {
2103  $letters[] = $table[0][9];
2104  $letters[] = $table[0][$num - 9];
2105  $num = 0;
2106  } else {
2107  $letters = array_merge(
2108  $letters,
2109  (array)$table[$i][intval( $num / $pow10 )]
2110  );
2111 
2112  if ( $pow10 === 1000 ) {
2113  $letters[] = "'";
2114  }
2115  }
2116  }
2117 
2118  $num = $num % $pow10;
2119  }
2120 
2121  $preTransformLength = count( $letters );
2122  if ( $preTransformLength === 1 ) {
2123  // Add geresh (single quote) to one-letter numbers
2124  $letters[] = "'";
2125  } else {
2126  $lastIndex = $preTransformLength - 1;
2127  $letters[$lastIndex] = str_replace(
2128  [ 'כ', 'מ', 'נ', 'פ', 'צ' ],
2129  [ 'ך', 'ם', 'ן', 'ף', 'ץ' ],
2130  $letters[$lastIndex]
2131  );
2132 
2133  // Add gershayim (double quote) to multiple-letter numbers,
2134  // but exclude numbers with only one letter after the thousands
2135  // (1001-1009, 1020, 1030, 2001-2009, etc.)
2136  if ( $letters[1] === "'" && $preTransformLength === 3 ) {
2137  $letters[] = "'";
2138  } else {
2139  array_splice( $letters, -1, 0, '"' );
2140  }
2141  }
2142 
2143  return implode( $letters );
2144  }
2145 
2154  public function userAdjust( $ts, $tz = false ) {
2155  global $wgUser, $wgLocalTZoffset;
2156 
2157  if ( $tz === false ) {
2158  $tz = $wgUser->getOption( 'timecorrection' );
2159  }
2160 
2161  $data = explode( '|', $tz, 3 );
2162 
2163  if ( $data[0] == 'ZoneInfo' ) {
2164  try {
2165  $userTZ = new DateTimeZone( $data[2] );
2166  $date = new DateTime( $ts, new DateTimeZone( 'UTC' ) );
2167  $date->setTimezone( $userTZ );
2168  return $date->format( 'YmdHis' );
2169  } catch ( Exception $e ) {
2170  // Unrecognized timezone, default to 'Offset' with the stored offset.
2171  $data[0] = 'Offset';
2172  }
2173  }
2174 
2175  if ( $data[0] == 'System' || $tz == '' ) {
2176  # Global offset in minutes.
2177  $minDiff = $wgLocalTZoffset;
2178  } elseif ( $data[0] == 'Offset' ) {
2179  $minDiff = intval( $data[1] );
2180  } else {
2181  $data = explode( ':', $tz );
2182  if ( count( $data ) == 2 ) {
2183  $data[0] = intval( $data[0] );
2184  $data[1] = intval( $data[1] );
2185  $minDiff = abs( $data[0] ) * 60 + $data[1];
2186  if ( $data[0] < 0 ) {
2187  $minDiff = -$minDiff;
2188  }
2189  } else {
2190  $minDiff = intval( $data[0] ) * 60;
2191  }
2192  }
2193 
2194  # No difference ? Return time unchanged
2195  if ( $minDiff == 0 ) {
2196  return $ts;
2197  }
2198 
2199  Wikimedia\suppressWarnings(); // E_STRICT system time bitching
2200  # Generate an adjusted date; take advantage of the fact that mktime
2201  # will normalize out-of-range values so we don't have to split $minDiff
2202  # into hours and minutes.
2203  $t = mktime( (
2204  (int)substr( $ts, 8, 2 ) ), # Hours
2205  (int)substr( $ts, 10, 2 ) + $minDiff, # Minutes
2206  (int)substr( $ts, 12, 2 ), # Seconds
2207  (int)substr( $ts, 4, 2 ), # Month
2208  (int)substr( $ts, 6, 2 ), # Day
2209  (int)substr( $ts, 0, 4 ) ); # Year
2210 
2211  $date = date( 'YmdHis', $t );
2212  Wikimedia\restoreWarnings();
2213 
2214  return $date;
2215  }
2216 
2232  function dateFormat( $usePrefs = true ) {
2233  global $wgUser;
2234 
2235  if ( is_bool( $usePrefs ) ) {
2236  if ( $usePrefs ) {
2237  $datePreference = $wgUser->getDatePreference();
2238  } else {
2239  $datePreference = (string)User::getDefaultOption( 'date' );
2240  }
2241  } else {
2242  $datePreference = (string)$usePrefs;
2243  }
2244 
2245  // return int
2246  if ( $datePreference == '' ) {
2247  return 'default';
2248  }
2249 
2250  return $datePreference;
2251  }
2252 
2263  function getDateFormatString( $type, $pref ) {
2264  $wasDefault = false;
2265  if ( $pref == 'default' ) {
2266  $wasDefault = true;
2267  $pref = $this->getDefaultDateFormat();
2268  }
2269 
2270  if ( !isset( $this->dateFormatStrings[$type][$pref] ) ) {
2271  $df =
2272  $this->localisationCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2273 
2274  if ( $type === 'pretty' && $df === null ) {
2275  $df = $this->getDateFormatString( 'date', $pref );
2276  }
2277 
2278  if ( !$wasDefault && $df === null ) {
2279  $pref = $this->getDefaultDateFormat();
2280  $df = $this->getLocalisationCache()
2281  ->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2282  }
2283 
2284  $this->dateFormatStrings[$type][$pref] = $df;
2285  }
2286  return $this->dateFormatStrings[$type][$pref];
2287  }
2288 
2299  public function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
2300  $ts = wfTimestamp( TS_MW, $ts );
2301  if ( $adj ) {
2302  $ts = $this->userAdjust( $ts, $timecorrection );
2303  }
2304  $df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
2305  return $this->sprintfDate( $df, $ts );
2306  }
2307 
2318  public function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
2319  $ts = wfTimestamp( TS_MW, $ts );
2320  if ( $adj ) {
2321  $ts = $this->userAdjust( $ts, $timecorrection );
2322  }
2323  $df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
2324  return $this->sprintfDate( $df, $ts );
2325  }
2326 
2338  public function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false ) {
2339  $ts = wfTimestamp( TS_MW, $ts );
2340  if ( $adj ) {
2341  $ts = $this->userAdjust( $ts, $timecorrection );
2342  }
2343  $df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) );
2344  return $this->sprintfDate( $df, $ts );
2345  }
2346 
2357  public function formatDuration( $seconds, array $chosenIntervals = [] ) {
2358  $intervals = $this->getDurationIntervals( $seconds, $chosenIntervals );
2359 
2360  $segments = [];
2361 
2362  foreach ( $intervals as $intervalName => $intervalValue ) {
2363  // Messages: duration-seconds, duration-minutes, duration-hours, duration-days, duration-weeks,
2364  // duration-years, duration-decades, duration-centuries, duration-millennia
2365  $message = wfMessage( 'duration-' . $intervalName )->numParams( $intervalValue );
2366  $segments[] = $message->inLanguage( $this )->escaped();
2367  }
2368 
2369  return $this->listToText( $segments );
2370  }
2371 
2383  public function getDurationIntervals( $seconds, array $chosenIntervals = [] ) {
2384  if ( empty( $chosenIntervals ) ) {
2385  $chosenIntervals = [
2386  'millennia',
2387  'centuries',
2388  'decades',
2389  'years',
2390  'days',
2391  'hours',
2392  'minutes',
2393  'seconds'
2394  ];
2395  }
2396 
2397  $intervals = array_intersect_key( self::$durationIntervals, array_flip( $chosenIntervals ) );
2398  $sortedNames = array_keys( $intervals );
2399  $smallestInterval = array_pop( $sortedNames );
2400 
2401  $segments = [];
2402 
2403  foreach ( $intervals as $name => $length ) {
2404  $value = floor( $seconds / $length );
2405 
2406  if ( $value > 0 || ( $name == $smallestInterval && empty( $segments ) ) ) {
2407  $seconds -= $value * $length;
2408  $segments[$name] = $value;
2409  }
2410  }
2411 
2412  return $segments;
2413  }
2414 
2434  private function internalUserTimeAndDate( $type, $ts, User $user, array $options ) {
2435  $ts = wfTimestamp( TS_MW, $ts );
2436  $options += [ 'timecorrection' => true, 'format' => true ];
2437  if ( $options['timecorrection'] !== false ) {
2438  if ( $options['timecorrection'] === true ) {
2439  $offset = $user->getOption( 'timecorrection' );
2440  } else {
2441  $offset = $options['timecorrection'];
2442  }
2443  $ts = $this->userAdjust( $ts, $offset );
2444  }
2445  if ( $options['format'] === true ) {
2446  $format = $user->getDatePreference();
2447  } else {
2448  $format = $options['format'];
2449  }
2450  $df = $this->getDateFormatString( $type, $this->dateFormat( $format ) );
2451  return $this->sprintfDate( $df, $ts );
2452  }
2453 
2473  public function userDate( $ts, User $user, array $options = [] ) {
2474  return $this->internalUserTimeAndDate( 'date', $ts, $user, $options );
2475  }
2476 
2496  public function userTime( $ts, User $user, array $options = [] ) {
2497  return $this->internalUserTimeAndDate( 'time', $ts, $user, $options );
2498  }
2499 
2519  public function userTimeAndDate( $ts, User $user, array $options = [] ) {
2520  return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
2521  }
2522 
2538  public function getHumanTimestamp(
2539  MWTimestamp $time, MWTimestamp $relativeTo = null, User $user = null
2540  ) {
2541  if ( $relativeTo === null ) {
2542  $relativeTo = new MWTimestamp();
2543  }
2544  if ( $user === null ) {
2545  $user = RequestContext::getMain()->getUser();
2546  }
2547 
2548  // Adjust for the user's timezone.
2549  $offsetThis = $time->offsetForUser( $user );
2550  $offsetRel = $relativeTo->offsetForUser( $user );
2551 
2552  $ts = '';
2553  if ( Hooks::run( 'GetHumanTimestamp', [ &$ts, $time, $relativeTo, $user, $this ] ) ) {
2554  $ts = $this->getHumanTimestampInternal( $time, $relativeTo, $user );
2555  }
2556 
2557  // Reset the timezone on the objects.
2558  $time->timestamp->sub( $offsetThis );
2559  $relativeTo->timestamp->sub( $offsetRel );
2560 
2561  return $ts;
2562  }
2563 
2575  private function getHumanTimestampInternal(
2576  MWTimestamp $ts, MWTimestamp $relativeTo, User $user
2577  ) {
2578  $diff = $ts->diff( $relativeTo );
2579  $diffDay = (bool)( (int)$ts->timestamp->format( 'w' ) -
2580  (int)$relativeTo->timestamp->format( 'w' ) );
2581  $days = $diff->days ?: (int)$diffDay;
2582  if ( $diff->invert || $days > 5
2583  && $ts->timestamp->format( 'Y' ) !== $relativeTo->timestamp->format( 'Y' )
2584  ) {
2585  // Timestamps are in different years: use full timestamp
2586  // Also do full timestamp for future dates
2590  $format = $this->getDateFormatString( 'both', $user->getDatePreference() ?: 'default' );
2591  $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
2592  } elseif ( $days > 5 ) {
2593  // Timestamps are in same year, but more than 5 days ago: show day and month only.
2594  $format = $this->getDateFormatString( 'pretty', $user->getDatePreference() ?: 'default' );
2595  $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
2596  } elseif ( $days > 1 ) {
2597  // Timestamp within the past week: show the day of the week and time
2598  $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2599  $weekday = self::$mWeekdayMsgs[$ts->timestamp->format( 'w' )];
2600  // Messages:
2601  // sunday-at, monday-at, tuesday-at, wednesday-at, thursday-at, friday-at, saturday-at
2602  $ts = wfMessage( "$weekday-at" )
2603  ->inLanguage( $this )
2604  ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2605  ->text();
2606  } elseif ( $days == 1 ) {
2607  // Timestamp was yesterday: say 'yesterday' and the time.
2608  $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2609  $ts = wfMessage( 'yesterday-at' )
2610  ->inLanguage( $this )
2611  ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2612  ->text();
2613  } elseif ( $diff->h > 1 || $diff->h == 1 && $diff->i > 30 ) {
2614  // Timestamp was today, but more than 90 minutes ago: say 'today' and the time.
2615  $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2616  $ts = wfMessage( 'today-at' )
2617  ->inLanguage( $this )
2618  ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2619  ->text();
2620 
2621  // From here on in, the timestamp was soon enough ago so that we can simply say
2622  // XX units ago, e.g., "2 hours ago" or "5 minutes ago"
2623  } elseif ( $diff->h == 1 ) {
2624  // Less than 90 minutes, but more than an hour ago.
2625  $ts = wfMessage( 'hours-ago' )->inLanguage( $this )->numParams( 1 )->text();
2626  } elseif ( $diff->i >= 1 ) {
2627  // A few minutes ago.
2628  $ts = wfMessage( 'minutes-ago' )->inLanguage( $this )->numParams( $diff->i )->text();
2629  } elseif ( $diff->s >= 30 ) {
2630  // Less than a minute, but more than 30 sec ago.
2631  $ts = wfMessage( 'seconds-ago' )->inLanguage( $this )->numParams( $diff->s )->text();
2632  } else {
2633  // Less than 30 seconds ago.
2634  $ts = wfMessage( 'just-now' )->text();
2635  }
2636 
2637  return $ts;
2638  }
2639 
2644  public function getMessage( $key ) {
2645  return $this->localisationCache->getSubitem( $this->mCode, 'messages', $key );
2646  }
2647 
2651  function getAllMessages() {
2652  return $this->localisationCache->getItem( $this->mCode, 'messages' );
2653  }
2654 
2661  public function iconv( $in, $out, $string ) {
2662  # Even with //IGNORE iconv can whine about illegal characters in
2663  # *input* string. We just ignore those too.
2664  # REF: https://bugs.php.net/bug.php?id=37166
2665  # REF: https://phabricator.wikimedia.org/T18885
2666  Wikimedia\suppressWarnings();
2667  $text = iconv( $in, $out . '//IGNORE', $string );
2668  Wikimedia\restoreWarnings();
2669  return $text;
2670  }
2671 
2672  // callback functions for ucwords(), ucwordbreaks()
2673 
2679  return $this->ucfirst( $matches[1] );
2680  }
2681 
2687  return mb_strtoupper( $matches[0] );
2688  }
2689 
2695  return mb_strtoupper( $matches[0] );
2696  }
2697 
2705  public function ucfirst( $str ) {
2706  $o = ord( $str );
2707  if ( $o < 96 ) { // if already uppercase...
2708  return $str;
2709  } elseif ( $o < 128 ) {
2710  return ucfirst( $str ); // use PHP's ucfirst()
2711  } else {
2712  // fall back to more complex logic in case of multibyte strings
2713  return $this->uc( $str, true );
2714  }
2715  }
2716 
2725  public function uc( $str, $first = false ) {
2726  if ( $first ) {
2727  if ( $this->isMultibyte( $str ) ) {
2728  return $this->mbUpperChar( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2729  } else {
2730  return ucfirst( $str );
2731  }
2732  } else {
2733  return $this->isMultibyte( $str ) ? mb_strtoupper( $str ) : strtoupper( $str );
2734  }
2735  }
2736 
2750  protected function mbUpperChar( $char ) {
2752  if ( array_key_exists( $char, $wgOverrideUcfirstCharacters ) ) {
2753  return $wgOverrideUcfirstCharacters[$char];
2754  } else {
2755  return mb_strtoupper( $char );
2756  }
2757  }
2758 
2763  function lcfirst( $str ) {
2764  $o = ord( $str );
2765  if ( !$o ) {
2766  return strval( $str );
2767  } elseif ( $o >= 128 ) {
2768  return $this->lc( $str, true );
2769  } elseif ( $o > 96 ) {
2770  return $str;
2771  } else {
2772  $str[0] = strtolower( $str[0] );
2773  return $str;
2774  }
2775  }
2776 
2782  function lc( $str, $first = false ) {
2783  if ( $first ) {
2784  if ( $this->isMultibyte( $str ) ) {
2785  return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2786  } else {
2787  return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
2788  }
2789  } else {
2790  return $this->isMultibyte( $str ) ? mb_strtolower( $str ) : strtolower( $str );
2791  }
2792  }
2793 
2798  function isMultibyte( $str ) {
2799  return strlen( $str ) !== mb_strlen( $str );
2800  }
2801 
2806  function ucwords( $str ) {
2807  if ( $this->isMultibyte( $str ) ) {
2808  $str = $this->lc( $str );
2809 
2810  // regexp to find first letter in each word (i.e. after each space)
2811  $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2812 
2813  // function to use to capitalize a single char
2814  return preg_replace_callback(
2815  $replaceRegexp,
2816  [ $this, 'ucwordsCallbackMB' ],
2817  $str
2818  );
2819  } else {
2820  return ucwords( strtolower( $str ) );
2821  }
2822  }
2823 
2830  function ucwordbreaks( $str ) {
2831  if ( $this->isMultibyte( $str ) ) {
2832  $str = $this->lc( $str );
2833 
2834  // since \b doesn't work for UTF-8, we explicitely define word break chars
2835  $breaks = "[ \-\(\)\}\{\.,\?!]";
2836 
2837  // find first letter after word break
2838  $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|" .
2839  "$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2840 
2841  return preg_replace_callback(
2842  $replaceRegexp,
2843  [ $this, 'ucwordbreaksCallbackMB' ],
2844  $str
2845  );
2846  } else {
2847  return preg_replace_callback(
2848  '/\b([\w\x80-\xff]+)\b/',
2849  [ $this, 'ucwordbreaksCallbackAscii' ],
2850  $str
2851  );
2852  }
2853  }
2854 
2870  function caseFold( $s ) {
2871  return $this->uc( $s );
2872  }
2873 
2879  function checkTitleEncoding( $s ) {
2880  if ( is_array( $s ) ) {
2881  throw new MWException( 'Given array to checkTitleEncoding.' );
2882  }
2883  if ( StringUtils::isUtf8( $s ) ) {
2884  return $s;
2885  }
2886 
2887  return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s );
2888  }
2889 
2894  return $this->localisationCache->getItem( $this->mCode, 'fallback8bitEncoding' );
2895  }
2896 
2905  function hasWordBreaks() {
2906  return true;
2907  }
2908 
2916  function segmentByWord( $string ) {
2917  return $string;
2918  }
2919 
2927  function normalizeForSearch( $string ) {
2928  return self::convertDoubleWidth( $string );
2929  }
2930 
2939  protected static function convertDoubleWidth( $string ) {
2940  static $full = null;
2941  static $half = null;
2942 
2943  if ( $full === null ) {
2944  $fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2945  $halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2946  $full = str_split( $fullWidth, 3 );
2947  $half = str_split( $halfWidth );
2948  }
2949 
2950  $string = str_replace( $full, $half, $string );
2951  return $string;
2952  }
2953 
2959  protected static function insertSpace( $string, $pattern ) {
2960  $string = preg_replace( $pattern, " $1 ", $string );
2961  $string = preg_replace( '/ +/', ' ', $string );
2962  return $string;
2963  }
2964 
2969  function convertForSearchResult( $termsArray ) {
2970  # some languages, e.g. Chinese, need to do a conversion
2971  # in order for search results to be displayed correctly
2972  return $termsArray;
2973  }
2974 
2981  function firstChar( $s ) {
2982  $matches = [];
2983  preg_match(
2984  '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
2985  '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/',
2986  $s,
2987  $matches
2988  );
2989 
2990  if ( isset( $matches[1] ) ) {
2991  if ( strlen( $matches[1] ) != 3 ) {
2992  return $matches[1];
2993  }
2994 
2995  // Break down Hangul syllables to grab the first jamo
2996  $code = UtfNormal\Utils::utf8ToCodepoint( $matches[1] );
2997  if ( $code < 0xac00 || 0xd7a4 <= $code ) {
2998  return $matches[1];
2999  } elseif ( $code < 0xb098 ) {
3000  return "\u{3131}";
3001  } elseif ( $code < 0xb2e4 ) {
3002  return "\u{3134}";
3003  } elseif ( $code < 0xb77c ) {
3004  return "\u{3137}";
3005  } elseif ( $code < 0xb9c8 ) {
3006  return "\u{3139}";
3007  } elseif ( $code < 0xbc14 ) {
3008  return "\u{3141}";
3009  } elseif ( $code < 0xc0ac ) {
3010  return "\u{3142}";
3011  } elseif ( $code < 0xc544 ) {
3012  return "\u{3145}";
3013  } elseif ( $code < 0xc790 ) {
3014  return "\u{3147}";
3015  } elseif ( $code < 0xcc28 ) {
3016  return "\u{3148}";
3017  } elseif ( $code < 0xce74 ) {
3018  return "\u{314A}";
3019  } elseif ( $code < 0xd0c0 ) {
3020  return "\u{314B}";
3021  } elseif ( $code < 0xd30c ) {
3022  return "\u{314C}";
3023  } elseif ( $code < 0xd558 ) {
3024  return "\u{314D}";
3025  } else {
3026  return "\u{314E}";
3027  }
3028  } else {
3029  return '';
3030  }
3031  }
3032 
3044  public function normalize( $s ) {
3045  global $wgAllUnicodeFixes;
3046  $s = UtfNormal\Validator::cleanUp( $s );
3047  if ( $wgAllUnicodeFixes ) {
3048  $s = $this->transformUsingPairFile( 'normalize-ar.php', $s );
3049  $s = $this->transformUsingPairFile( 'normalize-ml.php', $s );
3050  }
3051 
3052  return $s;
3053  }
3054 
3069  protected function transformUsingPairFile( $file, $string ) {
3070  if ( !isset( $this->transformData[$file] ) ) {
3071  global $IP;
3072  $data = require "$IP/languages/data/{$file}";
3073  $this->transformData[$file] = new ReplacementArray( $data );
3074  }
3075  return $this->transformData[$file]->replace( $string );
3076  }
3077 
3083  function isRTL() {
3084  return $this->localisationCache->getItem( $this->mCode, 'rtl' );
3085  }
3086 
3091  function getDir() {
3092  return $this->isRTL() ? 'rtl' : 'ltr';
3093  }
3094 
3103  function alignStart() {
3104  return $this->isRTL() ? 'right' : 'left';
3105  }
3106 
3115  function alignEnd() {
3116  return $this->isRTL() ? 'left' : 'right';
3117  }
3118 
3130  function getDirMarkEntity( $opposite = false ) {
3131  if ( $opposite ) {
3132  return $this->isRTL() ? '&lrm;' : '&rlm;';
3133  }
3134  return $this->isRTL() ? '&rlm;' : '&lrm;';
3135  }
3136 
3147  function getDirMark( $opposite = false ) {
3148  $lrm = "\u{200E}"; # LEFT-TO-RIGHT MARK, commonly abbreviated LRM
3149  $rlm = "\u{200F}"; # RIGHT-TO-LEFT MARK, commonly abbreviated RLM
3150  if ( $opposite ) {
3151  return $this->isRTL() ? $lrm : $rlm;
3152  }
3153  return $this->isRTL() ? $rlm : $lrm;
3154  }
3155 
3159  function capitalizeAllNouns() {
3160  return $this->localisationCache->getItem( $this->mCode, 'capitalizeAllNouns' );
3161  }
3162 
3170  function getArrow( $direction = 'forwards' ) {
3171  switch ( $direction ) {
3172  case 'forwards':
3173  return $this->isRTL() ? '←' : '→';
3174  case 'backwards':
3175  return $this->isRTL() ? '→' : '←';
3176  case 'left':
3177  return '←';
3178  case 'right':
3179  return '→';
3180  case 'up':
3181  return '↑';
3182  case 'down':
3183  return '↓';
3184  }
3185  }
3186 
3192  function linkPrefixExtension() {
3193  return $this->localisationCache->getItem( $this->mCode, 'linkPrefixExtension' );
3194  }
3195 
3200  function getMagicWords() {
3201  return $this->localisationCache->getItem( $this->mCode, 'magicWords' );
3202  }
3203 
3209  function getMagic( $mw ) {
3210  $rawEntry = $this->mMagicExtensions[$mw->mId] ??
3211  $this->localisationCache->getSubitem( $this->mCode, 'magicWords', $mw->mId );
3212 
3213  if ( !is_array( $rawEntry ) ) {
3214  wfWarn( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
3215  } else {
3216  $mw->mCaseSensitive = $rawEntry[0];
3217  $mw->mSynonyms = array_slice( $rawEntry, 1 );
3218  }
3219  }
3220 
3226  function addMagicWordsByLang( $newWords ) {
3227  $fallbackChain = $this->getFallbackLanguages();
3228  $fallbackChain = array_reverse( $fallbackChain );
3229  foreach ( $fallbackChain as $code ) {
3230  if ( isset( $newWords[$code] ) ) {
3231  $this->mMagicExtensions = $newWords[$code] + $this->mMagicExtensions;
3232  }
3233  }
3234  }
3235 
3242  // Cache aliases because it may be slow to load them
3243  if ( is_null( $this->mExtendedSpecialPageAliases ) ) {
3244  // Initialise array
3245  $this->mExtendedSpecialPageAliases =
3246  $this->localisationCache->getItem( $this->mCode, 'specialPageAliases' );
3247  }
3248 
3250  }
3251 
3258  function emphasize( $text ) {
3259  return "<em>$text</em>";
3260  }
3261 
3284  public function formatNum( $number, $nocommafy = false ) {
3285  global $wgTranslateNumerals;
3286  if ( !$nocommafy ) {
3287  $number = $this->commafy( $number );
3288  $s = $this->separatorTransformTable();
3289  if ( $s ) {
3290  $number = strtr( $number, $s );
3291  }
3292  }
3293 
3294  if ( $wgTranslateNumerals ) {
3295  $s = $this->digitTransformTable();
3296  if ( $s ) {
3297  $number = strtr( $number, $s );
3298  }
3299  }
3300 
3301  return (string)$number;
3302  }
3303 
3312  public function formatNumNoSeparators( $number ) {
3313  return $this->formatNum( $number, true );
3314  }
3315 
3320  public function parseFormattedNumber( $number ) {
3321  $s = $this->digitTransformTable();
3322  if ( $s ) {
3323  // eliminate empty array values such as ''. (T66347)
3324  $s = array_filter( $s );
3325  $number = strtr( $number, array_flip( $s ) );
3326  }
3327 
3328  $s = $this->separatorTransformTable();
3329  if ( $s ) {
3330  // eliminate empty array values such as ''. (T66347)
3331  $s = array_filter( $s );
3332  $number = strtr( $number, array_flip( $s ) );
3333  }
3334 
3335  $number = strtr( $number, [ ',' => '' ] );
3336  return $number;
3337  }
3338 
3345  function commafy( $number ) {
3348  if ( $number === null ) {
3349  return '';
3350  }
3351 
3352  if ( !$digitGroupingPattern || $digitGroupingPattern === "###,###,###" ) {
3353  // Default grouping is at thousands, use the same for ###,###,### pattern too.
3354  // In some languages it's conventional not to insert a thousands separator
3355  // in numbers that are four digits long (1000-9999).
3356  if ( $minimumGroupingDigits ) {
3357  // Number of '#' characters after last comma in the grouping pattern.
3358  // The pattern is hardcoded here, but this would vary for different patterns.
3359  $primaryGroupingSize = 3;
3360  // Maximum length of a number to suppress digit grouping for.
3361  $maximumLength = $minimumGroupingDigits + $primaryGroupingSize - 1;
3362  if ( preg_match( '/^\-?\d{1,' . $maximumLength . '}(\.\d+)?$/', $number ) ) {
3363  return $number;
3364  }
3365  }
3366  return strrev( (string)preg_replace( '/(\d{3})(?=\d)(?!\d*\.)/', '$1,', strrev( $number ) ) );
3367  } else {
3368  // Ref: http://cldr.unicode.org/translation/number-patterns
3369  $sign = "";
3370  if ( intval( $number ) < 0 ) {
3371  // For negative numbers apply the algorithm like positive number and add sign.
3372  $sign = "-";
3373  $number = substr( $number, 1 );
3374  }
3375  $integerPart = [];
3376  $decimalPart = [];
3377  $numMatches = preg_match_all( "/(#+)/", $digitGroupingPattern, $matches );
3378  preg_match( "/\d+/", $number, $integerPart );
3379  preg_match( "/\.\d*/", $number, $decimalPart );
3380  $groupedNumber = ( count( $decimalPart ) > 0 ) ? $decimalPart[0] : "";
3381  if ( $groupedNumber === $number ) {
3382  // the string does not have any number part. Eg: .12345
3383  return $sign . $groupedNumber;
3384  }
3385  $start = $end = ( $integerPart ) ? strlen( $integerPart[0] ) : 0;
3386  while ( $start > 0 ) {
3387  $match = $matches[0][$numMatches - 1];
3388  $matchLen = strlen( $match );
3389  $start = $end - $matchLen;
3390  if ( $start < 0 ) {
3391  $start = 0;
3392  }
3393  $groupedNumber = substr( $number, $start, $end - $start ) . $groupedNumber;
3394  $end = $start;
3395  if ( $numMatches > 1 ) {
3396  // use the last pattern for the rest of the number
3397  $numMatches--;
3398  }
3399  if ( $start > 0 ) {
3400  $groupedNumber = "," . $groupedNumber;
3401  }
3402  }
3403  return $sign . $groupedNumber;
3404  }
3405  }
3406 
3411  return $this->localisationCache->getItem( $this->mCode, 'digitGroupingPattern' );
3412  }
3413 
3417  function digitTransformTable() {
3418  return $this->localisationCache->getItem( $this->mCode, 'digitTransformTable' );
3419  }
3420 
3425  return $this->localisationCache->getItem( $this->mCode, 'separatorTransformTable' );
3426  }
3427 
3432  return $this->localisationCache->getItem( $this->mCode, 'minimumGroupingDigits' );
3433  }
3434 
3443  public function listToText( array $list ) {
3444  $itemCount = count( $list );
3445  if ( $itemCount < 1 ) {
3446  return '';
3447  }
3448  $text = array_pop( $list );
3449  if ( $itemCount > 1 ) {
3450  $and = $this->msg( 'and' )->escaped();
3451  $space = $this->msg( 'word-separator' )->escaped();
3452  $comma = '';
3453  if ( $itemCount > 2 ) {
3454  $comma = $this->msg( 'comma-separator' )->escaped();
3455  }
3456  $text = implode( $comma, $list ) . $and . $space . $text;
3457  }
3458  return $text;
3459  }
3460 
3467  function commaList( array $list ) {
3468  return implode(
3469  wfMessage( 'comma-separator' )->inLanguage( $this )->escaped(),
3470  $list
3471  );
3472  }
3473 
3480  function semicolonList( array $list ) {
3481  return implode(
3482  wfMessage( 'semicolon-separator' )->inLanguage( $this )->escaped(),
3483  $list
3484  );
3485  }
3486 
3492  function pipeList( array $list ) {
3493  return implode(
3494  wfMessage( 'pipe-separator' )->inLanguage( $this )->escaped(),
3495  $list
3496  );
3497  }
3498 
3514  function truncateForDatabase( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3515  return $this->truncateInternal(
3516  $string, $length, $ellipsis, $adjustLength, 'strlen', 'substr'
3517  );
3518  }
3519 
3538  function truncateForVisual( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3539  // Passing encoding to mb_strlen and mb_substr is optional.
3540  // Encoding defaults to mb_internal_encoding(), which is set to UTF-8 in Setup.php, so
3541  // explicit specification of encoding is skipped.
3542  // Note: Both multibyte methods are callables invoked in truncateInternal.
3543  return $this->truncateInternal(
3544  $string, $length, $ellipsis, $adjustLength, 'mb_strlen', 'mb_substr'
3545  );
3546  }
3547 
3564  private function truncateInternal(
3565  $string, $length, $ellipsis, $adjustLength, callable $measureLength, callable $getSubstring
3566  ) {
3567  # Check if there is no need to truncate
3568  if ( $measureLength( $string ) <= abs( $length ) ) {
3569  return $string; // no need to truncate
3570  }
3571 
3572  # Use the localized ellipsis character
3573  if ( $ellipsis == '...' ) {
3574  $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3575  }
3576  if ( $length == 0 ) {
3577  return $ellipsis; // convention
3578  }
3579 
3580  $stringOriginal = $string;
3581  # If ellipsis length is >= $length then we can't apply $adjustLength
3582  if ( $adjustLength && $measureLength( $ellipsis ) >= abs( $length ) ) {
3583  $string = $ellipsis; // this can be slightly unexpected
3584  # Otherwise, truncate and add ellipsis...
3585  } else {
3586  $ellipsisLength = $adjustLength ? $measureLength( $ellipsis ) : 0;
3587  if ( $length > 0 ) {
3588  $length -= $ellipsisLength;
3589  $string = $getSubstring( $string, 0, $length ); // xyz...
3590  $string = $this->removeBadCharLast( $string );
3591  $string = rtrim( $string ) . $ellipsis;
3592  } else {
3593  $length += $ellipsisLength;
3594  $string = $getSubstring( $string, $length ); // ...xyz
3595  $string = $this->removeBadCharFirst( $string );
3596  $string = $ellipsis . ltrim( $string );
3597  }
3598  }
3599 
3600  # Do not truncate if the ellipsis makes the string longer/equal (T24181).
3601  # This check is *not* redundant if $adjustLength, due to the single case where
3602  # LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string.
3603  if ( $measureLength( $string ) < $measureLength( $stringOriginal ) ) {
3604  return $string;
3605  } else {
3606  return $stringOriginal;
3607  }
3608  }
3609 
3617  protected function removeBadCharLast( $string ) {
3618  if ( $string != '' ) {
3619  $char = ord( $string[strlen( $string ) - 1] );
3620  $m = [];
3621  if ( $char >= 0xc0 ) {
3622  # We got the first byte only of a multibyte char; remove it.
3623  $string = substr( $string, 0, -1 );
3624  } elseif ( $char >= 0x80 &&
3625  // Use the /s modifier (PCRE_DOTALL) so (.*) also matches newlines
3626  preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
3627  '[\xf0-\xf7][\x80-\xbf]{1,2})$/s', $string, $m )
3628  ) {
3629  # We chopped in the middle of a character; remove it
3630  $string = $m[1];
3631  }
3632  }
3633  return $string;
3634  }
3635 
3643  protected function removeBadCharFirst( $string ) {
3644  if ( $string != '' ) {
3645  $char = ord( $string[0] );
3646  if ( $char >= 0x80 && $char < 0xc0 ) {
3647  # We chopped in the middle of a character; remove the whole thing
3648  $string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
3649  }
3650  }
3651  return $string;
3652  }
3653 
3669  function truncateHtml( $text, $length, $ellipsis = '...' ) {
3670  # Use the localized ellipsis character
3671  if ( $ellipsis == '...' ) {
3672  $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3673  }
3674  # Check if there is clearly no need to truncate
3675  if ( $length <= 0 ) {
3676  return $ellipsis; // no text shown, nothing to format (convention)
3677  } elseif ( strlen( $text ) <= $length ) {
3678  return $text; // string short enough even *with* HTML (short-circuit)
3679  }
3680 
3681  $dispLen = 0; // innerHTML legth so far
3682  $testingEllipsis = false; // checking if ellipses will make string longer/equal?
3683  $tagType = 0; // 0-open, 1-close
3684  $bracketState = 0; // 1-tag start, 2-tag name, 0-neither
3685  $entityState = 0; // 0-not entity, 1-entity
3686  $tag = $ret = ''; // accumulated tag name, accumulated result string
3687  $openTags = []; // open tag stack
3688  $maybeState = null; // possible truncation state
3689 
3690  $textLen = strlen( $text );
3691  $neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated
3692  for ( $pos = 0; true; ++$pos ) {
3693  # Consider truncation once the display length has reached the maximim.
3694  # We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
3695  # Check that we're not in the middle of a bracket/entity...
3696  if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) {
3697  if ( !$testingEllipsis ) {
3698  $testingEllipsis = true;
3699  # Save where we are; we will truncate here unless there turn out to
3700  # be so few remaining characters that truncation is not necessary.
3701  if ( !$maybeState ) { // already saved? ($neLength = 0 case)
3702  $maybeState = [ $ret, $openTags ]; // save state
3703  }
3704  } elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) {
3705  # String in fact does need truncation, the truncation point was OK.
3706  // @phan-suppress-next-line PhanTypeInvalidExpressionArrayDestructuring
3707  list( $ret, $openTags ) = $maybeState; // reload state
3708  $ret = $this->removeBadCharLast( $ret ); // multi-byte char fix
3709  $ret .= $ellipsis; // add ellipsis
3710  break;
3711  }
3712  }
3713  if ( $pos >= $textLen ) {
3714  break; // extra iteration just for above checks
3715  }
3716 
3717  # Read the next char...
3718  $ch = $text[$pos];
3719  $lastCh = $pos ? $text[$pos - 1] : '';
3720  $ret .= $ch; // add to result string
3721  if ( $ch == '<' ) {
3722  $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); // for bad HTML
3723  $entityState = 0; // for bad HTML
3724  $bracketState = 1; // tag started (checking for backslash)
3725  } elseif ( $ch == '>' ) {
3726  $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags );
3727  $entityState = 0; // for bad HTML
3728  $bracketState = 0; // out of brackets
3729  } elseif ( $bracketState == 1 ) {
3730  if ( $ch == '/' ) {
3731  $tagType = 1; // close tag (e.g. "</span>")
3732  } else {
3733  $tagType = 0; // open tag (e.g. "<span>")
3734  $tag .= $ch;
3735  }
3736  $bracketState = 2; // building tag name
3737  } elseif ( $bracketState == 2 ) {
3738  if ( $ch != ' ' ) {
3739  $tag .= $ch;
3740  } else {
3741  // Name found (e.g. "<a href=..."), add on tag attributes...
3742  $pos += $this->truncate_skip( $ret, $text, "<>", $pos + 1 );
3743  }
3744  } elseif ( $bracketState == 0 ) {
3745  if ( $entityState ) {
3746  if ( $ch == ';' ) {
3747  $entityState = 0;
3748  $dispLen++; // entity is one displayed char
3749  }
3750  } else {
3751  if ( $neLength == 0 && !$maybeState ) {
3752  // Save state without $ch. We want to *hit* the first
3753  // display char (to get tags) but not *use* it if truncating.
3754  $maybeState = [ substr( $ret, 0, -1 ), $openTags ];
3755  }
3756  if ( $ch == '&' ) {
3757  $entityState = 1; // entity found, (e.g. "&#160;")
3758  } else {
3759  $dispLen++; // this char is displayed
3760  // Add the next $max display text chars after this in one swoop...
3761  $max = ( $testingEllipsis ? $length : $neLength ) - $dispLen;
3762  $skipped = $this->truncate_skip( $ret, $text, "<>&", $pos + 1, $max );
3763  $dispLen += $skipped;
3764  $pos += $skipped;
3765  }
3766  }
3767  }
3768  }
3769  // Close the last tag if left unclosed by bad HTML
3770  $this->truncate_endBracket( $tag, $text[$textLen - 1], $tagType, $openTags );
3771  while ( count( $openTags ) > 0 ) {
3772  $ret .= '</' . array_pop( $openTags ) . '>'; // close open tags
3773  }
3774  return $ret;
3775  }
3776 
3788  private function truncate_skip( &$ret, $text, $search, $start, $len = null ) {
3789  if ( $len === null ) {
3790  $len = -1; // -1 means "no limit" for strcspn
3791  } elseif ( $len < 0 ) {
3792  $len = 0; // sanity
3793  }
3794  $skipCount = 0;
3795  if ( $start < strlen( $text ) ) {
3796  $skipCount = strcspn( $text, $search, $start, $len );
3797  $ret .= substr( $text, $start, $skipCount );
3798  }
3799  return $skipCount;
3800  }
3801 
3811  private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) {
3812  $tag = ltrim( $tag );
3813  if ( $tag != '' ) {
3814  if ( $tagType == 0 && $lastCh != '/' ) {
3815  $openTags[] = $tag; // tag opened (didn't close itself)
3816  } elseif ( $tagType == 1 ) {
3817  if ( $openTags && $tag == $openTags[count( $openTags ) - 1] ) {
3818  array_pop( $openTags ); // tag closed
3819  }
3820  }
3821  $tag = '';
3822  }
3823  }
3824 
3833  function convertGrammar( $word, $case ) {
3834  global $wgGrammarForms;
3835  if ( isset( $wgGrammarForms[$this->getCode()][$case][$word] ) ) {
3836  return $wgGrammarForms[$this->getCode()][$case][$word];
3837  }
3838 
3839  $grammarTransformations = $this->getGrammarTransformations();
3840 
3841  if ( isset( $grammarTransformations[$case] ) ) {
3842  $forms = $grammarTransformations[$case];
3843 
3844  // Some names of grammar rules are aliases for other rules.
3845  // In such cases the value is a string rather than object,
3846  // so load the actual rules.
3847  if ( is_string( $forms ) ) {
3848  $forms = $grammarTransformations[$forms];
3849  }
3850 
3851  foreach ( array_values( $forms ) as $rule ) {
3852  $form = $rule[0];
3853 
3854  if ( $form === '@metadata' ) {
3855  continue;
3856  }
3857 
3858  $replacement = $rule[1];
3859 
3860  $regex = '/' . addcslashes( $form, '/' ) . '/u';
3861  $patternMatches = preg_match( $regex, $word );
3862 
3863  if ( $patternMatches === false ) {
3864  wfLogWarning(
3865  'An error occurred while processing grammar. ' .
3866  "Word: '$word'. Regex: /$form/."
3867  );
3868  } elseif ( $patternMatches === 1 ) {
3869  $word = preg_replace( $regex, $replacement, $word );
3870 
3871  break;
3872  }
3873  }
3874  }
3875 
3876  return $word;
3877  }
3878 
3884  function getGrammarForms() {
3885  global $wgGrammarForms;
3886  if ( isset( $wgGrammarForms[$this->getCode()] )
3887  && is_array( $wgGrammarForms[$this->getCode()] )
3888  ) {
3889  return $wgGrammarForms[$this->getCode()];
3890  }
3891 
3892  return [];
3893  }
3894 
3904  public function getGrammarTransformations() {
3905  $languageCode = $this->getCode();
3906 
3907  if ( self::$grammarTransformations === null ) {
3908  self::$grammarTransformations = new MapCacheLRU( 10 );
3909  }
3910 
3911  if ( self::$grammarTransformations->has( $languageCode ) ) {
3912  return self::$grammarTransformations->get( $languageCode );
3913  }
3914 
3915  $data = [];
3916 
3917  $grammarDataFile = __DIR__ . "/data/grammarTransformations/$languageCode.json";
3918  if ( is_readable( $grammarDataFile ) ) {
3919  $data = FormatJson::decode(
3920  file_get_contents( $grammarDataFile ),
3921  true
3922  );
3923 
3924  if ( $data === null ) {
3925  throw new MWException( "Invalid grammar data for \"$languageCode\"." );
3926  }
3927 
3928  self::$grammarTransformations->set( $languageCode, $data );
3929  }
3930 
3931  return $data;
3932  }
3933 
3953  function gender( $gender, $forms ) {
3954  if ( !count( $forms ) ) {
3955  return '';
3956  }
3957  $forms = $this->preConvertPlural( $forms, 2 );
3958  if ( $gender === 'male' ) {
3959  return $forms[0];
3960  }
3961  if ( $gender === 'female' ) {
3962  return $forms[1];
3963  }
3964  return $forms[2] ?? $forms[0];
3965  }
3966 
3982  function convertPlural( $count, $forms ) {
3983  // Handle explicit n=pluralform cases
3984  $forms = $this->handleExplicitPluralForms( $count, $forms );
3985  if ( is_string( $forms ) ) {
3986  return $forms;
3987  }
3988  if ( !count( $forms ) ) {
3989  return '';
3990  }
3991 
3992  $pluralForm = $this->getPluralRuleIndexNumber( $count );
3993  $pluralForm = min( $pluralForm, count( $forms ) - 1 );
3994  return $forms[$pluralForm];
3995  }
3996 
4012  protected function handleExplicitPluralForms( $count, array $forms ) {
4013  foreach ( $forms as $index => $form ) {
4014  if ( preg_match( '/\d+=/i', $form ) ) {
4015  $pos = strpos( $form, '=' );
4016  if ( substr( $form, 0, $pos ) === (string)$count ) {
4017  return substr( $form, $pos + 1 );
4018  }
4019  unset( $forms[$index] );
4020  }
4021  }
4022  return array_values( $forms );
4023  }
4024 
4033  protected function preConvertPlural( /* Array */ $forms, $count ) {
4034  return array_pad( $forms, $count, end( $forms ) );
4035  }
4036 
4053  public function embedBidi( $text = '' ) {
4054  $dir = self::strongDirFromContent( $text );
4055  if ( $dir === 'ltr' ) {
4056  // Wrap in LEFT-TO-RIGHT EMBEDDING ... POP DIRECTIONAL FORMATTING
4057  return self::$lre . $text . self::$pdf;
4058  }
4059  if ( $dir === 'rtl' ) {
4060  // Wrap in RIGHT-TO-LEFT EMBEDDING ... POP DIRECTIONAL FORMATTING
4061  return self::$rle . $text . self::$pdf;
4062  }
4063  // No strong directionality: do not wrap
4064  return $text;
4065  }
4066 
4080  function translateBlockExpiry( $str, User $user = null, $now = 0 ) {
4081  $duration = SpecialBlock::getSuggestedDurations( $this );
4082  foreach ( $duration as $show => $value ) {
4083  if ( strcmp( $str, $value ) == 0 ) {
4084  return htmlspecialchars( trim( $show ) );
4085  }
4086  }
4087 
4088  if ( wfIsInfinity( $str ) ) {
4089  foreach ( $duration as $show => $value ) {
4090  if ( wfIsInfinity( $value ) ) {
4091  return htmlspecialchars( trim( $show ) );
4092  }
4093  }
4094  }
4095 
4096  // If all else fails, return a standard duration or timestamp description.
4097  $time = strtotime( $str, $now );
4098  if ( $time === false ) { // Unknown format. Return it as-is in case.
4099  return $str;
4100  } elseif ( $time !== strtotime( $str, $now + 1 ) ) { // It's a relative timestamp.
4101  // The result differs based on current time, so the difference
4102  // is a fixed duration length.
4103  return $this->formatDuration( $time - $now );
4104  } else { // It's an absolute timestamp.
4105  if ( $time === 0 ) {
4106  // wfTimestamp() handles 0 as current time instead of epoch.
4107  $time = '19700101000000';
4108  }
4109  if ( $user ) {
4110  return $this->userTimeAndDate( $time, $user );
4111  }
4112  return $this->timeanddate( $time );
4113  }
4114  }
4115 
4123  public function segmentForDiff( $text ) {
4124  return $text;
4125  }
4126 
4133  public function unsegmentForDiff( $text ) {
4134  return $text;
4135  }
4136 
4143  public function getConverter() {
4144  return $this->mConverter;
4145  }
4146 
4155  public function autoConvert( $text, $variant = false ) {
4156  return $this->mConverter->autoConvert( $text, $variant );
4157  }
4158 
4165  public function autoConvertToAllVariants( $text ) {
4166  return $this->mConverter->autoConvertToAllVariants( $text );
4167  }
4168 
4180  public function convert( $text ) {
4181  return $this->mConverter->convert( $text );
4182  }
4183 
4190  public function convertTitle( $title ) {
4191  return $this->mConverter->convertTitle( $title );
4192  }
4193 
4202  public function convertNamespace( $ns, $variant = null ) {
4203  return $this->mConverter->convertNamespace( $ns, $variant );
4204  }
4205 
4211  public function hasVariants() {
4212  return count( $this->getVariants() ) > 1;
4213  }
4214 
4225  public function hasVariant( $variant ) {
4226  return $variant && ( $variant === $this->mConverter->validateVariant( $variant ) );
4227  }
4228 
4235  public function convertHtml( $text ) {
4236  return htmlspecialchars( $this->convert( $text ) );
4237  }
4238 
4243  public function convertCategoryKey( $key ) {
4244  return $this->mConverter->convertCategoryKey( $key );
4245  }
4246 
4253  public function getVariants() {
4254  return $this->mConverter->getVariants();
4255  }
4256 
4260  public function getPreferredVariant() {
4261  return $this->mConverter->getPreferredVariant();
4262  }
4263 
4267  public function getDefaultVariant() {
4268  return $this->mConverter->getDefaultVariant();
4269  }
4270 
4274  public function getURLVariant() {
4275  return $this->mConverter->getURLVariant();
4276  }
4277 
4290  public function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
4291  $this->mConverter->findVariantLink( $link, $nt, $ignoreOtherCond );
4292  }
4293 
4300  function getExtraHashOptions() {
4301  return $this->mConverter->getExtraHashOptions();
4302  }
4303 
4311  public function getParsedTitle() {
4312  return $this->mConverter->getParsedTitle();
4313  }
4314 
4321  public function updateConversionTable( Title $title ) {
4322  $this->mConverter->updateConversionTable( $title );
4323  }
4324 
4331  public function linkTrail() {
4332  return $this->localisationCache->getItem( $this->mCode, 'linkTrail' );
4333  }
4334 
4341  public function linkPrefixCharset() {
4342  return $this->localisationCache->getItem( $this->mCode, 'linkPrefixCharset' );
4343  }
4344 
4352  public function getParentLanguage() {
4353  if ( $this->mParentLanguage !== false ) {
4354  return $this->mParentLanguage;
4355  }
4356 
4357  $code = explode( '-', $this->getCode() )[0];
4358  if ( !in_array( $code, LanguageConverter::$languagesWithVariants ) ) {
4359  $this->mParentLanguage = null;
4360  return null;
4361  }
4362  $lang = self::factory( $code );
4363  if ( !$lang->hasVariant( $this->getCode() ) ) {
4364  $this->mParentLanguage = null;
4365  return null;
4366  }
4367 
4368  $this->mParentLanguage = $lang;
4369  return $lang;
4370  }
4371 
4379  public function equals( Language $lang ) {
4380  return $lang === $this || $lang->getCode() === $this->mCode;
4381  }
4382 
4391  public function getCode() {
4392  return $this->mCode;
4393  }
4394 
4405  public function getHtmlCode() {
4406  if ( is_null( $this->mHtmlCode ) ) {
4407  $this->mHtmlCode = LanguageCode::bcp47( $this->getCode() );
4408  }
4409  return $this->mHtmlCode;
4410  }
4411 
4419  public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
4420  $m = null;
4421  preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
4422  preg_quote( $suffix, '/' ) . '/', $filename, $m );
4423  if ( !count( $m ) ) {
4424  return false;
4425  }
4426  return str_replace( '_', '-', strtolower( $m[1] ) );
4427  }
4428 
4434  public static function classFromCode( $code, $fallback = true ) {
4435  if ( $fallback && $code == 'en' ) {
4436  return 'Language';
4437  } else {
4438  return 'Language' . str_replace( '-', '_', ucfirst( $code ) );
4439  }
4440  }
4441 
4450  public static function getFileName( $prefix, $code, $suffix = '.php' ) {
4451  if ( !self::isValidBuiltInCode( $code ) ) {
4452  throw new MWException( "Invalid language code \"$code\"" );
4453  }
4454 
4455  return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
4456  }
4457 
4462  public static function getMessagesFileName( $code ) {
4463  global $IP;
4464  $file = self::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
4465  Hooks::run( 'Language::getMessagesFileName', [ $code, &$file ] );
4466  return $file;
4467  }
4468 
4475  public static function getJsonMessagesFileName( $code ) {
4476  global $IP;
4477 
4478  if ( !self::isValidBuiltInCode( $code ) ) {
4479  throw new MWException( "Invalid language code \"$code\"" );
4480  }
4481 
4482  return "$IP/languages/i18n/$code.json";
4483  }
4484 
4492  public static function getFallbackFor( $code ) {
4493  $fallbacks = self::getFallbacksFor( $code );
4494  if ( $fallbacks ) {
4495  return $fallbacks[0];
4496  }
4497  return false;
4498  }
4499 
4510  public static function getFallbacksFor( $code, $mode = self::MESSAGES_FALLBACKS ) {
4511  if ( $code === 'en' || !self::isValidBuiltInCode( $code ) ) {
4512  return [];
4513  }
4514  switch ( $mode ) {
4515  case self::MESSAGES_FALLBACKS:
4516  // For unknown languages, fallbackSequence returns an empty array,
4517  // hardcode fallback to 'en' in that case as English messages are
4518  // always defined.
4519  return self::getLocalisationCache()->getItem( $code, 'fallbackSequence' ) ?: [ 'en' ];
4520  case self::STRICT_FALLBACKS:
4521  // Use this mode when you don't want to fallback to English unless
4522  // explicitly defined, for example when you have language-variant icons
4523  // and an international language-independent fallback.
4524  return self::getLocalisationCache()->getItem( $code, 'originalFallbackSequence' );
4525  default:
4526  throw new MWException( "Invalid fallback mode \"$mode\"" );
4527  }
4528  }
4529 
4538  public static function getFallbacksIncludingSiteLanguage( $code ) {
4539  global $wgLanguageCode;
4540 
4541  // Usually, we will only store a tiny number of fallback chains, so we
4542  // keep them in static memory.
4543  $cacheKey = "{$code}-{$wgLanguageCode}";
4544 
4545  if ( !array_key_exists( $cacheKey, self::$fallbackLanguageCache ) ) {
4546  $fallbacks = self::getFallbacksFor( $code );
4547 
4548  // Append the site's fallback chain, including the site language itself
4549  $siteFallbacks = self::getFallbacksFor( $wgLanguageCode );
4550  array_unshift( $siteFallbacks, $wgLanguageCode );
4551 
4552  // Eliminate any languages already included in the chain
4553  $siteFallbacks = array_diff( $siteFallbacks, $fallbacks );
4554 
4555  self::$fallbackLanguageCache[$cacheKey] = [ $fallbacks, $siteFallbacks ];
4556  }
4557  return self::$fallbackLanguageCache[$cacheKey];
4558  }
4559 
4569  public static function getMessagesFor( $code ) {
4570  return self::getLocalisationCache()->getItem( $code, 'messages' );
4571  }
4572 
4581  public static function getMessageFor( $key, $code ) {
4582  return self::getLocalisationCache()->getSubitem( $code, 'messages', $key );
4583  }
4584 
4593  public static function getMessageKeysFor( $code ) {
4594  return self::getLocalisationCache()->getSubitemList( $code, 'messages' );
4595  }
4596 
4601  function fixVariableInNamespace( $talk ) {
4602  if ( strpos( $talk, '$1' ) === false ) {
4603  return $talk;
4604  }
4605 
4606  global $wgMetaNamespace;
4607  $talk = str_replace( '$1', $wgMetaNamespace, $talk );
4608 
4609  # Allow grammar transformations
4610  # Allowing full message-style parsing would make simple requests
4611  # such as action=raw much more expensive than they need to be.
4612  # This will hopefully cover most cases.
4613  $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
4614  [ $this, 'replaceGrammarInNamespace' ], $talk );
4615  return str_replace( ' ', '_', $talk );
4616  }
4617 
4622  function replaceGrammarInNamespace( $m ) {
4623  return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
4624  }
4625 
4636  public function formatExpiry( $expiry, $format = true, $infinity = 'infinity' ) {
4637  static $dbInfinity;
4638  if ( $dbInfinity === null ) {
4639  $dbInfinity = wfGetDB( DB_REPLICA )->getInfinity();
4640  }
4641 
4642  if ( $expiry == '' || $expiry === 'infinity' || $expiry == $dbInfinity ) {
4643  return $format === true
4644  ? $this->getMessageFromDB( 'infiniteblock' )
4645  : $infinity;
4646  } else {
4647  return $format === true
4648  ? $this->timeanddate( $expiry, /* User preference timezone */ true )
4649  : wfTimestamp( $format, $expiry );
4650  }
4651  }
4652 
4667  function formatTimePeriod( $seconds, $format = [] ) {
4668  if ( !is_array( $format ) ) {
4669  $format = [ 'avoid' => $format ]; // For backwards compatibility
4670  }
4671  if ( !isset( $format['avoid'] ) ) {
4672  $format['avoid'] = false;
4673  }
4674  if ( !isset( $format['noabbrevs'] ) ) {
4675  $format['noabbrevs'] = false;
4676  }
4677  $secondsMsg = wfMessage(
4678  $format['noabbrevs'] ? 'seconds' : 'seconds-abbrev' )->inLanguage( $this );
4679  $minutesMsg = wfMessage(
4680  $format['noabbrevs'] ? 'minutes' : 'minutes-abbrev' )->inLanguage( $this );
4681  $hoursMsg = wfMessage(
4682  $format['noabbrevs'] ? 'hours' : 'hours-abbrev' )->inLanguage( $this );
4683  $daysMsg = wfMessage(
4684  $format['noabbrevs'] ? 'days' : 'days-abbrev' )->inLanguage( $this );
4685 
4686  if ( round( $seconds * 10 ) < 100 ) {
4687  $s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
4688  $s = $secondsMsg->params( $s )->text();
4689  } elseif ( round( $seconds ) < 60 ) {
4690  $s = $this->formatNum( round( $seconds ) );
4691  $s = $secondsMsg->params( $s )->text();
4692  } elseif ( round( $seconds ) < 3600 ) {
4693  $minutes = floor( $seconds / 60 );
4694  $secondsPart = round( fmod( $seconds, 60 ) );
4695  if ( $secondsPart == 60 ) {
4696  $secondsPart = 0;
4697  $minutes++;
4698  }
4699  $s = $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4700  $s .= ' ';
4701  $s .= $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4702  } elseif ( round( $seconds ) <= 2 * 86400 ) {
4703  $hours = floor( $seconds / 3600 );
4704  $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
4705  $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
4706  if ( $secondsPart == 60 ) {
4707  $secondsPart = 0;
4708  $minutes++;
4709  }
4710  if ( $minutes == 60 ) {
4711  $minutes = 0;
4712  $hours++;
4713  }
4714  $s = $hoursMsg->params( $this->formatNum( $hours ) )->text();
4715  $s .= ' ';
4716  $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4717  if ( !in_array( $format['avoid'], [ 'avoidseconds', 'avoidminutes', 'avoidhours' ] ) ) {
4718  $s .= ' ' . $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4719  }
4720  } else {
4721  $days = floor( $seconds / 86400 );
4722  if ( $format['avoid'] === 'avoidhours' ) {
4723  $hours = round( ( $seconds - $days * 86400 ) / 3600 );
4724  if ( $hours == 24 ) {
4725  $hours = 0;
4726  $days++;
4727  }
4728  $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4729  } elseif ( $format['avoid'] === 'avoidminutes' ) {
4730  $hours = round( ( $seconds - $days * 86400 ) / 3600 );
4731  if ( $hours == 24 ) {
4732  $hours = 0;
4733  $days++;
4734  }
4735  $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4736  $s .= ' ';
4737  $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4738  } elseif ( $format['avoid'] === 'avoidseconds' ) {
4739  $hours = floor( ( $seconds - $days * 86400 ) / 3600 );
4740  $minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
4741  if ( $minutes == 60 ) {
4742  $minutes = 0;
4743  $hours++;
4744  }
4745  if ( $hours == 24 ) {
4746  $hours = 0;
4747  $days++;
4748  }
4749  $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4750  $s .= ' ';
4751  $s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4752  $s .= ' ';
4753  $s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4754  } else {
4755  $s = $daysMsg->params( $this->formatNum( $days ) )->text();
4756  $s .= ' ';
4757  $s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
4758  }
4759  }
4760  return $s;
4761  }
4762 
4774  function formatBitrate( $bps ) {
4775  return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" );
4776  }
4777 
4784  function formatComputingNumbers( $size, $boundary, $messageKey ) {
4785  if ( $size <= 0 ) {
4786  return str_replace( '$1', $this->formatNum( $size ),
4787  $this->getMessageFromDB( str_replace( '$1', '', $messageKey ) )
4788  );
4789  }
4790  $sizes = [ '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zeta', 'yotta' ];
4791  $index = 0;
4792 
4793  $maxIndex = count( $sizes ) - 1;
4794  while ( $size >= $boundary && $index < $maxIndex ) {
4795  $index++;
4796  $size /= $boundary;
4797  }
4798 
4799  // For small sizes no decimal places necessary
4800  $round = 0;
4801  if ( $index > 1 ) {
4802  // For MB and bigger two decimal places are smarter
4803  $round = 2;
4804  }
4805  $msg = str_replace( '$1', $sizes[$index], $messageKey );
4806 
4807  $size = round( $size, $round );
4808  $text = $this->getMessageFromDB( $msg );
4809  return str_replace( '$1', $this->formatNum( $size ), $text );
4810  }
4811 
4822  function formatSize( $size ) {
4823  return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" );
4824  }
4825 
4835  function specialList( $page, $details, $oppositedm = true ) {
4836  if ( !$details ) {
4837  return $page;
4838  }
4839 
4840  $dirmark = ( $oppositedm ? $this->getDirMark( true ) : '' ) . $this->getDirMark();
4841  return $page .
4842  $dirmark .
4843  $this->msg( 'word-separator' )->escaped() .
4844  $this->msg( 'parentheses' )->rawParams( $details )->escaped();
4845  }
4846 
4859  public function viewPrevNext( Title $title, $offset, $limit,
4860  array $query = [], $atend = false
4861  ) {
4862  wfDeprecated( __METHOD__, '1.34' );
4863  // @todo FIXME: Why on earth this needs one message for the text and another one for tooltip?
4864 
4865  # Make 'previous' link
4866  $prev = wfMessage( 'prevn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4867  if ( $offset > 0 ) {
4868  $plink = $this->numLink( $title, max( $offset - $limit, 0 ), $limit,
4869  $query, $prev, 'prevn-title', 'mw-prevlink' );
4870  } else {
4871  $plink = htmlspecialchars( $prev );
4872  }
4873 
4874  # Make 'next' link
4875  $next = wfMessage( 'nextn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4876  if ( $atend ) {
4877  $nlink = htmlspecialchars( $next );
4878  } else {
4879  $nlink = $this->numLink( $title, $offset + $limit, $limit,
4880  $query, $next, 'nextn-title', 'mw-nextlink' );
4881  }
4882 
4883  # Make links to set number of items per page
4884  $numLinks = [];
4885  foreach ( [ 20, 50, 100, 250, 500 ] as $num ) {
4886  $numLinks[] = $this->numLink( $title, $offset, $num,
4887  $query, $this->formatNum( $num ), 'shown-title', 'mw-numlink' );
4888  }
4889 
4890  return wfMessage( 'viewprevnext' )->inLanguage( $this )->title( $title
4891  )->rawParams( $plink, $nlink, $this->pipeList( $numLinks ) )->escaped();
4892  }
4893 
4906  private function numLink( Title $title, $offset, $limit, array $query, $link,
4907  $tooltipMsg, $class
4908  ) {
4909  $query = [ 'limit' => $limit, 'offset' => $offset ] + $query;
4910  $tooltip = wfMessage( $tooltipMsg )->inLanguage( $this )->title( $title )
4911  ->numParams( $limit )->text();
4912 
4913  return Html::element( 'a', [ 'href' => $title->getLocalURL( $query ),
4914  'title' => $tooltip, 'class' => $class ], $link );
4915  }
4916 
4922  public function getConvRuleTitle() {
4923  return $this->mConverter->getConvRuleTitle();
4924  }
4925 
4931  public function getCompiledPluralRules() {
4932  $pluralRules =
4933  $this->localisationCache->getItem( strtolower( $this->mCode ), 'compiledPluralRules' );
4934  $fallbacks = self::getFallbacksFor( $this->mCode );
4935  if ( !$pluralRules ) {
4936  foreach ( $fallbacks as $fallbackCode ) {
4937  $pluralRules = $this->localisationCache
4938  ->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
4939  if ( $pluralRules ) {
4940  break;
4941  }
4942  }
4943  }
4944  return $pluralRules;
4945  }
4946 
4952  public function getPluralRules() {
4953  $pluralRules =
4954  $this->localisationCache->getItem( strtolower( $this->mCode ), 'pluralRules' );
4955  $fallbacks = self::getFallbacksFor( $this->mCode );
4956  if ( !$pluralRules ) {
4957  foreach ( $fallbacks as $fallbackCode ) {
4958  $pluralRules = $this->localisationCache
4959  ->getItem( strtolower( $fallbackCode ), 'pluralRules' );
4960  if ( $pluralRules ) {
4961  break;
4962  }
4963  }
4964  }
4965  return $pluralRules;
4966  }
4967 
4973  public function getPluralRuleTypes() {
4974  $pluralRuleTypes =
4975  $this->localisationCache->getItem( strtolower( $this->mCode ), 'pluralRuleTypes' );
4976  $fallbacks = self::getFallbacksFor( $this->mCode );
4977  if ( !$pluralRuleTypes ) {
4978  foreach ( $fallbacks as $fallbackCode ) {
4979  $pluralRuleTypes = $this->localisationCache
4980  ->getItem( strtolower( $fallbackCode ), 'pluralRuleTypes' );
4981  if ( $pluralRuleTypes ) {
4982  break;
4983  }
4984  }
4985  }
4986  return $pluralRuleTypes;
4987  }
4988 
4994  public function getPluralRuleIndexNumber( $number ) {
4995  $pluralRules = $this->getCompiledPluralRules();
4996  $form = Evaluator::evaluateCompiled( $number, $pluralRules );
4997  return $form;
4998  }
4999 
5008  public function getPluralRuleType( $number ) {
5009  $index = $this->getPluralRuleIndexNumber( $number );
5010  $pluralRuleTypes = $this->getPluralRuleTypes();
5011  return $pluralRuleTypes[$index] ?? 'other';
5012  }
5013 }
static getSuggestedDurations(Language $lang=null, $includeOther=true)
Get an array of suggested block durations from MediaWiki:Ipboptions.
static newFromCode( $code, $fallback=false)
Create a language object for a given language code.
Definition: Language.php:239
getMessage( $key)
Definition: Language.php:2644
getPluralRuleType( $number)
Find the plural rule type appropriate for the given number For example, if the language is set to Ara...
Definition: Language.php:5008
getIranianCalendarMonthName( $key)
Definition: Language.php:1027
static $mMonthAbbrevMsgs
Definition: Language.php:114
$mParentLanguage
Definition: Language.php:64
getSpecialPageAliases()
Get special page names, as an associative array canonical name => array of valid names, including aliases.
Definition: Language.php:3241
getPluralRuleTypes()
Get the plural rule types for the language.
Definition: Language.php:4973
firstChar( $s)
Get the first character of a string.
Definition: Language.php:2981
getHumanTimestamp(MWTimestamp $time, MWTimestamp $relativeTo=null, User $user=null)
Get the timestamp in a human-friendly relative format, e.g., "3 days ago".
Definition: Language.php:2538
static getLocalisationCache()
Get the LocalisationCache instance.
Definition: Language.php:447
Wrapper around strtr() that holds replacements.
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
ucwords( $str)
Definition: Language.php:2806
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
static hebrewNumeral( $num)
Hebrew Gematria number formatting up to 9999.
Definition: Language.php:2067
translateBlockExpiry( $str, User $user=null, $now=0)
Definition: Language.php:4080
return true to allow those checks to and false if checking is done remove or add to the links of a group of changes in EnhancedChangesList Hook subscribers can return false to omit this line from recentchanges use this to change the tables headers change it to an object instance and return false override the list derivative used $groups Array of ChangesListFilterGroup objects(added in 1.34) 'FileDeleteComplete' 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:1535
ucwordbreaksCallbackAscii( $matches)
Definition: Language.php:2678
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:231
segmentByWord( $string)
Some languages such as Chinese require word segmentation, Specify such segmentation when overridden i...
Definition: Language.php:2916
normalize( $s)
Convert a UTF-8 string to normal form C.
Definition: Language.php:3044
handleExplicitPluralForms( $count, array $forms)
Handles explicit plural forms for Language::convertPlural()
Definition: Language.php:4012
static getTitleInvalidRegex()
Returns a simple regex that will match on characters and sequences invalid in titles.
getDurationIntervals( $seconds, array $chosenIntervals=[])
Takes a number of seconds and returns an array with a set of corresponding intervals.
Definition: Language.php:2383
getVariantname( $code, $usemsg=true)
short names for language variants used for language conversion links.
Definition: Language.php:759
setNamespaces(array $namespaces)
Arbitrarily set all of the namespace names at once.
Definition: Language.php:541
static HashBagOStuff null $languageNameCache
Cache for language names.
Definition: Language.php:183
removeBadCharLast( $string)
Remove bytes that represent an incomplete Unicode character at the end of string (e.g.
Definition: Language.php:3617
static romanNumeral( $num)
Roman number formatting up to 10000.
Definition: Language.php:2036
getWeekdayAbbreviation( $key)
Definition: Language.php:1019
getDirMark( $opposite=false)
A hidden direction mark (LRM or RLM), depending on the language direction.
Definition: Language.php:3147
getHtmlCode()
Get the code in BCP 47 format which we can use inside of html lang="" tags.
Definition: Language.php:4405
static isUtf8( $value)
Test whether a string is valid UTF-8.
Definition: StringUtils.php:41
truncate_skip(&$ret, $text, $search, $start, $len=null)
truncateHtml() helper function like strcspn() but adds the skipped chars to $ret
Definition: Language.php:3788
$IP
Definition: WebStart.php:41
equals(Language $lang)
Compare with an other language object.
Definition: Language.php:4379
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:1983
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
lcfirst( $str)
Definition: Language.php:2763
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Definition: router.php:42
static $IRANIAN_DAYS
Definition: Language.php:1611
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException' returning false will NOT prevent logging $e
Definition: hooks.txt:2158
offsetForUser(User $user)
Adjust the timestamp depending on the given user&#39;s preferences.
Definition: MWTimestamp.php:79
getHebrewCalendarMonthNameGen( $key)
Definition: Language.php:1043
alignEnd()
Return &#39;right&#39; or &#39;left&#39; as appropriate alignment for line-end for this language&#39;s text direction...
Definition: Language.php:3115
static insertSpace( $string, $pattern)
Definition: Language.php:2959
getFallbackLanguages()
Definition: Language.php:482
userTimeAndDate( $ts, User $user, array $options=[])
Get the formatted date and time for the given timestamp and formatted for the given user...
Definition: Language.php:2519
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
getMonthNameGen( $key)
Definition: Language.php:984
$wgLangObjCacheSize
Language cache size, or really how many languages can we handle simultaneously without degrading to c...
if(!isset( $args[0])) $lang
$namespaceAliases
Definition: Language.php:71
hasVariants()
Check if this is a language with variants.
Definition: Language.php:4211
getOption( $oname, $defaultOverride=null, $ignoreHidden=false)
Get the user&#39;s current setting for a given option.
Definition: User.php:2968
getCompiledPluralRules()
Get the compiled plural rules for the language.
Definition: Language.php:4931
getLocalNsIndex( $text)
Get a namespace key by value, case insensitive.
Definition: Language.php:650
convertCategoryKey( $key)
Definition: Language.php:4243
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
getLocalURL( $query='', $query2=false)
Get a URL with no fragment or server name (relative URL) from a Title object.
Definition: Title.php:2043
truncateForDatabase( $string, $length, $ellipsis='...', $adjustLength=true)
Truncate a string to a specified length in bytes, appending an optional string (e.g.
Definition: Language.php:3514
getHebrewCalendarMonthName( $key)
Definition: Language.php:1035
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
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
$value
const ALL
Return all known languages in fetchLanguageName(s).
Definition: Language.php:48
$wgMetaNamespace
Name of the project namespace.
fallback8bitEncoding()
Definition: Language.php:2893
wfIsInfinity( $str)
Determine input string is represents as infinity.
unsegmentForDiff( $text)
and unsegment to show the result
Definition: Language.php:4133
truncate_endBracket(&$tag, $tagType, $lastCh, &$openTags)
truncateHtml() helper function (a) push or pop $tag from $openTags as needed (b) clear $tag value ...
Definition: Language.php:3811
A fake language variant converter.
linkTrail()
A regular expression to match legal word-trailing characters which should be merged onto a link of th...
Definition: Language.php:4331
getAllMessages()
Definition: Language.php:2651
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 MediaWikiServices
Definition: injection.txt:23
$wgNamespaceAliases
Namespace aliases.
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
getPluralRuleIndexNumber( $number)
Find the index number of the plural rule appropriate for the given number.
Definition: Language.php:4994
getVariants()
Get the list of variants supported by this language see sample implementation in LanguageZh.php.
Definition: Language.php:4253
A helper class for throttling authentication attempts.
$dateFormatStrings
Definition: Language.php:66
static hebrewYearStart( $year)
This calculates the Hebrew year start, as days since 1 September.
Definition: Language.php:1875
getNamespaceAliases()
Definition: Language.php:659
getWeekdayName( $key)
Definition: Language.php:1011
minimumGroupingDigits()
Definition: Language.php:3431
static $mMonthMsgs
Definition: Language.php:104
convertForSearchResult( $termsArray)
Definition: Language.php:2969
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition: hooks.txt:1792
$wgExtraGenderNamespaces
Same as above, but for namespaces with gender distinction.
static getFileName( $prefix, $code, $suffix='.php')
Get the name of a file for a certain language code.
Definition: Language.php:4450
convertTitle( $title)
Convert a Title object to a string in the preferred variant.
Definition: Language.php:4190
static $mLangObjCache
Definition: Language.php:81
static classFromCode( $code, $fallback=true)
Definition: Language.php:4434
internalUserTimeAndDate( $type, $ts, User $user, array $options)
Internal helper function for userDate(), userTime() and userTimeAndDate()
Definition: Language.php:2434
array $wgOverrideUcfirstCharacters
List of Unicode characters for which capitalization is overridden in Language::ucfirst.
$wgTranslateNumerals
For Hindi and Arabic use local numerals instead of Western style (0-9) numerals in interface...
getHijriCalendarMonthName( $key)
Definition: Language.php:1051
static $mWeekdayAbbrevMsgs
Definition: Language.php:100
getMonthAbbreviation( $key)
Definition: Language.php:992
lc( $str, $first=false)
Definition: Language.php:2782
ucwordbreaksCallbackMB( $matches)
Definition: Language.php:2686
$mMagicExtensions
Definition: Language.php:63
LocalisationCache $localisationCache
Definition: Language.php:79
static $pdf
Definition: Language.php:190
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:411
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition: hooks.txt:3050
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:51
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition: hooks.txt:773
__construct()
Definition: Language.php:451
truncateHtml( $text, $length, $ellipsis='...')
Truncate a string of valid HTML to a specified length in bytes, appending an optional string (e...
Definition: Language.php:3669
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:1983
ucfirst( $str)
Make a string&#39;s first character uppercase.
Definition: Language.php:2705
$wgLanguageCode
Site language code.
semicolonList(array $list)
Take a list of strings and build a locale-friendly semicolon-separated list, using the local semicolo...
Definition: Language.php:3480
userAdjust( $ts, $tz=false)
Used by date() and time() to adjust the time output.
Definition: Language.php:2154
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
static getMessageFor( $key, $code)
Get a message for a given language.
Definition: Language.php:4581
static $lre
Unicode directional formatting characters, for embedBidi()
Definition: Language.php:188
formatNum( $number, $nocommafy=false)
Normally we output all numbers in plain en_US style, that is 293,291.235 for twohundredninetythreetho...
Definition: Language.php:3284
static getFallbackFor( $code)
Get the first fallback for a given language.
Definition: Language.php:4492
timeanddate( $ts, $adj=false, $format=true, $timecorrection=false)
Definition: Language.php:2338
formatExpiry( $expiry, $format=true, $infinity='infinity')
Decode an expiry (block, protection, etc) which has come from the DB.
Definition: Language.php:4636
listToText(array $list)
Take a list of strings and build a locale-friendly comma-separated list, using the local comma-separa...
Definition: Language.php:3443
static tsToYear( $ts, $cName)
Algorithm to convert Gregorian dates to Thai solar dates, Minguo dates or Minguo dates.
Definition: Language.php:1913
const NS_PROJECT
Definition: Defines.php:64
static $mHijriCalendarMonthMsgs
Definition: Language.php:142
linkPrefixCharset()
A regular expression character set to match legal word-prefixing characters which should be merged on...
Definition: Language.php:4341
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt
static getMain()
Get the RequestContext object associated with the main request.
getPreferredVariant()
Definition: Language.php:4260
LanguageConverter $mConverter
Definition: Language.php:60
static getMessagesFor( $code)
Get all messages for a given language WARNING: this may take a long time.
Definition: Language.php:4569
$minimumGroupingDigits
removeBadCharFirst( $string)
Remove bytes that represent an incomplete Unicode character at the start of string (e...
Definition: Language.php:3643
getDirMarkEntity( $opposite=false)
A hidden direction mark (LRM or RLM), depending on the language direction.
Definition: Language.php:3130
__destruct()
Reduce memory usage.
Definition: Language.php:465
static strongDirFromContent( $text='')
Gets directionality of the first strongly directional codepoint, for embedBidi()
Definition: Language.php:2019
getMagic( $mw)
Fill a MagicWord object with data from here.
Definition: Language.php:3209
static getMessageKeysFor( $code)
Get all message keys for a given language.
Definition: Language.php:4593
static getFallbacksIncludingSiteLanguage( $code)
Get the ordered list of fallback languages, ending with the fallback language chain for the site lang...
Definition: Language.php:4538
getDefaultVariant()
Definition: Language.php:4267
getNsText( $index)
Get a namespace value by key.
Definition: Language.php:580
const STRICT_FALLBACKS
Return a strict fallback chain in getFallbacksFor.
Definition: Language.php:93
viewPrevNext(Title $title, $offset, $limit, array $query=[], $atend=false)
Generate (prev x| next x) (20|50|100...) type links for paging.
Definition: Language.php:4859
getConverter()
Return the LanguageConverter used in the Language.
Definition: Language.php:4143
static $mIranianCalendarMonthMsgs
Definition: Language.php:119
const NS_PROJECT_TALK
Definition: Defines.php:65
autoConvertToAllVariants( $text)
convert text to all supported variants
Definition: Language.php:4165
getBookstoreList()
Exports $wgBookstoreListEn.
Definition: Language.php:490
capitalizeAllNouns()
Definition: Language.php:3159
static decode( $value, $assoc=false)
Decodes a JSON string.
Definition: FormatJson.php:174
$wgDummyLanguageCodes
Functionally the same as $wgExtraLanguageCodes, but deprecated.
emphasize( $text)
Italic is unsuitable for some languages.
Definition: Language.php:3258
date( $ts, $adj=false, $format=true, $timecorrection=false)
Definition: Language.php:2299
isMultibyte( $str)
Definition: Language.php:2798
segmentForDiff( $text)
languages like Chinese need to be segmented in order for the diff to be of any use ...
Definition: Language.php:4123
$wgAllUnicodeFixes
Set this to always convert certain Unicode sequences to modern ones regardless of the content languag...
$cache
Definition: mcc.php:33
normalizeForSearch( $string)
Some languages have special punctuation need to be normalized.
Definition: Language.php:2927
static $strongDirRegex
Directionality test regex for embedBidi().
Definition: Language.php:205
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:1983
static tsToHijri( $ts)
Converting Gregorian dates to Hijri dates.
Definition: Language.php:1685
needsGenderDistinction()
Whether this language uses gender-dependent namespace aliases.
Definition: Language.php:626
getURLVariant()
Definition: Language.php:4274
caseFold( $s)
Return a case-folded representation of $s.
Definition: Language.php:2870
updateConversionTable(Title $title)
Refresh the cache of conversion tables when MediaWiki:Conversiontable* is updated.
Definition: Language.php:4321
mbUpperChar( $char)
Convert character to uppercase, allowing overrides of the default mb_upper behaviour, which is buggy in many ways.
Definition: Language.php:2750
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:773
static $rle
Definition: Language.php:189
$wgMetaNamespaceTalk
Name of the project talk namespace.
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:918
static tsToIranian( $ts)
Algorithm by Roozbeh Pournader and Mohammad Toossi to convert Gregorian dates to Iranian dates...
Definition: Language.php:1625
getNamespaceIds()
Definition: Language.php:711
static factory( $code)
Get a cached or new language object for a given language code.
Definition: Language.php:214
dateFormat( $usePrefs=true)
This is meant to be used by time(), date(), and timeanddate() to get the date preference they&#39;re supp...
Definition: Language.php:2232
static $mHebrewCalendarMonthMsgs
Definition: Language.php:126
pipeList(array $list)
Same as commaList, but separate it with the pipe instead.
Definition: Language.php:3492
getGenderNsText( $index, $gender)
Returns gender-dependent namespace alias if available.
Definition: Language.php:611
getFormattedNsText( $index)
A convenience function that returns the same thing as getNsText() except with &#39;_&#39; changed to &#39; &#39;...
Definition: Language.php:598
$mExtendedSpecialPageAliases
Definition: Language.php:67
namespace and then decline to actually register it & $namespaces
Definition: hooks.txt:918
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable & $code
Definition: hooks.txt:773
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
Definition: distributors.txt:9
getPluralRules()
Get the plural rules for the language.
Definition: Language.php:4952
getDefaultDateFormat()
Definition: Language.php:790
embedBidi( $text='')
Wraps argument with unicode control characters for directionality safety.
Definition: Language.php:4053
resetNamespaces()
Resets all of the namespace caches.
Definition: Language.php:549
getDateFormatString( $type, $pref)
Get a format string for a given type and preference.
Definition: Language.php:2263
static MapCacheLRU null $grammarTransformations
Cache for grammar rules data.
Definition: Language.php:177
time( $ts, $adj=false, $format=true, $timecorrection=false)
Definition: Language.php:2318
getMagicWords()
Get all magic words from cache.
Definition: Language.php:3200
getGrammarTransformations()
Get the grammar transformations data for the language.
Definition: Language.php:3904
getMonthNamesArray()
Definition: Language.php:972
numLink(Title $title, $offset, $limit, array $query, $link, $tooltipMsg, $class)
Helper function for viewPrevNext() that generates links.
Definition: Language.php:4906
static getMessagesFileName( $code)
Definition: Language.php:4462
static getDefaultOption( $opt)
Get a given default option value.
Definition: User.php:1733
convertGrammar( $word, $case)
Grammatical transformations, needed for inflected languages Invoked by putting {{grammar:case|word}} ...
Definition: Language.php:3833
digitTransformTable()
Definition: Language.php:3417
getConvRuleTitle()
Get the conversion rule title, if any.
Definition: Language.php:4922
formatTimePeriod( $seconds, $format=[])
Formats a time given in seconds into a string representation of that time.
Definition: Language.php:4667
userDate( $ts, User $user, array $options=[])
Get the formatted date for the given timestamp and formatted for the given user.
Definition: Language.php:2473
getNsIndex( $text)
Get a namespace key by value, case insensitive.
Definition: Language.php:741
addMagicWordsByLang( $newWords)
Add magic words to the extension array.
Definition: Language.php:3226
autoConvert( $text, $variant=false)
convert text to a variant
Definition: Language.php:4155
static fetchLanguageName( $code, $inLanguage=self::AS_AUTONYMS, $include=self::ALL)
Definition: Language.php:931
static getJsonMessagesFileName( $code)
Definition: Language.php:4475
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:332
ucwordbreaks( $str)
capitalize words at word breaks
Definition: Language.php:2830
$wgExtraNamespaces
Additional namespaces.
static convertDoubleWidth( $string)
convert double-width roman characters to single-width.
Definition: Language.php:2939
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:35
getHumanTimestampInternal(MWTimestamp $ts, MWTimestamp $relativeTo, User $user)
Convert an MWTimestamp into a pretty human-readable timestamp using the given user preferences and re...
Definition: Language.php:2575
getCode()
Get the internal language code for this language object.
Definition: Language.php:4391
digitGroupingPattern()
Definition: Language.php:3410
specialList( $page, $details, $oppositedm=true)
Make a list item, used by various special pages.
Definition: Language.php:4835
static isKnownLanguageTag( $tag)
Returns true if a language code is an IETF tag known to MediaWiki.
Definition: Language.php:425
Using a hook running we can avoid having all this option specific stuff in our mainline code Using the function We ve cleaned up the code here by removing clumps of infrequently used code and moving them off somewhere else It s much easier for someone working with this code to see what s _really_ going and make changes or fix bugs In we can take all the code that deals with the little used title reversing options(say) and put it in one place. Instead of having little title-reversing if-blocks spread all over the codebase in showAnArticle
$fallback
Definition: MessagesAb.php:11
$digitGroupingPattern
Definition: MessagesAs.php:167
static isSupportedLanguage( $code)
Checks whether any localisation is available for that language tag in MediaWiki (MessagesXx.php exists).
Definition: Language.php:304
convertNamespace( $ns, $variant=null)
Convert a namespace index to a string in the preferred variant.
Definition: Language.php:4202
getArrow( $direction='forwards')
An arrow, depending on the language direction.
Definition: Language.php:3170
formatSize( $size)
Format a size in bytes for output, using an appropriate unit (B, KB, MB, GB, TB, PB, EB, ZB or YB) according to the magnitude in question.
Definition: Language.php:4822
convertHtml( $text)
Perform output conversion on a string, and encode for safe HTML output.
Definition: Language.php:4235
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
array null $namespaceNames
Definition: Language.php:70
commafy( $number)
Adds commas to a given number.
Definition: Language.php:3345
$transformData
ReplacementArray object caches.
Definition: Language.php:76
static getCodeFromFileName( $filename, $prefix='Language', $suffix='.php')
Get the language code from a file name.
Definition: Language.php:4419
getExtraHashOptions()
returns language specific options used by User::getPageRenderHash() for example, the preferred langua...
Definition: Language.php:4300
static getFallbacksFor( $code, $mode=self::MESSAGES_FALLBACKS)
Get the ordered list of fallback languages.
Definition: Language.php:4510
getParsedTitle()
For languages that support multiple variants, the title of an article may be displayed differently in...
Definition: Language.php:4311
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
static tsToHebrew( $ts)
Converting Gregorian dates to Hebrew dates.
Definition: Language.php:1737
preConvertPlural($forms, $count)
Checks that convertPlural was given an array and pads it to requested amount of forms by copying the ...
Definition: Language.php:4033
findVariantLink(&$link, &$nt, $ignoreOtherCond=false)
If a language supports multiple variants, it is possible that non-existing link in one variant actual...
Definition: Language.php:4290
$wgGrammarForms
Some languages need different word forms, usually for different cases.
const SUPPORTED
Return in fetchLanguageName(s) only the languages for which we have at least some localisation...
Definition: Language.php:55
hasVariant( $variant)
Strict check if the language has the specific variant.
Definition: Language.php:4225
formatBitrate( $bps)
Format a bitrate for output, using an appropriate unit (bps, kbps, Mbps, Gbps, Tbps, Pbps, Ebps, Zbps or Ybps) according to the magnitude in question.
Definition: Language.php:4774
const AS_AUTONYMS
Return autonyms in fetchLanguageName(s).
Definition: Language.php:42
transformUsingPairFile( $file, $string)
Transform a string using serialized data stored in the given file (which must be in the serialized su...
Definition: Language.php:3069
separatorTransformTable()
Definition: Language.php:3424
isRTL()
For right-to-left language support.
Definition: Language.php:3083
getDatePreference()
Get the user&#39;s preferred date format.
Definition: User.php:3291
sprintfDate( $format, $ts, DateTimeZone $zone=null, &$ttl='unused')
This is a workalike of PHP&#39;s date() function, but with better internationalisation, a reduced set of format characters, and a better escaping format.
Definition: Language.php:1141
getParentLanguage()
Get the "parent" language which has a converter to convert a "compatible" language (in another varian...
Definition: Language.php:4352
static clearCaches()
Intended for tests that may change configuration in a way that invalidates caches.
Definition: Language.php:283
getDir()
Return the correct HTML &#39;dir&#39; attribute value for this language.
Definition: Language.php:3091
getFormattedNamespaces()
A convenience function that returns getNamespaces() with spaces instead of underscores in values...
Definition: Language.php:561
$wgLocalTZoffset
Set an offset from UTC in minutes to use for the default timezone setting for anonymous users and new...
iconv( $in, $out, $string)
Definition: Language.php:2661
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:277
static $mMonthGenMsgs
Definition: Language.php:109
static $mWeekdayMsgs
Definition: Language.php:95
linkPrefixExtension()
To allow "foo[[bar]]" to extend the link over the whole word "foobar".
Definition: Language.php:3192
msg( $msg)
Get message object in this language.
Definition: Language.php:957
const MESSAGES_FALLBACKS
Return a fallback chain for messages in getFallbacksFor.
Definition: Language.php:87
const DB_REPLICA
Definition: defines.php:25
$mNamespaceIds
Definition: Language.php:71
uc( $str, $first=false)
Convert a string to uppercase.
Definition: Language.php:2725
getGrammarForms()
Get the grammar forms for the content language.
Definition: Language.php:3884
fixVariableInNamespace( $talk)
Definition: Language.php:4601
static dateTimeObjFormat(&$dateTimeObj, $ts, $zone, $code)
Pass through result from $dateTimeObj->format()
Definition: Language.php:1063
=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
formatComputingNumbers( $size, $boundary, $messageKey)
Definition: Language.php:4784
initContLang()
Hook which will be called if this is the content language.
Definition: Language.php:475
userTime( $ts, User $user, array $options=[])
Get the formatted time for the given timestamp and formatted for the given user.
Definition: Language.php:2496
ucwordsCallbackMB( $matches)
Definition: Language.php:2694
getNamespaces()
Returns an array of localised namespaces indexed by their numbers.
Definition: Language.php:500
static isValidCode( $code)
Returns true if a language code string is of a valid form, whether or not it exists.
Definition: Language.php:386
commaList(array $list)
Take a list of strings and build a locale-friendly comma-separated list, using the local comma-separa...
Definition: Language.php:3467
static $mHebrewCalendarMonthGenMsgs
Definition: Language.php:134
const NS_USER_TALK
Definition: Defines.php:63
static array $languagesWithVariants
languages supporting variants
truncateForVisual( $string, $length, $ellipsis='...', $adjustLength=true)
Truncate a string to a specified number of characters, appending an optional string (e...
Definition: Language.php:3538
gender( $gender, $forms)
Provides an alternative text depending on specified gender.
Definition: Language.php:3953
getMessageFromDB( $msg)
Get a message from the MediaWiki namespace.
Definition: Language.php:947
getMonthAbbreviationsArray()
Definition: Language.php:999
hasWordBreaks()
Most writing systems use whitespace to break up words.
Definition: Language.php:2905
convert( $text)
convert text to different variants of a language.
Definition: Language.php:4180
alignStart()
Return &#39;left&#39; or &#39;right&#39; as appropriate alignment for line-start for this language&#39;s text direction...
Definition: Language.php:3103
checkTitleEncoding( $s)
TODO: $s is not always a string per T218883.
Definition: Language.php:2879
static bcp47( $code)
Get the normalised IETF language tag See unit test for examples.
convertPlural( $count, $forms)
Plural form transformations, needed for some languages.
Definition: Language.php:3982
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1460
formatNumNoSeparators( $number)
Front-end for non-commafied formatNum.
Definition: Language.php:3312
parseFormattedNumber( $number)
Definition: Language.php:3320
truncateInternal( $string, $length, $ellipsis, $adjustLength, callable $measureLength, callable $getSubstring)
Internal method used for truncation.
Definition: Language.php:3564
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
replaceGrammarInNamespace( $m)
Definition: Language.php:4622
static $GREG_DAYS
Definition: Language.php:1610
getMonthName( $key)
Definition: Language.php:965
formatDuration( $seconds, array $chosenIntervals=[])
Takes a number of seconds and turns it into a text using values such as hours and minutes...
Definition: Language.php:2357
$matches