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