// Thin reactive layer over Paraglide, made SSR-safe. // // The problem it solves: Paraglide's client strategies (localStorage / preferredLanguage) resolve // the locale synchronously at load, so a `de` user's FIRST client render used German while the // server render (which has no localStorage / no request-locale wiring) used the base locale `en` — // a hydration mismatch plus a flash of English on every full page load. // // The fix: take over the READ side via `overwriteGetLocale`, returning a single `rendered` locale // that starts at `baseLocale`. So the server render AND the client's first (hydration) render agree // — no mismatch. After hydration, `adoptStoredLocale()` (called once from the root) switches to the // user's persisted/browser choice: one clean transition instead of a mismatch. Paraglide's strategy // still PERSISTS the choice (`setLocale` writes localStorage); we only own the read side. import { useSyncExternalStore } from "react"; import { baseLocale, isLocale, locales, localStorageKey, overwriteGetLocale, setLocale, } from "@/paraglide/runtime"; /** The available locales as a union (`'en' | 'de'`), derived from Paraglide's `locales`. */ export type Locale = (typeof locales)[number]; // The locale every `m.*()` renders in. Never mutated on the server (so SSR is always `baseLocale`, // identical across concurrent requests); mutated only in the browser, via `changeLocale`. let rendered: Locale = baseLocale as Locale; overwriteGetLocale(() => rendered); const listeners = new Set<() => void>(); /** Switch locale and notify subscribers. Persists via Paraglide's strategy (localStorage). */ export function changeLocale(locale: Locale) { rendered = locale; // `reload: false` keeps the SPA mounted; we re-render via the store below. setLocale(locale, { reload: false }); if (typeof document !== "undefined") document.documentElement.lang = locale; for (const l of listeners) l(); } /** * Adopt the user's persisted (localStorage) or browser (navigator) locale — call ONCE from a root * effect, AFTER hydration, so the switch never races the initial render (which must match SSR). */ export function adoptStoredLocale() { if (typeof window === "undefined") return; let target: Locale = baseLocale as Locale; const stored = window.localStorage?.getItem(localStorageKey); if (stored && isLocale(stored)) { target = stored; } else { const nav = navigator.language?.slice(0, 2); if (nav && isLocale(nav)) target = nav; } if (target !== rendered) changeLocale(target); } function subscribe(cb: () => void) { listeners.add(cb); return () => listeners.delete(cb); } /** Current locale, reactive — components using `m.*` should read this so they re-render. */ export function useLocale(): Locale { return useSyncExternalStore( subscribe, () => rendered, () => baseLocale as Locale, ); } export { locales };