Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 76 additions & 4 deletions babel/messages/plurals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -249,6 +293,8 @@
'or': (2, '(n != 1)'),
# Ossetic
'os': (2, '(n != 1)'),
# Osage
'osa': (1, '0'),
# Punjabi
'pa': (2, '(n > 1)'),
# Papiamento
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -321,6 +375,8 @@
'ssy': (2, '(n != 1)'),
# Southern Sotho
'st': (2, '(n != 1)'),
# Sundanese
'su': (1, '0'),
# Swedish
'sv': (2, '(n != 1)'),
# Swahili
Expand All @@ -333,6 +389,8 @@
'te': (2, '(n != 1)'),
# Teso
'teo': (2, '(n != 1)'),
# Thai
'th': (1, '0'),
# Tigrinya
'ti': (2, '(n > 1)'),
# Tigre
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
8 changes: 5 additions & 3 deletions scripts/dump_plurals_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@

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()):
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})"
Expand All @@ -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)
Expand Down
3 changes: 1 addition & 2 deletions tests/messages/frontend/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ def test_init_singular_plural_forms(cli):
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: ja_JP\n"
"Language-Team: ja_JP <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Plural-Forms: nplurals=1; plural=0;\n"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jun66j5 Fixed back to the correct form here (re #1303 (comment)). Thank you for checking! ❤️

"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
Expand All @@ -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:
Expand Down
3 changes: 1 addition & 2 deletions tests/messages/frontend/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ def test_correct_init_singular_plural_forms(init_cmd):
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: ja_JP\n"
"Language-Team: ja_JP <LL@li.org>\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"
Expand All @@ -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:
Expand Down
18 changes: 12 additions & 6 deletions tests/messages/test_plurals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -34,14 +34,20 @@ 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():
# 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;'

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
Expand Down
11 changes: 11 additions & 0 deletions tests/messages/test_pofile_read.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading