Timeline, offers, search and tables: translated labels are lower-cased inside sentences in Python, the pattern #168 retired #392

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

Found on 2026-09-29 in a read-only review of postulo at 95348908d, searching the Python code for .lower() applied to a translated label and reading the results against the German catalogue. Related to #168 (closed), which removed get_kind_display|lower from a CV's heading and added a template lint against the pattern (tests/test_template_lint.py:409-431); this issue lists the places where the same thing is still done in Python, which that lint does not read.

Checked again on 2026-10-01 against main at 0a5cad587: still present. The code is now at core/search.py:433 and :442 (the sent-document hits) and applications/views.py:1435 (the comparison page); the other locations have not moved.

A translated choice label or column name is passed through .lower() and dropped into a sentence. As #168 said, that is English typography applied to every language: it flattens an acronym (CV) in English, French and Portuguese, and in German, where every noun is capitalised, it is a spelling mistake that no catalogue can undo. Two of these strings are stored in the timeline and sent to notifiers, so the mistake is written into a person's record; two more are the accessible names of the controls on every table.

  • Where:
    • applications/services.py:520 in settle_interview: _("Nobody showed up for the %(kind)s") % {"kind": str(kind).lower()}, stored as a timeline event's summary. The cancelled branch just above (:510-511) uses the label as written.
    • applications/models.py:1011-1012 in Offer.terms: str(self.get_period_display()).lower(), then f"{figure} {self.currency} {period}", joined outside any translatable pattern. terms reaches the offer card (templates/applications/application_detail.html:225), the comparison page (applications/views.py:1395), the offer's timeline summary (_offer_summary, applications/services.py:629-631) and the offer_recorded payload (_tell_offer, applications/services.py:612-626). The comment at templates/applications/application_detail.html:77-79 records that #168 removed exactly this for the listing's salary period on the same page.
    • core/search.py:428 and :437 (the sent-document hits): sent.get_kind_display().lower() inside "in the %(kind)s you sent to %(company)s on %(when)s" and "in the %(kind)s rendered on %(when)s".
    • core/tables.py:150 (Header.filter_label): _("Filter by %(column)s") % {"column": str(self.column.label).lower()}, the aria-label of the header filters (templates/cotton/table/filter.html:35-87).
    • core/tables.py:542, :545 and :547 (Table.sort_hint): the same .lower() in "Stop sorting by …", "Sort by …, highest first" and "Sort by …, lowest first", rendered as the label of the sort icon at templates/cotton/table/head.html:139.
  • What happens:
    • With German active, a no-show is recorded as "Zum videogespräch ist niemand erschienen" (the catalogue has Videogespräch); an offer reads "65.000 EUR pro jahr" (Pro Jahr); a screen reader announces "Nach unternehmen sortieren, niedrigste zuerst" and "Nach unternehmen filtern"; a search hit reads "im lebenslauf, den Sie am … an … gesendet haben".
    • In English a search hit for a sent CV reads "in the cv you sent to Acme on …".
    • The slot also forces one grammatical form on every value, the second objection in #168. German "Zum %(kind)s" fits a masculine or neuter noun only; "im %(kind)s, den Sie …" is already wrong for Anschreiben, which is neuter. In English the no-show sentence reads "Nobody showed up for the on site" and "for the other".
  • Why it matters: the timeline summary and the offer terms are stored text and are handed to notifiers, so the misspelling outlives a later catalogue fix. The table-control names are what a screen reader says on every sortable or filterable column. The rest are misspelt or flattened words on pages a person sees daily.
  • Fix: stop lower-casing translated text in all eight places and let each catalogue decide the case.
    • Interview no-show and search subtitles: one whole sentence per value, as #168 did for the CV heading ("in the CV you sent to %(company)s on %(when)s", "in the cover letter you sent to …"; one no-show sentence per InterviewKind), or a separate in-sentence label with pgettext that translators case and inflect themselves.
    • Offer.terms: build it from a translatable pattern such as _("%(figure)s %(currency)s %(period)s") with the period's in-sentence form, so translators control order and case.
    • core/tables.py: drop the .lower(). #314 and #317 may reword these names; whichever lands second carries the rule.
    • Extend the lint in tests/test_template_lint.py to Python: reject get_\w+_display() and .label lower-cased under src/postulo. tests/test_interviews.py:174 and tests/test_offers.py:94 pin today's English wording and will need updating. plugins/postal_rules/__init__.py:179-182 (_lower) does the same to an address part's label; #306 is reworking that module and should either remove it or be the one named exception.

