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"  
    220220#, python-format
    221221msgid "an hour ago"
    222222msgid_plural "%(count)s hours ago"
    223 msgstr[0] "pre sat"
     223msgstr[0] "pre %(count)s sat"
    224224msgstr[1] "pre %(count)s sata"
    225225msgstr[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]'

Change History (1)

comment:1 by Yassin Bahri, 26 minutes ago

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:

$ msgfmt --check -o /dev/null django/contrib/humanize/locale/sr_Latn/LC_MESSAGES/django.po
django.po:223: a format specification for argument 'count' doesn't exist in 'msgstr[0]'
django.po:232: a format specification for argument 'count' doesn't exist in 'msgstr[0]'
django.po:241: a format specification for argument 'count' doesn't exist in 'msgstr[0]'
django.po:253: a format specification for argument 'count' doesn't exist in 'msgstr[0]'
django.po:262: a format specification for argument 'count' doesn't exist in 'msgstr[0]'
django.po:271: a format specification for argument 'count' doesn't exist in 'msgstr[0]'
msgfmt: found 6 fatal errors

Same six lines reported above. Adding %(count)s to those six msgstr[0] entries makes msgfmt --check exit 0, so the diagnosis holds.

Three things I don't think are in the ticket yet.

1. compilemessages cannot detect this

The check only fires when --check-format and --check-header are passed together — it needs the plural rules from the header to know that msgstr[0] is reachable for n>1:

flags errors
--check-format 0
--check-header 0
--check-format --check-header 6
--check 6

compilemessages passes --check-format alone (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 select msgstr[0]. Because msgstr[0] has no placeholder, the number is silently dropped:

from datetime import datetime, timedelta
from django.utils import translation
from django.contrib.humanize.templatetags.humanize import naturaltime

now = datetime.now()
with translation.override("sr-latn"):
    for m in (1, 2, 21, 31, 41):
        print(m, naturaltime(now - timedelta(minutes=m, seconds=1)))
1  pre minut
2  pre 2 minuta
21 pre minut     <- "a minute ago", for 21 minutes
31 pre minut
41 pre minut

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:

release msgstr[0]
4.2 ""
5.0 ""
5.1 / 5.1.7 ""
5.2 "pre sat"
6.0 "pre sat"

Introduced by cb27e5b9c0 (2025-03-28, "Updated translations from Transifex").

Scope

I ran msgfmt --check-format --check-header over all 1226 .po files in the tree: 6 errors total, all in sr_Latn/humanize. Scanning source strings for the same shape (placeholder in msgid_plural but not msgid) 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)s would 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:

  • Add --check-header to compilemessages' program_options so this class of breakage is caught rather than shipped. (Would need a check for what else that turns up across the tree first.)
  • Correct the six sr_Latn 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.

Note: See TracTickets for help on using tickets.
Back to Top