/* ============================================================================ LOCALE + PATH HELPERS ---------------------------------------------------------------------------- SHARP-ER is served with an EXPLICIT locale prefix on both languages: /en/... and /ar/.... Bare "/" is redirected by the server on Accept-Language; there is no ?lang= query parameter anywhere in the product. These are pure functions on purpose - they run identically during SSR and in the browser, and they are the single source of truth for "what is the other language's URL for the page I am on". ========================================================================= */ export const LOCALES = ["en", "ar"] as const; export type Locale = (typeof LOCALES)[number]; export type Direction = "ltr" | "rtl"; export const DEFAULT_LOCALE: Locale = "en"; /** Locale -> the value that belongs in . */ const DIRECTION_BY_LOCALE: Record = { en: "ltr", ar: "rtl", }; /** Locale -> its endonym, used as the accessible name of the switcher link. */ export const LOCALE_ENDONYM: Record = { en: "English", ar: "العربية", }; /** Locale -> the two-letter badge the switcher shows. */ export const LOCALE_SHORT_LABEL: Record = { en: "EN", ar: "AR", }; export function isLocale(value: unknown): value is Locale { return ( typeof value === "string" && (LOCALES as readonly string[]).includes(value) ); } export function getDirection(locale: Locale): Direction { return DIRECTION_BY_LOCALE[locale]; } /** The language a reader on `locale` can switch to. Two locales, so it flips. */ export function getCounterpartLocale(locale: Locale): Locale { return locale === "en" ? "ar" : "en"; } /** * The locale prefix of a pathname, or null when there is none. * "/ar/contact" -> "ar" "/about" -> null "/" -> null */ export function getLocaleFromPathname(pathname: string): Locale | null { const first = pathname.replace(/^\/+/, "").split("/")[0]?.toLowerCase(); return isLocale(first) ? first : null; } /** * The pathname with its locale prefix removed, always leading-slashed. * "/ar/news/2026" -> "/news/2026" "/en" -> "/" "/news" -> "/news" */ export function stripLocale(pathname: string): string { const locale = getLocaleFromPathname(pathname); const withoutLeadingSlashes = pathname.replace(/^\/+/, ""); const rest = locale ? withoutLeadingSlashes.slice(locale.length) : withoutLeadingSlashes; if (rest === "" || rest === "/") return "/"; return rest.startsWith("/") ? rest : `/${rest}`; } /** * The same page under `locale`. Safe to call with an already-prefixed path, an * unprefixed path, or a bare "/" - the locale prefix is normalised either way. * ("/en/contact", "ar") -> "/ar/contact" * ("/contact", "ar") -> "/ar/contact" * ("/", "en") -> "/en" */ export function localizePath(pathname: string, locale: Locale): string { const rest = stripLocale(pathname); return rest === "/" ? `/${locale}` : `/${locale}${rest}`; } export interface CounterpartPathOptions { /** Query string to carry over, with or without the leading "?". */ search?: string; /** Fragment to carry over, with or without the leading "#". */ hash?: string; } /** * The counterpart-language URL for the current location: same page, other * locale, query string and fragment preserved. */ export function getCounterpartPath( pathname: string, locale: Locale, options: CounterpartPathOptions = {}, ): string { return buildLocalizedHref( pathname, getCounterpartLocale(locale), options, ); } /** localizePath() plus the preserved search/hash. */ export function buildLocalizedHref( pathname: string, locale: Locale, { search = "", hash = "" }: CounterpartPathOptions = {}, ): string { const normalisedSearch = search && search !== "?" ? (search.startsWith("?") ? search : `?${search}`) : ""; const normalisedHash = hash && hash !== "#" ? (hash.startsWith("#") ? hash : `#${hash}`) : ""; return `${localizePath(pathname, locale)}${normalisedSearch}${normalisedHash}`; } /** * True when `href` addresses the page currently at `pathname`. Trailing * slashes and an absent locale prefix on either side are tolerated so a link * built as "/en/about" still matches a location of "/en/about/". */ export function isCurrentPath(href: string, pathname: string): boolean { return normalizeForCompare(href) === normalizeForCompare(pathname); } function normalizeForCompare(value: string): string { const pathOnly = value.split(/[?#]/)[0] ?? value; const withoutTrailingSlash = pathOnly.replace(/\/+$/, ""); return withoutTrailingSlash === "" ? "/" : withoutTrailingSlash.toLowerCase(); }