Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.84% covered (warning)
85.84%
97 / 113
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
LanguageData
85.84% covered (warning)
85.84%
97 / 113
33.33% covered (danger)
33.33%
1 / 3
43.32
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getLocalData
96.83% covered (success)
96.83%
61 / 63
0.00% covered (danger)
0.00%
0 / 1
9
 convertDateFormat
69.57% covered (warning)
69.57%
32 / 46
0.00% covered (danger)
0.00%
0 / 1
52.71
1<?php
2/**
3 * Generates language-specific data used by DiscussionTools.
4 *
5 * @file
6 * @ingroup Extensions
7 * @license MIT
8 */
9
10namespace MediaWiki\Extension\DiscussionTools;
11
12use DateTimeZone;
13use ILanguageConverter;
14use Language;
15use MediaWiki\Config\Config;
16use MediaWiki\Languages\LanguageConverterFactory;
17use MediaWiki\MainConfigNames;
18use MediaWiki\SpecialPage\SpecialPageFactory;
19
20class LanguageData {
21
22    private Config $config;
23    private Language $language;
24    private LanguageConverterFactory $languageConverterFactory;
25    private SpecialPageFactory $specialPageFactory;
26
27    public function __construct(
28        Config $config,
29        Language $language,
30        LanguageConverterFactory $languageConverterFactory,
31        SpecialPageFactory $specialPageFactory
32    ) {
33        $this->config = $config;
34        $this->language = $language;
35        $this->languageConverterFactory = $languageConverterFactory;
36        $this->specialPageFactory = $specialPageFactory;
37    }
38
39    /**
40     * Compute data we need to parse discussion threads on pages.
41     *
42     * @return array
43     */
44    public function getLocalData(): array {
45        $config = $this->config;
46        $lang = $this->language;
47        $langConv = $this->languageConverterFactory->getLanguageConverter( $lang );
48
49        $data = [];
50
51        $data['dateFormat'] = [];
52        $dateFormat = $lang->getDateFormatString( 'both', $lang->dateFormat( false ) );
53        foreach ( $langConv->getVariants() as $variant ) {
54            $convDateFormat = $this->convertDateFormat( $dateFormat, $langConv, $variant );
55            $data['dateFormat'][$variant] = $convDateFormat;
56        }
57
58        $data['digits'] = [];
59        foreach ( $langConv->getVariants() as $variant ) {
60            $data['digits'][$variant] = [];
61            foreach ( str_split( '0123456789' ) as $digit ) {
62                if ( $config->get( MainConfigNames::TranslateNumerals ) ) {
63                    $localDigit = $lang->formatNumNoSeparators( $digit );
64                } else {
65                    $localDigit = $digit;
66                }
67                $convLocalDigit = $langConv->translate( $localDigit, $variant );
68                $data['digits'][$variant][] = $convLocalDigit;
69            }
70        }
71
72        // ApiQuerySiteinfo
73        $data['localTimezone'] = $config->get( MainConfigNames::Localtimezone );
74
75        // special page names compared against Title::getText, which contains space
76        // But aliases are stored with underscores (db key) in the alias files
77        $data['specialContributionsName'] = str_replace( '_', ' ', $this->specialPageFactory
78            ->getLocalNameFor( 'Contributions' ) );
79        $data['specialNewSectionName'] = str_replace( '_', ' ', $this->specialPageFactory
80            ->getLocalNameFor( 'NewSection' ) );
81
82        $localTimezone = $config->get( MainConfigNames::Localtimezone );
83        // Return all timezone abbreviations for the local timezone (there will often be two, for
84        // non-DST and DST timestamps, and sometimes more due to historical data, but that's okay).
85        // Avoid DateTimeZone::listAbbreviations(), it returns some half-baked list that is different
86        // from the timezone data used by everything else in PHP.
87        $timezoneTransitions = ( new DateTimeZone( $localTimezone ) )->getTransitions();
88        if ( !is_array( $timezoneTransitions ) ) {
89            // Handle (arguably invalid) config where $wgLocaltimezone is an abbreviation like "CST"
90            // instead of a real IANA timezone name like "America/Chicago". (T312310)
91            // "DateTimeZone objects wrapping type 1 (UTC offsets) and type 2 (abbreviations) do not
92            // contain any transitions, and calling this method on them will return false."
93            // https://www.php.net/manual/en/datetimezone.gettransitions.php
94            $timezoneAbbrs = [ $localTimezone ];
95        } else {
96            $timezoneAbbrs = array_values( array_unique(
97                array_map( static function ( $transition ) {
98                    return $transition['abbr'];
99                }, $timezoneTransitions )
100            ) );
101        }
102
103        $data['timezones'] = [];
104        foreach ( $langConv->getVariants() as $variant ) {
105            $data['timezones'][$variant] = array_combine(
106                array_map( static function ( string $tzMsg ) use ( $lang, $langConv, $variant ) {
107                    // MWTimestamp::getTimezoneMessage()
108                    // Parser::pstPass2()
109                    // Messages used here: 'timezone-utc' and so on
110                    $key = 'timezone-' . strtolower( trim( $tzMsg ) );
111                    $msg = wfMessage( $key )->inLanguage( $lang );
112                    // TODO: This probably causes a similar issue to https://phabricator.wikimedia.org/T221294,
113                    // but we *must* check the message existence in the database, because the messages are not
114                    // actually defined by MediaWiki core for any timezone other than UTC...
115                    if ( $msg->exists() ) {
116                        $text = $msg->text();
117                    } else {
118                        $text = strtoupper( $tzMsg );
119                    }
120                    $convText = $langConv->translate( $text, $variant );
121                    return $convText;
122                }, $timezoneAbbrs ),
123                array_map( 'strtoupper', $timezoneAbbrs )
124            );
125        }
126
127        // Messages in content language
128        $messagesKeys = array_merge(
129            Language::WEEKDAY_MESSAGES,
130            Language::WEEKDAY_ABBREVIATED_MESSAGES,
131            Language::MONTH_MESSAGES,
132            Language::MONTH_GENITIVE_MESSAGES,
133            Language::MONTH_ABBREVIATED_MESSAGES
134        );
135        $data['contLangMessages'] = [];
136        foreach ( $langConv->getVariants() as $variant ) {
137            $data['contLangMessages'][$variant] = array_combine(
138                $messagesKeys,
139                array_map( static function ( $key ) use ( $lang, $langConv, $variant ) {
140                    $text = wfMessage( $key )->inLanguage( $lang )->text();
141                    return $langConv->translate( $text, $variant );
142                }, $messagesKeys )
143            );
144        }
145
146        return $data;
147    }
148
149    /**
150     * Convert a date format string to a different language variant, leaving all special characters
151     * unchanged and applying language conversion to the plain text fragments.
152     *
153     * @param string $format
154     * @param ILanguageConverter $langConv
155     * @param string $variant
156     * @return string
157     */
158    private function convertDateFormat(
159        string $format,
160        ILanguageConverter $langConv,
161        string $variant
162    ): string {
163        $formatLength = strlen( $format );
164        $s = '';
165        // The supported codes must match CommentParser::getTimestampRegexp()
166        for ( $p = 0; $p < $formatLength; $p++ ) {
167            $num = false;
168            $code = $format[ $p ];
169            if ( $code === 'x' && $p < $formatLength - 1 ) {
170                $code .= $format[++$p];
171            }
172            if ( $code === 'xk' && $p < $formatLength - 1 ) {
173                $code .= $format[++$p];
174            }
175
176            // LAZY SHORTCUTS that might cause bugs:
177            // * We assume that result of $langConv->translate() doesn't produce any special codes/characters
178            // * We assume that calling $langConv->translate() separately for each character is correct
179            switch ( $code ) {
180                case 'xx':
181                case 'xg':
182                case 'xn':
183                case 'd':
184                case 'D':
185                case 'j':
186                case 'l':
187                case 'F':
188                case 'M':
189                case 'm':
190                case 'n':
191                case 'Y':
192                case 'xkY':
193                case 'G':
194                case 'H':
195                case 'i':
196                case 's':
197                    // Special code - pass through unchanged
198                    $s .= $code;
199                    break;
200                case '\\':
201                    // Plain text (backslash escaping) - convert to language variant
202                    if ( $p < $formatLength - 1 ) {
203                        $s .= '\\' . $langConv->translate( $format[++$p], $variant );
204                    } else {
205                        $s .= $code;
206                    }
207                    break;
208                case '"':
209                    // Plain text (quoted literal) - convert to language variant
210                    if ( $p < $formatLength - 1 ) {
211                        $endQuote = strpos( $format, '"', $p + 1 );
212                        if ( $endQuote === false ) {
213                            // No terminating quote, assume literal "
214                            $s .= $code;
215                        } else {
216                            $s .= '"' .
217                                $langConv->translate( substr( $format, $p + 1, $endQuote - $p - 1 ), $variant ) .
218                                '"';
219                            $p = $endQuote;
220                        }
221                    } else {
222                        // Quote at end of string, assume literal "
223                        $s .= $code;
224                    }
225                    break;
226                default:
227                    // Plain text - convert to language variant
228                    $s .= $langConv->translate( $format[$p], $variant );
229            }
230        }
231
232        return $s;
233    }
234}