Completed
Branch master (bd473b)
by
unknown
28:50
created

Language::listToText()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 22
Code Lines 16

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 22
rs 8.6737
cc 6
eloc 16
nc 10
nop 1
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 41 and the first side effect is on line 29.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
/**
3
 * Internationalisation code.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup Language
22
 */
23
24
/**
25
 * @defgroup Language Language
26
 */
27
28
if ( !defined( 'MEDIAWIKI' ) ) {
29
	echo "This file is part of MediaWiki, it is not a valid entry point.\n";
30
	exit( 1 );
31
}
32
33
mb_internal_encoding( 'UTF-8' );
34
35
use CLDRPluralRuleParser\Evaluator;
36
37
/**
38
 * Internationalisation code
39
 * @ingroup Language
40
 */
41
class Language {
42
	/**
43
	 * @var LanguageConverter
44
	 */
45
	public $mConverter;
46
47
	public $mVariants, $mCode, $mLoaded = false;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
48
	public $mMagicExtensions = [], $mMagicHookDone = false;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
49
	private $mHtmlCode = null, $mParentLanguage = false;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
50
51
	public $dateFormatStrings = [];
52
	public $mExtendedSpecialPageAliases;
53
54
	protected $namespaceNames, $mNamespaceIds, $namespaceAliases;
0 ignored issues
show
Coding Style introduced by
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
55
56
	/**
57
	 * ReplacementArray object caches
58
	 */
59
	public $transformData = [];
60
61
	/**
62
	 * @var LocalisationCache
63
	 */
64
	static public $dataCache;
65
66
	static public $mLangObjCache = [];
67
68
	static public $mWeekdayMsgs = [
69
		'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
70
		'friday', 'saturday'
71
	];
72
73
	static public $mWeekdayAbbrevMsgs = [
74
		'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
75
	];
76
77
	static public $mMonthMsgs = [
78
		'january', 'february', 'march', 'april', 'may_long', 'june',
79
		'july', 'august', 'september', 'october', 'november',
80
		'december'
81
	];
82
	static public $mMonthGenMsgs = [
83
		'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
84
		'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
85
		'december-gen'
86
	];
87
	static public $mMonthAbbrevMsgs = [
88
		'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
89
		'sep', 'oct', 'nov', 'dec'
90
	];
91
92
	static public $mIranianCalendarMonthMsgs = [
93
		'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
94
		'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
95
		'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
96
		'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
97
	];
98
99
	static public $mHebrewCalendarMonthMsgs = [
100
		'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
101
		'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
102
		'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
103
		'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
104
		'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
105
	];
106
107
	static public $mHebrewCalendarMonthGenMsgs = [
108
		'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
109
		'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
110
		'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
111
		'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
112
		'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
113
	];
114
115
	static public $mHijriCalendarMonthMsgs = [
116
		'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
117
		'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
118
		'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
119
		'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
120
	];
121
122
	/**
123
	 * @since 1.20
124
	 * @var array
125
	 */
126
	static public $durationIntervals = [
127
		'millennia' => 31556952000,
128
		'centuries' => 3155695200,
129
		'decades' => 315569520,
130
		'years' => 31556952, // 86400 * ( 365 + ( 24 * 3 + 25 ) / 400 )
131
		'weeks' => 604800,
132
		'days' => 86400,
133
		'hours' => 3600,
134
		'minutes' => 60,
135
		'seconds' => 1,
136
	];
137
138
	/**
139
	 * Cache for language fallbacks.
140
	 * @see Language::getFallbacksIncludingSiteLanguage
141
	 * @since 1.21
142
	 * @var array
143
	 */
144
	static private $fallbackLanguageCache = [];
145
146
	/**
147
	 * Cache for language names
148
	 * @var HashBagOStuff|null
149
	 */
150
	static private $languageNameCache;
151
152
	/**
153
	 * Unicode directional formatting characters, for embedBidi()
154
	 */
155
	static private $lre = "\xE2\x80\xAA"; // U+202A LEFT-TO-RIGHT EMBEDDING
156
	static private $rle = "\xE2\x80\xAB"; // U+202B RIGHT-TO-LEFT EMBEDDING
157
	static private $pdf = "\xE2\x80\xAC"; // U+202C POP DIRECTIONAL FORMATTING
158
159
	/**
160
	 * Directionality test regex for embedBidi(). Matches the first strong directionality codepoint:
161
	 * - in group 1 if it is LTR
162
	 * - in group 2 if it is RTL
163
	 * Does not match if there is no strong directionality codepoint.
164
	 *
165
	 * The form is '/(?:([strong ltr codepoint])|([strong rtl codepoint]))/u' .
166
	 *
167
	 * Generated by UnicodeJS (see tools/strongDir) from the UCD; see
168
	 * https://git.wikimedia.org/summary/unicodejs.git .
169
	 */
170
	// @codingStandardsIgnoreStart
171
	// @codeCoverageIgnoreStart
172
	static private $strongDirRegex = '/(?:([\x{41}-\x{5a}\x{61}-\x{7a}\x{aa}\x{b5}\x{ba}\x{c0}-\x{d6}\x{d8}-\x{f6}\x{f8}-\x{2b8}\x{2bb}-\x{2c1}\x{2d0}\x{2d1}\x{2e0}-\x{2e4}\x{2ee}\x{370}-\x{373}\x{376}\x{377}\x{37a}-\x{37d}\x{37f}\x{386}\x{388}-\x{38a}\x{38c}\x{38e}-\x{3a1}\x{3a3}-\x{3f5}\x{3f7}-\x{482}\x{48a}-\x{52f}\x{531}-\x{556}\x{559}-\x{55f}\x{561}-\x{587}\x{589}\x{903}-\x{939}\x{93b}\x{93d}-\x{940}\x{949}-\x{94c}\x{94e}-\x{950}\x{958}-\x{961}\x{964}-\x{980}\x{982}\x{983}\x{985}-\x{98c}\x{98f}\x{990}\x{993}-\x{9a8}\x{9aa}-\x{9b0}\x{9b2}\x{9b6}-\x{9b9}\x{9bd}-\x{9c0}\x{9c7}\x{9c8}\x{9cb}\x{9cc}\x{9ce}\x{9d7}\x{9dc}\x{9dd}\x{9df}-\x{9e1}\x{9e6}-\x{9f1}\x{9f4}-\x{9fa}\x{a03}\x{a05}-\x{a0a}\x{a0f}\x{a10}\x{a13}-\x{a28}\x{a2a}-\x{a30}\x{a32}\x{a33}\x{a35}\x{a36}\x{a38}\x{a39}\x{a3e}-\x{a40}\x{a59}-\x{a5c}\x{a5e}\x{a66}-\x{a6f}\x{a72}-\x{a74}\x{a83}\x{a85}-\x{a8d}\x{a8f}-\x{a91}\x{a93}-\x{aa8}\x{aaa}-\x{ab0}\x{ab2}\x{ab3}\x{ab5}-\x{ab9}\x{abd}-\x{ac0}\x{ac9}\x{acb}\x{acc}\x{ad0}\x{ae0}\x{ae1}\x{ae6}-\x{af0}\x{af9}\x{b02}\x{b03}\x{b05}-\x{b0c}\x{b0f}\x{b10}\x{b13}-\x{b28}\x{b2a}-\x{b30}\x{b32}\x{b33}\x{b35}-\x{b39}\x{b3d}\x{b3e}\x{b40}\x{b47}\x{b48}\x{b4b}\x{b4c}\x{b57}\x{b5c}\x{b5d}\x{b5f}-\x{b61}\x{b66}-\x{b77}\x{b83}\x{b85}-\x{b8a}\x{b8e}-\x{b90}\x{b92}-\x{b95}\x{b99}\x{b9a}\x{b9c}\x{b9e}\x{b9f}\x{ba3}\x{ba4}\x{ba8}-\x{baa}\x{bae}-\x{bb9}\x{bbe}\x{bbf}\x{bc1}\x{bc2}\x{bc6}-\x{bc8}\x{bca}-\x{bcc}\x{bd0}\x{bd7}\x{be6}-\x{bf2}\x{c01}-\x{c03}\x{c05}-\x{c0c}\x{c0e}-\x{c10}\x{c12}-\x{c28}\x{c2a}-\x{c39}\x{c3d}\x{c41}-\x{c44}\x{c58}-\x{c5a}\x{c60}\x{c61}\x{c66}-\x{c6f}\x{c7f}\x{c82}\x{c83}\x{c85}-\x{c8c}\x{c8e}-\x{c90}\x{c92}-\x{ca8}\x{caa}-\x{cb3}\x{cb5}-\x{cb9}\x{cbd}-\x{cc4}\x{cc6}-\x{cc8}\x{cca}\x{ccb}\x{cd5}\x{cd6}\x{cde}\x{ce0}\x{ce1}\x{ce6}-\x{cef}\x{cf1}\x{cf2}\x{d02}\x{d03}\x{d05}-\x{d0c}\x{d0e}-\x{d10}\x{d12}-\x{d3a}\x{d3d}-\x{d40}\x{d46}-\x{d48}\x{d4a}-\x{d4c}\x{d4e}\x{d57}\x{d5f}-\x{d61}\x{d66}-\x{d75}\x{d79}-\x{d7f}\x{d82}\x{d83}\x{d85}-\x{d96}\x{d9a}-\x{db1}\x{db3}-\x{dbb}\x{dbd}\x{dc0}-\x{dc6}\x{dcf}-\x{dd1}\x{dd8}-\x{ddf}\x{de6}-\x{def}\x{df2}-\x{df4}\x{e01}-\x{e30}\x{e32}\x{e33}\x{e40}-\x{e46}\x{e4f}-\x{e5b}\x{e81}\x{e82}\x{e84}\x{e87}\x{e88}\x{e8a}\x{e8d}\x{e94}-\x{e97}\x{e99}-\x{e9f}\x{ea1}-\x{ea3}\x{ea5}\x{ea7}\x{eaa}\x{eab}\x{ead}-\x{eb0}\x{eb2}\x{eb3}\x{ebd}\x{ec0}-\x{ec4}\x{ec6}\x{ed0}-\x{ed9}\x{edc}-\x{edf}\x{f00}-\x{f17}\x{f1a}-\x{f34}\x{f36}\x{f38}\x{f3e}-\x{f47}\x{f49}-\x{f6c}\x{f7f}\x{f85}\x{f88}-\x{f8c}\x{fbe}-\x{fc5}\x{fc7}-\x{fcc}\x{fce}-\x{fda}\x{1000}-\x{102c}\x{1031}\x{1038}\x{103b}\x{103c}\x{103f}-\x{1057}\x{105a}-\x{105d}\x{1061}-\x{1070}\x{1075}-\x{1081}\x{1083}\x{1084}\x{1087}-\x{108c}\x{108e}-\x{109c}\x{109e}-\x{10c5}\x{10c7}\x{10cd}\x{10d0}-\x{1248}\x{124a}-\x{124d}\x{1250}-\x{1256}\x{1258}\x{125a}-\x{125d}\x{1260}-\x{1288}\x{128a}-\x{128d}\x{1290}-\x{12b0}\x{12b2}-\x{12b5}\x{12b8}-\x{12be}\x{12c0}\x{12c2}-\x{12c5}\x{12c8}-\x{12d6}\x{12d8}-\x{1310}\x{1312}-\x{1315}\x{1318}-\x{135a}\x{1360}-\x{137c}\x{1380}-\x{138f}\x{13a0}-\x{13f5}\x{13f8}-\x{13fd}\x{1401}-\x{167f}\x{1681}-\x{169a}\x{16a0}-\x{16f8}\x{1700}-\x{170c}\x{170e}-\x{1711}\x{1720}-\x{1731}\x{1735}\x{1736}\x{1740}-\x{1751}\x{1760}-\x{176c}\x{176e}-\x{1770}\x{1780}-\x{17b3}\x{17b6}\x{17be}-\x{17c5}\x{17c7}\x{17c8}\x{17d4}-\x{17da}\x{17dc}\x{17e0}-\x{17e9}\x{1810}-\x{1819}\x{1820}-\x{1877}\x{1880}-\x{18a8}\x{18aa}\x{18b0}-\x{18f5}\x{1900}-\x{191e}\x{1923}-\x{1926}\x{1929}-\x{192b}\x{1930}\x{1931}\x{1933}-\x{1938}\x{1946}-\x{196d}\x{1970}-\x{1974}\x{1980}-\x{19ab}\x{19b0}-\x{19c9}\x{19d0}-\x{19da}\x{1a00}-\x{1a16}\x{1a19}\x{1a1a}\x{1a1e}-\x{1a55}\x{1a57}\x{1a61}\x{1a63}\x{1a64}\x{1a6d}-\x{1a72}\x{1a80}-\x{1a89}\x{1a90}-\x{1a99}\x{1aa0}-\x{1aad}\x{1b04}-\x{1b33}\x{1b35}\x{1b3b}\x{1b3d}-\x{1b41}\x{1b43}-\x{1b4b}\x{1b50}-\x{1b6a}\x{1b74}-\x{1b7c}\x{1b82}-\x{1ba1}\x{1ba6}\x{1ba7}\x{1baa}\x{1bae}-\x{1be5}\x{1be7}\x{1bea}-\x{1bec}\x{1bee}\x{1bf2}\x{1bf3}\x{1bfc}-\x{1c2b}\x{1c34}\x{1c35}\x{1c3b}-\x{1c49}\x{1c4d}-\x{1c7f}\x{1cc0}-\x{1cc7}\x{1cd3}\x{1ce1}\x{1ce9}-\x{1cec}\x{1cee}-\x{1cf3}\x{1cf5}\x{1cf6}\x{1d00}-\x{1dbf}\x{1e00}-\x{1f15}\x{1f18}-\x{1f1d}\x{1f20}-\x{1f45}\x{1f48}-\x{1f4d}\x{1f50}-\x{1f57}\x{1f59}\x{1f5b}\x{1f5d}\x{1f5f}-\x{1f7d}\x{1f80}-\x{1fb4}\x{1fb6}-\x{1fbc}\x{1fbe}\x{1fc2}-\x{1fc4}\x{1fc6}-\x{1fcc}\x{1fd0}-\x{1fd3}\x{1fd6}-\x{1fdb}\x{1fe0}-\x{1fec}\x{1ff2}-\x{1ff4}\x{1ff6}-\x{1ffc}\x{200e}\x{2071}\x{207f}\x{2090}-\x{209c}\x{2102}\x{2107}\x{210a}-\x{2113}\x{2115}\x{2119}-\x{211d}\x{2124}\x{2126}\x{2128}\x{212a}-\x{212d}\x{212f}-\x{2139}\x{213c}-\x{213f}\x{2145}-\x{2149}\x{214e}\x{214f}\x{2160}-\x{2188}\x{2336}-\x{237a}\x{2395}\x{249c}-\x{24e9}\x{26ac}\x{2800}-\x{28ff}\x{2c00}-\x{2c2e}\x{2c30}-\x{2c5e}\x{2c60}-\x{2ce4}\x{2ceb}-\x{2cee}\x{2cf2}\x{2cf3}\x{2d00}-\x{2d25}\x{2d27}\x{2d2d}\x{2d30}-\x{2d67}\x{2d6f}\x{2d70}\x{2d80}-\x{2d96}\x{2da0}-\x{2da6}\x{2da8}-\x{2dae}\x{2db0}-\x{2db6}\x{2db8}-\x{2dbe}\x{2dc0}-\x{2dc6}\x{2dc8}-\x{2dce}\x{2dd0}-\x{2dd6}\x{2dd8}-\x{2dde}\x{3005}-\x{3007}\x{3021}-\x{3029}\x{302e}\x{302f}\x{3031}-\x{3035}\x{3038}-\x{303c}\x{3041}-\x{3096}\x{309d}-\x{309f}\x{30a1}-\x{30fa}\x{30fc}-\x{30ff}\x{3105}-\x{312d}\x{3131}-\x{318e}\x{3190}-\x{31ba}\x{31f0}-\x{321c}\x{3220}-\x{324f}\x{3260}-\x{327b}\x{327f}-\x{32b0}\x{32c0}-\x{32cb}\x{32d0}-\x{32fe}\x{3300}-\x{3376}\x{337b}-\x{33dd}\x{33e0}-\x{33fe}\x{3400}-\x{4db5}\x{4e00}-\x{9fd5}\x{a000}-\x{a48c}\x{a4d0}-\x{a60c}\x{a610}-\x{a62b}\x{a640}-\x{a66e}\x{a680}-\x{a69d}\x{a6a0}-\x{a6ef}\x{a6f2}-\x{a6f7}\x{a722}-\x{a787}\x{a789}-\x{a7ad}\x{a7b0}-\x{a7b7}\x{a7f7}-\x{a801}\x{a803}-\x{a805}\x{a807}-\x{a80a}\x{a80c}-\x{a824}\x{a827}\x{a830}-\x{a837}\x{a840}-\x{a873}\x{a880}-\x{a8c3}\x{a8ce}-\x{a8d9}\x{a8f2}-\x{a8fd}\x{a900}-\x{a925}\x{a92e}-\x{a946}\x{a952}\x{a953}\x{a95f}-\x{a97c}\x{a983}-\x{a9b2}\x{a9b4}\x{a9b5}\x{a9ba}\x{a9bb}\x{a9bd}-\x{a9cd}\x{a9cf}-\x{a9d9}\x{a9de}-\x{a9e4}\x{a9e6}-\x{a9fe}\x{aa00}-\x{aa28}\x{aa2f}\x{aa30}\x{aa33}\x{aa34}\x{aa40}-\x{aa42}\x{aa44}-\x{aa4b}\x{aa4d}\x{aa50}-\x{aa59}\x{aa5c}-\x{aa7b}\x{aa7d}-\x{aaaf}\x{aab1}\x{aab5}\x{aab6}\x{aab9}-\x{aabd}\x{aac0}\x{aac2}\x{aadb}-\x{aaeb}\x{aaee}-\x{aaf5}\x{ab01}-\x{ab06}\x{ab09}-\x{ab0e}\x{ab11}-\x{ab16}\x{ab20}-\x{ab26}\x{ab28}-\x{ab2e}\x{ab30}-\x{ab65}\x{ab70}-\x{abe4}\x{abe6}\x{abe7}\x{abe9}-\x{abec}\x{abf0}-\x{abf9}\x{ac00}-\x{d7a3}\x{d7b0}-\x{d7c6}\x{d7cb}-\x{d7fb}\x{e000}-\x{fa6d}\x{fa70}-\x{fad9}\x{fb00}-\x{fb06}\x{fb13}-\x{fb17}\x{ff21}-\x{ff3a}\x{ff41}-\x{ff5a}\x{ff66}-\x{ffbe}\x{ffc2}-\x{ffc7}\x{ffca}-\x{ffcf}\x{ffd2}-\x{ffd7}\x{ffda}-\x{ffdc}\x{10000}-\x{1000b}\x{1000d}-\x{10026}\x{10028}-\x{1003a}\x{1003c}\x{1003d}\x{1003f}-\x{1004d}\x{10050}-\x{1005d}\x{10080}-\x{100fa}\x{10100}\x{10102}\x{10107}-\x{10133}\x{10137}-\x{1013f}\x{101d0}-\x{101fc}\x{10280}-\x{1029c}\x{102a0}-\x{102d0}\x{10300}-\x{10323}\x{10330}-\x{1034a}\x{10350}-\x{10375}\x{10380}-\x{1039d}\x{1039f}-\x{103c3}\x{103c8}-\x{103d5}\x{10400}-\x{1049d}\x{104a0}-\x{104a9}\x{10500}-\x{10527}\x{10530}-\x{10563}\x{1056f}\x{10600}-\x{10736}\x{10740}-\x{10755}\x{10760}-\x{10767}\x{11000}\x{11002}-\x{11037}\x{11047}-\x{1104d}\x{11066}-\x{1106f}\x{11082}-\x{110b2}\x{110b7}\x{110b8}\x{110bb}-\x{110c1}\x{110d0}-\x{110e8}\x{110f0}-\x{110f9}\x{11103}-\x{11126}\x{1112c}\x{11136}-\x{11143}\x{11150}-\x{11172}\x{11174}-\x{11176}\x{11182}-\x{111b5}\x{111bf}-\x{111c9}\x{111cd}\x{111d0}-\x{111df}\x{111e1}-\x{111f4}\x{11200}-\x{11211}\x{11213}-\x{1122e}\x{11232}\x{11233}\x{11235}\x{11238}-\x{1123d}\x{11280}-\x{11286}\x{11288}\x{1128a}-\x{1128d}\x{1128f}-\x{1129d}\x{1129f}-\x{112a9}\x{112b0}-\x{112de}\x{112e0}-\x{112e2}\x{112f0}-\x{112f9}\x{11302}\x{11303}\x{11305}-\x{1130c}\x{1130f}\x{11310}\x{11313}-\x{11328}\x{1132a}-\x{11330}\x{11332}\x{11333}\x{11335}-\x{11339}\x{1133d}-\x{1133f}\x{11341}-\x{11344}\x{11347}\x{11348}\x{1134b}-\x{1134d}\x{11350}\x{11357}\x{1135d}-\x{11363}\x{11480}-\x{114b2}\x{114b9}\x{114bb}-\x{114be}\x{114c1}\x{114c4}-\x{114c7}\x{114d0}-\x{114d9}\x{11580}-\x{115b1}\x{115b8}-\x{115bb}\x{115be}\x{115c1}-\x{115db}\x{11600}-\x{11632}\x{1163b}\x{1163c}\x{1163e}\x{11641}-\x{11644}\x{11650}-\x{11659}\x{11680}-\x{116aa}\x{116ac}\x{116ae}\x{116af}\x{116b6}\x{116c0}-\x{116c9}\x{11700}-\x{11719}\x{11720}\x{11721}\x{11726}\x{11730}-\x{1173f}\x{118a0}-\x{118f2}\x{118ff}\x{11ac0}-\x{11af8}\x{12000}-\x{12399}\x{12400}-\x{1246e}\x{12470}-\x{12474}\x{12480}-\x{12543}\x{13000}-\x{1342e}\x{14400}-\x{14646}\x{16800}-\x{16a38}\x{16a40}-\x{16a5e}\x{16a60}-\x{16a69}\x{16a6e}\x{16a6f}\x{16ad0}-\x{16aed}\x{16af5}\x{16b00}-\x{16b2f}\x{16b37}-\x{16b45}\x{16b50}-\x{16b59}\x{16b5b}-\x{16b61}\x{16b63}-\x{16b77}\x{16b7d}-\x{16b8f}\x{16f00}-\x{16f44}\x{16f50}-\x{16f7e}\x{16f93}-\x{16f9f}\x{1b000}\x{1b001}\x{1bc00}-\x{1bc6a}\x{1bc70}-\x{1bc7c}\x{1bc80}-\x{1bc88}\x{1bc90}-\x{1bc99}\x{1bc9c}\x{1bc9f}\x{1d000}-\x{1d0f5}\x{1d100}-\x{1d126}\x{1d129}-\x{1d166}\x{1d16a}-\x{1d172}\x{1d183}\x{1d184}\x{1d18c}-\x{1d1a9}\x{1d1ae}-\x{1d1e8}\x{1d360}-\x{1d371}\x{1d400}-\x{1d454}\x{1d456}-\x{1d49c}\x{1d49e}\x{1d49f}\x{1d4a2}\x{1d4a5}\x{1d4a6}\x{1d4a9}-\x{1d4ac}\x{1d4ae}-\x{1d4b9}\x{1d4bb}\x{1d4bd}-\x{1d4c3}\x{1d4c5}-\x{1d505}\x{1d507}-\x{1d50a}\x{1d50d}-\x{1d514}\x{1d516}-\x{1d51c}\x{1d51e}-\x{1d539}\x{1d53b}-\x{1d53e}\x{1d540}-\x{1d544}\x{1d546}\x{1d54a}-\x{1d550}\x{1d552}-\x{1d6a5}\x{1d6a8}-\x{1d6da}\x{1d6dc}-\x{1d714}\x{1d716}-\x{1d74e}\x{1d750}-\x{1d788}\x{1d78a}-\x{1d7c2}\x{1d7c4}-\x{1d7cb}\x{1d800}-\x{1d9ff}\x{1da37}-\x{1da3a}\x{1da6d}-\x{1da74}\x{1da76}-\x{1da83}\x{1da85}-\x{1da8b}\x{1f110}-\x{1f12e}\x{1f130}-\x{1f169}\x{1f170}-\x{1f19a}\x{1f1e6}-\x{1f202}\x{1f210}-\x{1f23a}\x{1f240}-\x{1f248}\x{1f250}\x{1f251}\x{20000}-\x{2a6d6}\x{2a700}-\x{2b734}\x{2b740}-\x{2b81d}\x{2b820}-\x{2cea1}\x{2f800}-\x{2fa1d}\x{f0000}-\x{ffffd}\x{100000}-\x{10fffd}])|([\x{590}\x{5be}\x{5c0}\x{5c3}\x{5c6}\x{5c8}-\x{5ff}\x{7c0}-\x{7ea}\x{7f4}\x{7f5}\x{7fa}-\x{815}\x{81a}\x{824}\x{828}\x{82e}-\x{858}\x{85c}-\x{89f}\x{200f}\x{fb1d}\x{fb1f}-\x{fb28}\x{fb2a}-\x{fb4f}\x{10800}-\x{1091e}\x{10920}-\x{10a00}\x{10a04}\x{10a07}-\x{10a0b}\x{10a10}-\x{10a37}\x{10a3b}-\x{10a3e}\x{10a40}-\x{10ae4}\x{10ae7}-\x{10b38}\x{10b40}-\x{10e5f}\x{10e7f}-\x{10fff}\x{1e800}-\x{1e8cf}\x{1e8d7}-\x{1edff}\x{1ef00}-\x{1efff}\x{608}\x{60b}\x{60d}\x{61b}-\x{64a}\x{66d}-\x{66f}\x{671}-\x{6d5}\x{6e5}\x{6e6}\x{6ee}\x{6ef}\x{6fa}-\x{710}\x{712}-\x{72f}\x{74b}-\x{7a5}\x{7b1}-\x{7bf}\x{8a0}-\x{8e2}\x{fb50}-\x{fd3d}\x{fd40}-\x{fdcf}\x{fdf0}-\x{fdfc}\x{fdfe}\x{fdff}\x{fe70}-\x{fefe}\x{1ee00}-\x{1eeef}\x{1eef2}-\x{1eeff}]))/u';
173
	// @codeCoverageIgnoreEnd
174
	// @codingStandardsIgnoreEnd
175
176
	/**
177
	 * Get a cached or new language object for a given language code
178
	 * @param string $code
179
	 * @return Language
180
	 */
181
	static function factory( $code ) {
182
		global $wgDummyLanguageCodes, $wgLangObjCacheSize;
183
184
		if ( isset( $wgDummyLanguageCodes[$code] ) ) {
185
			$code = $wgDummyLanguageCodes[$code];
186
		}
187
188
		// get the language object to process
189
		$langObj = isset( self::$mLangObjCache[$code] )
190
			? self::$mLangObjCache[$code]
191
			: self::newFromCode( $code );
192
193
		// merge the language object in to get it up front in the cache
194
		self::$mLangObjCache = array_merge( [ $code => $langObj ], self::$mLangObjCache );
195
		// get rid of the oldest ones in case we have an overflow
196
		self::$mLangObjCache = array_slice( self::$mLangObjCache, 0, $wgLangObjCacheSize, true );
197
198
		return $langObj;
199
	}
200
201
	/**
202
	 * Create a language object for a given language code
203
	 * @param string $code
204
	 * @throws MWException
205
	 * @return Language
206
	 */
207
	protected static function newFromCode( $code ) {
208
		if ( !Language::isValidCode( $code ) ) {
209
			throw new MWException( "Invalid language code \"$code\"" );
210
		}
211
212
		if ( !Language::isValidBuiltInCode( $code ) ) {
213
			// It's not possible to customise this code with class files, so
214
			// just return a Language object. This is to support uselang= hacks.
215
			$lang = new Language;
216
			$lang->setCode( $code );
217
			return $lang;
218
		}
219
220
		// Check if there is a language class for the code
221
		$class = self::classFromCode( $code );
222
		if ( class_exists( $class ) ) {
223
			$lang = new $class;
224
			return $lang;
225
		}
226
227
		// Keep trying the fallback list until we find an existing class
228
		$fallbacks = Language::getFallbacksFor( $code );
229
		foreach ( $fallbacks as $fallbackCode ) {
230
			if ( !Language::isValidBuiltInCode( $fallbackCode ) ) {
231
				throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" );
232
			}
233
234
			$class = self::classFromCode( $fallbackCode );
235
			if ( class_exists( $class ) ) {
236
				$lang = new $class;
237
				$lang->setCode( $code );
238
				return $lang;
239
			}
240
		}
241
242
		throw new MWException( "Invalid fallback sequence for language '$code'" );
243
	}
244
245
	/**
246
	 * Checks whether any localisation is available for that language tag
247
	 * in MediaWiki (MessagesXx.php exists).
248
	 *
249
	 * @param string $code Language tag (in lower case)
250
	 * @return bool Whether language is supported
251
	 * @since 1.21
252
	 */
253
	public static function isSupportedLanguage( $code ) {
254
		if ( !self::isValidBuiltInCode( $code ) ) {
255
			return false;
256
		}
257
258
		if ( $code === 'qqq' ) {
259
			return false;
260
		}
261
262
		return is_readable( self::getMessagesFileName( $code ) ) ||
263
			is_readable( self::getJsonMessagesFileName( $code ) );
264
	}
265
266
	/**
267
	 * Returns true if a language code string is a well-formed language tag
268
	 * according to RFC 5646.
269
	 * This function only checks well-formedness; it doesn't check that
270
	 * language, script or variant codes actually exist in the repositories.
271
	 *
272
	 * Based on regexes by Mark Davis of the Unicode Consortium:
273
	 * http://unicode.org/repos/cldr/trunk/tools/java/org/unicode/cldr/util/data/langtagRegex.txt
274
	 *
275
	 * @param string $code
276
	 * @param bool $lenient Whether to allow '_' as separator. The default is only '-'.
277
	 *
278
	 * @return bool
279
	 * @since 1.21
280
	 */
281
	public static function isWellFormedLanguageTag( $code, $lenient = false ) {
282
		$alpha = '[a-z]';
283
		$digit = '[0-9]';
284
		$alphanum = '[a-z0-9]';
285
		$x = 'x'; # private use singleton
286
		$singleton = '[a-wy-z]'; # other singleton
287
		$s = $lenient ? '[-_]' : '-';
288
289
		$language = "$alpha{2,8}|$alpha{2,3}$s$alpha{3}";
290
		$script = "$alpha{4}"; # ISO 15924
291
		$region = "(?:$alpha{2}|$digit{3})"; # ISO 3166-1 alpha-2 or UN M.49
292
		$variant = "(?:$alphanum{5,8}|$digit$alphanum{3})";
293
		$extension = "$singleton(?:$s$alphanum{2,8})+";
294
		$privateUse = "$x(?:$s$alphanum{1,8})+";
295
296
		# Define certain grandfathered codes, since otherwise the regex is pretty useless.
297
		# Since these are limited, this is safe even later changes to the registry --
298
		# the only oddity is that it might change the type of the tag, and thus
299
		# the results from the capturing groups.
300
		# http://www.iana.org/assignments/language-subtag-registry
301
302
		$grandfathered = "en{$s}GB{$s}oed"
303
			. "|i{$s}(?:ami|bnn|default|enochian|hak|klingon|lux|mingo|navajo|pwn|tao|tay|tsu)"
304
			. "|no{$s}(?:bok|nyn)"
305
			. "|sgn{$s}(?:BE{$s}(?:fr|nl)|CH{$s}de)"
306
			. "|zh{$s}min{$s}nan";
307
308
		$variantList = "$variant(?:$s$variant)*";
309
		$extensionList = "$extension(?:$s$extension)*";
310
311
		$langtag = "(?:($language)"
312
			. "(?:$s$script)?"
313
			. "(?:$s$region)?"
314
			. "(?:$s$variantList)?"
315
			. "(?:$s$extensionList)?"
316
			. "(?:$s$privateUse)?)";
317
318
		# The final breakdown, with capturing groups for each of these components
319
		# The variants, extensions, grandfathered, and private-use may have interior '-'
320
321
		$root = "^(?:$langtag|$privateUse|$grandfathered)$";
322
323
		return (bool)preg_match( "/$root/", strtolower( $code ) );
324
	}
325
326
	/**
327
	 * Returns true if a language code string is of a valid form, whether or
328
	 * not it exists. This includes codes which are used solely for
329
	 * customisation via the MediaWiki namespace.
330
	 *
331
	 * @param string $code
332
	 *
333
	 * @return bool
334
	 */
335
	public static function isValidCode( $code ) {
336
		static $cache = [];
337
		if ( !isset( $cache[$code] ) ) {
338
			// People think language codes are html safe, so enforce it.
339
			// Ideally we should only allow a-zA-Z0-9-
340
			// but, .+ and other chars are often used for {{int:}} hacks
341
			// see bugs 37564, 37587, 36938
342
			$cache[$code] =
343
				// Protect against path traversal
344
				strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
345
				&& !preg_match( MediaWikiTitleCodec::getTitleInvalidRegex(), $code );
346
		}
347
		return $cache[$code];
348
	}
349
350
	/**
351
	 * Returns true if a language code is of a valid form for the purposes of
352
	 * internal customisation of MediaWiki, via Messages*.php or *.json.
353
	 *
354
	 * @param string $code
355
	 *
356
	 * @throws MWException
357
	 * @since 1.18
358
	 * @return bool
359
	 */
360
	public static function isValidBuiltInCode( $code ) {
361
362
		if ( !is_string( $code ) ) {
363
			if ( is_object( $code ) ) {
364
				$addmsg = " of class " . get_class( $code );
365
			} else {
366
				$addmsg = '';
367
			}
368
			$type = gettype( $code );
369
			throw new MWException( __METHOD__ . " must be passed a string, $type given$addmsg" );
370
		}
371
372
		return (bool)preg_match( '/^[a-z0-9-]{2,}$/', $code );
373
	}
374
375
	/**
376
	 * Returns true if a language code is an IETF tag known to MediaWiki.
377
	 *
378
	 * @param string $tag
379
	 *
380
	 * @since 1.21
381
	 * @return bool
382
	 */
383
	public static function isKnownLanguageTag( $tag ) {
384
		// Quick escape for invalid input to avoid exceptions down the line
385
		// when code tries to process tags which are not valid at all.
386
		if ( !self::isValidBuiltInCode( $tag ) ) {
387
			return false;
388
		}
389
390
		if ( isset( MediaWiki\Languages\Data\Names::$names[$tag] )
391
			|| self::fetchLanguageName( $tag, $tag ) !== ''
392
		) {
393
			return true;
394
		}
395
396
		return false;
397
	}
398
399
	/**
400
	 * Get the LocalisationCache instance
401
	 *
402
	 * @return LocalisationCache
403
	 */
404
	public static function getLocalisationCache() {
405
		if ( is_null( self::$dataCache ) ) {
406
			global $wgLocalisationCacheConf;
407
			$class = $wgLocalisationCacheConf['class'];
408
			self::$dataCache = new $class( $wgLocalisationCacheConf );
409
		}
410
		return self::$dataCache;
411
	}
412
413
	function __construct() {
414
		$this->mConverter = new FakeConverter( $this );
0 ignored issues
show
Documentation Bug introduced by
It seems like new \FakeConverter($this) of type object<FakeConverter> is incompatible with the declared type object<LanguageConverter> of property $mConverter.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
415
		// Set the code to the name of the descendant
416
		if ( get_class( $this ) == 'Language' ) {
417
			$this->mCode = 'en';
418
		} else {
419
			$this->mCode = str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) );
420
		}
