Opened 70 minutes ago
#36980 new Bug
TranslationCatalog.__getitem__ gives wrong priority to non-plural translations when plural forms mismatch
| Reported by: | UHHHHHHHHHHHHHH | Owned by: | |
|---|---|---|---|
| Component: | Internationalization | Version: | 5.1 |
| Severity: | Normal | Keywords: | i18n translation plural TranslationCatalog |
| Cc: | Triage Stage: | Unreviewed | |
| Has patch: | no | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
Description
When a third-party package (e.g. django-allauth) has a slightly different Plural-Forms header than the project's own translations (e.g. plural=n != 1 vs plural=(n != 1)), TranslationCatalog.update() correctly prepends a separate catalog to preserve plural lookup correctness.
However, TranslationCatalog.__getitem__() iterates catalogs in order and returns the first match. This means the prepended catalog wins for all lookups, including non-plural gettext() calls where plural form separation is irrelevant. This silently overrides higher-priority translations (from LOCALE_PATHS or later INSTALLED_APPS) with lower-priority ones.
Steps to reproduce
- Create a Django project with a translation in
LOCALE_PATHS:# project/locale/nl/LC_MESSAGES/django.po # Plural-Forms: nplurals=2; plural=(n != 1); msgid "Activate" msgstr "Activeren"
- Install a third-party app (e.g. django-allauth) that defines the same msgid with a different
Plural-Formsheader:# allauth/locale/nl/LC_MESSAGES/django.po # Plural-Forms: nplurals=2; plural=n != 1; msgid "Activate" msgstr "Activeer"
Note: n != 1 and (n != 1) are functionally identical and compile to identical bytecode, but Python's gettext.c2py() produces functions with different __code__ objects. Django's TranslationCatalog.update() compares __code__ to decide whether to merge or prepend.
gettext("Activate")returns"Activeer"(allauth's version) instead of"Activeren"(the project's version), even thoughLOCALE_PATHSshould have highest priority per Django's documentation.
Root cause
In django/utils/translation/trans_real.py:
class TranslationCatalog: def update(self, trans): # Mismatched plural -> prepend to position 0 for cat, plural in zip(self._catalogs, self._plurals): if trans.plural.__code__ == plural.__code__: cat.update(trans._catalog) break else: self._catalogs.insert(0, trans._catalog.copy()) self._plurals.insert(0, trans.plural) def __getitem__(self, key): # First catalog wins for ALL lookups for cat in self._catalogs: try: return cat[key] except KeyError: pass raise KeyError(key)
The update() prepend is correct for plural lookups (each catalog needs its own plural function). But __getitem__ shouldn't give the prepended catalog priority for non-plural string lookups — these should follow the normal priority order (LOCALE_PATHS > INSTALLED_APPS > Django built-in).
Expected behavior
Non-plural gettext() lookups should respect the documented translation priority order regardless of plural form differences between catalogs.
Actual behavior
A third-party package with a cosmetically different Plural-Forms header silently overrides all matching non-plural translations from higher-priority sources.
Impact
In our project, this causes 28 translations to be silently wrong. The project has no way to fix this without either:
- Monkey-patching
TranslationCatalog - Patching the third-party package's
.pofiles in the Docker build - Adding
pgettextcontext to every conflicting string