Bulk actions: \"N … changed.\" picks its noun by English plural rules and cannot agree in other languages #390

Closed
opened 2026-10-01 10:46:21 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the bulk actions' messages and their catalogues.

Checked again on 2026-10-01 against main at 0a5cad587: still present.

The sentence shown after every bulk action is built from a generic template and a standalone noun, with the noun chosen by count == 1. That is English's two-way plural rule applied to every language, and it leaves the participle unable to agree with the noun. It is the same hand-chosen plural that #225 removed from the "N rows ticked" count in the script, still present on the server.

  • Where: core/bulk.py:102-116 (changed); the nouns it is given at applications/tables.py:21, jobs/tables.py:25 and jobs/tables.py:178; called from applications/views.py:301, jobs/views.py:237 and jobs/listing_views.py:193.
  • What happens: changed returns _("%(count)d %(noun)s changed.") % {"count": count, "noun": singular if count == 1 else plural}. The catalogues can only translate the frame once for every table and every number:
    • French has "%(count)d %(noun)s modifiés.", and all three nouns are feminine (candidatures, entreprises, offres), so every French bulk message reads "3 candidatures modifiés" where it should read "modifiées".
    • Polish has "%(count)d %(noun)s zmieniono." with aplikacje; five or more needs the genitive plural (aplikacji), and 22 needs the form for 2–4 again. Czech, Ukrainian, Lithuanian, Irish, Welsh and Arabic have the same problem.
  • Why it matters: the confirmation after tagging, moving or triaging several rows is wrong in most of the shipped languages for most numbers, and no translator can fix it from the catalogue. The comment at core/bulk.py:110-112 explains the choice ("a plural form here would have to pluralise a word it does not know"), but the table does know its own word, so the table can carry the whole sentence.
  • Fix: give each table a changed_message = ngettext_lazy("%(count)d application changed.", "%(count)d applications changed.", "count") (and the same for companies and listings), and have bulk.changed(count, table) format table.changed_message % {"count": count}. Retire the generic "%(count)d %(noun)s changed." msgid and the noun attribute. The standalone "application"/"applications" msgids stay, since the models' verbose_name uses them.

Related to #225 (closed), which fixed the same pattern in app.js and did not reach core/bulk.py.

Done when

  • A test in tests/security/test_bulk_actions.py (or a new tests/test_bulk_messages.py) checks that each of the three tables' message is an ngettext with a count placeholder, and that "1 application changed." and "5 applications changed." still read as they do today in English. tests/security/test_bulk_actions.py::test_what_is_said_afterwards_counts_only_what_changed keeps passing.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading the bulk actions' messages and their catalogues. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. The sentence shown after every bulk action is built from a generic template and a standalone noun, with the noun chosen by `count == 1`. That is English's two-way plural rule applied to every language, and it leaves the participle unable to agree with the noun. It is the same hand-chosen plural that #225 removed from the "N rows ticked" count in the script, still present on the server. - **Where:** `core/bulk.py:102-116` (`changed`); the nouns it is given at `applications/tables.py:21`, `jobs/tables.py:25` and `jobs/tables.py:178`; called from `applications/views.py:301`, `jobs/views.py:237` and `jobs/listing_views.py:193`. - **What happens:** `changed` returns `_("%(count)d %(noun)s changed.") % {"count": count, "noun": singular if count == 1 else plural}`. The catalogues can only translate the frame once for every table and every number: - French has `"%(count)d %(noun)s modifiés."`, and all three nouns are feminine (`candidatures`, `entreprises`, `offres`), so every French bulk message reads "3 candidatures modifiés" where it should read "modifiées". - Polish has `"%(count)d %(noun)s zmieniono."` with `aplikacje`; five or more needs the genitive plural (`aplikacji`), and 22 needs the form for 2–4 again. Czech, Ukrainian, Lithuanian, Irish, Welsh and Arabic have the same problem. - **Why it matters:** the confirmation after tagging, moving or triaging several rows is wrong in most of the shipped languages for most numbers, and no translator can fix it from the catalogue. The comment at `core/bulk.py:110-112` explains the choice ("a plural form here would have to pluralise a word it does not know"), but the table does know its own word, so the table can carry the whole sentence. - **Fix:** give each table a `changed_message = ngettext_lazy("%(count)d application changed.", "%(count)d applications changed.", "count")` (and the same for companies and listings), and have `bulk.changed(count, table)` format `table.changed_message % {"count": count}`. Retire the generic `"%(count)d %(noun)s changed."` msgid and the `noun` attribute. The standalone `"application"`/`"applications"` msgids stay, since the models' `verbose_name` uses them. Related to #225 (closed), which fixed the same pattern in `app.js` and did not reach `core/bulk.py`. ## Done when - A test in `tests/security/test_bulk_actions.py` (or a new `tests/test_bulk_messages.py`) checks that each of the three tables' message is an `ngettext` with a `count` placeholder, and that "1 application changed." and "5 applications changed." still read as they do today in English. `tests/security/test_bulk_actions.py::test_what_is_said_afterwards_counts_only_what_changed` keeps passing.
tiagoagueda added this to the 0.6.0 milestone 2026-10-01 10:46:21 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#390
No description provided.