diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py index e01fd5677..ee5e58c23 100644 --- a/babel/messages/catalog.py +++ b/babel/messages/catalog.py @@ -1046,8 +1046,13 @@ def is_identical(self, other: Catalog) -> bool: """ assert isinstance(other, Catalog) for key in self._messages.keys() | other._messages.keys(): - message_1 = self.get(key) - message_2 = other.get(key) + # Index ``_messages`` directly: these are already internal keys, and + # passing one back through ``get`` would re-run ``_key_for`` on it. + # For a context-specific message that key is a ``(msgid, msgctxt)`` + # tuple, which ``_key_for`` mistakes for a pluralizable message's id + # and reduces to ``msgid`` -- a key no message is stored under. + message_1 = self._messages.get(key) + message_2 = other._messages.get(key) if message_1 is None or message_2 is None or not message_1.is_identical(message_2): return False return dict(self.mime_headers) == dict(other.mime_headers) diff --git a/tests/messages/test_catalog.py b/tests/messages/test_catalog.py index 4a60208c8..d31937d8f 100644 --- a/tests/messages/test_catalog.py +++ b/tests/messages/test_catalog.py @@ -596,3 +596,43 @@ def test_catalog_tz_pickleable(): msgstr "" "POT-Creation-Date: 2007-04-01 15:30+0200\n" """)))) + + +def test_catalog_is_identical_with_context(): + """A catalog holding a context-specific message compares equal to itself. + + ``is_identical`` iterates the internal ``_messages`` keys, and the key of a + context-specific message is a ``(msgid, msgctxt)`` tuple. Looking that key + up through ``Catalog.get`` re-ran ``_key_for`` on it, which took it for a + pluralizable message's id and reduced it to ``msgid`` -- so the lookup found + nothing and every such catalog compared as changed. That made + ``pybabel update --check`` report any catalog using ``pgettext`` or + ``npgettext`` as out of date, on every run. + """ + cat = catalog.Catalog(locale='en') + cat.add('Guide', 'Guide', context='navigation') + assert cat.is_identical(cat) + + same = catalog.Catalog(locale='en') + same.add('Guide', 'Guide', context='navigation') + assert cat.is_identical(same) + + translated_differently = catalog.Catalog(locale='en') + translated_differently.add('Guide', 'Manual', context='navigation') + assert not cat.is_identical(translated_differently) + + # The same msgid under a different context is a different message. + other_context = catalog.Catalog(locale='en') + other_context.add('Guide', 'Guide', context='footer') + assert not cat.is_identical(other_context) + + +def test_catalog_is_identical_with_pluralizable_context(): + """The same, for a context-specific *pluralizable* message.""" + cat = catalog.Catalog(locale='en') + cat.add(('%d page', '%d pages'), ('%d page', '%d pages'), context='report') + assert cat.is_identical(cat) + + same = catalog.Catalog(locale='en') + same.add(('%d page', '%d pages'), ('%d page', '%d pages'), context='report') + assert cat.is_identical(same)