|
1 | 1 | <script lang="ts"> |
| 2 | + import { browser } from '$app/environment'; |
| 3 | + import { createSelect, melt } from '@melt-ui/svelte'; |
| 4 | + import { onDestroy } from 'svelte'; |
2 | 5 | import { getLocale, setLocale, locales } from '$lib/paraglide/runtime.js'; |
3 | 6 | import * as m from '$lib/paraglide/messages'; |
4 | 7 |
|
5 | | - let currentLocale = $state(getLocale()); |
6 | | - const languageLabels = $derived({ |
7 | | - en: m.language_name_english(), |
8 | | - tr: m.language_name_turkish() |
9 | | - }); |
| 8 | + type Locale = (typeof locales)[number]; |
| 9 | +
|
| 10 | + const availableLocales = locales as readonly Locale[]; |
| 11 | +
|
| 12 | + const localeLabelGetters: Partial<Record<Locale, () => string>> = { |
| 13 | + en: m.language_name_english, |
| 14 | + tr: m.language_name_turkish |
| 15 | + }; |
| 16 | +
|
| 17 | + const initialLocale = getLocale() as Locale; |
| 18 | + let currentLocale = $state(initialLocale); |
| 19 | +
|
| 20 | + function getLanguageLabel(lang: Locale) { |
| 21 | + const getter = localeLabelGetters[lang]; |
| 22 | + if (getter) { |
| 23 | + return getter(); |
| 24 | + } |
| 25 | +
|
| 26 | + if (typeof Intl !== 'undefined' && typeof Intl.DisplayNames !== 'undefined') { |
| 27 | + try { |
| 28 | + const displayNames = new Intl.DisplayNames([currentLocale, 'en'], { type: 'language' }); |
| 29 | + const label = displayNames.of(lang); |
| 30 | + if (label) { |
| 31 | + return label; |
| 32 | + } |
| 33 | + } catch { |
| 34 | + // Ignore browsers without Intl.DisplayNames support for the given locale. |
| 35 | + } |
| 36 | + } |
| 37 | +
|
| 38 | + return lang.toUpperCase(); |
| 39 | + } |
| 40 | +
|
| 41 | + function switchLanguage(lang: Locale) { |
| 42 | + if (!browser) { |
| 43 | + return; |
| 44 | + } |
| 45 | +
|
| 46 | + if (lang === getLocale()) { |
| 47 | + return; |
| 48 | + } |
10 | 49 |
|
11 | | - function switchLanguage(lang: 'en' | 'tr') { |
12 | 50 | setLocale(lang, { reload: false }); |
13 | | - currentLocale = lang; |
14 | | - // Force a re-render by updating the state |
15 | 51 | setTimeout(() => window.location.reload(), 100); |
16 | 52 | } |
| 53 | +
|
| 54 | + const { |
| 55 | + elements: { |
| 56 | + trigger: selectTrigger, |
| 57 | + menu: selectMenu, |
| 58 | + option: selectOption, |
| 59 | + hiddenInput: selectHiddenInput |
| 60 | + }, |
| 61 | + states: { selected } |
| 62 | + } = createSelect<Locale>({ |
| 63 | + defaultSelected: { value: initialLocale, label: getLanguageLabel(initialLocale) } |
| 64 | + }); |
| 65 | +
|
| 66 | + const unsubscribe = selected.subscribe(($selected) => { |
| 67 | + const next = $selected?.value; |
| 68 | + if (!next || next === currentLocale) { |
| 69 | + return; |
| 70 | + } |
| 71 | +
|
| 72 | + currentLocale = next; |
| 73 | + switchLanguage(next); |
| 74 | + }); |
| 75 | +
|
| 76 | + onDestroy(unsubscribe); |
17 | 77 | </script> |
18 | 78 |
|
19 | | -<div class="flex gap-2 items-center"> |
20 | | - {#each locales as lang} |
21 | | - <button |
22 | | - onclick={() => switchLanguage(lang as 'en' | 'tr')} |
23 | | - class="px-3 py-1 rounded-full text-sm font-medium transition {currentLocale === lang |
24 | | - ? 'bg-rose-400 text-white' |
25 | | - : 'bg-white border-2 border-rose-300 text-rose-700 hover:bg-rose-50'}" |
26 | | - > |
27 | | - {languageLabels[lang as keyof typeof languageLabels]} |
28 | | - </button> |
29 | | - {/each} |
| 79 | +<div class="relative inline-flex items-center"> |
| 80 | + <button |
| 81 | + type="button" |
| 82 | + aria-label={m.language_switcher_label()} |
| 83 | + class="flex items-center gap-2 rounded-full border-2 border-rose-300 bg-white px-4 py-2 text-sm font-medium text-rose-700 shadow-sm transition data-[state=open]:border-rose-400 data-[state=open]:ring-rose-400 hover:border-rose-400 hover:bg-rose-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-400" |
| 84 | + use:melt={$selectTrigger} |
| 85 | + > |
| 86 | + <span>{getLanguageLabel(currentLocale)}</span> |
| 87 | + <svg class="h-3 w-3 text-rose-500" viewBox="0 0 10 6" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"> |
| 88 | + <path d="M1 1.5 5 4.5 9 1.5" stroke-linecap="round" stroke-linejoin="round" /> |
| 89 | + </svg> |
| 90 | + </button> |
| 91 | + <input class="hidden" use:melt={$selectHiddenInput} /> |
| 92 | + <div |
| 93 | + class="z-50 mt-2 min-w-[10rem] overflow-hidden rounded-2xl border border-rose-200 bg-white p-1 text-sm text-rose-700 shadow-xl ring-1 ring-black/5 focus:outline-none" |
| 94 | + use:melt={$selectMenu} |
| 95 | + > |
| 96 | + {#each availableLocales as lang (lang)} |
| 97 | + <button |
| 98 | + type="button" |
| 99 | + class="flex w-full items-center justify-between rounded-xl px-3 py-2 text-left transition data-[highlighted]:bg-rose-50 data-[highlighted]:text-rose-900" |
| 100 | + use:melt={$selectOption({ value: lang, label: getLanguageLabel(lang) })} |
| 101 | + > |
| 102 | + <span>{getLanguageLabel(lang)}</span> |
| 103 | + {#if lang === currentLocale} |
| 104 | + <svg |
| 105 | + class="h-4 w-4 text-rose-500" |
| 106 | + viewBox="0 0 16 16" |
| 107 | + fill="none" |
| 108 | + stroke="currentColor" |
| 109 | + stroke-width="2" |
| 110 | + stroke-linecap="round" |
| 111 | + stroke-linejoin="round" |
| 112 | + aria-hidden="true" |
| 113 | + > |
| 114 | + <path d="M4 8l3 3 5-6" /> |
| 115 | + </svg> |
| 116 | + {/if} |
| 117 | + </button> |
| 118 | + {/each} |
| 119 | + </div> |
30 | 120 | </div> |
0 commit comments