421
		self::getLocalisationCache();
422
	}
423
424
	/**
425
	 * Reduce memory usage
426
	 */
427
	function __destruct() {
428
		foreach ( $this as $name => $value ) {
0 ignored issues
show
Bug introduced by
The expression $this of type this<Language> is not traversable.
Loading history...
429
			unset( $this->$name );
430
		}
431
	}
432
433
	/**
434
	 * Hook which will be called if this is the content language.
435
	 * Descendants can use this to register hook functions or modify globals
436
	 */
437
	function initContLang() {
438
	}
439
440
	/**
441
	 * @return array
442
	 * @since 1.19
443
	 */
444
	public function getFallbackLanguages() {
445
		return self::getFallbacksFor( $this->mCode );
446
	}
447
448
	/**
449
	 * Exports $wgBookstoreListEn
450
	 * @return array
451
	 */
452
	public function getBookstoreList() {
453
		return self::$dataCache->getItem( $this->mCode, 'bookstoreList' );
454
	}
455
456
	/**
457
	 * Returns an array of localised namespaces indexed by their numbers. If the namespace is not
458
	 * available in localised form, it will be included in English.
459
	 *
460
	 * @return array
461
	 */
462
	public function getNamespaces() {
463
		if ( is_null( $this->namespaceNames ) ) {
464
			global $wgMetaNamespace, $wgMetaNamespaceTalk, $wgExtraNamespaces;
465
466
			$this->namespaceNames = self::$dataCache->getItem( $this->mCode, 'namespaceNames' );
467
			$validNamespaces = MWNamespace::getCanonicalNamespaces();
468
469
			$this->namespaceNames = $wgExtraNamespaces + $this->namespaceNames + $validNamespaces;
470
471
			$this->namespaceNames[NS_PROJECT] = $wgMetaNamespace;
472
			if ( $wgMetaNamespaceTalk ) {
473
				$this->namespaceNames[NS_PROJECT_TALK] = $wgMetaNamespaceTalk;
474
			} else {
475
				$talk = $this->namespaceNames[NS_PROJECT_TALK];
476
				$this->namespaceNames[NS_PROJECT_TALK] =
477
					$this->fixVariableInNamespace( $talk );
478
			}
479
480
			# Sometimes a language will be localised but not actually exist on this wiki.
481
			foreach ( $this->namespaceNames as $key => $text ) {
482
				if ( !isset( $validNamespaces[$key] ) ) {
483
					unset( $this->namespaceNames[$key] );
484
				}
485
			}
486
487
			# The above mixing may leave namespaces out of canonical order.
488
			# Re-order by namespace ID number...
489
			ksort( $this->namespaceNames );
490
491
			Hooks::run( 'LanguageGetNamespaces', [ &$this->namespaceNames ] );
492
		}
493
494
		return $this->namespaceNames;
495
	}
496
497
	/**
498
	 * Arbitrarily set all of the namespace names at once. Mainly used for testing
499
	 * @param array $namespaces Array of namespaces (id => name)
500
	 */
501
	public function setNamespaces( array $namespaces ) {
502
		$this->namespaceNames = $namespaces;
503
		$this->mNamespaceIds = null;
504
	}
505
506
	/**
507
	 * Resets all of the namespace caches. Mainly used for testing
508
	 */
509
	public function resetNamespaces() {
510
		$this->namespaceNames = null;
511
		$this->mNamespaceIds = null;
512
		$this->namespaceAliases = null;
513
	}
514
515
	/**
516
	 * A convenience function that returns getNamespaces() with spaces instead of underscores
517
	 * in values. Useful for producing output to be displayed e.g. in `<select>` forms.
518
	 *
519
	 * @return array
520
	 */
521
	public function getFormattedNamespaces() {
522
		$ns = $this->getNamespaces();
523
		foreach ( $ns as $k => $v ) {
524
			$ns[$k] = strtr( $v, '_', ' ' );
525
		}
526
		return $ns;
527
	}
528
529
	/**
530
	 * Get a namespace value by key
531
	 *
532
	 * <code>
533
	 * $mw_ns = $wgContLang->getNsText( NS_MEDIAWIKI );
534
	 * echo $mw_ns; // prints 'MediaWiki'
535
	 * </code>
536
	 *
537
	 * @param int $index The array key of the namespace to return
538
	 * @return string|bool String if the namespace value exists, otherwise false
539
	 */
540
	public function getNsText( $index ) {
541
		$ns = $this->getNamespaces();
542
		return isset( $ns[$index] ) ? $ns[$index] : false;
543
	}
544
545
	/**
546
	 * A convenience function that returns the same thing as
547
	 * getNsText() except with '_' changed to ' ', useful for
548
	 * producing output.
549
	 *
550
	 * <code>
551
	 * $mw_ns = $wgContLang->getFormattedNsText( NS_MEDIAWIKI_TALK );
552
	 * echo $mw_ns; // prints 'MediaWiki talk'
553
	 * </code>
554
	 *
555
	 * @param int $index The array key of the namespace to return
556
	 * @return string Namespace name without underscores (empty string if namespace does not exist)
557
	 */
558
	public function getFormattedNsText( $index ) {
559
		$ns = $this->getNsText( $index );
560
		return strtr( $ns, '_', ' ' );
0 ignored issues
show
Bug introduced by
It seems like $ns defined by $this->getNsText($index) on line 559 can also be of type boolean; however, strtr() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
561
	}
562
563
	/**
564
	 * Returns gender-dependent namespace alias if available.
565
	 * See https://www.mediawiki.org/wiki/Manual:$wgExtraGenderNamespaces
566
	 * @param int $index Namespace index
567
	 * @param string $gender Gender key (male, female... )
568
	 * @return string
569
	 * @since 1.18
570
	 */
571
	public function getGenderNsText( $index, $gender ) {
572
		global $wgExtraGenderNamespaces;
573
574
		$ns = $wgExtraGenderNamespaces +
575
			(array)self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
576
577
		return isset( $ns[$index][$gender] ) ? $ns[$index][$gender] : $this->getNsText( $index );
578
	}
579
580
	/**
581
	 * Whether this language uses gender-dependent namespace aliases.
582
	 * See https://www.mediawiki.org/wiki/Manual:$wgExtraGenderNamespaces
583
	 * @return bool
584
	 * @since 1.18
585
	 */
586
	public function needsGenderDistinction() {
587
		global $wgExtraGenderNamespaces, $wgExtraNamespaces;
588
		if ( count( $wgExtraGenderNamespaces ) > 0 ) {
589
			// $wgExtraGenderNamespaces overrides everything
590
			return true;
591
		} elseif ( isset( $wgExtraNamespaces[NS_USER] ) && isset( $wgExtraNamespaces[NS_USER_TALK] ) ) {
592
			/// @todo There may be other gender namespace than NS_USER & NS_USER_TALK in the future
593
			// $wgExtraNamespaces overrides any gender aliases specified in i18n files
594
			return false;
595
		} else {
596
			// Check what is in i18n files
597
			$aliases = self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
598
			return count( $aliases ) > 0;
599
		}
600
	}
601
602
	/**
603
	 * Get a namespace key by value, case insensitive.
604
	 * Only matches namespace names for the current language, not the
605
	 * canonical ones defined in Namespace.php.
606
	 *
607
	 * @param string $text
608
	 * @return int|bool An integer if $text is a valid value otherwise false
609
	 */
610
	function getLocalNsIndex( $text ) {
611
		$lctext = $this->lc( $text );
612
		$ids = $this->getNamespaceIds();
613
		return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
614
	}
615
616
	/**
617
	 * @return array
618
	 */
619
	public function getNamespaceAliases() {
620
		if ( is_null( $this->namespaceAliases ) ) {
621
			$aliases = self::$dataCache->getItem( $this->mCode, 'namespaceAliases' );
622
			if ( !$aliases ) {
623
				$aliases = [];
624
			} else {
625
				foreach ( $aliases as $name => $index ) {
626
					if ( $index === NS_PROJECT_TALK ) {
627
						unset( $aliases[$name] );
628
						$name = $this->fixVariableInNamespace( $name );
629
						$aliases[$name] = $index;
630
					}
631
				}
632
			}
633
634
			global $wgExtraGenderNamespaces;
635
			$genders = $wgExtraGenderNamespaces +
636
				(array)self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
637
			foreach ( $genders as $index => $forms ) {
638
				foreach ( $forms as $alias ) {
639
					$aliases[$alias] = $index;
640
				}
641
			}
642
643
			# Also add converted namespace names as aliases, to avoid confusion.
644
			$convertedNames = [];
645
			foreach ( $this->getVariants() as $variant ) {
646
				if ( $variant === $this->mCode ) {
647
					continue;
648
				}
649
				foreach ( $this->getNamespaces() as $ns => $_ ) {
650
					$convertedNames[$this->getConverter()->convertNamespace( $ns, $variant )] = $ns;
651
				}
652
			}
653
654
			$this->namespaceAliases = $aliases + $convertedNames;
655
		}
656
657
		return $this->namespaceAliases;
658
	}
659
660
	/**
661
	 * @return array
662
	 */
663
	public function getNamespaceIds() {
664
		if ( is_null( $this->mNamespaceIds ) ) {
665
			global $wgNamespaceAliases;
666
			# Put namespace names and aliases into a hashtable.
667
			# If this is too slow, then we should arrange it so that it is done
668
			# before caching. The catch is that at pre-cache time, the above
669
			# class-specific fixup hasn't been done.
670
			$this->mNamespaceIds = [];
671
			foreach ( $this->getNamespaces() as $index => $name ) {
672
				$this->mNamespaceIds[$this->lc( $name )] = $index;
673
			}
674
			foreach ( $this->getNamespaceAliases() as $name => $index ) {
675
				$this->mNamespaceIds[$this->lc( $name )] = $index;
676
			}
677
			if ( $wgNamespaceAliases ) {
678
				foreach ( $wgNamespaceAliases as $name => $index ) {
679
					$this->mNamespaceIds[$this->lc( $name )] = $index;
680
				}
681
			}
682
		}
683
		return $this->mNamespaceIds;
684
	}
685
686
	/**
687
	 * Get a namespace key by value, case insensitive.  Canonical namespace
688
	 * names override custom ones defined for the current language.
689
	 *
690
	 * @param string $text
691
	 * @return int|bool An integer if $text is a valid value otherwise false
692
	 */
693
	public function getNsIndex( $text ) {
694
		$lctext = $this->lc( $text );
695
		$ns = MWNamespace::getCanonicalIndex( $lctext );
696
		if ( $ns !== null ) {
697
			return $ns;
698
		}
699
		$ids = $this->getNamespaceIds();
700
		return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
701
	}
702
703
	/**
704
	 * short names for language variants used for language conversion links.
705
	 *
706
	 * @param string $code
707
	 * @param bool $usemsg Use the "variantname-xyz" message if it exists
708
	 * @return string
709
	 */
710
	public function getVariantname( $code, $usemsg = true ) {
711
		$msg = "variantname-$code";
712
		if ( $usemsg && wfMessage( $msg )->exists() ) {
713
			return $this->getMessageFromDB( $msg );
714
		}
715
		$name = self::fetchLanguageName( $code );
716
		if ( $name ) {
717
			return $name; # if it's defined as a language name, show that
718
		} else {
719
			# otherwise, output the language code
720
			return $code;
721
		}
722
	}
723
724
	/**
725
	 * @return array
726
	 */
727
	public function getDatePreferences() {
728
		return self::$dataCache->getItem( $this->mCode, 'datePreferences' );
729
	}
730
731
	/**
732
	 * @return array
733
	 */
734
	function getDateFormats() {
735
		return self::$dataCache->getItem( $this->mCode, 'dateFormats' );
736
	}
737
738
	/**
739
	 * @return array|string
740
	 */
741
	public function getDefaultDateFormat() {
742
		$df = self::$dataCache->getItem( $this->mCode, 'defaultDateFormat' );
743
		if ( $df === 'dmy or mdy' ) {
744
			global $wgAmericanDates;
745
			return $wgAmericanDates ? 'mdy' : 'dmy';
746
		} else {
747
			return $df;
748
		}
749
	}
750
751
	/**
752
	 * @return array
753
	 */
754
	public function getDatePreferenceMigrationMap() {
755
		return self::$dataCache->getItem( $this->mCode, 'datePreferenceMigrationMap' );
756
	}
757
758
	/**
759
	 * @param string $image
760
	 * @return array|null
761
	 */
762
	function getImageFile( $image ) {
763
		return self::$dataCache->getSubitem( $this->mCode, 'imageFiles', $image );
764
	}
765
766
	/**
767
	 * @return array
768
	 * @since 1.24
769
	 */
770
	public function getImageFiles() {
771
		return self::$dataCache->getItem( $this->mCode, 'imageFiles' );
772
	}
773
774
	/**
775
	 * @return array
776
	 */
777
	public function getExtraUserToggles() {
778
		return (array)self::$dataCache->getItem( $this->mCode, 'extraUserToggles' );
779
	}
780
781
	/**
782
	 * @param string $tog
783
	 * @return string
784
	 */
785
	function getUserToggle( $tog ) {
786
		return $this->getMessageFromDB( "tog-$tog" );
787
	}
788
789
	/**
790
	 * Get an array of language names, indexed by code.
791
	 * @param null|string $inLanguage Code of language in which to return the names
792
	 *		Use null for autonyms (native names)
793
	 * @param string $include One of:
794
	 *		'all' all available languages
795
	 *		'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
796
	 *		'mwfile' only if the language is in 'mw' *and* has a message file
797
	 * @return array Language code => language name
798
	 * @since 1.20
799
	 */
800
	public static function fetchLanguageNames( $inLanguage = null, $include = 'mw' ) {
801
		$cacheKey = $inLanguage === null ? 'null' : $inLanguage;
802
		$cacheKey .= ":$include";
803
		if ( self::$languageNameCache === null ) {
804
			self::$languageNameCache = new HashBagOStuff( [ 'maxKeys' => 20 ] );
805
		}
806
807
		$ret = self::$languageNameCache->get( $cacheKey );
808
		if ( !$ret ) {
809
			$ret = self::fetchLanguageNamesUncached( $inLanguage, $include );
810
			self::$languageNameCache->set( $cacheKey, $ret );
811
		}
812
		return $ret;
813
	}
814
815
	/**
816
	 * Uncached helper for fetchLanguageNames
817
	 * @param null|string $inLanguage Code of language in which to return the names
818
	 *		Use null for autonyms (native names)
819
	 * @param string $include One of:
820
	 *		'all' all available languages
821
	 *		'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
822
	 *		'mwfile' only if the language is in 'mw' *and* has a message file
823
	 * @return array Language code => language name
824
	 */
