]> git.ipfire.org Git - thirdparty/babel.git/commitdiff
Update messages' plural rules from CLDR data (#1303)
authorAarni Koskela <akx@iki.fi>
Thu, 30 Jul 2026 12:44:30 +0000 (15:44 +0300)
committerGitHub <noreply@github.com>
Thu, 30 Jul 2026 12:44:30 +0000 (15:44 +0300)
* Update messages' plural rules from CLDR data
* Adjust tests for new plural data

babel/messages/catalog.py
babel/messages/plurals.py
scripts/dump_plurals_dict.py [new file with mode: 0644]
tests/messages/frontend/test_cli.py
tests/messages/frontend/test_init.py
tests/messages/test_catalog.py
tests/messages/test_plurals.py
tests/messages/test_pofile_read.py

index 9a9739a724120a273e1fffd1db3d266a2f16ba48..fc345085c65f3f543ea230c9ea8bde39a33d67e1 100644 (file)
@@ -668,7 +668,7 @@ class Catalog:
         >>> Catalog(locale='en').plural_expr
         '(n != 1)'
         >>> Catalog(locale='ga').plural_expr
-        '(n==1 ? 0 : n==2 ? 1 : n>=3 && n<=6 ? 2 : n>=7 && n<=10 ? 3 : 4)'
+        '(n == 1 ? 0 : n == 2 ? 1 : n >= 3 && n <= 6 ? 2 : n >= 7 && n <= 10 ? 3 : 4)'
         >>> Catalog(locale='ding').plural_expr  # unknown locale
         '(n != 1)'
 
@@ -687,7 +687,7 @@ class Catalog:
         >>> Catalog(locale='en').plural_forms
         'nplurals=2; plural=(n != 1);'
         >>> Catalog(locale='pt_BR').plural_forms
