Forms

LanguagePicker

A drop-down of languages each written in its own name, sending the ISO 639-1 code with a form.

#forms#select#picker#accessibility#no-js

Demo

Preview

Each language is written in itself.

The site is shown in 日本語 (ja).

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_picker.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/language_picker.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/language_picker.qwe
component LanguagePickerDemoLive() {    state code = "ja"    <div.demo-stack>        <LanguagePicker name="ui_lang" label="Show the site in" value={code} field="language-picker-demo-live"                        only={["en", "fr", "de", "es", "pt", "ja", "zh", "ar"]} on_change={(c) => { code = c }}/>        <p.demo-note>The site is shown in {language_picker_name(code)} ({code}).</p>    </div>}view LanguagePickerDemo() =    <div.demo-stack.demo-narrow>        <LanguagePicker name="lang" label="Language" value="pt" field="language-picker-demo-all" favourites={["en", "pt"]}                        hint="Each language is written in itself." required={true}/>        <LanguagePickerDemoLive/>        <div.demo-row>            <LanguagePicker name="lang_en" label="With English names" size="sm" english={true} placeholder="Choose one"                            field="language-picker-demo-english" only={["de", "ja", "ko", "en"]}/>            <LanguagePicker name="locked" label="Disabled" size="sm" value="fr" only={["fr"]} disabled={true}/>        </div>    </div>

Source