825
	private static function fetchLanguageNamesUncached( $inLanguage = null, $include = 'mw' ) {
826
		global $wgExtraLanguageNames;
827
828
		// If passed an invalid language code to use, fallback to en
829
		if ( $inLanguage !== null && !Language::isValidCode( $inLanguage ) ) {
830
			$inLanguage = 'en';
831
		}
832
833
		$names = [];
834
835
		if ( $inLanguage ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $inLanguage of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
836
			# TODO: also include when $inLanguage is null, when this code is more efficient
837
			Hooks::run( 'LanguageGetTranslatedLanguageNames', [ &$names, $inLanguage ] );
838
		}
839
840
		$mwNames = $wgExtraLanguageNames + MediaWiki\Languages\Data\Names::$names;
841
		foreach ( $mwNames as $mwCode => $mwName ) {
842
			# - Prefer own MediaWiki native name when not using the hook
843
			# - For other names just add if not added through the hook
844
			if ( $mwCode === $inLanguage || !isset( $names[$mwCode] ) ) {
845
				$names[$mwCode] = $mwName;
846
			}
847
		}
848
849
		if ( $include === 'all' ) {
850
			ksort( $names );
851
			return $names;
852
		}
853
854
		$returnMw = [];
855
		$coreCodes = array_keys( $mwNames );
856
		foreach ( $coreCodes as $coreCode ) {
857
			$returnMw[$coreCode] = $names[$coreCode];
858
		}
859
860
		if ( $include === 'mwfile' ) {
861
			$namesMwFile = [];
862
			# We do this using a foreach over the codes instead of a directory
863
			# loop so that messages files in extensions will work correctly.
864
			foreach ( $returnMw as $code => $value ) {
865
				if ( is_readable( self::getMessagesFileName( $code ) )
866
					|| is_readable( self::getJsonMessagesFileName( $code ) )
867
				) {
868
					$namesMwFile[$code] = $names[$code];
869
				}
870
			}
871
872
			ksort( $namesMwFile );
873
			return $namesMwFile;
874
		}
875
876
		ksort( $returnMw );
877
		# 'mw' option; default if it's not one of the other two options (all/mwfile)
878
		return $returnMw;
879
	}
880
881
	/**
882
	 * @param string $code The code of the language for which to get the name
883
	 * @param null|string $inLanguage Code of language in which to return the name (null for autonyms)
884
	 * @param string $include 'all', 'mw' or 'mwfile'; see fetchLanguageNames()
885
	 * @return string Language name or empty
886
	 * @since 1.20
887
	 */
888
	public static function fetchLanguageName( $code, $inLanguage = null, $include = 'all' ) {
889
		$code = strtolower( $code );
890
		$array = self::fetchLanguageNames( $inLanguage, $include );
891
		return !array_key_exists( $code, $array ) ? '' : $array[$code];
892
	}
893
894
	/**
895
	 * Get a message from the MediaWiki namespace.
896
	 *
897
	 * @param string $msg Message name
898
	 * @return string
899
	 */
900
	public function getMessageFromDB( $msg ) {
901
		return $this->msg( $msg )->text();
902
	}
903
904
	/**
905
	 * Get message object in this language. Only for use inside this class.
906
	 *
907
	 * @param string $msg Message name
908
	 * @return Message
909
	 */
910
	protected function msg( $msg ) {
911
		return wfMessage( $msg )->inLanguage( $this );
912
	}
913
914
	/**
915
	 * @param string $key
916
	 * @return string
917
	 */
918
	public function getMonthName( $key ) {
919
		return $this->getMessageFromDB( self::$mMonthMsgs[$key - 1] );
920
	}
921
922
	/**
923
	 * @return array
924
	 */
925 View Code Duplication
	public function getMonthNamesArray() {
926
		$monthNames = [ '' ];
927
		for ( $i = 1; $i < 13; $i++ ) {
928
			$monthNames[] = $this->getMonthName( $i );
929
		}
930
		return $monthNames;
931
	}
932
933
	/**
934
	 * @param string $key
935
	 * @return string
936
	 */
937
	public function getMonthNameGen( $key ) {
938
		return $this->getMessageFromDB( self::$mMonthGenMsgs[$key - 1] );
939
	}
940
941
	/**
942
	 * @param string $key
943
	 * @return string
944
	 */
945
	public function getMonthAbbreviation( $key ) {
946
		return $this->getMessageFromDB( self::$mMonthAbbrevMsgs[$key - 1] );
947
	}
948
949
	/**
950
	 * @return array
951
	 */
952 View Code Duplication
	public function getMonthAbbreviationsArray() {
953
		$monthNames = [ '' ];
954
		for ( $i = 1; $i < 13; $i++ ) {
955
			$monthNames[] = $this->getMonthAbbreviation( $i );
956
		}
957
		return $monthNames;
958
	}
959
960
	/**
961
	 * @param string $key
962
	 * @return string
963
	 */
964
	public function getWeekdayName( $key ) {
965
		return $this->getMessageFromDB( self::$mWeekdayMsgs[$key - 1] );
966
	}
967
968
	/**
969
	 * @param string $key
970
	 * @return string
971
	 */
972
	function getWeekdayAbbreviation( $key ) {
973
		return $this->getMessageFromDB( self::$mWeekdayAbbrevMsgs[$key - 1] );
974
	}
975
976
	/**
977
	 * @param string $key
978
	 * @return string
979
	 */
980
	function getIranianCalendarMonthName( $key ) {
981
		return $this->getMessageFromDB( self::$mIranianCalendarMonthMsgs[$key - 1] );
982
	}
983
984
	/**
985
	 * @param string $key
986
	 * @return string
987
	 */
988
	function getHebrewCalendarMonthName( $key ) {
989
		return $this->getMessageFromDB( self::$mHebrewCalendarMonthMsgs[$key - 1] );
990
	}
991
992
	/**
993
	 * @param string $key
994
	 * @return string
995
	 */
996
	function getHebrewCalendarMonthNameGen( $key ) {
997
		return $this->getMessageFromDB( self::$mHebrewCalendarMonthGenMsgs[$key - 1] );
998
	}
999
1000
	/**
1001
	 * @param string $key
1002
	 * @return string
1003
	 */
1004
	function getHijriCalendarMonthName( $key ) {
1005
		return $this->getMessageFromDB( self::$mHijriCalendarMonthMsgs[$key - 1] );
1006
	}
1007
1008
	/**
1009
	 * Pass through result from $dateTimeObj->format()
1010
	 * @param DateTime|bool|null &$dateTimeObj
1011
	 * @param string $ts
1012
	 * @param DateTimeZone|bool|null $zone
1013
	 * @param string $code
1014
	 * @return string
1015
	 */
1016
	private static function dateTimeObjFormat( &$dateTimeObj, $ts, $zone, $code ) {
1017
		if ( !$dateTimeObj ) {
1018
			$dateTimeObj = DateTime::createFromFormat(
1019
				'YmdHis', $ts, $zone ?: new DateTimeZone( 'UTC' )
1020
			);
1021
		}
1022
		return $dateTimeObj->format( $code );
1023
	}
1024
1025
	/**
1026
	 * This is a workalike of PHP's date() function, but with better
1027
	 * internationalisation, a reduced set of format characters, and a better
1028
	 * escaping format.
1029
	 *
1030
	 * Supported format characters are dDjlNwzWFmMntLoYyaAgGhHiscrUeIOPTZ. See
1031
	 * the PHP manual for definitions. There are a number of extensions, which
1032
	 * start with "x":
1033
	 *
1034
	 *    xn   Do not translate digits of the next numeric format character
1035
	 *    xN   Toggle raw digit (xn) flag, stays set until explicitly unset
1036
	 *    xr   Use roman numerals for the next numeric format character
1037
	 *    xh   Use hebrew numerals for the next numeric format character
1038
	 *    xx   Literal x
1039
	 *    xg   Genitive month name
1040
	 *
1041
	 *    xij  j (day number) in Iranian calendar
1042
	 *    xiF  F (month name) in Iranian calendar
1043
	 *    xin  n (month number) in Iranian calendar
1044
	 *    xiy  y (two digit year) in Iranian calendar
1045
	 *    xiY  Y (full year) in Iranian calendar
1046
	 *
1047
	 *    xjj  j (day number) in Hebrew calendar
1048
	 *    xjF  F (month name) in Hebrew calendar
1049
	 *    xjt  t (days in month) in Hebrew calendar
1050
	 *    xjx  xg (genitive month name) in Hebrew calendar
1051
	 *    xjn  n (month number) in Hebrew calendar
1052
	 *    xjY  Y (full year) in Hebrew calendar
1053
	 *
1054
	 *    xmj  j (day number) in Hijri calendar
1055
	 *    xmF  F (month name) in Hijri calendar
1056
	 *    xmn  n (month number) in Hijri calendar
1057
	 *    xmY  Y (full year) in Hijri calendar
1058
	 *
1059
	 *    xkY  Y (full year) in Thai solar calendar. Months and days are
1060
	 *                       identical to the Gregorian calendar
1061
	 *    xoY  Y (full year) in Minguo calendar or Juche year.
1062
	 *                       Months and days are identical to the
1063
	 *                       Gregorian calendar
1064
	 *    xtY  Y (full year) in Japanese nengo. Months and days are
1065
	 *                       identical to the Gregorian calendar
1066
	 *
1067
	 * Characters enclosed in double quotes will be considered literal (with
1068
	 * the quotes themselves removed). Unmatched quotes will be considered
1069
	 * literal quotes. Example:
1070
	 *
1071
	 * "The month is" F       => The month is January
1072
	 * i's"                   => 20'11"
1073
	 *
1074
	 * Backslash escaping is also supported.
1075
	 *
1076
	 * Input timestamp is assumed to be pre-normalized to the desired local
1077
	 * time zone, if any. Note that the format characters crUeIOPTZ will assume
1078
	 * $ts is UTC if $zone is not given.
1079
	 *
1080
	 * @param string $format
1081
	 * @param string $ts 14-character timestamp
1082
	 *      YYYYMMDDHHMMSS
1083
	 *      01234567890123
1084
	 * @param DateTimeZone $zone Timezone of $ts
1085
	 * @param[out] int $ttl The amount of time (in seconds) the output may be cached for.
1086
	 * Only makes sense if $ts is the current time.
1087
	 * @todo handling of "o" format character for Iranian, Hebrew, Hijri & Thai?
1088
	 *
1089
	 * @throws MWException
1090
	 * @return string
1091
	 */
1092
	public function sprintfDate( $format, $ts, DateTimeZone $zone = null, &$ttl = null ) {
1093
		$s = '';
1094
		$raw = false;
1095
		$roman = false;
1096
		$hebrewNum = false;
1097
		$dateTimeObj = false;
1098
		$rawToggle = false;
1099
		$iranian = false;
1100
		$hebrew = false;
1101
		$hijri = false;
1102
		$thai = false;
1103
		$minguo = false;
1104
		$tenno = false;
1105
1106
		$usedSecond = false;
1107
		$usedMinute = false;
1108
		$usedHour = false;
1109
		$usedAMPM = false;
1110
		$usedDay = false;
1111
		$usedWeek = false;
1112
		$usedMonth = false;
1113
		$usedYear = false;
1114
		$usedISOYear = false;
1115
		$usedIsLeapYear = false;
1116
1117
		$usedHebrewMonth = false;
1118
		$usedIranianMonth = false;
1119
		$usedHijriMonth = false;
1120
		$usedHebrewYear = false;
1121
		$usedIranianYear = false;
1122
		$usedHijriYear = false;
1123
		$usedTennoYear = false;
1124
1125
		if ( strlen( $ts ) !== 14 ) {
1126
			throw new MWException( __METHOD__ . ": The timestamp $ts should have 14 characters" );
1127
		}
1128
1129
		if ( !ctype_digit( $ts ) ) {
1130
			throw new MWException( __METHOD__ . ": The timestamp $ts should be a number" );
1131
		}
1132
1133
		$formatLength = strlen( $format );
1134
		for ( $p = 0; $p < $formatLength; $p++ ) {
1135
			$num = false;
1136
			$code = $format[$p];
1137
			if ( $code == 'x' && $p < $formatLength - 1 ) {
1138
				$code .= $format[++$p];
1139
			}
1140
1141
			if ( ( $code === 'xi'
1142
					|| $code === 'xj'
1143
					|| $code === 'xk'
1144
					|| $code === 'xm'
1145
					|| $code === 'xo'
1146
					|| $code === 'xt' )
1147
				&& $p < $formatLength - 1 ) {
1148
				$code .= $format[++$p];
1149
			}
1150
1151
			switch ( $code ) {
1152
				case 'xx':
1153
					$s .= 'x';
1154
					break;
1155
				case 'xn':
1156
					$raw = true;
1157
					break;
1158
				case 'xN':
1159
					$rawToggle = !$rawToggle;
1160
					break;
1161
				case 'xr':
1162
					$roman = true;
1163
					break;
1164
				case 'xh':
1165
					$hebrewNum = true;
1166
					break;
1167
				case 'xg':
1168
					$usedMonth = true;
1169
					$s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) );
1170
					break;
1171 View Code Duplication
				case 'xjx':
1172
					$usedHebrewMonth = true;
1173
					if ( !$hebrew ) {
1174
						$hebrew = self::tsToHebrew( $ts );
1175
					}
1176
					$s .= $this->getHebrewCalendarMonthNameGen( $hebrew[1] );
1177
					break;
1178
				case 'd':
1179
					$usedDay = true;
1180
					$num = substr( $ts, 6, 2 );
1181
					break;
1182
				case 'D':
1183
					$usedDay = true;
1184
					$s .= $this->getWeekdayAbbreviation(
1185
						Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1
1186
					);
1187
					break;
1188
				case 'j':
1189
					$usedDay = true;
1190
					$num = intval( substr( $ts, 6, 2 ) );
1191
					break;
1192
				case 'xij':
1193
					$usedDay = true;
1194
					if ( !$iranian ) {
1195
						$iranian = self::tsToIranian( $ts );
1196
					}
1197
					$num = $iranian[2];
1198
					break;
1199
				case 'xmj':
1200
					$usedDay = true;
1201
					if ( !$hijri ) {
1202
						$hijri = self::tsToHijri( $ts );
1203
					}
1204
					$num = $hijri[2];
1205
					break;
1206
				case 'xjj':
1207
					$usedDay = true;
1208
					if ( !$hebrew ) {
1209
						$hebrew = self::tsToHebrew( $ts );
1210
					}
1211
					$num = $hebrew[2];
1212
					break;
1213
				case 'l':
1214
					$usedDay = true;
1215
					$s .= $this->getWeekdayName(
1216
						Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'w' ) + 1
1217
					);
1218
					break;
1219
				case 'F':
1220
					$usedMonth = true;
1221
					$s .= $this->getMonthName( substr( $ts, 4, 2 ) );
1222
					break;
1223 View Code Duplication
				case 'xiF':
1224
					$usedIranianMonth = true;
1225
					if ( !$iranian ) {
1226
						$iranian = self::tsToIranian( $ts );
1227
					}
1228
					$s .= $this->getIranianCalendarMonthName( $iranian[1] );
1229
					break;
1230 View Code Duplication
				case 'xmF':
1231
					$usedHijriMonth = true;
1232
					if ( !$hijri ) {
1233
						$hijri = self::tsToHijri( $ts );
1234
					}
1235
					$s .= $this->getHijriCalendarMonthName( $hijri[1] );
1236
					break;
1237 View Code Duplication
				case 'xjF':
1238
					$usedHebrewMonth = true;
1239
					if ( !$hebrew ) {
1240
						$hebrew = self::tsToHebrew( $ts );
1241
					}
1242
					$s .= $this->getHebrewCalendarMonthName( $hebrew[1] );
1243
					break;
1244
				case 'm':
1245
					$usedMonth = true;
1246
					$num = substr( $ts, 4, 2 );
1247
					break;
1248
				case 'M':
1249
					$usedMonth = true;
1250
					$s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) );
1251
					break;
1252
				case 'n':
1253
					$usedMonth = true;
1254
					$num = intval( substr( $ts, 4, 2 ) );
1255
					break;
1256 View Code Duplication
				case 'xin':
1257
					$usedIranianMonth = true;
1258
					if ( !$iranian ) {
1259
						$iranian = self::tsToIranian( $ts );
1260
					}
1261
					$num = $iranian[1];
1262
					break;
1263 View Code Duplication
				case 'xmn':
1264
					$usedHijriMonth = true;
1265
					if ( !$hijri ) {
1266
						$hijri = self::tsToHijri( $ts );
1267
					}
1268
					$num = $hijri[1];
1269
					break;
1270 View Code Duplication
				case 'xjn':
1271
					$usedHebrewMonth = true;
1272
					if ( !$hebrew ) {
1273
						$hebrew = self::tsToHebrew( $ts );
1274
					}
1275
					$num = $hebrew[1];
1276
					break;
1277 View Code Duplication
				case 'xjt':
1278
					$usedHebrewMonth = true;
1279
					if ( !$hebrew ) {
1280
						$hebrew = self::tsToHebrew( $ts );
1281
					}
1282
					$num = $hebrew[3];
1283
					break;
1284
				case 'Y':
1285
					$usedYear = true;
1286
					$num = substr( $ts, 0, 4 );
1287
					break;
1288 View Code Duplication
				case 'xiY':
1289
					$usedIranianYear = true;
1290
					if ( !$iranian ) {
1291
						$iranian = self::tsToIranian( $ts );
1292
					}
1293
					$num = $iranian[0];
1294
					break;
1295
				case 'xmY':
1296
					$usedHijriYear = true;
1297
					if ( !$hijri ) {
1298
						$hijri = self::tsToHijri( $ts );
1299
					}
1300
					$num = $hijri[0];
1301
					break;
1302
				case 'xjY':
1303
					$usedHebrewYear = true;
1304
					if ( !$hebrew ) {
1305
						$hebrew = self::tsToHebrew( $ts );
1306
					}
1307
					$num = $hebrew[0];
1308
					break;
1309 View Code Duplication
				case 'xkY':
1310
					$usedYear = true;
1311
					if ( !$thai ) {
1312
						$thai = self::tsToYear( $ts, 'thai' );
1313
					}
1314
					$num = $thai[0];
1315
					break;
1316 View Code Duplication
				case 'xoY':
1317
					$usedYear = true;
1318
					if ( !$minguo ) {
1319
						$minguo = self::tsToYear( $ts, 'minguo' );
1320
					}
1321
					$num = $minguo[0];
1322
					break;
1323
				case 'xtY':
1324
					$usedTennoYear = true;
1325
					if ( !$tenno ) {
1326
						$tenno = self::tsToYear( $ts, 'tenno' );
1327
					}
1328
					$num = $tenno[0];
1329
					break;
1330
				case 'y':
1331
					$usedYear = true;
1332
					$num = substr( $ts, 2, 2 );
1333
					break;
1334 View Code Duplication
				case 'xiy':
1335
					$usedIranianYear = true;
1336
					if ( !$iranian ) {
1337
						$iranian = self::tsToIranian( $ts );
1338
					}
1339
					$num = substr( $iranian[0], -2 );
1340
					break;
1341 View Code Duplication
				case 'a':
1342
					$usedAMPM = true;
1343
					$s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm';
1344
					break;
1345 View Code Duplication
				case 'A':
1346
					$usedAMPM = true;
1347
					$s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'AM' : 'PM';
1348
					break;
1349 View Code Duplication
				case 'g':
1350
					$usedHour = true;
1351
					$h = substr( $ts, 8, 2 );
1352
					$num = $h % 12 ? $h % 12 : 12;
1353
					break;
1354
				case 'G':
1355
					$usedHour = true;
1356
					$num = intval( substr( $ts, 8, 2 ) );
1357
					break;
1358 View Code Duplication
				case 'h':
1359
					$usedHour = true;
1360
					$h = substr( $ts, 8, 2 );
1361
					$num = sprintf( '%02d', $h % 12 ? $h % 12 : 12 );
1362
					break;
1363
				case 'H':
1364
					$usedHour = true;
1365
					$num = substr( $ts, 8, 2 );
1366
					break;
1367
				case 'i':
1368
					$usedMinute = true;
1369
					$num = substr( $ts, 10, 2 );
1370
					break;
1371
				case 's':
1372
					$usedSecond = true;
1373
					$num = substr( $ts, 12, 2 );
1374
					break;
1375
				case 'c':
1376
				case 'r':
1377
					$usedSecond = true;
1378
					// fall through
1379
				case 'e':
1380
				case 'O':
1381
				case 'P':
1382
				case 'T':
1383
					$s .= Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1384
					break;
1385
				case 'w':
1386
				case 'N':
1387
				case 'z':
1388
					$usedDay = true;
1389
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1390
					break;
1391
				case 'W':
1392
					$usedWeek = true;
1393
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1394
					break;
1395
				case 't':
1396
					$usedMonth = true;
1397
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1398
					break;
1399
				case 'L':
1400
					$usedIsLeapYear = true;
1401
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1402
					break;
1403
				case 'o':
1404
					$usedISOYear = true;
1405
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1406
					break;
1407
				case 'U':
1408
					$usedSecond = true;
1409
					// fall through
1410
				case 'I':
1411
				case 'Z':
1412
					$num = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, $code );
1413
					break;
1414
				case '\\':
1415
					# Backslash escaping
1416
					if ( $p < $formatLength - 1 ) {
1417
						$s .= $format[++$p];
1418
					} else {
1419
						$s .= '\\';
1420
					}
1421
					break;
1422
				case '"':
1423
					# Quoted literal
1424
					if ( $p < $formatLength - 1 ) {
1425
						$endQuote = strpos( $format, '"', $p + 1 );
1426
						if ( $endQuote === false ) {
1427
							# No terminating quote, assume literal "
1428
							$s .= '"';
1429
						} else {
1430
							$s .= substr( $format, $p + 1, $endQuote - $p - 1 );
1431
							$p = $endQuote;
1432
						}
1433
					} else {
1434
						# Quote at end of string, assume literal "
1435
						$s .= '"';
1436
					}
1437
					break;
1438
				default:
1439
					$s .= $format[$p];
1440
			}
1441
			if ( $num !== false ) {
1442
				if ( $rawToggle || $raw ) {
1443
					$s .= $num;
1444
					$raw = false;
1445
				} elseif ( $roman ) {
1446
					$s .= Language::romanNumeral( $num );
1447
					$roman = false;
1448
				} elseif ( $hebrewNum ) {
1449
					$s .= self::hebrewNumeral( $num );
1450
					$hebrewNum = false;
1451
				} else {
1452
					$s .= $this->formatNum( $num, true );
1453
				}
1454
			}
1455
		}
1456
1457
		if ( $usedSecond ) {
1458
			$ttl = 1;
1459
		} elseif ( $usedMinute ) {
1460
			$ttl = 60 - substr( $ts, 12, 2 );
1461
		} elseif ( $usedHour ) {
1462
			$ttl = 3600 - substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1463
		} elseif ( $usedAMPM ) {
1464
			$ttl = 43200 - ( substr( $ts, 8, 2 ) % 12 ) * 3600 -
1465
				substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1466
		} elseif (
1467
			$usedDay ||
1468
			$usedHebrewMonth ||
1469
			$usedIranianMonth ||
1470
			$usedHijriMonth ||
1471
			$usedHebrewYear ||
1472
			$usedIranianYear ||
1473
			$usedHijriYear ||
1474
			$usedTennoYear
1475
		) {
1476
			// @todo Someone who understands the non-Gregorian calendars
1477
			// should write proper logic for them so that they don't need purged every day.
1478
			$ttl = 86400 - substr( $ts, 8, 2 ) * 3600 -
1479
				substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1480
		} else {
1481
			$possibleTtls = [];
1482
			$timeRemainingInDay = 86400 - substr( $ts, 8, 2 ) * 3600 -
1483
				substr( $ts, 10, 2 ) * 60 - substr( $ts, 12, 2 );
1484
			if ( $usedWeek ) {
1485
				$possibleTtls[] =
1486
					( 7 - Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400 +
1487
					$timeRemainingInDay;
1488
			} elseif ( $usedISOYear ) {
1489
				// December 28th falls on the last ISO week of the year, every year.
1490
				// The last ISO week of a year can be 52 or 53.
1491
				$lastWeekOfISOYear = DateTime::createFromFormat(
1492
					'Ymd',
1493
					substr( $ts, 0, 4 ) . '1228',
1494
					$zone ?: new DateTimeZone( 'UTC' )
1495
				)->format( 'W' );
1496
				$currentISOWeek = Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'W' );
1497
				$weeksRemaining = $lastWeekOfISOYear - $currentISOWeek;
1498
				$timeRemainingInWeek =
1499
					( 7 - Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'N' ) ) * 86400
1500
					+ $timeRemainingInDay;
1501
				$possibleTtls[] = $weeksRemaining * 604800 + $timeRemainingInWeek;
1502
			}
1503
1504
			if ( $usedMonth ) {
1505
				$possibleTtls[] =
1506
					( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 't' ) -
1507
						substr( $ts, 6, 2 ) ) * 86400
1508
					+ $timeRemainingInDay;
1509
			} elseif ( $usedYear ) {
1510
				$possibleTtls[] =
1511
					( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) + 364 -
1512
						Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400
1513
					+ $timeRemainingInDay;
1514
			} elseif ( $usedIsLeapYear ) {
1515
				$year = substr( $ts, 0, 4 );
1516
				$timeRemainingInYear =
1517
					( Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'L' ) + 364 -
1518
						Language::dateTimeObjFormat( $dateTimeObj, $ts, $zone, 'z' ) ) * 86400
1519
					+ $timeRemainingInDay;
1520
				$mod = $year % 4;
1521
				if ( $mod || ( !( $year % 100 ) && $year % 400 ) ) {
1522
					// this isn't a leap year. see when the next one starts
1523
					$nextCandidate = $year - $mod + 4;
1524
					if ( $nextCandidate % 100 || !( $nextCandidate % 400 ) ) {
1525
						$possibleTtls[] = ( $nextCandidate - $year - 1 ) * 365 * 86400 +
1526
							$timeRemainingInYear;
1527
					} else {
1528
						$possibleTtls[] = ( $nextCandidate - $year + 3 ) * 365 * 86400 +
1529
							$timeRemainingInYear;
1530
					}
1531
				} else {
1532
					// this is a leap year, so the next year isn't
1533
					$possibleTtls[] = $timeRemainingInYear;
1534
				}