-        'nplurals=2; plural=(n > 1);'
+        'nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;'
 
         :type: `str`"""
         return f"nplurals={self.num_plurals}; plural={self.plural_expr};"
index a66fdfe4156c0aba4deaf928a0e0557f85b48265..b6a2e1d726d9d77600ea133fbc018b2a1e038258 100644 (file)
@@ -17,83 +17,78 @@ from babel.core import Locale, default_locale
 
 LC_CTYPE: str | None = default_locale('LC_CTYPE')
 
-
+# Generated with scripts/dump_plurals_dict.py
 PLURALS: dict[str, tuple[int, str]] = {
-    # Afar
-    # 'aa': (),
-    # Abkhazian
-    # 'ab': (),
-    # Avestan
-    # 'ae': (),
-    # Afrikaans - From Pootle's PO's
+    # Afrikaans
     'af': (2, '(n != 1)'),
     # Akan
-    # 'ak': (),
+    'ak': (2, '(n > 1)'),
     # Amharic
-    # 'am': (),
+    'am': (2, '(n > 1)'),
     # Aragonese
-    # 'an': (),
-    # Arabic - From Pootle's PO's
-    'ar': (6, '(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=0 && n%100<=2 ? 4 : 5)'),
+    'an': (2, '(n != 1)'),
+    # Arabic
+    'ar': (6, '(n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 && n % 100 <= 99 ? 4 : 5)'),
     # Assamese
-    # 'as': (),
-    # Avaric
-    # 'av': (),
-    # Aymara
-    # 'ay': (),
+    'as': (2, '(n > 1)'),
+    # Asu
+    'asa': (2, '(n != 1)'),
+    # Asturian
+    'ast': (2, '(n != 1)'),
     # Azerbaijani
-    # 'az': (),
-    # Bashkir
-    # 'ba': (),
+    'az': (2, '(n != 1)'),
+    # Baluchi
+    'bal': (2, '(n != 1)'),
     # Belarusian
-    'be': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'),
-    # Bulgarian - From Pootle's PO's
+    'be': (3, 'n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : 2'),
+    # Bemba
+    'bem': (2, '(n != 1)'),
+    # Bena
+    'bez': (2, '(n != 1)'),
+    # Bulgarian
     'bg': (2, '(n != 1)'),
-    # Bihari
-    # 'bh': (),
-    # Bislama
-    # 'bi': (),
-    # Bambara
-    # 'bm': (),
-    # Bengali - From Pootle's PO's
-    'bn': (2, '(n != 1)'),
-    # Tibetan - as discussed in private with Andrew West
-    'bo': (1, '0'),
+    # Bhojpuri
+    'bho': (2, '(n > 1)'),
+    # Anii
+    'blo': (3, '(n == 0 ? 0 : n == 1 ? 1 : 2)'),
+    # Bangla
+    'bn': (2, '(n > 1)'),
     # Breton
-    'br': (
-        6,
-        '(n==1 ? 0 : n%10==1 && n%100!=11 && n%100!=71 && n%100!=91 ? 1 : n%10==2 && n%100!=12 && n%100!=72 && '
-        'n%100!=92 ? 2 : (n%10==3 || n%10==4 || n%10==9) && n%100!=13 && n%100!=14 && n%100!=19 && n%100!=73 && '
-        'n%100!=74 && n%100!=79 && n%100!=93 && n%100!=94 && n%100!=99 ? 3 : n%1000000==0 ? 4 : 5)',
-    ),
+    'br': (5, 'n % 10 == 1 && n % 100 != 11 && n % 100 != 71 && n % 100 != 91 ? 0 : n % 10 == 2 && n % 100 != 12 && n % 100 != 72 && n % 100 != 92 ? 1 : ((n % 10 == 3 || n % 10 == 4) || n % 10 == 9) && (n % 100 < 10 || n % 100 > 19) && (n % 100 < 70 || n % 100 > 79) && (n % 100 < 90 || n % 100 > 99) ? 2 : n != 0 && n % 1000000 == 0 ? 3 : 4'),
+    # Bodo
+    'brx': (2, '(n != 1)'),
     # Bosnian
-    'bs': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'),
-    # Catalan - From Pootle's PO's
-    'ca': (2, '(n != 1)'),
+    'bs': (3, 'n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : 2'),
+    # Catalan
+    'ca': (3, '(n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2)'),
     # Chechen
-    # 'ce': (),
-    # Chamorro
-    # 'ch': (),
-    # Corsican
-    # 'co': (),
-    # Cree
-    # 'cr': (),
+    'ce': (2, '(n != 1)'),
+    # Cebuano
+    'ceb': (2, 'n != 1 && n != 2 && n != 3 && (n % 10 == 4 || n % 10 == 6 || n % 10 == 9)'),
+    # Chiga
+    'cgg': (2, '(n != 1)'),
+    # Cherokee
+    'chr': (2, '(n != 1)'),
+    # Central Kurdish
+    'ckb': (2, '(n != 1)'),
     # Czech
-    'cs': (3, '((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2)'),
-    # Church Slavic
-    # 'cu': (),
-    # Chuvash
-    'cv': (1, '0'),
+    'cs': (3, '(n == 1 ? 0 : n >= 2 && n <= 4 ? 1 : 2)'),
+    # Swampy Cree
+    'csw': (2, '(n > 1)'),
     # Welsh
-    'cy': (5, '(n==1 ? 1 : n==2 ? 2 : n==3 ? 3 : n==6 ? 4 : 0)'),
+    'cy': (6, '(n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n == 3 ? 3 : n == 6 ? 4 : 5)'),
     # Danish
     'da': (2, '(n != 1)'),
     # German
     'de': (2, '(n != 1)'),
+    # Dogri
+    'doi': (2, '(n > 1)'),
+    # Lower Sorbian
+    'dsb': (4, 'n % 100 == 1 ? 0 : n % 100 == 2 ? 1 : (n % 100 == 3 || n % 100 == 4) ? 2 : 3'),
     # Divehi
-    # 'dv': (),
-    # Dzongkha
-    'dz': (1, '0'),
+    'dv': (2, '(n != 1)'),
+    # Ewe
+    'ee': (2, '(n != 1)'),
     # Greek
     'el': (2, '(n != 1)'),
     # English
@@ -101,106 +96,287 @@ PLURALS: dict[str, tuple[int, str]] = {
     # Esperanto
     'eo': (2, '(n != 1)'),
     # Spanish
-    'es': (2, '(n != 1)'),
+    'es': (3, '(n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2)'),
     # Estonian
     'et': (2, '(n != 1)'),
-    # Basque - From Pootle's PO's
+    # Basque
     'eu': (2, '(n != 1)'),
-    # Persian - From Pootle's PO's
-    'fa': (1, '0'),
+    # Persian
+    'fa': (2, '(n > 1)'),
+    # Fula
+    'ff': (2, '(n > 1)'),
     # Finnish
     'fi': (2, '(n != 1)'),
+    # Filipino
+    'fil': (2, 'n != 1 && n != 2 && n != 3 && (n % 10 == 4 || n % 10 == 6 || n % 10 == 9)'),
+    # Faroese
+    'fo': (2, '(n != 1)'),
     # French
-    'fr': (2, '(n > 1)'),
-    # Friulian - From Pootle's PO's
-    'fur': (2, '(n > 1)'),
+    'fr': (3, '(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2'),
+    # Friulian
+    'fur': (2, '(n != 1)'),
+    # Western Frisian
+    'fy': (2, '(n != 1)'),
     # Irish
-    'ga': (5, '(n==1 ? 0 : n==2 ? 1 : n>=3 && n<=6 ? 2 : n>=7 && n<=10 ? 3 : 4)'),
-    # Galician - From Pootle's PO's
+    'ga': (5, '(n == 1 ? 0 : n == 2 ? 1 : n >= 3 && n <= 6 ? 2 : n >= 7 && n <= 10 ? 3 : 4)'),
+    # Scottish Gaelic
+    'gd': (4, '(n == 1 || n == 11) ? 0 : (n == 2 || n == 12) ? 1 : (n >= 3 && n <= 10 || n >= 13 && n <= 19) ? 2 : 3'),
+    # Galician
     'gl': (2, '(n != 1)'),
-    # Hausa - From Pootle's PO's
+    # Swiss German
+    'gsw': (2, '(n != 1)'),
+    # Gujarati
+    'gu': (2, '(n > 1)'),
+    # Manx
+    'gv': (4, 'n % 10 == 1 ? 0 : n % 10 == 2 ? 1 : (n % 100 == 0 || n % 100 == 20 || n % 100 == 40 || n % 100 == 60 || n % 100 == 80) ? 2 : 3'),
+    # Hausa
     'ha': (2, '(n != 1)'),
+    # Hawaiian
+    'haw': (2, '(n != 1)'),
     # Hebrew
-    'he': (2, '(n != 1)'),
-    # Hindi - From Pootle's PO's
-    'hi': (2, '(n != 1)'),
+    'he': (3, '(n == 1 ? 0 : n == 2 ? 1 : 2)'),
+    # Hindi
+    'hi': (2, '(n > 1)'),
     # Croatian
-    'hr': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'),
+    'hr': (3, 'n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : 2'),
+    # Upper Sorbian
+    'hsb': (4, 'n % 100 == 1 ? 0 : n % 100 == 2 ? 1 : (n % 100 == 3 || n % 100 == 4) ? 2 : 3'),
     # Hungarian
-    'hu': (1, '0'),
-    # Armenian - From Pootle's PO's
-    'hy': (1, '0'),
-    # Icelandic - From Pootle's PO's
-    'is': (2, '(n%10==1 && n%100!=11 ? 0 : 1)'),
+    'hu': (2, '(n != 1)'),
+    # Armenian
+    'hy': (2, '(n > 1)'),
+    # Interlingua
+    'ia': (2, '(n != 1)'),
+    # Ido
+    'io': (2, '(n != 1)'),
+    # Icelandic
+    'is': (2, '(n % 10 != 1 || n % 100 == 11)'),
     # Italian
-    'it': (2, '(n != 1)'),
-    # Japanese
-    'ja': (1, '0'),
-    # Georgian - From Pootle's PO's
-    'ka': (1, '0'),
-    # Kongo - From Pootle's PO's
-    'kg': (2, '(n != 1)'),
-    # Khmer - From Pootle's PO's
-    'km': (1, '0'),
-    # Korean
-    'ko': (1, '0'),
-    # Kurdish - From Pootle's PO's
+    'it': (3, '(n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2)'),
+    # Inuktitut
+    'iu': (3, '(n == 1 ? 0 : n == 2 ? 1 : 2)'),
+    # Ngomba
+    'jgo': (2, '(n != 1)'),
+    # Machame
+    'jmc': (2, '(n != 1)'),
+    # Georgian
+    'ka': (2, '(n != 1)'),
+    # Kabyle
+    'kab': (2, '(n > 1)'),
+    # Jju
+    'kaj': (2, '(n != 1)'),
+    # Tyap
+    'kcg': (2, '(n != 1)'),
+    # Kazakh
+    'kk': (2, '(n != 1)'),
+    # Kako
+    'kkj': (2, '(n != 1)'),
+    # Kalaallisut
+    'kl': (2, '(n != 1)'),
+    # Kannada
+    'kn': (2, '(n > 1)'),
+    # Kashmiri
+    'ks': (2, '(n != 1)'),
+    # Shambala
+    'ksb': (2, '(n != 1)'),
+    # Colognian
+    'ksh': (3, '(n == 0 ? 0 : n == 1 ? 1 : 2)'),
+    # Kurdish
     'ku': (2, '(n != 1)'),
-    # Lao - Another member of the Tai language family, like Thai.
-    'lo': (1, '0'),
+    # Cornish
+    'kw': (6, 'n == 0 ? 0 : n == 1 ? 1 : (n % 100 == 2 || n % 100 == 22 || n % 100 == 42 || n % 100 == 62 || n % 100 == 82) || n % 1000 == 0 && (n % 100000 >= 1000 && n % 100000 <= 20000 || n % 100000 == 40000 || n % 100000 == 60000 || n % 100000 == 80000) || n != 0 && n % 1000000 == 100000 ? 2 : (n % 100 == 3 || n % 100 == 23 || n % 100 == 43 || n % 100 == 63 || n % 100 == 83) ? 3 : n != 1 && (n % 100 == 1 || n % 100 == 21 || n % 100 == 41 || n % 100 == 61 || n % 100 == 81) ? 4 : 5'),
+    # Kyrgyz
+    'ky': (2, '(n != 1)'),
+    # Langi
+    'lag': (3, '(n == 0 ? 0 : n == 1 ? 1 : 2)'),
+    # Luxembourgish
+    'lb': (2, '(n != 1)'),
+    # Ganda
+    'lg': (2, '(n != 1)'),
+    # Ligurian
+    'lij': (2, '(n != 1)'),
+    # Dolomitic Ladin
+    'lld': (3, '(n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2)'),
+    # Lingala
+    'ln': (2, '(n > 1)'),
     # Lithuanian
-    'lt': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2)'),
+    'lt': (3, 'n % 10 == 1 && (n % 100 < 11 || n % 100 > 19) ? 0 : n % 10 >= 2 && n % 10 <= 9 && (n % 100 < 11 || n % 100 > 19) ? 1 : 2'),
     # Latvian
-    'lv': (3, '(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2)'),
-    # Maltese - From Pootle's PO's
-    'mt': (4, '(n==1 ? 0 : n==0 || ( n%100>=1 && n%100<=10) ? 1 : (n%100>10 && n%100<20 ) ? 2 : 3)'),
+    'lv': (3, '(n % 10 == 0 || n % 100 >= 11 && n % 100 <= 19 ? 0 : n % 10 == 1 && n % 100 != 11 ? 1 : 2)'),
+    # Masai
+    'mas': (2, '(n != 1)'),
+    # Malagasy
+    'mg': (2, '(n > 1)'),
+    # Metaʼ
+    'mgo': (2, '(n != 1)'),
+    # Macedonian
+    'mk': (2, '(n % 10 != 1 || n % 100 == 11)'),
+    # Malayalam
+    'ml': (2, '(n != 1)'),
+    # Mongolian
+    'mn': (2, '(n != 1)'),
+    # Marathi
+    'mr': (2, '(n != 1)'),
+    # Maltese
+    'mt': (5, '(n == 1 ? 0 : n == 2 ? 1 : n == 0 || n % 100 >= 3 && n % 100 <= 10 ? 2 : n % 100 >= 11 && n % 100 <= 19 ? 3 : 4)'),
+    # Nama
+    'naq': (3, '(n == 1 ? 0 : n == 2 ? 1 : 2)'),
     # Norwegian Bokmål
     'nb': (2, '(n != 1)'),
+    # North Ndebele
+    'nd': (2, '(n != 1)'),
+    # Nepali
+    'ne': (2, '(n != 1)'),
     # Dutch
     'nl': (2, '(n != 1)'),
     # Norwegian Nynorsk
     'nn': (2, '(n != 1)'),
+    # Ngiemboon
+    'nnh': (2, '(n != 1)'),
     # Norwegian
     'no': (2, '(n != 1)'),
-    # Punjabi - From Pootle's PO's
-    'pa': (2, '(n != 1)'),
+    # South Ndebele
+    'nr': (2, '(n != 1)'),
+    # Northern Sotho
+    'nso': (2, '(n > 1)'),
+    # Nyanja
+    'ny': (2, '(n != 1)'),
+    # Nyankole
+    'nyn': (2, '(n != 1)'),
+    # Oromo
+    'om': (2, '(n != 1)'),
+    # Odia
+    'or': (2, '(n != 1)'),
+    # Ossetic
+    'os': (2, '(n != 1)'),
+    # Punjabi
+    'pa': (2, '(n > 1)'),
+    # Papiamento
+    'pap': (2, '(n != 1)'),
+    # Nigerian Pidgin
+    'pcm': (2, '(n > 1)'),
     # Polish
-    'pl': (3, '(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'),
+    'pl': (3, 'n == 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : 2'),
+    # Prussian
+    'prg': (3, '(n % 10 == 0 || n % 100 >= 11 && n % 100 <= 19 ? 0 : n % 10 == 1 && n % 100 != 11 ? 1 : 2)'),
+    # Pashto
+    'ps': (2, '(n != 1)'),
     # Portuguese
-    'pt': (2, '(n != 1)'),
-    # Brazilian
-    'pt_BR': (2, '(n > 1)'),
-    # Romanian - From Pootle's PO's
-    'ro': (3, '(n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2)'),
+    'pt': (3, '(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2'),
+    # European Portuguese
+    'pt_PT': (3, '(n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2)'),
+    # Romansh
+    'rm': (2, '(n != 1)'),
+    # Romanian
+    'ro': (3, '(n == 1 ? 0 : n == 0 || n != 1 && n % 100 >= 1 && n % 100 <= 19 ? 1 : 2)'),
+    # Rombo
+    'rof': (2, '(n != 1)'),
     # Russian
-    'ru': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'),
+    'ru': (3, 'n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : 2'),
+    # Rwa
+    'rwk': (2, '(n != 1)'),
+    # Samburu
+    'saq': (2, '(n != 1)'),
+    # Santali
+    'sat': (3, '(n == 1 ? 0 : n == 2 ? 1 : 2)'),
+    # Sardinian
+    'sc': (2, '(n != 1)'),
+    # Sicilian
+    'scn': (3, '(n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2)'),
+    # Sindhi
+    'sd': (2, '(n != 1)'),
+    # Southern Kurdish
+    'sdh': (2, '(n != 1)'),
+    # Northern Sami
+    'se': (3, '(n == 1 ? 0 : n == 2 ? 1 : 2)'),
+    # Sena
+    'seh': (2, '(n != 1)'),
+    # Tachelhit
+    'shi': (3, '(n == 0 || n == 1 ? 0 : n >= 2 && n <= 10 ? 1 : 2)'),
+    # Sinhala
+    'si': (2, '(n > 1)'),
     # Slovak
-    'sk': (3, '((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2)'),
+    'sk': (3, '(n == 1 ? 0 : n >= 2 && n <= 4 ? 1 : 2)'),
     # Slovenian
-    'sl': (4, '(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3)'),
-    # Serbian - From Pootle's PO's
-    'sr': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'),
-    # Southern Sotho - From Pootle's PO's
+    'sl': (4, 'n % 100 == 1 ? 0 : n % 100 == 2 ? 1 : (n % 100 == 3 || n % 100 == 4) ? 2 : 3'),
+    # Southern Sami
+    'sma': (3, '(n == 1 ? 0 : n == 2 ? 1 : 2)'),
+    # Lule Sami
+    'smj': (3, '(n == 1 ? 0 : n == 2 ? 1 : 2)'),
+    # Inari Sami
+    'smn': (3, '(n == 1 ? 0 : n == 2 ? 1 : 2)'),
+    # Skolt Sami
+    'sms': (3, '(n == 1 ? 0 : n == 2 ? 1 : 2)'),
+    # Shona
+    'sn': (2, '(n != 1)'),
+    # Somali
+    'so': (2, '(n != 1)'),
+    # Albanian
+    'sq': (2, '(n != 1)'),
+    # Serbian
+    'sr': (3, 'n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : 2'),
+    # Swati
+    'ss': (2, '(n != 1)'),
+    # Saho
+    'ssy': (2, '(n != 1)'),
+    # Southern Sotho
     'st': (2, '(n != 1)'),
     # Swedish
     'sv': (2, '(n != 1)'),
-    # Thai
-    'th': (1, '0'),
+    # Swahili
+    'sw': (2, '(n != 1)'),
+    # Syriac
+    'syr': (2, '(n != 1)'),
+    # Tamil
+    'ta': (2, '(n != 1)'),
+    # Telugu
+    'te': (2, '(n != 1)'),
+    # Teso
+    'teo': (2, '(n != 1)'),
+    # Tigrinya
+    'ti': (2, '(n > 1)'),
+    # Tigre
+    'tig': (2, '(n != 1)'),
+    # Turkmen
+    'tk': (2, '(n != 1)'),
+    # Tswana
+    'tn': (2, '(n != 1)'),
     # Turkish
-    'tr': (1, '0'),
+    'tr': (2, '(n != 1)'),
+    # Tsonga
+    'ts': (2, '(n != 1)'),
+    # Central Atlas Tamazight
+    'tzm': (2, 'n >= 2 && (n < 11 || n > 99)'),
+    # Uyghur
+    'ug': (2, '(n != 1)'),
     # Ukrainian
-    'uk': (3, '(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'),
-    # Venda - From Pootle's PO's
+    'uk': (3, 'n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : 2'),
+    # Urdu
+    'ur': (2, '(n != 1)'),
+    # Uzbek
+    'uz': (2, '(n != 1)'),
+    # Venda
     've': (2, '(n != 1)'),
-    # Vietnamese - From Pootle's PO's
-    'vi': (1, '0'),
-    # Xhosa - From Pootle's PO's
+    # Venetian
+    'vec': (3, '(n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2)'),
+    # Volapük
+    'vo': (2, '(n != 1)'),
+    # Vunjo
+    'vun': (2, '(n != 1)'),
+    # Walloon
+    'wa': (2, '(n > 1)'),
+    # Walser
+    'wae': (2, '(n != 1)'),
+    # Xhosa
     'xh': (2, '(n != 1)'),
-    # Chinese - From Pootle's PO's (modified)
-    'zh': (1, '0'),
+    # Soga
+    'xog': (2, '(n != 1)'),
+    # Yiddish
+    'yi': (2, '(n != 1)'),
+    # Zulu
+    'zu': (2, '(n > 1)'),
 }  # fmt: skip
 
-
 DEFAULT_PLURAL: tuple[int, str] = (2, '(n != 1)')
 
 
@@ -238,23 +414,23 @@ def get_plural(locale: Locale | str | None = None) -> _PluralTuple:
     >>> get_plural(locale='en')
     (2, '(n != 1)')
     >>> get_plural(locale='ga')
-    (5, '(n==1 ? 0 : n==2 ? 1 : n>=3 && n<=6 ? 2 : n>=7 && n<=10 ? 3 : 4)')
+    (5, '(n == 1 ? 0 : n == 2 ? 1 : n >= 3 && n <= 6 ? 2 : n >= 7 && n <= 10 ? 3 : 4)')
 
     The object returned is a special tuple with additional members:
 
     >>> tup = get_plural("ja")
     >>> tup.num_plurals
-    1
+    2
     >>> tup.plural_expr
-    '0'
+    '(n != 1)'
     >>> tup.plural_forms
-    'nplurals=1; plural=0;'
+    'nplurals=2; plural=(n != 1);'
 
     Converting the tuple into a string prints the plural forms for a
     gettext catalog:
 
     >>> str(tup)
-    'nplurals=1; plural=0;'
+    'nplurals=2; plural=(n != 1);'
     """
     locale = Locale.parse(locale or LC_CTYPE)
     try:
diff --git a/scripts/dump_plurals_dict.py b/scripts/dump_plurals_dict.py
new file mode 100644 (file)
index 0000000..8ac0a5e
--- /dev/null
@@ -0,0 +1,49 @@
+import json
+from urllib.request import urlopen
+
+from babel.core import get_cldr_version
+from babel.localedata import locale_identifiers
+
+
+def write_dict(data):
+    ids = set(locale_identifiers())
+
+    print("PLURALS: dict[str, tuple[int, str]] = {")
+    for key, info in sorted(data.items()):
+        if key not in ids:
+            continue
+
+        n = info['plurals']
+        if n <= 1:
+            continue
+        formula = info['formulas']['standard']
+        if not formula.isdigit() and "(" not in formula:
+            formula = f"({formula})"
+        tup = (n, formula)
+        # We could be tempted to compare to `DEFAULT_PLURAL`
+        # here and emit a reference to it, but CPython is
+        # smart enough to do constant folding on constant tuples:
+        #   >>> p.PLURALS["ak"] is p.PLURALS["am"]
+        #   True
+        # so it actually becomes cheaper to emit these tuples,
+        # and avoid loading global values when the dict is
+        # being constructed.
+        # It also doesn't feel right to not emit DEFAULT_PLURAL
+        # valued items, because then we'd spend more time doing
+        # the fallback logic in `get_plural`.
+        print(f"    # {info['name']}")
+        print(f"    {key!r}: {tup!r},")
+    print("}  # fmt: skip")
+
+def main() -> None:
+    # The PHP-Gettext project has more concise/optimal gettext conversions of the CLDR rules
+    # than what our `_GettextCompiler` (correct, but not optimal) generates, so let's use those.
+
+    with urlopen(f"https://php-gettext.github.io/Languages/data/versions/{get_cldr_version()}.min.json") as fp:
+        data = json.loads(fp.read().decode('utf-8'))
+
+    write_dict(data)
+
+
+if __name__ == '__main__':
+    main()
index 200632ec3505d2123c315fe446a7d55564ac9c01..8dcbd7cfe40331617f2b178bb3ce28d2a5d6e252 100644 (file)
@@ -341,7 +341,7 @@ msgstr ""
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language: ja_JP\n"
 "Language-Team: ja_JP <LL@li.org>\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
