Pick a language from a menu, with its flag and how its translation was made #119

Closed
opened 2026-09-08 18:05:04 +00:00 by tiagoagueda · 1 comment
Owner

Observation

the user should select the display languague using a drop menu (as the timezone)
the drop menu should contain the flag the language name and a symbol indicating if the
translations was done using LLM / draft

What exists

Settings → Language and time puts the time zone in a <select> and the language in
twenty-four radio rows in a card, grouped under three headings — Reviewed by a speaker,
Awaiting review by a speaker, Partly translated — with the flag drawn as an <img> and
the name carrying its own lang.

That is not an oversight, and the reason is written down in three places
(settings/locale.html, accounts/forms.py::LanguageSelect, docs/TRANSLATING.md): an
<option> holds text and nothing else.
It takes lang on itself, so a <select> can say
this option is Greek, and that is all it can do. Consequently, in a native dropdown:

  • the flag cannot be an image, and cannot be aria-hidden, so a screen reader reads
    "Greek flag, Ελληνικά" where the flag was meant to be decoration beside a name that
    already says what it is;
  • the status symbol becomes part of the option's text, so a screen reader announces it, and
    it sits inside an element marked lang="el" while being a symbol or an English word.
    That is a WCAG 2.2 3.1.2 failure introduced deliberately;
  • there is nowhere to put the percentage a partly-translated language shows today.

The list solved all three by not being a dropdown. What the request is actually about is
the shape
: twenty-four rows is a wall where the time zone beside it is one line, and it is
now thirty-nine rows and will be more.

(The platform is moving: Chromium's customisable <select> — appearance: base-select —
does allow real markup inside an option. It is one engine, and an accessibility promise made
at AA cannot rest on one engine. Worth revisiting, not worth waiting for.)

What this asks for

The shape of the request, met without the element that cannot carry it: a disclosure
that collapses to one row like the time zone field and opens to the rows that exist today.

Postulo already has this pattern twice — the account menu in the header, and the row actions
in Server settings → People, which #91 moved to "the same disclosure the account menu uses,
so it opens on a keyboard without help". The same thing here gets all of it:

Wanted How it survives
One control, not a wall of rows <details>/<summary>, closed by default, showing the current language
The flag the existing {% flag %} <img>, alt="", aria-hidden="true"
The language's name a <span lang="…">, exactly as now
A symbol for how it was translated a glyph, aria-hidden, with an sr-only phrase beside it in the interface language and outside the lang-marked span
Choosing it with scripts off <details> opens without JavaScript and the radios inside still submit

Worth being careful about

The symbol may not be the only thing that says it. A glyph alone is a 1.3.3 failure for
anybody who cannot see it and a plain guess for anybody who has not learnt it. Every symbol
needs a text equivalent that is read out, and the visible legend should stay somewhere on
the page.

"LLM" is not what the flag means. The existing group is called Awaiting review by a
speaker
for a documented reason: pt-br was seeded from pt-pt and adapted by hand, which
is not machine translation, and the thing true of every entry in that group is that no
speaker has read it. The symbol should say draft / unreviewed, not machine-made.

Keyboard behaviour is the whole cost of not using <select>. A native dropdown gives
arrow keys, Home/End, type-ahead, Escape and a focus trap for free. The disclosure has to be
given them, and the browser suite has to walk it with a keyboard and through axe in both
themes and both directions.

The current language has to be visible while closed — flag, name and status — or the
setting page stops answering "what am I using?" without a click.

Right to left. The panel is positioned with logical properties only; the template lint
already fails a physical one.

