diff --git a/babel/messages/plurals.py b/babel/messages/plurals.py index b6a2e1d72..1103798ff 100644 --- a/babel/messages/plurals.py +++ b/babel/messages/plurals.py @@ -51,8 +51,12 @@ 'bho': (2, '(n > 1)'), # Anii 'blo': (3, '(n == 0 ? 0 : n == 1 ? 1 : 2)'), + # Bambara + 'bm': (1, '0'), # Bangla 'bn': (2, '(n > 1)'), + # Tibetan + 'bo': (1, '0'), # Breton '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 @@ -75,6 +79,8 @@ 'cs': (3, '(n == 1 ? 0 : n >= 2 && n <= 4 ? 1 : 2)'), # Swampy Cree 'csw': (2, '(n > 1)'), + # Chuvash + 'cv': (3, '(n == 0 ? 0 : n == 1 ? 1 : 2)'), # Welsh 'cy': (6, '(n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n == 3 ? 3 : n == 6 ? 4 : 5)'), # Danish @@ -87,6 +93,8 @@ 'dsb': (4, 'n % 100 == 1 ? 0 : n % 100 == 2 ? 1 : (n % 100 == 3 || n % 100 == 4) ? 2 : 3'), # Divehi 'dv': (2, '(n != 1)'), + # Dzongkha + 'dz': (1, '0'), # Ewe 'ee': (2, '(n != 1)'), # Greek @@ -137,6 +145,8 @@ 'he': (3, '(n == 1 ? 0 : n == 2 ? 1 : 2)'), # Hindi 'hi': (2, '(n > 1)'), + # Hmong Njua + 'hnj': (1, '0'), # Croatian '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 @@ -147,6 +157,14 @@ 'hy': (2, '(n > 1)'), # Interlingua 'ia': (2, '(n != 1)'), + # Indonesian + 'id': (1, '0'), + # Interlingue + 'ie': (2, '(n != 1)'), + # Igbo + 'ig': (1, '0'), + # Sichuan Yi + 'ii': (1, '0'), # Ido 'io': (2, '(n != 1)'), # Icelandic @@ -155,10 +173,16 @@ 'it': (3, '(n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2)'), # Inuktitut 'iu': (3, '(n == 1 ? 0 : n == 2 ? 1 : 2)'), + # Japanese + 'ja': (1, '0'), + # Lojban + 'jbo': (1, '0'), # Ngomba 'jgo': (2, '(n != 1)'), # Machame 'jmc': (2, '(n != 1)'), + # Javanese + 'jv': (1, '0'), # Georgian 'ka': (2, '(n != 1)'), # Kabyle @@ -167,14 +191,24 @@ 'kaj': (2, '(n != 1)'), # Tyap 'kcg': (2, '(n != 1)'), + # Makonde + 'kde': (1, '0'), + # Kabuverdianu + 'kea': (1, '0'), # Kazakh 'kk': (2, '(n != 1)'), # Kako 'kkj': (2, '(n != 1)'), # Kalaallisut 'kl': (2, '(n != 1)'), + # Khmer + 'km': (1, '0'), # Kannada 'kn': (2, '(n > 1)'), + # Korean + 'ko': (1, '0'), + # Konkani + 'kok': (2, '(n > 1)'), # Kashmiri 'ks': (2, '(n != 1)'), # Shambala @@ -195,10 +229,14 @@ 'lg': (2, '(n != 1)'), # Ligurian 'lij': (2, '(n != 1)'), + # Lakota + 'lkt': (1, '0'), # Dolomitic Ladin 'lld': (3, '(n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2)'), # Lingala 'ln': (2, '(n > 1)'), + # Lao + 'lo': (1, '0'), # Lithuanian '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 @@ -217,8 +255,12 @@ 'mn': (2, '(n != 1)'), # Marathi 'mr': (2, '(n != 1)'), + # Malay + 'ms': (1, '0'), # 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)'), + # Burmese + 'my': (1, '0'), # Nama 'naq': (3, '(n == 1 ? 0 : n == 2 ? 1 : 2)'), # Norwegian Bokmål @@ -235,6 +277,8 @@ 'nnh': (2, '(n != 1)'), # Norwegian 'no': (2, '(n != 1)'), + # N’Ko + 'nqo': (1, '0'), # South Ndebele 'nr': (2, '(n != 1)'), # Northern Sotho @@ -249,6 +293,8 @@ 'or': (2, '(n != 1)'), # Ossetic 'os': (2, '(n != 1)'), + # Osage + 'osa': (1, '0'), # Punjabi 'pa': (2, '(n > 1)'), # Papiamento @@ -275,6 +321,8 @@ '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)'), + # Yakut + 'sah': (1, '0'), # Samburu 'saq': (2, '(n != 1)'), # Santali @@ -291,6 +339,12 @@ 'se': (3, '(n == 1 ? 0 : n == 2 ? 1 : 2)'), # Sena 'seh': (2, '(n != 1)'), + # Koyraboro Senni + 'ses': (1, '0'), + # Sango + 'sg': (1, '0'), + # Samogitian + 'sgs': (4, 'n % 10 == 1 && n % 100 != 11 ? 0 : n == 2 ? 1 : n != 2 && n % 10 >= 2 && n % 10 <= 9 && (n % 100 < 11 || n % 100 > 19) ? 2 : 3'), # Tachelhit 'shi': (3, '(n == 0 || n == 1 ? 0 : n >= 2 && n <= 10 ? 1 : 2)'), # Sinhala @@ -321,6 +375,8 @@ 'ssy': (2, '(n != 1)'), # Southern Sotho 'st': (2, '(n != 1)'), + # Sundanese + 'su': (1, '0'), # Swedish 'sv': (2, '(n != 1)'), # Swahili @@ -333,6 +389,8 @@ 'te': (2, '(n != 1)'), # Teso 'teo': (2, '(n != 1)'), + # Thai + 'th': (1, '0'), # Tigrinya 'ti': (2, '(n > 1)'), # Tigre @@ -341,6 +399,10 @@ 'tk': (2, '(n != 1)'), # Tswana 'tn': (2, '(n != 1)'), + # Tongan + 'to': (1, '0'), + # Tok Pisin + 'tpi': (1, '0'), # Turkish 'tr': (2, '(n != 1)'), # Tsonga @@ -359,6 +421,8 @@ 've': (2, '(n != 1)'), # Venetian 'vec': (3, '(n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2)'), + # Vietnamese + 'vi': (1, '0'), # Volapük 'vo': (2, '(n != 1)'), # Vunjo @@ -367,12 +431,20 @@ 'wa': (2, '(n > 1)'), # Walser 'wae': (2, '(n != 1)'), + # Wolof + 'wo': (1, '0'), # Xhosa 'xh': (2, '(n != 1)'), # Soga 'xog': (2, '(n != 1)'), # Yiddish 'yi': (2, '(n != 1)'), + # Yoruba + 'yo': (1, '0'), + # Cantonese + 'yue': (1, '0'), + # Chinese + 'zh': (1, '0'), # Zulu 'zu': (2, '(n > 1)'), } # fmt: skip @@ -420,17 +492,17 @@ def get_plural(locale: Locale | str | None = None) -> _PluralTuple: >>> tup = get_plural("ja") >>> tup.num_plurals - 2 + 1 >>> tup.plural_expr - '(n != 1)' + '0' >>> tup.plural_forms - 'nplurals=2; plural=(n != 1);' + 'nplurals=1; plural=0;' Converting the tuple into a string prints the plural forms for a gettext catalog: >>> str(tup) - 'nplurals=2; plural=(n != 1);' + 'nplurals=1; plural=0;' """ locale = Locale.parse(locale or LC_CTYPE) try: diff --git a/scripts/dump_plurals_dict.py b/scripts/dump_plurals_dict.py index 8ac0a5e23..51081f71d 100644 --- a/scripts/dump_plurals_dict.py +++ b/scripts/dump_plurals_dict.py @@ -7,6 +7,7 @@ def write_dict(data): ids = set(locale_identifiers()) + ids |= {l.partition("_")[0] for l in ids} print("PLURALS: dict[str, tuple[int, str]] = {") for key, info in sorted(data.items()): @@ -14,8 +15,6 @@ def write_dict(data): continue n = info['plurals'] - if n <= 1: - continue formula = info['formulas']['standard'] if not formula.isdigit() and "(" not in formula: formula = f"({formula})" @@ -39,7 +38,10 @@ 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: + version = get_cldr_version() + if version == "48": # get_cldr_version only emits the major version + version = "48.1" + with urlopen(f"https://php-gettext.github.io/Languages/data/versions/{version}.json") as fp: data = json.loads(fp.read().decode('utf-8')) write_dict(data) diff --git a/tests/messages/frontend/test_cli.py b/tests/messages/frontend/test_cli.py index 8dcbd7cfe..480157189 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=2; plural=(n != 1);\n" +"Plural-Forms: nplurals=1; plural=0;\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -357,7 +357,6 @@ def test_init_singular_plural_forms(cli): msgid "foobar" msgid_plural "foobars" msgstr[0] "" -msgstr[1] "" """ with open(po_file) as f: diff --git a/tests/messages/frontend/test_init.py b/tests/messages/frontend/test_init.py index 867bc7977..f2db64448 100644 --- a/tests/messages/frontend/test_init.py +++ b/tests/messages/frontend/test_init.py @@ -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=2; plural=(n != 1);\n" +"Plural-Forms: nplurals=1; plural=0;\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -249,7 +249,6 @@ 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_plurals.py b/tests/messages/test_plurals.py index 3528acab9..8c071f8ca 100644 --- a/tests/messages/test_plurals.py +++ b/tests/messages/test_plurals.py @@ -19,11 +19,11 @@ (Locale('en'), 2, '(n != 1)'), (Locale('en', 'US'), 2, '(n != 1)'), (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)'), + (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'), ]) def test_get_plural_selection(locale, num_plurals, plural_expr): assert plurals.get_plural(locale) == (num_plurals, plural_expr) @@ -34,7 +34,7 @@ def test_get_plural_accepts_strings(): def test_get_plural_falls_back_to_default(): - assert plurals.get_plural('ii') == (2, '(n != 1)') + assert plurals.get_plural('ba') == (2, '(n != 1)') def test_get_plural(): @@ -42,6 +42,12 @@ def test_get_plural(): 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;' + plural_en_US = plurals.get_plural('en_US') assert str(plural_en_US) == plural_en_US.plural_forms == 'nplurals=2; plural=(n != 1);' assert plural_en_US.num_plurals == 2 diff --git a/tests/messages/test_pofile_read.py b/tests/messages/test_pofile_read.py index fc913b407..d17f5d4af 100644 --- a/tests/messages/test_pofile_read.py +++ b/tests/messages/test_pofile_read.py @@ -406,6 +406,17 @@ 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"