@@ -357,6 +357,7 @@ msgstr ""
 msgid "foobar"
 msgid_plural "foobars"
 msgstr[0] ""
+msgstr[1] ""
 
 """
     with open(po_file) as f:
@@ -390,8 +391,8 @@ msgstr ""
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language: lv_LV\n"
 "Language-Team: lv_LV <LL@li.org>\n"
-"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 :"
-" 2);\n"
+"Plural-Forms: nplurals=3; plural=(n % 10 == 0 || n % 100 >= 11 && n % 100"
+" <= 19 ? 0 : n % 10 == 1 && n % 100 != 11 ? 1 : 2);\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
index e69e5cce374ad245854dc0da7ea37ac05b933d27..867bc79777d3b958314fb9f52e7a80bfff6468bb 100644 (file)
@@ -182,8 +182,8 @@ msgstr ""
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language: lv_LV\n"
 "Language-Team: lv_LV <LL@li.org>\n"
-"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 :"
-" 2);\n"
+"Plural-Forms: nplurals=3; plural=(n % 10 == 0 || n % 100 >= 11 && n % 100"
+" <= 19 ? 0 : n % 10 == 1 && n % 100 != 11 ? 1 : 2);\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
@@ -233,7 +233,7 @@ msgstr ""
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language: ja_JP\n"
 "Language-Team: ja_JP <LL@li.org>\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=utf-8\n"
 "Content-Transfer-Encoding: 8bit\n"
@@ -249,6 +249,7 @@ msgstr ""
 msgid "foobar"
 msgid_plural "foobars"
 msgstr[0] ""
+msgstr[1] ""
 
 """
     with open(get_po_file_path('ja_JP')) as f:
index 191a2a4981609e1b252c18a7ec2a1a5883492171..4a60208c838f95b336ecc1de5ad8de50105e12df 100644 (file)
@@ -481,14 +481,14 @@ def test_catalog_num_plurals():
 def test_catalog_plural_expr():
     assert catalog.Catalog(locale='en').plural_expr == '(n != 1)'
     assert (catalog.Catalog(locale='ga').plural_expr
-            == '(n==1 ? 0 : n==2 ? 1 : n>=3 && n<=6 ? 2 : n>=7 && n<=10 ? 3 : 4)')
+            == '(n == 1 ? 0 : n == 2 ? 1 : n >= 3 && n <= 6 ? 2 : n >= 7 && n <= 10 ? 3 : 4)')
 
 
 def test_catalog_plural_forms():
     assert (catalog.Catalog(locale='en').plural_forms
             == 'nplurals=2; plural=(n != 1);')
     assert (catalog.Catalog(locale='pt_BR').plural_forms
-            == 'nplurals=2; plural=(n > 1);')
+            == 'nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;')
 
 
 def test_catalog_setitem():
index cd5a2fbaf5ae57bf19ad3c79de08d81654e0700f..3528acab945f90ead0af0e6f6d7ea34feddc35eb 100644 (file)
@@ -18,18 +18,19 @@ from babel.messages import plurals
 @pytest.mark.parametrize(('locale', 'num_plurals', 'plural_expr'), [
     (Locale('en'), 2, '(n != 1)'),
     (Locale('en', 'US'), 2, '(n != 1)'),
-    (Locale('zh'), 1, '0'),
-    (Locale('zh', script='Hans'), 1, '0'),
-    (Locale('zh', script='Hant'), 1, '0'),
-    (Locale('zh', 'CN', 'Hans'), 1, '0'),
-    (Locale('zh', 'TW', 'Hant'), 1, '0'),
+    (Locale('si'), 2, '(n > 1)'),
+    (Locale('zh'), 2, '(n != 1)'),
+    (Locale('zh', script='Hans'),  2, '(n != 1)'),
+    (Locale('zh', script='Hant'), 2, '(n != 1)'),
+    (Locale('zh', 'CN', 'Hans'), 2, '(n != 1)'),
+    (Locale('zh', 'TW', 'Hant'), 2, '(n != 1)'),
 ])
 def test_get_plural_selection(locale, num_plurals, plural_expr):
     assert plurals.get_plural(locale) == (num_plurals, plural_expr)
 
 
 def test_get_plural_accepts_strings():