## Observation > the user should select the display languague using a drop menu (as the timezone) > the drop menu should contain the flag the language name and a symbol indicating if the > translations was done using LLM / draft ## What exists *Settings → Language and time* puts the time zone in a `<select>` and the language in twenty-four radio rows in a card, grouped under three headings — *Reviewed by a speaker*, *Awaiting review by a speaker*, *Partly translated* — with the flag drawn as an `<img>` and the name carrying its own `lang`. That is not an oversight, and the reason is written down in three places (`settings/locale.html`, `accounts/forms.py::LanguageSelect`, `docs/TRANSLATING.md`): **an `<option>` holds text and nothing else.** It takes `lang` on itself, so a `<select>` can say *this option is Greek*, and that is all it can do. Consequently, in a native dropdown: - the flag cannot be an image, and cannot be `aria-hidden`, so a screen reader reads "Greek flag, Ελληνικά" where the flag was meant to be decoration beside a name that already says what it is; - the status symbol becomes part of the option's text, so a screen reader announces it, and it sits *inside* an element marked `lang="el"` while being a symbol or an English word. That is a WCAG 2.2 3.1.2 failure introduced deliberately; - there is nowhere to put the percentage a partly-translated language shows today. The list solved all three by not being a dropdown. **What the request is actually about is the shape**: twenty-four rows is a wall where the time zone beside it is one line, and it is now thirty-nine rows and will be more. (The platform is moving: Chromium's customisable `<select>` — `appearance: base-select` — does allow real markup inside an option. It is one engine, and an accessibility promise made at AA cannot rest on one engine. Worth revisiting, not worth waiting for.) ## What this asks for The **shape** of the request, met without the element that cannot carry it: a disclosure that collapses to one row like the time zone field and opens to the rows that exist today. Postulo already has this pattern twice — the account menu in the header, and the row actions in *Server settings → People*, which #91 moved to "the same disclosure the account menu uses, so it opens on a keyboard without help". The same thing here gets all of it: | Wanted | How it survives | | --- | --- | | One control, not a wall of rows | `<details>`/`<summary>`, closed by default, showing the current language | | The flag | the existing `{% flag %}` `<img>`, `alt=""`, `aria-hidden="true"` | | The language's name | a `<span lang="…">`, exactly as now | | A symbol for how it was translated | a glyph, `aria-hidden`, with an `sr-only` phrase beside it in the **interface** language and outside the `lang`-marked span | | Choosing it with scripts off | `<details>` opens without JavaScript and the radios inside still submit | ## Worth being careful about **The symbol may not be the only thing that says it.** A glyph alone is a 1.3.3 failure for anybody who cannot see it and a plain guess for anybody who has not learnt it. Every symbol needs a text equivalent that is read out, and the visible legend should stay somewhere on the page. **"LLM" is not what the flag means.** The existing group is called *Awaiting review by a speaker* for a documented reason: `pt-br` was seeded from `pt-pt` and adapted by hand, which is not machine translation, and the thing true of every entry in that group is that no speaker has read it. The symbol should say **draft / unreviewed**, not *machine-made*. **Keyboard behaviour is the whole cost of not using `<select>`.** A native dropdown gives arrow keys, Home/End, type-ahead, Escape and a focus trap for free. The disclosure has to be given them, and the browser suite has to walk it with a keyboard and through axe in both themes and both directions. **The current language has to be visible while closed** — flag, name and status — or the setting page stops answering "what am I using?" without a click. **Right to left.** The panel is positioned with logical properties only; the template lint already fails a physical one.
tiagoagueda added this to the 0.3.0 milestone 2026-09-08 18:05:04 +00:00
Author
Owner

The request asked for a flag, a language name and a symbol in one dropdown — which is
exactly the combination an <option> cannot hold. So the shape is what changed, and it
is the shape the issue said the request was really about: a disclosure, closed to one line
like the time zone beside it, open to the rows that were there before.

Every row from the table in the issue:

Wanted How it survives
One control, not a wall of rows <details data-menu>, closed, showing the language in use
The flag the existing {% flag %} image, alt="", aria-hidden
The language's name <span lang="…">, exactly as before
A symbol for how it was translated a glyph, aria-hidden, with an sr-only phrase in the interface language, outside the lang span
Choosing it with scripts off the disclosure opens and the radios submit, with no JavaScript at all

One thing got better rather than merely surviving. The percentage a partly-translated
language shows was being appended to the language's own name, inside the span marked as
being in that language, because a dropdown had nowhere else to put it. It has somewhere else
now.

