Page language: without an offered profile language the browser decides, past the offered list and the instance default #398
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#398
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, reading the locale middleware against the Configuration page of the wiki and requesting pages with differentAccept-Languageheaders through the test client.Checked again on 2026-10-01 against
mainat0a5cad587: still present. The code is now atcore/site.py:213-232(default_language(),offered_languages()andoffers()),accounts/forms.py:202(the Use the instance default label) andaccounts/forms.py:154(the docstring oflanguage_choices()); the middleware, the settings and the test have not moved, andlocale/status.jsonstill shows Swahili and Arabic at 0 of 1873.A profile's language is applied only when the instance offers it. In every other case (a profile language that has been withdrawn, a profile set to Use the instance default, a visitor who is not signed in) the page keeps whatever Django's
LocaleMiddlewarenegotiated from the language cookie or the browser'sAccept-Language, against all 69 entries ofLANGUAGES, falling back toen-gb. Neither the offered list nor the instance's default language is consulted. A withdrawn language keeps being served to everybody whose browser asks for it, the instance default is never used, and a language whose catalogue nobody has begun is declared on pages whose text is English.core/middleware.py:135-143(UserPreferencesMiddleware.__call__):if language and not self._offered(language): language = "", thenif language: translation.activate(language), with no other branch. The choice made earlier bydjango.middleware.locale.LocaleMiddleware(config/settings/base.py:95) stands.config/settings/base.py:450-455:LANGUAGE_CODE = "en-gb"andLANGUAGES = languages.LANGUAGES, the only listLocaleMiddlewareknows.core/site.py:251-270:default_language(),offered_languages()andoffers()are read by documents, notifications and the picker, and by the middleware only to reject a profile language.accounts/forms.py:201: the empty choice is labelled Use the instance default, while the docstring oflanguage_choices()(accounts/forms.py:153) calls it "an option to follow the browser". The code does the second.tests/test_offered_languages.py:96-106:test_a_withdrawn_language_is_not_applied_to_the_pagetakesclientand never requests a page; it asserts onlysite.offers()andUserPreferencesMiddleware._offered().offered_languages=["de"],default_language="de"):fr-fr(withdrawn) withAccept-Language: frgets<html lang="fr-fr">andContent-Language: fr-fr, the very language that was withdrawn. WithAccept-Language: ptit getspt-pt, a language the instance never offered. With no header it getsen-gb, not German.lang="sw"for a Swahili browser and<html lang="ar" dir="rtl">for an Arabic one. Both catalogues stand at 0 of 1873 inlocale/status.json, so Postulo's own text is English in a page declared as Swahili or Arabic, the second laid out right to left.Configuration.md:60-62in postulo.wiki), and Accessibility says "No right-to-left language is offered yet" (Accessibility.md:182).language_choices()keeps a language whose catalogue has not begun off the picker, so that nobody is offered their language with an English interface behind it (the rule from #70). The middleware applies no such filter, so the browser reaches what the picker hides.langattribute that names the wrong language fails WCAG 2.2 success criterion 3.1.1 (Language of Page): a screen reader pronounces English text by Swahili or Arabic rules.UserPreferencesMiddleware.__call__, when no offered profile language applies, do not leaveLocaleMiddleware's answer in place unchecked. Keep it only if the instance offers it and its catalogue has begun (the two filterslanguage_choices()applies; share them as one function instead of copying them); otherwise activatesite.default_language(). Setrequest.LANGUAGE_CODEto the result either way, and leave the stored profile value untouched, as now. Then decide what the empty profile choice means and make the label, the docstring and the behaviour agree: either it follows the instance default regardless of the browser (the label as written, and what the wiki says of a withdrawn language), or it follows the browser within the offered list and is relabelled. A withdrawn profile language should get the instance default in both cases, as Configuration says.Done when
test_a_withdrawn_language_is_not_applied_to_the_pageintests/test_offered_languages.pyrequests a page and asserts itslangattribute andContent-Language: a person whose profile language is withdrawn, withAccept-Languagenaming that language and then another unoffered one, gets the instance default both times. It fails today.accounts/forms.py:201, the docstring ataccounts/forms.py:153andConfiguration.mddescribe the behaviour the tests pin.Related to #120 (closed), which let an administrator narrow the offered languages and promised that a withdrawn language gives way to the instance default; this issue adds that the page middleware never applies that default, that signed-out visitors and the empty profile choice bypass the offered list altogether, and that the test meant to guard it requests no page. Related also to #322, which is about the Defaults form, not the language a page is served in.
The maintainer's list of 2026-10-02 reports this from the user's side: the chosen language is not respected “no matter the device”, because the page follows the browser's requested language, which differs between desktop and mobile.
That fits this issue; the lines have moved. Since #393 the rule is
src/postulo/core/preferences.py:50-56, called fromUserPreferencesMiddleware(core/middleware.py:175-206). A stored language that is still offered is activated. A blank one, or a visit with nobody signed in, keeps whatLocaleMiddlewaretook fromAccept-Language(core/middleware.py:151-172). The instance default is never consulted for the page.#698 relies on this fix: its “Default” label is only true once a blank language follows the instance default.
From the list of improvements of 2026-10-02.