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