The five warnings

The symbol may not be the only thing that says it. Each has words read out beside it, and
the legend is on the page rather than in a tooltip — which is not a thing a keyboard or a
touchscreen reaches.

"LLM" is not what the flag means. The wording is written, not yet read by a speaker.
Nothing in the interface says machine or LLM, and a test asserts that.

Keyboard behaviour is the whole cost. Paid, and in a browser: arrow keys come from the
radios; Home, End, type-ahead on each language's own name, Escape and returning the focus are
given back in app.js. Opening it also puts the focus on the current language rather than
leaving somebody to tab past the list. Eight browser tests walk it, and the accessibility
sweep covers the page in both themes.

The current language visible while closed. Flag, name and state, asserted twice — once in
the rendered HTML and once through a browser after saving.

Right to left. Logical properties only; the template lint already fails a physical one.

One thing found on the way

The browser suite broke — not on this page, on test_people_menu, with a 429 from the
sign-in limiter. Every browser test signs in, all from one address, and the limiter cannot
tell a suite from an attacker; adding eight tests crossed the allowance, and which test
failed depended on how many had run first. The limiter is now emptied before each browser
test rather than switched off, so it is still the real one and
tests/security/test_rate_limits.py still holds it to its numbers.

13 unit tests, 8 browser tests, 4 new strings in all 39 European catalogues.

Shipped in 86315ea on 0.3.0, with main kept level. The note about Chromium's
customisable <select> still stands: worth revisiting when it is not one engine.

The request asked for a flag, a language name and a symbol in one dropdown — which is exactly the combination an `<option>` cannot hold. So the **shape** is what changed, and it is the shape the issue said the request was really about: a disclosure, closed to one line like the time zone beside it, open to the rows that were there before. Every row from the table in the issue: | Wanted | How it survives | | --- | --- | | One control, not a wall of rows | `<details data-menu>`, closed, showing the language in use | | The flag | the existing `{% flag %}` image, `alt=""`, `aria-hidden` | | The language's name | `<span lang="…">`, exactly as before | | A symbol for how it was translated | a glyph, `aria-hidden`, with an `sr-only` phrase in the *interface* language, outside the `lang` span | | Choosing it with scripts off | the disclosure opens and the radios submit, with no JavaScript at all | **One thing got better rather than merely surviving.** The percentage a partly-translated language shows was being appended to the language's own name, *inside* the span marked as being in that language, because a dropdown had nowhere else to put it. It has somewhere else now. ## The five warnings **The symbol may not be the only thing that says it.** Each has words read out beside it, and the legend is on the page rather than in a tooltip — which is not a thing a keyboard or a touchscreen reaches. **"LLM" is not what the flag means.** The wording is *written, not yet read by a speaker*. Nothing in the interface says machine or LLM, and a test asserts that. **Keyboard behaviour is the whole cost.** Paid, and in a browser: arrow keys come from the radios; Home, End, type-ahead on each language's own name, Escape and returning the focus are given back in `app.js`. Opening it also puts the focus on the current language rather than leaving somebody to tab past the list. Eight browser tests walk it, and the accessibility sweep covers the page in both themes. **The current language visible while closed.** Flag, name and state, asserted twice — once in the rendered HTML and once through a browser after saving. **Right to left.** Logical properties only; the template lint already fails a physical one. ## One thing found on the way The browser suite broke — not on this page, on `test_people_menu`, with a `429` from the sign-in limiter. Every browser test signs in, all from one address, and the limiter cannot tell a suite from an attacker; adding eight tests crossed the allowance, and *which* test failed depended on how many had run first. The limiter is now emptied before each browser test rather than switched off, so it is still the real one and `tests/security/test_rate_limits.py` still holds it to its numbers. 13 unit tests, 8 browser tests, 4 new strings in all 39 European catalogues. Shipped in `86315ea` on `0.3.0`, with `main` kept level. The note about Chromium's customisable `<select>` still stands: worth revisiting when it is not one engine.
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#119
No description provided.