wiki.techinc.nl/tests/phpunit/includes/language/LanguageIntegrationTest.php

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

2527 lines
62 KiB
PHP
Raw Normal View History

<?php
use MediaWiki\Config\HashConfig;
use MediaWiki\Config\MultiConfig;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\Languages\LanguageConverterFactory;
use MediaWiki\Languages\LanguageFallback;
use MediaWiki\Languages\LanguageNameUtils;
use MediaWiki\MainConfigNames;
use MediaWiki\MediaWikiServices;
use MediaWiki\Tests\Unit\DummyServicesTrait;
use MediaWiki\Title\NamespaceInfo;
use MediaWiki\User\UserIdentityValue;
use Wikimedia\TestingAccessWrapper;
/**
* @group Language
*/
class LanguageIntegrationTest extends LanguageClassesTestCase {
use DummyServicesTrait;
use LanguageNameUtilsTestTrait;
private function newLanguage( $class = Language::class, $code = 'en' ) {
// Needed to support the setMwGlobals calls for the various tests, but this should
// probably be changed to have the configuration injected into this method instead
// at some point
$config = $this->getServiceContainer()->getMainConfig();
return new $class(
$code,
$this->createNoOpMock( NamespaceInfo::class ),
$this->createNoOpMock( LocalisationCache::class ),
$this->createNoOpMock( LanguageNameUtils::class ),
$this->createNoOpMock( LanguageFallback::class ),
Hooks::run() call site migration Migrate all callers of Hooks::run() to use the new HookContainer/HookRunner system. General principles: * Use DI if it is already used. We're not changing the way state is managed in this patch. * HookContainer is always injected, not HookRunner. HookContainer is a service, it's a more generic interface, it is the only thing that provides isRegistered() which is needed in some cases, and a HookRunner can be efficiently constructed from it (confirmed by benchmark). Because HookContainer is needed for object construction, it is also needed by all factories. * "Ask your friendly local base class". Big hierarchies like SpecialPage and ApiBase have getHookContainer() and getHookRunner() methods in the base class, and classes that extend that base class are not expected to know or care where the base class gets its HookContainer from. * ProtectedHookAccessorTrait provides protected getHookContainer() and getHookRunner() methods, getting them from the global service container. The point of this is to ease migration to DI by ensuring that call sites ask their local friendly base class rather than getting a HookRunner from the service container directly. * Private $this->hookRunner. In some smaller classes where accessor methods did not seem warranted, there is a private HookRunner property which is accessed directly. Very rarely (two cases), there is a protected property, for consistency with code that conventionally assumes protected=private, but in cases where the class might actually be overridden, a protected accessor is preferred over a protected property. * The last resort: Hooks::runner(). Mostly for static, file-scope and global code. In a few cases it was used for objects with broken construction schemes, out of horror or laziness. Constructors with new required arguments: * AuthManager * BadFileLookup * BlockManager * ClassicInterwikiLookup * ContentHandlerFactory * ContentSecurityPolicy * DefaultOptionsManager * DerivedPageDataUpdater * FullSearchResultWidget * HtmlCacheUpdater * LanguageFactory * LanguageNameUtils * LinkRenderer * LinkRendererFactory * LocalisationCache * MagicWordFactory * MessageCache * NamespaceInfo * PageEditStash * PageHandlerFactory * PageUpdater * ParserFactory * PermissionManager * RevisionStore * RevisionStoreFactory * SearchEngineConfig * SearchEngineFactory * SearchFormWidget * SearchNearMatcher * SessionBackend * SpecialPageFactory * UserNameUtils * UserOptionsManager * WatchedItemQueryService * WatchedItemStore Constructors with new optional arguments: * DefaultPreferencesFactory * Language * LinkHolderArray * MovePage * Parser * ParserCache * PasswordReset * Router setHookContainer() now required after construction: * AuthenticationProvider * ResourceLoaderModule * SearchEngine Change-Id: Id442b0dbe43aba84bd5cf801d86dedc768b082c7
2020-03-19 02:42:09 +00:00
$this->createNoOpMock( LanguageConverterFactory::class ),
$this->createHookContainer(),
$config
);
}
protected function setUp(): void {
parent::setUp();
$this->overrideConfigValue( MainConfigNames::UsePigLatinVariant, true );
}
/**
* @covers \Language::convertDoubleWidth
* @covers \Language::normalizeForSearch
*/
public function testLanguageConvertDoubleWidthToSingleWidth() {
$this->assertSame(
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
$this->getLang()->normalizeForSearch(
""
),
'convertDoubleWidth() with the full alphabet and digits'
);
}
/**
* @dataProvider provideFormattableTimes
* @covers \Language::formatTimePeriod
*/
public function testFormatTimePeriod( $seconds, $format, $expected, $desc ) {
$this->assertEquals( $expected, $this->getLang()->formatTimePeriod( $seconds, $format ), $desc );
}
public static function provideFormattableTimes() {
return [
[
9.45,
[],
'9.5 s',
'formatTimePeriod() rounding (<10s)'
],
[
9.45,
[ 'noabbrevs' => true ],
'9.5 seconds',
'formatTimePeriod() rounding (<10s)'
],
[
9.95,
[],
'10 s',
'formatTimePeriod() rounding (<10s)'
],
[
9.95,
[ 'noabbrevs' => true ],
'10 seconds',
'formatTimePeriod() rounding (<10s)'
],
[
59.55,
[],
'1 min 0 s',
'formatTimePeriod() rounding (<60s)'
],
[
59.55,
[ 'noabbrevs' => true ],
'1 minute 0 seconds',
'formatTimePeriod() rounding (<60s)'
],
[
119.55,
[],
'2 min 0 s',
'formatTimePeriod() rounding (<1h)'
],
[
119.55,
[ 'noabbrevs' => true ],
'2 minutes 0 seconds',
'formatTimePeriod() rounding (<1h)'
],
[
3599.55,
[],
'1 h 0 min 0 s',
'formatTimePeriod() rounding (<1h)'
],
[
3599.55,
[ 'noabbrevs' => true ],
'1 hour 0 minutes 0 seconds',
'formatTimePeriod() rounding (<1h)'
],
[
7199.55,
[],
'2 h 0 min 0 s',
'formatTimePeriod() rounding (>=1h)'
],
[
7199.55,
[ 'noabbrevs' => true ],
'2 hours 0 minutes 0 seconds',
'formatTimePeriod() rounding (>=1h)'
],
[
7199.55,
'avoidseconds',
'2 h 0 min',
'formatTimePeriod() rounding (>=1h), avoidseconds'
],
[
7199.55,
[ 'avoid' => 'avoidseconds', 'noabbrevs' => true ],
'2 hours 0 minutes',
'formatTimePeriod() rounding (>=1h), avoidseconds'
],
[
7199.55,
'avoidminutes',
'2 h 0 min',
'formatTimePeriod() rounding (>=1h), avoidminutes'
],
[
7199.55,
[ 'avoid' => 'avoidminutes', 'noabbrevs' => true ],
'2 hours 0 minutes',
'formatTimePeriod() rounding (>=1h), avoidminutes'
],
[
172799.55,
'avoidseconds',
'48 h 0 min',
'formatTimePeriod() rounding (=48h), avoidseconds'
],
[
172799.55,
[ 'avoid' => 'avoidseconds', 'noabbrevs' => true ],
'48 hours 0 minutes',
'formatTimePeriod() rounding (=48h), avoidseconds'
],
[
259199.55,
'avoidhours',
'3 d',
'formatTimePeriod() rounding (>48h), avoidhours'
],
[
259199.55,
[ 'avoid' => 'avoidhours', 'noabbrevs' => true ],
'3 days',
'formatTimePeriod() rounding (>48h), avoidhours'
],
[
259199.55,
'avoidminutes',
'3 d 0 h',
'formatTimePeriod() rounding (>48h), avoidminutes'
],
[
259199.55,
[ 'avoid' => 'avoidminutes', 'noabbrevs' => true ],
'3 days 0 hours',
'formatTimePeriod() rounding (>48h), avoidminutes'
],
[
176399.55,
'avoidseconds',
'2 d 1 h 0 min',
'formatTimePeriod() rounding (>48h), avoidseconds'
],
[
176399.55,
[ 'avoid' => 'avoidseconds', 'noabbrevs' => true ],
'2 days 1 hour 0 minutes',
'formatTimePeriod() rounding (>48h), avoidseconds'
],
[
176399.55,
'avoidminutes',
'2 d 1 h',
'formatTimePeriod() rounding (>48h), avoidminutes'
],
[
176399.55,
[ 'avoid' => 'avoidminutes', 'noabbrevs' => true ],
'2 days 1 hour',
'formatTimePeriod() rounding (>48h), avoidminutes'
],
[
259199.55,
'avoidseconds',
'3 d 0 h 0 min',
'formatTimePeriod() rounding (>48h), avoidseconds'
],
[
259199.55,
[ 'avoid' => 'avoidseconds', 'noabbrevs' => true ],
'3 days 0 hours 0 minutes',
'formatTimePeriod() rounding (>48h), avoidseconds'
],
[
172801.55,
'avoidseconds',
'2 d 0 h 0 min',
'formatTimePeriod() rounding, (>48h), avoidseconds'
],
[
172801.55,
[ 'avoid' => 'avoidseconds', 'noabbrevs' => true ],
'2 days 0 hours 0 minutes',
'formatTimePeriod() rounding, (>48h), avoidseconds'
],
[
176460.55,
[],
'2 d 1 h 1 min 1 s',
'formatTimePeriod() rounding, recursion, (>48h)'
],
[
176460.55,
[ 'noabbrevs' => true ],
'2 days 1 hour 1 minute 1 second',
'formatTimePeriod() rounding, recursion, (>48h)'
],
];
}
/**
* @covers \Language::truncateForDatabase
* @covers \Language::truncateInternal
*/
public function testTruncateForDatabase() {
$this->assertEquals(
"XXX",
$this->getLang()->truncateForDatabase( "1234567890", 0, 'XXX' ),
'truncate prefix, len 0, small ellipsis'
);
$this->assertEquals(
"12345XXX",
$this->getLang()->truncateForDatabase( "1234567890", 8, 'XXX' ),
'truncate prefix, small ellipsis'
);
$this->assertSame(
"123456789",
$this->getLang()->truncateForDatabase( "123456789", 5, 'XXXXXXXXXXXXXXX' ),
'truncate prefix, large ellipsis'
);
$this->assertEquals(
"XXX67890",
$this->getLang()->truncateForDatabase( "1234567890", -8, 'XXX' ),
'truncate suffix, small ellipsis'
);
$this->assertSame(
"123456789",
$this->getLang()->truncateForDatabase( "123456789", -5, 'XXXXXXXXXXXXXXX' ),
'truncate suffix, large ellipsis'
);
$this->assertEquals(
"123XXX",
$this->getLang()->truncateForDatabase( "123 ", 9, 'XXX' ),
'truncate prefix, with spaces'
);
$this->assertEquals(
"12345XXX",
$this->getLang()->truncateForDatabase( "12345 8", 11, 'XXX' ),
'truncate prefix, with spaces and non-space ending'
);
$this->assertEquals(
"XXX234",
$this->getLang()->truncateForDatabase( "1 234", -8, 'XXX' ),
'truncate suffix, with spaces'
);
$this->assertEquals(
"12345XXX",
$this->getLang()->truncateForDatabase( "1234567890", 5, 'XXX', false ),
'truncate without adjustment'
);
$this->assertEquals(
"泰乐菌...",
$this->getLang()->truncateForDatabase( "泰乐菌素123456789", 11, '...', false ),
'truncate does not chop Unicode characters in half'
);
$this->assertEquals(
"\n泰乐菌...",
$this->getLang()->truncateForDatabase( "\n泰乐菌素123456789", 12, '...', false ),
'truncate does not chop Unicode characters in half if there is a preceding newline'
);
}
/**
* @dataProvider provideTruncateData
* @covers \Language::truncateForVisual
* @covers \Language::truncateInternal
*/
public function testTruncateForVisual(
$expected, $string, $length, $ellipsis = '...', $adjustLength = true
) {
$this->assertEquals(
$expected,
$this->getLang()->truncateForVisual( $string, $length, $ellipsis, $adjustLength )
);
}
/**
* @return array Format is ($expected, $string, $length, $ellipsis, $adjustLength)
*/
public static function provideTruncateData() {
return [
[ "XXX", "тестирам да ли ради", 0, "XXX" ],
[ "testnXXX", "testni scenarij", 8, "XXX" ],
[ "حالة اختبار", "حالة اختبار", 5, "XXXXXXXXXXXXXXX" ],
[ "XXXедент", "прецедент", -8, "XXX" ],
[ "XXപിൾ", "ആപ്പിൾ", -5, "XX" ],
[ "神秘XXX", "神秘 ", 9, "XXX" ],
[ "ΔημιουργXXX", "Δημιουργία Σύμπαντος", 11, "XXX" ],
[ "XXXの家です", "地球は私たちの唯 の家です", -8, "XXX" ],
[ "زندگیXXX", "زندگی زیباست", 6, "XXX", false ],
[ "ცხოვრება...", "ცხოვრება არის საოცარი", 8, "...", false ],
[ "\nທ່ານ...", "\nທ່ານບໍ່ຮູ້ຫນັງສື", 5, "...", false ],
];
}
/**
* @dataProvider provideHTMLTruncateData
* @covers \Language::truncateHTML
*/
public function testTruncateHtml( $len, $ellipsis, $input, $expected ) {
// Actual HTML...
$this->assertEquals(
$expected,
$this->getLang()->truncateHtml( $input, $len, $ellipsis )
);
}
/**
* @return array Format is ($len, $ellipsis, $input, $expected)
*/
public static function provideHTMLTruncateData() {
return [
[ 0, 'XXX', "1234567890", "XXX" ],
[ 8, 'XXX', "1234567890", "12345XXX" ],
[ 5, 'XXXXXXXXXXXXXXX', '1234567890', "1234567890" ],
[ 2, '***',
'<p><span style="font-weight:bold;"></span></p>',
'<p><span style="font-weight:bold;"></span></p>',
],
[ 2, '***',
'<p><span style="font-weight:bold;">123456789</span></p>',
'<p><span style="font-weight:bold;">***</span></p>',
],
[ 2, '***',
'<p><span style="font-weight:bold;">&nbsp;23456789</span></p>',
'<p><span style="font-weight:bold;">***</span></p>',
],
[ 3, '***',
'<p><span style="font-weight:bold;">123456789</span></p>',
'<p><span style="font-weight:bold;">***</span></p>',
],
[ 4, '***',
'<p><span style="font-weight:bold;">123456789</span></p>',
'<p><span style="font-weight:bold;">1***</span></p>',
],
[ 5, '***',
'<tt><span style="font-weight:bold;">123456789</span></tt>',
'<tt><span style="font-weight:bold;">12***</span></tt>',
],
[ 6, '***',
'<p><a href="www.mediawiki.org">123456789</a></p>',
'<p><a href="www.mediawiki.org">123***</a></p>',
],
[ 6, '***',
'<p><a href="www.mediawiki.org">12&nbsp;456789</a></p>',
'<p><a href="www.mediawiki.org">12&nbsp;***</a></p>',
],
[ 7, '***',
'<small><span style="font-weight:bold;">123<p id="#moo">456</p>789</span></small>',
'<small><span style="font-weight:bold;">123<p id="#moo">4***</p></span></small>',
],
[ 8, '***',
'<div><span style="font-weight:bold;">123<span>4</span>56789</span></div>',
'<div><span style="font-weight:bold;">123<span>4</span>5***</span></div>',
],
[ 9, '***',
'<p><table style="font-weight:bold;"><tr><td>123456789</td></tr></table></p>',
'<p><table style="font-weight:bold;"><tr><td>123456789</td></tr></table></p>',
],
[ 10, '***',
'<p><font style="font-weight:bold;">123456789</font></p>',
'<p><font style="font-weight:bold;">123456789</font></p>',
],
[ 10, '***',
'<p><font style="font-weight:bold;">123456789</font',
'<p><font style="font-weight:bold;">123456789</font</p>',
],
];
}
/**
* Test too short timestamp
* @covers \Language::sprintfDate
*/
public function testSprintfDateTooShortTimestamp() {
$this->expectException( InvalidArgumentException::class );
$this->getLang()->sprintfDate( 'xiY', '1234567890123' );
}
/**
* Test too long timestamp
* @covers \Language::sprintfDate
*/
public function testSprintfDateTooLongTimestamp() {
$this->expectException( InvalidArgumentException::class );
$this->getLang()->sprintfDate( 'xiY', '123456789012345' );
}
/**
* Test too short timestamp
* @covers \Language::sprintfDate
*/
public function testSprintfDateNotAllDigitTimestamp() {
$this->expectException( InvalidArgumentException::class );
$this->getLang()->sprintfDate( 'xiY', '-1234567890123' );
}
/**
* @dataProvider provideSprintfDateSamples
* @covers \Language::sprintfDate
*/
public function testSprintfDate( $format, $ts, $expected, $msg ) {
$ttl = null;
$this->assertSame(
$expected,
$this->getLang()->sprintfDate( $format, $ts, null, $ttl ),
"sprintfDate('$format', '$ts'): $msg"
);
if ( $ttl ) {
$dt = new DateTime( $ts );
$lastValidTS = $dt->add( new DateInterval( 'PT' . ( $ttl - 1 ) . 'S' ) )->format( 'YmdHis' );
$this->assertSame(
$expected,
$this->getLang()->sprintfDate( $format, $lastValidTS, null ),
"sprintfDate('$format', '$ts'): TTL $ttl too high (output was different at $lastValidTS)"
);
} else {
// advance the time enough to make all of the possible outputs different (except possibly L)
$dt = new DateTime( $ts );
$newTS = $dt->add( new DateInterval( 'P1Y1M8DT13H1M1S' ) )->format( 'YmdHis' );
$this->assertSame(
$expected,
$this->getLang()->sprintfDate( $format, $newTS, null ),
"sprintfDate('$format', '$ts'): Missing TTL (output was different at $newTS)"
);
}
}
/**
* sprintfDate should always use UTC when no zone is given.
* @dataProvider provideSprintfDateSamples
* @covers \Language::sprintfDate
*/
public function testSprintfDateNoZone( $format, $ts, $expected, $ignore, $msg ) {
$oldTZ = date_default_timezone_get();
$res = date_default_timezone_set( 'Asia/Seoul' );
if ( !$res ) {
$this->markTestSkipped( "Error setting Timezone" );
}
$this->assertEquals(
$expected,
$this->getLang()->sprintfDate( $format, $ts ),
"sprintfDate('$format', '$ts'): $msg"
);
date_default_timezone_set( $oldTZ );
}
/**
* sprintfDate should use passed timezone
* @dataProvider provideSprintfDateSamples
* @covers \Language::sprintfDate
*/
public function testSprintfDateTZ( $format, $ts, $ignore, $expected, $msg ) {
$tz = new DateTimeZone( 'Asia/Seoul' );
if ( !$tz ) {
$this->markTestSkipped( "Error getting Timezone" );
}
$this->assertEquals(
$expected,
$this->getLang()->sprintfDate( $format, $ts, $tz ),
"sprintfDate('$format', '$ts', 'Asia/Seoul'): $msg"
);
}
/**
* sprintfDate should only calculate a TTL if the caller is going to use it.
* @covers \Language::sprintfDate
*/
public function testSprintfDateNoTtlIfNotNeeded() {
$noTtl = 'unused'; // Value used to represent that the caller didn't pass a variable in.
$ttl = null;
$this->getLang()->sprintfDate( 'YmdHis', wfTimestampNow(), null, $noTtl );
$this->getLang()->sprintfDate( 'YmdHis', wfTimestampNow(), null, $ttl );
$this->assertSame(
'unused',
$noTtl,
'If the caller does not set the $ttl variable, do not compute it.'
);
$this->assertIsInt( $ttl, 'TTL should have been computed.' );
}
public static function provideSprintfDateSamples() {
return [
[
'xiY',
'20111212000000',
'1390', // note because we're testing English locale we get Latin-standard digits
'1390',
'Iranian calendar full year'
],
[
'xiy',
'20111212000000',
'90',
'90',
'Iranian calendar short year'
],
[
'o',
'20120101235000',
'2011',
'2011',
'ISO 8601 (week) year'
],
[
'W',
'20120101235000',
'52',
'52',
'Week number'
],
[
'W',
'20120102235000',
'01',
'01',
'Week number'
],
[
'o-\\WW-N',
'20091231235000',
'2009-W53-4',
'2009-W53-4',
'leap week'
],
// What follows is mostly copied from
// https://www.mediawiki.org/wiki/Help:Extension:ParserFunctions#.23time
[
'Y',
'20120102090705',
'2012',
'2012',
'Full year'
],
[
'y',
'20120102090705',
'12',
'12',
'2 digit year'
],
[
'L',
'20120102090705',
'1',
'1',
'Leap year'
],
[
'n',
'20120102090705',
'1',
'1',
'Month index, not zero pad'
],
[
'N',
'20120102090705',
'1',
'1',
'Day of the week'
],
[
'M',
'20120102090705',
'Jan',
'Jan',
'Month abbrev'
],
[
'F',
'20120102090705',
'January',
'January',
'Full month'
],
[
'xg',
'20120102090705',
'January',
'January',
'Genitive month name (same in EN)'
],
[
'j',
'20120102090705',
'2',
'2',
'Day of month (not zero pad)'
],
[
'd',
'20120102090705',
'02',
'02',
'Day of month (zero-pad)'
],
[
'z',
'20120102090705',
'1',
'1',
'Day of year (zero-indexed)'
],
[
'D',
'20120102090705',
'Mon',
'Mon',
'Day of week (abbrev)'
],
[
'l',
'20120102090705',
'Monday',
'Monday',
'Full day of week'
],
[
'N',
'20120101090705',
'7',
'7',
'Day of week (Mon=1, Sun=7)'
],
[
'w',
'20120101090705',
'0',
'0',
'Day of week (Sun=0, Sat=6)'
],
[
'N',
'20120102090705',
'1',
'1',
'Day of week'
],
[
'a',
'20120102090705',
'am',
'am',
'am vs pm'
],
[
'A',
'20120102120000',
'PM',
'PM',
'AM vs PM'
],
[
'a',
'20120102000000',
'am',
'am',
'AM vs PM'
],
[
'g',
'20120102090705',
'9',
'9',
'12 hour, not Zero'
],
[
'h',
'20120102090705',
'09',
'09',
'12 hour, zero padded'
],
[
'G',
'20120102090705',
'9',
'9',
'24 hour, not zero'
],
[
'H',
'20120102090705',
'09',
'09',
'24 hour, zero'
],
[
'H',
'20120102110705',
'11',
'11',
'24 hour, zero'
],
[
'i',
'20120102090705',
'07',
'07',
'Minutes'
],
[
's',
'20120102090705',
'05',
'05',
'seconds'
],
[
'U',
'20120102090705',
'1325495225',
'1325462825',
'unix time'
],
[
't',
'20120102090705',
'31',
'31',
'Days in current month'
],
[
'c',
'20120102090705',
'2012-01-02T09:07:05+00:00',
'2012-01-02T09:07:05+09:00',
'ISO 8601 timestamp'
],
[
'r',
'20120102090705',
'Mon, 02 Jan 2012 09:07:05 +0000',
'Mon, 02 Jan 2012 09:07:05 +0900',
'RFC 5322'
],
[
'e',
'20120102090705',
'UTC',
'Asia/Seoul',
'Timezone identifier'
],
[
'I',
'19880602090705',
'0',
'1',
'DST indicator'
],
[
'O',
'20120102090705',
'+0000',
'+0900',
'Timezone offset'
],
[
'P',
'20120102090705',
'+00:00',
'+09:00',
'Timezone offset with colon'
],
[
'T',
'20120102090705',
'UTC',
'KST',
'Timezone abbreviation'
],
[
'Z',
'20120102090705',
'0',
'32400',
'Timezone offset in seconds'
],
[
'xmj xmF xmn xmY',
'20120102090705',
'7 Safar 2 1433',
'7 Safar 2 1433',
'Islamic'
],
[
'xij xiF xin xiY',
'20120102090705',
'12 Dey 10 1390',
'12 Dey 10 1390',
'Iranian'
],
[
'xjj xjF xjn xjY',
'20120102090705',
'7 Tevet 4 5772',
'7 Tevet 4 5772',
'Hebrew'
],
[
'xjt',
'20120102090705',
'29',
'29',
'Hebrew number of days in month'
],
[
'xjx',
'20120102090705',
'Tevet',
'Tevet',
'Hebrew genitive month name (No difference in EN)'
],
[
'xkY',
'20120102090705',
'2555',
'2555',
'Thai year'
],
[
'xkY',
'19410101090705',
'2484',
'2484',
'Thai year'
],
[
'xoY',
'20120102090705',
'101',
'101',
'Minguo'
],
[
'xtY',
'18660101000000',
'西暦1866',
'西暦1866',
'nengo - before meiji'
],
[
'xtY',
'18670101000000',
'西暦1867',
'西暦1867',
'nengo - before meiji'
],
[
'xtY',
'18721231235959',
'西暦1872',
'西暦1872',
'nengo - meiji, but Lunisolar calendar'
],
[
'xtY',
'18730101000000',
'明治6',
'明治6',
'nengo - meiji 6th'
],
[
'xtY',
'19120729235959',
'明治45',
'明治45',
'nengo - meiji 45th last day'
],
[
'xtY',
'19120730000000',
'大正元',
'大正元',
'nengo - taisho first day'
],
[
'xtY',
'19130101000000',
'大正2',
'大正2',
'nengo - taisho 2nd'
],
[
'xtY',
'19261224235959',
'大正15',
'大正15',
'nengo - taisho last day'
],
[
'xtY',
'19261225000000',
'昭和元',
'昭和元',
'nengo - first day of Showa'
],
[
'xtY',
'19270101000000',
'昭和2',
'昭和2',
'nengo - second year of Showa'
],
[
'xtY',
'19890107235959',
'昭和64',
'昭和64',
'nengo - last day of Showa'
],
[
'xtY',
'19890108000000',
'平成元',
'平成元',
'nengo - first day of Heisei'
],
[
'xtY',
'19900101000000',
'平成2',
'平成2',
'nengo - second year of Heisei'
],
[
'xtY',
'20190430235959',
'平成31',
'平成31',
'nengo - last day of Heisei'
],
[
'xtY',
'20190501000000',
'令和元',
'令和元',
'nengo - first day of Reiwa'
],
[
'xtY',
'20200501000000',
'令和2',
'令和2',
'nengo - second year of Reiwa'
],
[
'xrxkYY',
'20120102090705',
'MMDLV2012',
'MMDLV2012',
'Roman numerals'
],
[
'xhxjYY',
'20120102090705',
'ה\'תשע"ב2012',
'ה\'תשע"ב2012',
'Hebrew numberals'
],
[
'xnY',
'20120102090705',
'2012',
'2012',
'Raw numerals (doesn\'t mean much in EN)'
],
[
'[[Y "(yea"\\r)]] \\"xx\\"',
'20120102090705',
'[[2012 (year)]] "x"',
'[[2012 (year)]] "x"',
'Various escaping'
],
];
}
2011-12-31 02:13:59 +00:00
/**
* @dataProvider provideFormatSizes
* @covers \Language::formatSize
2011-12-31 02:13:59 +00:00
*/
public function testFormatSize( $size, $expected, $msg ) {
2011-12-31 02:13:59 +00:00
$this->assertEquals(
$expected,
$this->getLang()->formatSize( $size ),
2011-12-31 02:13:59 +00:00
"formatSize('$size'): $msg"
);
}
public static function provideFormatSizes() {
return [
[
2011-12-31 02:13:59 +00:00
0,
"0 bytes",
"Zero bytes"
],
[
2011-12-31 02:13:59 +00:00
1024,
"1 KB",
"1 kilobyte"
],
[
2011-12-31 02:13:59 +00:00
1024 * 1024,
"1 MB",
"1 megabyte"
],
[
2011-12-31 02:13:59 +00:00
1024 * 1024 * 1024,
"1 GB",
"1 gigabyte"
],
[
1024 ** 4,
"1 TB",
"1 terabyte"
],
[
1024 ** 5,
"1 PB",
"1 petabyte"
],
[
1024 ** 6,
"1 EB",
"1 exabyte"
],
[
1024 ** 7,
"1 ZB",
"1 zettabyte"
],
[
1024 ** 8,
"1 YB",
"1 yottabyte"
],
[
1024 ** 9,
"1 RB",
"1 ronnabyte"
],
[
1024 ** 10,
"1 QB",
"1 quettabyte"
],
[
1024 ** 11,
"1,024 QB",
"1,024 quettabytes"
],
// How big!? THIS BIG!
];
2011-12-31 02:13:59 +00:00
}
/**
* @dataProvider provideFormatBitrate
* @covers \Language::formatBitrate
*/
public function testFormatBitrate( $bps, $expected, $msg ) {
$this->assertEquals(
$expected,
$this->getLang()->formatBitrate( $bps ),
"formatBitrate('$bps'): $msg"
);
}
public static function provideFormatBitrate() {
return [
[
0,
"0 bps",
"0 bits per second"
],
[
2012-01-09 14:01:51 +00:00
999,
"999 bps",
"999 bits per second"
],
[
1000,
"1 kbps",
"1 kilobit per second"
],
[
1000 * 1000,
"1 Mbps",
"1 megabit per second"
],
[
10 ** 9,
"1 Gbps",
"1 gigabit per second"
],
[
10 ** 12,
"1 Tbps",
"1 terabit per second"
],
[
10 ** 15,
"1 Pbps",
"1 petabit per second"
],
[
10 ** 18,
"1 Ebps",
"1 exabit per second"
],
[
10 ** 21,
"1 Zbps",
"1 zettabit per second"
],
[
10 ** 24,
"1 Ybps",
"1 yottabit per second"
],
[
10 ** 27,
"1 Rbps",
"1 ronnabits per second"
],
[
10 ** 30,
"1 Qbps",
"1 quettabit per second"
],
[
10 ** 33,
"1,000 Qbps",
"1,000 quettabits per second"
],
];
2012-03-17 22:39:38 +00:00
}
/**
* @dataProvider provideFormatDuration
* @covers \Language::formatDuration
*/
public function testFormatDuration( $duration, $expected, $intervals = [] ) {
$this->assertEquals(
$expected,
$this->getLang()->formatDuration( $duration, $intervals ),
"formatDuration('$duration'): $expected"
);
}
public static function provideFormatDuration() {
return [
[
0,
'0 seconds',
],
[
1,
'1 second',
],
[
2,
'2 seconds',
],
[
60,
'1 minute',
],
[
2 * 60,
'2 minutes',
],
[
3600,
'1 hour',
],
[
2 * 3600,
'2 hours',
],
[
24 * 3600,
'1 day',
],
[
2 * 86400,
'2 days',
],
[
365.2425 * 24 * 3600 / 12,
'1 month',
[ 'months', 'days' ]
],
[
365.2425 * 24 * 3600 / 12 * 2,
'2 months',
[ 'months', 'days' ]
],
[
( 365.2425 * 24 * 3600 / 12 * 2 ) + 24 * 3600,
'2 months and 1 day',
[ 'months', 'days' ]
],
[
// ( 365 + ( 24 * 3 + 25 ) / 400 ) * 86400 = 31556952
( 365 + ( 24 * 3 + 25 ) / 400.0 ) * 86400,
'1 year',
[ 'months', 'years' ]
],
[
2 * 31556952,
'2 years',
],
[
10 * 31556952,
'1 decade',
],
[
20 * 31556952,
'2 decades',
],
[
100 * 31556952,
'1 century',
],
[
200 * 31556952,
'2 centuries',
],
[
1000 * 31556952,
'1 millennium',
],
[
2000 * 31556952,
'2 millennia',
],
[
9001,
'2 hours, 30 minutes and 1 second'
],
[
3601,
'1 hour and 1 second'
],
[
31556952 + 2 * 86400 + 9000,
'1 year, 2 days, 2 hours and 30 minutes'
],
[
42 * 1000 * 31556952 + 42,
'42 millennia and 42 seconds'
],
[
60,
'60 seconds',
[ 'seconds' ],
],
[
61,
'61 seconds',
[ 'seconds' ],
],
[
1,
'1 second',
[ 'seconds' ],
],
[
31556952 + 2 * 86400 + 9000,
'1 year, 2 days and 150 minutes',
[ 'years', 'days', 'minutes' ],
],
[
42,
'0 days',
[ 'years', 'days' ],
],
[
31556952 + 2 * 86400 + 9000,
'1 year, 2 days and 150 minutes',
[ 'minutes', 'days', 'years' ],
],
[
42,
'0 days',
[ 'days', 'years' ],
],
[
( new DateTime( '2025-05-03 20:00:00' ) )->getTimestamp() - ( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
'11 months',
[ 'months' ],
],
[
( new DateTime( '2025-05-03 20:00:00' ) )->getTimestamp() - ( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
'11 months, 30 days, 4 hours, 39 minutes and 54 seconds',
[ 'years', 'months', 'days', 'hours', 'minutes', 'seconds' ],
],
];
}
/**
* @dataProvider provideFormatDurationBetweenTimestamps
* @covers \Language::formatDurationBetweenTimestamps
*/
public function testFormatDurationBetweenTimestamps(
int $timestamp1,
int $timestamp2,
?int $precision,
string $expected
): void {
$this->assertSame(
$expected,
$this->getLang()->formatDurationBetweenTimestamps( $timestamp1, $timestamp2, $precision )
);
$this->assertSame(
$expected,
$this->getLang()->formatDurationBetweenTimestamps( $timestamp2, $timestamp1, $precision )
);
}
public function provideFormatDurationBetweenTimestamps(): array {
return [
// most test cases ported from provideFormatDuration()
[
( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
null,
'0 seconds',
],
[
( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTime( '2024-05-03 20:00:01' ) )->getTimestamp(),
null,
'1 second',
],
[
( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTime( '2024-05-03 20:00:02' ) )->getTimestamp(),
null,
'2 seconds',
],
[
( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTime( '2024-05-03 20:01:00' ) )->getTimestamp(),
null,
'1 minute',
],
[
( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTime( '2024-05-03 20:02:00' ) )->getTimestamp(),
null,
'2 minutes',
],
[
( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTime( '2024-05-03 21:00:00' ) )->getTimestamp(),
null,
'1 hour',
],
[
( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTime( '2024-05-03 22:00:00' ) )->getTimestamp(),
null,
'2 hours',
],
[
( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTime( '2024-05-04 20:00:00' ) )->getTimestamp(),
null,
'1 day',
],
[
( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTime( '2024-05-05 20:00:00' ) )->getTimestamp(),
null,
'2 days',
],
[
( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTime( '2024-06-03 20:00:00' ) )->getTimestamp(),
2,
'1 month',
],
[
( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTime( '2024-07-03 20:00:00' ) )->getTimestamp(),
2,
'2 months',
],
[
( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTime( '2024-07-04 20:00:00' ) )->getTimestamp(),
2,
'2 months and 1 day',
],
[
( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTime( '2025-05-03 20:00:00' ) )->getTimestamp(),
2,
'1 year',
],
[
( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTime( '2026-05-03 20:00:00' ) )->getTimestamp(),
null,
'2 years',
],
[
( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTime( '2034-05-03 20:00:00' ) )->getTimestamp(),
null,
'1 decade',
],
[
( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTime( '2044-05-03 20:00:00' ) )->getTimestamp(),
null,
'2 decades',
],
[
( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTime( '2124-05-03 20:00:00' ) )->getTimestamp(),
null,
'1 century',
],
[
( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTime( '2224-05-03 20:00:00' ) )->getTimestamp(),
null,
'2 centuries',
],
[
( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTime( '3024-05-03 20:00:00' ) )->getTimestamp(),
null,
'1 millennium',
],
[
( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTime( '4024-05-03 20:00:00' ) )->getTimestamp(),
null,
'2 millennia',
],
[
0,
9001,
null,
'2 hours, 30 minutes and 1 second',
],
[
0,
3601,
null,
'1 hour and 1 second',
],
[
( new DateTime( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTime( '2025-05-05 22:30:00' ) )->getTimestamp(),
null,
'1 year, 2 days, 2 hours and 30 minutes',
],
[
( new DateTimeImmutable( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTimeImmutable() )->setDate( 44024, 05, 03 )->setTime( 20, 0, 42 )->getTimestamp(),
null,
'42 millennia and 42 seconds',
],
[
0,
60,
null,
'1 minute',
],
[
0,
61,
null,
'1 minute and 1 second',
],
[
( new DateTimeImmutable( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTimeImmutable() )->setDate( 2025, 05, 05 )->setTime( 22, 30, 0 )->getTimestamp(),
null,
'1 year, 2 days, 2 hours and 30 minutes',
],
[
( new DateTimeImmutable( '2024-05-03 20:00:00' ) )->getTimestamp(),
( new DateTimeImmutable( '2024-10-09 20:15:37' ) )->getTimestamp(),
1,
'5 months',
],
[
( new DateTime( '2022-01-01 10:00:00' ) )->getTimestamp(),
( new DateTime( '2022-01-01 12:30:00' ) )->getTimestamp(),
2,
'2 hours and 30 minutes',
],
[
( new DateTime( '2022-01-01 10:00:00' ) )->getTimestamp(),
( new DateTime( '2022-01-02 12:30:00' ) )->getTimestamp(),
3,
'1 day, 2 hours and 30 minutes',
],
[
( new DateTime( '2022-01-01 10:00:00' ) )->getTimestamp(),
( new DateTime( '2022-01-01 10:30:27' ) )->getTimestamp(),
1,
'30 minutes',
],
[
( new DateTime( '2024-05-03 10:00:00' ) )->getTimestamp(),
( new DateTime( '2025-05-03 10:00:00' ) )->getTimestamp(),
6,
'1 year',
],
[
( new DateTime( '2024-01-28 10:00:00' ) )->getTimestamp(),
( new DateTime( '2024-03-01 10:00:00' ) )->getTimestamp(),
4,
'1 month and 2 days',
],
[
( new DateTime( '2023-01-28 10:00:00' ) )->getTimestamp(),
( new DateTime( '2023-03-01 10:00:00' ) )->getTimestamp(),
6,
'1 month and 1 day',
],
[
( new DateTime( '2023-01-29 20:00:00' ) )->getTimestamp(),
( new DateTime( '2023-02-28 20:00:00' ) )->getTimestamp(),
6,
'30 days',
],
[
( new DateTime( '2023-01-29 20:00:00' ) )->getTimestamp(),
( new DateTime( '2023-03-01 20:00:00' ) )->getTimestamp(),
6,
'1 month',
],
[
( new DateTime( '2023-01-30 20:00:00' ) )->getTimestamp(),
( new DateTime( '2023-03-01 20:00:00' ) )->getTimestamp(),
6,
'30 days',
],
[
( new DateTime( '2023-01-31 20:00:00' ) )->getTimestamp(),
( new DateTime( '2023-03-01 20:00:00' ) )->getTimestamp(),
6,
'29 days',
],
[
( new DateTime( '2023-01-31 20:00:00' ) )->getTimestamp(),
( new DateTime( '2023-01-31 20:00:01' ) )->getTimestamp(),
6,
'1 second',
],
[
( new DateTime( '2023-01-31 20:00:00' ) )->getTimestamp(),
( new DateTime( '2023-01-31 20:00:02' ) )->getTimestamp(),
6,
'2 seconds',
],
[
( new DateTime( '2023-01-31 20:00:00' ) )->getTimestamp(),
( new DateTime( '2023-01-31 20:01:00' ) )->getTimestamp(),
6,
'1 minute',
],
[
( new DateTime( '2023-01-31 20:00:00' ) )->getTimestamp(),
( new DateTime( '2023-01-31 20:02:00' ) )->getTimestamp(),
6,
'2 minutes',
],
[
( new DateTime( '2023-01-31 20:00:00' ) )->getTimestamp(),
( new DateTime( '2023-01-31 21:00:00' ) )->getTimestamp(),
6,
'1 hour',
],
[
( new DateTime( '2023-01-31 20:00:00' ) )->getTimestamp(),
( new DateTime( '2023-01-31 22:00:00' ) )->getTimestamp(),
6,
'2 hours',
],
[
( new DateTime( '2023-01-31 20:00:00' ) )->getTimestamp(),
( new DateTime( '2023-02-01 20:00:00' ) )->getTimestamp(),
6,
'1 day',
],
[
( new DateTime( '2023-01-31 20:00:00' ) )->getTimestamp(),
( new DateTime( '2023-02-02 20:00:00' ) )->getTimestamp(),
6,
'2 days',
],
[
( new DateTime( '2023-03-31 20:00:00' ) )->getTimestamp(),
( new DateTime( '2023-04-31 20:00:00' ) )->getTimestamp(),
6,
'1 month',
],
[
( new DateTime( '2023-01-31 20:00:00' ) )->getTimestamp(),
( new DateTime( '2023-03-31 20:00:00' ) )->getTimestamp(),
6,
'2 months',
],
[
( new DateTime( '2023-01-31 20:00:00' ) )->getTimestamp(),
( new DateTime( '2024-01-31 20:00:00' ) )->getTimestamp(),
6,
'1 year',
],
[
( new DateTime( '2023-01-27 15:00:00' ) )->getTimestamp(),
( new DateTime( '2025-04-31 20:06:00' ) )->getTimestamp(),
5,
'2 years, 3 months, 4 days, 5 hours and 6 minutes',
],
[
( new DateTime( '2023-01-31 15:00:00' ) )->getTimestamp(),
( new DateTime( '3025-04-31 20:06:07' ) )->getTimestamp(),
7,
'1 millennium, 2 years, 3 months, 5 hours, 6 minutes and 7 seconds',
],
[
( new DateTime( '2023-01-28 20:00:00' ) )->getTimestamp(),
( new DateTime( '4030-05-31 22:01:14' ) )->getTimestamp(),
9,
'2 millennia, 7 years, 4 months, 3 days, 2 hours, 1 minute and 14 seconds',
],
];
}
/**
* @dataProvider provideCheckTitleEncodingData
* @covers \Language::checkTitleEncoding
*/
public function testCheckTitleEncoding( $s ) {
$this->assertEquals(
$s,
$this->getLang()->checkTitleEncoding( $s ),
"checkTitleEncoding('$s')"
);
}
public static function provideCheckTitleEncodingData() {
return [
[ "" ],
[ "United States of America" ], // 7bit ASCII
[ rawurldecode( "S%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e" ) ],
[
rawurldecode(
"Acteur%7CAlbert%20Robbins%7CAnglais%7CAnn%20Donahue%7CAnthony%20E.%20Zuiker%7CCarol%20Mendelsohn"
)
],
// The following two data sets come from T38839. They fail if checkTitleEncoding uses a regexp to test for
// valid UTF-8 encoding and the pcre.recursion_limit is low (like, say, 1024). They succeed if checkTitleEncoding
// uses mb_check_encoding for its test.
[
rawurldecode(
"Acteur%7CAlbert%20Robbins%7CAnglais%7CAnn%20Donahue%7CAnthony%20E.%20Zuiker%7CCarol%20Mendelsohn%7C"
. "Catherine%20Willows%7CDavid%20Hodges%7CDavid%20Phillips%7CGil%20Grissom%7CGreg%20Sanders%7CHodges%7C"
. "Internet%20Movie%20Database%7CJim%20Brass%7CLady%20Heather%7C"
. "Les%20Experts%20(s%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e)%7CLes%20Experts%20:%20Manhattan%7C"
. "Les%20Experts%20:%20Miami%7CListe%20des%20personnages%20des%20Experts%7C"
. "Liste%20des%20%C3%A9pisodes%20des%20Experts%7CMod%C3%A8le%20discussion:Palette%20Les%20Experts%7C"
. "Nick%20Stokes%7CPersonnage%20de%20fiction%7CPersonnage%20fictif%7CPersonnage%20de%20fiction%7C"
. "Personnages%20r%C3%A9currents%20dans%20Les%20Experts%7CRaymond%20Langston%7CRiley%20Adams%7C"
. "Saison%201%20des%20Experts%7CSaison%2010%20des%20Experts%7CSaison%2011%20des%20Experts%7C"
. "Saison%2012%20des%20Experts%7CSaison%202%20des%20Experts%7CSaison%203%20des%20Experts%7C"
. "Saison%204%20des%20Experts%7CSaison%205%20des%20Experts%7CSaison%206%20des%20Experts%7C"
. "Saison%207%20des%20Experts%7CSaison%208%20des%20Experts%7CSaison%209%20des%20Experts%7C"
. "Sara%20Sidle%7CSofia%20Curtis%7CS%C3%A9rie%20t%C3%A9l%C3%A9vis%C3%A9e%7CWallace%20Langham%7C"
. "Warrick%20Brown%7CWendy%20Simms%7C%C3%89tats-Unis"
),
],
[
rawurldecode(
"Mod%C3%A8le%3AArrondissements%20homonymes%7CMod%C3%A8le%3ABandeau%20standard%20pour%20page%20d'homonymie%7C"
. "Mod%C3%A8le%3ABatailles%20homonymes%7CMod%C3%A8le%3ACantons%20homonymes%7C"
. "Mod%C3%A8le%3ACommunes%20fran%C3%A7aises%20homonymes%7CMod%C3%A8le%3AFilms%20homonymes%7C"
. "Mod%C3%A8le%3AGouvernements%20homonymes%7CMod%C3%A8le%3AGuerres%20homonymes%7CMod%C3%A8le%3AHomonymie%7C"
. "Mod%C3%A8le%3AHomonymie%20bateau%7CMod%C3%A8le%3AHomonymie%20d'%C3%A9tablissements%20scolaires%20ou"
. "%20universitaires%7CMod%C3%A8le%3AHomonymie%20d'%C3%AEles%7CMod%C3%A8le%3AHomonymie%20de%20clubs%20sportifs%7C"
. "Mod%C3%A8le%3AHomonymie%20de%20comt%C3%A9s%7CMod%C3%A8le%3AHomonymie%20de%20monument%7C"
. "Mod%C3%A8le%3AHomonymie%20de%20nom%20romain%7CMod%C3%A8le%3AHomonymie%20de%20parti%20politique%7C"
. "Mod%C3%A8le%3AHomonymie%20de%20route%7CMod%C3%A8le%3AHomonymie%20dynastique%7C"
. "Mod%C3%A8le%3AHomonymie%20vid%C3%A9oludique%7CMod%C3%A8le%3AHomonymie%20%C3%A9difice%20religieux%7C"
. "Mod%C3%A8le%3AInternationalisation%7CMod%C3%A8le%3AIsom%C3%A9rie%7CMod%C3%A8le%3AParonymie%7C"
. "Mod%C3%A8le%3APatronyme%7CMod%C3%A8le%3APatronyme%20basque%7CMod%C3%A8le%3APatronyme%20italien%7C"
. "Mod%C3%A8le%3APatronymie%7CMod%C3%A8le%3APersonnes%20homonymes%7CMod%C3%A8le%3ASaints%20homonymes%7C"
. "Mod%C3%A8le%3ATitres%20homonymes%7CMod%C3%A8le%3AToponymie%7CMod%C3%A8le%3AUnit%C3%A9s%20homonymes%7C"
. "Mod%C3%A8le%3AVilles%20homonymes%7CMod%C3%A8le%3A%C3%89difices%20religieux%20homonymes"
)
]
];
// phpcs:enable
}
/**
* @dataProvider provideRomanNumeralsData
* @covers \Language::romanNumeral
*/
public function testRomanNumerals( $num, $numerals ) {
$this->assertEquals(
$numerals,
Language::romanNumeral( $num ),
"romanNumeral('$num')"
);
}
public static function provideRomanNumeralsData() {
return [
[ 1, 'I' ],
[ 2, 'II' ],
[ 3, 'III' ],
[ 4, 'IV' ],
[ 5, 'V' ],
[ 6, 'VI' ],
[ 7, 'VII' ],
[ 8, 'VIII' ],
[ 9, 'IX' ],
[ 10, 'X' ],
[ 20, 'XX' ],
[ 30, 'XXX' ],
[ 40, 'XL' ],
[ 49, 'XLIX' ],
[ 50, 'L' ],
[ 60, 'LX' ],
[ 70, 'LXX' ],
[ 80, 'LXXX' ],
[ 90, 'XC' ],
[ 99, 'XCIX' ],
[ 100, 'C' ],
[ 200, 'CC' ],
[ 300, 'CCC' ],
[ 400, 'CD' ],
[ 500, 'D' ],
[ 600, 'DC' ],
[ 700, 'DCC' ],
[ 800, 'DCCC' ],
[ 900, 'CM' ],
[ 999, 'CMXCIX' ],
[ 1000, 'M' ],
[ 1989, 'MCMLXXXIX' ],
[ 2000, 'MM' ],
[ 3000, 'MMM' ],
[ 4000, 'MMMM' ],
[ 5000, 'MMMMM' ],
[ 6000, 'MMMMMM' ],
[ 7000, 'MMMMMMM' ],
[ 8000, 'MMMMMMMM' ],
[ 9000, 'MMMMMMMMM' ],
[ 9999, 'MMMMMMMMMCMXCIX' ],
[ 10000, 'MMMMMMMMMM' ],
];
}
/**
* @dataProvider provideHebrewNumeralsData
* @covers \Language::hebrewNumeral
*/
public function testHebrewNumeral( $num, $numerals ) {
$this->assertEquals(
$numerals,
Language::hebrewNumeral( $num ),
"hebrewNumeral('$num')"
);
}
public static function provideHebrewNumeralsData() {
return [
[ -1, -1 ],
[ 0, 0 ],
[ 1, "א'" ],
[ 2, "ב'" ],
[ 3, "ג'" ],
[ 4, "ד'" ],
[ 5, "ה'" ],
[ 6, "ו'" ],
[ 7, "ז'" ],
[ 8, "ח'" ],
[ 9, "ט'" ],
[ 10, "י'" ],
[ 11, 'י"א' ],
[ 14, 'י"ד' ],
[ 15, 'ט"ו' ],
[ 16, 'ט"ז' ],
[ 17, 'י"ז' ],
[ 20, "כ'" ],
[ 21, 'כ"א' ],
[ 30, "ל'" ],
[ 40, "מ'" ],
[ 50, "נ'" ],
[ 60, "ס'" ],
[ 70, "ע'" ],
[ 80, "פ'" ],
[ 90, "צ'" ],
[ 99, 'צ"ט' ],
[ 100, "ק'" ],
[ 101, 'ק"א' ],
[ 110, 'ק"י' ],
[ 200, "ר'" ],
[ 300, "ש'" ],
[ 400, "ת'" ],
[ 500, 'ת"ק' ],
[ 800, 'ת"ת' ],
[ 1000, "א' אלף" ],
[ 1001, "א'א'" ],
[ 1012, "א'י\"ב" ],
[ 1020, "א'ך'" ],
[ 1030, "א'ל'" ],
[ 1081, "א'פ\"א" ],
[ 2000, "ב' אלפים" ],
[ 2016, "ב'ט\"ז" ],
[ 3000, "ג' אלפים" ],
[ 4000, "ד' אלפים" ],
[ 4904, "ד'תתק\"ד" ],
[ 5000, "ה' אלפים" ],
[ 5680, "ה'תר\"ף" ],
[ 5690, "ה'תר\"ץ" ],
[ 5708, "ה'תש\"ח" ],
[ 5720, "ה'תש\"ך" ],
[ 5740, "ה'תש\"ם" ],
[ 5750, "ה'תש\"ן" ],
[ 5775, "ה'תשע\"ה" ],
];
}
/**
* @dataProvider providePluralData
* @covers \Language::convertPlural
*/
public function testConvertPlural( $expected, $number, $forms ) {
$chosen = $this->getLang()->convertPlural( $number, $forms );
$this->assertEquals( $expected, $chosen );
}
public static function providePluralData() {
// Params are: [expected text, number given, [the plural forms]]
return [
[ 'plural', 0, [
'singular', 'plural'
] ],
[ 'explicit zero', 0, [
'0=explicit zero', 'singular', 'plural'
] ],
[ 'explicit one', 1, [
'singular', 'plural', '1=explicit one',
] ],
[ 'singular', 1, [
'singular', 'plural', '0=explicit zero',
] ],
[ 'plural', 3, [
'0=explicit zero', '1=explicit one', 'singular', 'plural'
] ],
[ 'explicit eleven', 11, [
'singular', 'plural', '11=explicit eleven',
] ],
[ 'plural', 12, [
'singular', 'plural', '11=explicit twelve',
] ],
[ 'plural', 12, [
'singular', 'plural', '=explicit form',
] ],
[ 'other', 2, [
'kissa=kala', '1=2=3', 'other',
] ],
[ '', 2, [
'0=explicit zero', '1=explicit one',
] ],
];
}
/**
* @covers \Language::embedBidi()
*/
public function testEmbedBidi() {
$lre = "\u{202A}"; // U+202A LEFT-TO-RIGHT EMBEDDING
$rle = "\u{202B}"; // U+202B RIGHT-TO-LEFT EMBEDDING
$pdf = "\u{202C}"; // U+202C POP DIRECTIONAL FORMATTING
$lang = $this->getLang();
$this->assertSame(
'123',
$lang->embedBidi( '123' ),
'embedBidi with neutral argument'
);
$this->assertEquals(
$lre . 'Ben_(WMF)' . $pdf,
$lang->embedBidi( 'Ben_(WMF)' ),
'embedBidi with LTR argument'
);
$this->assertEquals(
$rle . 'יהודי (מנוחין)' . $pdf,
$lang->embedBidi( 'יהודי (מנוחין)' ),
'embedBidi with RTL argument'
);
}
/**
* @covers \Language::translateBlockExpiry()
* @dataProvider provideTranslateBlockExpiry
*/
public function testTranslateBlockExpiry( $expectedData, $str, $now, $desc ) {
$lang = $this->getLang();
if ( is_array( $expectedData ) ) {
[ $func, $arg ] = $expectedData;
$expected = $lang->$func( $arg );
} else {
$expected = $expectedData;
}
// HACK:
date_default_timezone_set( 'UTC' );
$this->assertSame( $expected, $lang->translateBlockExpiry( $str, null, $now ), $desc );
}
public static function provideTranslateBlockExpiry() {
return [
[ '2 hours', '2 hours', 0, 'simple data from ipboptions' ],
[ 'indefinite', 'infinite', 0, 'infinite from ipboptions' ],
[ 'indefinite', 'infinity', 0, 'alternative infinite from ipboptions' ],
[ 'indefinite', 'indefinite', 0, 'another alternative infinite from ipboptions' ],
[ [ 'formatDuration', 1023 * 60 * 60 ], '1023 hours', 0, 'relative' ],
[ [ 'formatDuration', -1023 ], '-1023 seconds', 0, 'negative relative' ],
[
[ 'formatDuration', 1023 * 60 * 60 ],
'1023 hours',
wfTimestamp( TS_UNIX, '19910203040506' ),
'relative with initial timestamp'
],
[ [ 'formatDuration', 0 ], 'now', 0, 'now' ],
[
[ 'timeanddate', '20120102070000' ],
'2012-1-1 7:00 +1 day',
0,
'mixed, handled as absolute'
],
[ [ 'timeanddate', '19910203040506' ], '1991-2-3 4:05:06', 0, 'absolute' ],
[ [ 'timeanddate', '19700101000000' ], '1970-1-1 0:00:00', 0, 'absolute at epoch' ],
[ [ 'timeanddate', '19691231235959' ], '1969-12-31 23:59:59', 0, 'time before epoch' ],
[
[ 'timeanddate', '19910910000000' ],
'10 september',
wfTimestamp( TS_UNIX, '19910203040506' ),
'partial'
],
[ 'dummy', 'dummy', 0, 'return garbage as is' ],
];
}
/**
* @dataProvider provideFormatNum
* @covers \Language::formatNum
* @covers \Language::formatNumNoSeparators
*/
public function testFormatNum(
$translateNumerals, $langCode, $number, $noSeparators, $expected
) {
$this->hideDeprecated( 'Language::formatNum with a non-numeric string' );
$this->overrideConfigValue( MainConfigNames::TranslateNumerals, $translateNumerals );
$lang = $this->getServiceContainer()->getLanguageFactory()->getLanguage( $langCode );
if ( $noSeparators ) {
$formattedNum = $lang->formatNumNoSeparators( $number );
} else {
$formattedNum = $lang->formatNum( $number );
}
$this->assertIsString( $formattedNum );
$this->assertEquals( $expected, $formattedNum );
}
public static function provideFormatNum() {
return [
[ true, 'en', 100, false, '100' ],
[ true, 'en', 101, true, '101' ],
[ false, 'en', 103, false, '103' ],
[ false, 'en', 104, true, '104' ],
[ true, 'en', '105', false, '105' ],
[ true, 'en', '106', true, '106' ],
[ false, 'en', '107', false, '107' ],
[ false, 'en', '108', true, '108' ],
[ true, 'en', -1, false, '1' ],
[ true, 'en', 10, false, '10' ],
[ true, 'en', 100, false, '100' ],
[ true, 'en', 1000, false, '1,000' ],
[ true, 'en', 10000, false, '10,000' ],
[ true, 'en', 100000, false, '100,000' ],
[ true, 'en', 1000000, false, '1,000,000' ],
[ true, 'en', -1.001, false, '1.001' ],
[ true, 'en', 1.001, false, '1.001' ],
[ true, 'en', 10.0001, false, '10.0001' ],
[ true, 'en', 100.001, false, '100.001' ],
[ true, 'en', 1000.001, false, '1,000.001' ],
[ true, 'en', 10000.001, false, '10,000.001' ],
[ true, 'en', 100000.001, false, '100,000.001' ],
[ true, 'en', 1000000.0001, false, '1,000,000.0001' ],
[ true, 'en', -1.0001, false, '1.0001' ],
[ true, 'en', '200000000000000000000', false, '200,000,000,000,000,000,000' ],
[ true, 'en', '-200000000000000000000', false, '200,000,000,000,000,000,000' ],
[ true, 'en', '1.23e10', false, '12,300,000,000' ],
[ true, 'en', 1.23e10, false, '12,300,000,000' ],
[ true, 'en', '1.23E-01', false, '0.123' ],
[ true, 'en', 1.23e-1, false, '0.123' ],
[ true, 'en', 0.0, false, '0' ],
[ true, 'en', -0.0, false, '0' ],
[ true, 'en', INF, false, '∞' ],
[ true, 'en', -INF, false, '−∞' ],
[ true, 'en', NAN, false, 'Not a Number' ],
Language: ensure commafy does not corrupt UTF-8 strings The commafy method "should" be given valid numeric strings, but this wasn't enforced, and if were provided input which started with a UTF-8 multibyte character and then had a single ASCII digit somewhere after that, it would return the first byte of the input string, resulting in an invalid UTF-8 sequence. Fix this bug with belt and suspenders: first, enforce the expected input structure at the top of the function. Since there is existing code which expects us to "do our best" with invalid input, split the input string into valid numeric chunks before processing it. This split code triggers a hard deprecation warning, so we can eventually remove it. Second, make the sign test more robust and anchor the $integerPart regexp to match assumptions made in the algorithm, so that even if bogus input *did* creep through (a sloppy future maintainer, say) it wouldn't lead to corrupt UTF-8 in the output. Add test cases covering these conditions, borrowing liberally from I741b70757e43b1312c86719920e29885566e916c, which points out that while commafy expects numeric strings, formatNum replaces – character by character – digits and separator characters with language specific ones. Optionally thousand separators are added (a.k.a. "commafy"). Eventually we should tighten the spec for formatNum as well; some of this has already been done in I03ffa99f7de1dcc48535ba1e1251567dbf3db116 and I89b17a9e11b3afc6c653ba7ccc6ff84c37863b66. Some additional test case fixes borrowed from If45ef33a50b2623322f17306d123f0d8cb468618 which updated a few test cases to be more specific, i.e. actually test stuff (for example, commafy doesn't happen on 3-digit numbers, and numerals are not translated in English). Bug: T237467 Depends-On: I89b17a9e11b3afc6c653ba7ccc6ff84c37863b66 Depends-On: I9dcbe91fa926dba1cfd24d9bf075ee1ebef36b9e Depends-On: I03ffa99f7de1dcc48535ba1e1251567dbf3db116 Change-Id: If3dcfd71acd8ebf3eea6a49408260f2aaa07e469
2020-09-08 23:19:41 +00:00
[ true, 'kn', '1050', false, '೧,೦೫೦' ],
[ true, 'kn', '1060', true, '೧೦೬೦' ],
[ false, 'kn', '1070', false, '1,070' ],
[ false, 'kn', '1080', true, '1080' ],
[ true, 'kn', '.1090', false, '.೧೦೯೦' ],
// Make sure non-numeric strings are not destroyed
[ false, 'en', 'The number is 1234', false, 'The number is 1,234' ],
[ false, 'en', '1234 is the number', false, '1,234 is the number' ],
[ false, 'de', '.', false, '.' ],
[ false, 'de', ',', false, ',' ],
Language: ensure commafy does not corrupt UTF-8 strings The commafy method "should" be given valid numeric strings, but this wasn't enforced, and if were provided input which started with a UTF-8 multibyte character and then had a single ASCII digit somewhere after that, it would return the first byte of the input string, resulting in an invalid UTF-8 sequence. Fix this bug with belt and suspenders: first, enforce the expected input structure at the top of the function. Since there is existing code which expects us to "do our best" with invalid input, split the input string into valid numeric chunks before processing it. This split code triggers a hard deprecation warning, so we can eventually remove it. Second, make the sign test more robust and anchor the $integerPart regexp to match assumptions made in the algorithm, so that even if bogus input *did* creep through (a sloppy future maintainer, say) it wouldn't lead to corrupt UTF-8 in the output. Add test cases covering these conditions, borrowing liberally from I741b70757e43b1312c86719920e29885566e916c, which points out that while commafy expects numeric strings, formatNum replaces – character by character – digits and separator characters with language specific ones. Optionally thousand separators are added (a.k.a. "commafy"). Eventually we should tighten the spec for formatNum as well; some of this has already been done in I03ffa99f7de1dcc48535ba1e1251567dbf3db116 and I89b17a9e11b3afc6c653ba7ccc6ff84c37863b66. Some additional test case fixes borrowed from If45ef33a50b2623322f17306d123f0d8cb468618 which updated a few test cases to be more specific, i.e. actually test stuff (for example, commafy doesn't happen on 3-digit numbers, and numerals are not translated in English). Bug: T237467 Depends-On: I89b17a9e11b3afc6c653ba7ccc6ff84c37863b66 Depends-On: I9dcbe91fa926dba1cfd24d9bf075ee1ebef36b9e Depends-On: I03ffa99f7de1dcc48535ba1e1251567dbf3db116 Change-Id: If3dcfd71acd8ebf3eea6a49408260f2aaa07e469
2020-09-08 23:19:41 +00:00
/** @see https://phabricator.wikimedia.org/T237467 */
[ false, 'kn', "\u{FFFD}0", false, "\u{FFFD}0" ],
[ false, 'kn', "-೭\u{FFFD}0", false, "-೭\u{FFFD}0" ],
[ false, 'kn', "-1೭\u{FFFD}0", false, "1೭\u{FFFD}0" ],
/** @see https://phabricator.wikimedia.org/T267614 */
[ false, 'ar', "1", false, "1" ],
[ false, 'ar', "1234.5", false, "1٬234٫5" ],
[ true, 'ar', "1", false, "١" ],
[ true, 'ar', "1234.5", false, "١٬٢٣٤٫٥" ],
// Test minimumGroupingDigits > 1
[ false, 'pl', 1, false, '1' ],
[ false, 'pl', 100, false, '100' ],
[ false, 'pl', 1000, false, '1000' ],
[ false, 'pl', 10000, false, "10\u{00A0}000" ],
[ false, 'pl', 1000000, false, "1\u{00A0}000\u{00A0}000" ],
[ false, 'pl', '1000.1', false, "1000,1" ],
];
}
/**
* @covers \Language::parseFormattedNumber
* @dataProvider parseFormattedNumberProvider
*/
public function testParseFormattedNumber( $langCode, $number ) {
$lang = $this->getServiceContainer()->getLanguageFactory()->getLanguage( $langCode );
$localisedNum = $lang->formatNum( $number );
$normalisedNum = $lang->parseFormattedNumber( $localisedNum );
$this->assertEquals( $number, $normalisedNum );
}
public static function parseFormattedNumberProvider() {
return [
[ 'de', 377.01 ],
[ 'fa', 334 ],
[ 'fa', 382.772 ],
[ 'ar', 1844 ],
[ 'lzh', 3731 ],
[ 'zh-classical', 7432 ],
[ 'en', 1234.567 ],
[ 'en', 0.0 ],
[ 'en', -0.0 ],
[ 'en', INF ],
[ 'en', -INF ],
[ 'en', NAN ],
];
}
/**
* @covers \Language::listToText
*/
public function testListToText() {
$lang = $this->getLang();
$and = $lang->getMessageFromDB( 'and' );
$s = $lang->getMessageFromDB( 'word-separator' );
$c = $lang->getMessageFromDB( 'comma-separator' );
$this->assertSame( '', $lang->listToText( [] ) );
$this->assertEquals( 'a', $lang->listToText( [ 'a' ] ) );
$this->assertEquals( "a{$and}{$s}b", $lang->listToText( [ 'a', 'b' ] ) );
$this->assertEquals( "a{$c}b{$and}{$s}c", $lang->listToText( [ 'a', 'b', 'c' ] ) );
$this->assertEquals( "a{$c}b{$c}c{$and}{$s}d", $lang->listToText( [ 'a', 'b', 'c', 'd' ] ) );
}
/**
* Example of the real localisation files being loaded.
*
* This might be a bit cumbersome to maintain long-term,
* but still valueable to have as integration test.
*
* @covers \Language
* @covers \LocalisationCache
*/
public function testGetNamespaceAliasesReal() {
$language = $this->getServiceContainer()->getLanguageFactory()->getLanguage( 'zh' );
$aliases = $language->getNamespaceAliases();
$this->assertSame( NS_FILE, $aliases['文件'] );
$this->assertSame( NS_FILE, $aliases['檔案'] );
}
/**
* @covers \Language::getNamespaceAliases
*/
public function testGetNamespaceAliasesFullLogic() {
$hooks = $this->createHookContainer( [
'Language::getMessagesFileName' => static function ( $code, &$file ) {
$file = __DIR__ . '/../../data/messages/Messages_' . $code . '.php';
}
] );
$langNameUtils = $this->getDummyLanguageNameUtils( [ 'hookContainer' => $hooks ] );
$this->overrideConfigValue( MainConfigNames::NamespaceAliases, [
'Mouse' => NS_SPECIAL,
] );
$this->setService( 'LanguageNameUtils', $langNameUtils );
$language = $this->getServiceContainer()->getLanguageFactory()->getLanguage( 'x-bar' );
$this->assertEquals(
[
// from x-bar
'Cat' => NS_FILE,
'Cat_toots' => NS_FILE_TALK,
// inherited from x-foo
'Dog' => NS_USER,
'Dog_woofs' => NS_USER_TALK,
// add from site configuration
'Mouse' => NS_SPECIAL,
],
$language->getNamespaceAliases()
);
}
/**
* @covers \Language::equals
*/
public function testEquals() {
$languageFactory = $this->getServiceContainer()->getLanguageFactory();
$en1 = $languageFactory->getLanguage( 'en' );
$en2 = $languageFactory->getLanguage( 'en' );
$en3 = $this->newLanguage();
$this->assertTrue( $en1->equals( $en2 ), 'en1 equals en2' );
$this->assertTrue( $en2->equals( $en3 ), 'en2 equals en3' );
$this->assertTrue( $en3->equals( $en1 ), 'en3 equals en1' );
$fr = $languageFactory->getLanguage( 'fr' );
$this->assertFalse( $en1->equals( $fr ), 'en not equals fr' );
$ar1 = $languageFactory->getLanguage( 'ar' );
$ar2 = $this->newLanguage( LanguageAr::class, 'ar' );
$this->assertTrue( $ar1->equals( $ar2 ), 'ar equals ar' );
}
/**
* @dataProvider provideUcfirst
* @covers \Language::ucfirst
*/
public function testUcfirst( $orig, $expected, $desc, $overrides = false ) {
$lang = $this->newLanguage();
if ( is_array( $overrides ) ) {
$this->overrideConfigValue(
MainConfigNames::OverrideUcfirstCharacters,
$overrides
);
}
$this->assertSame( $expected, $lang->ucfirst( $orig ), $desc );
}
public static function provideUcfirst() {
return [
[ 'alice', 'Alice', 'simple ASCII string', false ],
[ 'århus', 'Århus', 'unicode string', false ],
// overrides do not affect ASCII characters
[ 'foo', 'Foo', 'ASCII is not overridden', [ 'f' => 'b' ] ],
// but they do affect non-ascii ones
[ 'èl', 'Ll', 'Non-ASCII is overridden', [ 'è' => 'L' ] ],
[ 'ვიკიპედია', 'ვიკიპედია', 'Georgian case is preserved', false ],
];
}
// The following methods are for LanguageNameUtilsTestTrait
private function isSupportedLanguage( $code ) {
return $this->getServiceContainer()->getLanguageNameUtils()->isSupportedLanguage( $code );
}
private function isValidCode( $code ) {
return $this->getServiceContainer()->getLanguageNameUtils()->isValidCode( $code );
}
private function isValidBuiltInCode( $code ) {
return $this->getServiceContainer()->getLanguageNameUtils()->isValidBuiltInCode( $code );
}
private function isKnownLanguageTag( $code ) {
return $this->getServiceContainer()->getLanguageNameUtils()->isKnownLanguageTag( $code );
}
protected function setLanguageTemporaryHook( string $hookName, $handler ): void {
$this->setTemporaryHook( $hookName, $handler );
}
protected function clearLanguageHook( string $hookName ): void {
$this->clearHook( $hookName );
}
/**
* Call getLanguageName() and getLanguageNames() using the Language static methods.
*
* @param array $options To set globals for testing Language
* @param string $expected
* @param string $code
* @param mixed ...$otherArgs Optionally, pass $inLanguage and/or $include.
*/
private function assertGetLanguageNames( array $options, $expected, $code, ...$otherArgs ) {
if ( $options ) {
$this->overrideConfigValues( $options );
}
$langNameUtils = $this->getServiceContainer()->getLanguageNameUtils();
$this->assertSame( $expected,
$langNameUtils->getLanguageNames( ...$otherArgs )[strtolower( $code )] ?? '' );
$this->assertSame( $expected, $langNameUtils->getLanguageName( $code, ...$otherArgs ) );
}
private function getLanguageNames( ...$args ) {
return $this->getServiceContainer()->getLanguageNameUtils()->getLanguageNames( ...$args );
}
private function getLanguageName( ...$args ) {
return $this->getServiceContainer()->getLanguageNameUtils()->getLanguageName( ...$args );
}
private function getFileName( ...$args ) {
return MediaWikiServices::getInstance()->getLanguageNameUtils()->getFileName( ...$args );
}
private function getMessagesFileName( $code ) {
return MediaWikiServices::getInstance()->getLanguageNameUtils()->getMessagesFileName( $code );
}
private function getJsonMessagesFileName( $code ) {
return MediaWikiServices::getInstance()->getLanguageNameUtils()->getJsonMessagesFileName( $code );
}
/**
* @todo This really belongs in the cldr extension's tests.
*
* @covers \MediaWiki\Languages\LanguageNameUtils::isKnownLanguageTag
*/
public function testCldr() {
$this->markTestSkippedIfExtensionNotLoaded( 'CLDR' );
$languageNameUtils = $this->getServiceContainer()->getLanguageNameUtils();
// "pal" is an ancient language, which probably will not appear in Names.php, but appears in
// CLDR in English
$this->assertTrue( $languageNameUtils->isKnownLanguageTag( 'pal' ) );
$this->assertSame( 'allemand', $languageNameUtils->getLanguageName( 'de', 'fr' ) );
}
/**
* @covers \Language::getNamespaces
* @covers \Language::fixVariableInNamespace
* @dataProvider provideGetNamespaces
*/
public function testGetNamespaces( string $langCode, array $config, array $expected ) {
$services = $this->getServiceContainer();
$langClass = Language::class . ucfirst( $langCode );
if ( !class_exists( $langClass ) ) {
$langClass = Language::class;
}
$config += [
MainConfigNames::MetaNamespace => 'Project',
MainConfigNames::MetaNamespaceTalk => false,
MainConfigNames::ExtraNamespaces => [],
];
$nsInfo = new NamespaceInfo(
new ServiceOptions( NamespaceInfo::CONSTRUCTOR_OPTIONS, $config, $services->getMainConfig() ),
$services->getHookContainer(),
ExtensionRegistry::getInstance()->getAttribute( 'ExtensionNamespaces' ),
ExtensionRegistry::getInstance()->getAttribute( 'ImmovableNamespaces' )
);
/** @var Language $lang */
$lang = new $langClass(
$langCode,
$nsInfo,
$services->getLocalisationCache(),
$this->createNoOpMock( LanguageNameUtils::class ),
$this->createNoOpMock( LanguageFallback::class ),
Hooks::run() call site migration Migrate all callers of Hooks::run() to use the new HookContainer/HookRunner system. General principles: * Use DI if it is already used. We're not changing the way state is managed in this patch. * HookContainer is always injected, not HookRunner. HookContainer is a service, it's a more generic interface, it is the only thing that provides isRegistered() which is needed in some cases, and a HookRunner can be efficiently constructed from it (confirmed by benchmark). Because HookContainer is needed for object construction, it is also needed by all factories. * "Ask your friendly local base class". Big hierarchies like SpecialPage and ApiBase have getHookContainer() and getHookRunner() methods in the base class, and classes that extend that base class are not expected to know or care where the base class gets its HookContainer from. * ProtectedHookAccessorTrait provides protected getHookContainer() and getHookRunner() methods, getting them from the global service container. The point of this is to ease migration to DI by ensuring that call sites ask their local friendly base class rather than getting a HookRunner from the service container directly. * Private $this->hookRunner. In some smaller classes where accessor methods did not seem warranted, there is a private HookRunner property which is accessed directly. Very rarely (two cases), there is a protected property, for consistency with code that conventionally assumes protected=private, but in cases where the class might actually be overridden, a protected accessor is preferred over a protected property. * The last resort: Hooks::runner(). Mostly for static, file-scope and global code. In a few cases it was used for objects with broken construction schemes, out of horror or laziness. Constructors with new required arguments: * AuthManager * BadFileLookup * BlockManager * ClassicInterwikiLookup * ContentHandlerFactory * ContentSecurityPolicy * DefaultOptionsManager * DerivedPageDataUpdater * FullSearchResultWidget * HtmlCacheUpdater * LanguageFactory * LanguageNameUtils * LinkRenderer * LinkRendererFactory * LocalisationCache * MagicWordFactory * MessageCache * NamespaceInfo * PageEditStash * PageHandlerFactory * PageUpdater * ParserFactory * PermissionManager * RevisionStore * RevisionStoreFactory * SearchEngineConfig * SearchEngineFactory * SearchFormWidget * SearchNearMatcher * SessionBackend * SpecialPageFactory * UserNameUtils * UserOptionsManager * WatchedItemQueryService * WatchedItemStore Constructors with new optional arguments: * DefaultPreferencesFactory * Language * LinkHolderArray * MovePage * Parser * ParserCache * PasswordReset * Router setHookContainer() now required after construction: * AuthenticationProvider * ResourceLoaderModule * SearchEngine Change-Id: Id442b0dbe43aba84bd5cf801d86dedc768b082c7
2020-03-19 02:42:09 +00:00
$this->createNoOpMock( LanguageConverterFactory::class ),
$this->createMock( HookContainer::class ),
new MultiConfig( [ new HashConfig( $config ), $services->getMainConfig() ] )
);
$namespaces = $lang->getNamespaces();
$this->assertArraySubmapSame( $expected, $namespaces );
}
public static function provideGetNamespaces() {
$enNamespaces = [
NS_MEDIA => 'Media',
NS_SPECIAL => 'Special',
NS_MAIN => '',
NS_TALK => 'Talk',
NS_USER => 'User',
NS_USER_TALK => 'User_talk',
NS_FILE => 'File',
NS_FILE_TALK => 'File_talk',
NS_MEDIAWIKI => 'MediaWiki',
NS_MEDIAWIKI_TALK => 'MediaWiki_talk',
NS_TEMPLATE => 'Template',
NS_TEMPLATE_TALK => 'Template_talk',
NS_HELP => 'Help',
NS_HELP_TALK => 'Help_talk',
NS_CATEGORY => 'Category',
NS_CATEGORY_TALK => 'Category_talk',
];
$ukNamespaces = [
NS_MEDIA => 'Медіа',
NS_SPECIAL => 'Спеціальна',
NS_TALK => 'Обговорення',
NS_USER => 'Користувач',
NS_USER_TALK => 'Обговорення_користувача',
NS_FILE => 'Файл',
NS_FILE_TALK => 'Обговорення_файлу',
NS_MEDIAWIKI => 'MediaWiki',
NS_MEDIAWIKI_TALK => 'Обговорення_MediaWiki',
NS_TEMPLATE => 'Шаблон',
NS_TEMPLATE_TALK => 'Обговорення_шаблону',
NS_HELP => 'Довідка',
NS_HELP_TALK => 'Обговорення_довідки',
NS_CATEGORY => 'Категорія',
NS_CATEGORY_TALK => 'Обговорення_категорії',
];
return [
'Default configuration' => [
'en',
[],
$enNamespaces + [
NS_PROJECT => 'Project',
NS_PROJECT_TALK => 'Project_talk',
],
],
'Custom project NS + extra' => [
'en',
[
MainConfigNames::MetaNamespace => 'Wikipedia',
MainConfigNames::ExtraNamespaces => [
100 => 'Borderlands',
101 => 'Borderlands_talk',
],
],
$enNamespaces + [
NS_PROJECT => 'Wikipedia',
NS_PROJECT_TALK => 'Wikipedia_talk',
100 => 'Borderlands',
101 => 'Borderlands_talk',
],
],
'Custom project NS and talk + extra' => [
'en',
[
MainConfigNames::MetaNamespace => 'Wikipedia',
MainConfigNames::MetaNamespaceTalk => 'Wikipedia_drama',
MainConfigNames::ExtraNamespaces => [
100 => 'Borderlands',
101 => 'Borderlands_talk',
],
],
$enNamespaces + [
NS_PROJECT => 'Wikipedia',
NS_PROJECT_TALK => 'Wikipedia_drama',
100 => 'Borderlands',
101 => 'Borderlands_talk',
],
],
'Ukrainian default' => [
'uk',
[],
$ukNamespaces + [
NS_MAIN => '',
NS_PROJECT => 'Project',
NS_PROJECT_TALK => 'Обговорення_Project',
],
],
'Ukrainian custom NS' => [
'uk',
[
MainConfigNames::MetaNamespace => 'Вікіпедія',
],
$ukNamespaces + [
NS_MAIN => '',
NS_PROJECT => 'Вікіпедія',
NS_PROJECT_TALK => 'Обговорення_Вікіпедії',
],
],
];
}
/**
* @covers \Language::getGroupName
*/
public function testGetGroupName() {
$lang = $this->getLang();
$groupName = $lang->getGroupName( 'bot' );
$this->assertSame( 'Bots', $groupName );
}
/**
* @covers \Language::getGroupMemberName
*/
public function testGetGroupMemberName() {
$lang = $this->getLang();
$user = new UserIdentityValue( 1, 'user' );
$groupMemberName = $lang->getGroupMemberName( 'bot', $user );
$this->assertSame( 'bot', $groupMemberName );
$lang = $this->getServiceContainer()->getLanguageFactory()->getLanguage( 'qqx' );
$groupMemberName = $lang->getGroupMemberName( 'bot', $user );
$this->assertSame( '(group-bot-member: user)', $groupMemberName );
}
/**
* @covers \Language::msg
*/
public function testMsg() {
$lang = TestingAccessWrapper::newFromObject( $this->getLang() );
$this->assertSame( 'Line 1:', $lang->msg( 'lineno', '1' )->text() );
}
/**
* @covers \Language::getBlockDurations
*/
public function testBlockDurations() {
$lang = $this->getLang();
$durations = $lang->getBlockDurations();
$this->assertContains( 'other', $durations );
$this->assertContains( 'infinite', $durations );
$this->assertContains( '1 day', $durations );
}
}