Files

137 lines
4.5 KiB
TypeScript
Raw Permalink Normal View History

2026-09-11 02:46:56 +02:00
/* ============================================================================
I18N CONFIG
----------------------------------------------------------------------------
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.
The locale primitives themselves live in the LanguageSwitcher's pure
`locale-path` module - it is imported by <Navigation />, <LanguageSwitcher />
and <Footer />, so re-exporting from there (rather than declaring a second
`Locale` union) keeps one type across the whole app. A relative import is
used because this module is reachable from `app/routes.ts`, which the dev
plugin evaluates outside the app's normal alias resolution.
========================================================================= */
import {
DEFAULT_LOCALE,
getDirection,
getLocaleFromPathname,
isLocale,
LOCALES,
LOCALE_ENDONYM,
LOCALE_SHORT_LABEL,
type Direction,
type Locale,
} from "../components/ui/LanguageSwitcher/locale-path";
export type { Direction, Locale };
export {
DEFAULT_LOCALE,
getLocaleFromPathname,
isLocale,
LOCALE_ENDONYM,
LOCALE_SHORT_LABEL,
};
/** Every locale the site is published in, in preference order. */
export const SUPPORTED_LOCALES = LOCALES;
/** `<html dir>` for a locale. Alias of the component library's helper. */
export const dirFor = getDirection;
/**
* The BCP-47 tag handed to `Intl.*` and to `<html lang>`'s consumers.
* Libyan Arabic content is written in MSA, and the draft copy uses Western
* (latn) digits throughout, so number formatting stays latn - see ./format.ts.
*/
export const INTL_LOCALE: Readonly<Record<Locale, string>> = {
en: "en-GB",
ar: "ar-LY",
};
/** Open Graph `og:locale` values. */
export const OG_LOCALE: Readonly<Record<Locale, string>> = {
en: "en_GB",
ar: "ar_LY",
};
/* ---------------------------------------------------------------------------
ACCEPT-LANGUAGE NEGOTIATION
Only ever used for bare "/" - once the reader is on a prefixed URL the URL
wins, so a shared /ar/... link never silently flips to English.
------------------------------------------------------------------------ */
/** One parsed entry of an `Accept-Language` header. */
export interface LanguageRange {
readonly tag: string;
readonly quality: number;
}
/**
* Parse an `Accept-Language` header into ranges sorted by descending quality.
* Malformed entries are dropped rather than throwing - this runs on a header
* an arbitrary client controls.
*/
export function parseAcceptLanguage(
header: string | null | undefined,
): LanguageRange[] {
if (!header) return [];
return header
.split(",")
.map((part, index): LanguageRange | null => {
const [rawTag, ...parameters] = part.trim().split(";");
const tag = rawTag?.trim().toLowerCase();
if (!tag) return null;
const qParameter = parameters
.map((parameter) => parameter.trim())
.find((parameter) => parameter.startsWith("q="));
const parsed = qParameter ? Number.parseFloat(qParameter.slice(2)) : 1;
const quality = Number.isFinite(parsed) ? parsed : 0;
// Index keeps the header's own order stable inside one quality band.
return { tag, quality: quality - index * 1e-6 };
})
.filter((range): range is LanguageRange => range !== null && range.quality > 0)
.sort((a, b) => b.quality - a.quality);
}
/**
* The best supported locale for an `Accept-Language` header.
* "ar-LY,ar;q=0.9,en;q=0.8" -> "ar"; "fr-FR,fr;q=0.9" -> the default locale.
*/
export function negotiateLocale(
header: string | null | undefined,
fallback: Locale = DEFAULT_LOCALE,
): Locale {
for (const { tag } of parseAcceptLanguage(header)) {
if (tag === "*") return fallback;
const base = tag.split("-")[0];
if (isLocale(base)) return base;
}
return fallback;
}
/** `negotiateLocale()` applied to a Fetch `Request`'s headers. */
export function negotiateLocaleFromRequest(
request: Request,
fallback: Locale = DEFAULT_LOCALE,
): Locale {
return negotiateLocale(request.headers.get("accept-language"), fallback);
}
/**
* The locale a URL addresses. Every page URL carries its prefix, so this is
* the authoritative resolver used by the root layout and by `useLocale()`.
*/
export function localeFromRequest(
request: Request,
fallback: Locale = DEFAULT_LOCALE,
): Locale {
return getLocaleFromPathname(new URL(request.url).pathname) ?? fallback;
}