Translations: the catalogue check passes a stray '%' that makes the page formatting it fail #497
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#497
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, by running the snapshot'sproblems_inagainst hand-made catalogue entries.Checked again on 2026-10-01 against
mainat0a5cad587: still present. The code is now atcore/templatetags/postulo.py:510-520for thepercentfilter (#248 moved the flag lookup out of that file);core/messages_tool.pyis unchanged.scripts/messages.py checkis Postulo's only placeholder gate: the tool replacesmsgfmt, and with itmsgfmt --check-format. It compares the sets of matches of one pattern, and that leaves two blind spots:%sequence in a translation is invisible to it, so it passes text that makes Python's%operator raise.docs/TRANSLATING.md:140promises that "a translation that would raise at render time, does not get in".Where:
core/messages_tool.py:200(PLACEHOLDER = re.compile(r"%\((\w+)\)[sdifr]|%[sdifr%]|\{(\w*)\}"));core/messages_tool.py:699-746(placeholders,problems_in, wherenot got <= expectedis the only test for a message with no named placeholder);core/templatetags/postulo.py:528-538(thepercentfilter,gettext("%(share)s%%") % {"share": value}, with nothing catching an error).What happens: run against
problems_inin this snapshot, each of these passes the check, and the runtime then fails when it formats the text:%(share)s%%%(share)s %ValueError: incomplete format%(share)s%%% %(share)sValueError: unsupported format character '%'%(n)s done%(n)s % dTypeError: not enough arguments for format stringDelete %s?Supprimer ?TypeError: not all arguments convertedThe first two are the shapes the
percentfilter's own docstring invites: a space before the sign in French, and the sign in front in Turkish. A translator who writes the sign as one%instead of%%gets through the check, the catalogue compiles, and every page that uses|percentfails for everybody reading that language. Those pages are the funnel, industries, sources and insight-counter widgets on the dashboard, and the translation bars on Defaults.Why it matters: nothing fails today. All 14,561 translated python-format forms in the snapshot's 18 catalogue sets format cleanly, and no msgid in them uses a positional placeholder yet. But the gate does not do what the documentation says it does. Translations arrive from Weblate and from pull requests, and this check is what CI relies on, so the first such translation would surface as a 500 on the dashboard.
Fix: in
problems_in, for each translated message flaggedpython-format, format every form the way the runtime will. Useform % {name: 1 for name in named}for named placeholders, or a tuple of the right length for positional ones, and report any exception. For brace messages, check thatstring.Formatter().parse(form)parses and yields only the msgid's field names. For singular messages, compare positional placeholders as a list rather than a set. Together that reproducesmsgfmt --check-format.Done when
tests/test_messages_tool.pyhas a case for each row of the table above. Each asserts thatproblems_inreports the entry. All four pass the check today.