Split some Language methods to LanguageNameUtils
[lhc/web/wiklou.git] / tests / phpunit / languages / LanguageTest.php
1 <?php
2
3 use Wikimedia\TestingAccessWrapper;
4
5 class LanguageTest extends LanguageClassesTestCase {
6 use LanguageNameUtilsTestTrait;
7
8 /** @var array Copy of $wgHooks from before we unset LanguageGetTranslatedLanguageNames */
9 private $origHooks;
10
11 public function setUp() {
12 global $wgHooks;
13
14 parent::setUp();
15
16 // Don't allow installed hooks to run, except if a test restores them via origHooks (needed
17 // for testIsKnownLanguageTag_cldr)
18 $this->origHooks = $wgHooks;
19 $newHooks = $wgHooks;
20 unset( $newHooks['LanguageGetTranslatedLanguageNames'] );
21 $this->setMwGlobals( 'wgHooks', $newHooks );
22 }
23
24 /**
25 * @covers Language::convertDoubleWidth
26 * @covers Language::normalizeForSearch
27 */
28 public function testLanguageConvertDoubleWidthToSingleWidth() {
29 $this->assertEquals(
30 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
31 $this->getLang()->normalizeForSearch(
32 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
33 ),
34 'convertDoubleWidth() with the full alphabet and digits'
35 );
36 }
37
38 /**
39 * @dataProvider provideFormattableTimes
40 * @covers Language::formatTimePeriod
41 */
42 public function testFormatTimePeriod( $seconds, $format, $expected, $desc ) {
43 $this->assertEquals( $expected, $this->getLang()->formatTimePeriod( $seconds, $format ), $desc );
44 }
45
46 public static function provideFormattableTimes() {
47 return [
48 [
49 9.45,
50 [],
51 '9.5 s',
52 'formatTimePeriod() rounding (<10s)'
53 ],
54 [
55 9.45,
56 [ 'noabbrevs' => true ],
57 '9.5 seconds',
58 'formatTimePeriod() rounding (<10s)'
59 ],
60 [
61 9.95,
62 [],
63 '10 s',
64 'formatTimePeriod() rounding (<10s)'
65 ],
66 [
67 9.95,
68 [ 'noabbrevs' => true ],
69 '10 seconds',
70 'formatTimePeriod() rounding (<10s)'
71 ],
72 [
73 59.55,
74 [],
75 '1 min 0 s',
76 'formatTimePeriod() rounding (<60s)'
77 ],
78 [
79 59.55,
80 [ 'noabbrevs' => true ],
81 '1 minute 0 seconds',
82 'formatTimePeriod() rounding (<60s)'
83 ],
84 [
85 119.55,
86 [],
87 '2 min 0 s',
88 'formatTimePeriod() rounding (<1h)'
89 ],
90 [
91 119.55,
92 [ 'noabbrevs' => true ],
93 '2 minutes 0 seconds',
94 'formatTimePeriod() rounding (<1h)'
95 ],
96 [
97 3599.55,
98 [],
99 '1 h 0 min 0 s',
100 'formatTimePeriod() rounding (<1h)'
101 ],
102 [
103 3599.55,
104 [ 'noabbrevs' => true ],
105 '1 hour 0 minutes 0 seconds',
106 'formatTimePeriod() rounding (<1h)'
107 ],
108 [
109 7199.55,
110 [],
111 '2 h 0 min 0 s',
112 'formatTimePeriod() rounding (>=1h)'
113 ],
114 [
115 7199.55,
116 [ 'noabbrevs' => true ],
117 '2 hours 0 minutes 0 seconds',
118 'formatTimePeriod() rounding (>=1h)'
119 ],
120 [
121 7199.55,
122 'avoidseconds',
123 '2 h 0 min',
124 'formatTimePeriod() rounding (>=1h), avoidseconds'
125 ],
126 [
127 7199.55,
128 [ 'avoid' => 'avoidseconds', 'noabbrevs' => true ],
129 '2 hours 0 minutes',
130 'formatTimePeriod() rounding (>=1h), avoidseconds'
131 ],
132 [
133 7199.55,
134 'avoidminutes',
135 '2 h 0 min',
136 'formatTimePeriod() rounding (>=1h), avoidminutes'
137 ],
138 [
139 7199.55,
140 [ 'avoid' => 'avoidminutes', 'noabbrevs' => true ],
141 '2 hours 0 minutes',
142 'formatTimePeriod() rounding (>=1h), avoidminutes'
143 ],
144 [
145 172799.55,
146 'avoidseconds',
147 '48 h 0 min',
148 'formatTimePeriod() rounding (=48h), avoidseconds'
149 ],
150 [
151 172799.55,
152 [ 'avoid' => 'avoidseconds', 'noabbrevs' => true ],
153 '48 hours 0 minutes',
154 'formatTimePeriod() rounding (=48h), avoidseconds'
155 ],
156 [
157 259199.55,
158 'avoidhours',
159 '3 d',
160 'formatTimePeriod() rounding (>48h), avoidhours'
161 ],
162 [
163 259199.55,
164 [ 'avoid' => 'avoidhours', 'noabbrevs' => true ],
165 '3 days',
166 'formatTimePeriod() rounding (>48h), avoidhours'
167 ],
168 [
169 259199.55,
170 'avoidminutes',
171 '3 d 0 h',
172 'formatTimePeriod() rounding (>48h), avoidminutes'
173 ],
174 [
175 259199.55,
176 [ 'avoid' => 'avoidminutes', 'noabbrevs' => true ],
177 '3 days 0 hours',
178 'formatTimePeriod() rounding (>48h), avoidminutes'
179 ],
180 [
181 176399.55,
182 'avoidseconds',
183 '2 d 1 h 0 min',
184 'formatTimePeriod() rounding (>48h), avoidseconds'
185 ],
186 [
187 176399.55,
188 [ 'avoid' => 'avoidseconds', 'noabbrevs' => true ],
189 '2 days 1 hour 0 minutes',
190 'formatTimePeriod() rounding (>48h), avoidseconds'
191 ],
192 [
193 176399.55,
194 'avoidminutes',
195 '2 d 1 h',
196 'formatTimePeriod() rounding (>48h), avoidminutes'
197 ],
198 [
199 176399.55,
200 [ 'avoid' => 'avoidminutes', 'noabbrevs' => true ],
201 '2 days 1 hour',
202 'formatTimePeriod() rounding (>48h), avoidminutes'
203 ],
204 [
205 259199.55,
206 'avoidseconds',
207 '3 d 0 h 0 min',
208 'formatTimePeriod() rounding (>48h), avoidseconds'
209 ],
210 [
211 259199.55,
212 [ 'avoid' => 'avoidseconds', 'noabbrevs' => true ],
213 '3 days 0 hours 0 minutes',
214 'formatTimePeriod() rounding (>48h), avoidseconds'
215 ],
216 [
217 172801.55,
218 'avoidseconds',
219 '2 d 0 h 0 min',
220 'formatTimePeriod() rounding, (>48h), avoidseconds'
221 ],
222 [
223 172801.55,
224 [ 'avoid' => 'avoidseconds', 'noabbrevs' => true ],
225 '2 days 0 hours 0 minutes',
226 'formatTimePeriod() rounding, (>48h), avoidseconds'
227 ],
228 [
229 176460.55,
230 [],
231 '2 d 1 h 1 min 1 s',
232 'formatTimePeriod() rounding, recursion, (>48h)'
233 ],
234 [
235 176460.55,
236 [ 'noabbrevs' => true ],
237 '2 days 1 hour 1 minute 1 second',
238 'formatTimePeriod() rounding, recursion, (>48h)'
239 ],
240 ];
241 }
242
243 /**
244 * @covers Language::truncateForDatabase
245 * @covers Language::truncateInternal
246 */
247 public function testTruncateForDatabase() {
248 $this->assertEquals(
249 "XXX",
250 $this->getLang()->truncateForDatabase( "1234567890", 0, 'XXX' ),
251 'truncate prefix, len 0, small ellipsis'
252 );
253
254 $this->assertEquals(
255 "12345XXX",
256 $this->getLang()->truncateForDatabase( "1234567890", 8, 'XXX' ),
257 'truncate prefix, small ellipsis'
258 );
259
260 $this->assertEquals(
261 "123456789",
262 $this->getLang()->truncateForDatabase( "123456789", 5, 'XXXXXXXXXXXXXXX' ),
263 'truncate prefix, large ellipsis'
264 );
265
266 $this->assertEquals(
267 "XXX67890",
268 $this->getLang()->truncateForDatabase( "1234567890", -8, 'XXX' ),
269 'truncate suffix, small ellipsis'
270 );
271
272 $this->assertEquals(
273 "123456789",
274 $this->getLang()->truncateForDatabase( "123456789", -5, 'XXXXXXXXXXXXXXX' ),
275 'truncate suffix, large ellipsis'
276 );
277 $this->assertEquals(
278 "123XXX",
279 $this->getLang()->truncateForDatabase( "123 ", 9, 'XXX' ),
280 'truncate prefix, with spaces'
281 );
282 $this->assertEquals(
283 "12345XXX",
284 $this->getLang()->truncateForDatabase( "12345 8", 11, 'XXX' ),
285 'truncate prefix, with spaces and non-space ending'
286 );
287 $this->assertEquals(
288 "XXX234",
289 $this->getLang()->truncateForDatabase( "1 234", -8, 'XXX' ),
290 'truncate suffix, with spaces'
291 );
292 $this->assertEquals(
293 "12345XXX",
294 $this->getLang()->truncateForDatabase( "1234567890", 5, 'XXX', false ),
295 'truncate without adjustment'
296 );
297 $this->assertEquals(
298 "泰乐菌...",
299 $this->getLang()->truncateForDatabase( "泰乐菌素123456789", 11, '...', false ),
300 'truncate does not chop Unicode characters in half'
301 );
302 $this->assertEquals(
303 "\n泰乐菌...",
304 $this->getLang()->truncateForDatabase( "\n泰乐菌素123456789", 12, '...', false ),
305 'truncate does not chop Unicode characters in half if there is a preceding newline'
306 );
307 }
308
309 /**
310 * @dataProvider provideTruncateData
311 * @covers Language::truncateForVisual
312 * @covers Language::truncateInternal
313 */
314 public function testTruncateForVisual(
315 $expected, $string, $length, $ellipsis = '...', $adjustLength = true
316 ) {
317 $this->assertEquals(
318 $expected,
319 $this->getLang()->truncateForVisual( $string, $length, $ellipsis, $adjustLength )
320 );
321 }
322
323 /**
324 * @return array Format is ($expected, $string, $length, $ellipsis, $adjustLength)
325 */
326 public static function provideTruncateData() {
327 return [
328 [ "XXX", "тестирам да ли ради", 0, "XXX" ],
329 [ "testnXXX", "testni scenarij", 8, "XXX" ],
330 [ "حالة اختبار", "حالة اختبار", 5, "XXXXXXXXXXXXXXX" ],
331 [ "XXXедент", "прецедент", -8, "XXX" ],
332 [ "XXപിൾ", "ആപ്പിൾ", -5, "XX" ],
333 [ "神秘XXX", "神秘 ", 9, "XXX" ],
334 [ "ΔημιουργXXX", "Δημιουργία Σύμπαντος", 11, "XXX" ],
335 [ "XXXの家です", "地球は私たちの唯 の家です", -8, "XXX" ],
336 [ "زندگیXXX", "زندگی زیباست", 6, "XXX", false ],
337 [ "ცხოვრება...", "ცხოვრება არის საოცარი", 8, "...", false ],
338 [ "\nທ່ານ...", "\nທ່ານບໍ່ຮູ້ຫນັງສື", 5, "...", false ],
339 ];
340 }
341
342 /**
343 * @dataProvider provideHTMLTruncateData
344 * @covers Language::truncateHTML
345 */
346 public function testTruncateHtml( $len, $ellipsis, $input, $expected ) {
347 // Actual HTML...
348 $this->assertEquals(
349 $expected,
350 $this->getLang()->truncateHtml( $input, $len, $ellipsis )
351 );
352 }
353
354 /**
355 * @return array Format is ($len, $ellipsis, $input, $expected)
356 */
357 public static function provideHTMLTruncateData() {
358 return [
359 [ 0, 'XXX', "1234567890", "XXX" ],
360 [ 8, 'XXX', "1234567890", "12345XXX" ],
361 [ 5, 'XXXXXXXXXXXXXXX', '1234567890', "1234567890" ],
362 [ 2, '***',
363 '<p><span style="font-weight:bold;"></span></p>',
364 '<p><span style="font-weight:bold;"></span></p>',
365 ],
366 [ 2, '***',
367 '<p><span style="font-weight:bold;">123456789</span></p>',
368 '<p><span style="font-weight:bold;">***</span></p>',
369 ],
370 [ 2, '***',
371 '<p><span style="font-weight:bold;">&nbsp;23456789</span></p>',
372 '<p><span style="font-weight:bold;">***</span></p>',
373 ],
374 [ 3, '***',
375 '<p><span style="font-weight:bold;">123456789</span></p>',
376 '<p><span style="font-weight:bold;">***</span></p>',
377 ],
378 [ 4, '***',
379 '<p><span style="font-weight:bold;">123456789</span></p>',
380 '<p><span style="font-weight:bold;">1***</span></p>',
381 ],
382 [ 5, '***',
383 '<tt><span style="font-weight:bold;">123456789</span></tt>',
384 '<tt><span style="font-weight:bold;">12***</span></tt>',
385 ],
386 [ 6, '***',
387 '<p><a href="www.mediawiki.org">123456789</a></p>',
388 '<p><a href="www.mediawiki.org">123***</a></p>',
389 ],
390 [ 6, '***',
391 '<p><a href="www.mediawiki.org">12&nbsp;456789</a></p>',
392 '<p><a href="www.mediawiki.org">12&nbsp;***</a></p>',
393 ],
394 [ 7, '***',
395 '<small><span style="font-weight:bold;">123<p id="#moo">456</p>789</span></small>',
396 '<small><span style="font-weight:bold;">123<p id="#moo">4***</p></span></small>',
397 ],
398 [ 8, '***',
399 '<div><span style="font-weight:bold;">123<span>4</span>56789</span></div>',
400 '<div><span style="font-weight:bold;">123<span>4</span>5***</span></div>',
401 ],
402 [ 9, '***',
403 '<p><table style="font-weight:bold;"><tr><td>123456789</td></tr></table></p>',
404 '<p><table style="font-weight:bold;"><tr><td>123456789</td></tr></table></p>',
405 ],
406 [ 10, '***',
407 '<p><font style="font-weight:bold;">123456789</font></p>',
408 '<p><font style="font-weight:bold;">123456789</font></p>',
409 ],
410 ];
411 }
412
413 /**
414 * Test Language::isWellFormedLanguageTag()
415 * @dataProvider provideWellFormedLanguageTags
416 * @covers Language::isWellFormedLanguageTag
417 */
418 public function testWellFormedLanguageTag( $code, $message = '' ) {
419 $this->assertTrue(
420 Language::isWellFormedLanguageTag( $code ),
421 "validating code $code $message"
422 );
423 }
424
425 /**
426 * The test cases are based on the tests in the GaBuZoMeu parser
427 * written by Stéphane Bortzmeyer <bortzmeyer@nic.fr>
428 * and distributed as free software, under the GNU General Public Licence.
429 * http://www.bortzmeyer.org/gabuzomeu-parsing-language-tags.html
430 */
431 public static function provideWellFormedLanguageTags() {
432 return [
433 [ 'fr', 'two-letter code' ],
434 [ 'fr-latn', 'two-letter code with lower case script code' ],
435 [ 'fr-Latn-FR', 'two-letter code with title case script code and uppercase country code' ],
436 [ 'fr-Latn-419', 'two-letter code with title case script code and region number' ],
437 [ 'fr-FR', 'two-letter code with uppercase' ],
438 [ 'ax-TZ', 'Not in the registry, but well-formed' ],
439 [ 'fr-shadok', 'two-letter code with variant' ],
440 [ 'fr-y-myext-myext2', 'non-x singleton' ],
441 [ 'fra-Latn', 'ISO 639 can be 3-letters' ],
442 [ 'fra', 'three-letter language code' ],
443 [ 'fra-FX', 'three-letter language code with country code' ],
444 [ 'i-klingon', 'grandfathered with singleton' ],
445 [ 'I-kLINgon', 'tags are case-insensitive...' ],
446 [ 'no-bok', 'grandfathered without singleton' ],
447 [ 'i-enochian', 'Grandfathered' ],
448 [ 'x-fr-CH', 'private use' ],
449 [ 'es-419', 'two-letter code with region number' ],
450 [ 'en-Latn-GB-boont-r-extended-sequence-x-private', 'weird, but well-formed' ],
451 [ 'ab-x-abc-x-abc', 'anything goes after x' ],
452 [ 'ab-x-abc-a-a', 'anything goes after x, including several non-x singletons' ],
453 [ 'i-default', 'grandfathered' ],
454 [ 'abcd-Latn', 'Language of 4 chars reserved for future use' ],
455 [ 'AaBbCcDd-x-y-any-x', 'Language of 5-8 chars, registered' ],
456 [ 'de-CH-1901', 'with country and year' ],
457 [ 'en-US-x-twain', 'with country and singleton' ],
458 [ 'zh-cmn', 'three-letter variant' ],
459 [ 'zh-cmn-Hant', 'three-letter variant and script' ],
460 [ 'zh-cmn-Hant-HK', 'three-letter variant, script and country' ],
461 [ 'xr-p-lze', 'Extension' ],
462 ];
463 }
464
465 /**
466 * Negative test for Language::isWellFormedLanguageTag()
467 * @dataProvider provideMalformedLanguageTags
468 * @covers Language::isWellFormedLanguageTag
469 */
470 public function testMalformedLanguageTag( $code, $message = '' ) {
471 $this->assertFalse(
472 Language::isWellFormedLanguageTag( $code ),
473 "validating that code $code is a malformed language tag - $message"
474 );
475 }
476
477 /**
478 * The test cases are based on the tests in the GaBuZoMeu parser
479 * written by Stéphane Bortzmeyer <bortzmeyer@nic.fr>
480 * and distributed as free software, under the GNU General Public Licence.
481 * http://www.bortzmeyer.org/gabuzomeu-parsing-language-tags.html
482 */
483 public static function provideMalformedLanguageTags() {
484 return [
485 [ 'f', 'language too short' ],
486 [ 'f-Latn', 'language too short with script' ],
487 [ 'xr-lxs-qut', 'variants too short' ], # extlangS
488 [ 'fr-Latn-F', 'region too short' ],
489 [ 'a-value', 'language too short with region' ],
490 [ 'tlh-a-b-foo', 'valid three-letter with wrong variant' ],
491 [
492 'i-notexist',
493 'grandfathered but not registered: invalid, even if we only test well-formedness'
494 ],
495 [ 'abcdefghi-012345678', 'numbers too long' ],
496 [ 'ab-abc-abc-abc-abc', 'invalid extensions' ],
497 [ 'ab-abcd-abc', 'invalid extensions' ],
498 [ 'ab-ab-abc', 'invalid extensions' ],
499 [ 'ab-123-abc', 'invalid extensions' ],
500 [ 'a-Hant-ZH', 'short language with valid extensions' ],
501 [ 'a1-Hant-ZH', 'invalid character in language' ],
502 [ 'ab-abcde-abc', 'invalid extensions' ],
503 [ 'ab-1abc-abc', 'invalid characters in extensions' ],
504 [ 'ab-ab-abcd', 'invalid order of extensions' ],
505 [ 'ab-123-abcd', 'invalid order of extensions' ],
506 [ 'ab-abcde-abcd', 'invalid extensions' ],
507 [ 'ab-1abc-abcd', 'invalid characters in extensions' ],
508 [ 'ab-a-b', 'extensions too short' ],
509 [ 'ab-a-x', 'extensions too short, even with singleton' ],
510 [ 'ab--ab', 'two separators' ],
511 [ 'ab-abc-', 'separator in the end' ],
512 [ '-ab-abc', 'separator in the beginning' ],
513 [ 'abcd-efg', 'language too long' ],
514 [ 'aabbccddE', 'tag too long' ],
515 [ 'pa_guru', 'A tag with underscore is invalid in strict mode' ],
516 [ 'de-f', 'subtag too short' ],
517 ];
518 }
519
520 /**
521 * Negative test for Language::isWellFormedLanguageTag()
522 * @covers Language::isWellFormedLanguageTag
523 */
524 public function testLenientLanguageTag() {
525 $this->assertTrue(
526 Language::isWellFormedLanguageTag( 'pa_guru', true ),
527 'pa_guru is a well-formed language tag in lenient mode'
528 );
529 }
530
531 /**
532 * Test too short timestamp
533 * @expectedException MWException
534 * @covers Language::sprintfDate
535 */
536 public function testSprintfDateTooShortTimestamp() {
537 $this->getLang()->sprintfDate( 'xiY', '1234567890123' );
538 }
539
540 /**
541 * Test too long timestamp
542 * @expectedException MWException
543 * @covers Language::sprintfDate
544 */
545 public function testSprintfDateTooLongTimestamp() {
546 $this->getLang()->sprintfDate( 'xiY', '123456789012345' );
547 }
548
549 /**
550 * Test too short timestamp
551 * @expectedException MWException
552 * @covers Language::sprintfDate
553 */
554 public function testSprintfDateNotAllDigitTimestamp() {
555 $this->getLang()->sprintfDate( 'xiY', '-1234567890123' );
556 }
557
558 /**
559 * @dataProvider provideSprintfDateSamples
560 * @covers Language::sprintfDate
561 */
562 public function testSprintfDate( $format, $ts, $expected, $msg ) {
563 $ttl = null;
564 $this->assertEquals(
565 $expected,
566 $this->getLang()->sprintfDate( $format, $ts, null, $ttl ),
567 "sprintfDate('$format', '$ts'): $msg"
568 );
569 if ( $ttl ) {
570 $dt = new DateTime( $ts );
571 $lastValidTS = $dt->add( new DateInterval( 'PT' . ( $ttl - 1 ) . 'S' ) )->format( 'YmdHis' );
572 $this->assertEquals(
573 $expected,
574 $this->getLang()->sprintfDate( $format, $lastValidTS, null ),
575 "sprintfDate('$format', '$ts'): TTL $ttl too high (output was different at $lastValidTS)"
576 );
577 } else {
578 // advance the time enough to make all of the possible outputs different (except possibly L)
579 $dt = new DateTime( $ts );
580 $newTS = $dt->add( new DateInterval( 'P1Y1M8DT13H1M1S' ) )->format( 'YmdHis' );
581 $this->assertEquals(
582 $expected,
583 $this->getLang()->sprintfDate( $format, $newTS, null ),
584 "sprintfDate('$format', '$ts'): Missing TTL (output was different at $newTS)"
585 );
586 }
587 }
588
589 /**
590 * sprintfDate should always use UTC when no zone is given.
591 * @dataProvider provideSprintfDateSamples
592 * @covers Language::sprintfDate
593 */
594 public function testSprintfDateNoZone( $format, $ts, $expected, $ignore, $msg ) {
595 $oldTZ = date_default_timezone_get();
596 $res = date_default_timezone_set( 'Asia/Seoul' );
597 if ( !$res ) {
598 $this->markTestSkipped( "Error setting Timezone" );
599 }
600
601 $this->assertEquals(
602 $expected,
603 $this->getLang()->sprintfDate( $format, $ts ),
604 "sprintfDate('$format', '$ts'): $msg"
605 );
606
607 date_default_timezone_set( $oldTZ );
608 }
609
610 /**
611 * sprintfDate should use passed timezone
612 * @dataProvider provideSprintfDateSamples
613 * @covers Language::sprintfDate
614 */
615 public function testSprintfDateTZ( $format, $ts, $ignore, $expected, $msg ) {
616 $tz = new DateTimeZone( 'Asia/Seoul' );
617 if ( !$tz ) {
618 $this->markTestSkipped( "Error getting Timezone" );
619 }
620
621 $this->assertEquals(
622 $expected,
623 $this->getLang()->sprintfDate( $format, $ts, $tz ),
624 "sprintfDate('$format', '$ts', 'Asia/Seoul'): $msg"
625 );
626 }
627
628 /**
629 * sprintfDate should only calculate a TTL if the caller is going to use it.
630 * @covers Language::sprintfDate
631 */
632 public function testSprintfDateNoTtlIfNotNeeded() {
633 $noTtl = 'unused'; // Value used to represent that the caller didn't pass a variable in.
634 $ttl = null;
635 $this->getLang()->sprintfDate( 'YmdHis', wfTimestampNow(), null, $noTtl );
636 $this->getLang()->sprintfDate( 'YmdHis', wfTimestampNow(), null, $ttl );
637
638 $this->assertSame(
639 'unused',
640 $noTtl,
641 'If the caller does not set the $ttl variable, do not compute it.'
642 );
643 $this->assertInternalType( 'int', $ttl, 'TTL should have been computed.' );
644 }
645
646 public static function provideSprintfDateSamples() {
647 return [
648 [
649 'xiY',
650 '20111212000000',
651 '1390', // note because we're testing English locale we get Latin-standard digits
652 '1390',
653 'Iranian calendar full year'
654 ],
655 [
656 'xiy',
657 '20111212000000',
658 '90',
659 '90',
660 'Iranian calendar short year'
661 ],
662 [
663 'o',
664 '20120101235000',
665 '2011',
666 '2011',
667 'ISO 8601 (week) year'
668 ],
669 [
670 'W',
671 '20120101235000',
672 '52',
673 '52',
674 'Week number'
675 ],
676 [
677 'W',
678 '20120102235000',
679 '1',
680 '1',
681 'Week number'
682 ],
683 [
684 'o-\\WW-N',
685 '20091231235000',
686 '2009-W53-4',
687 '2009-W53-4',
688 'leap week'
689 ],
690 // What follows is mostly copied from
691 // https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions#.23time
692 [
693 'Y',
694 '20120102090705',
695 '2012',
696 '2012',
697 'Full year'
698 ],
699 [
700 'y',
701 '20120102090705',
702 '12',
703 '12',
704 '2 digit year'
705 ],
706 [
707 'L',
708 '20120102090705',
709 '1',
710 '1',
711 'Leap year'
712 ],
713 [
714 'n',
715 '20120102090705',
716 '1',
717 '1',
718 'Month index, not zero pad'
719 ],
720 [
721 'N',
722 '20120102090705',
723 '01',
724 '01',
725 'Month index. Zero pad'
726 ],
727 [
728 'M',
729 '20120102090705',
730 'Jan',
731 'Jan',
732 'Month abbrev'
733 ],
734 [
735 'F',
736 '20120102090705',
737 'January',
738 'January',
739 'Full month'
740 ],
741 [
742 'xg',
743 '20120102090705',
744 'January',
745 'January',
746 'Genitive month name (same in EN)'
747 ],
748 [
749 'j',
750 '20120102090705',
751 '2',
752 '2',
753 'Day of month (not zero pad)'
754 ],
755 [
756 'd',
757 '20120102090705',
758 '02',
759 '02',
760 'Day of month (zero-pad)'
761 ],
762 [
763 'z',
764 '20120102090705',
765 '1',
766 '1',
767 'Day of year (zero-indexed)'
768 ],
769 [
770 'D',
771 '20120102090705',
772 'Mon',
773 'Mon',
774 'Day of week (abbrev)'
775 ],
776 [
777 'l',
778 '20120102090705',
779 'Monday',
780 'Monday',
781 'Full day of week'
782 ],
783 [
784 'N',
785 '20120101090705',
786 '7',
787 '7',
788 'Day of week (Mon=1, Sun=7)'
789 ],
790 [
791 'w',
792 '20120101090705',
793 '0',
794 '0',
795 'Day of week (Sun=0, Sat=6)'
796 ],
797 [
798 'N',
799 '20120102090705',
800 '1',
801 '1',
802 'Day of week'
803 ],
804 [
805 'a',
806 '20120102090705',
807 'am',
808 'am',
809 'am vs pm'
810 ],
811 [
812 'A',
813 '20120102120000',
814 'PM',
815 'PM',
816 'AM vs PM'
817 ],
818 [
819 'a',
820 '20120102000000',
821 'am',
822 'am',
823 'AM vs PM'
824 ],
825 [
826 'g',
827 '20120102090705',
828 '9',
829 '9',
830 '12 hour, not Zero'
831 ],
832 [
833 'h',
834 '20120102090705',
835 '09',
836 '09',
837 '12 hour, zero padded'
838 ],
839 [
840 'G',
841 '20120102090705',
842 '9',
843 '9',
844 '24 hour, not zero'
845 ],
846 [
847 'H',
848 '20120102090705',
849 '09',
850 '09',
851 '24 hour, zero'
852 ],
853 [
854 'H',
855 '20120102110705',
856 '11',
857 '11',
858 '24 hour, zero'
859 ],
860 [
861 'i',
862 '20120102090705',
863 '07',
864 '07',
865 'Minutes'
866 ],
867 [
868 's',
869 '20120102090705',
870 '05',
871 '05',
872 'seconds'
873 ],
874 [
875 'U',
876 '20120102090705',
877 '1325495225',
878 '1325462825',
879 'unix time'
880 ],
881 [
882 't',
883 '20120102090705',
884 '31',
885 '31',
886 'Days in current month'
887 ],
888 [
889 'c',
890 '20120102090705',
891 '2012-01-02T09:07:05+00:00',
892 '2012-01-02T09:07:05+09:00',
893 'ISO 8601 timestamp'
894 ],
895 [
896 'r',
897 '20120102090705',
898 'Mon, 02 Jan 2012 09:07:05 +0000',
899 'Mon, 02 Jan 2012 09:07:05 +0900',
900 'RFC 5322'
901 ],
902 [
903 'e',
904 '20120102090705',
905 'UTC',
906 'Asia/Seoul',
907 'Timezone identifier'
908 ],
909 [
910 'I',
911 '19880602090705',
912 '0',
913 '1',
914 'DST indicator'
915 ],
916 [
917 'O',
918 '20120102090705',
919 '+0000',
920 '+0900',
921 'Timezone offset'
922 ],
923 [
924 'P',
925 '20120102090705',
926 '+00:00',
927 '+09:00',
928 'Timezone offset with colon'
929 ],
930 [
931 'T',
932 '20120102090705',
933 'UTC',
934 'KST',
935 'Timezone abbreviation'
936 ],
937 [
938 'Z',
939 '20120102090705',
940 '0',
941 '32400',
942 'Timezone offset in seconds'
943 ],
944 [
945 'xmj xmF xmn xmY',
946 '20120102090705',
947 '7 Safar 2 1433',
948 '7 Safar 2 1433',
949 'Islamic'
950 ],
951 [
952 'xij xiF xin xiY',
953 '20120102090705',
954 '12 Dey 10 1390',
955 '12 Dey 10 1390',
956 'Iranian'
957 ],
958 [
959 'xjj xjF xjn xjY',
960 '20120102090705',
961 '7 Tevet 4 5772',
962 '7 Tevet 4 5772',
963 'Hebrew'
964 ],
965 [
966 'xjt',
967 '20120102090705',
968 '29',
969 '29',
970 'Hebrew number of days in month'
971 ],
972 [
973 'xjx',
974 '20120102090705',
975 'Tevet',
976 'Tevet',
977 'Hebrew genitive month name (No difference in EN)'
978 ],
979 [
980 'xkY',
981 '20120102090705',
982 '2555',
983 '2555',
984 'Thai year'
985 ],
986 [
987 'xkY',
988 '19410101090705',
989 '2484',
990 '2484',
991 'Thai year'
992 ],
993 [
994 'xoY',
995 '20120102090705',
996 '101',
997 '101',
998 'Minguo'
999 ],
1000 [
1001 'xtY',
1002 '20120102090705',
1003 '平成24',
1004 '平成24',
1005 'nengo'
1006 ],
1007 [
1008 'xtY',
1009 '20190430235959',
1010 '平成31',
1011 '平成31',
1012 'nengo - last day of heisei'
1013 ],
1014 [
1015 'xtY',
1016 '20190501000000',
1017 '令和元',
1018 '令和元',
1019 'nengo - first day of reiwa'
1020 ],
1021 [
1022 'xtY',
1023 '20200501000000',
1024 '令和2',
1025 '令和2',
1026 'nengo - second year of reiwa'
1027 ],
1028 [
1029 'xrxkYY',
1030 '20120102090705',
1031 'MMDLV2012',
1032 'MMDLV2012',
1033 'Roman numerals'
1034 ],
1035 [
1036 'xhxjYY',
1037 '20120102090705',
1038 \'תשע"ב2012',
1039 \'תשע"ב2012',
1040 'Hebrew numberals'
1041 ],
1042 [
1043 'xnY',
1044 '20120102090705',
1045 '2012',
1046 '2012',
1047 'Raw numerals (doesn\'t mean much in EN)'
1048 ],
1049 [
1050 '[[Y "(yea"\\r)]] \\"xx\\"',
1051 '20120102090705',
1052 '[[2012 (year)]] "x"',
1053 '[[2012 (year)]] "x"',
1054 'Various escaping'
1055 ],
1056
1057 ];
1058 }
1059
1060 /**
1061 * @dataProvider provideFormatSizes
1062 * @covers Language::formatSize
1063 */
1064 public function testFormatSize( $size, $expected, $msg ) {
1065 $this->assertEquals(
1066 $expected,
1067 $this->getLang()->formatSize( $size ),
1068 "formatSize('$size'): $msg"
1069 );
1070 }
1071
1072 public static function provideFormatSizes() {
1073 return [
1074 [
1075 0,
1076 "0 bytes",
1077 "Zero bytes"
1078 ],
1079 [
1080 1024,
1081 "1 KB",
1082 "1 kilobyte"
1083 ],
1084 [
1085 1024 * 1024,
1086 "1 MB",
1087 "1,024 megabytes"
1088 ],
1089 [
1090 1024 * 1024 * 1024,
1091 "1 GB",
1092 "1 gigabyte"
1093 ],
1094 [
1095 1024 ** 4,
1096 "1 TB",
1097 "1 terabyte"
1098 ],
1099 [
1100 1024 ** 5,
1101 "1 PB",
1102 "1 petabyte"
1103 ],
1104 [
1105 1024 ** 6,
1106 "1 EB",
1107 "1,024 exabyte"
1108 ],
1109 [
1110 1024 ** 7,
1111 "1 ZB",
1112 "1 zetabyte"
1113 ],
1114 [
1115 1024 ** 8,
1116 "1 YB",
1117 "1 yottabyte"
1118 ],
1119 // How big!? THIS BIG!
1120 ];
1121 }
1122
1123 /**
1124 * @dataProvider provideFormatBitrate
1125 * @covers Language::formatBitrate
1126 */
1127 public function testFormatBitrate( $bps, $expected, $msg ) {
1128 $this->assertEquals(
1129 $expected,
1130 $this->getLang()->formatBitrate( $bps ),
1131 "formatBitrate('$bps'): $msg"
1132 );
1133 }
1134
1135 public static function provideFormatBitrate() {
1136 return [
1137 [
1138 0,
1139 "0 bps",
1140 "0 bits per second"
1141 ],
1142 [
1143 999,
1144 "999 bps",
1145 "999 bits per second"
1146 ],
1147 [
1148 1000,
1149 "1 kbps",
1150 "1 kilobit per second"
1151 ],
1152 [
1153 1000 * 1000,
1154 "1 Mbps",
1155 "1 megabit per second"
1156 ],
1157 [
1158 10 ** 9,
1159 "1 Gbps",
1160 "1 gigabit per second"
1161 ],
1162 [
1163 10 ** 12,
1164 "1 Tbps",
1165 "1 terabit per second"
1166 ],
1167 [
1168 10 ** 15,
1169 "1 Pbps",
1170 "1 petabit per second"
1171 ],
1172 [
1173 10 ** 18,
1174 "1 Ebps",
1175 "1 exabit per second"
1176 ],
1177 [
1178 10 ** 21,
1179 "1 Zbps",
1180 "1 zetabit per second"
1181 ],
1182 [
1183 10 ** 24,
1184 "1 Ybps",
1185 "1 yottabit per second"
1186 ],
1187 [
1188 10 ** 27,
1189 "1,000 Ybps",
1190 "1,000 yottabits per second"
1191 ],
1192 ];
1193 }
1194
1195 /**
1196 * @dataProvider provideFormatDuration
1197 * @covers Language::formatDuration
1198 */
1199 public function testFormatDuration( $duration, $expected, $intervals = [] ) {
1200 $this->assertEquals(
1201 $expected,
1202 $this->getLang()->formatDuration( $duration, $intervals ),
1203 "formatDuration('$duration'): $expected"
1204 );
1205 }
1206
1207 public static function provideFormatDuration() {
1208 return [
1209 [
1210 0,
1211 '0 seconds',
1212 ],
1213 [
1214 1,
1215 '1 second',
1216 ],
1217 [
1218 2,
1219 '2 seconds',
1220 ],
1221 [
1222 60,
1223 '1 minute',
1224 ],
1225 [
1226 2 * 60,
1227 '2 minutes',
1228 ],
1229 [
1230 3600,
1231 '1 hour',
1232 ],
1233 [
1234 2 * 3600,
1235 '2 hours',
1236 ],
1237 [
1238 24 * 3600,
1239 '1 day',
1240 ],
1241 [
1242 2 * 86400,
1243 '2 days',
1244 ],
1245 [
1246 // ( 365 + ( 24 * 3 + 25 ) / 400 ) * 86400 = 31556952
1247 ( 365 + ( 24 * 3 + 25 ) / 400.0 ) * 86400,
1248 '1 year',
1249 ],
1250 [
1251 2 * 31556952,
1252 '2 years',
1253 ],
1254 [
1255 10 * 31556952,
1256 '1 decade',
1257 ],
1258 [
1259 20 * 31556952,
1260 '2 decades',
1261 ],
1262 [
1263 100 * 31556952,
1264 '1 century',
1265 ],
1266 [
1267 200 * 31556952,
1268 '2 centuries',
1269 ],
1270 [
1271 1000 * 31556952,
1272 '1 millennium',
1273 ],
1274 [
1275 2000 * 31556952,
1276 '2 millennia',
1277 ],
1278 [
1279 9001,
1280 '2 hours, 30 minutes and 1 second'
1281 ],
1282 [
1283 3601,
1284 '1 hour and 1 second'
1285 ],
1286 [
1287 31556952 + 2 * 86400 + 9000,
1288 '1 year, 2 days, 2 hours and 30 minutes'
1289 ],
1290 [
1291 42 * 1000 * 31556952 + 42,
1292 '42 millennia and 42 seconds'
1293 ],
1294 [
1295 60,
1296 '60 seconds',
1297 [ 'seconds' ],
1298 ],
1299 [
1300 61,
1301 '61 seconds',
1302 [ 'seconds' ],
1303 ],
1304 [
1305 1,
1306 '1 second',
1307 [ 'seconds' ],
1308 ],
1309 [
1310 31556952 + 2 * 86400 + 9000,
1311 '1 year, 2 days and 150 minutes',
1312 [ 'years', 'days', 'minutes' ],
1313 ],
1314 [
1315 42,
1316 '0 days',
1317 [ 'years', 'days' ],
1318 ],
1319 [
1320 31556952 + 2 * 86400 + 9000,
1321 '1 year, 2 days and 150 minutes',
1322 [ 'minutes', 'days', 'years' ],
1323 ],
1324 [
1325 42,
1326 '0 days',
1327 [ 'days', 'years' ],
1328 ],
1329 ];
1330 }
1331
1332 /**
1333 * @dataProvider provideCheckTitleEncodingData
1334 * @covers Language::checkTitleEncoding
1335 */
1336 public function testCheckTitleEncoding( $s ) {
1337 $this->assertEquals(
1338 $s,
1339 $this->getLang()->checkTitleEncoding( $s ),
1340 "checkTitleEncoding('$s')"
1341 );
1342 }
1343
1344 public static function provideCheckTitleEncodingData() {
1345 // phpcs:disable Generic.Files.LineLength
1346 return [
1347 [ "" ],
1348 [ "United States of America" ], // 7bit ASCII
1349 [ rawurldecode( "S%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e" ) ],
1350 [
1351 rawurldecode(
1352 "Acteur%7CAlbert%20Robbins%7CAnglais%7CAnn%20Donahue%7CAnthony%20E.%20Zuiker%7CCarol%20Mendelsohn"
1353 )
1354 ],
1355 // The following two data sets come from T38839. They fail if checkTitleEncoding uses a regexp to test for
1356 // valid UTF-8 encoding and the pcre.recursion_limit is low (like, say, 1024). They succeed if checkTitleEncoding
1357 // uses mb_check_encoding for its test.
1358 [
1359 rawurldecode(
1360 "Acteur%7CAlbert%20Robbins%7CAnglais%7CAnn%20Donahue%7CAnthony%20E.%20Zuiker%7CCarol%20Mendelsohn%7C"
1361 . "Catherine%20Willows%7CDavid%20Hodges%7CDavid%20Phillips%7CGil%20Grissom%7CGreg%20Sanders%7CHodges%7C"
1362 . "Internet%20Movie%20Database%7CJim%20Brass%7CLady%20Heather%7C"
1363 . "Les%20Experts%20(s%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e)%7CLes%20Experts%20:%20Manhattan%7C"
1364 . "Les%20Experts%20:%20Miami%7CListe%20des%20personnages%20des%20Experts%7C"
1365 . "Liste%20des%20%C3%A9pisodes%20des%20Experts%7CMod%C3%A8le%20discussion:Palette%20Les%20Experts%7C"
1366 . "Nick%20Stokes%7CPersonnage%20de%20fiction%7CPersonnage%20fictif%7CPersonnage%20de%20fiction%7C"
1367 . "Personnages%20r%C3%A9currents%20dans%20Les%20Experts%7CRaymond%20Langston%7CRiley%20Adams%7C"
1368 . "Saison%201%20des%20Experts%7CSaison%2010%20des%20Experts%7CSaison%2011%20des%20Experts%7C"
1369 . "Saison%2012%20des%20Experts%7CSaison%202%20des%20Experts%7CSaison%203%20des%20Experts%7C"
1370 . "Saison%204%20des%20Experts%7CSaison%205%20des%20Experts%7CSaison%206%20des%20Experts%7C"
1371 . "Saison%207%20des%20Experts%7CSaison%208%20des%20Experts%7CSaison%209%20des%20Experts%7C"
1372 . "Sara%20Sidle%7CSofia%20Curtis%7CS%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e%7CWallace%20Langham%7C"
1373 . "Warrick%20Brown%7CWendy%20Simms%7C%C3%89tats-Unis"
1374 ),
1375 ],
1376 [
1377 rawurldecode(
1378 "Mod%C3%A8le%3AArrondissements%20homonymes%7CMod%C3%A8le%3ABandeau%20standard%20pour%20page%20d'homonymie%7C"
1379 . "Mod%C3%A8le%3ABatailles%20homonymes%7CMod%C3%A8le%3ACantons%20homonymes%7C"
1380 . "Mod%C3%A8le%3ACommunes%20fran%C3%A7aises%20homonymes%7CMod%C3%A8le%3AFilms%20homonymes%7C"
1381 . "Mod%C3%A8le%3AGouvernements%20homonymes%7CMod%C3%A8le%3AGuerres%20homonymes%7CMod%C3%A8le%3AHomonymie%7C"
1382 . "Mod%C3%A8le%3AHomonymie%20bateau%7CMod%C3%A8le%3AHomonymie%20d'%C3%A9tablissements%20scolaires%20ou"
1383 . "%20universitaires%7CMod%C3%A8le%3AHomonymie%20d'%C3%AEles%7CMod%C3%A8le%3AHomonymie%20de%20clubs%20sportifs%7C"
1384 . "Mod%C3%A8le%3AHomonymie%20de%20comt%C3%A9s%7CMod%C3%A8le%3AHomonymie%20de%20monument%7C"
1385 . "Mod%C3%A8le%3AHomonymie%20de%20nom%20romain%7CMod%C3%A8le%3AHomonymie%20de%20parti%20politique%7C"
1386 . "Mod%C3%A8le%3AHomonymie%20de%20route%7CMod%C3%A8le%3AHomonymie%20dynastique%7C"
1387 . "Mod%C3%A8le%3AHomonymie%20vid%C3%A9oludique%7CMod%C3%A8le%3AHomonymie%20%C3%A9difice%20religieux%7C"
1388 . "Mod%C3%A8le%3AInternationalisation%7CMod%C3%A8le%3AIsom%C3%A9rie%7CMod%C3%A8le%3AParonymie%7C"
1389 . "Mod%C3%A8le%3APatronyme%7CMod%C3%A8le%3APatronyme%20basque%7CMod%C3%A8le%3APatronyme%20italien%7C"
1390 . "Mod%C3%A8le%3APatronymie%7CMod%C3%A8le%3APersonnes%20homonymes%7CMod%C3%A8le%3ASaints%20homonymes%7C"
1391 . "Mod%C3%A8le%3ATitres%20homonymes%7CMod%C3%A8le%3AToponymie%7CMod%C3%A8le%3AUnit%C3%A9s%20homonymes%7C"
1392 . "Mod%C3%A8le%3AVilles%20homonymes%7CMod%C3%A8le%3A%C3%89difices%20religieux%20homonymes"
1393 )
1394 ]
1395 ];
1396 // phpcs:enable
1397 }
1398
1399 /**
1400 * @dataProvider provideRomanNumeralsData
1401 * @covers Language::romanNumeral
1402 */
1403 public function testRomanNumerals( $num, $numerals ) {
1404 $this->assertEquals(
1405 $numerals,
1406 Language::romanNumeral( $num ),
1407 "romanNumeral('$num')"
1408 );
1409 }
1410
1411 public static function provideRomanNumeralsData() {
1412 return [
1413 [ 1, 'I' ],
1414 [ 2, 'II' ],
1415 [ 3, 'III' ],
1416 [ 4, 'IV' ],
1417 [ 5, 'V' ],
1418 [ 6, 'VI' ],
1419 [ 7, 'VII' ],
1420 [ 8, 'VIII' ],
1421 [ 9, 'IX' ],
1422 [ 10, 'X' ],
1423 [ 20, 'XX' ],
1424 [ 30, 'XXX' ],
1425 [ 40, 'XL' ],
1426 [ 49, 'XLIX' ],
1427 [ 50, 'L' ],
1428 [ 60, 'LX' ],
1429 [ 70, 'LXX' ],
1430 [ 80, 'LXXX' ],
1431 [ 90, 'XC' ],
1432 [ 99, 'XCIX' ],
1433 [ 100, 'C' ],
1434 [ 200, 'CC' ],
1435 [ 300, 'CCC' ],
1436 [ 400, 'CD' ],
1437 [ 500, 'D' ],
1438 [ 600, 'DC' ],
1439 [ 700, 'DCC' ],
1440 [ 800, 'DCCC' ],
1441 [ 900, 'CM' ],
1442 [ 999, 'CMXCIX' ],
1443 [ 1000, 'M' ],
1444 [ 1989, 'MCMLXXXIX' ],
1445 [ 2000, 'MM' ],
1446 [ 3000, 'MMM' ],
1447 [ 4000, 'MMMM' ],
1448 [ 5000, 'MMMMM' ],
1449 [ 6000, 'MMMMMM' ],
1450 [ 7000, 'MMMMMMM' ],
1451 [ 8000, 'MMMMMMMM' ],
1452 [ 9000, 'MMMMMMMMM' ],
1453 [ 9999, 'MMMMMMMMMCMXCIX' ],
1454 [ 10000, 'MMMMMMMMMM' ],
1455 ];
1456 }
1457
1458 /**
1459 * @dataProvider provideHebrewNumeralsData
1460 * @covers Language::hebrewNumeral
1461 */
1462 public function testHebrewNumeral( $num, $numerals ) {
1463 $this->assertEquals(
1464 $numerals,
1465 Language::hebrewNumeral( $num ),
1466 "hebrewNumeral('$num')"
1467 );
1468 }
1469
1470 public static function provideHebrewNumeralsData() {
1471 return [
1472 [ -1, -1 ],
1473 [ 0, 0 ],
1474 [ 1, "א'" ],
1475 [ 2, "ב'" ],
1476 [ 3, "ג'" ],
1477 [ 4, "ד'" ],
1478 [ 5, "ה'" ],
1479 [ 6, "ו'" ],
1480 [ 7, "ז'" ],
1481 [ 8, "ח'" ],
1482 [ 9, "ט'" ],
1483 [ 10, "י'" ],
1484 [ 11, 'י"א' ],
1485 [ 14, 'י"ד' ],
1486 [ 15, 'ט"ו' ],
1487 [ 16, 'ט"ז' ],
1488 [ 17, 'י"ז' ],
1489 [ 20, "כ'" ],
1490 [ 21, 'כ"א' ],
1491 [ 30, "ל'" ],
1492 [ 40, "מ'" ],
1493 [ 50, "נ'" ],
1494 [ 60, "ס'" ],
1495 [ 70, "ע'" ],
1496 [ 80, "פ'" ],
1497 [ 90, "צ'" ],
1498 [ 99, 'צ"ט' ],
1499 [ 100, "ק'" ],
1500 [ 101, 'ק"א' ],
1501 [ 110, 'ק"י' ],
1502 [ 200, "ר'" ],
1503 [ 300, "ש'" ],
1504 [ 400, "ת'" ],
1505 [ 500, 'ת"ק' ],
1506 [ 800, 'ת"ת' ],
1507 [ 1000, "א' אלף" ],
1508 [ 1001, "א'א'" ],
1509 [ 1012, "א'י\"ב" ],
1510 [ 1020, "א'ך'" ],
1511 [ 1030, "א'ל'" ],
1512 [ 1081, "א'פ\"א" ],
1513 [ 2000, "ב' אלפים" ],
1514 [ 2016, "ב'ט\"ז" ],
1515 [ 3000, "ג' אלפים" ],
1516 [ 4000, "ד' אלפים" ],
1517 [ 4904, "ד'תתק\"ד" ],
1518 [ 5000, "ה' אלפים" ],
1519 [ 5680, "ה'תר\"ף" ],
1520 [ 5690, "ה'תר\"ץ" ],
1521 [ 5708, "ה'תש\"ח" ],
1522 [ 5720, "ה'תש\"ך" ],
1523 [ 5740, "ה'תש\"ם" ],
1524 [ 5750, "ה'תש\"ן" ],
1525 [ 5775, "ה'תשע\"ה" ],
1526 ];
1527 }
1528
1529 /**
1530 * @dataProvider providePluralData
1531 * @covers Language::convertPlural
1532 */
1533 public function testConvertPlural( $expected, $number, $forms ) {
1534 $chosen = $this->getLang()->convertPlural( $number, $forms );
1535 $this->assertEquals( $expected, $chosen );
1536 }
1537
1538 public static function providePluralData() {
1539 // Params are: [expected text, number given, [the plural forms]]
1540 return [
1541 [ 'plural', 0, [
1542 'singular', 'plural'
1543 ] ],
1544 [ 'explicit zero', 0, [
1545 '0=explicit zero', 'singular', 'plural'
1546 ] ],
1547 [ 'explicit one', 1, [
1548 'singular', 'plural', '1=explicit one',
1549 ] ],
1550 [ 'singular', 1, [
1551 'singular', 'plural', '0=explicit zero',
1552 ] ],
1553 [ 'plural', 3, [
1554 '0=explicit zero', '1=explicit one', 'singular', 'plural'
1555 ] ],
1556 [ 'explicit eleven', 11, [
1557 'singular', 'plural', '11=explicit eleven',
1558 ] ],
1559 [ 'plural', 12, [
1560 'singular', 'plural', '11=explicit twelve',
1561 ] ],
1562 [ 'plural', 12, [
1563 'singular', 'plural', '=explicit form',
1564 ] ],
1565 [ 'other', 2, [
1566 'kissa=kala', '1=2=3', 'other',
1567 ] ],
1568 [ '', 2, [
1569 '0=explicit zero', '1=explicit one',
1570 ] ],
1571 ];
1572 }
1573
1574 /**
1575 * @covers Language::embedBidi()
1576 */
1577 public function testEmbedBidi() {
1578 $lre = "\u{202A}"; // U+202A LEFT-TO-RIGHT EMBEDDING
1579 $rle = "\u{202B}"; // U+202B RIGHT-TO-LEFT EMBEDDING
1580 $pdf = "\u{202C}"; // U+202C POP DIRECTIONAL FORMATTING
1581 $lang = $this->getLang();
1582 $this->assertEquals(
1583 '123',
1584 $lang->embedBidi( '123' ),
1585 'embedBidi with neutral argument'
1586 );
1587 $this->assertEquals(
1588 $lre . 'Ben_(WMF)' . $pdf,
1589 $lang->embedBidi( 'Ben_(WMF)' ),
1590 'embedBidi with LTR argument'
1591 );
1592 $this->assertEquals(
1593 $rle . 'יהודי (מנוחין)' . $pdf,
1594 $lang->embedBidi( 'יהודי (מנוחין)' ),
1595 'embedBidi with RTL argument'
1596 );
1597 }
1598
1599 /**
1600 * @covers Language::translateBlockExpiry()
1601 * @dataProvider provideTranslateBlockExpiry
1602 */
1603 public function testTranslateBlockExpiry( $expectedData, $str, $now, $desc ) {
1604 $lang = $this->getLang();
1605 if ( is_array( $expectedData ) ) {
1606 list( $func, $arg ) = $expectedData;
1607 $expected = $lang->$func( $arg );
1608 } else {
1609 $expected = $expectedData;
1610 }
1611 $this->assertEquals( $expected, $lang->translateBlockExpiry( $str, null, $now ), $desc );
1612 }
1613
1614 public static function provideTranslateBlockExpiry() {
1615 return [
1616 [ '2 hours', '2 hours', 0, 'simple data from ipboptions' ],
1617 [ 'indefinite', 'infinite', 0, 'infinite from ipboptions' ],
1618 [ 'indefinite', 'infinity', 0, 'alternative infinite from ipboptions' ],
1619 [ 'indefinite', 'indefinite', 0, 'another alternative infinite from ipboptions' ],
1620 [ [ 'formatDuration', 1023 * 60 * 60 ], '1023 hours', 0, 'relative' ],
1621 [ [ 'formatDuration', -1023 ], '-1023 seconds', 0, 'negative relative' ],
1622 [
1623 [ 'formatDuration', 1023 * 60 * 60 ],
1624 '1023 hours',
1625 wfTimestamp( TS_UNIX, '19910203040506' ),
1626 'relative with initial timestamp'
1627 ],
1628 [ [ 'formatDuration', 0 ], 'now', 0, 'now' ],
1629 [
1630 [ 'timeanddate', '20120102070000' ],
1631 '2012-1-1 7:00 +1 day',
1632 0,
1633 'mixed, handled as absolute'
1634 ],
1635 [ [ 'timeanddate', '19910203040506' ], '1991-2-3 4:05:06', 0, 'absolute' ],
1636 [ [ 'timeanddate', '19700101000000' ], '1970-1-1 0:00:00', 0, 'absolute at epoch' ],
1637 [ [ 'timeanddate', '19691231235959' ], '1969-12-31 23:59:59', 0, 'time before epoch' ],
1638 [
1639 [ 'timeanddate', '19910910000000' ],
1640 '10 september',
1641 wfTimestamp( TS_UNIX, '19910203040506' ),
1642 'partial'
1643 ],
1644 [ 'dummy', 'dummy', 0, 'return garbage as is' ],
1645 ];
1646 }
1647
1648 /**
1649 * @dataProvider provideFormatNum
1650 * @covers Language::formatNum
1651 */
1652 public function testFormatNum(
1653 $translateNumerals, $langCode, $number, $nocommafy, $expected
1654 ) {
1655 $this->setMwGlobals( [ 'wgTranslateNumerals' => $translateNumerals ] );
1656 $lang = Language::factory( $langCode );
1657 $formattedNum = $lang->formatNum( $number, $nocommafy );
1658 $this->assertType( 'string', $formattedNum );
1659 $this->assertEquals( $expected, $formattedNum );
1660 }
1661
1662 public function provideFormatNum() {
1663 return [
1664 [ true, 'en', 100, false, '100' ],
1665 [ true, 'en', 101, true, '101' ],
1666 [ false, 'en', 103, false, '103' ],
1667 [ false, 'en', 104, true, '104' ],
1668 [ true, 'en', '105', false, '105' ],
1669 [ true, 'en', '106', true, '106' ],
1670 [ false, 'en', '107', false, '107' ],
1671 [ false, 'en', '108', true, '108' ],
1672 ];
1673 }
1674
1675 /**
1676 * @covers Language::parseFormattedNumber
1677 * @dataProvider parseFormattedNumberProvider
1678 */
1679 public function testParseFormattedNumber( $langCode, $number ) {
1680 $lang = Language::factory( $langCode );
1681
1682 $localisedNum = $lang->formatNum( $number );
1683 $normalisedNum = $lang->parseFormattedNumber( $localisedNum );
1684
1685 $this->assertEquals( $number, $normalisedNum );
1686 }
1687
1688 public function parseFormattedNumberProvider() {
1689 return [
1690 [ 'de', 377.01 ],
1691 [ 'fa', 334 ],
1692 [ 'fa', 382.772 ],
1693 [ 'ar', 1844 ],
1694 [ 'lzh', 3731 ],
1695 [ 'zh-classical', 7432 ]
1696 ];
1697 }
1698
1699 /**
1700 * @covers Language::commafy()
1701 * @dataProvider provideCommafyData
1702 */
1703 public function testCommafy( $number, $numbersWithCommas ) {
1704 $this->assertEquals(
1705 $numbersWithCommas,
1706 $this->getLang()->commafy( $number ),
1707 "commafy('$number')"
1708 );
1709 }
1710
1711 public static function provideCommafyData() {
1712 return [
1713 [ -1, '-1' ],
1714 [ 10, '10' ],
1715 [ 100, '100' ],
1716 [ 1000, '1,000' ],
1717 [ 10000, '10,000' ],
1718 [ 100000, '100,000' ],
1719 [ 1000000, '1,000,000' ],
1720 [ -1.0001, '-1.0001' ],
1721 [ 1.0001, '1.0001' ],
1722 [ 10.0001, '10.0001' ],
1723 [ 100.0001, '100.0001' ],
1724 [ 1000.0001, '1,000.0001' ],
1725 [ 10000.0001, '10,000.0001' ],
1726 [ 100000.0001, '100,000.0001' ],
1727 [ 1000000.0001, '1,000,000.0001' ],
1728 [ '200000000000000000000', '200,000,000,000,000,000,000' ],
1729 [ '-200000000000000000000', '-200,000,000,000,000,000,000' ],
1730 ];
1731 }
1732
1733 /**
1734 * @covers Language::listToText
1735 */
1736 public function testListToText() {
1737 $lang = $this->getLang();
1738 $and = $lang->getMessageFromDB( 'and' );
1739 $s = $lang->getMessageFromDB( 'word-separator' );
1740 $c = $lang->getMessageFromDB( 'comma-separator' );
1741
1742 $this->assertSame( '', $lang->listToText( [] ) );
1743 $this->assertEquals( 'a', $lang->listToText( [ 'a' ] ) );
1744 $this->assertEquals( "a{$and}{$s}b", $lang->listToText( [ 'a', 'b' ] ) );
1745 $this->assertEquals( "a{$c}b{$and}{$s}c", $lang->listToText( [ 'a', 'b', 'c' ] ) );
1746 $this->assertEquals( "a{$c}b{$c}c{$and}{$s}d", $lang->listToText( [ 'a', 'b', 'c', 'd' ] ) );
1747 }
1748
1749 /**
1750 * @covers Language::clearCaches
1751 */
1752 public function testClearCaches() {
1753 $languageClass = TestingAccessWrapper::newFromClass( Language::class );
1754
1755 // Populate $mLangObjCache
1756 $lang = Language::factory( 'en' );
1757 $this->assertNotCount( 0, Language::$mLangObjCache );
1758
1759 // Populate $fallbackLanguageCache
1760 Language::getFallbacksIncludingSiteLanguage( 'en' );
1761 $this->assertNotCount( 0, $languageClass->fallbackLanguageCache );
1762
1763 // Populate $grammarTransformations
1764 $lang->getGrammarTransformations();
1765 $this->assertNotNull( $languageClass->grammarTransformations );
1766
1767 Language::clearCaches();
1768
1769 $this->assertCount( 0, Language::$mLangObjCache );
1770 $this->assertCount( 0, $languageClass->fallbackLanguageCache );
1771 $this->assertNull( $languageClass->grammarTransformations );
1772 }
1773
1774 /**
1775 * @dataProvider provideGetParentLanguage
1776 * @covers Language::getParentLanguage
1777 */
1778 public function testGetParentLanguage( $code, $expected, $comment ) {
1779 $lang = Language::factory( $code );
1780 if ( is_null( $expected ) ) {
1781 $this->assertNull( $lang->getParentLanguage(), $comment );
1782 } else {
1783 $this->assertEquals( $expected, $lang->getParentLanguage()->getCode(), $comment );
1784 }
1785 }
1786
1787 public static function provideGetParentLanguage() {
1788 return [
1789 [ 'zh-cn', 'zh', 'zh is the parent language of zh-cn' ],
1790 [ 'zh', 'zh', 'zh is defined as the parent language of zh, '
1791 . 'because zh converter can convert zh-cn to zh' ],
1792 [ 'zh-invalid', null, 'do not be fooled by arbitrarily composed language codes' ],
1793 [ 'de-formal', null, 'de does not have converter' ],
1794 [ 'de', null, 'de does not have converter' ],
1795 ];
1796 }
1797
1798 /**
1799 * @dataProvider provideGetNamespaceAliases
1800 * @covers Language::getNamespaceAliases
1801 */
1802 public function testGetNamespaceAliases( $languageCode, $subset ) {
1803 $language = Language::factory( $languageCode );
1804 $aliases = $language->getNamespaceAliases();
1805 foreach ( $subset as $alias => $nsId ) {
1806 $this->assertEquals( $nsId, $aliases[$alias] );
1807 }
1808 }
1809
1810 public static function provideGetNamespaceAliases() {
1811 // TODO: Add tests for NS_PROJECT_TALK and GenderNamespaces
1812 return [
1813 [
1814 'zh',
1815 [
1816 '文件' => NS_FILE,
1817 '檔案' => NS_FILE,
1818 ],
1819 ],
1820 ];
1821 }
1822
1823 /**
1824 * @covers Language::hasVariant
1825 */
1826 public function testHasVariant() {
1827 // See LanguageSrTest::testHasVariant() for additional tests
1828 $en = Language::factory( 'en' );
1829 $this->assertTrue( $en->hasVariant( 'en' ), 'base is always a variant' );
1830 $this->assertFalse( $en->hasVariant( 'en-bogus' ), 'bogus en variant' );
1831
1832 $bogus = Language::factory( 'bogus' );
1833 $this->assertTrue( $bogus->hasVariant( 'bogus' ), 'base is always a variant' );
1834 }
1835
1836 /**
1837 * @covers Language::equals
1838 */
1839 public function testEquals() {
1840 $en1 = Language::factory( 'en' );
1841 $en2 = Language::factory( 'en' );
1842 $en3 = new Language();
1843 $this->assertTrue( $en1->equals( $en2 ), 'en1 equals en2' );
1844 $this->assertTrue( $en2->equals( $en3 ), 'en2 equals en3' );
1845 $this->assertTrue( $en3->equals( $en1 ), 'en3 equals en1' );
1846
1847 $fr = Language::factory( 'fr' );
1848 $this->assertFalse( $en1->equals( $fr ), 'en not equals fr' );
1849
1850 $ar1 = Language::factory( 'ar' );
1851 $ar2 = new LanguageAr();
1852 $this->assertTrue( $ar1->equals( $ar2 ), 'ar equals ar' );
1853 }
1854
1855 /**
1856 * @dataProvider provideUcfirst
1857 * @covers Language::ucfirst
1858 */
1859 public function testUcfirst( $orig, $expected, $desc, $overrides = false ) {
1860 $lang = new Language();
1861 if ( is_array( $overrides ) ) {
1862 $this->setMwGlobals( [ 'wgOverrideUcfirstCharacters' => $overrides ] );
1863 }
1864 $this->assertSame( $lang->ucfirst( $orig ), $expected, $desc );
1865 }
1866
1867 public static function provideUcfirst() {
1868 return [
1869 [ 'alice', 'Alice', 'simple ASCII string', false ],
1870 [ 'århus', 'Århus', 'unicode string', false ],
1871 //overrides do not affect ASCII characters
1872 [ 'foo', 'Foo', 'ASCII is not overriden', [ 'f' => 'b' ] ],
1873 // but they do affect non-ascii ones
1874 [ 'èl', 'Ll' , 'Non-ASCII is overridden', [ 'è' => 'L' ] ],
1875 ];
1876 }
1877
1878 // The following methods are for LanguageNameUtilsTestTrait
1879
1880 private function isSupportedLanguage( $code ) {
1881 return Language::isSupportedLanguage( $code );
1882 }
1883
1884 private function isValidCode( $code ) {
1885 return Language::isValidCode( $code );
1886 }
1887
1888 private function isValidBuiltInCode( $code ) {
1889 return Language::isValidBuiltInCode( $code );
1890 }
1891
1892 private function isKnownLanguageTag( $code ) {
1893 return Language::isKnownLanguageTag( $code );
1894 }
1895
1896 /**
1897 * Call getLanguageName() and getLanguageNames() using the Language static methods.
1898 *
1899 * @param array $options To set globals for testing Language
1900 * @param string $expected
1901 * @param string $code
1902 * @param mixed ...$otherArgs Optionally, pass $inLanguage and/or $include.
1903 */
1904 private function assertGetLanguageNames( array $options, $expected, $code, ...$otherArgs ) {
1905 if ( $options ) {
1906 foreach ( $options as $key => $val ) {
1907 $this->setMwGlobals( "wg$key", $val );
1908 }
1909 $this->resetServices();
1910 }
1911 $this->assertSame( $expected,
1912 Language::fetchLanguageNames( ...$otherArgs )[strtolower( $code )] ?? '' );
1913 $this->assertSame( $expected, Language::fetchLanguageName( $code, ...$otherArgs ) );
1914 }
1915
1916 private function getLanguageNames( ...$args ) {
1917 return Language::fetchLanguageNames( ...$args );
1918 }
1919
1920 private function getLanguageName( ...$args ) {
1921 return Language::fetchLanguageName( ...$args );
1922 }
1923
1924 private static function getFileName( ...$args ) {
1925 return Language::getFileName( ...$args );
1926 }
1927
1928 private static function getMessagesFileName( $code ) {
1929 return Language::getMessagesFileName( $code );
1930 }
1931
1932 private static function getJsonMessagesFileName( $code ) {
1933 return Language::getJsonMessagesFileName( $code );
1934 }
1935
1936 /**
1937 * @todo This really belongs in the cldr extension's tests.
1938 *
1939 * @covers MediaWiki\Languages\LanguageNameUtils::isKnownLanguageTag
1940 * @covers Language::isKnownLanguageTag
1941 */
1942 public function testIsKnownLanguageTag_cldr() {
1943 if ( !class_exists( 'LanguageNames' ) ) {
1944 $this->markTestSkipped( 'The LanguageNames class is not available. '
1945 . 'The CLDR extension is probably not installed.' );
1946 }
1947
1948 // We need to restore the extension's hook that we removed.
1949 $this->setMwGlobals( 'wgHooks', $this->origHooks );
1950
1951 // "pal" is an ancient language, which probably will not appear in Names.php, but appears in
1952 // CLDR in English
1953 $this->assertTrue( Language::isKnownLanguageTag( 'pal' ) );
1954 }
1955 }