Language picker
The crew's choice of language for the screen, each language named in itself.
On this page
- ROTTERDAM EXPRESSCargoMMSI 244123456Dangerous targetCPA0.3 NMTCPA8 minSOG18.2 knCOG095°12 s ago
- KESTRELSailingMMSI 235098765CPA2.4 NMTCPA31 minSOG5.6 knCOG212°No report for 7 min
Code
import { AisTargetRow } from "@bosunkit/components/ais-target-row";
import { LanguagePicker } from "@bosunkit/components/language-picker";
import { Reading } from "@bosunkit/components/reading";
import {
BUNDLED_CATALOGS,
LANGUAGES,
createTranslator,
isLanguage,
type Language,
type MessageKey,
type Translator,
} from "@bosunkit/i18n";
import { BosunkitProvider, PresentationScope } from "@bosunkit/provider";
import { useMemo, useState, type CSSProperties, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"A traffic panel with the language menu in its header: choosing a language redraws the panel's kit components in " +
"it, the ship types, the dangerous target mark, the report ages and the depth's state, with the vessel's language " +
"marked in the menu.";
/** The panel's own words, which the app translates and passes in; the kit translates only its built-in words. */
const WORDS: Readonly<Record<Language, { title: string; depth: string; source: string; note: string }>> = {
en: { title: "Traffic", depth: "Depth", source: "Echo sounder", note: "This screen only." },
de: { title: "Verkehr", depth: "Tiefe", source: "Echolot", note: "Nur dieser Bildschirm." },
fr: { title: "Trafic", depth: "Profondeur", source: "Sondeur", note: "Cet écran uniquement." },
es: { title: "Tráfico", depth: "Profundidad", source: "Sonda", note: "Solo esta pantalla." },
it: { title: "Traffico", depth: "Profondità", source: "Ecoscandaglio", note: "Solo questo schermo." },
nl: { title: "Verkeer", depth: "Diepte", source: "Dieptemeter", note: "Alleen dit scherm." },
};
const PANEL: CSSProperties = {
display: "flex",
flexDirection: "column",
gap: "var(--space-md)",
padding: "var(--space-md)",
border: "1px solid var(--border)",
borderRadius: "var(--radius-lg)",
background: "var(--surface)",
};
const HEADER: CSSProperties = { display: "flex", alignItems: "center", justifyContent: "space-between" };
const LIST: CSSProperties = { display: "flex", flexDirection: "column", gap: "var(--space-sm)", margin: 0, padding: 0 };
export default function Showcase(): ReactElement {
const [language, setLanguage]: [Language, Dispatch<SetStateAction<Language>>] = useState<Language>("en");
// The app builds its translator from the catalog it fetched for the chosen language; the bundled subset here.
const translator: Translator<MessageKey> = useMemo(
(): Translator<MessageKey> =>
createTranslator<MessageKey>(language, [BUNDLED_CATALOGS[language]], [BUNDLED_CATALOGS.en]),
[language],
);
const words: { title: string; depth: string; source: string; note: string } = WORDS[language];
return (
<div style={{ width: "100%", maxWidth: 560 }}>
<BosunkitProvider i18n={translator}>
<PresentationScope lang={language}>
<section style={PANEL} aria-label={words.title}>
<div style={HEADER}>
<strong>{words.title}</strong>
<LanguagePicker
languages={LANGUAGES}
value={language}
vesselLanguage="en"
note={words.note}
onValueChange={(tag: string): void => {
if (isLanguage(tag)) {
setLanguage(tag);
}
}}
/>
</div>
<ul style={LIST}>
<AisTargetRow
name="ROTTERDAM EXPRESS"
mmsi="244123456"
shipType="cargo"
cpa={0.3}
tcpa={8}
sog={18.2}
cog={95}
cpaLimit={0.5}
tcpaLimit={12}
ageSeconds={12}
/>
<AisTargetRow
name="KESTREL"
mmsi="235098765"
shipType="sailing"
cpa={2.4}
tcpa={31}
sog={5.6}
cog={212}
cpaLimit={0.5}
tcpaLimit={12}
ageSeconds={420}
/>
</ul>
<Reading
value={8.4}
fractionDigits={1}
unit="m"
label={words.depth}
source={words.source}
integrity="stale"
ageSeconds={180}
/>
</section>
</PresentationScope>
</BosunkitProvider>
</div>
);
}
A language picker lets a crew member switch the language of the screen while using it, without leaving the page
they are on. It takes the supported languages as BCP 47 tags and the current one as props and fetches nothing: the
application passes the set from /i18n/config and stores the choice where it belongs.
Each language is named in itself (English, Deutsch, Français, Español, Italiano, Nederlands), so a crew member finds
their own language whatever the screen shows now. The others carry their name in the screen's language after a dash,
Deutsch — German on an English screen and Deutsch — Duits on a Dutch one, from the browser's Intl.DisplayNames,
with the kit's catalog for the six languages where the browser has none. Each option carries its language's lang
and dir, so a screen reader reads each name in its own language; the name in the screen's language and the vessel
default mark carry the screen's lang back.
Examples
Menu
variant="menu", the default, is for a header or the status strip: a 48 px trigger with a globe and the language's
code, named "Language: Deutsch" for a screen reader. It opens a menu in the page body on the dropdown layer, with
the current language checked. Arrows, Home and End move through it, a letter moves to the language that starts with
it, Enter chooses, and Escape closes it and returns focus to the trigger.
Code
import { LanguagePicker } from "@bosunkit/components/language-picker";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"The menu for a header or the status strip: a globe and the language's code, opening the languages, each named " +
"in itself, with a check on the current one.";
export default function Menu(): ReactElement {
const [language, setLanguage]: [string, Dispatch<SetStateAction<string>>] = useState<string>("de");
return (
<LanguagePicker
languages={["en", "de", "fr", "es", "it", "nl"]}
value={language}
onValueChange={setLanguage}
note="This screen only."
/>
);
}
List
variant="list" is for a settings page: a radio group named by its heading. Arrows move and choose, as in any radio
group; Home, End and a letter move focus only, so typing never switches the screen's language by itself. The note
says the choice follows the crew member to every screen.
For you, on every screen you sign in to.
Code
import { LanguagePicker } from "@bosunkit/components/language-picker";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"The radio list for a settings page, with a note saying the choice follows the crew member to every screen they " +
"sign in to.";
export default function List(): ReactElement {
const [language, setLanguage]: [string, Dispatch<SetStateAction<string>>] = useState<string>("en");
return (
<div style={{ width: "100%", maxWidth: 360 }}>
<LanguagePicker
variant="list"
languages={["en", "de", "fr", "es", "it", "nl"]}
value={language}
onValueChange={setLanguage}
note="For you, on every screen you sign in to."
/>
</div>
);
}
Vessel default
vesselLanguage marks the vessel's language with "Vessel default". A picker that holds its own state starts on it.
This screen only. The vessel's language is set in Settings.
Code
import { LanguagePicker } from "@bosunkit/components/language-picker";
import type { ReactElement } from "react";
export const description: string =
"The vessel's language marked in the list, and chosen at first when the picker holds its own state.";
export default function VesselDefault(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 360 }}>
<LanguagePicker
variant="list"
languages={["en", "de", "fr", "es", "it", "nl"]}
vesselLanguage="nl"
note="This screen only. The vessel's language is set in Settings."
/>
</div>
);
}
On a Dutch screen
The heading, the vessel default mark and the second name of each language follow the screen's translator.
Alleen dit scherm.
Code
import { LanguagePicker } from "@bosunkit/components/language-picker";
import { BUNDLED_CATALOGS, createTranslator, type MessageKey, type Translator } from "@bosunkit/i18n";
import { BosunkitProvider, PresentationScope } from "@bosunkit/provider";
import type { ReactElement } from "react";
export const description: string =
"On a Dutch screen: the heading and the vessel default mark in Dutch, and each other language's Dutch name after " +
"its own, Deutsch — Duits.";
const dutch: Translator<MessageKey> = createTranslator<MessageKey>("nl", [BUNDLED_CATALOGS.nl], [BUNDLED_CATALOGS.en]);
export default function DutchScreen(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 360 }}>
<BosunkitProvider i18n={dutch}>
<PresentationScope lang="nl">
<LanguagePicker
variant="list"
languages={["en", "de", "fr", "es", "it", "nl"]}
defaultValue="nl"
vesselLanguage="en"
note="Alleen dit scherm."
/>
</PresentationScope>
</BosunkitProvider>
</div>
);
}
Disabled
Sign in to choose your language. Until then this screen follows the vessel's language.
Code
import { LanguagePicker } from "@bosunkit/components/language-picker";
import type { ReactElement } from "react";
export const description: string =
"Disabled while a choice cannot be stored, with the note saying why and when it can be changed.";
export default function Disabled(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 360 }}>
<LanguagePicker
variant="list"
languages={["en", "de", "fr", "es", "it", "nl"]}
value="en"
disabled
note="Sign in to choose your language. Until then this screen follows the vessel's language."
/>
</div>
);
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| languagesrequired | readonly string[] | – | The languages to offer, as BCP 47 tags (nl, de), in the order to list them. |
| value | string | undefined | – | The chosen language, for a caller that holds the state; pair it with onValueChange. |
| defaultValue | string | undefined | – | The language chosen at first, when the caller does not hold the state; the vessel's default when unset. |
| onValueChange | ((language: string) => void) | undefined | – | Called with the tag of the language the crew choose. |
| vesselLanguage | string | undefined | – | The vessel's default language, marked "Vessel default" in the list. |
| variant | LanguagePickerVariant | undefined | "menu" | menu (the default) for a header or status strip, or list for a settings page. |
| note | string | undefined | – | Text under the languages saying where the choice applies, translated by the caller: this screen only, or this crew member on every screen. |
| label | string | undefined | – | The picker's name, translated by the caller; "Language" by default. |
| open | boolean | undefined | – | Whether the menu is open, for a caller that holds the state; pair it with onOpenChange. Menu variant only. |
| defaultOpen | boolean | undefined | – | Whether the menu starts open, when the caller does not hold the state. Menu variant only. |
| onOpenChange | ((open: boolean) => void) | undefined | – | Called with the open state a press, a choice or Escape asks for. Menu variant only. |
| disabled | boolean | undefined | – | Whether the picker takes no input. |
| motion | boolean | undefined | true | false opens the menu at once, without its fade. Menu variant only. |
| className | string | undefined | – | Classes added to the root. |
| testId | string | undefined | – | data-testid on the root; the open menu is ${testId}--menu. |
| ref | Ref<HTMLDivElement> | undefined | – | The root element. |
- Inglese
- Tedesco
- Italiano
Related
- Status strip: the header row the menu sits in.
- Setting control: the other controls a settings page holds.
- Accessibility: the target sizes and the
langa screen reader reads by.