2026-09-11 02:46:56 +02:00
|
|
|
/* ============================================================================
|
|
|
|
|
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 `<html dir>`. */
|
|
|
|
|
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;
|
2026-10-05 17:32:06 +02:00
|
|
|
/** The locale-prefixed URL of one of the eleven sections. */
|
2026-09-11 02:46:56 +02:00
|
|
|
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<T>(
|
|
|
|
|
read: (handle: Record<string, unknown>) => 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<string, unknown>);
|
|
|
|
|
if (value !== undefined) return value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|