1535
			}
1536
1537
			if ( $possibleTtls ) {
1538
				$ttl = min( $possibleTtls );
1539
			}
1540
		}
1541
1542
		return $s;
1543
	}
1544
1545
	private static $GREG_DAYS = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
1546
	private static $IRANIAN_DAYS = [ 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 ];
1547
1548
	/**
1549
	 * Algorithm by Roozbeh Pournader and Mohammad Toossi to convert
1550
	 * Gregorian dates to Iranian dates. Originally written in C, it
1551
	 * is released under the terms of GNU Lesser General Public
1552
	 * License. Conversion to PHP was performed by Niklas Laxström.
1553
	 *
1554
	 * Link: http://www.farsiweb.info/jalali/jalali.c
1555
	 *
1556
	 * @param string $ts
1557
	 *
1558
	 * @return int[]
1559
	 */
1560
	private static function tsToIranian( $ts ) {
1561
		$gy = substr( $ts, 0, 4 ) -1600;
1562
		$gm = substr( $ts, 4, 2 ) -1;
1563
		$gd = substr( $ts, 6, 2 ) -1;
1564
1565
		# Days passed from the beginning (including leap years)
1566
		$gDayNo = 365 * $gy
1567
			+ floor( ( $gy + 3 ) / 4 )
1568
			- floor( ( $gy + 99 ) / 100 )
1569
			+ floor( ( $gy + 399 ) / 400 );
1570
1571
		// Add days of the past months of this year
1572
		for ( $i = 0; $i < $gm; $i++ ) {
1573
			$gDayNo += self::$GREG_DAYS[$i];
1574
		}
1575
1576
		// Leap years
1577
		if ( $gm > 1 && ( ( $gy % 4 === 0 && $gy % 100 !== 0 || ( $gy % 400 == 0 ) ) ) ) {
1578
			$gDayNo++;
1579
		}
1580
1581
		// Days passed in current month
1582
		$gDayNo += (int)$gd;
1583
1584
		$jDayNo = $gDayNo - 79;
1585
1586
		$jNp = floor( $jDayNo / 12053 );
1587
		$jDayNo %= 12053;
1588
1589
		$jy = 979 + 33 * $jNp + 4 * floor( $jDayNo / 1461 );
1590
		$jDayNo %= 1461;
1591
1592
		if ( $jDayNo >= 366 ) {
1593
			$jy += floor( ( $jDayNo - 1 ) / 365 );
1594
			$jDayNo = floor( ( $jDayNo - 1 ) % 365 );
1595
		}
1596
1597
		for ( $i = 0; $i < 11 && $jDayNo >= self::$IRANIAN_DAYS[$i]; $i++ ) {
1598
			$jDayNo -= self::$IRANIAN_DAYS[$i];
1599
		}
1600
1601
		$jm = $i + 1;
1602
		$jd = $jDayNo + 1;
1603
1604
		return [ $jy, $jm, $jd ];
1605
	}
1606
1607
	/**
1608
	 * Converting Gregorian dates to Hijri dates.
1609
	 *
1610
	 * Based on a PHP-Nuke block by Sharjeel which is released under GNU/GPL license
1611
	 *
1612
	 * @see http://phpnuke.org/modules.php?name=News&file=article&sid=8234&mode=thread&order=0&thold=0
1613
	 *
1614
	 * @param string $ts
1615
	 *
1616
	 * @return int[]
1617
	 */
1618
	private static function tsToHijri( $ts ) {
1619
		$year = substr( $ts, 0, 4 );
1620
		$month = substr( $ts, 4, 2 );
1621
		$day = substr( $ts, 6, 2 );
1622
1623
		$zyr = $year;
1624
		$zd = $day;
1625
		$zm = $month;
1626
		$zy = $zyr;
1627
1628
		if (
1629
			( $zy > 1582 ) || ( ( $zy == 1582 ) && ( $zm > 10 ) ) ||
1630
			( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) )
1631
		) {
1632
			$zjd = (int)( ( 1461 * ( $zy + 4800 + (int)( ( $zm - 14 ) / 12 ) ) ) / 4 ) +
1633
					(int)( ( 367 * ( $zm - 2 - 12 * ( (int)( ( $zm - 14 ) / 12 ) ) ) ) / 12 ) -
1634
					(int)( ( 3 * (int)( ( ( $zy + 4900 + (int)( ( $zm - 14 ) / 12 ) ) / 100 ) ) ) / 4 ) +
1635
					$zd - 32075;
1636
		} else {
1637
			$zjd = 367 * $zy - (int)( ( 7 * ( $zy + 5001 + (int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) +
1638
								(int)( ( 275 * $zm ) / 9 ) + $zd + 1729777;
1639
		}
1640
1641
		$zl = $zjd -1948440 + 10632;
1642
		$zn = (int)( ( $zl - 1 ) / 10631 );
1643
		$zl = $zl - 10631 * $zn + 354;
1644
		$zj = ( (int)( ( 10985 - $zl ) / 5316 ) ) * ( (int)( ( 50 * $zl ) / 17719 ) ) +
1645
			( (int)( $zl / 5670 ) ) * ( (int)( ( 43 * $zl ) / 15238 ) );
1646
		$zl = $zl - ( (int)( ( 30 - $zj ) / 15 ) ) * ( (int)( ( 17719 * $zj ) / 50 ) ) -
1647
			( (int)( $zj / 16 ) ) * ( (int)( ( 15238 * $zj ) / 43 ) ) + 29;
1648
		$zm = (int)( ( 24 * $zl ) / 709 );
1649
		$zd = $zl - (int)( ( 709 * $zm ) / 24 );
1650
		$zy = 30 * $zn + $zj - 30;
1651
1652
		return [ $zy, $zm, $zd ];
1653
	}
1654
1655
	/**
1656
	 * Converting Gregorian dates to Hebrew dates.
1657
	 *
1658
	 * Based on a JavaScript code by Abu Mami and Yisrael Hersch
1659
	 * ([email protected], http://www.kaluach.net), who permitted
1660
	 * to translate the relevant functions into PHP and release them under
1661
	 * GNU GPL.
1662
	 *
1663
	 * The months are counted from Tishrei = 1. In a leap year, Adar I is 13
1664
	 * and Adar II is 14. In a non-leap year, Adar is 6.
1665
	 *
1666
	 * @param string $ts
1667
	 *
1668
	 * @return int[]
1669
	 */
1670
	private static function tsToHebrew( $ts ) {
1671
		# Parse date
1672
		$year = substr( $ts, 0, 4 );
1673
		$month = substr( $ts, 4, 2 );
1674
		$day = substr( $ts, 6, 2 );
1675
1676
		# Calculate Hebrew year
1677
		$hebrewYear = $year + 3760;
1678
1679
		# Month number when September = 1, August = 12
1680
		$month += 4;
1681
		if ( $month > 12 ) {
1682
			# Next year
1683
			$month -= 12;
1684
			$year++;
1685
			$hebrewYear++;
1686
		}
1687
1688
		# Calculate day of year from 1 September
1689
		$dayOfYear = $day;
1690
		for ( $i = 1; $i < $month; $i++ ) {
1691
			if ( $i == 6 ) {
1692
				# February
1693
				$dayOfYear += 28;
1694
				# Check if the year is leap
1695 View Code Duplication
				if ( $year % 400 == 0 || ( $year % 4 == 0 && $year % 100 > 0 ) ) {
1696
					$dayOfYear++;
1697
				}
1698
			} elseif ( $i == 8 || $i == 10 || $i == 1 || $i == 3 ) {
1699
				$dayOfYear += 30;
1700
			} else {
1701
				$dayOfYear += 31;
1702
			}
1703
		}
1704
1705
		# Calculate the start of the Hebrew year
1706
		$start = self::hebrewYearStart( $hebrewYear );
1707
1708
		# Calculate next year's start
1709
		if ( $dayOfYear <= $start ) {
1710
			# Day is before the start of the year - it is the previous year
1711
			# Next year's start
1712
			$nextStart = $start;
1713
			# Previous year
1714
			$year--;
1715
			$hebrewYear--;
1716
			# Add days since previous year's 1 September
1717
			$dayOfYear += 365;
1718 View Code Duplication
			if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1719
				# Leap year
1720
				$dayOfYear++;
1721
			}
1722
			# Start of the new (previous) year
1723
			$start = self::hebrewYearStart( $hebrewYear );
1724
		} else {
1725
			# Next year's start
1726
			$nextStart = self::hebrewYearStart( $hebrewYear + 1 );
1727
		}
1728
1729
		# Calculate Hebrew day of year
1730
		$hebrewDayOfYear = $dayOfYear - $start;
1731
1732
		# Difference between year's days
1733
		$diff = $nextStart - $start;
1734
		# Add 12 (or 13 for leap years) days to ignore the difference between
1735
		# Hebrew and Gregorian year (353 at least vs. 365/6) - now the
1736
		# difference is only about the year type
1737
		if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1738
			$diff += 13;
1739
		} else {
1740
			$diff += 12;
1741
		}
1742
1743
		# Check the year pattern, and is leap year
1744
		# 0 means an incomplete year, 1 means a regular year, 2 means a complete year
1745
		# This is mod 30, to work on both leap years (which add 30 days of Adar I)
1746
		# and non-leap years
1747
		$yearPattern = $diff % 30;
1748
		# Check if leap year
1749
		$isLeap = $diff >= 30;
1750
1751
		# Calculate day in the month from number of day in the Hebrew year
1752
		# Don't check Adar - if the day is not in Adar, we will stop before;
1753
		# if it is in Adar, we will use it to check if it is Adar I or Adar II
1754
		$hebrewDay = $hebrewDayOfYear;
1755
		$hebrewMonth = 1;
1756
		$days = 0;
1757
		while ( $hebrewMonth <= 12 ) {
1758
			# Calculate days in this month
1759
			if ( $isLeap && $hebrewMonth == 6 ) {
1760
				# Adar in a leap year
1761
				if ( $isLeap ) {
1762
					# Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1763
					$days = 30;
1764
					if ( $hebrewDay <= $days ) {
1765
						# Day in Adar I
1766
						$hebrewMonth = 13;
1767
					} else {
1768
						# Subtract the days of Adar I
1769
						$hebrewDay -= $days;
1770
						# Try Adar II
1771
						$days = 29;
1772
						if ( $hebrewDay <= $days ) {
1773
							# Day in Adar II
1774
							$hebrewMonth = 14;
1775
						}
1776
					}
1777
				}
1778
			} elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
1779
				# Cheshvan in a complete year (otherwise as the rule below)
1780
				$days = 30;
1781
			} elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) {
1782
				# Kislev in an incomplete year (otherwise as the rule below)
1783
				$days = 29;
1784
			} else {
1785
				# Odd months have 30 days, even have 29
1786
				$days = 30 - ( $hebrewMonth - 1 ) % 2;
1787
			}
1788
			if ( $hebrewDay <= $days ) {
1789
				# In the current month
1790
				break;
1791
			} else {
1792
				# Subtract the days of the current month
1793
				$hebrewDay -= $days;
1794
				# Try in the next month
1795
				$hebrewMonth++;
1796
			}
1797
		}
1798
1799
		return [ $hebrewYear, $hebrewMonth, $hebrewDay, $days ];
1800
	}
1801
1802
	/**
1803
	 * This calculates the Hebrew year start, as days since 1 September.
1804
	 * Based on Carl Friedrich Gauss algorithm for finding Easter date.
1805
	 * Used for Hebrew date.
1806
	 *
1807
	 * @param int $year
1808
	 *
1809
	 * @return string
1810
	 */
1811
	private static function hebrewYearStart( $year ) {
1812
		$a = intval( ( 12 * ( $year - 1 ) + 17 ) % 19 );
1813
		$b = intval( ( $year - 1 ) % 4 );
1814
		$m = 32.044093161144 + 1.5542417966212 * $a + $b / 4.0 - 0.0031777940220923 * ( $year - 1 );
1815
		if ( $m < 0 ) {
1816
			$m--;
1817
		}
1818
		$Mar = intval( $m );
1819
		if ( $m < 0 ) {
1820
			$m++;
1821
		}
1822
		$m -= $Mar;
1823
1824
		$c = intval( ( $Mar + 3 * ( $year - 1 ) + 5 * $b + 5 ) % 7 );
1825
		if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1826
			$Mar++;
1827
		} elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1828
			$Mar += 2;
1829
		} elseif ( $c == 2 || $c == 4 || $c == 6 ) {
1830
			$Mar++;
1831
		}
1832
1833
		$Mar += intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1834
		return $Mar;
1835
	}
1836
1837
	/**
1838
	 * Algorithm to convert Gregorian dates to Thai solar dates,
1839
	 * Minguo dates or Minguo dates.
1840
	 *
1841
	 * Link: http://en.wikipedia.org/wiki/Thai_solar_calendar
1842
	 *       http://en.wikipedia.org/wiki/Minguo_calendar
1843
	 *       http://en.wikipedia.org/wiki/Japanese_era_name
1844
	 *
1845
	 * @param string $ts 14-character timestamp
1846
	 * @param string $cName Calender name
1847
	 * @return array Converted year, month, day
1848
	 */
1849
	private static function tsToYear( $ts, $cName ) {
1850
		$gy = substr( $ts, 0, 4 );
1851
		$gm = substr( $ts, 4, 2 );
1852
		$gd = substr( $ts, 6, 2 );
1853
1854
		if ( !strcmp( $cName, 'thai' ) ) {
1855
			# Thai solar dates
1856
			# Add 543 years to the Gregorian calendar
1857
			# Months and days are identical
1858
			$gy_offset = $gy + 543;
1859
		} elseif ( ( !strcmp( $cName, 'minguo' ) ) || !strcmp( $cName, 'juche' ) ) {
1860
			# Minguo dates
1861
			# Deduct 1911 years from the Gregorian calendar
1862
			# Months and days are identical
1863
			$gy_offset = $gy - 1911;
1864
		} elseif ( !strcmp( $cName, 'tenno' ) ) {
1865
			# Nengō dates up to Meiji period
1866
			# Deduct years from the Gregorian calendar
1867
			# depending on the nengo periods
1868
			# Months and days are identical
1869
			if ( ( $gy < 1912 )
1870
				|| ( ( $gy == 1912 ) && ( $gm < 7 ) )
1871
				|| ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd < 31 ) )
1872
			) {
1873
				# Meiji period
1874
				$gy_gannen = $gy - 1868 + 1;
1875
				$gy_offset = $gy_gannen;
1876
				if ( $gy_gannen == 1 ) {
1877
					$gy_offset = '元';
1878
				}
1879
				$gy_offset = '明治' . $gy_offset;
1880
			} elseif (
1881
				( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd == 31 ) ) ||
1882
				( ( $gy == 1912 ) && ( $gm >= 8 ) ) ||
1883
				( ( $gy > 1912 ) && ( $gy < 1926 ) ) ||
1884
				( ( $gy == 1926 ) && ( $gm < 12 ) ) ||
1885
				( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd < 26 ) )
1886
			) {
1887
				# Taishō period
1888
				$gy_gannen = $gy - 1912 + 1;
1889
				$gy_offset = $gy_gannen;
1890
				if ( $gy_gannen == 1 ) {
1891
					$gy_offset = '元';
1892
				}
1893
				$gy_offset = '大正' . $gy_offset;
1894
			} elseif (
1895
				( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd >= 26 ) ) ||
1896
				( ( $gy > 1926 ) && ( $gy < 1989 ) ) ||
1897
				( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd < 8 ) )
1898
			) {
1899
				# Shōwa period
1900
				$gy_gannen = $gy - 1926 + 1;
1901
				$gy_offset = $gy_gannen;
1902
				if ( $gy_gannen == 1 ) {
1903
					$gy_offset = '元';
1904
				}
1905
				$gy_offset = '昭和' . $gy_offset;
1906
			} else {
1907
				# Heisei period
1908
				$gy_gannen = $gy - 1989 + 1;
1909
				$gy_offset = $gy_gannen;
1910
				if ( $gy_gannen == 1 ) {
1911
					$gy_offset = '元';
1912
				}
1913
				$gy_offset = '平成' . $gy_offset;
1914
			}
1915
		} else {
1916
			$gy_offset = $gy;
1917
		}
1918
1919
		return [ $gy_offset, $gm, $gd ];
1920
	}
1921
1922
	/**
1923
	 * Gets directionality of the first strongly directional codepoint, for embedBidi()
1924
	 *
1925
	 * This is the rule the BIDI algorithm uses to determine the directionality of
1926
	 * paragraphs ( http://unicode.org/reports/tr9/#The_Paragraph_Level ) and
1927
	 * FSI isolates ( http://unicode.org/reports/tr9/#Explicit_Directional_Isolates ).
1928
	 *
1929
	 * TODO: Does not handle BIDI control characters inside the text.
1930
	 * TODO: Does not handle unallocated characters.
1931
	 *
1932
	 * @param string $text Text to test
1933
	 * @return null|string Directionality ('ltr' or 'rtl') or null
1934
	 */
1935
	private static function strongDirFromContent( $text = '' ) {
1936
		if ( !preg_match( self::$strongDirRegex, $text, $matches ) ) {
1937
			return null;
1938
		}
1939
		if ( $matches[1] === '' ) {
1940
			return 'rtl';
1941
		}
1942
		return 'ltr';
1943
	}
1944
1945
	/**
1946
	 * Roman number formatting up to 10000
1947
	 *
1948
	 * @param int $num
1949
	 *
1950
	 * @return string
1951
	 */
1952
	static function romanNumeral( $num ) {
1953
		static $table = [
1954
			[ '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ],
1955
			[ '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ],
1956
			[ '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ],
1957
			[ '', 'M', 'MM', 'MMM', 'MMMM', 'MMMMM', 'MMMMMM', 'MMMMMMM',
1958
				'MMMMMMMM', 'MMMMMMMMM', 'MMMMMMMMMM' ]
1959
		];
1960
1961
		$num = intval( $num );
1962
		if ( $num > 10000 || $num <= 0 ) {
1963
			return $num;
1964
		}
1965
1966
		$s = '';
1967
		for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1968
			if ( $num >= $pow10 ) {
1969
				$s .= $table[$i][(int)floor( $num / $pow10 )];
1970
			}
1971
			$num = $num % $pow10;
1972
		}
1973
		return $s;
1974
	}
1975
1976
	/**
1977
	 * Hebrew Gematria number formatting up to 9999
1978
	 *
1979
	 * @param int $num
1980
	 *
1981
	 * @return string
1982
	 */
1983
	static function hebrewNumeral( $num ) {
1984
		static $table = [
1985
			[ '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ],
1986
			[ '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ],
1987
			[ '',
1988
				[ 'ק' ],
1989
				[ 'ר' ],
1990
				[ 'ש' ],
1991
				[ 'ת' ],
1992
				[ 'ת', 'ק' ],
1993
				[ 'ת', 'ר' ],
1994
				[ 'ת', 'ש' ],
1995
				[ 'ת', 'ת' ],
1996
				[ 'ת', 'ת', 'ק' ],
1997
				[ 'ת', 'ת', 'ר' ],
1998
			],
1999
			[ '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ]
2000
		];
2001
2002
		$num = intval( $num );
2003
		if ( $num > 9999 || $num <= 0 ) {
2004
			return $num;
2005
		}
2006
2007
		// Round thousands have special notations
2008
		if ( $num === 1000 ) {
2009
			return "א' אלף";
2010
		} elseif ( $num % 1000 === 0 ) {
2011
			return $table[0][$num / 1000] . "' אלפים";
2012
		}
2013
2014
		$letters = [];
2015
2016
		for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
2017
			if ( $num >= $pow10 ) {
2018
				if ( $num === 15 || $num === 16 ) {
2019
					$letters[] = $table[0][9];
2020
					$letters[] = $table[0][$num - 9];
2021
					$num = 0;
2022
				} else {
2023
					$letters = array_merge(
2024
						$letters,
2025
						(array)$table[$i][intval( $num / $pow10 )]
2026
					);
2027
2028
					if ( $pow10 === 1000 ) {
2029
						$letters[] = "'";
2030
					}
2031
				}
2032
			}
2033
2034
			$num = $num % $pow10;
2035
		}
2036
2037
		$preTransformLength = count( $letters );
2038
		if ( $preTransformLength === 1 ) {
2039
			// Add geresh (single quote) to one-letter numbers
2040
			$letters[] = "'";
2041
		} else {
2042
			$lastIndex = $preTransformLength - 1;
2043
			$letters[$lastIndex] = str_replace(
2044
				[ 'כ', 'מ', 'נ', 'פ', 'צ' ],
2045
				[ 'ך', 'ם', 'ן', 'ף', 'ץ' ],
2046
				$letters[$lastIndex]
2047
			);
2048
2049
			// Add gershayim (double quote) to multiple-letter numbers,
2050
			// but exclude numbers with only one letter after the thousands
2051
			// (1001-1009, 1020, 1030, 2001-2009, etc.)
2052
			if ( $letters[1] === "'" && $preTransformLength === 3 ) {
2053
				$letters[] = "'";
2054
			} else {
2055
				array_splice( $letters, -1, 0, '"' );
2056
			}
2057
		}
2058
2059
		return implode( $letters );
2060
	}
2061
2062
	/**
2063
	 * Used by date() and time() to adjust the time output.
2064
	 *
2065
	 * @param string $ts The time in date('YmdHis') format
2066
	 * @param mixed $tz Adjust the time by this amount (default false, mean we
2067
	 *   get user timecorrection setting)
2068
	 * @return int
2069
	 */
2070
	public function userAdjust( $ts, $tz = false ) {
2071
		global $wgUser, $wgLocalTZoffset;
2072
2073
		if ( $tz === false ) {
2074
			$tz = $wgUser->getOption( 'timecorrection' );
2075
		}
2076
2077
		$data = explode( '|', $tz, 3 );
2078
2079
		if ( $data[0] == 'ZoneInfo' ) {
2080
			MediaWiki\suppressWarnings();
2081
			$userTZ = timezone_open( $data[2] );
2082
			MediaWiki\restoreWarnings();
2083
			if ( $userTZ !== false ) {
2084
				$date = date_create( $ts, timezone_open( 'UTC' ) );
2085
				date_timezone_set( $date, $userTZ );
2086
				$date = date_format( $date, 'YmdHis' );
2087
				return $date;
2088
			}
2089
			# Unrecognized timezone, default to 'Offset' with the stored offset.
2090
			$data[0] = 'Offset';
2091
		}
2092
2093
		if ( $data[0] == 'System' || $tz == '' ) {
2094
			# Global offset in minutes.
2095
			$minDiff = $wgLocalTZoffset;
2096
		} elseif ( $data[0] == 'Offset' ) {
2097
			$minDiff = intval( $data[1] );
2098 View Code Duplication
		} else {
2099
			$data = explode( ':', $tz );
2100
			if ( count( $data ) == 2 ) {
2101
				$data[0] = intval( $data[0] );
2102
				$data[1] = intval( $data[1] );
2103
				$minDiff = abs( $data[0] ) * 60 + $data[1];
2104
				if ( $data[0] < 0 ) {
2105
					$minDiff = -$minDiff;
2106
				}
2107
			} else {
2108
				$minDiff = intval( $data[0] ) * 60;
2109
			}
2110
		}
2111
2112
		# No difference ? Return time unchanged
2113
		if ( 0 == $minDiff ) {
2114
			return $ts;
2115
		}
2116
2117
		MediaWiki\suppressWarnings(); // E_STRICT system time bitching
2118
		# Generate an adjusted date; take advantage of the fact that mktime
2119
		# will normalize out-of-range values so we don't have to split $minDiff
2120
		# into hours and minutes.
2121
		$t = mktime( (
2122
			(int)substr( $ts, 8, 2 ) ), # Hours
2123
			(int)substr( $ts, 10, 2 ) + $minDiff, # Minutes
2124
			(int)substr( $ts, 12, 2 ), # Seconds
2125
			(int)substr( $ts, 4, 2 ), # Month
2126
			(int)substr( $ts, 6, 2 ), # Day
2127
			(int)substr( $ts, 0, 4 ) ); # Year
2128
2129
		$date = date( 'YmdHis', $t );
2130
		MediaWiki\restoreWarnings();
2131
2132
		return $date;
2133
	}
2134
2135
	/**
2136
	 * This is meant to be used by time(), date(), and timeanddate() to get
2137
	 * the date preference they're supposed to use, it should be used in
2138
	 * all children.
2139
	 *
2140
	 *<code>
2141
	 * function timeanddate([...], $format = true) {
2142
	 * 	$datePreference = $this->dateFormat($format);
2143
	 * [...]
2144
	 * }
2145
	 *</code>
2146
	 *
2147
	 * @param int|string|bool $usePrefs If true, the user's preference is used
2148
	 *   if false, the site/language default is used
2149
	 *   if int/string, assumed to be a format.
2150
	 * @return string
2151
	 */
2152
	function dateFormat( $usePrefs = true ) {
2153
		global $wgUser;
2154
2155
		if ( is_bool( $usePrefs ) ) {
2156
			if ( $usePrefs ) {
2157
				$datePreference = $wgUser->getDatePreference();
2158
			} else {
2159
				$datePreference = (string)User::getDefaultOption( 'date' );
2160
			}
2161
		} else {
2162
			$datePreference = (string)$usePrefs;
2163
		}
2164
2165
		// return int
2166
		if ( $datePreference == '' ) {
2167
			return 'default';
2168
		}
2169
2170
		return $datePreference;
2171
	}
2172
2173
	/**
2174
	 * Get a format string for a given type and preference
2175
	 * @param string $type May be 'date', 'time', 'both', or 'pretty'.
2176
	 * @param string $pref The format name as it appears in Messages*.php under
2177
	 *  $datePreferences.
2178
	 *
2179
	 * @since 1.22 New type 'pretty' that provides a more readable timestamp format
2180
	 *
2181
	 * @return string
2182
	 */
2183
	function getDateFormatString( $type, $pref ) {
2184
		$wasDefault = false;
2185
		if ( $pref == 'default' ) {
2186
			$wasDefault = true;
2187
			$pref = $this->getDefaultDateFormat();
2188
		}
2189
2190
		if ( !isset( $this->dateFormatStrings[$type][$pref] ) ) {
2191
			$df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2192
2193
			if ( $type === 'pretty' && $df === null ) {
2194
				$df = $this->getDateFormatString( 'date', $pref );
0 ignored issues
show
Bug introduced by
It seems like $pref defined by $this->getDefaultDateFormat() on line 2187 can also be of type array; however, Language::getDateFormatString() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
2195
			}
2196
2197
			if ( !$wasDefault && $df === null ) {
2198
				$pref = $this->getDefaultDateFormat();
2199
				$df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
2200
			}
2201
2202
			$this->dateFormatStrings[$type][$pref] = $df;
2203
		}
2204
		return $this->dateFormatStrings[$type][$pref];
2205
	}
2206
2207
	/**
2208
	 * @param string $ts The time format which needs to be turned into a
2209
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2210
	 * @param bool $adj Whether to adjust the time output according to the
2211
	 *   user configured offset ($timecorrection)
2212
	 * @param mixed $format True to use user's date format preference
2213
	 * @param string|bool $timecorrection The time offset as returned by
2214
	 *   validateTimeZone() in Special:Preferences
2215
	 * @return string
2216
	 */
2217 View Code Duplication
	public function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
2218
		$ts = wfTimestamp( TS_MW, $ts );
2219
		if ( $adj ) {
2220
			$ts = $this->userAdjust( $ts, $timecorrection );
0 ignored issues
show
Security Bug introduced by
It seems like $ts defined by $this->userAdjust($ts, $timecorrection) on line 2220 can also be of type false; however, Language::userAdjust() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
2221
		}
2222
		$df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
2223
		return $this->sprintfDate( $df, $ts );
2224
	}
2225
2226
	/**
2227
	 * @param string $ts The time format which needs to be turned into a
2228
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2229
	 * @param bool $adj Whether to adjust the time output according to the
2230
	 *   user configured offset ($timecorrection)
2231
	 * @param mixed $format True to use user's date format preference
2232
	 * @param string|bool $timecorrection The time offset as returned by
2233
	 *   validateTimeZone() in Special:Preferences
2234
	 * @return string
2235
	 */
2236 View Code Duplication
	public function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
2237
		$ts = wfTimestamp( TS_MW, $ts );
2238
		if ( $adj ) {
2239
			$ts = $this->userAdjust( $ts, $timecorrection );
0 ignored issues
show
Security Bug introduced by
It seems like $ts defined by $this->userAdjust($ts, $timecorrection) on line 2239 can also be of type false; however, Language::userAdjust() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
2240
		}
2241
		$df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
2242
		return $this->sprintfDate( $df, $ts );
2243
	}
2244
2245
	/**
2246
	 * @param string $ts The time format which needs to be turned into a
2247
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2248
	 * @param bool $adj Whether to adjust the time output according to the
2249
	 *   user configured offset ($timecorrection)
2250
	 * @param mixed $format What format to return, if it's false output the
2251
	 *   default one (default true)
2252
	 * @param string|bool $timecorrection The time offset as returned by
2253
	 *   validateTimeZone() in Special:Preferences
2254
	 * @return string
2255
	 */
2256 View Code Duplication
	public function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false ) {
2257
		$ts = wfTimestamp( TS_MW, $ts );
2258
		if ( $adj ) {
2259
			$ts = $this->userAdjust( $ts, $timecorrection );
0 ignored issues
show
Security Bug introduced by
It seems like $ts defined by $this->userAdjust($ts, $timecorrection) on line 2259 can also be of type false; however, Language::userAdjust() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
2260
		}
2261
		$df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) );
