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
12 changes: 11 additions & 1 deletion babel/messages/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ def __init__(
self.previous_id = []
self.lineno = lineno
self.context = context
# msgstr[N] entries dropped while reading a PO file because
# N was >= catalog.num_plurals. Does not affect message.string.
self.discarded_plural_forms = 0

def __repr__(self) -> str:
return f"<{type(self).__name__} {self.id!r} (flags: {list(self.flags)!r})>"
Expand Down Expand Up @@ -232,7 +235,7 @@ def is_identical(self, other: Message) -> bool:
return self.__dict__ == other.__dict__

def clone(self) -> Message:
return Message(
cloned = Message(
id=copy(self.id),
string=copy(self.string),
locations=copy(self.locations),
Expand All @@ -243,6 +246,8 @@ def clone(self) -> Message:
lineno=self.lineno, # immutable (str/None)
context=self.context, # immutable (str/None)
)
cloned.discarded_plural_forms = self.discarded_plural_forms
return cloned

def check(self, catalog: Catalog | None = None) -> list[TranslationError]:
"""Run various validation checks on the message. Some validations
Expand Down Expand Up @@ -774,6 +779,10 @@ def __setitem__(self, id: _MessageID, message: Message) -> None:
current.auto_comments = list(dict.fromkeys([*current.auto_comments, *message.auto_comments])) # fmt:skip
current.user_comments = list(dict.fromkeys([*current.user_comments, *message.user_comments])) # fmt:skip
current.flags |= message.flags
current.discarded_plural_forms = max(
current.discarded_plural_forms,
message.discarded_plural_forms,
)
elif id == '':
# special treatment for the header message
self.mime_headers = message_from_string(message.string).items()
Expand Down Expand Up @@ -996,6 +1005,7 @@ def _merge(
oldmsg = remaining.pop(oldkey, None)
assert oldmsg is not None
message.string = oldmsg.string
message.discarded_plural_forms = oldmsg.discarded_plural_forms

if keep_user_comments and oldmsg.user_comments:
message.user_comments = list(dict.fromkeys(oldmsg.user_comments))
Expand Down
3 changes: 2 additions & 1 deletion babel/messages/checkers.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ def num_plurals(catalog: Catalog | None, message: Message) -> None:
msgstrs = message.string
if not isinstance(msgstrs, (list, tuple)):
msgstrs = (msgstrs,)
if len(msgstrs) != catalog.num_plurals:
n_forms = len(msgstrs) + message.discarded_plural_forms
if n_forms != catalog.num_plurals:
raise TranslationError(
f"Wrong number of plural forms (expected {catalog.num_plurals})",
)
Expand Down
3 changes: 3 additions & 0 deletions babel/messages/pofile.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ def _add_message(self) -> None:
Add a message to the catalog based on the current parser state and
clear the state ready to process the next message.
"""
discarded_plural_forms = 0
if len(self.messages) > 1:
msgid = tuple(m.denormalize() for m in self.messages)
string = ['' for _ in range(self.catalog.num_plurals)]
Expand All @@ -204,6 +205,7 @@ def _add_message(self) -> None:
self.offset,
"msg has more translations than num_plurals of catalog",
)
discarded_plural_forms += 1
continue
string[idx] = translation.denormalize()
string = tuple(string)
Expand All @@ -221,6 +223,7 @@ def _add_message(self) -> None:
lineno=self.offset + 1,
context=msgctxt,
)
message.discarded_plural_forms = discarded_plural_forms
if self.obsolete:
if not self.ignore_obsolete:
self.catalog.obsolete[self.catalog._key_for(msgid, msgctxt)] = message
Expand Down
31 changes: 25 additions & 6 deletions tests/messages/test_checkers.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ def test_1_num_plurals_checkers():


def test_2_num_plurals_checkers():
# in this testcase we add an extra msgstr[idx], we should be
# disregarding it
# Extra msgstr[idx] is dropped from message.string, but the checker
# still reports the mismatch.
for _locale in [p for p in PLURALS if PLURALS[p][0] == 2]:
if _locale in ['nn', 'no']:
_locale = 'nn_NO'
Expand Down Expand Up @@ -132,12 +132,31 @@ def test_2_num_plurals_checkers():
msgstr[2] ""

""".encode('utf-8')
# we should be adding the missing msgstr[0]

# This test will fail for revisions <= 406 because so far
# catalog.num_plurals was neglected
catalog = read_po(BytesIO(po_file), _locale)
message = catalog['foobar']
assert len(message.string) == num_plurals
with pytest.raises(TranslationError, match="Wrong number of plural forms"):
checkers.num_plurals(catalog, message)


def test_num_plurals_checker_on_parsed_extra_forms():
po_file = b'''\
msgid ""
msgstr ""
"Language: en\\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\\n"

msgid "file"
msgid_plural "files"
msgstr[0] "file"
msgstr[1] "files"
msgstr[2] "too many"
'''
catalog = read_po(BytesIO(po_file), 'en')
message = catalog['file']
assert message.string == ('file', 'files')
assert message.discarded_plural_forms == 1
with pytest.raises(TranslationError, match="Wrong number of plural forms"):
checkers.num_plurals(catalog, message)


Expand Down
19 changes: 19 additions & 0 deletions tests/messages/test_pofile_read.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,25 @@ def test_missing_plural_in_the_middle():
assert message.string[2] == 'Vohs [text]'


def test_plural_forms_beyond_nplurals_are_discarded():
buf = StringIO('''\
msgid ""
msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\\n"

msgid "foo"
msgid_plural "foos"
msgstr[0] "Voh"
msgstr[1] "Vohs"
msgstr[2] "extra"
''')
catalog = pofile.read_po(buf, locale='en')
message = catalog['foo']
assert catalog.num_plurals == 2
assert message.string == ('Voh', 'Vohs')
assert message.discarded_plural_forms == 1


def test_with_location():
buf = StringIO('''\
#: main.py:1 \u2068filename with whitespace.py\u2069:123
Expand Down