CLDR Plural rules based plural form calculation
authorSanthosh Thottingal <santhosh.thottingal@gmail.com>
Mon, 18 Jun 2012 08:28:44 +0000 (13:58 +0530)
committerTim Starling <tstarling@wikimedia.org>
Thu, 16 Aug 2012 01:45:17 +0000 (11:45 +1000)
* Use the plurals.xml of CLDR for the plural rules of languages
* Use plurals-mediawiki.xml to override or extend the rules inside MW
* Remove the convertPlural method in each LanguageXX.php
* Parse and load the xml files in LocalisationCache
* Use the CLDRPluralRuleEvaluator.php for parsing the cldr plural rules
  (This is taken from Translate extension and might require a replacement
   parser without using eval)
* Add getPluralRules() to make the CLDR plural rules available to JS.

PS3: More method documentation, cleanup

Change-Id: I58a9cdfe60c7b9027bf031c91370472054f04ae2

21 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]
tests/phpunit/languages/LanguageHeTest.php

index 752f09c..8244bdf 100644 (file)
@@ -1006,6 +1006,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..c9dd697 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,11 @@ class LocalisationCache {
         */
        static public $preloadedKeys = array( 'dateFormats', 'namespaceNames' );
 
+       /*
+        * Associative array containing plural rules.
+        */
+       var $pluralRules = array();
+
        var $mergeableKeys = null;
 
        /**
@@ -202,6 +207,7 @@ class LocalisationCache {
                                $this->$var = $conf[$var];
                        }
                }
+               $this->readPluralRules();
        }
 
        /**
@@ -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,11 +487,43 @@ 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;
        }
+       /**
+        * Read the plural rule xml files.
+        * First the CLDR xml will be read and it will be extended with
+        * mediawiki specific tailoring.
+        * @since 1.20
+        */
+       protected function readPluralRules() {
+               $CLDRPlural = __DIR__ . "/../languages/data/plurals.xml";
+               $MWPlural = __DIR__ . "/../languages/data/plurals-mediawiki.xml";
+               # Load CLDR plural rules
+               $this->parsePluralXML( $CLDRPlural );
+               if ( file_exists( $MWPlural ) ) {
+                       // override or extend.
+                       $this->parsePluralXML( $MWPlural );
+               }
+       }
+
+       private function parsePluralXML( $xmlFile ) {
+               $pluraldoc = new DOMDocument();
+               $pluraldoc->load( $xmlFile );
+               $rulesets = $pluraldoc->getElementsByTagName( "pluralRules" );
+               foreach ( $rulesets as $ruleset ) {
+                       $codes = $ruleset->getAttribute( 'locales' );
+                       $parsedRules = array();
+                       $rules = $ruleset->getElementsByTagName( "pluralRule" );
+                       foreach ( $rules as $rule ) {
+                               $parsedRules[$rule->getAttribute( 'count' )] = $rule->nodeValue;
+                       }
+                       foreach ( explode( ' ', $codes ) as $code ) {
+                               $this->pluralRules[$code] = $parsedRules;
+                       }
+               }
+       }
 
        /**
         * Merge two localisation values, a primary and a fallback, overwriting the
@@ -587,12 +625,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 +643,6 @@ class LocalisationCache {
                if ( is_null( $coreData['fallback'] ) ) {
                        $coreData['fallback'] = $code === 'en' ? false : 'en';
                }
-
                if ( $coreData['fallback'] === false ) {
                        $coreData['fallbackSequence'] = array();
                } else {
@@ -654,7 +691,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 +721,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
+               if ( isset( $this->pluralRules[$code] ) ) {
+                       $allData['pluralRules'] = $this->pluralRules[$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 +964,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 +1008,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 +1080,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 +1098,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 +1221,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 320cdf2..e67c086 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.
@@ -3418,9 +3418,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];
        }
 
        /**
@@ -4189,4 +4189,25 @@ class Language {
        public function getConvRuleTitle() {
                return $this->mConverter->getConvRuleTitle();
        }
+
+       /**
+        * 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->getPluralRules();
+               $form = CLDRPluralRuleEvaluator::evaluate( $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..f420e41
--- /dev/null
@@ -0,0 +1,72 @@
+<?php
+/**
+ * Parse and evaluate a plural rule
+ *
+ * @author Niklas Laxstrom
+ *
+ * @copyright Copyright © 2010-2012, Niklas Laxström
+ * @license http://www.gnu.org/copyleft/gpl.html GNU General Public License 2.0 or later
+ * @file
+ */
+
+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, $rules ) {
+               $formIndex = 0;
+               if ( !$rules ) {
+                       return 0;
+               }
+               foreach ( $rules as $form => $rule ) {
+                       $parsedRule = self::parseCLDRRule( $rule, $number );
+                       // FIXME eval is bad.
+                       if ( eval( "return $parsedRule;" ) ) {
+                               return $formIndex;
+                       }
+                       $formIndex++;
+               }
+               return $formIndex;
+       }\r
+       private static function parseCLDRRule( $rule ) {
+               $rule = preg_replace( '/\bn\b/', '$number', $rule );
+               $rule = preg_replace( '/([^ ]+) mod (\d+)/', 'self::mod(\1,\2)', $rule );
+               $rule = preg_replace( '/([^ ]+) is not (\d+)/' , '\1!=\2', $rule );
+               $rule = preg_replace( '/([^ ]+) is (\d+)/', '\1==\2', $rule );
+               $rule = preg_replace( '/([^ ]+) not in (\d+)\.\.(\d+)/', '!self::in(\1,\2,\3)', $rule );
+               $rule = preg_replace( '/([^ ]+) not within (\d+)\.\.(\d+)/', '!self::within(\1,\2,\3)', $rule );
+               $rule = preg_replace( '/([^ ]+) in (\d+)\.\.(\d+)/', 'self::in(\1,\2,\3)', $rule );
+               $rule = preg_replace( '/([^ ]+) within (\d+)\.\.(\d+)/', 'self::within(\1,\2,\3)', $rule );
+               // AND takes precedence over OR
+               $andrule = '/([^ ]+) and ([^ ]+)/i';
+               while ( preg_match( $andrule, $rule ) ) {
+                       $rule = preg_replace( $andrule, '(\1&&\2)', $rule );
+               }
+               $orrule = '/([^ ]+) or ([^ ]+)/i';
+               while ( preg_match( $orrule, $rule ) ) {
+                       $rule = preg_replace( $orrule, '(\1||\2)', $rule );
+               }
+
+               return $rule;
+       }
+
+       private static function in( $num, $low, $high ) {
+               return is_int( $num ) && $num >= $low && $num <= $high;
+       }
+
+       private static function within( $num, $low, $high ) {
+               return $num >= $low && $num <= $high;
+       }
+
+       private static function mod( $num, $mod ) {
+               if ( is_int( $num ) ) {
+                       return (int) fmod( $num, $mod );
+               }
+               return fmod( $num, $mod );
+       }
+}
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
                );
        }
 }