diff options
author | Máté Szabó <mszabo@wikimedia.org> | 2025-03-10 16:35:29 +0100 |
---|---|---|
committer | Máté Szabó <mszabo@wikimedia.org> | 2025-03-10 16:51:33 +0100 |
commit | ff7b937f5276e97ef0db2707843635ea1601b84b (patch) | |
tree | b4c7353d96bdc93877e949c76ea4683d2c6dc3c9 | |
parent | e0c9c34cdf723a0671fce193198f3d2fca6eea7e (diff) | |
download | mediawikicore-ff7b937f5276e97ef0db2707843635ea1601b84b.tar.gz mediawikicore-ff7b937f5276e97ef0db2707843635ea1601b84b.zip |
language: fix mw.language.convertGrammar with word-specific casing rules
Why:
- mw.language.convertGrammar is responsible for implementing grammatical
transformations for inflected languages, similar to its backend
counterpart Language::convertGrammar.
- The default implementation may be overridden by language-specific
implementations loaded dynamically based on the user language.
- Both the default implementation and all language-specific
implementations suffer from a bug where requesting a case
transformation for which word-specific casing rules are defined will
return "undefined" if the case transformation was requested for a word
for which no word-specific casing rule exists.
- There are QUnit tests for the logic, but they only run if the user
language matches the language being tested. This means they never run
in CI and cannot even be run locally, since Special:JavaScriptTest has
been forcing the user language to "qqx" for more than five years.
What:
- Place language-specific convertGrammar overrides into a new
convertGrammarMapping property keyed by language code to allow
loading more than one simultaneously.
- Introduce and use a new 'mediawiki.language.grammar.testdata'
test-only RL module that loads every language-specific convertGrammar
override.
- Introduce and use a new 'mediawiki.language.testdata' test-only RL
module that loads language-specific rules for languages that we have
QUnit tests for. Since this only covers less than two dozen languages,
there'd be no value in loading this data for all 300+ languages
supported by MediaWiki.
- Update QUnit tests for convertGrammar to override the user language
and language rules before each case.
- Ensure word-specific casing rules always take precedence over
language-specific convertGrammar implementations to avoid repeated
boilerplate in language-specific code.
- Don't attempt to use word-specific casing rules for words that do not
have one.
- As we're here, add matching PHPUnit tests for Language* subclasses
that had preexisting QUnit tests but no PHPUnit tests.
Bug: T388370
Change-Id: I3f2432f5f801c2a7e4390c2ff2038363a36e2ed9
24 files changed, 388 insertions, 90 deletions
diff --git a/resources/src/mediawiki.language/languages/bs.js b/resources/src/mediawiki.language/languages/bs.js index 1ade9a773156..f261d4fb27ef 100644 --- a/resources/src/mediawiki.language/languages/bs.js +++ b/resources/src/mediawiki.language/languages/bs.js @@ -2,11 +2,7 @@ * Bosnian (bosanski) language functions */ -mw.language.convertGrammar = function ( word, form ) { - const grammarForms = mw.language.getData( 'bs', 'grammarForms' ); - if ( grammarForms && grammarForms[ form ] ) { - return grammarForms[ form ][ word ]; - } +mw.language.convertGrammarMapping.bs = function ( word, form ) { switch ( form ) { case 'instrumental': // instrumental word = 's ' + word; diff --git a/resources/src/mediawiki.language/languages/dsb.js b/resources/src/mediawiki.language/languages/dsb.js index ff30168c30c1..e0035cecf512 100644 --- a/resources/src/mediawiki.language/languages/dsb.js +++ b/resources/src/mediawiki.language/languages/dsb.js @@ -1,12 +1,7 @@ /*! * Lower Sorbian (Dolnoserbski) language functions */ - -mw.language.convertGrammar = function ( word, form ) { - const grammarForms = mw.language.getData( 'dsb', 'grammarForms' ); - if ( grammarForms && grammarForms[ form ] ) { - return grammarForms[ form ][ word ]; - } +mw.language.convertGrammarMapping.dsb = function ( word, form ) { switch ( form ) { case 'instrumental': // instrumental word = 'z ' + word; diff --git a/resources/src/mediawiki.language/languages/fi.js b/resources/src/mediawiki.language/languages/fi.js index a9d512e84e1c..77adb6da7628 100644 --- a/resources/src/mediawiki.language/languages/fi.js +++ b/resources/src/mediawiki.language/languages/fi.js @@ -3,12 +3,7 @@ * @author Santhosh Thottingal */ -mw.language.convertGrammar = function ( word, form ) { - const grammarForms = mw.language.getData( 'fi', 'grammarForms' ); - if ( grammarForms && grammarForms[ form ] ) { - return grammarForms[ form ][ word ]; - } - +mw.language.convertGrammarMapping.fi = function ( word, form ) { // vowel harmony flag let aou = /[aou][^äöy]*$/i.test( word ); const origWord = word; diff --git a/resources/src/mediawiki.language/languages/ga.js b/resources/src/mediawiki.language/languages/ga.js index b83f89813f83..05c54fe656a1 100644 --- a/resources/src/mediawiki.language/languages/ga.js +++ b/resources/src/mediawiki.language/languages/ga.js @@ -2,11 +2,7 @@ * Irish (Gaeilge) language functions */ -mw.language.convertGrammar = function ( word, form ) { - const grammarForms = mw.language.getData( 'ga', 'grammarForms' ); - if ( grammarForms && grammarForms[ form ] ) { - return grammarForms[ form ][ word ]; - } +mw.language.convertGrammarMapping.ga = function ( word, form ) { switch ( form ) { case 'ainmlae': switch ( word ) { diff --git a/resources/src/mediawiki.language/languages/hsb.js b/resources/src/mediawiki.language/languages/hsb.js index c7c6b03082fb..ffc1bcd8e255 100644 --- a/resources/src/mediawiki.language/languages/hsb.js +++ b/resources/src/mediawiki.language/languages/hsb.js @@ -2,11 +2,7 @@ * Upper Sorbian (Hornjoserbsce) language functions */ -mw.language.convertGrammar = function ( word, form ) { - const grammarForms = mw.language.getData( 'hsb', 'grammarForms' ); - if ( grammarForms && grammarForms[ form ] ) { - return grammarForms[ form ][ word ]; - } +mw.language.convertGrammarMapping.hsb = function ( word, form ) { switch ( form ) { case 'instrumental': // instrumental word = 'z ' + word; diff --git a/resources/src/mediawiki.language/languages/hu.js b/resources/src/mediawiki.language/languages/hu.js index 74847feabce9..4ca2ed5bd8ed 100644 --- a/resources/src/mediawiki.language/languages/hu.js +++ b/resources/src/mediawiki.language/languages/hu.js @@ -3,11 +3,7 @@ * @author Santhosh Thottingal */ -mw.language.convertGrammar = function ( word, form ) { - const grammarForms = mw.language.getData( 'hu', 'grammarForms' ); - if ( grammarForms && grammarForms[ form ] ) { - return grammarForms[ form ][ word ]; - } +mw.language.convertGrammarMapping.hu = function ( word, form ) { switch ( form ) { case 'rol': word += 'ról'; diff --git a/resources/src/mediawiki.language/languages/hy.js b/resources/src/mediawiki.language/languages/hy.js index 69dce79926a5..6f4bc136129e 100644 --- a/resources/src/mediawiki.language/languages/hy.js +++ b/resources/src/mediawiki.language/languages/hy.js @@ -2,12 +2,7 @@ * Armenian (Հայերեն) language functions */ -mw.language.convertGrammar = function ( word, form ) { - const grammarForms = mw.language.getData( 'hy', 'grammarForms' ); - if ( grammarForms && grammarForms[ form ] ) { - return grammarForms[ form ][ word ]; - } - +mw.language.convertGrammarMapping.hy = function ( word, form ) { // These rules are not perfect, but they are currently only used for site names so it doesn't // matter if they are wrong sometimes. Just add a special case for your site name if necessary. diff --git a/resources/src/mediawiki.language/languages/la.js b/resources/src/mediawiki.language/languages/la.js index cc7c95f0de6e..4fa08e00e3a1 100644 --- a/resources/src/mediawiki.language/languages/la.js +++ b/resources/src/mediawiki.language/languages/la.js @@ -3,11 +3,7 @@ * @author Santhosh Thottingal */ -mw.language.convertGrammar = function ( word, form ) { - const grammarForms = mw.language.getData( 'la', 'grammarForms' ); - if ( grammarForms && grammarForms[ form ] ) { - return grammarForms[ form ][ word ]; - } +mw.language.convertGrammarMapping.la = function ( word, form ) { switch ( form ) { case 'genitive': // only a few declensions, and even for those mostly the singular only diff --git a/resources/src/mediawiki.language/languages/os.js b/resources/src/mediawiki.language/languages/os.js index fb3a15a4322a..9e8859cada87 100644 --- a/resources/src/mediawiki.language/languages/os.js +++ b/resources/src/mediawiki.language/languages/os.js @@ -3,8 +3,7 @@ * @author Santhosh Thottingal */ -mw.language.convertGrammar = function ( word, form ) { - const grammarForms = mw.language.getData( 'os', 'grammarForms' ); +mw.language.convertGrammarMapping.os = function ( word, form ) { // Ending for allative case let endAllative = 'мæ', // Variable for 'j' beetwen vowels @@ -14,9 +13,6 @@ mw.language.convertGrammar = function ( word, form ) { // Variable for ending ending = ''; - if ( grammarForms && grammarForms[ form ] ) { - return grammarForms[ form ][ word ]; - } // Checking if the $word is in plural form if ( /тæ$/i.test( word ) ) { word = word.slice( 0, -1 ); diff --git a/resources/src/mediawiki.language/languages/sl.js b/resources/src/mediawiki.language/languages/sl.js index 7dc63dc663c4..fd2afd45c29f 100644 --- a/resources/src/mediawiki.language/languages/sl.js +++ b/resources/src/mediawiki.language/languages/sl.js @@ -2,11 +2,7 @@ * Slovenian (Slovenščina) language functions */ -mw.language.convertGrammar = function ( word, form ) { - const grammarForms = mw.language.getData( 'sl', 'grammarForms' ); - if ( grammarForms && grammarForms[ form ] ) { - return grammarForms[ form ][ word ]; - } +mw.language.convertGrammarMapping.sl = function ( word, form ) { switch ( form ) { case 'mestnik': // locative word = 'o ' + word; diff --git a/resources/src/mediawiki.language/mediawiki.language.js b/resources/src/mediawiki.language/mediawiki.language.js index 618fb29fc121..13bee34e5fed 100644 --- a/resources/src/mediawiki.language/mediawiki.language.js +++ b/resources/src/mediawiki.language/mediawiki.language.js @@ -72,6 +72,11 @@ }, /** + * Map of language-specific convertGrammar() implementations keyed by language code. + */ + convertGrammarMapping: {}, + + /** * Grammatical transformations, needed for inflected languages. * Invoked by putting `{{grammar:case|word}}` in a message. * @@ -85,11 +90,18 @@ convertGrammar: function ( word, form ) { const userLanguage = mw.config.get( 'wgUserLanguage' ); + // Word-specific casing rules have the highest precedence. const forms = mw.language.getData( userLanguage, 'grammarForms' ); - if ( forms && forms[ form ] ) { + if ( forms && forms[ form ] && forms[ form ][ word ] ) { return forms[ form ][ word ]; } + // If no word-specific casing rule exists, prefer to use a language-specific + // convertGrammar() implementation if one is available. + if ( Object.prototype.hasOwnProperty.call( mw.language.convertGrammarMapping, userLanguage ) ) { + return mw.language.convertGrammarMapping[ userLanguage ]( word, form ); + } + const transformations = mw.language.getData( userLanguage, 'grammarTransformations' ); if ( !( transformations && transformations[ form ] ) ) { diff --git a/tests/phpunit/includes/languages/LanguageBsTest.php b/tests/phpunit/includes/languages/LanguageBsTest.php index 8d46d33126ba..7333113f5de9 100644 --- a/tests/phpunit/includes/languages/LanguageBsTest.php +++ b/tests/phpunit/includes/languages/LanguageBsTest.php @@ -44,4 +44,17 @@ class LanguageBsTest extends LanguageClassesTestCase { [ 'other', 200 ], ]; } + + /** + * @dataProvider provideConvertGrammar + */ + public function testConvertGrammar( string $word, string $case, string $expected ): void { + $this->assertSame( $expected, $this->getLang()->convertGrammar( $word, $case ) ); + } + + public static function provideConvertGrammar(): iterable { + yield [ 'word', 'instrumental', 's word' ]; + yield [ 'word', 'lokativ', 'o word' ]; + yield [ 'word', 'nominativ', 'word' ]; + } } diff --git a/tests/phpunit/includes/languages/LanguageDsbTest.php b/tests/phpunit/includes/languages/LanguageDsbTest.php index 3087426767c9..7d0714988d5e 100644 --- a/tests/phpunit/includes/languages/LanguageDsbTest.php +++ b/tests/phpunit/includes/languages/LanguageDsbTest.php @@ -41,4 +41,17 @@ class LanguageDsbTest extends LanguageClassesTestCase { [ 'other', 555 ], ]; } + + /** + * @dataProvider provideConvertGrammar + */ + public function testConvertGrammar( string $word, string $case, string $expected ): void { + $this->assertSame( $expected, $this->getLang()->convertGrammar( $word, $case ) ); + } + + public static function provideConvertGrammar(): iterable { + yield [ 'word', 'nominatiw', 'word' ]; + yield [ 'word', 'instrumental', 'wo z word' ]; + yield [ 'word', 'lokatiw', 'wo word' ]; + } } diff --git a/tests/phpunit/includes/languages/LanguageFiTest.php b/tests/phpunit/includes/languages/LanguageFiTest.php new file mode 100644 index 000000000000..fb9efd76e493 --- /dev/null +++ b/tests/phpunit/includes/languages/LanguageFiTest.php @@ -0,0 +1,40 @@ +<?php +declare( strict_types=1 ); + +/** + * @group Language + * @covers LanguageFi + */ +class LanguageFiTest extends LanguageClassesTestCase { + /** + * @dataProvider provideConvertGrammar + */ + public function testConvertGrammar( string $word, string $case, string $expected ): void { + $this->assertSame( $expected, $this->getLang()->convertGrammar( $word, $case ) ); + } + + public static function provideConvertGrammar(): iterable { + $wordCaseMappings = [ + 'talo' => [ + 'genitive' => 'talon', + 'elative' => 'talosta', + 'partitive' => 'taloa', + 'illative' => 'taloon', + 'inessive' => 'talossa', + ], + 'pastöroitu' => [ + 'partitive' => 'pastöroitua', + ], + 'Wikipedia' => [ + 'elative' => 'Wikipediasta', + 'partitive' => 'Wikipediaa', + ], + ]; + + foreach ( $wordCaseMappings as $word => $caseMappings ) { + foreach ( $caseMappings as $case => $expected ) { + yield "$word $case" => [ (string)$word, $case, $expected ]; + } + } + } +} diff --git a/tests/phpunit/includes/languages/LanguageGaTest.php b/tests/phpunit/includes/languages/LanguageGaTest.php index fc6a5ac3a236..08cd052e072f 100644 --- a/tests/phpunit/includes/languages/LanguageGaTest.php +++ b/tests/phpunit/includes/languages/LanguageGaTest.php @@ -37,4 +37,23 @@ class LanguageGaTest extends LanguageClassesTestCase { [ 'other', 200 ], ]; } + + /** + * @dataProvider provideConvertGrammar + */ + public function testConvertGrammar( string $word, string $case, string $expected ): void { + $this->assertSame( $expected, $this->getLang()->convertGrammar( $word, $case ) ); + } + + public static function provideConvertGrammar(): iterable { + yield [ 'an Domhnach', 'ainmlae', 'Dé Domhnaigh' ]; + yield [ 'an Luan', 'ainmlae', 'Dé Luain' ]; + yield [ 'an Mháirt', 'ainmlae', 'Dé Mháirt' ]; + yield [ 'an Chéadaoin', 'ainmlae', 'Dé Chéadaoin' ]; + yield [ 'an Déardaoin', 'ainmlae', 'Déardaoin' ]; + yield [ 'an Aoine', 'ainmlae', 'Dé hAoine' ]; + yield [ 'an Satharn', 'ainmlae', 'Dé Sathairn' ]; + + yield [ 'an Domhnach', 'other', 'an Domhnach' ]; + } } diff --git a/tests/phpunit/includes/languages/LanguageHsbTest.php b/tests/phpunit/includes/languages/LanguageHsbTest.php index c90983daab17..728496438b8e 100644 --- a/tests/phpunit/includes/languages/LanguageHsbTest.php +++ b/tests/phpunit/includes/languages/LanguageHsbTest.php @@ -41,4 +41,17 @@ class LanguageHsbTest extends LanguageClassesTestCase { [ 'other', 555 ], ]; } + + /** + * @dataProvider provideConvertGrammar + */ + public function testConvertGrammar( string $word, string $case, string $expected ): void { + $this->assertSame( $expected, $this->getLang()->convertGrammar( $word, $case ) ); + } + + public static function provideConvertGrammar(): iterable { + yield [ 'word', 'nominatiw', 'word' ]; + yield [ 'word', 'instrumental', 'z word' ]; + yield [ 'word', 'lokatiw', 'wo word' ]; + } } diff --git a/tests/phpunit/includes/languages/LanguageHuTest.php b/tests/phpunit/includes/languages/LanguageHuTest.php index a8b0a578840e..445d164a65f5 100644 --- a/tests/phpunit/includes/languages/LanguageHuTest.php +++ b/tests/phpunit/includes/languages/LanguageHuTest.php @@ -35,4 +35,29 @@ class LanguageHuTest extends LanguageClassesTestCase { [ 'other', 200 ], ]; } + + /** + * @dataProvider provideConvertGrammar + */ + public function testConvertGrammar( string $word, string $case, string $expected ): void { + $this->assertSame( $expected, $this->getLang()->convertGrammar( $word, $case ) ); + } + + public static function provideConvertGrammar(): iterable { + $wordCaseMappings = [ + 'kocsmafal' => [ + 'rol' => 'kocsmafalról', + 'ba' => 'kocsmafalba', + ], + 'Bevezető' => [ + 'k' => 'Bevezetők', + ], + ]; + + foreach ( $wordCaseMappings as $word => $caseMappings ) { + foreach ( $caseMappings as $case => $expected ) { + yield "$word $case" => [ (string)$word, $case, $expected ]; + } + } + } } diff --git a/tests/phpunit/includes/languages/LanguageHyTest.php b/tests/phpunit/includes/languages/LanguageHyTest.php index 01e8be7a17e0..44548b69cd93 100644 --- a/tests/phpunit/includes/languages/LanguageHyTest.php +++ b/tests/phpunit/includes/languages/LanguageHyTest.php @@ -37,4 +37,20 @@ class LanguageHyTest extends LanguageClassesTestCase { [ 'other', 200 ], ]; } + + /** + * @dataProvider provideConvertGrammar + */ + public function testConvertGrammar( string $word, string $case, string $expected ): void { + $this->assertSame( $expected, $this->getLang()->convertGrammar( $word, $case ) ); + } + + public static function provideConvertGrammar(): iterable { + yield [ 'Մաունա', 'genitive', 'Մաունայի' ]; + yield [ 'հետո', 'genitive', 'հետոյի' ]; + yield [ 'գիրք', 'genitive', 'գրքի' ]; + yield [ 'ժամանակի', 'genitive', 'ժամանակիի' ]; + + yield [ 'Մաունա', 'dative', 'Մաունա' ]; + } } diff --git a/tests/phpunit/includes/languages/LanguageLaTest.php b/tests/phpunit/includes/languages/LanguageLaTest.php new file mode 100644 index 000000000000..6b8f895c1664 --- /dev/null +++ b/tests/phpunit/includes/languages/LanguageLaTest.php @@ -0,0 +1,71 @@ +<?php +declare( strict_types=1 ); + +/** + * @group Language + * @covers LanguageLa + */ +class LanguageLaTest extends LanguageClassesTestCase { + /** + * @dataProvider provideConvertGrammar + */ + public function testConvertGrammar( string $word, string $case, string $expected ): void { + $this->assertSame( $expected, $this->getLang()->convertGrammar( $word, $case ) ); + } + + public static function provideConvertGrammar(): iterable { + $wordCaseMappings = [ + 'translatio' => [ + 'genitive' => 'translationis', + 'accusative' => 'translationem', + 'ablative' => 'translatione', + ], + 'ursus' => [ + 'genitive' => 'ursi', + 'accusative' => 'ursum', + 'ablative' => 'urso', + ], + 'gens' => [ + 'genitive' => 'gentis', + 'accusative' => 'gentem', + 'ablative' => 'gente', + ], + 'bellum' => [ + 'genitive' => 'belli', + 'accusative' => 'bellum', + 'ablative' => 'bello', + ], + 'communia' => [ + 'genitive' => 'communium', + 'accusative' => 'communia', + 'ablative' => 'communibus', + ], + 'libri' => [ + 'genitive' => 'librorum', + 'accusative' => 'libros', + 'ablative' => 'libris', + ], + 'dies' => [ + 'genitive' => 'diei', + 'accusative' => 'diem', + 'ablative' => 'die', + ], + 'declinatio' => [ + 'genitive' => 'declinationis', + 'accusative' => 'declinationem', + 'ablative' => 'declinatione', + ], + 'vanitas' => [ + 'genitive' => 'vanitatis', + 'accusative' => 'vanitatem', + 'ablative' => 'vanitate', + ], + ]; + + foreach ( $wordCaseMappings as $word => $caseMappings ) { + foreach ( $caseMappings as $case => $expected ) { + yield "$word $case" => [ (string)$word, $case, $expected ]; + } + } + } +} diff --git a/tests/phpunit/includes/languages/LanguageOsTest.php b/tests/phpunit/includes/languages/LanguageOsTest.php new file mode 100644 index 000000000000..dc4bb44c8309 --- /dev/null +++ b/tests/phpunit/includes/languages/LanguageOsTest.php @@ -0,0 +1,47 @@ +<?php +declare( strict_types=1 ); + +/** + * @group Language + * @covers LanguageOs + */ +class LanguageOsTest extends LanguageClassesTestCase { + /** + * @dataProvider provideConvertGrammar + */ + public function testConvertGrammar( string $word, string $case, string $expected ): void { + $this->assertSame( $expected, $this->getLang()->convertGrammar( $word, $case ) ); + } + + public static function provideConvertGrammar(): iterable { + $wordCaseMappings = [ + 'бæстæ' => [ + 'genitive' => 'бæсты', + 'allative' => 'бæстæм', + 'dative' => 'бæстæн', + 'ablative' => 'бæстæй', + 'inessive' => 'бæст', + 'superessive' => 'бæстыл', + ], + + 'лæппу' => [ + 'genitive' => 'лæппуйы', + 'allative' => 'лæппумæ', + 'dative' => 'лæппуйæн', + 'ablative' => 'лæппуйæ', + 'inessive' => 'лæппу', + 'superessive' => 'лæппуйыл', + ], + + '2011' => [ + 'equative' => '2011-ау', + ], + ]; + + foreach ( $wordCaseMappings as $word => $caseMappings ) { + foreach ( $caseMappings as $case => $expected ) { + yield "$word $case" => [ (string)$word, $case, $expected ]; + } + } + } +} diff --git a/tests/phpunit/includes/languages/LanguageSlTest.php b/tests/phpunit/includes/languages/LanguageSlTest.php index ab5d0efa7afd..f99445a7db85 100644 --- a/tests/phpunit/includes/languages/LanguageSlTest.php +++ b/tests/phpunit/includes/languages/LanguageSlTest.php @@ -43,4 +43,17 @@ class LanguageSlTest extends LanguageClassesTestCase { [ 'one', 201 ], ]; } + + /** + * @dataProvider provideConvertGrammar + */ + public function testConvertGrammar( string $word, string $case, string $expected ): void { + $this->assertSame( $expected, $this->getLang()->convertGrammar( $word, $case ) ); + } + + public static function provideConvertGrammar(): iterable { + yield [ 'word', 'mestnik', 'o word' ]; + yield [ 'word', 'orodnik', 'z word' ]; + yield [ 'word', 'imenovalnik', 'word' ]; + } } diff --git a/tests/qunit/QUnitTestResources.php b/tests/qunit/QUnitTestResources.php index 2ca510a2f117..65cad8e887d7 100644 --- a/tests/qunit/QUnitTestResources.php +++ b/tests/qunit/QUnitTestResources.php @@ -1,5 +1,7 @@ <?php // These modules are only registered when $wgEnableJavaScriptTest is true +use MediaWiki\Html\Html; +use MediaWiki\MediaWikiServices; use MediaWiki\ResourceLoader\FilePath; return [ @@ -21,23 +23,71 @@ return [ ], ], + // Test module exposing language-specific rules for mediawiki.language.test.js. 'mediawiki.language.testdata' => [ - 'localBasePath' => "{$GLOBALS['IP']}/resources/src/mediawiki.language/languages", - 'remoteBasePath' => "{$GLOBALS['wgResourceBasePath']}/resources/src/mediawiki.language/languages", + 'packageFiles' => [ + [ + 'name' => 'mediawiki.language.testdata.js', + 'callback' => static function () { + // Optimization: Only compute and load data for languages that we have tests for. + $langCodes = [ + 'bs', + 'he', + 'hsb', + 'dsb', + 'hy', + 'fi', + 'ka', + 'ru', + 'hu', + 'ga', + 'mn', + 'uk', + 'sl', + 'os', + 'la', + ]; + + $languageFactory = MediaWikiServices::getInstance()->getLanguageFactory(); + + $data = []; + + foreach ( $langCodes as $langCode ) { + $data[$langCode] = $languageFactory->getLanguage( $langCode )->getJsData(); + } + + return 'module.exports = ' . Html::encodeJsVar( $data ) . ';'; + }, + ] + ], + ], + + 'mediawiki.language.jqueryMsg.testdata' => [ + 'localBasePath' => __DIR__ . '/data', 'packageFiles' => [ new FilePath( 'mediawiki.jqueryMsg.testdata.js', __DIR__ . '/data' ), new FilePath( 'mediawiki.jqueryMsg.data.json', __DIR__ . '/data' ), - 'bs.js', - 'dsb.js', - 'fi.js', - 'ga.js', - 'hsb.js', - 'hu.js', - 'hy.js', - 'la.js', - 'os.js', - 'sl.js', - ] + ], + ], + + // Test module loading all language-specific convertGrammar() implementations. + 'mediawiki.language.grammar.testdata' => [ + 'localBasePath' => "{$GLOBALS['IP']}/resources/src/mediawiki.language/languages", + // Automatically discover and load every language-specific convertGrammar() implementation. + 'scripts' => ( static function () { + $basePath = "{$GLOBALS['IP']}/resources/src/mediawiki.language/languages"; + $scripts = []; + + foreach ( new DirectoryIterator( $basePath ) as $file ) { + /** @var DirectoryIterator $file */ + if ( $file->isFile() && $file->getExtension() === 'js' ) { + $scripts[] = $file->getBasename(); + } + } + + return $scripts; + } )(), + 'dependencies' => [ 'mediawiki.language' ], ], 'test.MediaWiki' => [ @@ -133,6 +183,8 @@ return [ 'mediawiki.rcfilters.filters.ui', 'mediawiki.language', 'mediawiki.language.testdata', + 'mediawiki.language.grammar.testdata', + 'mediawiki.language.jqueryMsg.testdata', 'mediawiki.cldr', 'mediawiki.cookie', 'mediawiki.deflate', diff --git a/tests/qunit/resources/mediawiki.jqueryMsg.test.js b/tests/qunit/resources/mediawiki.jqueryMsg.test.js index 5f1a75b530d4..1423d4a1ad9c 100644 --- a/tests/qunit/resources/mediawiki.jqueryMsg.test.js +++ b/tests/qunit/resources/mediawiki.jqueryMsg.test.js @@ -4,7 +4,7 @@ /* eslint-disable camelcase */ let formatText, formatParse, specialCharactersPageName, expectedListUsers, expectedListUsersSitename, expectedLinkPagenamee, expectedEntrypoints; - const testData = require( 'mediawiki.language.testdata' ), + const testData = require( 'mediawiki.language.jqueryMsg.testdata' ), phpParserData = testData.phpParserData; // When the expected result is the same in both modes diff --git a/tests/qunit/resources/mediawiki.language.test.js b/tests/qunit/resources/mediawiki.language.test.js index de9e2d285263..bdeb0c4a9dd8 100644 --- a/tests/qunit/resources/mediawiki.language.test.js +++ b/tests/qunit/resources/mediawiki.language.test.js @@ -3,11 +3,13 @@ QUnit.module( 'mediawiki.language', QUnit.newMwEnvironment( { beforeEach: function () { + this.userLang = mw.config.get( 'wgUserLanguage' ); this.liveLangData = mw.language.data; mw.language.data = {}; }, afterEach: function () { mw.language.data = this.liveLangData; + mw.config.set( 'wgUserLanguage', this.userLang ); }, messages: { // mw.language.listToText test @@ -77,21 +79,6 @@ assert.strictEqual( mw.language.convertNumber( '1.200', true ), 1200, 'unformat plain (digit transform disabled)' ); } ); - function grammarTest( langCode, test ) { - // The test works only if the content language is opt.language - // because it requires [lang].js to be loaded. - QUnit.test( 'Grammar test for lang=' + langCode, ( assert ) => { - let i; - for ( i = 0; i < test.length; i++ ) { - assert.strictEqual( - mw.language.convertGrammar( test[ i ].word, test[ i ].grammarForm ), - test[ i ].expected, - test[ i ].description - ); - } - } ); - } - // These tests run only for the current UI language. const grammarTests = { bs: [ @@ -449,7 +436,7 @@ { word: 'Викитека', grammarForm: 'accusative', - expected: 'Викитека', + expected: 'Викитеку', description: 'Grammar test for accusative case, Викитека -> Викитеку' }, { @@ -644,7 +631,7 @@ { word: 'Викитолийн', grammarForm: 'genitive', - expected: 'Викитоль', + expected: 'Викитолийн', description: 'Grammar test for genitive case' } ], @@ -758,11 +745,31 @@ ] }; - // eslint-disable-next-line no-jquery/no-each-util - $.each( grammarTests, ( langCode, test ) => { - if ( langCode === mw.config.get( 'wgUserLanguage' ) ) { - grammarTest( langCode, test ); - } + const languageTestData = require( 'mediawiki.language.testdata' ); + + Object.keys( grammarTests ).forEach( ( langCode ) => { + QUnit.test( `Language data for lang=${ langCode }`, ( assert ) => { + assert.strictEqual( + Object.prototype.hasOwnProperty.call( languageTestData, langCode ), + true, + `Missing test data for lang=${ langCode }.` + + 'Update the definition of the "mediawiki.language.testdata" module.' + ); + } ); + + QUnit.test( `Grammar test for lang=${ langCode }`, ( assert ) => { + mw.config.set( 'wgUserLanguage', langCode ); + mw.language.setData( langCode, languageTestData[ langCode ] ); + + const test = grammarTests[ langCode ]; + for ( let i = 0; i < test.length; i++ ) { + assert.strictEqual( + mw.language.convertGrammar( test[ i ].word, test[ i ].grammarForm ), + test[ i ].expected, + test[ i ].description + ); + } + } ); } ); QUnit.test( 'List to text test', ( assert ) => { |