Opened 3 weeks ago

Closed 3 weeks ago

#37253 closed Bug (wontfix)

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 (3)

comment:1 by Yassin Bahri, 3 weeks 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.

comment:2 by Jacob Walls, 3 weeks ago

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.

Yes, doing that would invalidate all translations for these strings, and we would we be doing it to work around an infelicity in Transifex. We might be able to tinker with Transifex settings to make this particular error check softer. We can do this per check at the org level. Weblate lets you adjust per file/string. Apparently we can also bypass the check by using the API, only the web UI is a hard stop.

But my point is that this is an anti-pattern in the python source string (for it to be written in a way that is problematic for other tooling).

This alternative:

Correct the six sr_Latn msgstr[0] entries directly in the repo

...would need to be done every single time that file is updated in Transifex and later fetched. It wouldn't be every time we fetch, but it will probably happen again.

So there is a tradeoff between causing work for translators and causing work for maintainers/translation managers. If we decide to put the burden on the maintainers, we would need to evaluate maintaining automated patches that we apply to the PO files after they are fetched.

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.)

Would you be willing to open a separate ticket for that?

comment:3 by Jacob Walls, 3 weeks ago

Resolution: wontfix
Status: newclosed

Apparently we can also bypass the check by using the API

I'm going to close this ticket for now, because if we can bypass the check, that's enough -- we won't to need to invalidate any other translations.

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