Navigation

LanguageSwitcher

A button showing the page's language that opens the others, each in its own name, as links to the same page in it.

#navigation#menu#select#popover#no-js

Demo

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/language_switcher.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/language_switcher.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/language_switcher.qwe
let language_switcher_demo_langs = [    { code: "en", label: "English", href: "#language_switcher", flag: "🇬🇧" },    { code: "pt", label: "Português", href: "#pt", flag: "🇵🇹" },    { code: "es", label: "Español", href: "#es", flag: "🇪🇸" },    { code: "de", label: "Deutsch", href: "#de", flag: "🇩🇪" },    { code: "ja", label: "日本語", href: "#ja", flag: "🇯🇵" }]component LanguageSwitcherDemoLive() {    state code = "fr"    <div.demo-row>        <LanguageSwitcher id="language-switcher-demo-live" current={code} short={true} variant="outline" languages={[            { code: "fr", label: "Français" }, { code: "it", label: "Italiano" }, { code: "nl", label: "Nederlands" }        ]} on_select={(c) => { code = c }}/>        <span.demo-note>Picked: {code}</span>    </div>}view LanguageSwitcherDemo() =    <div.demo-stack>        <div.demo-row>            <LanguageSwitcher id="language-switcher-demo" current="en" languages={language_switcher_demo_langs}/>            <LanguageSwitcher id="language-switcher-demo-end" current="ja" align="end" variant="soft" size="md"                              languages={language_switcher_demo_langs}/>        </div>        <LanguageSwitcherDemoLive/>    </div>

Source

ui/language_switcher.qwe
// LanguageSwitcher: a button showing the language the page is in, that opens the list// of the others, each written in its own language, as links to the same page in it.// It is the browser's `popover`, so Escape or a click elsewhere closes it, and it needs// no script. Each link carries `lang` and `hreflang`, so browsers and screen readers// read the names right.////   <LanguageSwitcher id="lang" current="en" languages={[//       { code: "en", label: "English", href: "/en/docs", flag: "🇬🇧" },//       { code: "pt", label: "Português", href: "/pt/docs", flag: "🇵🇹" },//       { code: "ja", label: "日本語", href: "/ja/docs", flag: "🇯🇵" }//   ]}/>//// id (required): unique on the page; the button opens the list by it.// languages: { code (a BCP 47 tag), label (the name in that language), href, flag? }.// current: the code of the page's language; the button shows it, and it is marked in// the list (aria-current="true"). An item without an href is a button that, inside one// of your components, calls on_select(code). short: the button shows the code ("EN")// instead of the name. align: "start" (default) or "end" (the list's right edge under// the button's). variant and size as a Button ("ghost", "sm"). label: what screen// readers call the list ("Language").import ui/theme.qwe use ui_clslet language_switcher_css = css`.ui-lang-btn svg { width: 16px; height: 16px; flex: none; }.ui-lang-btn .ui-lang-chev { width: 14px; height: 14px; margin-right: -4px; transition: transform .15s; }.ui-lang-btn:has(+ :popover-open) .ui-lang-chev { transform: rotate(180deg); }.ui-lang-flag { font-size: 16px; line-height: 1; }.ui-lang {  min-width: 180px; max-width: min(300px, calc(100vw - 16px)); margin: auto; padding: 5px;  border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface);  color: var(--ui-fg); box-shadow: var(--ui-shadow-lg); font: 14px/1.4 var(--ui-font);}.ui-lang ul { display: grid; gap: 1px; margin: 0; padding: 0; list-style: none; }.ui-lang-item {  display: flex; align-items: center; gap: 10px; width: 100%; padding: 8px 10px; border: 0; border-radius: var(--ui-radius-sm);  background: transparent; color: var(--ui-fg); font: inherit; text-align: left; text-decoration: none; cursor: pointer;}.ui-lang-item:hover, .ui-lang-item:focus-visible { outline: none; background: var(--ui-subtle); }.ui-lang-item:focus-visible { box-shadow: var(--ui-focus); }.ui-lang-item[aria-current] { color: var(--ui-accent); font-weight: 500; }.ui-lang-item span:nth-child(2), .ui-lang-item > span:only-child { flex: 1; }.ui-lang-code { color: var(--ui-muted); font-size: 12px; text-transform: uppercase; }.ui-lang-item svg { width: 14px; height: 14px; flex: none; color: var(--ui-accent); }.ui-lang[popover]:popover-open { animation: ui-lang-in .12s ease-out; }@keyframes ui-lang-in { from { opacity: 0; transform: translateY(-4px); } }@supports (position-area: bottom) {  .ui-lang { inset: auto; margin: 4px 0; position-area: bottom span-right; position-try-fallbacks: flip-block, flip-inline; }  .ui-lang-end { position-area: bottom span-left; }}@supports not (position-area: bottom) { .ui-lang::backdrop { background: var(--ui-overlay); } }@media (prefers-reduced-motion: reduce) { .ui-lang[popover]:popover-open { animation: none; } .ui-lang-btn .ui-lang-chev { transition: none; } }`view LanguageSwitcher(id: str, languages: list = [], current: str? = null, short: bool = false, align: str = "start",                      variant: str = "ghost", size: str = "sm", label: str = "Language", on_select = null, class: str? = null) {    style global language_switcher_css    let now = find(languages, (l) => l.code == current) ?? (len(languages) > 0 ? languages[0] : null)    <span class={class}>        <button type="button" popovertarget={id} aria-label={now != null ? `${label}: ${now.label}` : label}                class={ui_cls(["ui-btn", `ui-btn-${variant}`, size != "md" && `ui-btn-${size}`, "ui-lang-btn"])}                style={`anchor-name: --${id}`}>            {now != null && get(now, "flag") != null                ? <span.ui-lang-flag aria-hidden="true">{now.flag}</span>                : <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"                       stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/></svg>}            <span lang={now != null ? now.code : null}>{now == null ? label : short ? upper(now.code) : now.label}</span>            <svg.ui-lang-chev viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"                 stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>        </button>        <div id={id} popover="auto" class={ui_cls(["ui-lang", align == "end" && "ui-lang-end"])} style={`position-anchor: --${id}`}>            <ul aria-label={label}>                {for l in languages => {                    let here = now != null && l.code == now.code                    let body = [                        get(l, "flag") != null && <span.ui-lang-flag aria-hidden="true">{l.flag}</span>,                        <span lang={l.code}>{l.label}</span>,                        <span.ui-lang-code aria-hidden="true">{l.code}</span>,                        here && <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"                                     stroke-linejoin="round" aria-hidden="true"><path d="M3.5 8.5l3 3 6-7"/></svg>,                    ]                    <li>{get(l, "href") != null                        ? <a.ui-lang-item href={l.href} hreflang={l.code} aria-current={here ? "true" : null}>{body}</a>                        : <button.ui-lang-item type="button" popovertarget={id} popovertargetaction="hide" aria-current={here ? "true" : null}                                  @click={() => { on_select && on_select(l.code) }}>{body}</button>}</li>                }}            </ul>        </div>    </span>}
Thread
56Useful? Like it, or add to its thread below.

Thread 0 posts

On LanguageSwitcher: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.