-    assert plurals.get_plural(locale='ga') == (5, '(n==1 ? 0 : n==2 ? 1 : n>=3 && n<=6 ? 2 : n>=7 && n<=10 ? 3 : 4)')
+    assert plurals.get_plural(locale='ga') == (5, '(n == 1 ? 0 : n == 2 ? 1 : n >= 3 && n <= 6 ? 2 : n >= 7 && n <= 10 ? 3 : 4)')
 
 
 def test_get_plural_falls_back_to_default():
@@ -39,25 +40,19 @@ def test_get_plural_falls_back_to_default():
 def test_get_plural():
     # See https://localization-guide.readthedocs.io/en/latest/l10n/pluralforms.html for more details.
     assert plurals.get_plural(locale='en') == (2, '(n != 1)')
-    assert plurals.get_plural(locale='ga') == (5, '(n==1 ? 0 : n==2 ? 1 : n>=3 && n<=6 ? 2 : n>=7 && n<=10 ? 3 : 4)')
-
-    plural_ja = plurals.get_plural("ja")
-    assert str(plural_ja) == 'nplurals=1; plural=0;'
-    assert plural_ja.num_plurals == 1
-    assert plural_ja.plural_expr == '0'
-    assert plural_ja.plural_forms == 'nplurals=1; plural=0;'
+    assert plurals.get_plural(locale='ga') == (5, '(n == 1 ? 0 : n == 2 ? 1 : n >= 3 && n <= 6 ? 2 : n >= 7 && n <= 10 ? 3 : 4)')
 
     plural_en_US = plurals.get_plural('en_US')
