Skip to content

feat(ui): add a language switcher to the settings dropdown - #1748

Merged
nimdanitro merged 1 commit into
developfrom
feat/language-switcher
Aug 21, 2026
Merged

feat(ui): add a language switcher to the settings dropdown#1748
nimdanitro merged 1 commit into
developfrom
feat/language-switcher

Conversation

@nimdanitro

Copy link
Copy Markdown
Member

Adds a compact DE/FR/IT picker beside the dark-mode switch in the settings dropdown.

The app has shipped four locales for a long time with no in-app way to change language — the only routes were the ?lang= querystring or clearing site data to re-trigger browser detection.

The control

A segmented control, not a cycling toggle like the dark-mode switch next to it. With two states a toggle is fine; with three, cycling makes reaching the third a guessing game, and the language currently in effect is worth showing rather than leaving the user to infer it from the surrounding text.

🌐  [ DE ][ FR ][ IT ]

Built from Bulma's buttons has-addons are-small, so it inherits the existing button styling and theming rather than introducing new CSS. One mb-0 is needed because Bulma's .buttons carries a bottom margin meant for standalone groups, which otherwise misaligns the group against the icon.

English is excluded, as requested. It is also the right call independently: the BABS symbol catalogue has no English labels — resolveBabsLang maps anything unknown onto German — so an English UI is half translated. It stays reachable via ?lang=en for anyone who wants it.

Details worth a second look

Autonyms, not translated names. The accessible name of each button is Deutsch / Français / Italiano, not a name translated into the current UI language. A language picker that names languages in a language you may not read is not much use, and this is the near-universal convention. The visible label is the abbreviation, so the autonym lives in aria-label; lang= on each button stops a screen reader pronouncing "Français" with English phonetics. Each abbreviation is also a prefix of its autonym, so WCAG 2.5.3 (Label in Name) holds.

aria-pressed, not aria-current. These are toggle buttons, not navigation, so a screen reader announces which language is in effect.

Matches on resolvedLanguage, not language. i18next collapses regional variants, so a de-CH browser — the common case for this audience — marks the German button active instead of leaving the control looking broken with nothing selected.

A language outside the three highlights nothing. An English browser resolves to en, and no button reads as active. That is deliberate: clicking any button moves to a supported language, whereas pretending German is active would contradict the text on screen. Flagging it because it is the one visible consequence of excluding English — adding en to the LANGUAGES array is a one-line change if you would rather it never happen.

No persistence work needed. i18next-browser-languagedetector is already configured to cache to session and local storage on change (i18n/index.ts), so the choice survives a reload without touching the detection config.

Also in here

Navbar.test.tsx mocked react-i18next with only t. The switcher reads i18n off the same hook, so every navbar render threw Cannot read properties of undefined (reading 'resolvedLanguage') — 5 failures. The mock now supplies i18n too.

Verified

check result
yarn test --run 172 passed, 17 files — includes 7 new
npx tsc --noEmit 111 errors, unchanged from develop baseline (measured by stashing)
yarn lint 0 errors
yarn build clean

The 7 new tests cover: the three languages are offered and English is not (asserting exactly 3 buttons, so adding a language is a deliberate change); clicking switches; only the active button is pressed; a regional variant resolves to its base; clicking the active language is a no-op; nothing is active for an unsupported language; and each button carries lang= with the autonym as its accessible name.

Rendered markup was also inspected directly to confirm the classes land where intended (is-primary is-selected on the active button only) — the part tests cannot judge.

Adds a compact DE/FR/IT picker beside the dark-mode switch. The app has
carried four locales with no way to change language short of the ?lang=
querystring or clearing site data.

A segmented control rather than a cycling toggle like the dark-mode
switch: with three options, cycling makes reaching the third a guessing
game, and the current language is worth showing rather than inferring from
the surrounding text.

English is deliberately absent, as requested. It is also the right call
independently: the BABS catalogue has no English labels — resolveBabsLang
maps anything unknown onto German — so an English UI is half translated.
It remains reachable via ?lang=en.

Buttons are labelled with autonyms (Deutsch, Français, Italiano) rather
than translated names: a picker that names languages in a language you may
not read is not much use. The visible label is the abbreviation, so the
autonym is the accessible name, and lang= on each button stops a screen
reader pronouncing "Français" with English phonetics. Active state uses
aria-pressed, since these are toggles rather than navigation.

Matching on resolvedLanguage rather than language so a regional variant
like de-CH marks the German button active instead of leaving the control
looking broken. A language outside the three highlights nothing, which is
honest — clicking any button moves to a supported one, whereas pretending
German is active would contradict the text on screen.

Persistence needs no work: the detector already caches to session and
local storage on change (see i18n/index.ts).

Navbar.test.tsx needed its react-i18next mock extended — it supplied only
`t`, and the switcher reads i18n off the same hook, so every render of the
navbar threw.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nimdanitro
nimdanitro requested a review from a team as a code owner August 21, 2026 21:02
@github-actions github-actions Bot added the feat label Aug 21, 2026
@github-code-quality

Copy link
Copy Markdown

Code Coverage Overview

Languages: TypeScript

TypeScript / code-coverage-agent

The overall line coverage in commit aebf499 in the feat/language-switch... branch is 44%. The line coverage in commit 9cdf63b in the develop branch is 37%.

Show a line coverage summary of the most impacted files.
File develop 9cdf63b feat/language-switch... aebf499 +/-
src/utils/ReloadSWPrompt.tsx 2% 2% 0%
public/map/spri...es/basemap.json 0% 100% +100%
src/components/...iconResolver.ts 0% 100% +100%
src/utils/version.ts 0% 100% +100%
src/components/...AndZoneTypes.ts 0% 100% +100%
src/components/...ageSwitcher.tsx 0% 100% +100%
src/components/...xcludedIcons.ts 0% 100% +100%
src/components/...pickerConfig.ts 0% 100% +100%
src/components/...acyAtlasKeys.ts 0% 100% +100%
src/components/...acyIconNames.ts 0% 100% +100%

@nimdanitro
nimdanitro merged commit 3e8841c into develop Aug 21, 2026
19 checks passed
@nimdanitro
nimdanitro deleted the feat/language-switcher branch August 21, 2026 21:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant