Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
79.19% |
156 / 197 |
|
77.78% |
14 / 18 |
CRAP | |
0.00% |
0 / 1 |
LanguageLibrary | |
79.19% |
156 / 197 |
|
77.78% |
14 / 18 |
98.78 | |
0.00% |
0 / 1 |
register | |
100.00% |
39 / 39 |
|
100.00% |
1 / 1 |
3 | |||
getContLangCode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isSupportedLanguage | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
isKnownLanguageTag | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
isValidCode | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
isValidBuiltInCode | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
fetchLanguageName | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
fetchLanguageNames | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getFallbacksFor | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
toBcp47Code | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
languageMethod | |
79.31% |
23 / 29 |
|
0.00% |
0 / 1 |
16.99 | |||
convertPlural | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
convertGrammar | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
gender | |
38.46% |
10 / 26 |
|
0.00% |
0 / 1 |
39.20 | |||
formatNum | |
71.43% |
10 / 14 |
|
0.00% |
0 / 1 |
5.58 | |||
formatDate | |
65.12% |
28 / 43 |
|
0.00% |
0 / 1 |
22.32 | |||
formatDuration | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
getDurationIntervals | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Scribunto\Engines\LuaCommon; |
4 | |
5 | use DateTime; |
6 | use DateTimeZone; |
7 | use Exception; |
8 | use Language; |
9 | use LanguageCode; |
10 | use MediaWiki\Languages\LanguageNameUtils; |
11 | use MediaWiki\MainConfigNames; |
12 | use MediaWiki\MediaWikiServices; |
13 | use MediaWiki\Title\Title; |
14 | use MediaWiki\User\User; |
15 | use MediaWiki\Utils\MWTimestamp; |
16 | use Wikimedia\RequestTimeout\TimeoutException; |
17 | |
18 | class LanguageLibrary extends LibraryBase { |
19 | /** @var Language[] */ |
20 | public $langCache = []; |
21 | /** @var array */ |
22 | public $timeCache = []; |
23 | /** @var int */ |
24 | public $maxLangCacheSize; |
25 | |
26 | public function register() { |
27 | // Pre-populate the language cache |
28 | $contLang = MediaWikiServices::getInstance()->getContentLanguage(); |
29 | $this->langCache[$contLang->getCode()] = $contLang; |
30 | $this->maxLangCacheSize = $this->getEngine()->getOption( 'maxLangCacheSize' ); |
31 | |
32 | $statics = [ |
33 | 'getContLangCode', |
34 | 'isSupportedLanguage', |
35 | 'isKnownLanguageTag', |
36 | 'isValidCode', |
37 | 'isValidBuiltInCode', |
38 | 'fetchLanguageName', |
39 | 'fetchLanguageNames', |
40 | 'getFallbacksFor', |
41 | 'toBcp47Code', |
42 | ]; |
43 | $methods = [ |
44 | 'lcfirst', |
45 | 'ucfirst', |
46 | 'lc', |
47 | 'uc', |
48 | 'caseFold', |
49 | 'formatNum', |
50 | 'formatDate', |
51 | 'formatDuration', |
52 | 'getDurationIntervals', |
53 | 'parseFormattedNumber', |
54 | 'convertPlural', |
55 | 'convertGrammar', |
56 | 'gender', |
57 | 'isRTL', |
58 | ]; |
59 | $lib = []; |
60 | foreach ( $statics as $name ) { |
61 | $lib[$name] = [ $this, $name ]; |
62 | } |
63 | foreach ( $methods as $name ) { |
64 | $lib[$name] = function () use ( $name ) { |
65 | $args = func_get_args(); |
66 | return $this->languageMethod( $name, $args ); |
67 | }; |
68 | } |
69 | return $this->getEngine()->registerInterface( 'mw.language.lua', $lib ); |
70 | } |
71 | |
72 | /** |
73 | * Handler for getContLangCode |
74 | * @internal |
75 | * @return string[] |
76 | */ |
77 | public function getContLangCode() { |
78 | return [ MediaWikiServices::getInstance()->getContentLanguage()->getCode() ]; |
79 | } |
80 | |
81 | /** |
82 | * Handler for isSupportedLanguage |
83 | * @internal |
84 | * @param string $code |
85 | * @return bool[] |
86 | */ |
87 | public function isSupportedLanguage( $code ) { |
88 | $this->checkType( 'isSupportedLanguage', 1, $code, 'string' ); |
89 | return [ MediaWikiServices::getInstance()->getLanguageNameUtils()->isSupportedLanguage( $code ) ]; |
90 | } |
91 | |
92 | /** |
93 | * Handler for isKnownLanguageTag |
94 | * @internal |
95 | * @param string $code |
96 | * @return bool[] |
97 | */ |
98 | public function isKnownLanguageTag( $code ) { |
99 | $this->checkType( 'isKnownLanguageTag', 1, $code, 'string' ); |
100 | return [ MediaWikiServices::getInstance()->getLanguageNameUtils()->isKnownLanguageTag( $code ) ]; |
101 | } |
102 | |
103 | /** |
104 | * Handler for isValidCode |
105 | * @internal |
106 | * @param string $code |
107 | * @return bool[] |
108 | */ |
109 | public function isValidCode( $code ) { |
110 | $this->checkType( 'isValidCode', 1, $code, 'string' ); |
111 | return [ MediaWikiServices::getInstance()->getLanguageNameUtils()->isValidCode( $code ) ]; |
112 | } |
113 | |
114 | /** |
115 | * Handler for isValidBuiltInCode |
116 | * @internal |
117 | * @param string $code |
118 | * @return bool[] |
119 | */ |
120 | public function isValidBuiltInCode( $code ) { |
121 | $this->checkType( 'isValidBuiltInCode', 1, $code, 'string' ); |
122 | return [ MediaWikiServices::getInstance()->getLanguageNameUtils()->isValidBuiltInCode( $code ) ]; |
123 | } |
124 | |
125 | /** |
126 | * Handler for fetchLanguageName |
127 | * @internal |
128 | * @param string $code |
129 | * @param null|string $inLanguage |
130 | * @return string[] |
131 | */ |
132 | public function fetchLanguageName( $code, $inLanguage ) { |
133 | $this->checkType( 'fetchLanguageName', 1, $code, 'string' ); |
134 | $this->checkTypeOptional( 'fetchLanguageName', 2, $inLanguage, 'string', LanguageNameUtils::AUTONYMS ); |
135 | return [ MediaWikiServices::getInstance()->getLanguageNameUtils() |
136 | ->getLanguageName( $code, $inLanguage ) ]; |
137 | } |
138 | |
139 | /** |
140 | * Handler for fetchLanguageNames |
141 | * @internal |
142 | * @param null|string $inLanguage |
143 | * @param null|string $include |
144 | * @return string[][] |
145 | */ |
146 | public function fetchLanguageNames( $inLanguage, $include ) { |
147 | $this->checkTypeOptional( 'fetchLanguageNames', 1, $inLanguage, 'string', LanguageNameUtils::AUTONYMS ); |
148 | $this->checkTypeOptional( 'fetchLanguageNames', 2, $include, 'string', LanguageNameUtils::DEFINED ); |
149 | return [ MediaWikiServices::getInstance()->getLanguageNameUtils() |
150 | ->getLanguageNames( $inLanguage, $include ) ]; |
151 | } |
152 | |
153 | /** |
154 | * Handler for fetchLanguageNames |
155 | * @internal |
156 | * @param string $code |
157 | * @return string[][] |
158 | */ |
159 | public function getFallbacksFor( $code ) { |
160 | $this->checkType( 'getFallbacksFor', 1, $code, 'string' ); |
161 | $ret = MediaWikiServices::getInstance()->getLanguageFallback()->getAll( $code ); |
162 | // Make 1-based |
163 | if ( count( $ret ) ) { |
164 | $ret = array_combine( range( 1, count( $ret ) ), $ret ); |
165 | } |
166 | return [ $ret ]; |
167 | } |
168 | |
169 | /** |
170 | * Handler for toBcp47Code |
171 | * @internal |
172 | * @param string $code a MediaWiki-internal code |
173 | * @return string[] a BCP-47 language tag |
174 | */ |
175 | public function toBcp47Code( $code ) { |
176 | $this->checkType( 'toBcp47Code', 1, $code, 'string' ); |
177 | $ret = LanguageCode::bcp47( $code ); |
178 | return [ $ret ]; |
179 | } |
180 | |
181 | /** |
182 | * Language object method handler |
183 | * @internal |
184 | * @param string $name |
185 | * @param array $args |
186 | * @return array |
187 | * @throws LuaError |
188 | */ |
189 | public function languageMethod( string $name, array $args ): array { |
190 | if ( !is_string( $args[0] ?? null ) ) { |
191 | throw new LuaError( |
192 | "invalid code property of language object when calling $name" |
193 | ); |
194 | } |
195 | $code = array_shift( $args ); |
196 | if ( !isset( $this->langCache[$code] ) ) { |
197 | if ( count( $this->langCache ) > $this->maxLangCacheSize ) { |
198 | throw new LuaError( 'too many language codes requested' ); |
199 | } |
200 | $services = MediaWikiServices::getInstance(); |
201 | if ( $services->getLanguageNameUtils()->isValidCode( $code ) ) { |
202 | $this->langCache[$code] = $services->getLanguageFactory()->getLanguage( $code ); |
203 | } else { |
204 | throw new LuaError( "language code '$code' is invalid" ); |
205 | } |
206 | } |
207 | $lang = $this->langCache[$code]; |
208 | switch ( $name ) { |
209 | // Zero arguments |
210 | case 'isRTL': |
211 | return [ $lang->$name() ]; |
212 | |
213 | // One string argument passed straight through |
214 | case 'lcfirst': |
215 | case 'ucfirst': |
216 | case 'lc': |
217 | case 'uc': |
218 | case 'caseFold': |
219 | $this->checkType( $name, 1, $args[0], 'string' ); |
220 | return [ $lang->$name( $args[0] ) ]; |
221 | |
222 | case 'parseFormattedNumber': |
223 | if ( is_numeric( $args[0] ) ) { |
224 | $args[0] = strval( $args[0] ); |
225 | } |
226 | if ( $this->getLuaType( $args[0] ) !== 'string' ) { |
227 | // Be like tonumber(), return nil instead of erroring out |
228 | return [ null ]; |
229 | } |
230 | return [ $lang->$name( $args[0] ) ]; |
231 | |
232 | // Custom handling |
233 | default: |
234 | return $this->$name( $lang, $args ); |
235 | } |
236 | } |
237 | |
238 | /** |
239 | * convertPlural handler |
240 | * @internal |
241 | * @param Language $lang |
242 | * @param array $args |
243 | * @return array |
244 | */ |
245 | public function convertPlural( $lang, $args ) { |
246 | $number = array_shift( $args ); |
247 | $this->checkType( 'convertPlural', 1, $number, 'number' ); |
248 | if ( is_array( $args[0] ) ) { |
249 | $args = $args[0]; |
250 | } |
251 | $forms = array_values( array_map( 'strval', $args ) ); |
252 | return [ $lang->convertPlural( $number, $forms ) ]; |
253 | } |
254 | |
255 | /** |
256 | * convertGrammar handler |
257 | * @internal |
258 | * @param Language $lang |
259 | * @param array $args |
260 | * @return array |
261 | */ |
262 | public function convertGrammar( $lang, $args ) { |
263 | $this->checkType( 'convertGrammar', 1, $args[0], 'string' ); |
264 | $this->checkType( 'convertGrammar', 2, $args[1], 'string' ); |
265 | return [ $lang->convertGrammar( $args[0], $args[1] ) ]; |
266 | } |
267 | |
268 | /** |
269 | * gender handler |
270 | * @internal |
271 | * @param Language $lang |
272 | * @param array $args |
273 | * @return array |
274 | */ |
275 | public function gender( $lang, $args ) { |
276 | $this->checkType( 'gender', 1, $args[0], 'string' ); |
277 | $username = trim( array_shift( $args ) ); |
278 | |
279 | if ( is_array( $args[0] ) ) { |
280 | $args = $args[0]; |
281 | } |
282 | $forms = array_values( array_map( 'strval', $args ) ); |
283 | |
284 | // Shortcuts |
285 | if ( count( $forms ) === 0 ) { |
286 | return [ '' ]; |
287 | } elseif ( count( $forms ) === 1 ) { |
288 | return [ $forms[0] ]; |
289 | } |
290 | |
291 | if ( $username === 'male' || $username === 'female' ) { |
292 | $gender = $username; |
293 | } else { |
294 | $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup(); |
295 | // default |
296 | $gender = $userOptionsLookup->getDefaultOption( 'gender' ); |
297 | |
298 | // Check for "User:" prefix |
299 | $title = Title::newFromText( $username ); |
300 | if ( $title && $title->getNamespace() === NS_USER ) { |
301 | $username = $title->getText(); |
302 | } |
303 | |
304 | // check parameter, or use the ParserOptions if in interface message |
305 | $user = User::newFromName( $username ); |
306 | if ( $user ) { |
307 | $genderCache = MediaWikiServices::getInstance()->getGenderCache(); |
308 | $gender = $genderCache->getGenderOf( $user, __METHOD__ ); |
309 | } elseif ( $username === '' ) { |
310 | $parserOptions = $this->getParserOptions(); |
311 | if ( $parserOptions->getInterfaceMessage() ) { |
312 | $genderCache = MediaWikiServices::getInstance()->getGenderCache(); |
313 | $gender = $genderCache->getGenderOf( $parserOptions->getUserIdentity(), __METHOD__ ); |
314 | } |
315 | } |
316 | } |
317 | return [ $lang->gender( $gender, $forms ) ]; |
318 | } |
319 | |
320 | /** |
321 | * formatNum handler |
322 | * @internal |
323 | * @param Language $lang |
324 | * @param array $args |
325 | * @return array |
326 | */ |
327 | public function formatNum( $lang, $args ) { |
328 | $num = $args[0]; |
329 | $this->checkType( 'formatNum', 1, $num, 'number' ); |
330 | if ( is_infinite( $num ) ) { |
331 | throw new LuaError( "bad argument #1 to 'formatNum' (infinite)" ); |
332 | } |
333 | if ( is_nan( $num ) ) { |
334 | throw new LuaError( "bad argument #1 to 'formatNum' (NaN)" ); |
335 | } |
336 | |
337 | $noCommafy = false; |
338 | if ( isset( $args[1] ) ) { |
339 | $this->checkType( 'formatNum', 2, $args[1], 'table' ); |
340 | $options = $args[1]; |
341 | $noCommafy = !empty( $options['noCommafy'] ); |
342 | } |
343 | if ( $noCommafy ) { |
344 | return [ $lang->formatNumNoSeparators( $num ) ]; |
345 | } else { |
346 | return [ $lang->formatNum( $num ) ]; |
347 | } |
348 | } |
349 | |
350 | /** |
351 | * formatDate handler |
352 | * @internal |
353 | * @param Language $lang |
354 | * @param array $args |
355 | * @return array |
356 | * @throws LuaError |
357 | */ |
358 | public function formatDate( $lang, $args ) { |
359 | $this->checkType( 'formatDate', 1, $args[0], 'string' ); |
360 | $this->checkTypeOptional( 'formatDate', 2, $args[1], 'string', '' ); |
361 | $this->checkTypeOptional( 'formatDate', 3, $args[2], 'boolean', false ); |
362 | |
363 | [ $format, $date, $local ] = $args; |
364 | $langcode = $lang->getCode(); |
365 | |
366 | if ( $date === '' ) { |
367 | $cacheKey = $this->getParserOptions()->getTimestamp(); |
368 | $timestamp = new MWTimestamp( $cacheKey ); |
369 | $date = $timestamp->getTimestamp( TS_ISO_8601 ); |
370 | $useTTL = true; |
371 | } else { |
372 | # Correct for DateTime interpreting 'XXXX' as XX:XX o'clock |
373 | if ( preg_match( '/^[0-9]{4}$/', $date ) ) { |
374 | $date = '00:00 ' . $date; |
375 | } |
376 | |
377 | $cacheKey = $date; |
378 | $useTTL = false; |
379 | } |
380 | |
381 | if ( isset( $this->timeCache[$format][$cacheKey][$langcode][$local] ) ) { |
382 | $ttl = $this->timeCache[$format][$cacheKey][$langcode][$local][1]; |
383 | if ( $useTTL && $ttl !== null ) { |
384 | $this->getEngine()->setTTL( $ttl ); |
385 | } |
386 | return [ $this->timeCache[$format][$cacheKey][$langcode][$local][0] ]; |
387 | } |
388 | |
389 | # Default input timezone is UTC. |
390 | try { |
391 | $utc = new DateTimeZone( 'UTC' ); |
392 | $dateObject = new DateTime( $date, $utc ); |
393 | } catch ( TimeoutException $ex ) { |
394 | // Unfortunately DateTime throws a generic Exception, but we can't |
395 | // ignore an exception generated by the RequestTimeout library. |
396 | throw $ex; |
397 | } catch ( Exception $ex ) { |
398 | throw new LuaError( "bad argument #2 to 'formatDate': invalid timestamp '$date'" ); |
399 | } |
400 | |
401 | # Set output timezone. |
402 | if ( $local ) { |
403 | $localtimezone = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::Localtimezone ); |
404 | if ( isset( $localtimezone ) ) { |
405 | $tz = new DateTimeZone( $localtimezone ); |
406 | } else { |
407 | $tz = new DateTimeZone( date_default_timezone_get() ); |
408 | } |
409 | } else { |
410 | $tz = $utc; |
411 | } |
412 | $dateObject->setTimezone( $tz ); |
413 | # Generate timestamp |
414 | $ts = $dateObject->format( 'YmdHis' ); |
415 | |
416 | if ( $ts < 0 ) { |
417 | throw new LuaError( "mw.language:formatDate() only supports years from 0" ); |
418 | } elseif ( $ts >= 100000000000000 ) { |
419 | throw new LuaError( "mw.language:formatDate() only supports years up to 9999" ); |
420 | } |
421 | |
422 | $ttl = null; |
423 | $ret = $lang->sprintfDate( $format, $ts, $tz, $ttl ); |
424 | $this->timeCache[$format][$cacheKey][$langcode][$local] = [ $ret, $ttl ]; |
425 | if ( $useTTL && $ttl !== null ) { |
426 | $this->getEngine()->setTTL( $ttl ); |
427 | } |
428 | return [ $ret ]; |
429 | } |
430 | |
431 | /** |
432 | * formatDuration handler |
433 | * @internal |
434 | * @param Language $lang |
435 | * @param array $args |
436 | * @return array |
437 | */ |
438 | public function formatDuration( $lang, $args ) { |
439 | $this->checkType( 'formatDuration', 1, $args[0], 'number' ); |
440 | $this->checkTypeOptional( 'formatDuration', 2, $args[1], 'table', [] ); |
441 | |
442 | [ $seconds, $chosenIntervals ] = $args; |
443 | $chosenIntervals = array_values( $chosenIntervals ); |
444 | |
445 | $ret = $lang->formatDuration( $seconds, $chosenIntervals ); |
446 | return [ $ret ]; |
447 | } |
448 | |
449 | /** |
450 | * getDurationIntervals handler |
451 | * @internal |
452 | * @param Language $lang |
453 | * @param array $args |
454 | * @return array |
455 | */ |
456 | public function getDurationIntervals( $lang, $args ) { |
457 | $this->checkType( 'getDurationIntervals', 1, $args[0], 'number' ); |
458 | $this->checkTypeOptional( 'getDurationIntervals', 2, $args[1], 'table', [] ); |
459 | |
460 | [ $seconds, $chosenIntervals ] = $args; |
461 | $chosenIntervals = array_values( $chosenIntervals ); |
462 | |
463 | $ret = $lang->getDurationIntervals( $seconds, $chosenIntervals ); |
464 | return [ $ret ]; |
465 | } |
466 | } |