Say which language and time zone “the default” is, with its flag and name, in Settings and Server settings #698

Closed
opened 2026-10-03 01:59:09 +00:00 by tiagoagueda · 0 comments
Owner

Where a person, or an administrator, has chosen nothing, the language and the time zone menus say “Use the instance default” and leave them to guess what that is. The maintainer's list asks for the answer itself, in Settings → Language and time and in Server settings → Defaults: for the language, "Flag + Language (Region) + greyed BCP 47 code + ‘ - Default’"; for the time zone, "Timezone + ‘ - Default’".

What exists today

  • The blank choice of both menus is the string “Use the instance default”: for the language at src/postulo/accounts/forms.py:219 (first entry of language_choices()), for the time zone at :289 (first entry of time_zone_choices()). Neither function is told what the default is. The string is in all 68 catalogues (src/postulo/locale/*/LC_MESSAGES/django.po).
  • Settings → Language and time. The language is a disclosure of radio rows, not a <select> (templates/settings/locale.html:28-101). LocaleForm.language_groups() builds the rows and writes the first one by hand from language_choices()[0]: code blank, name “Use the instance default”, no flag, no state (accounts/forms.py:1037-1053); language_now() returns the selected row for the closed summary (:1087-1096). A row's name sits in a <span lang="…"> and the words about it, in the interface language, outside it (partials/translation_state.html); the blank row has no lang, and tests/test_translations.py:411-416 asserts that (lang="" must not appear). The time zone is a plain field with the blank option first (settings/locale.html, c-field :field="form.time_zone"; options name.replace("_", " "), accounts/forms.py:289-291).
  • Server settings → Defaults. The same two functions feed DefaultsForm (core/server_forms.py:259-282). There the blank choice means something else: nothing stored, so the built-in fallback, which is settings.LANGUAGE_CODE (the source language, config/settings/base.py:458) and settings.TIME_ZONE (POSTULO_TIME_ZONE, Europe/Paris unless set, :468). Labelled “Use the instance default” on the page that sets the instance default. The language is a native <select> with the chosen flag drawn beside it by script (templates/server/defaults.html:30-35, data-flag-select, #208). Under the card, “In force now: {language}, {zone}.” prints raw values: a language code such as en-GB and a zone name (defaults.html:48-52; DefaultsView.get_context_data already computes effective_language and effective_time_zone, core/server_views.py:573-579; site.default_language() and site.default_time_zone() are core/site.py:207-215).
  • What the blank choice does to a page. The time zone does what it says: UserPreferencesMiddleware calls preferences.apply, which takes the person's zone, else the instance default (src/postulo/core/preferences.py:34-42, instance_zone() at :69; the call is at core/middleware.py:206). The language does not. A profile with no language is left to whatever LocaleMiddleware negotiated from the browser's Accept-Language; the instance default is never consulted (core/preferences.py:50-56, where the rule moved with #393; it was in the middleware before). apply's own docstring says it: without a profile “the language is left as it stands, which is whatever LocaleMiddleware took from the request”. That is #398, open, and it is also the maintainer's own report in this list ("Defined displayed language should be respected no matter the device. Currently it follows the demanded browser language, different from desktop and mobile browser"). So a label that says “English (United Kingdom) — Default” would be wrong on the page it is written on until #398 is fixed.
  • Names. core/languages.py holds each language's name in itself, NATIVE_NAMES (:139-212), 69 tags. Exactly five carry anything beyond the language: en-GB “English (United Kingdom)”, fr-FR “français (France)”, pt-PT “português (Portugal)”, pt-BR “português (Brasil)” (a region each, written by hand) and sr-Cyrl “српски” (a script, not in the name). The other 64 are the language alone (“Deutsch”, “español”). languages.flag_country(code) gives the country whose flag stands for a language; 13 languages have none by design (ar ee ff ha ig ln om ss st sw ti tn yo, core/languages.py:60-66), and {% language_flag %} draws nothing for them (core/templatetags/postulo.py:94-103).
  • The document-language menus do not use this function: documents/forms.py:32 has a language_choices() of its own (blank means “Follow your profile”). Only Settings and Server settings read accounts.forms.language_choices, and so does LocaleForm.language_groups().

Checked against main at f09b6010b.

Proposal

  1. Tell the two choice builders what the default is. language_choices(default=…) and time_zone_choices(default=…) take the value the blank choice stands for: in Settings, site.default_language() and site.default_time_zone(); in Server settings → Defaults, the built-in settings.LANGUAGE_CODE and settings.TIME_ZONE. The stored value of the blank choice stays the empty string, so it still means “follow the default”, not “pin today's default”.
  2. The language default row. The first row of the picker and the closed summary show, in this order: the flag of flag_country(code) (decorative, as every flag here); the language's name in its own language in the lang span; the BCP 47 tag, in the secondary text colour, in a <bdi dir="ltr"> so a right-to-left page does not reorder en-GB; then the word “Default” with its separator, outside the lang span and in the interface language, as the translation state is. The row has a lang like every other row, so the assertion at tests/test_translations.py:411-416 becomes “the default row's name carries its own language and no text outside it does”.
  3. The time zone default. The blank option reads “Europe/Paris — Default” (the zone as the list writes its own, spaces for underscores), through one message with a placeholder so a translation can order and punctuate it.
  4. Server settings → Defaults. The same two labels on its blank options (the list names the language there; the time zone is the same blank option on the same page, so it is done with it). A native <option> holds text only, so there the flag stays where it is (beside the closed select, drawn by script) and the code and the word follow the name in plain text; the grey code appears wherever the control can draw rows, which is the picker on Settings now and every select once #301 lands. “In force now” names the language the same way instead of printing a code.
  5. Strings. The new message(s) go in the working catalogues (en-GB, fr-FR and pt-PT as drafts) and in the other 66, where “Use the instance default” leaves; Weblate carries the change (#349).

Decisions

  • Order with #398. Show the language label once the page really uses the default. Recommended: fix #398 first (same milestone) with its outcome “a blank profile language follows the instance default, never the browser”, which is what this list also says; this issue then lands as written. If #398 chooses the other outcome (follow the browser within the offered list), the blank row cannot name one language and this issue's language half has to be redone as “Your browser's language”. The time zone half is true today and can land at once.
  • “Language (Region)” for a language whose tag has no region. The list's example has a region in brackets. Of the 69 tags five carry something beyond the language, and NATIVE_NAMES already gives the region where there is one. For the other 64 there is no region to show. Options: show the language alone; or add a region taken from the flag's country (“Deutsch (Deutschland)” for de); or take regional names from CLDR (Babel, not a dependency: see #689). Recommended: the language alone, and a region exactly where the tag has one. A region the tag does not carry is a claim the code never made (de is not de-DE), and 13 languages have no country at all. If every name must read “Language (Region)”, that is part of #689, not this issue.
  • The separator. The list writes “ - Default” with a hyphen. The masthead's own string is “{name} — dashboard” (base.html:112). Recommended: a spaced em dash, in one message with a placeholder so a language can change it.
  • The word “Default”. The catalogues already hold msgid "Default" for the button-size menu (core/design.py:90); reusing it would tie two meanings to one translation. Recommended: a new message with a context saying it names the value used when nothing was chosen.
  • The code and a screen reader. The tag is useful to a sighted administrator comparing catalogues and noise when read letter by letter. Recommended: aria-hidden on the code, since the name already says what it is; the flag stays decorative.
  • The default is not an offered language. An administrator may have a default that the offered list excludes, or whose catalogue has not begun (language_choices() skips both). Recommended: the row still names it, because it is what the page is in once #398 is fixed; it is a statement of fact about the instance, not an offer.
  • A duplicate of a listed row. The default language is also a row further down; choosing that one pins it. Recommended: leave both; the first says “follow the default”, the second says “this one, whatever the default becomes”.

Builds on

#398 (the page ignores the instance default for the language), #208 (a flag beside each language on Defaults), #119 (the picker as rows, the state words outside the lang span), #64 (each language named in itself), #337 (BCP 47 tags), #312 (the translation bar on Defaults), #322 (a region survives in a name), #301 (Basecoat's select, where each option can be a row). #689 is the general source of names with a region. #492 and #426 are open bugs on the same two forms and do not overlap.

Done when

  • Settings → Language and time with no stored language shows, in the closed summary and as the first row, the flag, the name in its own lang, the grey tag and “Default”; with the instance default set to de it is “Deutsch de — Default” and no flag is a wrong one; for a language with no flag the row closes up. New cases in tests/test_language_picker.py and tests/test_translations.py (the lang assertion above rewritten, not removed); tests/e2e/test_language_picker.py:100 still finds the first row's value empty.
  • The time zone's blank option reads the instance default zone, with Europe/Paris and an administrator-stored zone both covered, and the environment-pinned case (site.default_time_zone() returns settings.TIME_ZONE when the environment overrides it, core/site.py:207-210). tests/test_settings.py.
  • Server settings → Defaults labels its two blank options with the built-in fallback, and “In force now” names the language rather than printing a code. tests/test_server_settings.py.
  • The document-language and career-record-language menus are untouched (tests/test_language_of_the_document.py, tests/test_career_languages.py still pass).
  • The grey tag meets contrast in both themes (tests/test_contrast.py) and the axe run over both pages (tests/e2e/test_accessibility.py) is clean; a right-to-left page keeps the tag whole (tests/e2e/test_direction.py).
  • The new messages are in en-GB, fr-FR and pt-PT as drafts and in the other 66 catalogues by scripts/messages.py extract; Weblate (#349) shows “Use the instance default” gone and the new message waiting. The wiki's Configuration page names the labels in the same commit.

From the list of improvements of 2026-10-02.

Where a person, or an administrator, has chosen nothing, the language and the time zone menus say “Use the instance default” and leave them to guess what that is. The maintainer's list asks for the answer itself, in Settings → Language and time and in Server settings → Defaults: for the language, "Flag + Language (Region) + greyed BCP 47 code + ‘ - Default’"; for the time zone, "Timezone + ‘ - Default’". ## What exists today - The blank choice of both menus is the string “Use the instance default”: for the language at `src/postulo/accounts/forms.py:219` (first entry of `language_choices()`), for the time zone at `:289` (first entry of `time_zone_choices()`). Neither function is told what the default is. The string is in all 68 catalogues (`src/postulo/locale/*/LC_MESSAGES/django.po`). - **Settings → Language and time.** The language is a disclosure of radio rows, not a `<select>` (`templates/settings/locale.html:28-101`). `LocaleForm.language_groups()` builds the rows and writes the first one by hand from `language_choices()[0]`: code blank, name “Use the instance default”, no flag, no state (`accounts/forms.py:1037-1053`); `language_now()` returns the selected row for the closed summary (`:1087-1096`). A row's name sits in a `<span lang="…">` and the words about it, in the interface language, outside it (`partials/translation_state.html`); the blank row has no `lang`, and `tests/test_translations.py:411-416` asserts that (`lang=""` must not appear). The time zone is a plain field with the blank option first (`settings/locale.html`, `c-field :field="form.time_zone"`; options `name.replace("_", " ")`, `accounts/forms.py:289-291`). - **Server settings → Defaults.** The same two functions feed `DefaultsForm` (`core/server_forms.py:259-282`). There the blank choice means something else: nothing stored, so the built-in fallback, which is `settings.LANGUAGE_CODE` (the source language, `config/settings/base.py:458`) and `settings.TIME_ZONE` (`POSTULO_TIME_ZONE`, `Europe/Paris` unless set, `:468`). Labelled “Use the instance default” on the page that sets the instance default. The language is a native `<select>` with the chosen flag drawn beside it by script (`templates/server/defaults.html:30-35`, `data-flag-select`, #208). Under the card, “In force now: {language}, {zone}.” prints raw values: a language code such as `en-GB` and a zone name (`defaults.html:48-52`; `DefaultsView.get_context_data` already computes `effective_language` and `effective_time_zone`, `core/server_views.py:573-579`; `site.default_language()` and `site.default_time_zone()` are `core/site.py:207-215`). - **What the blank choice does to a page.** The time zone does what it says: `UserPreferencesMiddleware` calls `preferences.apply`, which takes the person's zone, else the instance default (`src/postulo/core/preferences.py:34-42`, `instance_zone()` at `:69`; the call is at `core/middleware.py:206`). **The language does not.** A profile with no language is left to whatever `LocaleMiddleware` negotiated from the browser's `Accept-Language`; the instance default is never consulted (`core/preferences.py:50-56`, where the rule moved with #393; it was in the middleware before). `apply`'s own docstring says it: without a profile “the language is left as it stands, which is whatever `LocaleMiddleware` took from the request”. That is #398, open, and it is also the maintainer's own report in this list ("Defined displayed language should be respected no matter the device. Currently it follows the demanded browser language, different from desktop and mobile browser"). So a label that says “English (United Kingdom) — Default” would be wrong on the page it is written on until #398 is fixed. - Names. `core/languages.py` holds each language's name in itself, `NATIVE_NAMES` (`:139-212`), 69 tags. Exactly five carry anything beyond the language: `en-GB` “English (United Kingdom)”, `fr-FR` “français (France)”, `pt-PT` “português (Portugal)”, `pt-BR` “português (Brasil)” (a region each, written by hand) and `sr-Cyrl` “српски” (a script, not in the name). The other 64 are the language alone (“Deutsch”, “español”). `languages.flag_country(code)` gives the country whose flag stands for a language; 13 languages have none by design (`ar ee ff ha ig ln om ss st sw ti tn yo`, `core/languages.py:60-66`), and `{% language_flag %}` draws nothing for them (`core/templatetags/postulo.py:94-103`). - The document-language menus do not use this function: `documents/forms.py:32` has a `language_choices()` of its own (blank means “Follow your profile”). Only Settings and Server settings read `accounts.forms.language_choices`, and so does `LocaleForm.language_groups()`. Checked against `main` at `f09b6010b`. ## Proposal 1. **Tell the two choice builders what the default is.** `language_choices(default=…)` and `time_zone_choices(default=…)` take the value the blank choice stands for: in Settings, `site.default_language()` and `site.default_time_zone()`; in Server settings → Defaults, the built-in `settings.LANGUAGE_CODE` and `settings.TIME_ZONE`. The stored value of the blank choice stays the empty string, so it still means “follow the default”, not “pin today's default”. 2. **The language default row.** The first row of the picker and the closed summary show, in this order: the flag of `flag_country(code)` (decorative, as every flag here); the language's name in its own language in the `lang` span; the BCP 47 tag, in the secondary text colour, in a `<bdi dir="ltr">` so a right-to-left page does not reorder `en-GB`; then the word “Default” with its separator, **outside** the `lang` span and in the interface language, as the translation state is. The row has a `lang` like every other row, so the assertion at `tests/test_translations.py:411-416` becomes “the default row's name carries its own language and no text outside it does”. 3. **The time zone default.** The blank option reads “Europe/Paris — Default” (the zone as the list writes its own, spaces for underscores), through one message with a placeholder so a translation can order and punctuate it. 4. **Server settings → Defaults.** The same two labels on its blank options (the list names the language there; the time zone is the same blank option on the same page, so it is done with it). A native `<option>` holds text only, so there the flag stays where it is (beside the closed select, drawn by script) and the code and the word follow the name in plain text; the grey code appears wherever the control can draw rows, which is the picker on Settings now and every select once #301 lands. “In force now” names the language the same way instead of printing a code. 5. **Strings.** The new message(s) go in the working catalogues (en-GB, fr-FR and pt-PT as drafts) and in the other 66, where “Use the instance default” leaves; Weblate carries the change (#349). ## Decisions - **Order with #398.** Show the language label once the page really uses the default. **Recommended:** fix #398 first (same milestone) with its outcome “a blank profile language follows the instance default, never the browser”, which is what this list also says; this issue then lands as written. If #398 chooses the other outcome (follow the browser within the offered list), the blank row cannot name one language and this issue's language half has to be redone as “Your browser's language”. The time zone half is true today and can land at once. - **“Language (Region)” for a language whose tag has no region.** The list's example has a region in brackets. Of the 69 tags five carry something beyond the language, and `NATIVE_NAMES` already gives the region where there is one. For the other 64 there is no region to show. Options: show the language alone; or add a region taken from the flag's country (“Deutsch (Deutschland)” for `de`); or take regional names from CLDR (Babel, not a dependency: see #689). **Recommended:** the language alone, and a region exactly where the tag has one. A region the tag does not carry is a claim the code never made (`de` is not `de-DE`), and 13 languages have no country at all. If every name must read “Language (Region)”, that is part of #689, not this issue. - **The separator.** The list writes “ - Default” with a hyphen. The masthead's own string is “{name} — dashboard” (`base.html:112`). **Recommended:** a spaced em dash, in one message with a placeholder so a language can change it. - **The word “Default”.** The catalogues already hold `msgid "Default"` for the button-size menu (`core/design.py:90`); reusing it would tie two meanings to one translation. **Recommended:** a new message with a context saying it names the value used when nothing was chosen. - **The code and a screen reader.** The tag is useful to a sighted administrator comparing catalogues and noise when read letter by letter. **Recommended:** `aria-hidden` on the code, since the name already says what it is; the flag stays decorative. - **The default is not an offered language.** An administrator may have a default that the offered list excludes, or whose catalogue has not begun (`language_choices()` skips both). **Recommended:** the row still names it, because it is what the page is in once #398 is fixed; it is a statement of fact about the instance, not an offer. - **A duplicate of a listed row.** The default language is also a row further down; choosing that one pins it. **Recommended:** leave both; the first says “follow the default”, the second says “this one, whatever the default becomes”. ## Builds on #398 (the page ignores the instance default for the language), #208 (a flag beside each language on Defaults), #119 (the picker as rows, the state words outside the `lang` span), #64 (each language named in itself), #337 (BCP 47 tags), #312 (the translation bar on Defaults), #322 (a region survives in a name), #301 (Basecoat's select, where each option can be a row). #689 is the general source of names with a region. #492 and #426 are open bugs on the same two forms and do not overlap. ## Done when - Settings → Language and time with no stored language shows, in the closed summary and as the first row, the flag, the name in its own `lang`, the grey tag and “Default”; with the instance default set to `de` it is “Deutsch de — Default” and no flag is a wrong one; for a language with no flag the row closes up. New cases in `tests/test_language_picker.py` and `tests/test_translations.py` (the `lang` assertion above rewritten, not removed); `tests/e2e/test_language_picker.py:100` still finds the first row's value empty. - The time zone's blank option reads the instance default zone, with `Europe/Paris` and an administrator-stored zone both covered, and the environment-pinned case (`site.default_time_zone()` returns `settings.TIME_ZONE` when the environment overrides it, `core/site.py:207-210`). `tests/test_settings.py`. - Server settings → Defaults labels its two blank options with the built-in fallback, and “In force now” names the language rather than printing a code. `tests/test_server_settings.py`. - The document-language and career-record-language menus are untouched (`tests/test_language_of_the_document.py`, `tests/test_career_languages.py` still pass). - The grey tag meets contrast in both themes (`tests/test_contrast.py`) and the axe run over both pages (`tests/e2e/test_accessibility.py`) is clean; a right-to-left page keeps the tag whole (`tests/e2e/test_direction.py`). - The new messages are in en-GB, fr-FR and pt-PT as drafts and in the other 66 catalogues by `scripts/messages.py extract`; Weblate (#349) shows “Use the instance default” gone and the new message waiting. The wiki's *Configuration* page names the labels in the same commit. From the list of improvements of 2026-10-02.
tiagoagueda added this to the 0.5.0 milestone 2026-10-03 01:59:09 +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#698
No description provided.