122 lines
4.4 KiB
TypeScript
122 lines
4.4 KiB
TypeScript
import { Link } from "react-router";
|
|
|
|
import {
|
|
getCounterpartLocale,
|
|
getCounterpartPath,
|
|
getDirection,
|
|
LOCALE_ENDONYM,
|
|
LOCALE_SHORT_LABEL,
|
|
type Locale,
|
|
} from "./locale-path";
|
|
|
|
/* Reinforces the global :focus-visible rule in app.css with the same tier-2
|
|
focus tokens, so the ring is visible even if a caller overrides `outline`. */
|
|
const FOCUS_RING =
|
|
"focus-visible:outline-[color:var(--focus-ring-color)] " +
|
|
"focus-visible:outline-[length:var(--focus-ring-width)] " +
|
|
"focus-visible:outline-offset-[var(--focus-ring-offset)]";
|
|
|
|
/** Padding presets. Kept as a prop rather than an overridable class, because
|
|
two padding utilities on one element are resolved by the order Tailwind
|
|
emits them in, not by the order they appear in the class attribute. */
|
|
const SIZE_CLASSES = {
|
|
sm: "px-sm py-2xs",
|
|
md: "min-h-[44px] px-md py-xs",
|
|
} as const;
|
|
|
|
export type LanguageSwitcherSize = keyof typeof SIZE_CLASSES;
|
|
|
|
export interface LanguageSwitcherProps {
|
|
/** The locale the reader is currently on. */
|
|
locale: Locale;
|
|
/** Current pathname, e.g. "/en/news-and-updates". */
|
|
pathname: string;
|
|
/** Query string to carry across the switch. Usually `location.search`. */
|
|
search?: string;
|
|
/** Fragment to carry across the switch. Usually `location.hash`. */
|
|
hash?: string;
|
|
/**
|
|
* Navigate with a full document request instead of a client transition.
|
|
* Off by default: the root route re-renders <html lang>/<html dir> on a
|
|
* client navigation, so a soft transition already lands on a correct
|
|
* document. Turn it on if a page needs the server to re-render first.
|
|
*/
|
|
reloadDocument?: boolean;
|
|
/** "sm" for the desktop bar, "md" for a 44px touch target in the drawer. */
|
|
size?: LanguageSwitcherSize;
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* Compact language toggle: shows the two-letter code of the OTHER language
|
|
* ("AR" while reading English, "EN" while reading Arabic) and links to the
|
|
* same page under that locale, preserving the rest of the path.
|
|
*
|
|
* The Figma navigation has no language switcher - a gap recorded in
|
|
* docs/SHARP-ER_Website Recommendations.docx - so this is designed to sit in
|
|
* the existing bar: label-md on a pill outline, matching the EU badge height.
|
|
*/
|
|
export function LanguageSwitcher({
|
|
locale,
|
|
pathname,
|
|
search,
|
|
hash,
|
|
reloadDocument = false,
|
|
size = "sm",
|
|
className = "",
|
|
}: LanguageSwitcherProps) {
|
|
const target = getCounterpartLocale(locale);
|
|
const href = getCounterpartPath(pathname, locale, { search, hash });
|
|
|
|
return (
|
|
<Link
|
|
to={href}
|
|
reloadDocument={reloadDocument}
|
|
hrefLang={target}
|
|
lang={target}
|
|
dir={getDirection(target)}
|
|
aria-label={LOCALE_ENDONYM[target]}
|
|
title={LOCALE_ENDONYM[target]}
|
|
data-language-switcher={target}
|
|
className={[
|
|
"label-md inline-flex shrink-0 items-center justify-center rounded-pill",
|
|
"border border-[var(--border-default)]",
|
|
SIZE_CLASSES[size],
|
|
"text-[var(--text-primary)] no-underline",
|
|
"transition-colors duration-[var(--motion-duration-fast)]",
|
|
"ease-[var(--motion-ease-standard)]",
|
|
"hover:border-[var(--border-strong)] hover:bg-[var(--surface-muted)]",
|
|
FOCUS_RING,
|
|
className,
|
|
]
|
|
.filter(Boolean)
|
|
.join(" ")}
|
|
>
|
|
<span aria-hidden="true">{LOCALE_SHORT_LABEL[target]}</span>
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
export default LanguageSwitcher;
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
USAGE
|
|
---------------------------------------------------------------------------
|
|
// Reading English at /en/news-and-updates?page=2 -> links to
|
|
// /ar/news-and-updates?page=2 and renders the pill "AR".
|
|
<LanguageSwitcher locale="en" pathname="/en/news-and-updates" search="?page=2" />
|
|
|
|
// Reading Arabic -> renders "EN", links to /en/...
|
|
<LanguageSwitcher locale="ar" pathname="/ar/contact" />
|
|
|
|
// Inside the mobile drawer, where a 44px touch target is wanted:
|
|
<LanguageSwitcher locale={locale} pathname={pathname} size="md" />
|
|
|
|
// Force a document navigation (server re-renders <html lang>/<html dir>):
|
|
<LanguageSwitcher locale={locale} pathname={pathname} reloadDocument />
|
|
|
|
// Wired from a route, using the router's own location:
|
|
const { pathname, search, hash } = useLocation();
|
|
<LanguageSwitcher locale={locale} pathname={pathname} search={search} hash={hash} />
|
|
------------------------------------------------------------------------ */
|