2262
		return $this->sprintfDate( $df, $ts );
2263
	}
2264
2265
	/**
2266
	 * Takes a number of seconds and turns it into a text using values such as hours and minutes.
2267
	 *
2268
	 * @since 1.20
2269
	 *
2270
	 * @param int $seconds The amount of seconds.
2271
	 * @param array $chosenIntervals The intervals to enable.
2272
	 *
2273
	 * @return string
2274
	 */
2275
	public function formatDuration( $seconds, array $chosenIntervals = [] ) {
2276
		$intervals = $this->getDurationIntervals( $seconds, $chosenIntervals );
2277
2278
		$segments = [];
2279
2280 View Code Duplication
		foreach ( $intervals as $intervalName => $intervalValue ) {
2281
			// Messages: duration-seconds, duration-minutes, duration-hours, duration-days, duration-weeks,
2282
			// duration-years, duration-decades, duration-centuries, duration-millennia
2283
			$message = wfMessage( 'duration-' . $intervalName )->numParams( $intervalValue );
2284
			$segments[] = $message->inLanguage( $this )->escaped();
2285
		}
2286
2287
		return $this->listToText( $segments );
2288
	}
2289
2290
	/**
2291
	 * Takes a number of seconds and returns an array with a set of corresponding intervals.
2292
	 * For example 65 will be turned into array( minutes => 1, seconds => 5 ).
2293
	 *
2294
	 * @since 1.20
2295
	 *
2296
	 * @param int $seconds The amount of seconds.
2297
	 * @param array $chosenIntervals The intervals to enable.
2298
	 *
2299
	 * @return array
2300
	 */
2301
	public function getDurationIntervals( $seconds, array $chosenIntervals = [] ) {
2302 View Code Duplication
		if ( empty( $chosenIntervals ) ) {
2303
			$chosenIntervals = [
2304
				'millennia',
2305
				'centuries',
2306
				'decades',
2307
				'years',
2308
				'days',
2309
				'hours',
2310
				'minutes',
2311
				'seconds'
2312
			];
2313
		}
2314
2315
		$intervals = array_intersect_key( self::$durationIntervals, array_flip( $chosenIntervals ) );
2316
		$sortedNames = array_keys( $intervals );
2317
		$smallestInterval = array_pop( $sortedNames );
2318
2319
		$segments = [];
2320
2321
		foreach ( $intervals as $name => $length ) {
2322
			$value = floor( $seconds / $length );
2323
2324
			if ( $value > 0 || ( $name == $smallestInterval && empty( $segments ) ) ) {
2325
				$seconds -= $value * $length;
2326
				$segments[$name] = $value;
2327
			}
2328
		}
2329
2330
		return $segments;
2331
	}
2332
2333
	/**
2334
	 * Internal helper function for userDate(), userTime() and userTimeAndDate()
2335
	 *
2336
	 * @param string $type Can be 'date', 'time' or 'both'
2337
	 * @param string $ts The time format which needs to be turned into a
2338
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2339
	 * @param User $user User object used to get preferences for timezone and format
2340
	 * @param array $options Array, can contain the following keys:
2341
	 *   - 'timecorrection': time correction, can have the following values:
2342
	 *     - true: use user's preference
2343
	 *     - false: don't use time correction
2344
	 *     - int: value of time correction in minutes
2345
	 *   - 'format': format to use, can have the following values:
2346
	 *     - true: use user's preference
2347
	 *     - false: use default preference
2348
	 *     - string: format to use
2349
	 * @since 1.19
2350
	 * @return string
2351
	 */
2352
	private function internalUserTimeAndDate( $type, $ts, User $user, array $options ) {
2353
		$ts = wfTimestamp( TS_MW, $ts );
2354
		$options += [ 'timecorrection' => true, 'format' => true ];
2355
		if ( $options['timecorrection'] !== false ) {
2356
			if ( $options['timecorrection'] === true ) {
2357
				$offset = $user->getOption( 'timecorrection' );
2358
			} else {
2359
				$offset = $options['timecorrection'];
2360
			}
2361
			$ts = $this->userAdjust( $ts, $offset );
0 ignored issues
show
Security Bug introduced by
It seems like $ts defined by $this->userAdjust($ts, $offset) on line 2361 can also be of type false; however, Language::userAdjust() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
2362
		}
2363
		if ( $options['format'] === true ) {
2364
			$format = $user->getDatePreference();
2365
		} else {
2366
			$format = $options['format'];
2367
		}
2368
		$df = $this->getDateFormatString( $type, $this->dateFormat( $format ) );
2369
		return $this->sprintfDate( $df, $ts );
2370
	}
2371
2372
	/**
2373
	 * Get the formatted date for the given timestamp and formatted for
2374
	 * the given user.
2375
	 *
2376
	 * @param mixed $ts Mixed: the time format which needs to be turned into a
2377
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2378
	 * @param User $user User object used to get preferences for timezone and format
2379
	 * @param array $options Array, can contain the following keys:
2380
	 *   - 'timecorrection': time correction, can have the following values:
2381
	 *     - true: use user's preference
2382
	 *     - false: don't use time correction
2383
	 *     - int: value of time correction in minutes
2384
	 *   - 'format': format to use, can have the following values:
2385
	 *     - true: use user's preference
2386
	 *     - false: use default preference
2387
	 *     - string: format to use
2388
	 * @since 1.19
2389
	 * @return string
2390
	 */
2391
	public function userDate( $ts, User $user, array $options = [] ) {
2392
		return $this->internalUserTimeAndDate( 'date', $ts, $user, $options );
2393
	}
2394
2395
	/**
2396
	 * Get the formatted time for the given timestamp and formatted for
2397
	 * the given user.
2398
	 *
2399
	 * @param mixed $ts The time format which needs to be turned into a
2400
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2401
	 * @param User $user User object used to get preferences for timezone and format
2402
	 * @param array $options Array, can contain the following keys:
2403
	 *   - 'timecorrection': time correction, can have the following values:
2404
	 *     - true: use user's preference
2405
	 *     - false: don't use time correction
2406
	 *     - int: value of time correction in minutes
2407
	 *   - 'format': format to use, can have the following values:
2408
	 *     - true: use user's preference
2409
	 *     - false: use default preference
2410
	 *     - string: format to use
2411
	 * @since 1.19
2412
	 * @return string
2413
	 */
2414
	public function userTime( $ts, User $user, array $options = [] ) {
2415
		return $this->internalUserTimeAndDate( 'time', $ts, $user, $options );
2416
	}
2417
2418
	/**
2419
	 * Get the formatted date and time for the given timestamp and formatted for
2420
	 * the given user.
2421
	 *
2422
	 * @param mixed $ts The time format which needs to be turned into a
2423
	 *   date('YmdHis') format with wfTimestamp(TS_MW,$ts)
2424
	 * @param User $user User object used to get preferences for timezone and format
2425
	 * @param array $options Array, can contain the following keys:
2426
	 *   - 'timecorrection': time correction, can have the following values:
2427
	 *     - true: use user's preference
2428
	 *     - false: don't use time correction
2429
	 *     - int: value of time correction in minutes
2430
	 *   - 'format': format to use, can have the following values:
2431
	 *     - true: use user's preference
2432
	 *     - false: use default preference
2433
	 *     - string: format to use
2434
	 * @since 1.19
2435
	 * @return string
2436
	 */
2437
	public function userTimeAndDate( $ts, User $user, array $options = [] ) {
2438
		return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
2439
	}
2440
2441
	/**
2442
	 * Get the timestamp in a human-friendly relative format, e.g., "3 days ago".
2443
	 *
2444
	 * Determine the difference between the timestamp and the current time, and
2445
	 * generate a readable timestamp by returning "<N> <units> ago", where the
2446
	 * largest possible unit is used.
2447
	 *
2448
	 * @since 1.26 (Prior to 1.26 method existed but was not meant to be used directly)
2449
	 *
2450
	 * @param MWTimestamp $time
2451
	 * @param MWTimestamp|null $relativeTo The base timestamp to compare to (defaults to now)
2452
	 * @param User|null $user User the timestamp is being generated for
2453
	 *  (or null to use main context's user)
2454
	 * @return string Formatted timestamp
2455
	 */
2456
	public function getHumanTimestamp(
2457
		MWTimestamp $time, MWTimestamp $relativeTo = null, User $user = null
2458
	) {
2459
		if ( $relativeTo === null ) {
2460
			$relativeTo = new MWTimestamp();
2461
		}
2462
		if ( $user === null ) {
2463
			$user = RequestContext::getMain()->getUser();
2464
		}
2465
2466
		// Adjust for the user's timezone.
2467
		$offsetThis = $time->offsetForUser( $user );
2468
		$offsetRel = $relativeTo->offsetForUser( $user );
2469
2470
		$ts = '';
2471
		if ( Hooks::run( 'GetHumanTimestamp', [ &$ts, $time, $relativeTo, $user, $this ] ) ) {
2472
			$ts = $this->getHumanTimestampInternal( $time, $relativeTo, $user );
2473
		}
2474
2475
		// Reset the timezone on the objects.
2476
		$time->timestamp->sub( $offsetThis );
2477
		$relativeTo->timestamp->sub( $offsetRel );
2478
2479
		return $ts;
2480
	}
2481
2482
	/**
2483
	 * Convert an MWTimestamp into a pretty human-readable timestamp using
2484
	 * the given user preferences and relative base time.
2485
	 *
2486
	 * @see Language::getHumanTimestamp
2487
	 * @param MWTimestamp $ts Timestamp to prettify
2488
	 * @param MWTimestamp $relativeTo Base timestamp
2489
	 * @param User $user User preferences to use
2490
	 * @return string Human timestamp
2491
	 * @since 1.26
2492
	 */
2493
	private function getHumanTimestampInternal(
2494
		MWTimestamp $ts, MWTimestamp $relativeTo, User $user
2495
	) {
2496
		$diff = $ts->diff( $relativeTo );
2497
		$diffDay = (bool)( (int)$ts->timestamp->format( 'w' ) -
2498
			(int)$relativeTo->timestamp->format( 'w' ) );
2499
		$days = $diff->days ?: (int)$diffDay;
2500
		if ( $diff->invert || $days > 5
2501
			&& $ts->timestamp->format( 'Y' ) !== $relativeTo->timestamp->format( 'Y' )
2502
		) {
2503
			// Timestamps are in different years: use full timestamp
2504
			// Also do full timestamp for future dates
2505
			/**
2506
			 * @todo FIXME: Add better handling of future timestamps.
2507
			 */
2508
			$format = $this->getDateFormatString( 'both', $user->getDatePreference() ?: 'default' );
2509
			$ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
2510
		} elseif ( $days > 5 ) {
2511
			// Timestamps are in same year,  but more than 5 days ago: show day and month only.
2512
			$format = $this->getDateFormatString( 'pretty', $user->getDatePreference() ?: 'default' );
2513
			$ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
2514
		} elseif ( $days > 1 ) {
2515
			// Timestamp within the past week: show the day of the week and time
2516
			$format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2517
			$weekday = self::$mWeekdayMsgs[$ts->timestamp->format( 'w' )];
2518
			// Messages:
2519
			// sunday-at, monday-at, tuesday-at, wednesday-at, thursday-at, friday-at, saturday-at
2520
			$ts = wfMessage( "$weekday-at" )
2521
				->inLanguage( $this )
2522
				->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2523
				->text();
2524
		} elseif ( $days == 1 ) {
2525
			// Timestamp was yesterday: say 'yesterday' and the time.
2526
			$format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2527
			$ts = wfMessage( 'yesterday-at' )
2528
				->inLanguage( $this )
2529
				->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2530
				->text();
2531
		} elseif ( $diff->h > 1 || $diff->h == 1 && $diff->i > 30 ) {
2532
			// Timestamp was today, but more than 90 minutes ago: say 'today' and the time.
2533
			$format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
2534
			$ts = wfMessage( 'today-at' )
2535
				->inLanguage( $this )
2536
				->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
2537
				->text();
2538
2539
		// From here on in, the timestamp was soon enough ago so that we can simply say
2540
		// XX units ago, e.g., "2 hours ago" or "5 minutes ago"
2541
		} elseif ( $diff->h == 1 ) {
2542
			// Less than 90 minutes, but more than an hour ago.
2543
			$ts = wfMessage( 'hours-ago' )->inLanguage( $this )->numParams( 1 )->text();
2544
		} elseif ( $diff->i >= 1 ) {
2545
			// A few minutes ago.
2546
			$ts = wfMessage( 'minutes-ago' )->inLanguage( $this )->numParams( $diff->i )->text();
2547
		} elseif ( $diff->s >= 30 ) {
2548
			// Less than a minute, but more than 30 sec ago.
2549
			$ts = wfMessage( 'seconds-ago' )->inLanguage( $this )->numParams( $diff->s )->text();
2550
		} else {
2551
			// Less than 30 seconds ago.
2552
			$ts = wfMessage( 'just-now' )->text();
2553
		}
2554
2555
		return $ts;
2556
	}
2557
2558
	/**
2559
	 * @param string $key
2560
	 * @return array|null
2561
	 */
2562
	public function getMessage( $key ) {
2563
		return self::$dataCache->getSubitem( $this->mCode, 'messages', $key );
2564
	}
2565
2566
	/**
2567
	 * @return array
2568
	 */
2569
	function getAllMessages() {
2570
		return self::$dataCache->getItem( $this->mCode, 'messages' );
2571
	}
2572
2573
	/**
2574
	 * @param string $in
2575
	 * @param string $out
2576
	 * @param string $string
2577
	 * @return string
2578
	 */
2579
	public function iconv( $in, $out, $string ) {
2580
		# This is a wrapper for iconv in all languages except esperanto,
2581
		# which does some nasty x-conversions beforehand
2582
2583
		# Even with //IGNORE iconv can whine about illegal characters in
2584
		# *input* string. We just ignore those too.
2585
		# REF: http://bugs.php.net/bug.php?id=37166
2586
		# REF: https://phabricator.wikimedia.org/T18885
2587
		MediaWiki\suppressWarnings();
2588
		$text = iconv( $in, $out . '//IGNORE', $string );
2589
		MediaWiki\restoreWarnings();
2590
		return $text;
2591
	}
2592
2593
	// callback functions for ucwords(), ucwordbreaks()
2594
2595
	/**
2596
	 * @param array $matches
2597
	 * @return mixed|string
2598
	 */
2599
	function ucwordbreaksCallbackAscii( $matches ) {
2600
		return $this->ucfirst( $matches[1] );
2601
	}
2602
2603
	/**
2604
	 * @param array $matches
2605
	 * @return string
2606
	 */
2607
	function ucwordbreaksCallbackMB( $matches ) {
2608
		return mb_strtoupper( $matches[0] );
2609
	}
2610
2611
	/**
2612
	 * @param array $matches
2613
	 * @return string
2614
	 */
2615
	function ucwordsCallbackMB( $matches ) {
2616
		return mb_strtoupper( $matches[0] );
2617
	}
2618
2619
	/**
2620
	 * Make a string's first character uppercase
2621
	 *
2622
	 * @param string $str
2623
	 *
2624
	 * @return string
2625
	 */
2626
	public function ucfirst( $str ) {
2627
		$o = ord( $str );
2628
		if ( $o < 96 ) { // if already uppercase...
2629
			return $str;
2630
		} elseif ( $o < 128 ) {
2631
			return ucfirst( $str ); // use PHP's ucfirst()
2632
		} else {
2633
			// fall back to more complex logic in case of multibyte strings
2634
			return $this->uc( $str, true );
2635
		}
2636
	}
2637
2638
	/**
2639
	 * Convert a string to uppercase
2640
	 *
2641
	 * @param string $str
2642
	 * @param bool $first
2643
	 *
2644
	 * @return string
2645
	 */
2646
	public function uc( $str, $first = false ) {
2647
		if ( $first ) {
2648
			if ( $this->isMultibyte( $str ) ) {
2649
				return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2650
			} else {
2651
				return ucfirst( $str );
2652
			}
2653
		} else {
2654
			return $this->isMultibyte( $str ) ? mb_strtoupper( $str ) : strtoupper( $str );
2655
		}
2656
	}
2657
2658
	/**
2659
	 * @param string $str
2660
	 * @return mixed|string
2661
	 */
2662
	function lcfirst( $str ) {
2663
		$o = ord( $str );
2664
		if ( !$o ) {
2665
			return strval( $str );
2666
		} elseif ( $o >= 128 ) {
2667
			return $this->lc( $str, true );
2668
		} elseif ( $o > 96 ) {
2669
			return $str;
2670
		} else {
2671
			$str[0] = strtolower( $str[0] );
2672
			return $str;
2673
		}
2674
	}
2675
2676
	/**
2677
	 * @param string $str
2678
	 * @param bool $first
2679
	 * @return mixed|string
2680
	 */
2681
	function lc( $str, $first = false ) {
2682
		if ( $first ) {
2683
			if ( $this->isMultibyte( $str ) ) {
2684
				return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
2685
			} else {
2686
				return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
2687
			}
2688
		} else {
2689
			return $this->isMultibyte( $str ) ? mb_strtolower( $str ) : strtolower( $str );
2690
		}
2691
	}
2692
2693
	/**
2694
	 * @param string $str
2695
	 * @return bool
2696
	 */
2697
	function isMultibyte( $str ) {
2698
		return strlen( $str ) !== mb_strlen( $str );
2699
	}
2700
2701
	/**
2702
	 * @param string $str
2703
	 * @return mixed|string
2704
	 */
2705
	function ucwords( $str ) {
2706
		if ( $this->isMultibyte( $str ) ) {
2707
			$str = $this->lc( $str );
2708
2709
			// regexp to find first letter in each word (i.e. after each space)
2710
			$replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2711
2712
			// function to use to capitalize a single char
2713
			return preg_replace_callback(
2714
				$replaceRegexp,
2715
				[ $this, 'ucwordsCallbackMB' ],
2716
				$str
2717
			);
2718
		} else {
2719
			return ucwords( strtolower( $str ) );
2720
		}
2721
	}
2722
2723
	/**
2724
	 * capitalize words at word breaks
2725
	 *
2726
	 * @param string $str
2727
	 * @return mixed
2728
	 */
2729
	function ucwordbreaks( $str ) {
2730
		if ( $this->isMultibyte( $str ) ) {
2731
			$str = $this->lc( $str );
2732
2733
			// since \b doesn't work for UTF-8, we explicitely define word break chars
2734
			$breaks = "[ \-\(\)\}\{\.,\?!]";
2735
2736
			// find first letter after word break
2737
			$replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|" .
2738
				"$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2739
2740
			return preg_replace_callback(
2741
				$replaceRegexp,
2742
				[ $this, 'ucwordbreaksCallbackMB' ],
2743
				$str
2744
			);
2745
		} else {
2746
			return preg_replace_callback(
2747
				'/\b([\w\x80-\xff]+)\b/',
2748
				[ $this, 'ucwordbreaksCallbackAscii' ],
2749
				$str
2750
			);
2751
		}
2752
	}
2753
2754
	/**
2755
	 * Return a case-folded representation of $s
2756
	 *
2757
	 * This is a representation such that caseFold($s1)==caseFold($s2) if $s1
2758
	 * and $s2 are the same except for the case of their characters. It is not
2759
	 * necessary for the value returned to make sense when displayed.
2760
	 *
2761
	 * Do *not* perform any other normalisation in this function. If a caller
2762
	 * uses this function when it should be using a more general normalisation
2763
	 * function, then fix the caller.
2764
	 *
2765
	 * @param string $s
2766
	 *
2767
	 * @return string
2768
	 */
2769
	function caseFold( $s ) {
2770
		return $this->uc( $s );
2771
	}
2772
2773
	/**
2774
	 * @param string $s
2775
	 * @return string
2776
	 * @throws MWException
2777
	 */
2778
	function checkTitleEncoding( $s ) {
2779
		if ( is_array( $s ) ) {
2780
			throw new MWException( 'Given array to checkTitleEncoding.' );
2781
		}
2782
		if ( StringUtils::isUtf8( $s ) ) {
2783
			return $s;
2784
		}
2785
2786
		return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s );
2787
	}
2788
2789
	/**
2790
	 * @return array
2791
	 */
2792
	function fallback8bitEncoding() {
2793
		return self::$dataCache->getItem( $this->mCode, 'fallback8bitEncoding' );
2794
	}
2795
2796
	/**
2797
	 * Most writing systems use whitespace to break up words.
2798
	 * Some languages such as Chinese don't conventionally do this,
2799
	 * which requires special handling when breaking up words for
2800
	 * searching etc.
2801
	 *
2802
	 * @return bool
2803
	 */
2804
	function hasWordBreaks() {
2805
		return true;
2806
	}
2807
2808
	/**
2809
	 * Some languages such as Chinese require word segmentation,
2810
	 * Specify such segmentation when overridden in derived class.
2811
	 *
2812
	 * @param string $string
2813
	 * @return string
2814
	 */
2815
	function segmentByWord( $string ) {
2816
		return $string;
2817
	}
2818
2819
	/**
2820
	 * Some languages have special punctuation need to be normalized.
2821
	 * Make such changes here.
2822
	 *
2823
	 * @param string $string
2824
	 * @return string
2825
	 */
