48 lines
1.6 KiB
TypeScript
48 lines
1.6 KiB
TypeScript
/* ============================================================================
|
|||
|
|
":lang" LAYOUT
|
||
|
|
----------------------------------------------------------------------------
|
||
|
|
Owned by the app shell, not by a page agent.
|
||
|
|
|
||
|
|
One route tree serves both languages: every page module below this layout is
|
||
|
|
rendered for /en/... and /ar/... alike, and reads its locale from
|
||
|
|
`useLocale()` (or from this route's loader data via
|
||
|
|
`useRouteLoaderData("locale")`).
|
||
|
|
|
||
|
|
The layout itself renders nothing but the outlet - the header, the footer
|
||
|
|
and <html lang/dir> live in the root route, above it - so its whole job is
|
||
|
|
validating the segment:
|
||
|
|
/en, /ar -> render
|
||
|
|
/EN, /Ar -> 301 to the lower-case canonical URL
|
||
|
|
/fr, /xx -> 404 through the root ErrorBoundary
|
||
|
|
========================================================================= */
|
||
|
|
|
||
|
|
import { data, Outlet, redirect } from "react-router";
|
||
|
|
|
||
|
|
import { dirFor, isLocale } from "~/i18n/config";
|
||
|
|
|
||
|
|
import type { Route } from "./+types/locale-layout";
|
||
|
|
|
||
|
|
export function loader({ params, request }: Route.LoaderArgs) {
|
||
|
|
const segment = params.lang;
|
||
|
|
const normalized = segment?.toLowerCase();
|
||
|
|
|
||
|
|
if (!isLocale(normalized)) {
|
||
|
|
throw data("Not Found", { status: 404, statusText: "Not Found" });
|
||
|
|
}
|
||
|
|
|
||
|
|
if (normalized !== segment) {
|
||
|
|
const url = new URL(request.url);
|
||
|
|
url.pathname = url.pathname.replace(
|
||
|
|
/^\/[^/]+/,
|
||
|
|
`/${normalized}`,
|
||
|
|
);
|
||
|
|
throw redirect(`${url.pathname}${url.search}`, 301);
|
||
|
|
}
|
||
|
|
|
||
|
|
return { locale: normalized, dir: dirFor(normalized) };
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function LocaleLayout() {
|
||
|
|
return <Outlet />;
|
||
|
|
}
|