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