2826
	function normalizeForSearch( $string ) {
2827
		return self::convertDoubleWidth( $string );
2828
	}
2829
2830
	/**
2831
	 * convert double-width roman characters to single-width.
2832
	 * range: ff00-ff5f ~= 0020-007f
2833
	 *
2834
	 * @param string $string
2835
	 *
2836
	 * @return string
2837
	 */
2838
	protected static function convertDoubleWidth( $string ) {
2839
		static $full = null;
2840
		static $half = null;
2841
2842
		if ( $full === null ) {
2843
			$fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2844
			$halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2845
			$full = str_split( $fullWidth, 3 );
2846
			$half = str_split( $halfWidth );
2847
		}
2848
2849
		$string = str_replace( $full, $half, $string );
2850
		return $string;
2851
	}
2852
2853
	/**
2854
	 * @param string $string
2855
	 * @param string $pattern
2856
	 * @return string
2857
	 */
2858
	protected static function insertSpace( $string, $pattern ) {
2859
		$string = preg_replace( $pattern, " $1 ", $string );
2860
		$string = preg_replace( '/ +/', ' ', $string );
2861
		return $string;
2862
	}
2863
2864
	/**
2865
	 * @param array $termsArray
2866
	 * @return array
2867
	 */
2868
	function convertForSearchResult( $termsArray ) {
2869
		# some languages, e.g. Chinese, need to do a conversion
2870
		# in order for search results to be displayed correctly
2871
		return $termsArray;
2872
	}
2873
2874
	/**
2875
	 * Get the first character of a string.
2876
	 *
2877
	 * @param string $s
2878
	 * @return string
2879
	 */
2880
	function firstChar( $s ) {
2881
		$matches = [];
2882
		preg_match(
2883
			'/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
2884
				'[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/',
2885
			$s,
2886
			$matches
2887
		);
2888
2889
		if ( isset( $matches[1] ) ) {
2890
			if ( strlen( $matches[1] ) != 3 ) {
2891
				return $matches[1];
2892
			}
2893
2894
			// Break down Hangul syllables to grab the first jamo
2895
			$code = UtfNormal\Utils::utf8ToCodepoint( $matches[1] );
2896
			if ( $code < 0xac00 || 0xd7a4 <= $code ) {
2897
				return $matches[1];
2898
			} elseif ( $code < 0xb098 ) {
2899
				return "\xe3\x84\xb1";
2900
			} elseif ( $code < 0xb2e4 ) {
2901
				return "\xe3\x84\xb4";
2902
			} elseif ( $code < 0xb77c ) {
2903
				return "\xe3\x84\xb7";
2904
			} elseif ( $code < 0xb9c8 ) {
2905
				return "\xe3\x84\xb9";
2906
			} elseif ( $code < 0xbc14 ) {
2907
				return "\xe3\x85\x81";
2908
			} elseif ( $code < 0xc0ac ) {
2909
				return "\xe3\x85\x82";
2910
			} elseif ( $code < 0xc544 ) {
2911
				return "\xe3\x85\x85";
2912
			} elseif ( $code < 0xc790 ) {
2913
				return "\xe3\x85\x87";
2914
			} elseif ( $code < 0xcc28 ) {
2915
				return "\xe3\x85\x88";
2916
			} elseif ( $code < 0xce74 ) {
2917
				return "\xe3\x85\x8a";
2918
			} elseif ( $code < 0xd0c0 ) {
2919
				return "\xe3\x85\x8b";
2920
			} elseif ( $code < 0xd30c ) {
2921
				return "\xe3\x85\x8c";
2922
			} elseif ( $code < 0xd558 ) {
2923
				return "\xe3\x85\x8d";
2924
			} else {
2925
				return "\xe3\x85\x8e";
2926
			}
2927
		} else {
2928
			return '';
2929
		}
2930
	}
2931
2932
	function initEncoding() {
2933
		# Some languages may have an alternate char encoding option
2934
		# (Esperanto X-coding, Japanese furigana conversion, etc)
2935
		# If this language is used as the primary content language,
2936
		# an override to the defaults can be set here on startup.
2937
	}
2938
2939
	/**
2940
	 * @param string $s
2941
	 * @return string
2942
	 */
2943
	function recodeForEdit( $s ) {
2944
		# For some languages we'll want to explicitly specify
2945
		# which characters make it into the edit box raw
2946
		# or are converted in some way or another.
2947
		global $wgEditEncoding;
2948
		if ( $wgEditEncoding == '' || $wgEditEncoding == 'UTF-8' ) {
2949
			return $s;
2950
		} else {
2951
			return $this->iconv( 'UTF-8', $wgEditEncoding, $s );
2952
		}
2953
	}
2954
2955
	/**
2956
	 * @param string $s
2957
	 * @return string
2958
	 */
2959
	function recodeInput( $s ) {
2960
		# Take the previous into account.
2961
		global $wgEditEncoding;
2962
		if ( $wgEditEncoding != '' ) {
2963
			$enc = $wgEditEncoding;
2964
		} else {
2965
			$enc = 'UTF-8';
2966
		}
2967
		if ( $enc == 'UTF-8' ) {
2968
			return $s;
2969
		} else {
2970
			return $this->iconv( $enc, 'UTF-8', $s );
2971
		}
2972
	}
2973
2974
	/**
2975
	 * Convert a UTF-8 string to normal form C. In Malayalam and Arabic, this
2976
	 * also cleans up certain backwards-compatible sequences, converting them
2977
	 * to the modern Unicode equivalent.
2978
	 *
2979
	 * This is language-specific for performance reasons only.
2980
	 *
2981
	 * @param string $s
2982
	 *
2983
	 * @return string
2984
	 */
2985
	function normalize( $s ) {
2986
		global $wgAllUnicodeFixes;
2987
		$s = UtfNormal\Validator::cleanUp( $s );
2988
		if ( $wgAllUnicodeFixes ) {
2989
			$s = $this->transformUsingPairFile( 'normalize-ar.ser', $s );
2990
			$s = $this->transformUsingPairFile( 'normalize-ml.ser', $s );
2991
		}
2992
2993
		return $s;
2994
	}
2995
2996
	/**
2997
	 * Transform a string using serialized data stored in the given file (which
2998
	 * must be in the serialized subdirectory of $IP). The file contains pairs
2999
	 * mapping source characters to destination characters.
3000
	 *
3001
	 * The data is cached in process memory. This will go faster if you have the
3002
	 * FastStringSearch extension.
3003
	 *
3004
	 * @param string $file
3005
	 * @param string $string
3006
	 *
3007
	 * @throws MWException
3008
	 * @return string
3009
	 */
3010
	function transformUsingPairFile( $file, $string ) {
3011
		if ( !isset( $this->transformData[$file] ) ) {
3012
			$data = wfGetPrecompiledData( $file );
3013
			if ( $data === false ) {
3014
				throw new MWException( __METHOD__ . ": The transformation file $file is missing" );
3015
			}
3016
			$this->transformData[$file] = new ReplacementArray( $data );
3017
		}
3018
		return $this->transformData[$file]->replace( $string );
3019
	}
3020
3021
	/**
3022
	 * For right-to-left language support
3023
	 *
3024
	 * @return bool
3025
	 */
3026
	function isRTL() {
3027
		return self::$dataCache->getItem( $this->mCode, 'rtl' );
3028
	}
3029
3030
	/**
3031
	 * Return the correct HTML 'dir' attribute value for this language.
3032
	 * @return string
3033
	 */
3034
	function getDir() {
3035
		return $this->isRTL() ? 'rtl' : 'ltr';
3036
	}
3037
3038
	/**
3039
	 * Return 'left' or 'right' as appropriate alignment for line-start
3040
	 * for this language's text direction.
3041
	 *
3042
	 * Should be equivalent to CSS3 'start' text-align value....
3043
	 *
3044
	 * @return string
3045
	 */
3046
	function alignStart() {
3047
		return $this->isRTL() ? 'right' : 'left';
3048
	}
3049
3050
	/**
3051
	 * Return 'right' or 'left' as appropriate alignment for line-end
3052
	 * for this language's text direction.
3053
	 *
3054
	 * Should be equivalent to CSS3 'end' text-align value....
3055
	 *
3056
	 * @return string
3057
	 */
3058
	function alignEnd() {
3059
		return $this->isRTL() ? 'left' : 'right';
3060
	}
3061
3062
	/**
3063
	 * A hidden direction mark (LRM or RLM), depending on the language direction.
3064
	 * Unlike getDirMark(), this function returns the character as an HTML entity.
3065
	 * This function should be used when the output is guaranteed to be HTML,
3066
	 * because it makes the output HTML source code more readable. When
3067
	 * the output is plain text or can be escaped, getDirMark() should be used.
3068
	 *
3069
	 * @param bool $opposite Get the direction mark opposite to your language
3070
	 * @return string
3071
	 * @since 1.20
3072
	 */
3073
	function getDirMarkEntity( $opposite = false ) {
3074
		if ( $opposite ) {
3075
			return $this->isRTL() ? '&lrm;' : '&rlm;';
3076
		}
3077
		return $this->isRTL() ? '&rlm;' : '&lrm;';
3078
	}
3079
3080
	/**
3081
	 * A hidden direction mark (LRM or RLM), depending on the language direction.
3082
	 * This function produces them as invisible Unicode characters and
3083
	 * the output may be hard to read and debug, so it should only be used
3084
	 * when the output is plain text or can be escaped. When the output is
3085
	 * HTML, use getDirMarkEntity() instead.
3086
	 *
3087
	 * @param bool $opposite Get the direction mark opposite to your language
3088
	 * @return string
3089
	 */
3090
	function getDirMark( $opposite = false ) {
3091
		$lrm = "\xE2\x80\x8E"; # LEFT-TO-RIGHT MARK, commonly abbreviated LRM
3092
		$rlm = "\xE2\x80\x8F"; # RIGHT-TO-LEFT MARK, commonly abbreviated RLM
3093
		if ( $opposite ) {
3094
			return $this->isRTL() ? $lrm : $rlm;
3095
		}
3096
		return $this->isRTL() ? $rlm : $lrm;
3097
	}
3098
3099
	/**
3100
	 * @return array
3101
	 */
3102
	function capitalizeAllNouns() {
3103
		return self::$dataCache->getItem( $this->mCode, 'capitalizeAllNouns' );
3104
	}
3105
3106
	/**
3107
	 * An arrow, depending on the language direction.
3108
	 *
3109
	 * @param string $direction The direction of the arrow: forwards (default),
3110
	 *   backwards, left, right, up, down.
3111
	 * @return string
3112
	 */
3113
	function getArrow( $direction = 'forwards' ) {
3114
		switch ( $direction ) {
3115
		case 'forwards':
3116
			return $this->isRTL() ? '←' : '→';
3117
		case 'backwards':
3118
			return $this->isRTL() ? '→' : '←';
3119
		case 'left':
3120
			return '←';
3121
		case 'right':
3122
			return '→';
3123
		case 'up':
3124
			return '↑';
3125
		case 'down':
3126
			return '↓';
3127
		}
3128
	}
3129
3130
	/**
3131
	 * To allow "foo[[bar]]" to extend the link over the whole word "foobar"
3132
	 *
3133
	 * @return bool
3134
	 */
3135
	function linkPrefixExtension() {
3136
		return self::$dataCache->getItem( $this->mCode, 'linkPrefixExtension' );
3137
	}
3138
3139
	/**
3140
	 * Get all magic words from cache.
3141
	 * @return array
3142
	 */
3143
	function getMagicWords() {
3144
		return self::$dataCache->getItem( $this->mCode, 'magicWords' );
3145
	}
3146
3147
	/**
3148
	 * Run the LanguageGetMagic hook once.
3149
	 */
3150
	protected function doMagicHook() {
3151
		if ( $this->mMagicHookDone ) {
3152
			return;
3153
		}
3154
		$this->mMagicHookDone = true;
3155
		Hooks::run( 'LanguageGetMagic', [ &$this->mMagicExtensions, $this->getCode() ] );
3156
	}
3157
3158
	/**
3159
	 * Fill a MagicWord object with data from here
3160
	 *
3161
	 * @param MagicWord $mw
3162
	 */
3163
	function getMagic( $mw ) {
3164
		// Saves a function call
3165
		if ( !$this->mMagicHookDone ) {
3166
			$this->doMagicHook();
3167
		}
3168
3169
		if ( isset( $this->mMagicExtensions[$mw->mId] ) ) {
3170
			$rawEntry = $this->mMagicExtensions[$mw->mId];
3171
		} else {
3172
			$rawEntry = self::$dataCache->getSubitem(
3173
				$this->mCode, 'magicWords', $mw->mId );
3174
		}
3175
3176
		if ( !is_array( $rawEntry ) ) {
3177
			wfWarn( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
3178
		} else {
3179
			$mw->mCaseSensitive = $rawEntry[0];
3180
			$mw->mSynonyms = array_slice( $rawEntry, 1 );
3181
		}
3182
	}
3183
3184
	/**
3185
	 * Add magic words to the extension array
3186
	 *
3187
	 * @param array $newWords
3188
	 */
3189
	function addMagicWordsByLang( $newWords ) {
3190
		$fallbackChain = $this->getFallbackLanguages();
3191
		$fallbackChain = array_reverse( $fallbackChain );
3192
		foreach ( $fallbackChain as $code ) {
3193
			if ( isset( $newWords[$code] ) ) {
3194
				$this->mMagicExtensions = $newWords[$code] + $this->mMagicExtensions;
3195
			}
3196
		}
3197
	}
3198
3199
	/**
3200
	 * Get special page names, as an associative array
3201
	 *   canonical name => array of valid names, including aliases
3202
	 * @return array
3203
	 */
3204
	function getSpecialPageAliases() {
3205
		// Cache aliases because it may be slow to load them
3206
		if ( is_null( $this->mExtendedSpecialPageAliases ) ) {
3207
			// Initialise array
3208
			$this->mExtendedSpecialPageAliases =
3209
				self::$dataCache->getItem( $this->mCode, 'specialPageAliases' );
3210
			Hooks::run( 'LanguageGetSpecialPageAliases',
3211
				[ &$this->mExtendedSpecialPageAliases, $this->getCode() ] );
3212
		}
3213
3214
		return $this->mExtendedSpecialPageAliases;
3215
	}
3216
3217
	/**
3218
	 * Italic is unsuitable for some languages
3219
	 *
3220
	 * @param string $text The text to be emphasized.
3221
	 * @return string
3222
	 */
3223
	function emphasize( $text ) {
3224
		return "<em>$text</em>";
3225
	}
3226
3227
	/**
3228
	 * Normally we output all numbers in plain en_US style, that is
3229
	 * 293,291.235 for twohundredninetythreethousand-twohundredninetyone
3230
	 * point twohundredthirtyfive. However this is not suitable for all
3231
	 * languages, some such as Bengali (bn) want ২,৯৩,২৯১.২৩৫ and others such as
3232
	 * Icelandic just want to use commas instead of dots, and dots instead
3233
	 * of commas like "293.291,235".
3234
	 *
3235
	 * An example of this function being called:
3236
	 * <code>
3237
	 * wfMessage( 'message' )->numParams( $num )->text()
3238
	 * </code>
3239
	 *
3240
	 * See $separatorTransformTable on MessageIs.php for
3241
	 * the , => . and . => , implementation.
3242
	 *
3243
	 * @todo check if it's viable to use localeconv() for the decimal separator thing.
3244
	 * @param int|float $number The string to be formatted, should be an integer
3245
	 *   or a floating point number.
3246
	 * @param bool $nocommafy Set to true for special numbers like dates
3247
	 * @return string
3248
	 */
3249
	public function formatNum( $number, $nocommafy = false ) {
3250
		global $wgTranslateNumerals;
3251
		if ( !$nocommafy ) {
3252
			$number = $this->commafy( $number );
3253
			$s = $this->separatorTransformTable();
3254
			if ( $s ) {
3255
				$number = strtr( $number, $s );
3256
			}
3257
		}
3258
3259
		if ( $wgTranslateNumerals ) {
3260
			$s = $this->digitTransformTable();
3261
			if ( $s ) {
3262
				$number = strtr( $number, $s );
3263
			}
3264
		}
3265
3266
		return $number;
3267
	}
3268
3269
	/**
3270
	 * Front-end for non-commafied formatNum
3271
	 *
3272
	 * @param int|float $number The string to be formatted, should be an integer
3273
	 *        or a floating point number.
3274
	 * @since 1.21
3275
	 * @return string
3276
	 */
3277
	public function formatNumNoSeparators( $number ) {
3278
		return $this->formatNum( $number, true );
3279
	}
3280
3281
	/**
3282
	 * @param string $number
3283
	 * @return string
3284
	 */
3285
	public function parseFormattedNumber( $number ) {
3286
		$s = $this->digitTransformTable();
3287
		if ( $s ) {
3288
			// eliminate empty array values such as ''. (bug 64347)
3289
			$s = array_filter( $s );
3290
			$number = strtr( $number, array_flip( $s ) );
3291
		}
3292
3293
		$s = $this->separatorTransformTable();
3294
		if ( $s ) {
3295
			// eliminate empty array values such as ''. (bug 64347)
3296
			$s = array_filter( $s );
3297
			$number = strtr( $number, array_flip( $s ) );
3298
		}
3299
3300
		$number = strtr( $number, [ ',' => '' ] );
3301
		return $number;
3302
	}
3303
3304
	/**
3305
	 * Adds commas to a given number
3306
	 * @since 1.19
3307
	 * @param mixed $number
3308
	 * @return string
3309
	 */
3310
	function commafy( $number ) {
3311
		$digitGroupingPattern = $this->digitGroupingPattern();
3312
		if ( $number === null ) {
3313
			return '';
3314
		}
3315
3316
		if ( !$digitGroupingPattern || $digitGroupingPattern === "###,###,###" ) {
3317
			// default grouping is at thousands,  use the same for ###,###,### pattern too.
3318
			return strrev( (string)preg_replace( '/(\d{3})(?=\d)(?!\d*\.)/', '$1,', strrev( $number ) ) );
3319
		} else {
3320
			// Ref: http://cldr.unicode.org/translation/number-patterns
3321
			$sign = "";
3322
			if ( intval( $number ) < 0 ) {
3323
				// For negative numbers apply the algorithm like positive number and add sign.
3324
				$sign = "-";
3325
				$number = substr( $number, 1 );
3326
			}
3327
			$integerPart = [];
3328
			$decimalPart = [];
3329
			$numMatches = preg_match_all( "/(#+)/", $digitGroupingPattern, $matches );
3330
			preg_match( "/\d+/", $number, $integerPart );
3331
			preg_match( "/\.\d*/", $number, $decimalPart );
3332
			$groupedNumber = ( count( $decimalPart ) > 0 ) ? $decimalPart[0] : "";
3333
			if ( $groupedNumber === $number ) {
3334
				// the string does not have any number part. Eg: .12345
3335
				return $sign . $groupedNumber;
3336
			}
3337
			$start = $end = ( $integerPart ) ? strlen( $integerPart[0] ) : 0;
3338
			while ( $start > 0 ) {
3339
				$match = $matches[0][$numMatches - 1];
3340
				$matchLen = strlen( $match );
3341
				$start = $end - $matchLen;
3342
				if ( $start < 0 ) {
3343
					$start = 0;
3344
				}
3345
				$groupedNumber = substr( $number, $start, $end -$start ) . $groupedNumber;
3346
				$end = $start;
3347
				if ( $numMatches > 1 ) {
3348
					// use the last pattern for the rest of the number
3349
					$numMatches--;
3350
				}
3351
				if ( $start > 0 ) {
3352
					$groupedNumber = "," . $groupedNumber;
3353
				}
3354
			}
3355
			return $sign . $groupedNumber;
3356
		}
3357
	}
3358
3359
	/**
3360
	 * @return string
3361
	 */
3362
	function digitGroupingPattern() {
3363
		return self::$dataCache->getItem( $this->mCode, 'digitGroupingPattern' );
3364
	}
3365
3366
	/**
3367
	 * @return array
3368
	 */
3369
	function digitTransformTable() {
3370
		return self::$dataCache->getItem( $this->mCode, 'digitTransformTable' );
3371
	}
3372
3373
	/**
3374
	 * @return array
3375
	 */
3376
	function separatorTransformTable() {
3377
		return self::$dataCache->getItem( $this->mCode, 'separatorTransformTable' );
3378
	}
3379
3380
	/**
3381
	 * Take a list of strings and build a locale-friendly comma-separated
3382
	 * list, using the local comma-separator message.
3383
	 * The last two strings are chained with an "and".
3384
	 * NOTE: This function will only work with standard numeric array keys (0, 1, 2…)
3385
	 *
3386
	 * @param string[] $l
3387
	 * @return string
3388
	 */
3389
	function listToText( array $l ) {
3390
		$m = count( $l ) - 1;
3391
		if ( $m < 0 ) {
3392
			return '';
3393
		}
3394
		if ( $m > 0 ) {
3395
			$and = $this->msg( 'and' )->escaped();
3396
			$space = $this->msg( 'word-separator' )->escaped();
3397
			if ( $m > 1 ) {
3398
				$comma = $this->msg( 'comma-separator' )->escaped();
3399
			}
3400
		}
3401
		$s = $l[$m];
3402
		for ( $i = $m - 1; $i >= 0; $i-- ) {
3403
			if ( $i == $m - 1 ) {
3404
				$s = $l[$i] . $and . $space . $s;
0 ignored issues
show
Bug introduced by
The variable $and does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $space does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
3405
			} else {
3406
				$s = $l[$i] . $comma . $s;
0 ignored issues
show
Bug introduced by
The variable $comma does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
3407
			}
3408
		}
3409
		return $s;
3410
	}
3411
3412
	/**
3413
	 * Take a list of strings and build a locale-friendly comma-separated
3414
	 * list, using the local comma-separator message.
3415
	 * @param string[] $list Array of strings to put in a comma list
3416
	 * @return string
3417
	 */
3418
	function commaList( array $list ) {
3419
		return implode(
3420
			wfMessage( 'comma-separator' )->inLanguage( $this )->escaped(),
3421
			$list
3422
		);
3423
	}
3424
3425
	/**
3426
	 * Take a list of strings and build a locale-friendly semicolon-separated
3427
	 * list, using the local semicolon-separator message.
3428
	 * @param string[] $list Array of strings to put in a semicolon list
3429
	 * @return string
3430
	 */
3431
	function semicolonList( array $list ) {
3432
		return implode(
3433
			wfMessage( 'semicolon-separator' )->inLanguage( $this )->escaped(),
3434
			$list
3435
		);
3436
	}
3437
3438
	/**
3439
	 * Same as commaList, but separate it with the pipe instead.
3440
	 * @param string[] $list Array of strings to put in a pipe list
3441
	 * @return string
3442
	 */
3443
	function pipeList( array $list ) {
3444
		return implode(
3445
			wfMessage( 'pipe-separator' )->inLanguage( $this )->escaped(),
3446
			$list
3447
		);
3448
	}
3449
3450
	/**
3451
	 * Truncate a string to a specified length in bytes, appending an optional
3452
	 * string (e.g. for ellipses)
3453
	 *
3454
	 * The database offers limited byte lengths for some columns in the database;
3455
	 * multi-byte character sets mean we need to ensure that only whole characters
3456
	 * are included, otherwise broken characters can be passed to the user
3457
	 *
3458
	 * If $length is negative, the string will be truncated from the beginning
3459
	 *
3460
	 * @param string $string String to truncate
3461
	 * @param int $length Maximum length (including ellipses)
3462
	 * @param string $ellipsis String to append to the truncated text
3463
	 * @param bool $adjustLength Subtract length of ellipsis from $length.
3464
	 *	$adjustLength was introduced in 1.18, before that behaved as if false.
3465
	 * @return string
3466
	 */
3467
	function truncate( $string, $length, $ellipsis = '...', $adjustLength = true ) {
3468
		# Use the localized ellipsis character
3469
		if ( $ellipsis == '...' ) {
3470
			$ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3471
		}
3472
		# Check if there is no need to truncate
3473
		if ( $length == 0 ) {
3474
			return $ellipsis; // convention
3475
		} elseif ( strlen( $string ) <= abs( $length ) ) {
3476
			return $string; // no need to truncate
3477
		}
3478
		$stringOriginal = $string;
3479
		# If ellipsis length is >= $length then we can't apply $adjustLength
3480
		if ( $adjustLength && strlen( $ellipsis ) >= abs( $length ) ) {
3481
			$string = $ellipsis; // this can be slightly unexpected
3482
		# Otherwise, truncate and add ellipsis...
3483
		} else {
3484
			$eLength = $adjustLength ? strlen( $ellipsis ) : 0;
3485
			if ( $length > 0 ) {
3486
				$length -= $eLength;
3487
				$string = substr( $string, 0, $length ); // xyz...
3488
				$string = $this->removeBadCharLast( $string );
3489
				$string = rtrim( $string );
3490
				$string = $string . $ellipsis;
3491
			} else {
3492
				$length += $eLength;
3493
				$string = substr( $string, $length ); // ...xyz
3494
				$string = $this->removeBadCharFirst( $string );
3495
				$string = ltrim( $string );
3496
				$string = $ellipsis . $string;
3497
			}
3498
		}
3499
		# Do not truncate if the ellipsis makes the string longer/equal (bug 22181).
3500
		# This check is *not* redundant if $adjustLength, due to the single case where
3501
		# LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string.
3502
		if ( strlen( $string ) < strlen( $stringOriginal ) ) {
3503
			return $string;
3504
		} else {
3505
			return $stringOriginal;
3506
		}
3507
	}
3508
3509
	/**
3510
	 * Remove bytes that represent an incomplete Unicode character
3511
	 * at the end of string (e.g. bytes of the char are missing)
3512
	 *
3513
	 * @param string $string
3514
	 * @return string
3515
	 */
3516
	protected function removeBadCharLast( $string ) {
3517
		if ( $string != '' ) {
3518
			$char = ord( $string[strlen( $string ) - 1] );
3519
			$m = [];
3520
			if ( $char >= 0xc0 ) {
3521
				# We got the first byte only of a multibyte char; remove it.
3522
				$string = substr( $string, 0, -1 );
3523
			} elseif ( $char >= 0x80 &&
3524
				// Use the /s modifier (PCRE_DOTALL) so (.*) also matches newlines
3525
				preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
3526
					'[\xf0-\xf7][\x80-\xbf]{1,2})$/s', $string, $m )
3527
			) {
3528
				# We chopped in the middle of a character; remove it
3529
				$string = $m[1];
3530
			}
3531
		}
3532
		return $string;
3533
	}