Done when

  • A test in tests/test_interviews.py settles an interview as a no-show with German active and asserts that the stored summary keeps the catalogue's capitalisation; a test in tests/test_offers.py asserts the same for Offer.terms. Both fail today.
  • A test in tests/test_search.py finds "in the CV you sent to" in the subtitle of a sent CV's hit.
  • A test in tests/test_tables.py renders a table with German active and asserts that the filter and sort names carry the column's label as the catalogue wrote it.
  • A test in tests/test_template_lint.py scans src/postulo/**/*.py for a translated label lower-cased into a sentence; it fails on today's code and passes after.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, searching the Python code for `.lower()` applied to a translated label and reading the results against the German catalogue. Related to #168 (closed), which removed `get_kind_display|lower` from a CV's heading and added a template lint against the pattern (`tests/test_template_lint.py:409-431`); this issue lists the places where the same thing is still done in Python, which that lint does not read. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. The code is now at `core/search.py:433` and `:442` (the sent-document hits) and `applications/views.py:1435` (the comparison page); the other locations have not moved. A translated choice label or column name is passed through `.lower()` and dropped into a sentence. As #168 said, that is English typography applied to every language: it flattens an acronym (*CV*) in English, French and Portuguese, and in German, where every noun is capitalised, it is a spelling mistake that no catalogue can undo. Two of these strings are stored in the timeline and sent to notifiers, so the mistake is written into a person's record; two more are the accessible names of the controls on every table. - **Where:** - `applications/services.py:520` in `settle_interview`: `_("Nobody showed up for the %(kind)s") % {"kind": str(kind).lower()}`, stored as a timeline event's summary. The cancelled branch just above (`:510-511`) uses the label as written. - `applications/models.py:1011-1012` in `Offer.terms`: `str(self.get_period_display()).lower()`, then `f"{figure} {self.currency} {period}"`, joined outside any translatable pattern. `terms` reaches the offer card (`templates/applications/application_detail.html:225`), the comparison page (`applications/views.py:1395`), the offer's timeline summary (`_offer_summary`, `applications/services.py:629-631`) and the `offer_recorded` payload (`_tell_offer`, `applications/services.py:612-626`). The comment at `templates/applications/application_detail.html:77-79` records that #168 removed exactly this for the listing's salary period on the same page. - `core/search.py:428` and `:437` (the sent-document hits): `sent.get_kind_display().lower()` inside "in the %(kind)s you sent to %(company)s on %(when)s" and "in the %(kind)s rendered on %(when)s". - `core/tables.py:150` (`Header.filter_label`): `_("Filter by %(column)s") % {"column": str(self.column.label).lower()}`, the `aria-label` of the header filters (`templates/cotton/table/filter.html:35-87`). - `core/tables.py:542`, `:545` and `:547` (`Table.sort_hint`): the same `.lower()` in "Stop sorting by …", "Sort by …, highest first" and "Sort by …, lowest first", rendered as the label of the sort icon at `templates/cotton/table/head.html:139`. - **What happens:** - With German active, a no-show is recorded as "Zum videogespräch ist niemand erschienen" (the catalogue has *Videogespräch*); an offer reads "65.000 EUR pro jahr" (*Pro Jahr*); a screen reader announces "Nach unternehmen sortieren, niedrigste zuerst" and "Nach unternehmen filtern"; a search hit reads "im lebenslauf, den Sie am … an … gesendet haben". - In English a search hit for a sent CV reads "in the cv you sent to Acme on …". - The slot also forces one grammatical form on every value, the second objection in #168. German "Zum %(kind)s" fits a masculine or neuter noun only; "im %(kind)s, den Sie …" is already wrong for *Anschreiben*, which is neuter. In English the no-show sentence reads "Nobody showed up for the on site" and "for the other". - **Why it matters:** the timeline summary and the offer terms are stored text and are handed to notifiers, so the misspelling outlives a later catalogue fix. The table-control names are what a screen reader says on every sortable or filterable column. The rest are misspelt or flattened words on pages a person sees daily. - **Fix:** stop lower-casing translated text in all eight places and let each catalogue decide the case. - Interview no-show and search subtitles: one whole sentence per value, as #168 did for the CV heading ("in the CV you sent to %(company)s on %(when)s", "in the cover letter you sent to …"; one no-show sentence per `InterviewKind`), or a separate in-sentence label with `pgettext` that translators case and inflect themselves. - `Offer.terms`: build it from a translatable pattern such as `_("%(figure)s %(currency)s %(period)s")` with the period's in-sentence form, so translators control order and case. - `core/tables.py`: drop the `.lower()`. #314 and #317 may reword these names; whichever lands second carries the rule. - Extend the lint in `tests/test_template_lint.py` to Python: reject `get_\w+_display()` and `.label` lower-cased under `src/postulo`. `tests/test_interviews.py:174` and `tests/test_offers.py:94` pin today's English wording and will need updating. `plugins/postal_rules/__init__.py:179-182` (`_lower`) does the same to an address part's label; #306 is reworking that module and should either remove it or be the one named exception. ## Done when - A test in `tests/test_interviews.py` settles an interview as a no-show with German active and asserts that the stored summary keeps the catalogue's capitalisation; a test in `tests/test_offers.py` asserts the same for `Offer.terms`. Both fail today. - A test in `tests/test_search.py` finds "in the CV you sent to" in the subtitle of a sent CV's hit. - A test in `tests/test_tables.py` renders a table with German active and asserts that the filter and sort names carry the column's label as the catalogue wrote it. - A test in `tests/test_template_lint.py` scans `src/postulo/**/*.py` for a translated label lower-cased into a sentence; it fails on today's code and passes after.
tiagoagueda added this to the 0.6.0 milestone 2026-10-01 10:46:22 +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#392
No description provided.