/** * Runtime language switch — German default, English on request. * * WHY NOT @nuxtjs/i18n * -------------------- * This site is 12 components, 4 pages and one form. @nuxtjs/i18n would pull in vue-i18n * plus a build-time message transform, and its route strategies want to own the URL * space (`/en/...`) — which would break every in-page anchor (`/#vorteil`) and every * link that already exists, for no gain. A typed message catalogue plus this composable * is ~40 lines of runtime code, adds nothing to the dependency tree, and keeps the * language on ONE url so a shared link always works. * * SSR CORRECTNESS * --------------- * The active locale lives in `useState`, so it is resolved exactly once on the server, * serialised into the payload and re-used verbatim during hydration — there is no * second, client-side resolution that could disagree with the server render, which is * the usual source of hydration mismatches in hand-rolled i18n. * * PRECEDENCE: ?lang= → cookie → Accept-Language → German. * `?lang=` is what makes a language linkable and testable with plain curl; whatever it * resolves to is written back to the cookie, so the choice survives the next visit. */ import { DEFAULT_LOCALE, isLocale, localeFromAcceptLanguage, messages, type Locale, type Messages, } from '~/locales' export const LOCALE_COOKIE = 'b24_lang' const LOCALE_STATE = 'b24-locale' const ONE_YEAR = 60 * 60 * 24 * 365 export const useLocale = () => { const cookie = useCookie(LOCALE_COOKIE, { maxAge: ONE_YEAR, sameSite: 'lax', path: '/', }) // Resolved here, during setup, rather than inside setLocale: a click handler runs // outside the component's setup context, and these are Nuxt composables. const route = useRoute() const router = useRouter() const locale = useState(LOCALE_STATE, () => { // 1 · explicit ?lang= wins, so a language can be linked to and curl'd. const requested = route.query.lang const fromQuery = Array.isArray(requested) ? requested[0] : requested if (isLocale(fromQuery)) return fromQuery // 2 · the visitor's stored choice. if (isLocale(cookie.value)) return cookie.value // 3 · browser preference, German winning whenever it is ambiguous. if (import.meta.server) { const header = useRequestHeaders(['accept-language'])['accept-language'] || '' return localeFromAcceptLanguage(header) } return DEFAULT_LOCALE }) // Persist a ?lang= (or header-derived) resolution. No-op once they agree, so this // does not write a cookie on every component that calls useLocale(). if (cookie.value !== locale.value) cookie.value = locale.value const t = computed(() => messages[locale.value] ?? messages[DEFAULT_LOCALE]) const isGerman = computed(() => locale.value === 'de') const isEnglish = computed(() => locale.value === 'en') const setLocale = (next: Locale) => { if (!isLocale(next) || next === locale.value) return locale.value = next cookie.value = next // Drop a `?lang=` that is now contradicted by the choice just made. Without this the // switch works, but reloading the page would silently undo it — `?lang=` outranks the // cookie by design. Same URL otherwise, so nothing else about the page changes. if (!import.meta.client) return if (!('lang' in route.query)) return const { lang: _dropped, ...rest } = route.query router.replace({ path: route.path, query: rest, hash: route.hash }) } return { locale, t, isGerman, isEnglish, setLocale } }