Files
sharper.ly/apps/web/app/components/ui/LanguageSwitcher/LanguageToggle.tsx
T

120 lines
4.0 KiB
TypeScript
Raw Normal View History

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" />
------------------------------------------------------------------------ */