3534
3535
	/**
3536
	 * Remove bytes that represent an incomplete Unicode character
3537
	 * at the start of string (e.g. bytes of the char are missing)
3538
	 *
3539
	 * @param string $string
3540
	 * @return string
3541
	 */
3542
	protected function removeBadCharFirst( $string ) {
3543
		if ( $string != '' ) {
3544
			$char = ord( $string[0] );
3545
			if ( $char >= 0x80 && $char < 0xc0 ) {
3546
				# We chopped in the middle of a character; remove the whole thing
3547
				$string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
3548
			}
3549
		}
3550
		return $string;
3551
	}
3552
3553
	/**
3554
	 * Truncate a string of valid HTML to a specified length in bytes,
3555
	 * appending an optional string (e.g. for ellipses), and return valid HTML
3556
	 *
3557
	 * This is only intended for styled/linked text, such as HTML with
3558
	 * tags like <span> and <a>, were the tags are self-contained (valid HTML).
3559
	 * Also, this will not detect things like "display:none" CSS.
3560
	 *
3561
	 * Note: since 1.18 you do not need to leave extra room in $length for ellipses.
3562
	 *
3563
	 * @param string $text HTML string to truncate
3564
	 * @param int $length (zero/positive) Maximum length (including ellipses)
3565
	 * @param string $ellipsis String to append to the truncated text
3566
	 * @return string
3567
	 */
3568
	function truncateHtml( $text, $length, $ellipsis = '...' ) {
3569
		# Use the localized ellipsis character
3570
		if ( $ellipsis == '...' ) {
3571
			$ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this )->escaped();
3572
		}
3573
		# Check if there is clearly no need to truncate
3574
		if ( $length <= 0 ) {
3575
			return $ellipsis; // no text shown, nothing to format (convention)
3576
		} elseif ( strlen( $text ) <= $length ) {
3577
			return $text; // string short enough even *with* HTML (short-circuit)
3578
		}
3579
3580
		$dispLen = 0; // innerHTML legth so far
3581
		$testingEllipsis = false; // checking if ellipses will make string longer/equal?
3582
		$tagType = 0; // 0-open, 1-close
3583
		$bracketState = 0; // 1-tag start, 2-tag name, 0-neither
3584
		$entityState = 0; // 0-not entity, 1-entity
3585
		$tag = $ret = ''; // accumulated tag name, accumulated result string
3586
		$openTags = []; // open tag stack
3587
		$maybeState = null; // possible truncation state
3588
3589
		$textLen = strlen( $text );
3590
		$neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated
3591
		for ( $pos = 0; true; ++$pos ) {
3592
			# Consider truncation once the display length has reached the maximim.
3593
			# We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
3594
			# Check that we're not in the middle of a bracket/entity...
3595
			if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) {
3596
				if ( !$testingEllipsis ) {
3597
					$testingEllipsis = true;
3598
					# Save where we are; we will truncate here unless there turn out to
3599
					# be so few remaining characters that truncation is not necessary.
3600
					if ( !$maybeState ) { // already saved? ($neLength = 0 case)
3601
						$maybeState = [ $ret, $openTags ]; // save state
3602
					}
3603
				} elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) {
3604
					# String in fact does need truncation, the truncation point was OK.
3605
					list( $ret, $openTags ) = $maybeState; // reload state
3606
					$ret = $this->removeBadCharLast( $ret ); // multi-byte char fix
3607
					$ret .= $ellipsis; // add ellipsis
3608
					break;
3609
				}
3610
			}
3611
			if ( $pos >= $textLen ) {
3612
				break; // extra iteration just for above checks
3613
			}
3614
3615
			# Read the next char...
3616
			$ch = $text[$pos];
3617
			$lastCh = $pos ? $text[$pos - 1] : '';
3618
			$ret .= $ch; // add to result string
3619
			if ( $ch == '<' ) {
3620
				$this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); // for bad HTML
3621
				$entityState = 0; // for bad HTML
3622
				$bracketState = 1; // tag started (checking for backslash)
3623
			} elseif ( $ch == '>' ) {
3624
				$this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags );
3625
				$entityState = 0; // for bad HTML
3626
				$bracketState = 0; // out of brackets
3627
			} elseif ( $bracketState == 1 ) {
3628
				if ( $ch == '/' ) {
3629
					$tagType = 1; // close tag (e.g. "</span>")
3630
				} else {
3631
					$tagType = 0; // open tag (e.g. "<span>")
3632
					$tag .= $ch;
3633
				}
3634
				$bracketState = 2; // building tag name
3635
			} elseif ( $bracketState == 2 ) {
3636
				if ( $ch != ' ' ) {
3637
					$tag .= $ch;
3638
				} else {
3639
					// Name found (e.g. "<a href=..."), add on tag attributes...
3640
					$pos += $this->truncate_skip( $ret, $text, "<>", $pos + 1 );
3641
				}
3642
			} elseif ( $bracketState == 0 ) {
3643
				if ( $entityState ) {
3644
					if ( $ch == ';' ) {
3645
						$entityState = 0;
3646
						$dispLen++; // entity is one displayed char
3647
					}
3648
				} else {
3649
					if ( $neLength == 0 && !$maybeState ) {
3650
						// Save state without $ch. We want to *hit* the first
3651
						// display char (to get tags) but not *use* it if truncating.
3652
						$maybeState = [ substr( $ret, 0, -1 ), $openTags ];
3653
					}
3654
					if ( $ch == '&' ) {
3655
						$entityState = 1; // entity found, (e.g. "&#160;")
3656
					} else {
3657
						$dispLen++; // this char is displayed
3658
						// Add the next $max display text chars after this in one swoop...
3659
						$max = ( $testingEllipsis ? $length : $neLength ) - $dispLen;
3660
						$skipped = $this->truncate_skip( $ret, $text, "<>&", $pos + 1, $max );
3661
						$dispLen += $skipped;
3662
						$pos += $skipped;
3663
					}
3664
				}
3665
			}
3666
		}
3667
		// Close the last tag if left unclosed by bad HTML
3668
		$this->truncate_endBracket( $tag, $text[$textLen - 1], $tagType, $openTags );
3669
		while ( count( $openTags ) > 0 ) {
3670
			$ret .= '</' . array_pop( $openTags ) . '>'; // close open tags
3671
		}
3672
		return $ret;
3673
	}
3674
3675
	/**
3676
	 * truncateHtml() helper function
3677
	 * like strcspn() but adds the skipped chars to $ret
3678
	 *
3679
	 * @param string $ret
3680
	 * @param string $text
3681
	 * @param string $search
3682
	 * @param int $start
3683
	 * @param null|int $len
3684
	 * @return int
3685
	 */
3686
	private function truncate_skip( &$ret, $text, $search, $start, $len = null ) {
3687
		if ( $len === null ) {
3688
			$len = -1; // -1 means "no limit" for strcspn
3689
		} elseif ( $len < 0 ) {
3690
			$len = 0; // sanity
3691
		}
3692
		$skipCount = 0;
3693
		if ( $start < strlen( $text ) ) {
3694
			$skipCount = strcspn( $text, $search, $start, $len );
3695
			$ret .= substr( $text, $start, $skipCount );
3696
		}
3697
		return $skipCount;
3698
	}
3699
3700
	/**
3701
	 * truncateHtml() helper function
3702
	 * (a) push or pop $tag from $openTags as needed
3703
	 * (b) clear $tag value
3704
	 * @param string &$tag Current HTML tag name we are looking at
3705
	 * @param int $tagType (0-open tag, 1-close tag)
3706
	 * @param string $lastCh Character before the '>' that ended this tag
3707
	 * @param array &$openTags Open tag stack (not accounting for $tag)
3708
	 */
3709
	private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) {
3710
		$tag = ltrim( $tag );
3711
		if ( $tag != '' ) {
3712
			if ( $tagType == 0 && $lastCh != '/' ) {
3713
				$openTags[] = $tag; // tag opened (didn't close itself)
3714
			} elseif ( $tagType == 1 ) {
3715
				if ( $openTags && $tag == $openTags[count( $openTags ) - 1] ) {
3716
					array_pop( $openTags ); // tag closed
3717
				}
3718
			}
3719
			$tag = '';
3720
		}
3721
	}
3722
3723
	/**
3724
	 * Grammatical transformations, needed for inflected languages
3725
	 * Invoked by putting {{grammar:case|word}} in a message
3726
	 *
3727
	 * @param string $word
3728
	 * @param string $case
3729
	 * @return string
3730
	 */
3731
	function convertGrammar( $word, $case ) {
3732
		global $wgGrammarForms;
3733 View Code Duplication
		if ( isset( $wgGrammarForms[$this->getCode()][$case][$word] ) ) {
3734
			return $wgGrammarForms[$this->getCode()][$case][$word];
3735
		}
3736
3737
		return $word;
3738
	}
3739
	/**
3740
	 * Get the grammar forms for the content language
3741
	 * @return array Array of grammar forms
3742
	 * @since 1.20
3743
	 */
3744
	function getGrammarForms() {
3745
		global $wgGrammarForms;
3746
		if ( isset( $wgGrammarForms[$this->getCode()] )
3747
			&& is_array( $wgGrammarForms[$this->getCode()] )
3748
		) {
3749
			return $wgGrammarForms[$this->getCode()];
3750
		}
3751
3752
		return [];
3753
	}
3754
	/**
3755
	 * Provides an alternative text depending on specified gender.
3756
	 * Usage {{gender:username|masculine|feminine|unknown}}.
3757
	 * username is optional, in which case the gender of current user is used,
3758
	 * but only in (some) interface messages; otherwise default gender is used.
3759
	 *
3760
	 * If no forms are given, an empty string is returned. If only one form is
3761
	 * given, it will be returned unconditionally. These details are implied by
3762
	 * the caller and cannot be overridden in subclasses.
3763
	 *
3764
	 * If three forms are given, the default is to use the third (unknown) form.
3765
	 * If fewer than three forms are given, the default is to use the first (masculine) form.
3766
	 * These details can be overridden in subclasses.
3767
	 *
3768
	 * @param string $gender
3769
	 * @param array $forms
3770
	 *
3771
	 * @return string
3772
	 */
3773
	function gender( $gender, $forms ) {
3774
		if ( !count( $forms ) ) {
3775
			return '';
3776
		}
3777
		$forms = $this->preConvertPlural( $forms, 2 );
3778
		if ( $gender === 'male' ) {
3779
			return $forms[0];
3780
		}
3781
		if ( $gender === 'female' ) {
3782
			return $forms[1];
3783
		}
3784
		return isset( $forms[2] ) ? $forms[2] : $forms[0];
3785
	}
3786
3787
	/**
3788
	 * Plural form transformations, needed for some languages.
3789
	 * For example, there are 3 form of plural in Russian and Polish,
3790
	 * depending on "count mod 10". See [[w:Plural]]
3791
	 * For English it is pretty simple.
3792
	 *
3793
	 * Invoked by putting {{plural:count|wordform1|wordform2}}
3794
	 * or {{plural:count|wordform1|wordform2|wordform3}}
3795
	 *
3796
	 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
3797
	 *
3798
	 * @param int $count Non-localized number
3799
	 * @param array $forms Different plural forms
3800
	 * @return string Correct form of plural for $count in this language
3801
	 */
3802
	function convertPlural( $count, $forms ) {
3803
		// Handle explicit n=pluralform cases
3804
		$forms = $this->handleExplicitPluralForms( $count, $forms );
3805
		if ( is_string( $forms ) ) {
3806
			return $forms;
3807
		}
3808
		if ( !count( $forms ) ) {
3809
			return '';
3810
		}
3811
3812
		$pluralForm = $this->getPluralRuleIndexNumber( $count );
3813
		$pluralForm = min( $pluralForm, count( $forms ) - 1 );
3814
		return $forms[$pluralForm];
3815
	}
3816
3817
	/**
3818
	 * Handles explicit plural forms for Language::convertPlural()
3819
	 *
3820
	 * In {{PLURAL:$1|0=nothing|one|many}}, 0=nothing will be returned if $1 equals zero.
3821
	 * If an explicitly defined plural form matches the $count, then
3822
	 * string value returned, otherwise array returned for further consideration
3823
	 * by CLDR rules or overridden convertPlural().
3824
	 *
3825
	 * @since 1.23
3826
	 *
3827
	 * @param int $count Non-localized number
3828
	 * @param array $forms Different plural forms
3829
	 *
3830
	 * @return array|string
3831
	 */
3832
	protected function handleExplicitPluralForms( $count, array $forms ) {
3833
		foreach ( $forms as $index => $form ) {
3834
			if ( preg_match( '/\d+=/i', $form ) ) {
3835
				$pos = strpos( $form, '=' );
3836
				if ( substr( $form, 0, $pos ) === (string)$count ) {
3837
					return substr( $form, $pos + 1 );
3838
				}
3839
				unset( $forms[$index] );
3840
			}
3841
		}
3842
		return array_values( $forms );
3843
	}
3844
3845
	/**
3846
	 * Checks that convertPlural was given an array and pads it to requested
3847
	 * amount of forms by copying the last one.
3848
	 *
3849
	 * @param array $forms Array of forms given to convertPlural
3850
	 * @param int $count How many forms should there be at least
3851
	 * @return array Padded array of forms or an exception if not an array
3852
	 */
3853
	protected function preConvertPlural( /* Array */ $forms, $count ) {
3854
		while ( count( $forms ) < $count ) {
3855
			$forms[] = $forms[count( $forms ) - 1];
3856
		}
3857
		return $forms;
3858
	}
3859
3860
	/**
3861
	 * Wraps argument with unicode control characters for directionality safety
3862
	 *
3863
	 * This solves the problem where directionality-neutral characters at the edge of
3864
	 * the argument string get interpreted with the wrong directionality from the
3865
	 * enclosing context, giving renderings that look corrupted like "(Ben_(WMF".
3866
	 *
3867
	 * The wrapping is LRE...PDF or RLE...PDF, depending on the detected
3868
	 * directionality of the argument string, using the BIDI algorithm's own "First
3869
	 * strong directional codepoint" rule. Essentially, this works round the fact that
3870
	 * there is no embedding equivalent of U+2068 FSI (isolation with heuristic
3871
	 * direction inference). The latter is cleaner but still not widely supported.
3872
	 *
3873
	 * @param string $text Text to wrap
3874
	 * @return string Text, wrapped in LRE...PDF or RLE...PDF or nothing
3875
	 */
3876
	public function embedBidi( $text = '' ) {
3877
		$dir = Language::strongDirFromContent( $text );
3878
		if ( $dir === 'ltr' ) {
3879
			// Wrap in LEFT-TO-RIGHT EMBEDDING ... POP DIRECTIONAL FORMATTING
3880
			return self::$lre . $text . self::$pdf;
3881
		}
3882
		if ( $dir === 'rtl' ) {
3883
			// Wrap in RIGHT-TO-LEFT EMBEDDING ... POP DIRECTIONAL FORMATTING
3884
			return self::$rle . $text . self::$pdf;
3885
		}
3886
		// No strong directionality: do not wrap
3887
		return $text;
3888
	}
3889
3890
	/**
3891
	 * @todo Maybe translate block durations.  Note that this function is somewhat misnamed: it
3892
	 * deals with translating the *duration* ("1 week", "4 days", etc), not the expiry time
3893
	 * (which is an absolute timestamp). Please note: do NOT add this blindly, as it is used
3894
	 * on old expiry lengths recorded in log entries. You'd need to provide the start date to
3895
	 * match up with it.
3896
	 *
3897
	 * @param string $str The validated block duration in English
3898
	 * @param User $user User object to use timezone from or null for $wgUser
3899
	 * @return string Somehow translated block duration
3900
	 * @see LanguageFi.php for example implementation
3901
	 */
3902
	function translateBlockExpiry( $str, User $user = null ) {
3903
		$duration = SpecialBlock::getSuggestedDurations( $this );
3904
		foreach ( $duration as $show => $value ) {
3905
			if ( strcmp( $str, $value ) == 0 ) {
3906
				return htmlspecialchars( trim( $show ) );
3907
			}
3908
		}
3909
3910
		if ( wfIsInfinity( $str ) ) {
3911
			foreach ( $duration as $show => $value ) {
3912
				if ( wfIsInfinity( $value ) ) {
3913
					return htmlspecialchars( trim( $show ) );
3914
				}
3915
			}
3916
		}
3917
3918
		// If all else fails, return a standard duration or timestamp description.
3919
		$time = strtotime( $str, 0 );
3920
		if ( $time === false ) { // Unknown format. Return it as-is in case.
3921
			return $str;
3922
		} elseif ( $time !== strtotime( $str, 1 ) ) { // It's a relative timestamp.
3923
			// $time is relative to 0 so it's a duration length.
3924
			return $this->formatDuration( $time );
3925
		} else { // It's an absolute timestamp.
3926
			if ( $time === 0 ) {
3927
				// wfTimestamp() handles 0 as current time instead of epoch.
3928
				$time = '19700101000000';
3929
			}
3930
			if ( $user ) {
3931
				return $this->userTimeAndDate( $time, $user );
3932
			}
3933
			return $this->timeanddate( $time );
3934
		}
3935
	}
3936
3937
	/**
3938
	 * languages like Chinese need to be segmented in order for the diff
3939
	 * to be of any use
3940
	 *
3941
	 * @param string $text
3942
	 * @return string
3943
	 */
3944
	public function segmentForDiff( $text ) {
3945
		return $text;
3946
	}
3947
3948
	/**
3949
	 * and unsegment to show the result
3950
	 *
3951
	 * @param string $text
3952
	 * @return string
3953
	 */
3954
	public function unsegmentForDiff( $text ) {
3955
		return $text;
3956
	}
3957
3958
	/**
3959
	 * Return the LanguageConverter used in the Language
3960
	 *
3961
	 * @since 1.19
3962
	 * @return LanguageConverter
3963
	 */
3964
	public function getConverter() {
3965
		return $this->mConverter;
3966
	}
3967
3968
	/**
3969
	 * convert text to all supported variants
3970
	 *
3971
	 * @param string $text
3972
	 * @return array
3973
	 */
3974
	public function autoConvertToAllVariants( $text ) {
3975
		return $this->mConverter->autoConvertToAllVariants( $text );
3976
	}
3977
3978
	/**
3979
	 * convert text to different variants of a language.
3980
	 *
3981
	 * @param string $text
3982
	 * @return string
3983
	 */
3984
	public function convert( $text ) {
3985
		return $this->mConverter->convert( $text );
3986
	}
3987
3988
	/**
3989
	 * Convert a Title object to a string in the preferred variant
3990
	 *
3991
	 * @param Title $title
3992
	 * @return string
3993
	 */
3994
	public function convertTitle( $title ) {
3995
		return $this->mConverter->convertTitle( $title );
3996
	}
3997
3998
	/**
3999
	 * Convert a namespace index to a string in the preferred variant
4000
	 *
4001
	 * @param int $ns
4002
	 * @return string
4003
	 */
4004
	public function convertNamespace( $ns ) {
4005
		return $this->mConverter->convertNamespace( $ns );
4006
	}
4007
4008
	/**
4009
	 * Check if this is a language with variants
4010
	 *
4011
	 * @return bool
4012
	 */
4013
	public function hasVariants() {
4014
		return count( $this->getVariants() ) > 1;
4015
	}
4016
4017
	/**
4018
	 * Check if the language has the specific variant
4019
	 *
4020
	 * @since 1.19
4021
	 * @param string $variant
4022
	 * @return bool
4023
	 */
4024
	public function hasVariant( $variant ) {
4025
		return (bool)$this->mConverter->validateVariant( $variant );
4026
	}
4027
4028
	/**
4029
	 * Perform output conversion on a string, and encode for safe HTML output.
4030
	 * @param string $text Text to be converted
4031
	 * @param bool $isTitle Whether this conversion is for the article title
4032
	 * @return string
4033
	 * @todo this should get integrated somewhere sane
4034
	 */
4035
	public function convertHtml( $text, $isTitle = false ) {
4036
		return htmlspecialchars( $this->convert( $text, $isTitle ) );
4037
	}
4038
4039
	/**
4040
	 * @param string $key
4041
	 * @return string
4042
	 */
4043
	public function convertCategoryKey( $key ) {
4044
		return $this->mConverter->convertCategoryKey( $key );
4045
	}
4046
4047
	/**
4048
	 * Get the list of variants supported by this language
4049
	 * see sample implementation in LanguageZh.php
4050
	 *
4051
	 * @return array An array of language codes
4052
	 */
4053
	public function getVariants() {
4054
		return $this->mConverter->getVariants();
4055
	}
4056
4057
	/**
4058
	 * @return string
4059
	 */
4060
	public function getPreferredVariant() {
4061
		return $this->mConverter->getPreferredVariant();
4062
	}
4063
4064
	/**
4065
	 * @return string
4066
	 */
4067
	public function getDefaultVariant() {
4068
		return $this->mConverter->getDefaultVariant();
4069
	}
4070
4071
	/**
4072
	 * @return string
4073
	 */
4074
	public function getURLVariant() {
4075
		return $this->mConverter->getURLVariant();
4076
	}
4077
4078
	/**
4079
	 * If a language supports multiple variants, it is
4080
	 * possible that non-existing link in one variant
4081
	 * actually exists in another variant. this function
4082
	 * tries to find it. See e.g. LanguageZh.php
4083
	 * The input parameters may be modified upon return
4084
	 *
4085
	 * @param string &$link The name of the link
4086
	 * @param Title &$nt The title object of the link
4087
	 * @param bool $ignoreOtherCond To disable other conditions when
4088
	 *   we need to transclude a template or update a category's link
4089
	 */
4090
	public function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
4091
		$this->mConverter->findVariantLink( $link, $nt, $ignoreOtherCond );
4092
	}
4093
4094
	/**
4095
	 * returns language specific options used by User::getPageRenderHash()
4096
	 * for example, the preferred language variant
4097
	 *
4098
	 * @return string
4099
	 */
4100
	function getExtraHashOptions() {
4101
		return $this->mConverter->getExtraHashOptions();
4102
	}
4103
4104
	/**
4105
	 * For languages that support multiple variants, the title of an
4106
	 * article may be displayed differently in different variants. this
4107
	 * function returns the apporiate title defined in the body of the article.
4108
	 *
4109
	 * @return string
4110
	 */
4111
	public function getParsedTitle() {
4112
		return $this->mConverter->getParsedTitle();
0 ignored issues
show
Bug introduced by
The method getParsedTitle() does not seem to exist on object<LanguageConverter>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
4113
	}
4114
4115
	/**
4116
	 * Refresh the cache of conversion tables when
4117
	 * MediaWiki:Conversiontable* is updated.
4118
	 *
4119
	 * @param Title $title The Title of the page being updated
4120
	 */
4121
	public function updateConversionTable( Title $title ) {
4122
		$this->mConverter->updateConversionTable( $title );
4123
	}
4124
4125
	/**
4126
	 * Prepare external link text for conversion. When the text is
4127
	 * a URL, it shouldn't be converted, and it'll be wrapped in
4128
	 * the "raw" tag (-{R| }-) to prevent conversion.
4129
	 *
4130
	 * This function is called "markNoConversion" for historical
4131
	 * reasons.
4132
	 *
4133
	 * @param string $text Text to be used for external link
4134
	 * @param bool $noParse Wrap it without confirming it's a real URL first
4135
	 * @return string The tagged text
4136
	 */
4137
	public function markNoConversion( $text, $noParse = false ) {
4138
		// Excluding protocal-relative URLs may avoid many false positives.
4139
		if ( $noParse || preg_match( '/^(?:' . wfUrlProtocolsWithoutProtRel() . ')/', $text ) ) {
4140
			return $this->mConverter->markNoConversion( $text );
4141
		} else {
4142
			return $text;
4143
		}
4144
	}
4145
4146
	/**
4147
	 * A regular expression to match legal word-trailing characters
4148
	 * which should be merged onto a link of the form [[foo]]bar.
4149
	 *
4150
	 * @return string
4151
	 */
4152
	public function linkTrail() {
4153
		return self::$dataCache->getItem( $this->mCode, 'linkTrail' );
4154
	}
4155
4156
	/**
4157
	 * A regular expression character set to match legal word-prefixing
4158
	 * characters which should be merged onto a link of the form foo[[bar]].
4159
	 *
4160
	 * @return string
4161
	 */
4162
	public function linkPrefixCharset() {
4163
		return self::$dataCache->getItem( $this->mCode, 'linkPrefixCharset' );
4164
	}
