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