Opened 73 minutes ago
Last modified 26 minutes ago
#37253 new Bug
Translatable strings omitting placeholders in singular form cannot be validated by both msgfmt 0.22 and Transifex
| Reported by: | Jacob Walls | Owned by: | |
|---|---|---|---|
| Component: | contrib.humanize | Version: | dev |
| Severity: | Normal | Keywords: | |
| Cc: | Triage Stage: | Unreviewed | |
| Has patch: | no | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
We need to make the singular forms of translatable strings use the same placeholders their plural counterparts do.
Some translatable strings in the humanize app look like this:
"past-hour": ngettext_lazy("an hour ago", "%(count)s hours ago", "count"),
The Serbian (Latin script) locale has this set of translations for it, in its three plural forms:
#. Translators: please keep a non-breaking space (U+00A0) between count #. and time unit. #, python-format msgid "an hour ago" msgid_plural "%(count)s hours ago" msgstr[0] "pre sat" msgstr[1] "pre %(count)s sata" msgstr[2] "pre %(count)s sati"
This fails the stricter validation in msgfmt 0.22, which requires the %(count)s placeholder to be present in every plural form.
Our usual practice is to fix errors in Transifex and refetch. I attempted to enter this change on Transifex:
-
django/contrib/humanize/locale/sr_Latn/LC_MESSAGES/django.po
diff --git a/django/contrib/humanize/locale/sr_Latn/LC_MESSAGES/django.po b/django/contrib/humanize/locale/sr_Latn/LC_MESSAGES/django.po index d8a24ef699..2aa3bc07a0 100644
a b msgstr "pre %(delta)s" 220 220 #, python-format 221 221 msgid "an hour ago" 222 222 msgid_plural "%(count)s hours ago" 223 msgstr[0] "pre sat"223 msgstr[0] "pre %(count)s sat" 224 224 msgstr[1] "pre %(count)s sata" 225 225 msgstr[2] "pre %(count)s sati"
... but I received this error: "The expression '%(count)s' is not present in the original phrase." Indeed, the singular form in the catalog is just "an hour ago". Apparently, the msgfmt validation scheme is incompatible with Transifex's validation.
(As a consequence, the affected .mo file for the humanize app was not recompiled for Django 6.1, but they likely haven't been updated in years, due to the oversight fixed in 30ebe0001c1d10bebc68567f796d821f7b309f4d.)
We could see if this issue would go away if we move to Weblate, but barring that, we need to fix the source strings to always use placeholders in singular forms to avoid compilation problems.
Errors:
/Users/.../django/django/contrib/humanize/locale/sr_Latn/LC_MESSAGES/django.po:223: a format specification for argument 'count' doesn't exist in 'msgstr[0]' /Users/.../django/django/contrib/humanize/locale/sr_Latn/LC_MESSAGES/django.po:232: a format specification for argument 'count' doesn't exist in 'msgstr[0]' /Users/.../django/django/contrib/humanize/locale/sr_Latn/LC_MESSAGES/django.po:241: a format specification for argument 'count' doesn't exist in 'msgstr[0]' /Users/.../django/django/contrib/humanize/locale/sr_Latn/LC_MESSAGES/django.po:253: a format specification for argument 'count' doesn't exist in 'msgstr[0]' /Users/.../django/django/contrib/humanize/locale/sr_Latn/LC_MESSAGES/django.po:262: a format specification for argument 'count' doesn't exist in 'msgstr[0]' /Users/.../django/django/contrib/humanize/locale/sr_Latn/LC_MESSAGES/django.po:271: a format specification for argument 'count' doesn't exist in 'msgstr[0]'
Confirmed, and I think it's a user-visible regression in 5.2, not only a translation-tooling problem.
Reproduced on
main(dfc52e53f1d) with gettext 1.0:Same six lines reported above. Adding
%(count)sto those sixmsgstr[0]entries makesmsgfmt --checkexit 0, so the diagnosis holds.Three things I don't think are in the ticket yet.
1.
compilemessagescannot detect thisThe check only fires when
--check-formatand--check-headerare passed together — it needs the plural rules from the header to know thatmsgstr[0]is reachable for n>1:--check-format--check-header--check-format --check-header--checkcompilemessagespasses--check-formatalone (core/management/commands/compilemessages.py,program_options = ["--check-format"]), so it compiles this catalog silently with exit 0. That is presumably how the broken catalog shipped. (Measured on gettext 1.0; someone with 0.22 may want to confirm the flag interaction is the same there.)2. It produces wrong output for Serbian users
The sr_Latn plural rule is
n%10==1 && n%100!=11 ? 0 : ..., so 21, 31, 41, 101 … all selectmsgstr[0]. Becausemsgstr[0]has no placeholder, the number is silently dropped:Same for hours: 21 hours ago renders as
pre sat("an hour ago").3. Regression, first shipped in 5.2
The sr_Latn entries were previously untranslated, so nothing tripped. Bisecting the catalog:
msgstr[0]"""""""pre sat""pre sat"Introduced by cb27e5b9c0 (2025-03-28, "Updated translations from Transifex").
Scope
I ran
msgfmt --check-format --check-headerover all 1226.pofiles in the tree: 6 errors total, all in sr_Latn/humanize. Scanning source strings for the same shape (placeholder inmsgid_pluralbut notmsgid) also finds exactly 6, all in humanize:"an hour ago"/"%(count)s hours ago""a minute ago"/"%(count)s minutes ago""a second ago"/"%(count)s seconds ago""a second from now"/"%(count)s seconds from now""a minute from now"/"%(count)s minutes from now""an hour from now"/"%(count)s hours from now"These date to #12771 (1.4), so the latent hazard is old; only the sr_Latn translation is new.
On the proposed fix
Changing the source singulars to carry
%(count)swould change the default English output from "an hour ago" to "1 hour ago" — a deliberate wording change in every locale that hasn't diverged, in order to satisfy a Transifex validation rule. That may still be the right call, but it seems worth naming explicitly, since fixing the sr_Latn catalog directly is blocked only by Transifex, not by gettext.Two smaller options that might be worth considering alongside it:
--check-headertocompilemessages'program_optionsso this class of breakage is caught rather than shipped. (Would need a check for what else that turns up across the tree first.)msgstr[0]entries directly in the repo to stop the wrong output for 5.2/6.0 users, independently of whatever happens to the source strings.Happy to work up a patch for whichever direction is preferred.