/* ============================================================================ I18N HOOKS ---------------------------------------------------------------------------- The locale is a property of the URL, never of a cookie or of client state, so every hook here is derived from the router - which means SSR and the first client render agree by construction and hydration never mismatches. `useParams().lang` is the fast path inside a page route. The root layout and the root ErrorBoundary sit ABOVE the ":lang" segment, where that param does not exist, so the pathname is the fallback and both are correct. ========================================================================= */ import { useMemo } from "react"; import { useLocation, useMatches, useParams } from "react-router"; import { DEFAULT_LOCALE, dirFor, getLocaleFromPathname, isLocale, type Direction, type Locale, } from "./config"; import { alternatePath, localizedPath, sectionPath } from "./paths"; import type { RouteSlugKey } from "./paths"; import { createTranslator, getMessages, type Messages, type Translator, } from "./t"; /** The locale of the page being rendered. */ export function useLocale(): Locale { const params = useParams(); const { pathname } = useLocation(); const lang = params.lang; return useMemo(() => { if (isLocale(lang)) return lang; return getLocaleFromPathname(pathname) ?? DEFAULT_LOCALE; }, [lang, pathname]); } /** "ltr" for /en/*, "rtl" for /ar/*. Mirrors ``. */ export function useDirection(): Direction { return dirFor(useLocale()); } export interface Translation { readonly locale: Locale; readonly dir: Direction; /** `t("nav.sections.about")`, `t("pagination.page", { page, pageCount })`. */ readonly t: Translator; /** The raw catalogue, for the rare case a whole namespace is needed. */ readonly messages: Messages; /** `/{locale}` prefixed URL for an unprefixed path. */ readonly localizedPath: (path: string) => string; /** The locale-prefixed URL of one of the ten sections. */ readonly sectionPath: (key: RouteSlugKey) => string; } /** Everything a component needs to render in the current language. */ export function useTranslation(): Translation { const locale = useLocale(); return useMemo( () => ({ locale, dir: dirFor(locale), t: createTranslator(locale), messages: getMessages(locale), localizedPath: (path: string) => localizedPath(path, locale), sectionPath: (key: RouteSlugKey) => sectionPath(key, locale), }), [locale], ); } /** * The counterpart-language URL for the current location, query string and * fragment preserved. This is what the header's language switch points at. */ export function useAlternatePath(targetLocale: Locale): string { const { pathname, search, hash } = useLocation(); return useMemo( () => alternatePath(pathname, targetLocale, { search, hash }), [pathname, search, hash, targetLocale], ); } /** * Read a value off the deepest route `handle` that declares it. Used by the * shell to let a page opt into a light navigation or footer surface. */ export function useRouteHandleValue( read: (handle: Record) => T | undefined, ): T | undefined { const matches = useMatches(); for (let index = matches.length - 1; index >= 0; index -= 1) { const handle = matches[index]?.handle; if (handle && typeof handle === "object") { const value = read(handle as Record); if (value !== undefined) return value; } } return undefined; }