ui/language_picker.qwe
// LanguagePicker: a drop-down of languages, each written in its own name// ("Deutsch", "日本語"), so anyone finds theirs whatever the page's language. It is// the browser's own <select>, so a plain <form> sends the ISO 639-1 code ("de")// as `name`, and it works with the keyboard.////   <LanguagePicker name="lang" label="Language" value="pt"/>//   <LanguagePicker name="ui_lang" label="Show the site in" only={["en", "fr", "de", "es"]}/>//   <LanguagePicker name="lang" label="Language" english={true} favourites={["en"]}/>//// value: the code chosen to start with. only: the codes to offer, in place of the// built-in list of about seventy (`language_picker_languages`, a list of// [code, own name, English name]). favourites: codes listed first, under "Common".// english (false): also the English name after the own one ("Deutsch (German)").// placeholder: a first line that cannot be chosen. hint, error, required,// disabled, field (the id, default "language-picker-" + name) and size ("sm" or// "md") as in Select. Each option carries lang="<code>", so screen readers say// the name in its language.// on_change(code): inside one of your components, given the code chosen.import ui/theme.qwe use ui_clslet language_picker_css = css`.ui-language-picker { display: grid; gap: 6px; min-width: 0; font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-language-picker-label { font-size: 13px; font-weight: 500; color: var(--ui-fg); }.ui-language-picker-req { margin-left: 2px; color: var(--ui-danger); }.ui-language-picker-box { position: relative; display: block; }.ui-language-picker-box::after {  content: ""; position: absolute; right: 14px; top: 50%; width: 7px; height: 7px; margin-top: -6px;  border: solid var(--ui-muted); border-width: 0 1.5px 1.5px 0; transform: rotate(45deg); pointer-events: none;}.ui-language-picker select {  appearance: none; display: block; width: 100%; height: 38px; padding: 0 36px 0 12px;  background: var(--ui-surface); color: var(--ui-fg); font: 14px var(--ui-font);  border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); cursor: pointer;  text-overflow: ellipsis; transition: border-color .15s, box-shadow .15s;}.ui-language-picker select:hover { border-color: var(--ui-strong); }.ui-language-picker select:focus-visible { outline: none; border-color: var(--ui-accent); box-shadow: var(--ui-focus); }.ui-language-picker select:has(option[value=""]:checked) { color: var(--ui-muted); }.ui-language-picker option { color: var(--ui-fg); background: var(--ui-surface); }.ui-language-picker-sm select { height: 30px; padding: 0 30px 0 10px; font-size: 13px; }.ui-language-picker-sm .ui-language-picker-box::after { right: 11px; width: 6px; height: 6px; margin-top: -5px; }.ui-language-picker-invalid select, .ui-language-picker-invalid select:hover { border-color: var(--ui-danger); }.ui-language-picker-hint, .ui-language-picker-error { margin: 0; font-size: 13px; color: var(--ui-muted); }.ui-language-picker-error { color: var(--ui-danger); }.ui-language-picker-disabled { opacity: .55; }.ui-language-picker-disabled select { cursor: not-allowed; }@media (prefers-reduced-motion: reduce) { .ui-language-picker select { transition: none; } }`// [ISO 639-1 code, the language's own name, its English name], in the order of the English names.let language_picker_languages = [    ["af", "Afrikaans", "Afrikaans"], ["sq", "Shqip", "Albanian"], ["am", "አማርኛ", "Amharic"], ["ar", "العربية", "Arabic"],    ["hy", "Հայերեն", "Armenian"], ["az", "Azərbaycan dili", "Azerbaijani"], ["eu", "Euskara", "Basque"],    ["be", "Беларуская", "Belarusian"], ["bn", "বাংলা", "Bengali"], ["bs", "Bosanski", "Bosnian"], ["bg", "Български", "Bulgarian"],    ["my", "မြန်မာဘာသာ", "Burmese"], ["ca", "Català", "Catalan"], ["zh", "中文", "Chinese"], ["hr", "Hrvatski", "Croatian"],    ["cs", "Čeština", "Czech"], ["da", "Dansk", "Danish"], ["nl", "Nederlands", "Dutch"], ["en", "English", "English"],    ["et", "Eesti", "Estonian"], ["fi", "Suomi", "Finnish"], ["fr", "Français", "French"], ["gl", "Galego", "Galician"],    ["ka", "ქართული", "Georgian"], ["de", "Deutsch", "German"], ["el", "Ελληνικά", "Greek"], ["gu", "ગુજરાતી", "Gujarati"],    ["he", "עברית", "Hebrew"], ["hi", "हिन्दी", "Hindi"], ["hu", "Magyar", "Hungarian"], ["is", "Íslenska", "Icelandic"],    ["id", "Bahasa Indonesia", "Indonesian"], ["ga", "Gaeilge", "Irish"], ["it", "Italiano", "Italian"], ["ja", "日本語", "Japanese"],    ["kn", "ಕನ್ನಡ", "Kannada"], ["kk", "Қазақ тілі", "Kazakh"], ["km", "ខ្មែរ", "Khmer"], ["ko", "한국어", "Korean"],    ["lo", "ລາວ", "Lao"], ["lv", "Latviešu", "Latvian"], ["lt", "Lietuvių", "Lithuanian"], ["mk", "Македонски", "Macedonian"],    ["ms", "Bahasa Melayu", "Malay"], ["ml", "മലയാളം", "Malayalam"], ["mt", "Malti", "Maltese"], ["mr", "मराठी", "Marathi"],    ["mn", "Монгол", "Mongolian"], ["ne", "नेपाली", "Nepali"], ["nb", "Norsk bokmål", "Norwegian"], ["fa", "فارسی", "Persian"],    ["pl", "Polski", "Polish"], ["pt", "Português", "Portuguese"], ["pa", "ਪੰਜਾਬੀ", "Punjabi"], ["ro", "Română", "Romanian"],    ["ru", "Русский", "Russian"], ["sr", "Српски", "Serbian"], ["si", "සිංහල", "Sinhala"], ["sk", "Slovenčina", "Slovak"],    ["sl", "Slovenščina", "Slovenian"], ["es", "Español", "Spanish"], ["sw", "Kiswahili", "Swahili"], ["sv", "Svenska", "Swedish"],    ["tl", "Tagalog", "Tagalog"], ["ta", "தமிழ்", "Tamil"], ["te", "తెలుగు", "Telugu"], ["th", "ไทย", "Thai"],    ["tr", "Türkçe", "Turkish"], ["uk", "Українська", "Ukrainian"], ["ur", "اردو", "Urdu"], ["uz", "Oʻzbekcha", "Uzbek"],    ["vi", "Tiếng Việt", "Vietnamese"], ["cy", "Cymraeg", "Welsh"],]// A language's own name ("Deutsch" for "de"), or the code when it is not known.fn language_picker_name(code: str) -> str {    let c = lower(code)    let hit = find(language_picker_languages, (l) => l[0] == c)    hit != null ? hit[1] : code}view LanguagePicker(name: str? = null, label: str? = null, value: str? = null, only: list? = null, favourites: list = [],                    english: bool = false, placeholder: str? = null, hint: str? = null, error: str? = null,                    required: bool = false, disabled: bool = false, field: str? = null, size: str = "md",                    on_change = null, class: str? = null) {    style global language_picker_css    let id = field ?? `language-picker-${name ?? "language"}`    let described = ui_cls([hint != null && `${id}-hint`, error != null && `${id}-error`])    let chosen = value != null ? lower(value) : null    let wanted = only != null ? only | map((c) => lower(c)) : null    let all = language_picker_languages | filter((l) => wanted == null || contains(wanted, l[0]))    let favs = favourites | map((c) => lower(c))    let top = favs | map((c) => find(all, (l) => l[0] == c)) | filter((l) => l != null)    let rest = len(top) > 0 ? all | filter((l) => !contains(favs, l[0])) : all    let opt = (l) => <option value={l[0]} lang={l[0]} selected={l[0] == chosen}>        {english && l[1] != l[2] ? `${l[1]} (${l[2]})` : l[1]}    </option>    <div class={ui_cls(["ui-language-picker", size == "sm" && "ui-language-picker-sm", error != null && "ui-language-picker-invalid",                        disabled && "ui-language-picker-disabled", class])}>        {label != null && <label.ui-language-picker-label for={id}>            {label}{required && <span.ui-language-picker-req aria-hidden="true">*</span>}        </label>}        <span.ui-language-picker-box>            <select id={id} name={name} required={required} disabled={disabled} autocomplete="language"                    aria-invalid={error != null ? "true" : null} aria-describedby={described != "" ? described : null}                    @change={(e) => { on_change && on_change(e.value) }}>                {placeholder != null && <option value="" disabled selected={chosen == null}>{placeholder}</option>}                {len(top) > 0                    ? [<optgroup label="Common">{for l in top => opt(l)}</optgroup>,                       <optgroup label="All languages">{for l in rest => opt(l)}</optgroup>]                    : (for l in rest => opt(l))}            </select>        </span>        {hint != null && <p.ui-language-picker-hint id={`${id}-hint`}>{hint}</p>}        {error != null && <p.ui-language-picker-error id={`${id}-error`} role="alert">{error}</p>}    </div>}
Thread
55Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.