From ac03cd2ac41bed58dfc0667ae72700723301cd15 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 30 Jul 2026 14:51:17 +0300 Subject: [PATCH 1/2] Update messages' plural rules from CLDR data Fixes #495 Fixes #711 Fixes #816 Fixes #817 --- babel/messages/plurals.py | 416 +++++++++++++++++++++++++---------- scripts/dump_plurals_dict.py | 49 +++++ 2 files changed, 345 insertions(+), 120 deletions(-) create mode 100644 scripts/dump_plurals_dict.py diff --git a/babel/messages/plurals.py b/babel/messages/plurals.py index a66fdfe41..99534f5a8 100644 --- a/babel/messages/plurals.py +++ b/babel/messages/plurals.py @@ -17,83 +17,78 @@ 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 @@ # 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)') diff --git a/scripts/dump_plurals_dict.py b/scripts/dump_plurals_dict.py new file mode 100644 index 000000000..8ac0a5e23 --- /dev/null +++ b/scripts/dump_plurals_dict.py @@ -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() From a70e5df2a487de7420c5aea198d80549b96e9449 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Thu, 30 Jul 2026 15:24:31 +0300 Subject: [PATCH 2/2] Adjust tests for new plural data --- babel/messages/catalog.py | 4 ++-- babel/messages/plurals.py | 10 ++++----- tests/messages/frontend/test_cli.py | 7 +++--- tests/messages/frontend/test_init.py | 7 +++--- tests/messages/test_catalog.py | 4 ++-- tests/messages/test_plurals.py | 33 ++++++++++++---------------- tests/messages/test_pofile_read.py | 11 ---------- 7 files changed, 31 insertions(+), 45 deletions(-) diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index 9a9739a72..fc345085c 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -668,7 +668,7 @@ def plural_expr(self) -> str: >>> 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 @@ def plural_forms(self) -> str: >>> 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};" diff --git a/babel/messages/plurals.py b/babel/messages/plurals.py index 99534f5a8..b6a2e1d72 100644 --- a/babel/messages/plurals.py +++ b/babel/messages/plurals.py @@ -414,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/tests/messages/frontend/test_cli.py b/tests/messages/frontend/test_cli.py index 200632ec3..8dcbd7cfe 100644 --- a/tests/messages/frontend/test_cli.py +++ b/tests/messages/frontend/test_cli.py @@ -341,7 +341,7 @@ def test_init_singular_plural_forms(cli): "Last-Translator: FULL NAME \n" "Language: ja_JP\n" "Language-Team: ja_JP \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 @@ def test_init_singular_plural_forms(cli): msgid "foobar" msgid_plural "foobars" msgstr[0] "" +msgstr[1] "" """ with open(po_file) as f: @@ -390,8 +391,8 @@ def test_init_more_than_2_plural_forms(cli): "Last-Translator: FULL NAME \n" "Language: lv_LV\n" "Language-Team: lv_LV \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" diff --git a/tests/messages/frontend/test_init.py b/tests/messages/frontend/test_init.py index e69e5cce3..867bc7977 100644 --- a/tests/messages/frontend/test_init.py +++ b/tests/messages/frontend/test_init.py @@ -182,8 +182,8 @@ def test_correct_init_more_than_2_plurals(init_cmd): "Last-Translator: FULL NAME \n" "Language: lv_LV\n" "Language-Team: lv_LV \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 @@ def test_correct_init_singular_plural_forms(init_cmd): "Last-Translator: FULL NAME \n" "Language: ja_JP\n" "Language-Team: ja_JP \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 @@ def test_correct_init_singular_plural_forms(init_cmd): msgid "foobar" msgid_plural "foobars" msgstr[0] "" +msgstr[1] "" """ with open(get_po_file_path('ja_JP')) as f: diff --git a/tests/messages/test_catalog.py b/tests/messages/test_catalog.py index 191a2a498..4a60208c8 100644 --- a/tests/messages/test_catalog.py +++ b/tests/messages/test_catalog.py @@ -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(): diff --git a/tests/messages/test_plurals.py b/tests/messages/test_plurals.py index cd5a2fbaf..3528acab9 100644 --- a/tests/messages/test_plurals.py +++ b/tests/messages/test_plurals.py @@ -18,18 +18,19 @@ @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' diff --git a/tests/messages/test_pofile_read.py b/tests/messages/test_pofile_read.py index d17f5d4af..fc913b407 100644 --- a/tests/messages/test_pofile_read.py +++ b/tests/messages/test_pofile_read.py @@ -406,17 +406,6 @@ def test_with_context_two(): 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"