-    assert str(plural_en_US) == 'nplurals=2; plural=(n != 1);'
+    assert str(plural_en_US) == plural_en_US.plural_forms == 'nplurals=2; plural=(n != 1);'
     assert plural_en_US.num_plurals == 2
     assert plural_en_US.plural_expr == '(n != 1)'
 
     plural_fr_FR = plurals.get_plural('fr_FR')
-    assert str(plural_fr_FR) == 'nplurals=2; plural=(n > 1);'
-    assert plural_fr_FR.num_plurals == 2
-    assert plural_fr_FR.plural_expr == '(n > 1)'
+    assert str(plural_fr_FR) == plural_fr_FR.plural_forms == 'nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;'
+    assert plural_fr_FR.num_plurals == 3
+    assert plural_fr_FR.plural_expr == '(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2'
 
     plural_pl_PL = plurals.get_plural('pl_PL')
-    assert str(plural_pl_PL) == 'nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);'
+    assert str(plural_pl_PL) == 'nplurals=3; plural=n == 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : 2;'
     assert plural_pl_PL.num_plurals == 3
-    assert plural_pl_PL.plural_expr == '(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)'
+    assert plural_pl_PL.plural_expr == 'n == 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : 2'
index d17f5d4aff925115d2a080a6f5843d7b568a5924..fc913b407a7d88aaf45575576d40635efa342730 100644 (file)
@@ -406,17 +406,6 @@ msgstr "Bahr"
     assert out_buf.getvalue().strip() == buf.getvalue().strip(), out_buf.getvalue()
 
 
-def test_single_plural_form():
-    buf = StringIO(r'''msgid "foo"
-msgid_plural "foos"
-msgstr[0] "Voh"''')
-    catalog = pofile.read_po(buf, locale='ja_JP')
-    assert len(catalog) == 1
-    assert catalog.num_plurals == 1
-    message = catalog['foo']
-    assert len(message.string) == 1
-
-
 def test_singular_plural_form():
     buf = StringIO(r'''msgid "foo"
 msgid_plural "foos"