120 lines
4.0 KiB
TypeScript
120 lines
4.0 KiB
TypeScript
import { Link } from "react-router";
|
|||
|
|
|
||
|
|
import "./LanguageSwitcher.css";
|
||
|
|
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)]";
|
||
|
|
|
||
|
|
export interface LanguageToggleProps {
|
||
|
|
/** 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;
|
||
|
|
/** Accessible name of the pair ("Language"), in the reader's language. */
|
||
|
|
label: string;
|
||
|
|
/** Navigate with a full document request instead of a client transition. */
|
||
|
|
reloadDocument?: boolean;
|
||
|
|
className?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Both languages, side by side: the globe, the current locale in an accent
|
||
|
|
* chip, and the other locale as a link to the same page under that locale -
|
||
|
|
* the redesign's nav control, replacing the globe-plus-dropdown on the desktop
|
||
|
|
* bar.
|
||
|
|
*
|
||
|
|
* One tap to switch and no state to open, which is why the draft prefers it;
|
||
|
|
* the drawer still uses <LanguageSwitcher /> and <LanguageMenu /> remains for
|
||
|
|
* anywhere a list of more than two languages is needed.
|
||
|
|
*
|
||
|
|
* The current language is marked with `aria-current`, not by colour alone. The
|
||
|
|
* link carries its own lang/hrefLang/dir and is named with the target
|
||
|
|
* language's endonym ("العربية"), so a screen reader switches voice for it
|
||
|
|
* while the visible "AR" stays a two-letter code.
|
||
|
|
*/
|
||
|
|
export function LanguageToggle({
|
||
|
|
locale,
|
||
|
|
pathname,
|
||
|
|
search,
|
||
|
|
hash,
|
||
|
|
label,
|
||
|
|
reloadDocument = false,
|
||
|
|
className,
|
||
|
|
}: LanguageToggleProps) {
|
||
|
|
const target = getCounterpartLocale(locale);
|
||
|
|
const href = getCounterpartPath(pathname, locale, { search, hash });
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
role="group"
|
||
|
|
aria-label={label}
|
||
|
|
className={[
|
||
|
|
"flex shrink-0 items-center gap-xs text-[var(--text-secondary)]",
|
||
|
|
className,
|
||
|
|
]
|
||
|
|
.filter(Boolean)
|
||
|
|
.join(" ")}
|
||
|
|
>
|
||
|
|
<span aria-hidden="true" className="sh-language-globe" />
|
||
|
|
|
||
|
|
<span className="flex items-center">
|
||
|
|
<span
|
||
|
|
aria-current="true"
|
||
|
|
lang={locale}
|
||
|
|
dir={getDirection(locale)}
|
||
|
|
className="label-md rounded-control bg-[var(--surface-accent)] px-xs py-2xs text-[var(--text-on-accent)]"
|
||
|
|
>
|
||
|
|
{LOCALE_SHORT_LABEL[locale]}
|
||
|
|
</span>
|
||
|
|
|
||
|
|
<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 rounded-control px-xs py-2xs no-underline",
|
||
|
|
"text-[var(--text-secondary)]",
|
||
|
|
"transition-colors duration-[var(--motion-duration-fast)]",
|
||
|
|
"ease-[var(--motion-ease-standard)]",
|
||
|
|
"hover:text-[var(--text-primary)]",
|
||
|
|
FOCUS_RING,
|
||
|
|
].join(" ")}
|
||
|
|
>
|
||
|
|
<span aria-hidden="true">{LOCALE_SHORT_LABEL[target]}</span>
|
||
|
|
</Link>
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
export default LanguageToggle;
|
||
|
|
|
||
|
|
/* ---------------------------------------------------------------------------
|
||
|
|
USAGE
|
||
|
|
---------------------------------------------------------------------------
|
||
|
|
// Reading English at /en/news-and-updates -> "EN" chipped, "AR" links to
|
||
|
|
// /ar/news-and-updates.
|
||
|
|
<LanguageToggle locale="en" pathname="/en/news-and-updates" label="Language" />
|
||
|
|
------------------------------------------------------------------------ */
|