4165
4166
	/**
4167
	 * Get the "parent" language which has a converter to convert a "compatible" language
4168
	 * (in another variant) to this language (eg. zh for zh-cn, but not en for en-gb).
4169
	 *
4170
	 * @return Language|null
4171
	 * @since 1.22
4172
	 */
4173
	public function getParentLanguage() {
4174
		if ( $this->mParentLanguage !== false ) {
4175
			return $this->mParentLanguage;
4176
		}
4177
4178
		$code = explode( '-', $this->getCode() )[0];
4179
		if ( !in_array( $code, LanguageConverter::$languagesWithVariants ) ) {
4180
			$this->mParentLanguage = null;
4181
			return null;
4182
		}
4183
		$lang = Language::factory( $code );
4184
		if ( !$lang->hasVariant( $this->getCode() ) ) {
4185
			$this->mParentLanguage = null;
4186
			return null;
4187
		}
4188
4189
		$this->mParentLanguage = $lang;
0 ignored issues
show
Documentation Bug introduced by
It seems like $lang of type object<Language> is incompatible with the declared type boolean of property $mParentLanguage.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
4190
		return $lang;
4191
	}
4192
4193
	/**
4194
	 * Compare with an other language object
4195
	 *
4196
	 * @since 1.28
4197
	 * @param Language $lang
4198
	 * @return boolean
4199
	 */
4200
	public function equals( Language $lang ) {
4201
		return $lang->getCode() === $this->mCode;
4202
	}
4203
4204
	/**
4205
	 * Get the internal language code for this language object
4206
	 *
4207
	 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
4208
	 * htmlspecialchars() or similar
4209
	 *
4210
	 * @return string
4211
	 */
4212
	public function getCode() {
4213
		return $this->mCode;
4214
	}
4215
4216
	/**
4217
	 * Get the code in BCP 47 format which we can use
4218
	 * inside of html lang="" tags.
4219
	 *
4220
	 * NOTE: The return value of this function is NOT HTML-safe and must be escaped with
4221
	 * htmlspecialchars() or similar.
4222
	 *
4223
	 * @since 1.19
4224
	 * @return string
4225
	 */
4226
	public function getHtmlCode() {
4227
		if ( is_null( $this->mHtmlCode ) ) {
4228
			$this->mHtmlCode = wfBCP47( $this->getCode() );
4229
		}
4230
		return $this->mHtmlCode;
4231
	}
4232
4233
	/**
4234
	 * @param string $code
4235
	 */
4236
	public function setCode( $code ) {
4237
		$this->mCode = $code;
4238
		// Ensure we don't leave incorrect cached data lying around
4239
		$this->mHtmlCode = null;
4240
		$this->mParentLanguage = false;
4241
	}
4242
4243
	/**
4244
	 * Get the language code from a file name. Inverse of getFileName()
4245
	 * @param string $filename $prefix . $languageCode . $suffix
4246
	 * @param string $prefix Prefix before the language code
4247
	 * @param string $suffix Suffix after the language code
4248
	 * @return string Language code, or false if $prefix or $suffix isn't found
4249
	 */
4250
	public static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
4251
		$m = null;
4252
		preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
4253
			preg_quote( $suffix, '/' ) . '/', $filename, $m );
4254
		if ( !count( $m ) ) {
4255
			return false;
4256
		}
4257
		return str_replace( '_', '-', strtolower( $m[1] ) );
4258
	}
4259
4260
	/**
4261
	 * @param string $code
4262
	 * @return string Name of the language class
4263
	 */
4264
	public static function classFromCode( $code ) {
4265
		if ( $code == 'en' ) {
4266
			return 'Language';
4267
		} else {
4268
			return 'Language' . str_replace( '-', '_', ucfirst( $code ) );
4269
		}
4270
	}
4271
4272
	/**
4273
	 * Get the name of a file for a certain language code
4274
	 * @param string $prefix Prepend this to the filename
4275
	 * @param string $code Language code
4276
	 * @param string $suffix Append this to the filename
4277
	 * @throws MWException
4278
	 * @return string $prefix . $mangledCode . $suffix
4279
	 */
4280
	public static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
4281
		if ( !self::isValidBuiltInCode( $code ) ) {
4282
			throw new MWException( "Invalid language code \"$code\"" );
4283
		}
4284
4285
		return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
4286
	}
4287
4288
	/**
4289
	 * @param string $code
4290
	 * @return string
4291
	 */
4292
	public static function getMessagesFileName( $code ) {
4293
		global $IP;
4294
		$file = self::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
4295
		Hooks::run( 'Language::getMessagesFileName', [ $code, &$file ] );
4296
		return $file;
4297
	}
4298
4299
	/**
4300
	 * @param string $code
4301
	 * @return string
4302
	 * @throws MWException
4303
	 * @since 1.23
4304
	 */
4305
	public static function getJsonMessagesFileName( $code ) {
4306
		global $IP;
4307
4308
		if ( !self::isValidBuiltInCode( $code ) ) {
4309
			throw new MWException( "Invalid language code \"$code\"" );
4310
		}
4311
4312
		return "$IP/languages/i18n/$code.json";
4313
	}
4314
4315
	/**
4316
	 * Get the first fallback for a given language.
4317
	 *
4318
	 * @param string $code
4319
	 *
4320
	 * @return bool|string
4321
	 */
4322
	public static function getFallbackFor( $code ) {
4323
		$fallbacks = self::getFallbacksFor( $code );
4324
		if ( $fallbacks ) {
4325
			return $fallbacks[0];
4326
		}
4327
		return false;
4328
	}
4329
4330
	/**
4331
	 * Get the ordered list of fallback languages.
4332
	 *
4333
	 * @since 1.19
4334
	 * @param string $code Language code
4335
	 * @return array Non-empty array, ending in "en"
4336
	 */
4337
	public static function getFallbacksFor( $code ) {
4338
		if ( $code === 'en' || !Language::isValidBuiltInCode( $code ) ) {
4339
			return [];
4340
		}
4341
		// For unknown languages, fallbackSequence returns an empty array,
4342
		// hardcode fallback to 'en' in that case.
4343
		return self::getLocalisationCache()->getItem( $code, 'fallbackSequence' ) ?: [ 'en' ];
4344
	}
4345
4346
	/**
4347
	 * Get the ordered list of fallback languages, ending with the fallback
4348
	 * language chain for the site language.
4349
	 *
4350
	 * @since 1.22
4351
	 * @param string $code Language code
4352
	 * @return array Array( fallbacks, site fallbacks )
4353
	 */
4354
	public static function getFallbacksIncludingSiteLanguage( $code ) {
4355
		global $wgLanguageCode;
4356
4357
		// Usually, we will only store a tiny number of fallback chains, so we
4358
		// keep them in static memory.
4359
		$cacheKey = "{$code}-{$wgLanguageCode}";
4360
4361
		if ( !array_key_exists( $cacheKey, self::$fallbackLanguageCache ) ) {
4362
			$fallbacks = self::getFallbacksFor( $code );
4363
4364
			// Append the site's fallback chain, including the site language itself
4365
			$siteFallbacks = self::getFallbacksFor( $wgLanguageCode );
4366
			array_unshift( $siteFallbacks, $wgLanguageCode );
4367
4368
			// Eliminate any languages already included in the chain
4369
			$siteFallbacks = array_diff( $siteFallbacks, $fallbacks );
4370
4371
			self::$fallbackLanguageCache[$cacheKey] = [ $fallbacks, $siteFallbacks ];
4372
		}
4373
		return self::$fallbackLanguageCache[$cacheKey];
4374
	}
4375
4376
	/**
4377
	 * Get all messages for a given language
4378
	 * WARNING: this may take a long time. If you just need all message *keys*
4379
	 * but need the *contents* of only a few messages, consider using getMessageKeysFor().
4380
	 *
4381
	 * @param string $code
4382
	 *
4383
	 * @return array
4384
	 */
4385
	public static function getMessagesFor( $code ) {
4386
		return self::getLocalisationCache()->getItem( $code, 'messages' );
4387
	}
4388
4389
	/**
4390
	 * Get a message for a given language
4391
	 *
4392
	 * @param string $key
4393
	 * @param string $code
4394
	 *
4395
	 * @return string
4396
	 */
4397
	public static function getMessageFor( $key, $code ) {
4398
		return self::getLocalisationCache()->getSubitem( $code, 'messages', $key );
4399
	}
4400
4401
	/**
4402
	 * Get all message keys for a given language. This is a faster alternative to
4403
	 * array_keys( Language::getMessagesFor( $code ) )
4404
	 *
4405
	 * @since 1.19
4406
	 * @param string $code Language code
4407
	 * @return array Array of message keys (strings)
4408
	 */
4409
	public static function getMessageKeysFor( $code ) {
4410
		return self::getLocalisationCache()->getSubitemList( $code, 'messages' );
4411
	}
4412
4413
	/**
4414
	 * @param string $talk
4415
	 * @return mixed
4416
	 */
4417
	function fixVariableInNamespace( $talk ) {
4418
		if ( strpos( $talk, '$1' ) === false ) {
4419
			return $talk;
4420
		}
4421
4422
		global $wgMetaNamespace;
4423
		$talk = str_replace( '$1', $wgMetaNamespace, $talk );
4424
4425
		# Allow grammar transformations
4426
		# Allowing full message-style parsing would make simple requests
4427
		# such as action=raw much more expensive than they need to be.
4428
		# This will hopefully cover most cases.
4429
		$talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
4430
			[ &$this, 'replaceGrammarInNamespace' ], $talk );
4431
		return str_replace( ' ', '_', $talk );
4432
	}
4433
4434
	/**
4435
	 * @param string $m
4436
	 * @return string
4437
	 */
4438
	function replaceGrammarInNamespace( $m ) {
4439
		return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
4440
	}
4441
4442
	/**
4443
	 * Decode an expiry (block, protection, etc) which has come from the DB
4444
	 *
4445
	 * @param string $expiry Database expiry String
4446
	 * @param bool|int $format True to process using language functions, or TS_ constant
4447
	 *     to return the expiry in a given timestamp
4448
	 * @param string $infinity If $format is not true, use this string for infinite expiry
4449
	 * @return string
4450
	 * @since 1.18
4451
	 */
4452
	public function formatExpiry( $expiry, $format = true, $infinity = 'infinity' ) {
4453
		static $dbInfinity;
4454
		if ( $dbInfinity === null ) {
4455
			$dbInfinity = wfGetDB( DB_SLAVE )->getInfinity();
4456
		}
4457
4458
		if ( $expiry == '' || $expiry === 'infinity' || $expiry == $dbInfinity ) {
4459
			return $format === true
4460
				? $this->getMessageFromDB( 'infiniteblock' )
4461
				: $infinity;
4462
		} else {
4463
			return $format === true
4464
				? $this->timeanddate( $expiry, /* User preference timezone */ true )
4465
				: wfTimestamp( $format, $expiry );
4466
		}
4467
	}
4468
4469
	/**
4470
	 * @todo Document
4471
	 * @param int|float $seconds
4472
	 * @param array $format Optional
4473
	 *   If $format['avoid'] === 'avoidseconds': don't mention seconds if $seconds >= 1 hour.
4474
	 *   If $format['avoid'] === 'avoidminutes': don't mention seconds/minutes if $seconds > 48 hours.
4475
	 *   If $format['noabbrevs'] is true: use 'seconds' and friends instead of 'seconds-abbrev'
4476
	 *     and friends.
4477
	 *   For backwards compatibility, $format may also be one of the strings 'avoidseconds'
4478
	 *     or 'avoidminutes'.
4479
	 * @return string
4480
	 */
4481
	function formatTimePeriod( $seconds, $format = [] ) {
4482
		if ( !is_array( $format ) ) {
4483
			$format = [ 'avoid' => $format ]; // For backwards compatibility
4484
		}
4485
		if ( !isset( $format['avoid'] ) ) {
4486
			$format['avoid'] = false;
4487
		}
4488
		if ( !isset( $format['noabbrevs'] ) ) {
4489
			$format['noabbrevs'] = false;
4490
		}
4491
		$secondsMsg = wfMessage(
4492
			$format['noabbrevs'] ? 'seconds' : 'seconds-abbrev' )->inLanguage( $this );
4493
		$minutesMsg = wfMessage(
4494
			$format['noabbrevs'] ? 'minutes' : 'minutes-abbrev' )->inLanguage( $this );
4495
		$hoursMsg = wfMessage(
4496
			$format['noabbrevs'] ? 'hours' : 'hours-abbrev' )->inLanguage( $this );
4497
		$daysMsg = wfMessage(
4498
			$format['noabbrevs'] ? 'days' : 'days-abbrev' )->inLanguage( $this );
4499
4500
		if ( round( $seconds * 10 ) < 100 ) {
4501
			$s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
4502
			$s = $secondsMsg->params( $s )->text();
4503
		} elseif ( round( $seconds ) < 60 ) {
4504
			$s = $this->formatNum( round( $seconds ) );
4505
			$s = $secondsMsg->params( $s )->text();
4506
		} elseif ( round( $seconds ) < 3600 ) {
4507
			$minutes = floor( $seconds / 60 );
4508
			$secondsPart = round( fmod( $seconds, 60 ) );
4509
			if ( $secondsPart == 60 ) {
4510
				$secondsPart = 0;
4511
				$minutes++;
4512
			}
4513
			$s = $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4514
			$s .= ' ';
4515
			$s .= $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4516
		} elseif ( round( $seconds ) <= 2 * 86400 ) {
4517
			$hours = floor( $seconds / 3600 );
4518
			$minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
4519
			$secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
4520
			if ( $secondsPart == 60 ) {
4521
				$secondsPart = 0;
4522
				$minutes++;
4523
			}
4524
			if ( $minutes == 60 ) {
4525
				$minutes = 0;
4526
				$hours++;
4527
			}
4528
			$s = $hoursMsg->params( $this->formatNum( $hours ) )->text();
4529
			$s .= ' ';
4530
			$s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4531
			if ( !in_array( $format['avoid'], [ 'avoidseconds', 'avoidminutes' ] ) ) {
4532
				$s .= ' ' . $secondsMsg->params( $this->formatNum( $secondsPart ) )->text();
4533
			}
4534
		} else {
4535
			$days = floor( $seconds / 86400 );
4536
			if ( $format['avoid'] === 'avoidminutes' ) {
4537
				$hours = round( ( $seconds - $days * 86400 ) / 3600 );
4538
				if ( $hours == 24 ) {
4539
					$hours = 0;
4540
					$days++;
4541
				}
4542
				$s = $daysMsg->params( $this->formatNum( $days ) )->text();
4543
				$s .= ' ';
4544
				$s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4545
			} elseif ( $format['avoid'] === 'avoidseconds' ) {
4546
				$hours = floor( ( $seconds - $days * 86400 ) / 3600 );
4547
				$minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
4548
				if ( $minutes == 60 ) {
4549
					$minutes = 0;
4550
					$hours++;
4551
				}
4552
				if ( $hours == 24 ) {
4553
					$hours = 0;
4554
					$days++;
4555
				}
4556
				$s = $daysMsg->params( $this->formatNum( $days ) )->text();
4557
				$s .= ' ';
4558
				$s .= $hoursMsg->params( $this->formatNum( $hours ) )->text();
4559
				$s .= ' ';
4560
				$s .= $minutesMsg->params( $this->formatNum( $minutes ) )->text();
4561
			} else {
4562
				$s = $daysMsg->params( $this->formatNum( $days ) )->text();
4563
				$s .= ' ';
4564
				$s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
4565
			}
4566
		}
4567
		return $s;
4568
	}
4569
4570
	/**
4571
	 * Format a bitrate for output, using an appropriate
4572
	 * unit (bps, kbps, Mbps, Gbps, Tbps, Pbps, Ebps, Zbps or Ybps) according to
4573
	 *   the magnitude in question.
4574
	 *
4575
	 * This use base 1000. For base 1024 use formatSize(), for another base
4576
	 * see formatComputingNumbers().
4577
	 *
4578
	 * @param int $bps
4579
	 * @return string
4580
	 */
4581
	function formatBitrate( $bps ) {
4582
		return $this->formatComputingNumbers( $bps, 1000, "bitrate-$1bits" );
4583
	}
4584
4585
	/**
4586
	 * @param int $size Size of the unit
4587
	 * @param int $boundary Size boundary (1000, or 1024 in most cases)
4588
	 * @param string $messageKey Message key to be uesd
4589
	 * @return string
4590
	 */
4591
	function formatComputingNumbers( $size, $boundary, $messageKey ) {
4592
		if ( $size <= 0 ) {
4593
			return str_replace( '$1', $this->formatNum( $size ),
4594
				$this->getMessageFromDB( str_replace( '$1', '', $messageKey ) )
4595
			);
4596
		}
4597
		$sizes = [ '', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zeta', 'yotta' ];
4598
		$index = 0;
4599
4600
		$maxIndex = count( $sizes ) - 1;
4601
		while ( $size >= $boundary && $index < $maxIndex ) {
4602
			$index++;
4603
			$size /= $boundary;
4604
		}
4605
4606
		// For small sizes no decimal places necessary
4607
		$round = 0;
4608
		if ( $index > 1 ) {
4609
			// For MB and bigger two decimal places are smarter
4610
			$round = 2;
4611
		}
4612
		$msg = str_replace( '$1', $sizes[$index], $messageKey );
4613
4614
		$size = round( $size, $round );
4615
		$text = $this->getMessageFromDB( $msg );
4616
		return str_replace( '$1', $this->formatNum( $size ), $text );
4617
	}
4618
4619
	/**
4620
	 * Format a size in bytes for output, using an appropriate
4621
	 * unit (B, KB, MB, GB, TB, PB, EB, ZB or YB) according to the magnitude in question
4622
	 *
4623
	 * This method use base 1024. For base 1000 use formatBitrate(), for
4624
	 * another base see formatComputingNumbers()
4625
	 *
4626
	 * @param int $size Size to format
4627
	 * @return string Plain text (not HTML)
4628
	 */
4629
	function formatSize( $size ) {
4630
		return $this->formatComputingNumbers( $size, 1024, "size-$1bytes" );
4631
	}
4632
4633
	/**
4634
	 * Make a list item, used by various special pages
4635
	 *
4636
	 * @param string $page Page link
4637
	 * @param string $details HTML safe text between brackets
4638
	 * @param bool $oppositedm Add the direction mark opposite to your
4639
	 *   language, to display text properly
4640
	 * @return HTML escaped string
4641
	 */
4642
	function specialList( $page, $details, $oppositedm = true ) {
4643
		if ( !$details ) {
4644
			return $page;
4645
		}
4646
4647
		$dirmark = ( $oppositedm ? $this->getDirMark( true ) : '' ) . $this->getDirMark();
4648
		return
4649
			$page .
4650
			$dirmark .
4651
			$this->msg( 'word-separator' )->escaped() .
4652
			$this->msg( 'parentheses' )->rawParams( $details )->escaped();
4653
	}
4654
4655
	/**
4656
	 * Generate (prev x| next x) (20|50|100...) type links for paging
4657
	 *
4658
	 * @param Title $title Title object to link
4659
	 * @param int $offset
4660
	 * @param int $limit
4661
	 * @param array $query Optional URL query parameter string
4662
	 * @param bool $atend Optional param for specified if this is the last page
4663
	 * @return string
4664
	 */
4665
	public function viewPrevNext( Title $title, $offset, $limit,
4666
		array $query = [], $atend = false
4667
	) {
4668
		// @todo FIXME: Why on earth this needs one message for the text and another one for tooltip?
4669
4670
		# Make 'previous' link
4671
		$prev = wfMessage( 'prevn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4672 View Code Duplication
		if ( $offset > 0 ) {
4673
			$plink = $this->numLink( $title, max( $offset - $limit, 0 ), $limit,
4674
				$query, $prev, 'prevn-title', 'mw-prevlink' );
4675
		} else {
4676
			$plink = htmlspecialchars( $prev );
4677
		}
4678
4679
		# Make 'next' link
4680
		$next = wfMessage( 'nextn' )->inLanguage( $this )->title( $title )->numParams( $limit )->text();
4681 View Code Duplication
		if ( $atend ) {
4682
			$nlink = htmlspecialchars( $next );
4683
		} else {
4684
			$nlink = $this->numLink( $title, $offset + $limit, $limit,
4685
				$query, $next, 'nextn-title', 'mw-nextlink' );
4686
		}
4687
4688
		# Make links to set number of items per page
4689
		$numLinks = [];
4690
		foreach ( [ 20, 50, 100, 250, 500 ] as $num ) {
4691
			$numLinks[] = $this->numLink( $title, $offset, $num,
4692
				$query, $this->formatNum( $num ), 'shown-title', 'mw-numlink' );
4693
		}
4694
4695
		return wfMessage( 'viewprevnext' )->inLanguage( $this )->title( $title
4696
			)->rawParams( $plink, $nlink, $this->pipeList( $numLinks ) )->escaped();
4697
	}
4698
4699
	/**
4700
	 * Helper function for viewPrevNext() that generates links
4701
	 *
4702
	 * @param Title $title Title object to link
4703
	 * @param int $offset
4704
	 * @param int $limit
4705
	 * @param array $query Extra query parameters
4706
	 * @param string $link Text to use for the link; will be escaped
4707
	 * @param string $tooltipMsg Name of the message to use as tooltip
4708
	 * @param string $class Value of the "class" attribute of the link
4709
	 * @return string HTML fragment
4710
	 */
4711
	private function numLink( Title $title, $offset, $limit, array $query, $link,
4712
		$tooltipMsg, $class
4713
	) {
4714
		$query = [ 'limit' => $limit, 'offset' => $offset ] + $query;
4715
		$tooltip = wfMessage( $tooltipMsg )->inLanguage( $this )->title( $title )
4716
			->numParams( $limit )->text();
4717
4718
		return Html::element( 'a', [ 'href' => $title->getLocalURL( $query ),
4719
			'title' => $tooltip, 'class' => $class ], $link );
4720
	}
4721
4722
	/**
4723
	 * Get the conversion rule title, if any.
4724
	 *
4725
	 * @return string
4726
	 */
4727
	public function getConvRuleTitle() {
4728
		return $this->mConverter->getConvRuleTitle();
4729
	}
4730
4731
	/**
4732
	 * Get the compiled plural rules for the language
4733
	 * @since 1.20
4734
	 * @return array Associative array with plural form, and plural rule as key-value pairs
4735
	 */
4736 View Code Duplication
	public function getCompiledPluralRules() {
4737
		$pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'compiledPluralRules' );
4738
		$fallbacks = Language::getFallbacksFor( $this->mCode );
4739
		if ( !$pluralRules ) {
4740
			foreach ( $fallbacks as $fallbackCode ) {
4741
				$pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'compiledPluralRules' );
4742
				if ( $pluralRules ) {
4743
					break;
4744
				}
4745
			}
4746
		}
4747
		return $pluralRules;
4748
	}
4749
4750
	/**
4751
	 * Get the plural rules for the language
4752
	 * @since 1.20
4753
	 * @return array Associative array with plural form number and plural rule as key-value pairs
4754
	 */
4755 View Code Duplication
	public function getPluralRules() {
4756
		$pluralRules = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRules' );
4757
		$fallbacks = Language::getFallbacksFor( $this->mCode );
4758
		if ( !$pluralRules ) {
4759
			foreach ( $fallbacks as $fallbackCode ) {
4760
				$pluralRules = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRules' );
4761
				if ( $pluralRules ) {
4762
					break;
4763
				}
4764
			}
4765
		}
4766
		return $pluralRules;
4767
	}
4768
4769
	/**
4770
	 * Get the plural rule types for the language
4771
	 * @since 1.22
4772
	 * @return array Associative array with plural form number and plural rule type as key-value pairs
4773
	 */
4774 View Code Duplication
	public function getPluralRuleTypes() {
4775
		$pluralRuleTypes = self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRuleTypes' );
4776
		$fallbacks = Language::getFallbacksFor( $this->mCode );
4777
		if ( !$pluralRuleTypes ) {
4778
			foreach ( $fallbacks as $fallbackCode ) {
4779
				$pluralRuleTypes = self::$dataCache->getItem( strtolower( $fallbackCode ), 'pluralRuleTypes' );
4780
				if ( $pluralRuleTypes ) {
4781
					break;
4782
				}
4783
			}
4784
		}
4785
		return $pluralRuleTypes;
4786
	}
4787
4788
	/**
4789
	 * Find the index number of the plural rule appropriate for the given number
4790
	 * @param int $number
4791
	 * @return int The index number of the plural rule
4792
	 */
4793
	public function getPluralRuleIndexNumber( $number ) {
4794
		$pluralRules = $this->getCompiledPluralRules();
4795
		$form = Evaluator::evaluateCompiled( $number, $pluralRules );
4796
		return $form;
4797
	}
4798
4799
	/**
4800
	 * Find the plural rule type appropriate for the given number
4801
	 * For example, if the language is set to Arabic, getPluralType(5) should
4802
	 * return 'few'.
4803
	 * @since 1.22
4804
	 * @param int $number
4805
	 * @return string The name of the plural rule type, e.g. one, two, few, many
4806
	 */
4807
	public function getPluralRuleType( $number ) {
4808
		$index = $this->getPluralRuleIndexNumber( $number );
4809
		$pluralRuleTypes = $this->getPluralRuleTypes();
4810
		if ( isset( $pluralRuleTypes[$index] ) ) {
4811
			return $pluralRuleTypes[$index];
4812
		} else {
4813
			return 'other';
4814
		}
4815
	}
4816
}
4817