Merge changes I1aa3b081,I65ee788c,I58a9cdfe
authorNikerabbit <niklas.laxstrom@gmail.com>
Thu, 23 Aug 2012 18:40:31 +0000 (18:40 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 23 Aug 2012 18:40:31 +0000 (18:40 +0000)
* changes:
  Adding JavaScript CLDR plural parser.
  CLDR plural parser in PHP
  CLDR Plural rules based plural form calculation

31 files changed:
includes/AutoLoader.php
includes/LocalisationCache.php
includes/resourceloader/ResourceLoaderLanguageDataModule.php
languages/Language.php
languages/classes/LanguageAm.php [deleted file]
languages/classes/LanguageAr.php
languages/classes/LanguageBe.php [deleted file]
languages/classes/LanguageBh.php [deleted file]
languages/classes/LanguageBs.php
languages/classes/LanguageCs.php [deleted file]
languages/classes/LanguageCu.php
languages/classes/LanguageCy.php [deleted file]
languages/classes/LanguageDsb.php
languages/classes/LanguageFr.php [deleted file]
languages/classes/LanguageGa.php
languages/classes/LanguageGd.php [deleted file]
languages/classes/LanguageHe.php
languages/data/plurals-mediawiki.xml [new file with mode: 0644]
languages/data/plurals.xml [new file with mode: 0644]
languages/utils/CLDRPluralRuleEvaluator.php [new file with mode: 0644]
resources/Resources.php
resources/mediawiki.language/languages/ar.js [deleted file]
resources/mediawiki.language/languages/he.js
resources/mediawiki.language/languages/hi.js [deleted file]
resources/mediawiki.language/mediawiki.cldr.js [new file with mode: 0644]
resources/mediawiki.language/mediawiki.language.js
resources/mediawiki.libs/CLDRPluralRuleParser.js [new file with mode: 0644]
tests/phpunit/languages/LanguageHeTest.php
tests/phpunit/languages/utils/CLDRPluralRuleEvaluatorTest.php [new file with mode: 0644]
tests/qunit/QUnitTestResources.php
tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js [new file with mode: 0644]

index 1b46c6a..b299796 100644 (file)
@@ -1007,6 +1007,7 @@ $wgAutoloadLocalClasses = array(
        'FakeConverter' => 'languages/Language.php',
        'Language' => 'languages/Language.php',
        'LanguageConverter' => 'languages/LanguageConverter.php',
+       'CLDRPluralRuleEvaluator' => 'languages/utils/CLDRPluralRuleEvaluator.php',
 
        # maintenance
        'ConvertLinks' => 'maintenance/convertLinks.php',
index 9ce26d0..c1ac848 100644 (file)
@@ -110,7 +110,7 @@ class LocalisationCache {
                'dateFormats', 'datePreferences', 'datePreferenceMigrationMap',
                'defaultDateFormat', 'extraUserToggles', 'specialPageAliases',
                'imageFiles', 'preloadedMessages', 'namespaceGenderAliases',
-               'digitGroupingPattern'
+               'digitGroupingPattern', 'pluralRules'
        );
 
        /**
@@ -118,7 +118,7 @@ class LocalisationCache {
         * by a fallback sequence.
         */
        static public $mergeableMapKeys = array( 'messages', 'namespaceNames',
-               'dateFormats', 'imageFiles', 'preloadedMessages',
+               'dateFormats', 'imageFiles', 'preloadedMessages', 'pluralRules'
        );
 
        /**
@@ -154,6 +154,12 @@ class LocalisationCache {
         */
        static public $preloadedKeys = array( 'dateFormats', 'namespaceNames' );
 
+       /**
+        * Associative array of cached plural rules. The key is the language code,
+        * the value is an array of plural rules for that language.
+        */
+       var $pluralRules = null;
+
        var $mergeableKeys = null;
 
        /**
@@ -234,9 +240,9 @@ class LocalisationCache {
         */
        public function getItem( $code, $key ) {
                if ( !isset( $this->loadedItems[$code][$key] ) ) {
-                       wfProfileIn( __METHOD__.'-load' );
+                       wfProfileIn( __METHOD__ . '-load' );
                        $this->loadItem( $code, $key );
-                       wfProfileOut( __METHOD__.'-load' );
+                       wfProfileOut( __METHOD__ . '-load' );
                }
 
                if ( $key === 'fallback' && isset( $this->shallowFallbacks[$code] ) ) {
@@ -256,9 +262,9 @@ class LocalisationCache {
        public function getSubitem( $code, $key, $subkey ) {
                if ( !isset( $this->loadedSubitems[$code][$key][$subkey] ) &&
                         !isset( $this->loadedItems[$code][$key] ) ) {
-                       wfProfileIn( __METHOD__.'-load' );
+                       wfProfileIn( __METHOD__ . '-load' );
                        $this->loadSubitem( $code, $key, $subkey );
-                       wfProfileOut( __METHOD__.'-load' );
+                       wfProfileOut( __METHOD__ . '-load' );
                }
 
                if ( isset( $this->data[$code][$key][$subkey] ) ) {
@@ -367,7 +373,7 @@ class LocalisationCache {
         */
        public function isExpired( $code ) {
                if ( $this->forceRecache && !isset( $this->recachedLangs[$code] ) ) {
-                       wfDebug( __METHOD__."($code): forced reload\n" );
+                       wfDebug( __METHOD__ . "($code): forced reload\n" );
                        return true;
                }
 
@@ -376,7 +382,7 @@ class LocalisationCache {
                $preload = $this->store->get( $code, 'preload' );
                // Different keys may expire separately, at least in LCStore_Accel
                if ( $deps === null || $keys === null || $preload === null ) {
-                       wfDebug( __METHOD__."($code): cache missing, need to make one\n" );
+                       wfDebug( __METHOD__ . "($code): cache missing, need to make one\n" );
                        return true;
                }
 
@@ -386,7 +392,7 @@ class LocalisationCache {
                        // anymore (e.g. uninstalled extensions)
                        // When this happens, always expire the cache
                        if ( !$dep instanceof CacheDependency || $dep->isExpired() ) {
-                               wfDebug( __METHOD__."($code): cache for $code expired due to " .
+                               wfDebug( __METHOD__ . "($code): cache for $code expired due to " .
                                        get_class( $dep ) . "\n" );
                                return true;
                        }
@@ -481,12 +487,70 @@ class LocalisationCache {
                } elseif ( $_fileType == 'aliases' ) {
                        $data = compact( 'aliases' );
                } else {
-                       throw new MWException( __METHOD__.": Invalid file type: $_fileType" );
+                       throw new MWException( __METHOD__ . ": Invalid file type: $_fileType" );
                }
-
                return $data;
        }
 
+       /**
+        * Get the compiled plural rules for a given language from the XML files.
+        * @since 1.20
+        */
+       public function getCompiledPluralRules( $code ) {
+               $rules = $this->getPluralRules( $code );
+               try {
+                       $compiledRules = CLDRPluralRuleEvaluator::compile( $rules );
+               } catch( CLDRPluralRuleError $e ) {
+                       wfDebugLog( 'l10n', $e->getMessage() . "\n" );
+                       return array();
+               }
+               return $compiledRules;
+       }
+
+       /**
+        * Get the plural rules for a given language from the XML files.
+        * Cached.
+        * @since 1.20
+        */
+       public function getPluralRules( $code ) {
+               if ( $this->pluralRules === null ) {
+                       $cldrPlural = __DIR__ . "/../languages/data/plurals.xml";
+                       $mwPlural = __DIR__ . "/../languages/data/plurals-mediawiki.xml";
+                       // Load CLDR plural rules
+                       $this->loadPluralFile( $cldrPlural );
+                       if ( file_exists( $mwPlural ) ) {
+                               // Override or extend
+                               $this->loadPluralFile( $mwPlural );
+                       }
+               }
+               if ( !isset( $this->pluralRules[$code] ) ) {
+                       return array();
+               } else {
+                       return $this->pluralRules[$code];
+               }
+       }
+
+       /**
+        * Load a plural XML file with the given filename, compile the relevant
+        * rules, and save the compiled rules in a process-local cache.
+        */
+       private function loadPluralFile( $fileName ) {
+               $doc = new DOMDocument;
+               $doc->load( $fileName );
+               $rulesets = $doc->getElementsByTagName( "pluralRules" );
+               foreach ( $rulesets as $ruleset ) {
+                       $codes = $ruleset->getAttribute( 'locales' );
+                       $rules = array();
+                       $ruleElements = $ruleset->getElementsByTagName( "pluralRule" );
+                       foreach ( $ruleElements as $elt ) {
+                               $rules[] = $elt->nodeValue;
+                       }
+                       foreach ( explode( ' ', $codes ) as $code ) {
+                               $this->pluralRules[$code] = $rules;
+                       }
+               }
+       }
+
        /**
         * Merge two localisation values, a primary and a fallback, overwriting the
         * primary value in place.
@@ -587,12 +651,12 @@ class LocalisationCache {
                # Load the primary localisation from the source file
                $fileName = Language::getMessagesFileName( $code );
                if ( !file_exists( $fileName ) ) {
-                       wfDebug( __METHOD__.": no localisation file for $code, using fallback to en\n" );
+                       wfDebug( __METHOD__ . ": no localisation file for $code, using fallback to en\n" );
                        $coreData['fallback'] = 'en';
                } else {
                        $deps[] = new FileDependency( $fileName );
                        $data = $this->readPHPFile( $fileName, 'core' );
-                       wfDebug( __METHOD__.": got localisation for $code from source\n" );
+                       wfDebug( __METHOD__ . ": got localisation for $code from source\n" );
 
                        # Merge primary localisation
                        foreach ( $data as $key => $value ) {
@@ -605,7 +669,6 @@ class LocalisationCache {
                if ( is_null( $coreData['fallback'] ) ) {
                        $coreData['fallback'] = $code === 'en' ? false : 'en';
                }
-
                if ( $coreData['fallback'] === false ) {
                        $coreData['fallbackSequence'] = array();
                } else {
@@ -654,7 +717,7 @@ class LocalisationCache {
                        $used = false;
 
                        foreach ( $data as $key => $item ) {
-                               if( $this->mergeExtensionItem( $codeSequence, $key, $allData[$key], $item ) ) {
+                               if ( $this->mergeExtensionItem( $codeSequence, $key, $allData[$key], $item ) ) {
                                        $used = true;
                                }
                        }
@@ -684,19 +747,22 @@ class LocalisationCache {
                        $page = str_replace( ' ', '_', $page );
                }
                # Decouple the reference to prevent accidental damage
-               unset($page);
+               unset( $page );
 
                # Set the list keys
                $allData['list'] = array();
                foreach ( self::$splitKeys as $key ) {
                        $allData['list'][$key] = array_keys( $allData[$key] );
                }
-
+               # Load CLDR plural rules for JavaScript
+               $allData['pluralRules'] = $this->getPluralRules( $code );
+               # And for PHP
+               $allData['compiledPluralRules'] = $this->getCompiledPluralRules( $code );
                # Run hooks
                wfRunHooks( 'LocalisationCacheRecache', array( $this, $code, &$allData ) );
 
                if ( is_null( $allData['namespaceNames'] ) ) {
-                       throw new MWException( __METHOD__.': Localisation data failed sanity check! ' .
+                       throw new MWException( __METHOD__ . ': Localisation data failed sanity check! ' .
                                'Check that your languages/messages/MessagesEn.php file is intact.' );
                }
 
@@ -924,7 +990,7 @@ class LCStore_DB implements LCStore {
                }
 
                if ( !$code ) {
-                       throw new MWException( __METHOD__.": Invalid language \"$code\"" );
+                       throw new MWException( __METHOD__ . ": Invalid language \"$code\"" );
                }
 
                $this->dbw = wfGetDB( DB_MASTER );
@@ -968,7 +1034,7 @@ class LCStore_DB implements LCStore {
                }
 
                if ( is_null( $this->currentLang ) ) {
-                       throw new MWException( __CLASS__.': must call startWrite() before calling set()' );
+                       throw new MWException( __CLASS__ . ': must call startWrite() before calling set()' );
                }
 
                $this->batch[] = array(
@@ -1040,7 +1106,7 @@ class LCStore_CDB implements LCStore {
                }
 
                // Close reader to stop permission errors on write
-               if( !empty($this->readers[$code]) ) {
+               if ( !empty( $this->readers[$code] ) ) {
                        $this->readers[$code]->close();
                }
 
@@ -1058,14 +1124,14 @@ class LCStore_CDB implements LCStore {
 
        public function set( $key, $value ) {
                if ( is_null( $this->writer ) ) {
-                       throw new MWException( __CLASS__.': must call startWrite() before calling set()' );
+                       throw new MWException( __CLASS__ . ': must call startWrite() before calling set()' );
                }
                $this->writer->set( $key, serialize( $value ) );
        }
 
        protected function getFileName( $code ) {
                if ( !$code || strpos( $code, '/' ) !== false ) {
-                       throw new MWException( __METHOD__.": Invalid language \"$code\"" );
+                       throw new MWException( __METHOD__ . ": Invalid language \"$code\"" );
                }
                return "{$this->directory}/l10n_cache-$code.cdb";
        }
@@ -1181,8 +1247,9 @@ class LocalisationCache_BulkLoad extends LocalisationCache {
                while ( count( $this->data ) > $this->maxLoadedLangs && count( $this->mruLangs ) ) {
                        reset( $this->mruLangs );
                        $code = key( $this->mruLangs );
-                       wfDebug( __METHOD__.": unloading $code\n" );
+                       wfDebug( __METHOD__ . ": unloading $code\n" );
                        $this->unload( $code );
                }
        }
+
 }
index a36aaec..c916c4a 100644 (file)
@@ -29,7 +29,7 @@ class ResourceLoaderLanguageDataModule extends ResourceLoaderModule {
 
        protected $language;
        /**
-        * Get the grammer forms for the site content language.
+        * Get the grammar forms for the site content language.
         *
         * @return array
         */
@@ -37,6 +37,15 @@ class ResourceLoaderLanguageDataModule extends ResourceLoaderModule {
                return $this->language->getGrammarForms();
        }
 
+       /**
+        * Get the plural forms for the site content language.
+        *
+        * @return array
+        */
+       protected function getPluralRules() {
+               return $this->language->getPluralRules();
+       }
+
        /**
         * Get the digit transform table for the content language
         * Seperator transform table also required here to convert
@@ -61,17 +70,19 @@ class ResourceLoaderLanguageDataModule extends ResourceLoaderModule {
         * @return array
         */
        protected function getData() {
-               return array( 'grammarForms' => $this->getSiteLangGrammarForms(),
-                               'digitTransformTable' => $this->getDigitTransformTable()
-                       );
+               return array(
+                       'digitTransformTable' => $this->getDigitTransformTable(),
+                       'grammarForms' => $this->getSiteLangGrammarForms(),
+                       'pluralRules' => $this->getPluralRules(),
+               );
        }
 
        /**
         * @param $context ResourceLoaderContext
-        * @return string: Javascript code
+        * @return string: JavaScript code
         */
        public function getScript( ResourceLoaderContext $context ) {
-               $this->language = Language::factory( $context ->getLanguage() );
+               $this->language = Language::factory( $context->getLanguage() );
                return Xml::encodeJsCall( 'mw.language.setData', array(
                        $this->language->getCode(),
                        $this->getData()
index ae66d7c..69a72eb 100644 (file)
@@ -266,9 +266,9 @@ class Language {
         */
        public static function isValidBuiltInCode( $code ) {
 
-               if( !is_string($code) ) {
+               if ( !is_string( $code ) ) {
                        $type = gettype( $code );
-                       if( $type === 'object' ) {
+                       if ( $type === 'object' ) {
                                $addmsg = " of class " . get_class( $code );
                        } else {
                                $addmsg = '';
@@ -742,7 +742,7 @@ class Language {
 
                $names = array();
 
-               if( $inLanguage ) {
+               if ( $inLanguage ) {
                        # TODO: also include when $inLanguage is null, when this code is more efficient
                        wfRunHooks( 'LanguageGetTranslatedLanguageNames', array( &$names, $inLanguage ) );
                }
@@ -762,11 +762,11 @@ class Language {
 
                $returnMw = array();
                $coreCodes = array_keys( $mwNames );
-               foreach( $coreCodes as $coreCode ) {
+               foreach ( $coreCodes as $coreCode ) {
                        $returnMw[$coreCode] = $names[$coreCode];
                }
 
-               if( $include === 'mwfile' ) {
+               if ( $include === 'mwfile' ) {
                        $namesMwFile = array();
                        # We do this using a foreach over the codes instead of a directory
                        # loop so that messages files in extensions will work correctly.
@@ -3409,9 +3409,9 @@ class Language {
                if ( !count( $forms ) ) {
                        return '';
                }
-               $forms = $this->preConvertPlural( $forms, 2 );
-
-               return ( $count == 1 ) ? $forms[0] : $forms[1];
+               $pluralForm = $this->getPluralForm( $count );
+               $pluralForm = min( $pluralForm, count( $forms ) - 1 );
+               return $forms[$pluralForm];
        }
 
        /**
@@ -4180,4 +4180,34 @@ class Language {
        public function getConvRuleTitle() {
                return $this->mConverter->getConvRuleTitle();
        }
+
+       /**
+        * Get the compiled plural rules for the language
+        * @since 1.20
+        * @return array Associative array with plural form, and plural rule as key-value pairs
+        */
+       public function getCompiledPluralRules() {
+               return self::$dataCache->getItem( strtolower( $this->mCode ), 'compiledPluralRules' );
+       }
+
+       /**
+        * Get the plural rules for the language
+        * @since 1.20
+        * @return array Associative array with plural form, and plural rule as key-value pairs
+        */
+       public function getPluralRules() {
+               return self::$dataCache->getItem( strtolower( $this->mCode ), 'pluralRules' );
+       }
+
+       /**
+        * Find the plural form matching to the given number
+        * It return the form index.
+        * @return int The index of the plural form
+        */
+       private function getPluralForm( $number ) {
+               $pluralRules = $this->getCompiledPluralRules();
+               $form = CLDRPluralRuleEvaluator::evaluateCompiled( $number, $pluralRules );
+               return $form;
+       }
+
 }
diff --git a/languages/classes/LanguageAm.php b/languages/classes/LanguageAm.php
deleted file mode 100644 (file)
index 4c39c26..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
-/**
- * Amharic (አማርኛ) specific code.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Language
- */
-
-/**
- * Amharic (አማርኛ)
- *
- * @ingroup Language
- */
-class LanguageAm extends Language {
-       /**
-        * Use singular form for zero
-        *
-        * @param $count int
-        * @param $forms array
-        *
-        * @return string
-        */
-       function convertPlural( $count, $forms ) {
-               if ( !count( $forms ) ) { return ''; }
-               $forms = $this->preConvertPlural( $forms, 2 );
-
-               return ( $count <= 1 ) ? $forms[0] : $forms[1];
-       }
-}
index cc6b85c..553ff07 100644 (file)
  */
 class LanguageAr extends Language {
 
-       /**
-        * @param $count int
-        * @param $forms array
-        * @return string
-        */
-       function convertPlural( $count, $forms ) {
-               if ( !count( $forms ) ) { return ''; }
-               $forms = $this->preConvertPlural( $forms, 6 );
-
-               if ( $count == 0 ) {
-                       $index = 0;
-               } elseif ( $count == 1 ) {
-                       $index = 1;
-               } elseif ( $count == 2 ) {
-                       $index = 2;
-               } elseif ( $count % 100 >= 3 && $count % 100 <= 10 ) {
-                       $index = 3;
-               } elseif ( $count % 100 >= 11 && $count % 100 <= 99 ) {
-                       $index = 4;
-               } else {
-                       $index = 5;
-               }
-               return $forms[$index];
-       }
-
        /**
         * Temporary hack for bug 9413: replace Arabic presentation forms with their
         * standard equivalents.
diff --git a/languages/classes/LanguageBe.php b/languages/classes/LanguageBe.php
deleted file mode 100644 (file)
index b5b5966..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-<?php
-/**
- * Belarusian normative (Беларуская мова) specific code.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @author Ævar Arnfjörð Bjarmason <avarab@gmail.com>
- * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License
- * @license http://www.gnu.org/copyleft/fdl.html GNU Free Documentation License
- * @ingroup Language
- */
-
-/**
- * Belarusian normative (Беларуская мова)
- *
- * This is still the version from Be-x-old, only duplicated for consistency of
- * plural and grammar functions. If there are errors please send a patch.
- *
- * @ingroup Language
- * @see http://be.wikipedia.org/wiki/Talk:LanguageBe.php
- */
-class LanguageBe extends Language {
-
-       /**
-        * @param $count int
-        * @param $forms array
-        *
-        * @return string
-        */
-       function convertPlural( $count, $forms ) {
-               if ( !count( $forms ) ) { return ''; }
-               // @todo FIXME: CLDR defines 4 plural forms instead of 3
-               //        http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
-               $forms = $this->preConvertPlural( $forms, 3 );
-
-               if ( $count > 10 && floor( ( $count % 100 ) / 10 ) == 1 ) {
-                       return $forms[2];
-               } else {
-                       switch ( $count % 10 ) {
-                               case 1:  return $forms[0];
-                               case 2:
-                               case 3:
-                               case 4:  return $forms[1];
-                               default: return $forms[2];
-                       }
-               }
-       }
-}
diff --git a/languages/classes/LanguageBh.php b/languages/classes/LanguageBh.php
deleted file mode 100644 (file)
index 0eaf2ff..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
-/**
- * Bihari (भोजपुरी) specific code.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Language
- */
-
-/**
- * Bihari (भोजपुरी)
- *
- * @ingroup Language
- */
-class LanguageBh extends Language {
-       /**
-        * Use singular form for zero
-        *
-        * @param $count int
-        * @param $forms array
-        *
-        * @return string
-        */
-       function convertPlural( $count, $forms ) {
-               if ( !count( $forms ) ) { return ''; }
-               $forms = $this->preConvertPlural( $forms, 2 );
-
-               return ( $count <= 1 ) ? $forms[0] : $forms[1];
-       }
-}
index 0929641..3da7711 100644 (file)
  */
 class LanguageBs extends Language {
 
-       /**
-        * @param $count int
-        * @param $forms array
-        * @return string
-        */
-       function convertPlural( $count, $forms ) {
-               if ( !count( $forms ) ) { return ''; }
-               $forms = $this->preConvertPlural( $forms, 3 );
-
-               // @todo FIXME: CLDR defines 4 plural forms instead of 3. Plural for decimals is missing.
-               //        http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
-               if ( $count > 10 && floor( ( $count % 100 ) / 10 ) == 1 ) {
-                       return $forms[2];
-               } else {
-                       switch ( $count % 10 ) {
-                               case 1:  return $forms[0];
-                               case 2:
-                               case 3:
-                               case 4:  return $forms[1];
-                               default: return $forms[2];
-                       }
-               }
-       }
 
        /**
         * Convert from the nominative form of a noun to some other case
diff --git a/languages/classes/LanguageCs.php b/languages/classes/LanguageCs.php
deleted file mode 100644 (file)
index 49c4756..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-<?php
-/**
- * Czech (čeština [subst.], český [adj.], česky [adv.]) specific code.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Language
- */
-
-/**
- * Czech (čeština [subst.], český [adj.], česky [adv.])
- *
- * @ingroup Language
- */
-class LanguageCs extends Language {
-
-       /**
-        * Plural transformations
-        * Invoked by putting
-        * {{plural:count|form1|form2-4|form0,5+}} for two forms plurals
-        * {{plural:count|form1|form0,2+}} for single form plurals
-        * in a message
-        * @param $count int
-        * @param $forms array
-        * @return string
-        */
-       function convertPlural( $count, $forms ) {
-               if ( !count( $forms ) ) { return ''; }
-               $forms = $this->preConvertPlural( $forms, 3 );
-
-               switch ( $count ) {
-                       case 1:
-                               return $forms[0];
-                       case 2:
-                       case 3:
-                       case 4:
-                               return $forms[1];
-                       default:
-                               return $forms[2];
-               }
-       }
-}
index bfa95cf..2016a43 100644 (file)
@@ -20,7 +20,7 @@
  * @file
  * @ingroup Language
  */
+
 /**
  * Old Church Slavonic (Ѩзыкъ словѣньскъ)
  *
diff --git a/languages/classes/LanguageCy.php b/languages/classes/LanguageCy.php
deleted file mode 100644 (file)
index 9c28279..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-<?php
-/**
- * Welsh (Cymraeg) specific code.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @author Niklas Laxström
- * @ingroup Language
- */
-
-/**
- * Welsh (Cymraeg)
- *
- * @ingroup Language
- */
-class LanguageCy extends Language {
-
-       /**
-        * @param $count int
-        * @param $forms array
-        * @return string
-        */
-       function convertPlural( $count, $forms ) {
-               if ( !count( $forms ) ) { return ''; }
-
-               $forms = $this->preConvertPlural( $forms, 6 );
-               $count = abs( $count );
-               if ( $count >= 0 && $count <= 3 ) {
-                       return $forms[$count];
-               } elseif ( $count == 6 ) {
-                       return $forms[4];
-               } else {
-                       return $forms[5];
-               }
-       }
-}
index b8ed7fc..975157f 100644 (file)
@@ -54,21 +54,4 @@ class LanguageDsb extends Language {
                return $word; # this will return the original value for 'nominatiw' (nominativ) and all undefined case values
        }
 
-       /**
-        * @param $count int
-        * @param $forms array
-        * @return string
-        */
-       function convertPlural( $count, $forms ) {
-               if ( !count( $forms ) ) { return ''; }
-               $forms = $this->preConvertPlural( $forms, 4 );
-
-               switch ( abs( $count ) % 100 ) {
-                       case 1:  return $forms[0]; // singular
-                       case 2:  return $forms[1]; // dual
-                       case 3:
-                       case 4:  return $forms[2]; // plural
-                       default: return $forms[3]; // pluralgen
-               }
-       }
 }
diff --git a/languages/classes/LanguageFr.php b/languages/classes/LanguageFr.php
deleted file mode 100644 (file)
index edbe1fb..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
-/**
- * French (Français) specific code.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @ingroup Language
- */
-
-/**
- * French (Français)
- *
- * @ingroup Language
- */
-class LanguageFr extends Language {
-       /**
-        * Use singular form for zero (see bug 7309)
-        *
-        * @param $count int
-        * @param $forms array
-        *
-        * @return string
-        */
-       function convertPlural( $count, $forms ) {
-               if ( !count( $forms ) ) { return ''; }
-               $forms = $this->preConvertPlural( $forms, 2 );
-
-               return ( $count <= 1 ) ? $forms[0] : $forms[1];
-       }
-}
index cb9fa04..2f58384 100644 (file)
@@ -64,24 +64,4 @@ class LanguageGa extends Language {
                return $word;
        }
 
-       /**
-        * @param $count int
-        * @param $forms array
-        * @return string
-        */
-       function convertPlural( $count, $forms ) {
-               if ( !count( $forms ) ) { return ''; }
-
-               // plural forms per http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ga
-               $forms = $this->preConvertPlural( $forms, 3 );
-
-               if ( $count == 1 ) {
-                       $index = 0;
-               } elseif ( $count == 2 ) {
-                       $index = 1;
-               } else {
-                       $index = 2;
-               }
-               return $forms[$index];
-       }
 }
diff --git a/languages/classes/LanguageGd.php b/languages/classes/LanguageGd.php
deleted file mode 100644 (file)
index f042b02..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-/**
- * Scots Gaelic (Gàidhlig) specific code.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along
- * with this program; if not, write to the Free Software Foundation, Inc.,
- * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- * http://www.gnu.org/copyleft/gpl.html
- *
- * @file
- * @author Raimond Spekking
- * @author Niklas Laxström
- * @ingroup Language
- */
-
-/**
- * Scots Gaelic (Gàidhlig)
- *
- * @ingroup Language
- */
-class LanguageGd extends Language {
-
-       /**
-        * Plural form transformations
-        * Based on this discussion: http://translatewiki.net/wiki/Thread:Support/New_plural_rules_for_Scots_Gaelic_(gd)
-        *
-        * $forms[0] - 1
-        * $forms[1] - 2
-        * $forms[2] - 11
-        * $forms[3] - 12
-        * $forms[4] - 3-10, 13-19
-        * $forms[5] - 0, 20, rest
-        *
-        * @param $count int
-        * @param $forms array
-        *
-        * @return string
-        */
-       function convertPlural( $count, $forms ) {
-               if ( !count( $forms ) ) { return ''; }
-               $forms = $this->preConvertPlural( $forms, 6 );
-
-               $count = abs( $count );
-               if ( $count == 1 ) {
-                       return $forms[0];
-               } elseif ( $count == 2 ) {
-                       return $forms[1];
-               } elseif ( $count == 11 ) {
-                       return $forms[2];
-               } elseif ( $count == 12 ) {
-                       return $forms[3];
-               } elseif ( ($count >= 3 && $count <= 10) || ($count >= 13 && $count <= 19) ) {
-                       return $forms[4];
-               } else {
-                       return $forms[5];
-               }
-       }
-}
index 22be1de..48c0c05 100644 (file)
@@ -68,23 +68,4 @@ class LanguageHe extends Language {
                return $word;
        }
 
-       /**
-        * Gets a number and uses the suited form of the word.
-        *
-        * @param $count Integer: the number of items
-        * @param $forms Array with 3 items: the three plural forms
-        * @return String: the suited form of word
-        */
-       function convertPlural( $count, $forms ) {
-               if ( !count( $forms ) ) { return ''; }
-               $forms = $this->preConvertPlural( $forms, 3 );
-
-               if ( $count == 1 ) {
-                       return $forms[0]; // Singular
-               } elseif ( $count == 2 ) {
-                       return $forms[2]; // Dual or plural if dual is not provided (filled in preConvertPlural)
-               } else {
-                       return $forms[1]; // Plural
-               }
-       }
 }
diff --git a/languages/data/plurals-mediawiki.xml b/languages/data/plurals-mediawiki.xml
new file mode 100644 (file)
index 0000000..fe9e031
--- /dev/null
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE supplementalData SYSTEM "../../common/dtd/ldmlSupplemental.dtd">
+<supplementalData>
+       <plurals>
+               <pluralRules locales="he">
+                       <pluralRule count="one">n is 1</pluralRule>
+                       <pluralRule count="two">n is 2</pluralRule>
+               </pluralRules>
+       </plurals>
+       <pluralRules locales="dsb">
+               <pluralRule count="one">n mod 100 is 1</pluralRule>
+               <pluralRule count="two">n mod 100 is 2</pluralRule>
+               <pluralRule count="few">n mod 100 in 3..4</pluralRule>
+       </pluralRules>
+       <pluralRules locales="cu">
+               <pluralRule count="one">n mod 10 is 1</pluralRule>
+               <pluralRule count="two">n mod 10 is 2</pluralRule>
+               <pluralRule count="few">n mod 10 in 3..4</pluralRule>
+       </pluralRules>
+       <!-- Plural form transformations
+       Based on this discussion: http://translatewiki.net/wiki/Thread:Support/New_plural_rules_for_Scots_Gaelic_(gd)
+       $forms[0] - 1
+       $forms[1] - 2
+       $forms[2] - 11
+       $forms[3] - 12
+       $forms[4] - 3-10, 13-19
+       $forms[5] - 0, 20, rest -->
+       <pluralRules locales="gd">
+               <pluralRule count="one">n is 1</pluralRule>
+               <pluralRule count="two">n is 2</pluralRule>
+               <pluralRule count="elevan">n is 11</pluralRule>
+               <pluralRule count="twelve">n is 12</pluralRule>
+               <pluralRule count="few">n in 3..10 or n in 13..19</pluralRule>
+       </pluralRules>
+</supplementalData>
diff --git a/languages/data/plurals.xml b/languages/data/plurals.xml
new file mode 100644 (file)
index 0000000..8432df4
--- /dev/null
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE supplementalData SYSTEM "../../common/dtd/ldmlSupplemental.dtd">
+<supplementalData>
+    <version number="$Revision: 6155 $"/>
+    <generation date="$Date: 2011-09-21 23:51:12 +0530 (ബു, 21 സെപ് 2011) $"/>
+    <plurals>
+        <!-- if locale is known to have no plurals, there are no rules -->
+        <pluralRules locales="az bm bo dz fa id ig ii hu ja jv ka kde kea km kn ko lo ms my sah ses sg th to tr vi wo yo zh"/>
+        <pluralRules locales="ar">
+            <pluralRule count="zero">n is 0</pluralRule>
+            <pluralRule count="one">n is 1</pluralRule>
+            <pluralRule count="two">n is 2</pluralRule>
+            <pluralRule count="few">n mod 100 in 3..10</pluralRule>
+            <pluralRule count="many">n mod 100 in 11..99</pluralRule>
+        </pluralRules>
+        <pluralRules locales="asa af bem bez bg bn brx ca cgg chr da de dv ee el en eo es et eu fi fo fur fy gl gsw gu ha haw he is it jmc kaj kcg kk kl ksb ku lb lg mas ml mn mr nah nb nd ne nl nn no nr ny nyn om or pa pap ps pt rof rm rwk saq seh sn so sq ss ssy st sv sw syr ta te teo tig tk tn ts ur wae ve vun xh xog zu">
+            <pluralRule count="one">n is 1</pluralRule>
+        </pluralRules>
+        <pluralRules locales="ak am bh fil tl guw hi ln mg nso ti wa">
+            <pluralRule count="one">n in 0..1</pluralRule>
+        </pluralRules>
+        <pluralRules locales="ff fr kab">
+            <pluralRule count="one">n within 0..2 and n is not 2</pluralRule>
+        </pluralRules>
+        <pluralRules locales="lv">
+            <pluralRule count="zero">n is 0</pluralRule>
+            <pluralRule count="one">n mod 10 is 1 and n mod 100 is not 11</pluralRule>
+        </pluralRules>
+        <pluralRules locales="iu kw naq se sma smi smj smn sms">
+            <pluralRule count="one">n is 1</pluralRule>
+            <pluralRule count="two">n is 2</pluralRule>
+        </pluralRules>
+        <pluralRules locales="ga"> <!-- http://unicode.org/cldr/trac/ticket/3915 -->
+            <pluralRule count="one">n is 1</pluralRule>
+            <pluralRule count="two">n is 2</pluralRule>
+            <pluralRule count="few">n in 3..6</pluralRule>
+            <pluralRule count="many">n in 7..10</pluralRule>
+        </pluralRules>
+        <pluralRules locales="ro mo">
+            <pluralRule count="one">n is 1</pluralRule>
+            <pluralRule count="few">n is 0 OR n is not 1 AND n mod 100 in 1..19</pluralRule>
+        </pluralRules>
+        <pluralRules locales="lt">
+            <pluralRule count="one">n mod 10 is 1 and n mod 100 not in 11..19</pluralRule>
+            <pluralRule count="few">n mod 10 in 2..9 and n mod 100 not in 11..19</pluralRule>
+        </pluralRules>
+        <pluralRules locales="be bs hr ru sh sr uk">
+            <pluralRule count="one">n mod 10 is 1 and n mod 100 is not 11</pluralRule>
+            <pluralRule count="few">n mod 10 in 2..4 and n mod 100 not in 12..14</pluralRule>
+            <pluralRule count="many">n mod 10 is 0 or n mod 10 in 5..9 or n mod 100 in 11..14</pluralRule>
+            <!-- others are fractions -->
+        </pluralRules>
+        <pluralRules locales="cs sk">
+            <pluralRule count="one">n is 1</pluralRule>
+            <pluralRule count="few">n in 2..4</pluralRule>
+        </pluralRules>
+        <pluralRules locales="pl">
+            <pluralRule count="one">n is 1</pluralRule>
+            <pluralRule count="few">n mod 10 in 2..4 and n mod 100 not in 12..14</pluralRule>
+            <pluralRule count="many">n is not 1 and n mod 10 in 0..1 or n mod 10 in 5..9 or n mod 100 in 12..14</pluralRule>
+            <!-- others are fractions -->
+            <!-- and n mod 100 not in 22..24 from Tamplin -->
+        </pluralRules>
+        <pluralRules locales="sl">
+            <pluralRule count="one">n mod 100 is 1</pluralRule>
+            <pluralRule count="two">n mod 100 is 2</pluralRule>
+            <pluralRule count="few">n mod 100 in 3..4</pluralRule>
+        </pluralRules>
+        <pluralRules locales="mt"> <!-- from Tamplin's data -->
+            <pluralRule count="one">n is 1</pluralRule>
+            <pluralRule count="few">n is 0 or n mod 100 in 2..10</pluralRule>
+            <pluralRule count="many">n mod 100 in 11..19</pluralRule>
+        </pluralRules>
+        <pluralRules locales="mk"> <!-- from Tamplin's data -->
+            <pluralRule count="one">n mod 10 is 1 and n is not 11</pluralRule>
+        </pluralRules>
+        <pluralRules locales="cy"> <!-- from http://www.saltcymru.org/wordpress/?p=99&lang=en -->
+            <pluralRule count="zero">n is 0</pluralRule>
+            <pluralRule count="one">n is 1</pluralRule>
+            <pluralRule count="two">n is 2</pluralRule>
+            <pluralRule count="few">n is 3</pluralRule>
+            <pluralRule count="many">n is 6</pluralRule>
+        </pluralRules>
+        <pluralRules locales="lag">
+            <pluralRule count="zero">n is 0</pluralRule>
+            <pluralRule count="one">n within 0..2 and n is not 0 and n is not 2</pluralRule>
+        </pluralRules>
+        <pluralRules locales="shi">
+            <pluralRule count="one">n within 0..1</pluralRule>
+            <pluralRule count="few">n in 2..10</pluralRule>
+        </pluralRules>
+        <pluralRules locales="br"> <!-- from http://unicode.org/cldr/trac/ticket/2886 -->
+            <pluralRule count="one">n mod 10 is 1 and n mod 100 not in 11,71,91</pluralRule>
+            <pluralRule count="two">n mod 10 is 2 and n mod 100 not in 12,72,92</pluralRule>
+            <pluralRule count="few">n mod 10 in 3..4,9 and n mod 100 not in 10..19,70..79,90..99</pluralRule>
+            <pluralRule count="many">n mod 1000000 is 0 and n is not 0</pluralRule>
+        </pluralRules>
+        <pluralRules locales="ksh">
+            <pluralRule count="zero">n is 0</pluralRule>
+            <pluralRule count="one">n is 1</pluralRule>
+        </pluralRules>
+        <pluralRules locales="tzm">
+            <pluralRule count="one">n in 0..1 or n in 11..99</pluralRule>
+        </pluralRules>
+        <pluralRules locales="gv">
+            <pluralRule count="one">n mod 10 in 1..2 or n mod 20 is 0</pluralRule>
+        </pluralRules>
+        <pluralRules locales="gd">
+            <pluralRule count="one">n in 1,11</pluralRule>
+            <pluralRule count="two">n in 2,12</pluralRule>
+            <pluralRule count="few">n in 3..10,13..19</pluralRule>
+        </pluralRules>
+    </plurals>
+</supplementalData>
diff --git a/languages/utils/CLDRPluralRuleEvaluator.php b/languages/utils/CLDRPluralRuleEvaluator.php
new file mode 100644 (file)
index 0000000..6b11704
--- /dev/null
@@ -0,0 +1,574 @@
+<?php
+/**
+ * Parse and evaluate a plural rule.
+ *
+ * http://unicode.org/reports/tr35/#Language_Plural_Rules
+ *
+ * @author Niklas Laxstrom, Tim Starling
+ *
+ * @copyright Copyright © 2010-2012, Niklas Laxström
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ * @file
+ * @since 1.20
+ */
+
+class CLDRPluralRuleEvaluator {
+       /**
+        * Evaluate a number against a set of plural rules. If a rule passes,
+        * return the index of plural rule.
+        *
+        * @param int The number to be evaluated against the rules
+        * @param array The associative array of plural rules in pluralform => rule format.
+        * @return int The index of the plural form which passed the evaluation
+        */
+       public static function evaluate( $number, array $rules ) {
+               $rules = self::compile( $rules );
+               return self::evaluateCompiled( $number, $rules );
+       }
+
+       /**
+        * Convert a set of rules to a compiled form which is optimised for
+        * fast evaluation. The result will be an array of strings, and may be cached.
+        *
+        * @param $rules The rules to compile
+        * @return An array of compile rules.
+        */
+       public static function compile( array $rules ) {
+               // We can't use array_map() for this because it generates a warning if
+               // there is an exception.
+               foreach ( $rules as &$rule ) {
+                       $rule = CLDRPluralRuleConverter::convert( $rule );
+               }
+               return $rules;
+       }
+
+       /**
+        * Evaluate a compiled set of rules returned by compile(). Do not allow
+        * the user to edit the compiled form, or else PHP errors may result.
+        */
+       public static function evaluateCompiled( $number, array $rules ) {
+               // The compiled form is RPN, with tokens strictly delimited by
+               // spaces, so this is a simple RPN evaluator.
+               foreach ( $rules as $i => $rule  ) {
+                       $stack = array();
+                       $zero = ord( '0' );
+                       $nine = ord( '9' );
+                       foreach ( StringUtils::explode( ' ', $rule ) as $token ) {
+                               $ord = ord( $token );
+                               if ( $token === 'n' ) {
+                                       $stack[] = $number;
+                               } elseif ( $ord >= $zero && $ord <= $nine ) {
+                                       $stack[] = intval( $token );
+                               } else {
+                                       $right = array_pop( $stack );
+                                       $left = array_pop( $stack );
+                                       $result = self::doOperation( $token, $left, $right );
+                                       $stack[] = $result;
+                               }
+                       }
+                       if ( $stack[0] ) {
+                               return $i;
+                       }
+               }
+               // None of the provided rules match. The number belongs to caregory
+               // 'other' which comes last.
+               return count( $rules );
+       }
+
+       /**
+        * Do a single operation
+        *
+        * @param $token string The token string
+        * @param $left The left operand. If it is an object, its state may be destroyed.
+        * @param $right The right operand
+        * @return mixed
+        */
+       private static function doOperation( $token, $left, $right ) {
+               if ( in_array( $token, array( 'in', 'not-in', 'within', 'not-within' ) ) ) {
+                       if ( !($right instanceof CLDRPluralRuleEvaluator_Range ) ) {
+                               $right = new CLDRPluralRuleEvaluator_Range( $right );
+                       }
+               }
+               switch ( $token ) {
+                       case 'or':
+                               return $left || $right;
+                       case 'and':
+                               return $left && $right;
+                       case 'is':
+                               return $left == $right;
+                       case 'is-not':
+                               return $left != $right;
+                       case 'in':
+                               return $right->isNumberIn( $left );
+                       case 'not-in':
+                               return !$right->isNumberIn( $left );
+                       case 'within':
+                               return $right->isNumberWithin( $left );
+                       case 'not-within':
+                               return !$right->isNumberWithin( $left );
+                       case 'mod':
+                               if ( is_int( $left ) ) {
+                                       return (int) fmod( $left, $right );
+                               }
+                               return fmod( $left, $right );
+                       case ',':
+                               if ( $left instanceof CLDRPluralRuleEvaluator_Range ) {
+                                       $range = $left;
+                               } else {
+                                       $range = new CLDRPluralRuleEvaluator_Range( $left );
+                               }
+                               $range->add( $right );
+                               return $range;
+                       case '..':
+                               return new CLDRPluralRuleEvaluator_Range( $left, $right );
+                       default:
+                               throw new CLDRPluralRuleError( "Invalid RPN token" );
+               }
+       }
+}
+
+/**
+ * Evaluator helper class representing a range list.
+ */
+class CLDRPluralRuleEvaluator_Range {
+       var $parts = array();
+
+       function __construct( $start, $end = false ) {
+               if ( $end === false ) {
+                       $this->parts[] = $start;
+               } else {
+                       $this->parts[] = array( $start, $end );
+               }
+       }
+
+       /**
+        * Determine if the given number is inside the range. If $integerConstraint
+        * is true, the number must additionally be an integer if it is to match
+        * any interval part.
+        */
+       function isNumberIn( $number, $integerConstraint = true ) {
+               foreach ( $this->parts as $part ) {
+                       if ( is_array( $part ) ) {
+                               if ( ( !$integerConstraint || floor( $number ) === (float)$number )
+                                       && $number >= $part[0] && $number <= $part[1] )
+                               {
+                                       return true;
+                               }
+                       } else {
+                               if ( $number == $part ) {
+                                       return true;
+                               }
+                       }
+               }
+               return false;
+       }
+
+       /**
+        * Readable alias for isNumberIn( $number, false ), and the implementation
+        * of the "within" operator.
+        */
+       function isNumberWithin( $number ) {
+               return $this->isNumberIn( $number, false );
+       }
+
+       /**
+        * Add another part to this range. The supplied new part may either be a
+        * range object itself, or a single number.
+        */
+       function add( $other ) {
+               if ( $other instanceof self ) {
+                       $this->parts = array_merge( $this->parts, $other->parts );
+               } else {
+                       $this->parts[] = $other;
+               }
+       }
+
+       /**
+        * For debugging
+        */
+       function __toString() {
+               $s = 'Range(';
+               foreach ( $this->parts as $i => $part ) {
+                       if ( $i ) {
+                               $s .= ', ';
+                       }
+                       if ( is_array( $part ) ) {
+                               $s .= $part[0] . '..' . $part[1];
+                       } else {
+                               $s .= $part;
+                       }
+               }
+               $s .= ')';
+               return $s;
+       }
+
+}
+
+/**
+ * Helper class for converting rules to reverse polish notation (RPN).
+ */
+class CLDRPluralRuleConverter {
+       var $rule, $pos, $end;
+       var $operators = array();
+       var $operands = array();
+
+       /**
+        * Precedence levels. Note that there's no need to worry about associativity
+        * for the level 4 operators, since they return boolean and don't accept
+        * boolean inputs.
+        */
+       static $precedence = array(
+               'or' => 2,
+               'and' => 3,
+               'is' => 4,
+               'is-not' => 4,
+               'in' => 4,
+               'not-in' => 4,
+               'within' => 4,
+               'not-within' => 4,
+               'mod' => 5,
+               ',' => 6,
+               '..' => 7,
+       );
+
+       /**
+        * A character list defining whitespace, for use in strspn() etc.
+        */
+       const WHITESPACE_CLASS = " \t\r\n";
+
+       /**
+        * Same for digits. Note that the grammar given in UTS #35 doesn't allow
+        * negative numbers or decimals.
+        */
+       const NUMBER_CLASS = '0123456789';
+
+       /**
+        * An anchored regular expression which matches a word at the current offset.
+        */
+       const WORD_REGEX = '/[a-zA-Z]+/A';
+
+       /**
+        * Convert a rule to RPN. This is the only public entry point.
+        */
+       public static function convert( $rule ) {
+               $parser = new self( $rule );
+               return $parser->doConvert();
+       }
+
+       /**
+        * Private constructor.
+        */
+       protected function __construct( $rule ) {
+               $this->rule = $rule;
+               $this->pos = 0;
+               $this->end = strlen( $rule );
+       }
+
+       /**
+        * Do the operation.
+        */
+       protected function doConvert() {
+               $expectOperator = true;
+
+               // Iterate through all tokens, saving the operators and operands to a
+               // stack per Dijkstra's shunting yard algorithm.
+               while ( false !== ( $token = $this->nextToken() ) ) {
+                       // In this grammar, there are only binary operators, so every valid
+                       // rule string will alternate between operator and operand tokens.
+                       $expectOperator = !$expectOperator;
+
+                       if ( $token instanceof CLDRPluralRuleConverter_Expression ) {
+                               // Operand
+                               if ( $expectOperator ) {
+                                       $token->error( 'unexpected operand' );
+                               }
+                               $this->operands[] = $token;
+                               continue;
+                       } else {
+                               // Operator
+                               if  ( !$expectOperator ) {
+                                       $token->error( 'unexpected operator' );
+                               }
+                               // Resolve higher precedence levels
+                               $lastOp = end( $this->operators );
+                               while ( $lastOp && self::$precedence[$token->name] <= self::$precedence[$lastOp->name] ) {
+                                       $this->doOperation( $lastOp, $this->operands );
+                                       array_pop( $this->operators );
+                                       $lastOp = end( $this->operators );
+                               }
+                               $this->operators[] = $token;
+                       }
+               }
+
+               // Finish off the stack
+               while ( $op = array_pop( $this->operators ) ) {
+                       $this->doOperation( $op, $this->operands );
+               }
+
+               // Make sure the result is sane. The first case is possible for an empty
+               // string input, the second should be unreachable.
+               if ( !count( $this->operands ) ) {
+                       $this->error( 'condition expected' );
+               } elseif ( count( $this->operands ) > 1 ) {
+                       $this->error( 'missing operator or too many operands' );
+               }
+
+               $value = $this->operands[0];
+               if ( $value->type !== 'boolean' ) {
+                       $this->error( 'the result must have a boolean type' );
+               }
+
+               return $this->operands[0]->rpn;
+       }
+
+       /**
+        * Fetch the next token from the input string. Return it as a
+        * CLDRPluralRuleConverter_Fragment object.
+        */
+       protected function nextToken() {
+               if ( $this->pos >= $this->end ) {
+                       return false;
+               }
+
+               // Whitespace
+               $length = strspn( $this->rule, self::WHITESPACE_CLASS, $this->pos );
+               $this->pos += $length;
+
+               if ( $this->pos >= $this->end ) {
+                       return false;
+               }
+
+               // Number
+               $length = strspn( $this->rule, self::NUMBER_CLASS, $this->pos );
+               if ( $length !== 0 ) {
+                       $token = $this->newNumber( substr( $this->rule, $this->pos, $length ), $this->pos );
+                       $this->pos += $length;
+                       return $token;
+               }
+
+               // Comma
+               if ( $this->rule[$this->pos] === ',' ) {
+                       $token = $this->newOperator( ',', $this->pos, 1 );
+                       $this->pos ++;
+                       return $token;
+               }
+
+               // Dot dot
+               if ( substr( $this->rule, $this->pos, 2 ) === '..' ) {
+                       $token = $this->newOperator( '..', $this->pos, 2 );
+                       $this->pos += 2;
+                       return $token;
+               }
+
+               // Word
+               if ( !preg_match( self::WORD_REGEX, $this->rule, $m, 0, $this->pos ) ) {
+                       $this->error( 'unexpected character "' . $this->rule[$this->pos] . '"'  );
+               }
+               $word1 = strtolower( $m[0] );
+               $word2 = '';
+               $nextTokenPos = $this->pos + strlen( $word1 );
+               if ( $word1 === 'not' || $word1 === 'is' ) {
+                       // Look ahead one word
+                       $nextTokenPos += strspn( $this->rule, self::WHITESPACE_CLASS, $nextTokenPos );
+                       if ( $nextTokenPos < $this->end
+                                       && preg_match( self::WORD_REGEX, $this->rule, $m, 0, $nextTokenPos ) )
+                       {
+                               $word2 = strtolower( $m[0] );
+                               $nextTokenPos += strlen( $word2 );
+                       }
+               }
+
+               // Two-word operators like "is not" take precedence over single-word operators like "is"
+               if ( $word2 !== '' ) {
+                       $bothWords = "{$word1}-{$word2}";
+                       if ( isset( self::$precedence[$bothWords] ) ) {
+                               $token = $this->newOperator( $bothWords, $this->pos, $nextTokenPos - $this->pos );
+                               $this->pos = $nextTokenPos;
+                               return $token;
+                       }
+               }
+
+               // Single-word operators
+               if ( isset( self::$precedence[$word1] ) ) {
+                       $token = $this->newOperator( $word1, $this->pos, strlen( $word1 ) );
+                       $this->pos += strlen( $word1 );
+                       return $token;
+               }
+
+               // The special numerical keyword "n"
+               if ( $word1 === 'n' ) {
+                       $token = $this->newNumber( 'n', $this->pos );
+                       $this->pos ++;
+                       return $token;
+               }
+
+               $this->error( 'unrecognised word' );
+       }
+
+       /**
+        * For the binary operator $op, pop its operands off the stack and push
+        * a fragment with rpn and type members describing the result of that
+        * operation.
+        */
+       protected function doOperation( $op ) {
+               if ( count( $this->operands ) < 2 ) {
+                       $op->error( 'missing operand' );
+               }
+               $right = array_pop( $this->operands );
+               $left = array_pop( $this->operands );
+               $result = $op->operate( $left, $right );
+               $this->operands[] = $result;
+       }
+
+       /**
+        * Create a numerical expression object
+        */
+       protected function newNumber( $text, $pos ) {
+               return new CLDRPluralRuleConverter_Expression( $this, 'number', $text, $pos, strlen( $text ) );
+       }
+
+       /**
+        * Create a binary operator
+        */
+       protected function newOperator( $type, $pos, $length ) {
+               return new CLDRPluralRuleConverter_Operator( $this, $type, $pos, $length );
+       }
+
+       /**
+        * Throw an error
+        */
+       protected function error( $message ) {
+               throw new CLDRPluralRuleError( $message );
+       }
+}
+
+/**
+ * Helper for CLDRPluralRuleConverter.
+ * The base class for operators and expressions, describing a region of the input string.
+ */
+class CLDRPluralRuleConverter_Fragment {
+       var $parser, $pos, $length, $end;
+
+       function __construct( $parser, $pos, $length ) {
+               $this->parser = $parser;
+               $this->pos = $pos;
+               $this->length = $length;
+               $this->end = $pos + $length;
+       }
+
+       public function error( $message ) {
+               $text = $this->getText();
+               throw new CLDRPluralRuleError( "$message at position " . ( $this->pos + 1 ) . ": \"$text\"" );
+       }
+
+       public function getText() {
+               return substr( $this->parser->rule, $this->pos, $this->length );
+       }
+}
+
+/**
+ * Helper for CLDRPluralRuleConverter.
+ * An expression object, representing a region of the input string (for error
+ * messages), the RPN notation used to evaluate it, and the result type for
+ * validation.
+ */
+class CLDRPluralRuleConverter_Expression extends CLDRPluralRuleConverter_Fragment {
+       var $type, $rpn;
+
+       function __construct( $parser, $type, $rpn, $pos, $length ) {
+               parent::__construct( $parser, $pos, $length );
+               $this->type = $type;
+               $this->rpn = $rpn;
+       }
+
+       public function isType( $type ) {
+               if ( $type === 'range' && ( $this->type === 'range' || $this->type === 'number' ) ) {
+                       return true;
+               }
+               if ( $type === $this->type ) {
+                       return true;
+               }
+               return false;
+       }
+}
+
+/**
+ * Helper for CLDRPluralRuleConverter.
+ * An operator object, representing a region of the input string (for error
+ * messages), and the binary operator at that location.
+ */
+class CLDRPluralRuleConverter_Operator extends CLDRPluralRuleConverter_Fragment {
+       var $name;
+
+       /**
+        * Each op type has three characters: left operand type, right operand type and result type
+        *
+        *   b = boolean
+        *   n = number
+        *   r = range
+        *
+        * A number is a kind of range.
+        */
+       static $opTypes = array(
+               'or' => 'bbb',
+               'and' => 'bbb',
+               'is' => 'nnb',
+               'is-not' => 'nnb',
+               'in' => 'nrb',
+               'not-in' => 'nrb',
+               'within' => 'nrb',
+               'not-within' => 'nrb',
+               'mod' => 'nnn',
+               ',' => 'rrr',
+               '..' => 'nnr',
+       );
+
+       /**
+        * Map converting from the abbrevation to the full form.
+        */
+       static $typeSpecMap = array(
+               'b' => 'boolean',
+               'n' => 'number',
+               'r' => 'range',
+       );
+
+       function __construct( $parser, $name, $pos, $length ) {
+               parent::__construct( $parser, $pos, $length );
+               $this->name = $name;
+       }
+
+       public function operate( $left, $right ) {
+               $typeSpec = self::$opTypes[$this->name];
+
+               $leftType = self::$typeSpecMap[$typeSpec[0]];
+               $rightType = self::$typeSpecMap[$typeSpec[1]];
+               $resultType = self::$typeSpecMap[$typeSpec[2]];
+
+               $start = min( $this->pos, $left->pos, $right->pos );
+               $end = max( $this->end, $left->end, $right->end );
+               $length = $end - $start;
+
+               $newExpr = new CLDRPluralRuleConverter_Expression( $this->parser, $resultType,
+                       "{$left->rpn} {$right->rpn} {$this->name}",
+                       $start, $length );
+
+               if ( !$left->isType( $leftType ) ) {
+                       $newExpr->error( "invalid type for left operand: expected $leftType, got {$left->type}" );
+               }
+
+               if ( !$right->isType( $rightType ) ) {
+                       $newExpr->error( "invalid type for right operand: expected $rightType, got {$right->type}" );
+               }
+               return $newExpr;
+       }
+}
+
+/**
+ * The exception class for all the classes in this file. This will be thrown
+ * back to the caller if there is any validation error.
+ */
+class CLDRPluralRuleError extends MWException {
+       function __construct( $message ) {
+               parent::__construct( 'CLDR plural rule error: ' . $message );
+       }
+}
index 380a099..5a24355 100644 (file)
@@ -671,7 +671,6 @@ return array(
                'scripts' => 'resources/mediawiki.language/mediawiki.language.js',
                'languageScripts' => array(
                        'am' => 'resources/mediawiki.language/languages/am.js',
-                       'ar' => 'resources/mediawiki.language/languages/ar.js',
                        'bat-smg' => 'resources/mediawiki.language/languages/bat-smg.js',
                        'be' => 'resources/mediawiki.language/languages/be.js',
                        'be-tarask' => 'resources/mediawiki.language/languages/be-tarask.js',
@@ -687,7 +686,6 @@ return array(
                        'gd' => 'resources/mediawiki.language/languages/gd.js',
                        'gv' => 'resources/mediawiki.language/languages/gv.js',
                        'he' => 'resources/mediawiki.language/languages/he.js',
-                       'hi' => 'resources/mediawiki.language/languages/hi.js',
                        'hr' => 'resources/mediawiki.language/languages/hr.js',
                        'hsb' => 'resources/mediawiki.language/languages/hsb.js',
                        'hu' => 'resources/mediawiki.language/languages/hu.js',
@@ -719,7 +717,21 @@ return array(
                        'uk' => 'resources/mediawiki.language/languages/uk.js',
                        'wa' => 'resources/mediawiki.language/languages/wa.js',
                ),
-               'dependencies' => array( 'mediawiki.language.data' ),
+               'dependencies' => array(
+                               'mediawiki.language.data',
+                               'mediawiki.cldr'
+                       ),
+       ),
+
+       'mediawiki.cldr' => array(
+               'scripts' => 'resources/mediawiki.language/mediawiki.cldr.js',
+               'dependencies' => array(
+                       'mediawiki.libs.pluralruleparser',
+               ),
+       ),
+
+       'mediawiki.libs.pluralruleparser' => array(
+               'scripts' => 'resources/mediawiki.libs/CLDRPluralRuleParser.js',
        ),
 
        'mediawiki.language.init' => array(
diff --git a/resources/mediawiki.language/languages/ar.js b/resources/mediawiki.language/languages/ar.js
deleted file mode 100644 (file)
index d21df7e..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Arabic (العربية) language functions
- */
-
-mediaWiki.language.convertPlural = function( count, forms ) {
-       forms = mediaWiki.language.preConvertPlural( forms, 6 );
-       if ( count == 0 ) {
-               return forms[0];
-       }
-       if ( count == 1 ) {
-               return forms[1];
-       }
-       if ( count == 2 ) {
-               return forms[2];
-       }
-       if ( count % 100 >= 3 && count % 100 <= 10 ) {
-               return forms[3];
-       }
-       if ( count % 100 >= 11 && count % 100 <= 99 ) {
-               return forms[4];
-       }
-       return forms[5];
-};
-
index e737a7c..d35f77e 100644 (file)
@@ -2,17 +2,6 @@
  * Hebrew (עברית) language functions
  */
 
-mediaWiki.language.convertPlural = function( count, forms ) {
-       forms = mediaWiki.language.preConvertPlural( forms, 3 );
-       if ( count == 1 ) {
-               return forms[0];
-       }
-       if ( count == 2 && forms[2] ) {
-               return forms[2];
-       }
-       return forms[1];
-};
-
 mediaWiki.language.convertGrammar = function( word, form ) {
        var grammarForms = mw.language.getData( 'he', 'grammarForms' );
        if ( grammarForms && grammarForms[form] ) {
diff --git a/resources/mediawiki.language/languages/hi.js b/resources/mediawiki.language/languages/hi.js
deleted file mode 100644 (file)
index a22a0e1..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-/**
- * Hindi (हिन्दी) language functions
- */
-
-mediaWiki.language.convertPlural = function( count, forms ) {
-       forms = mediaWiki.language.preConvertPlural( forms, 2 );
-       return ( count <= 1 ) ? forms[0] : forms[1];
-};
diff --git a/resources/mediawiki.language/mediawiki.cldr.js b/resources/mediawiki.language/mediawiki.cldr.js
new file mode 100644 (file)
index 0000000..6660eca
--- /dev/null
@@ -0,0 +1,28 @@
+/**
+ *  CLDR related utility methods
+ */
+( function( mw ) {
+       "use strict";
+
+       var cldr = {
+               /**
+                * For the number, get the plural for index
+                * In case none of the rules passed, we return pluralRules.length
+                * That means it is the "other" form.
+                * @param number
+                * @param pluralRules
+                * @return plural form index
+                */
+               getPluralForm: function( number, pluralRules ) {
+                       var pluralFormIndex = 0;
+                       for ( pluralFormIndex = 0; pluralFormIndex < pluralRules.length; pluralFormIndex++ ) {
+                               if ( mw.libs.pluralRuleParser( pluralRules[pluralFormIndex], number ) ) {
+                                       break;
+                               }
+                       }
+                       return pluralFormIndex;
+               }
+       };
+
+       mw.cldr = cldr;
+} )( mediaWiki );
index 1234637..935d4ff 100644 (file)
@@ -43,11 +43,19 @@ var language = {
         * @param forms array List of plural forms
         * @return string Correct form for quantifier in this language
         */
-       convertPlural: function ( count, forms ){
+       convertPlural: function( count, forms ) {
+               var pluralFormIndex = 0;
                if ( !forms || forms.length === 0 ) {
                        return '';
                }
-               return ( parseInt( count, 10 ) === 1 ) ? forms[0] : forms[1];
+               var pluralRules = mw.language.getData( mw.config.get( 'wgUserLanguage' ), 'pluralRules' );
+               if ( !pluralRules ) {
+                       // default fallback.
+                       return ( count === 1 ) ? forms[0] : forms[1];
+               }
+               pluralFormIndex = mw.cldr.getPluralForm( count, pluralRules );
+               pluralFormIndex = Math.min( pluralFormIndex, forms.length - 1 );
+               return forms[pluralFormIndex];
        },
 
        /**
diff --git a/resources/mediawiki.libs/CLDRPluralRuleParser.js b/resources/mediawiki.libs/CLDRPluralRuleParser.js
new file mode 100644 (file)
index 0000000..bb1491d
--- /dev/null
@@ -0,0 +1,306 @@
+/* This is cldrpluralparser 1.0, ported to MediaWiki ResourceLoader */
+
+/**
+* cldrpluralparser.js
+* A parser engine for CLDR plural rules.
+*
+* Copyright 2012 GPLV3+, Santhosh Thottingal
+*
+* @version 0.1.0-alpha
+* @source https://github.com/santhoshtr/CLDRPluralRuleParser
+* @author Santhosh Thottingal <santhosh.thottingal@gmail.com>
+* @author Timo Tijhof
+* @author Amir Aharoni
+*/
+
+/**
+ * Evaluates a plural rule in CLDR syntax for a number
+ * @param rule
+ * @param number
+ * @return true|false|null
+ */
+( function( mw ) {
+
+function pluralRuleParser(rule, number) {
+       /*
+       Syntax: see http://unicode.org/reports/tr35/#Language_Plural_Rules
+       -----------------------------------------------------------------
+
+       condition     = and_condition ('or' and_condition)*
+       and_condition = relation ('and' relation)*
+       relation      = is_relation | in_relation | within_relation | 'n' <EOL>
+       is_relation   = expr 'is' ('not')? value
+       in_relation   = expr ('not')? 'in' range_list
+       within_relation = expr ('not')? 'within' range_list
+       expr          = 'n' ('mod' value)?
+       range_list    = (range | value) (',' range_list)*
+       value         = digit+
+       digit         = 0|1|2|3|4|5|6|7|8|9
+       range         = value'..'value
+
+       */
+       // Indicates current position in the rule as we parse through it.
+       // Shared among all parsing functions below.
+       var pos = 0;
+
+       var whitespace = makeRegexParser(/^\s+/);
+       var digits = makeRegexParser(/^\d+/);
+
+       var _n_ = makeStringParser('n');
+       var _is_ = makeStringParser('is');
+       var _mod_ = makeStringParser('mod');
+       var _not_ = makeStringParser('not');
+       var _in_ = makeStringParser('in');
+       var _within_ = makeStringParser('within');
+       var _range_ = makeStringParser('..');
+       var _comma_ = makeStringParser(',');
+       var _or_ = makeStringParser('or');
+       var _and_ = makeStringParser('and');
+
+       function debug() {
+               /* console.log.apply(console, arguments);*/
+       }
+
+       debug('pluralRuleParser', rule, number);
+
+       // Try parsers until one works, if none work return null
+       function choice(parserSyntax) {
+               return function () {
+                       for (var i = 0; i < parserSyntax.length; i++) {
+                               var result = parserSyntax[i]();
+                               if (result !== null) {
+                                       return result;
+                               }
+                       }
+                       return null;
+               };
+       }
+
+       // Try several parserSyntax-es in a row.
+       // All must succeed; otherwise, return null.
+       // This is the only eager one.
+       function sequence(parserSyntax) {
+               var originalPos = pos;
+               var result = [];
+               for (var i = 0; i < parserSyntax.length; i++) {
+                       var res = parserSyntax[i]();
+                       if (res === null) {
+                               pos = originalPos;
+                               return null;
+                       }
+                       result.push(res);
+               }
+               return result;
+       }
+
+       // Run the same parser over and over until it fails.
+       // Must succeed a minimum of n times; otherwise, return null.
+       function nOrMore(n, p) {
+               return function () {
+                       var originalPos = pos;
+                       var result = [];
+                       var parsed = p();
+                       while (parsed !== null) {
+                               result.push(parsed);
+                               parsed = p();
+                       }
+                       if (result.length < n) {
+                               pos = originalPos;
+                               return null;
+                       }
+                       return result;
+               };
+       }
+
+       // Helpers -- just make parserSyntax out of simpler JS builtin types
+
+       function makeStringParser(s) {
+               var len = s.length;
+               return function () {
+                       var result = null;
+                       if (rule.substr(pos, len) === s) {
+                               result = s;
+                               pos += len;
+                       }
+                       return result;
+               };
+       }
+
+       function makeRegexParser(regex) {
+               return function () {
+                       var matches = rule.substr(pos).match(regex);
+                       if (matches === null) {
+                               return null;
+                       }
+                       pos += matches[0].length;
+                       return matches[0];
+               };
+       }
+
+       function n() {
+               var result = _n_();
+               if (result === null) {
+                       debug(" -- failed n");
+                       return result;
+               }
+               result = parseInt(number, 10);
+               debug(" -- passed n ", result);
+               return result;
+       }
+
+       var expression = choice([mod, n]);
+
+       function mod() {
+               var result = sequence([n, whitespace, _mod_, whitespace, digits]);
+               if (result === null) {
+                       debug(" -- failed mod");
+                       return null;
+               }
+               debug(" -- passed mod");
+               return parseInt(result[0], 10) % parseInt(result[4], 10);
+       }
+
+       function not() {
+               var result = sequence([whitespace, _not_]);
+               if (result === null) {
+                       debug(" -- failed not");
+                       return null;
+               } else {
+                       return result[1];
+               }
+       }
+
+       function is() {
+               var result = sequence([expression, whitespace, _is_, nOrMore(0, not), whitespace, digits]);
+               if (result !== null) {
+                       debug(" -- passed is");
+                       if (result[3][0] === 'not') {
+                               return result[0] !== parseInt(result[5], 10);
+                       } else {
+                               return result[0] === parseInt(result[5], 10);
+                       }
+               }
+               debug(" -- failed is");
+               return null;
+       }
+
+       function rangeList() {
+               // range_list    = (range | value) (',' range_list)*
+               var result = sequence([choice([range, digits]), nOrMore(0, rangeTail)]);
+               var resultList = [];
+               if (result !== null) {
+                       resultList = resultList.concat(result[0], result[1][0]);
+                       return resultList;
+               }
+               debug(" -- failed rangeList");
+               return null;
+       }
+
+       function rangeTail() {
+               // ',' range_list
+               var result = sequence([_comma_, rangeList]);
+               if (result !== null) {
+                       return result[1];
+               }
+               debug(" -- failed rangeTail");
+               return null;
+       }
+
+       function range() {
+               var result = sequence([digits, _range_, digits]);
+               if (result !== null) {
+                       debug(" -- passed range");
+                       var array = [];
+                       var left = parseInt(result[0], 10);
+                       var right = parseInt(result[2], 10);
+                       for ( i = left; i <= right; i++) {
+                               array.push(i);
+                       }
+                       return array;
+               }
+               debug(" -- failed range");
+               return null;
+       }
+
+       function _in() {
+               // in_relation   = expr ('not')? 'in' range_list
+               var result = sequence([expression, nOrMore(0, not), whitespace, _in_, whitespace, rangeList]);
+               if (result !== null) {
+                       debug(" -- passed _in");
+                       var range_list = result[5];
+                       for (var i = 0; i < range_list.length; i++) {
+                               if (parseInt(range_list[i], 10) === result[0]) {
+                                       return (result[1][0] !== 'not');
+                               }
+                       }
+                       return (result[1][0] === 'not');
+               }
+               debug(" -- failed _in ");
+               return null;
+       }
+
+       function within() {
+               var result = sequence([expression, whitespace, _within_, whitespace, rangeList]);
+               if (result !== null) {
+                       debug(" -- passed within ");
+                       var range_list = result[4];
+                       return (parseInt( range_list[0],10 )<= result[0] && result[0] <= parseInt( range_list[1], 10));
+               }
+               debug(" -- failed within ");
+               return null;
+       }
+
+
+       var relation = choice([is, _in, within]);
+
+       function and() {
+               var result = sequence([relation, whitespace, _and_, whitespace, condition]);
+               if (result) {
+                       debug(" -- passed and");
+                       return result[0] && result[4];
+               }
+               debug(" -- failed and");
+               return null;
+       }
+
+       function or() {
+               var result = sequence([relation, whitespace, _or_, whitespace, condition]);
+               if (result) {
+                       debug(" -- passed or");
+                       return result[0] || result[4];
+               }
+               debug(" -- failed or");
+               return null;
+       }
+
+       var condition = choice([and, or, relation]);
+
+       function start() {
+               var result = condition();
+               return result;
+       }
+
+
+       var result = start();
+
+       /*
+        * For success, the pos must have gotten to the end of the rule
+        * and returned a non-null.
+        * n.b. This is part of language infrastructure, so we do not throw an internationalizable message.
+        */
+       if (result === null || pos !== rule.length) {
+               // throw new Error("Parse error at position " + pos.toString() + " in input: " + rule + " result is " + result);
+       }
+
+       return result;
+}
+
+/* For module loaders, e.g. NodeJS, NPM */
+if (typeof module !== 'undefined' && module.exports) {
+       module.exports = pluralRuleParser;
+}
+
+/* pluralRuleParser ends here */
+mw.libs.pluralRuleParser = pluralRuleParser;
+
+} )( mediaWiki );
index 9ac0f95..7833da7 100644 (file)
@@ -18,31 +18,31 @@ class LanguageHeTest extends MediaWikiTestCase {
 
        /** @dataProvider providerPluralDual */
        function testPluralDual( $result, $value ) {
-               $forms = array( 'one', 'many', 'two' );
+               $forms = array( 'one', 'two', 'other' );
                $this->assertEquals( $result, $this->lang->convertPlural( $value, $forms ) );
        }
 
        function providerPluralDual() {
                return array (
-                       array( 'many', 0 ), // Zero -> plural
+                       array( 'other', 0 ), // Zero -> plural
                        array( 'one', 1 ), // Singular
                        array( 'two', 2 ), // Dual
-                       array( 'many', 3 ), // Plural
+                       array( 'other', 3 ), // Plural
                );
        }
 
        /** @dataProvider providerPlural */
        function testPlural( $result, $value ) {
-               $forms = array( 'one', 'many' );
+               $forms = array( 'one', 'other' );
                $this->assertEquals( $result, $this->lang->convertPlural( $value, $forms ) );
        }
 
        function providerPlural() {
                return array (
-                       array( 'many', 0 ), // Zero -> plural
+                       array( 'other', 0 ), // Zero -> plural
                        array( 'one', 1 ), // Singular
-                       array( 'many', 2 ), // Plural, no dual provided
-                       array( 'many', 3 ), // Plural
+                       array( 'other', 2 ), // Plural, no dual provided
+                       array( 'other', 3 ), // Plural
                );
        }
 }
diff --git a/tests/phpunit/languages/utils/CLDRPluralRuleEvaluatorTest.php b/tests/phpunit/languages/utils/CLDRPluralRuleEvaluatorTest.php
new file mode 100644 (file)
index 0000000..033164b
--- /dev/null
@@ -0,0 +1,95 @@
+<?php
+/**
+ * @author Niklas Laxström
+ * @file
+ */
+
+class CLDRPluralRuleEvaluatorTest extends MediaWikiTestCase {
+       /**
+        * @dataProvider validTestCases
+        */
+       function testValidRules( $expected, $rules, $number, $comment ) {
+               $result = CLDRPluralRuleEvaluator::evaluate( $number, (array) $rules );
+               $this->assertEquals( $expected, $result, $comment );
+       }
+
+       /**
+        * @dataProvider invalidTestCases
+        * @expectedException CLDRPluralRuleError
+        */
+       function testInvalidRules( $rules, $comment ) {
+               CLDRPluralRuleEvaluator::evaluate( 1, (array) $rules );
+       }
+
+       function validTestCases() {
+               $tests = array(
+                       # expected, number, rule, comment
+                       array( 0, 'n is 1', 1, 'integer number and is' ),
+                       array( 0, 'n is 1', "1", 'string integer number and is' ),
+                       array( 0, 'n is 1', 1.0, 'float number and is' ),
+                       array( 0, 'n is 1', "1.0", 'string float number and is' ),
+                       array( 1, 'n is 1', 1.1, 'float number and is' ),
+                       array( 1, 'n is 1', 2, 'float number and is' ),
+
+                       array( 0, 'n in 1,3,5',     3, '' ),
+                       array( 1, 'n not in 1,3,5', 5, '' ),
+
+                       array( 1, 'n in 1,3,5',     2, '' ),
+                       array( 0, 'n not in 1,3,5', 4, '' ),
+
+                       array( 0, 'n in 1..3',      2, '' ),
+                       array( 0, 'n in 1..3',      3, 'in is inclusive' ),
+                       array( 1, 'n in 1..3',      0, '' ),
+
+                       array( 1, 'n not in 1..3',      2, '' ),
+                       array( 1, 'n not in 1..3',      3, 'in is inclusive' ),
+                       array( 0, 'n not in 1..3',      0, '' ),
+
+                       array( 1, 'n is not 1 and n is not 2 and n is not 3', 1, 'and relation' ),
+                       array( 0, 'n is not 1 and n is not 2 and n is not 4', 3, 'and relation' ),
+
+                       array( 0, 'n is not 1 or n is 1', 1, 'or relation' ),
+                       array( 1, 'n is 1 or n is 2', 3, 'or relation' ),
+
+                       array( 0, 'n              is      1', 1, 'extra whitespace' ),
+
+                       array( 0, 'n mod 3 is 1', 7, 'mod' ),
+                       array( 0, 'n mod 3 is not 1', 4.3, 'mod with floats' ),
+
+                       array( 0, 'n within 1..3', 2, 'within with integer' ),
+                       array( 0, 'n within 1..3', 2.5, 'within with float' ),
+                       array( 0, 'n in 1..3', 2, 'in with integer' ),
+                       array( 1, 'n in 1..3', 2.5, 'in with float' ),
+
+                       array( 0, 'n in 3 or n is 4 and n is 5', 3, 'and binds more tightly than or' ),
+                       array( 1, 'n is 3 or n is 4 and n is 5', 4, 'and binds more tightly than or' ),
+
+                       array( 0, 'n mod 10 in 3..4,9 and n mod 100 not in 10..19,70..79,90..99', 24, 'breton rule' ),
+                       array( 1, 'n mod 10 in 3..4,9 and n mod 100 not in 10..19,70..79,90..99', 25, 'breton rule' ),
+
+                       array( 0, 'n within 0..2 and n is not 2', 0, 'french rule' ),
+                       array( 0, 'n within 0..2 and n is not 2', 1, 'french rule' ),
+                       array( 0, 'n within 0..2 and n is not 2', 1.2, 'french rule' ),
+                       array( 1, 'n within 0..2 and n is not 2', 2, 'french rule' ),
+
+                       array( 1, 'n in 3..10,13..19', 2, 'scottish rule - ranges with comma' ),
+                       array( 0, 'n in 3..10,13..19', 4, 'scottish rule - ranges with comma' ),
+                       array( 1, 'n in 3..10,13..19', 12.999, 'scottish rule - ranges with comma' ),
+                       array( 0, 'n in 3..10,13..19', 13, 'scottish rule - ranges with comma' ),
+
+                       array( 0, '5 mod 3 is n', 2, 'n as result of mod - no need to pass' ),
+               );
+
+               return $tests;
+       }
+
+       function invalidTestCases() {
+               $tests = array(
+                       array( 'n mod mod 5 is 1', 'mod mod' ),
+                       array( 'n', 'just n' ),
+                       array( 'n is in 5', 'is in' ),
+               );
+               return $tests;
+       }
+
+}
index 1cd085f..59ae73c 100644 (file)
@@ -30,6 +30,7 @@ return array(
                        'tests/qunit/suites/resources/mediawiki.api/mediawiki.api.parse.test.js',
                        'tests/qunit/suites/resources/mediawiki.special/mediawiki.special.recentchanges.test.js',
                        'tests/qunit/suites/resources/mediawiki/mediawiki.language.test.js',
+                       'tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js',
                ),
                'dependencies' => array(
                        'jquery.autoEllipsis',
@@ -55,6 +56,7 @@ return array(
                        'mediawiki.util',
                        'mediawiki.special.recentchanges',
                        'mediawiki.language',
+                       'mediawiki.cldr',
                ),
                'position' => 'top',
        )
diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.cldr.test.js
new file mode 100644 (file)
index 0000000..09a11b0
--- /dev/null
@@ -0,0 +1,67 @@
+module( 'mediawiki.cldr' );
+
+test( '-- Initial check', function() {
+       expect( 1 );
+       ok( mw.cldr, 'mw.cldr defined' );
+} );
+
+var pluralTestcases = {
+       /*
+        * Sample:
+        *"languagecode" : [
+        *      [ number, [ "form1", "form2", ... ],  "expected", "description" ],
+        * ]
+        */
+       "en": [
+               [ 0, [ "one", "other" ], "other", "English plural test- 0 is other" ],
+               [ 1, [ "one", "other" ], "one", "English plural test- 1 is one" ]
+       ],
+       "hi": [
+               [ 0, [ "one", "other" ], "one", "Hindi plural test- 0 is one" ],
+               [ 1, [ "one", "other" ], "one", "Hindi plural test- 1 is one" ],
+               [ 2, [ "one", "other" ], "other", "Hindi plural test- 2 is other" ]
+       ],
+       "he": [
+               [ 0, [ "one", "other" ], "other", "Hebrew plural test- 0 is other" ],
+               [ 1, [ "one", "other" ], "one", "Hebrew plural test- 1 is one" ],
+               [ 2, [ "one", "other" ], "other", "Hebrew plural test- 2 is other with 2 forms" ],
+               [ 2, [ "one", "dual", "other" ], "dual", "Hebrew plural test- 2 is dual with 3 forms" ]
+       ],
+       "ar": [
+               [ 0, [ "zero", "one", "two", "few", "many", "other" ], "zero", "Arabic plural test - 0 is zero" ],
+               [ 1, [ "zero", "one", "two", "few", "many", "other" ], "one", "Arabic plural test - 1 is one" ],
+               [ 2, [ "zero", "one", "two", "few", "many", "other" ], "two", "Arabic plural test - 2 is two" ],
+               [ 3, [ "zero", "one", "two", "few", "many", "other" ], "few", "Arabic plural test - 3 is few" ],
+               [ 9, [ "zero", "one", "two", "few", "many", "other" ], "few", "Arabic plural test - 9 is few" ],
+               [ "9", [ "zero", "one", "two", "few", "many", "other" ], "few", "Arabic plural test - 9 is few" ],
+               [ 110, [ "zero", "one", "two", "few", "many", "other" ], "few", "Arabic plural test - 110 is few" ],
+               [ 11, [ "zero", "one", "two", "few", "many", "other" ], "many", "Arabic plural test - 11 is many" ],
+               [ 15, [ "zero", "one", "two", "few", "many", "other" ], "many", "Arabic plural test - 15 is many" ],
+               [ 99, [ "zero", "one", "two", "few", "many", "other" ], "many", "Arabic plural test - 99 is many" ],
+               [ 9999, [ "zero", "one", "two", "few", "many", "other" ], "many", "Arabic plural test - 9999 is many" ],
+               [ 100, [ "zero", "one", "two", "few", "many", "other" ], "other", "Arabic plural test - 100 is other" ],
+               [ 102, [ "zero", "one", "two", "few", "many", "other" ], "other", "Arabic plural test - 102 is other" ],
+               [ 1000, [ "zero", "one", "two", "few", "many", "other" ], "other", "Arabic plural test - 1000 is other" ]
+               // FIXME plural rules for decimal numbers does not work
+               // [ 1.7, [ "zero", "one", "two", "few", "many", "other" ], "other", "Arabic plural test - 1.7 is other" ],
+       ]
+};
+
+function pluralTest( langCode, tests ) {
+       QUnit.test('-- Plural Test for ' + langCode, function( assert ) {
+               QUnit.expect( tests.length );
+               for ( var i = 0; i < tests.length; i++ ) {
+                       assert.equal(
+                               mw.language.convertPlural( tests[i][0], tests[i][1] ),
+                               tests[i][2], // Expected plural form
+                               tests[i][3] // Description
+                       );
+               }
+       } );
+}
+
+$.each( pluralTestcases, function( langCode, tests ) {
+       if ( langCode === mw.config.get( 'wgUserLanguage' ) ) {
+               pluralTest( langCode, tests );
+       }
+} );