Documents: show each document's language as a flag beside its kind #280

Closed
opened 2026-09-19 09:37:12 +00:00 by tiagoagueda · 1 comment
Owner

A CV and a cover letter each declare the language they are written in, and nothing in Documents says which. The field decides the lang of the PDF an employer receives — which is what a recruiter's screen reader reads it out with, and what WeasyPrint hyphenates and justifies by (#223) — so it is not a detail of the edit form. Somebody keeping "Backend, English" beside "Backend, Português" has to open each one to tell them apart, or trust the name they gave it.

Show the language as a flag beside the kind, the way Settings → Language and time shows one beside every language in the picker (#52, #88, #119), and the way #208 asks for on Server settings → Defaults and #214 on postal addresses.

What it is today

src/postulo/templates/documents/cv_list.html:30-45 — the cards. Kind sits in a chip at the top right of each, and the language appears nowhere:

<div class="flex flex-wrap items-baseline justify-between gap-2">
  <p class="font-medium"><bdi>{{ cv.name }}</bdi></p>
  <span class="chip py-0 text-xs"><span class="chip-text">{{ cv.get_kind_display }}</span></span>
</div>

src/postulo/templates/documents/letter_list.html:35-50 — rows rather than cards, but the same omission: {{ letter.get_kind_display }} on the secondary line, no language.

src/postulo/templates/documents/cv_detail.html:11 — kind · headline · theme, no language either. The only place a CV's language is ever named in the interface outside the form is the {% if fell_back %} warning at line 36, and that fires only when an entry has not been translated into it.

CV.language and CoverLetter.language are documents/models.py:120 and :333. Both are blank=True, meaning follow your profile.

What already exists to reuse

  • {% flag country %} (core/templatetags/postulo.py:126) draws an SVG out of static/flags/, 20×15 with the ratio stated so the row does not reflow. It returns "" for an unknown or empty country, and takes label= for the case where the flag has to speak for itself.
  • languages.flag_country(code) (core/languages.py:123) gives the country whose flag stands for a language, and deliberately returns "" for the ones with no uncontested home — ar, sw, ha and the rest listed at :61. No flag beats a wrong flag.
  • rendering.document_language(document) (documents/rendering.py:69) already resolves the effective language: what the document says, else what its owner reads Postulo in, else the instance default. This is the value the flag should stand for, not the raw field.
  • languages.NATIVE_NAMES (core/languages.py:135) gives the language's own name for itself, for the label.
  • settings/locale.html:34,64 is the worked example of a flag beside a language.

Points to settle

  • The flag stands alone here, so it cannot be decoration. In the picker the language's name is right beside it and {% flag %}'s default alt="" aria-hidden="true" is correct — "Portugal flag, português (Portugal)" is worse than silence. On a card there is no name beside it, so it has to carry label=, and the label is the language, not the country: Português, never Portugal. tests/test_flags.py:133 pins the distinction.
  • What a language with no flag shows. flag_country returns "" for perhaps a dozen offered languages, and the card must read as well without the image as with it. Either the language's short name in the same slot, or nothing — but decided once here rather than per template.
  • Whether an inherited language is drawn differently from a declared one. A blank field means follow your profile, and a flag drawn for a value nobody chose invites the reading that they did. Options: draw it the same and say "follows your profile" in the label; draw it dimmed; draw nothing until the field is set. The first is probably right — what goes in the PDF is what matters, and that is the resolved value either way.
  • How far it goes. Letters share the problem and the field, so they are in scope; the chip is a row rather than a card there. Uploads and sent documents (upload_list.html:34, rendered_list.html:34) show a kind too but carry no language field at all — an uploaded certificate's language is not known, and a rendered snapshot's was fixed at the moment it was frozen. Either leave both alone, or take the rendered ones from the source document and say so; not worth widening this issue for.
  • The detail pages (cv_detail.html:11, letter_detail.html) should almost certainly gain the same flag on the same line, since that is where somebody checks a document before exporting it.

Notes for whoever takes it

  • CVListView (documents/views.py:67) is scoped by OwnedObjectMixin (core/mixins.py:16), so every row on the page belongs to the same person: the profile fallback in document_language resolves once for the list, not once per card. Resolving it naively per row is an N+1 on owner.profile.
  • Adding a property to documents/models.py moves the #: source references of every translatable string below it and makes scripts/messages.py extract --check fail with no new string at all — #193 was exactly this. Run extract, then scope the revert to src/postulo/plugins/*/locale.
  • Tests: tests/test_flags.py is where the flag promises live, and the browser walk will want the cards checked at 320 pixels with a long language name — #167's lesson.
A CV and a cover letter each declare the language they are written in, and nothing in *Documents* says which. The field decides the `lang` of the PDF an employer receives — which is what a recruiter's screen reader reads it out with, and what WeasyPrint hyphenates and justifies by (#223) — so it is not a detail of the edit form. Somebody keeping "Backend, English" beside "Backend, Português" has to open each one to tell them apart, or trust the name they gave it. Show the language as a flag beside the kind, the way *Settings → Language and time* shows one beside every language in the picker (#52, #88, #119), and the way #208 asks for on *Server settings → Defaults* and #214 on postal addresses. ## What it is today **`src/postulo/templates/documents/cv_list.html:30-45`** — the cards. Kind sits in a chip at the top right of each, and the language appears nowhere: ```django <div class="flex flex-wrap items-baseline justify-between gap-2"> <p class="font-medium"><bdi>{{ cv.name }}</bdi></p> <span class="chip py-0 text-xs"><span class="chip-text">{{ cv.get_kind_display }}</span></span> </div> ``` **`src/postulo/templates/documents/letter_list.html:35-50`** — rows rather than cards, but the same omission: `{{ letter.get_kind_display }}` on the secondary line, no language. **`src/postulo/templates/documents/cv_detail.html:11`** — `kind · headline · theme`, no language either. The only place a CV's language is ever named in the interface outside the form is the `{% if fell_back %}` warning at line 36, and that fires only when an entry has not been translated into it. `CV.language` and `CoverLetter.language` are `documents/models.py:120` and `:333`. Both are `blank=True`, meaning *follow your profile*. ## What already exists to reuse - **`{% flag country %}`** (`core/templatetags/postulo.py:126`) draws an SVG out of `static/flags/`, 20×15 with the ratio stated so the row does not reflow. It returns `""` for an unknown or empty country, and takes `label=` for the case where the flag has to speak for itself. - **`languages.flag_country(code)`** (`core/languages.py:123`) gives the country whose flag stands for a language, and deliberately returns `""` for the ones with no uncontested home — `ar`, `sw`, `ha` and the rest listed at `:61`. *No flag beats a wrong flag.* - **`rendering.document_language(document)`** (`documents/rendering.py:69`) already resolves the effective language: what the document says, else what its owner reads Postulo in, else the instance default. This is the value the flag should stand for, not the raw field. - **`languages.NATIVE_NAMES`** (`core/languages.py:135`) gives the language's own name for itself, for the label. - **`settings/locale.html:34,64`** is the worked example of a flag beside a language. ## Points to settle - **The flag stands alone here, so it cannot be decoration.** In the picker the language's name is right beside it and `{% flag %}`'s default `alt="" aria-hidden="true"` is correct — "Portugal flag, português (Portugal)" is worse than silence. On a card there is no name beside it, so it has to carry `label=`, and the label is the language, not the country: *Português*, never *Portugal*. `tests/test_flags.py:133` pins the distinction. - **What a language with no flag shows.** `flag_country` returns `""` for perhaps a dozen offered languages, and the card must read as well without the image as with it. Either the language's short name in the same slot, or nothing — but decided once here rather than per template. - **Whether an inherited language is drawn differently from a declared one.** A blank field means *follow your profile*, and a flag drawn for a value nobody chose invites the reading that they did. Options: draw it the same and say "follows your profile" in the label; draw it dimmed; draw nothing until the field is set. The first is probably right — what goes in the PDF is what matters, and that is the resolved value either way. - **How far it goes.** Letters share the problem and the field, so they are in scope; the chip is a row rather than a card there. Uploads and sent documents (`upload_list.html:34`, `rendered_list.html:34`) show a kind too but carry **no `language` field at all** — an uploaded certificate's language is not known, and a rendered snapshot's was fixed at the moment it was frozen. Either leave both alone, or take the rendered ones from the source document and say so; not worth widening this issue for. - **The detail pages** (`cv_detail.html:11`, `letter_detail.html`) should almost certainly gain the same flag on the same line, since that is where somebody checks a document before exporting it. ## Notes for whoever takes it - `CVListView` (`documents/views.py:67`) is scoped by `OwnedObjectMixin` (`core/mixins.py:16`), so every row on the page belongs to the same person: the profile fallback in `document_language` resolves **once for the list**, not once per card. Resolving it naively per row is an N+1 on `owner.profile`. - Adding a property to `documents/models.py` moves the `#:` source references of every translatable string below it and makes `scripts/messages.py extract --check` fail with no new string at all — #193 was exactly this. Run `extract`, then scope the revert to `src/postulo/plugins/*/locale`. - Tests: `tests/test_flags.py` is where the flag promises live, and the browser walk will want the cards checked at 320 pixels with a long language name — #167's lesson.
tiagoagueda added this to the 0.4.0 milestone 2026-09-19 09:37:12 +00:00
Author
Owner

Landed on main as 916217648.

Landed on `main` as 916217648.
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#280
No description provided.