UNPKG

astro

Version:

Astro is a modern site builder with web best practices, performance, and DX front-of-mind.

199 lines (198 loc) • 6.59 kB
import { getAllCodes } from "./index.js"; import { normalizeTheLocale, normalizeThePath } from "./path.js"; /** * Parses the value of the `Accept-Language` header: * * More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language * * Complex example: `fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5` * */ export function parseLocale(header) { // Any language, early return if (header === "*") { return [{ locale: header, qualityValue: undefined }]; } const result = []; // we split by `,` and trim the white spaces const localeValues = header.split(",").map((str) => str.trim()); for (const localeValue of localeValues) { // split the locale name from the quality value const split = localeValue.split(";").map((str) => str.trim()); const localeName = split[0]; const qualityValue = split[1]; if (!split) { // invalid value continue; } // we check if the quality value is present, and it is actually `q=` if (qualityValue && qualityValue.startsWith("q=")) { const qualityValueAsFloat = Number.parseFloat(qualityValue.slice("q=".length)); // The previous operation can return a `NaN`, so we check if it is a safe operation if (Number.isNaN(qualityValueAsFloat) || qualityValueAsFloat > 1) { result.push({ locale: localeName, qualityValue: undefined }); } else { result.push({ locale: localeName, qualityValue: qualityValueAsFloat }); } } else { result.push({ locale: localeName, qualityValue: undefined }); } } return result; } function sortAndFilterLocales(browserLocaleList, locales) { const normalizedLocales = getAllCodes(locales).map(normalizeTheLocale); return browserLocaleList.filter((browserLocale) => { if (browserLocale.locale !== "*") { return normalizedLocales.includes(normalizeTheLocale(browserLocale.locale)); } return true; }).sort((a, b) => { // An absent `q=` means quality 1.0 (RFC 7231), while a bare `*` defaults // to 0 so it is never ranked above a real locale. const qa = a.locale === "*" ? a.qualityValue ?? 0 : a.qualityValue ?? 1; const qb = b.locale === "*" ? b.qualityValue ?? 0 : b.qualityValue ?? 1; return qb - qa; }); } /** * Set the current locale by parsing the value passed from the `Accept-Header`. * * If multiple locales are present in the header, they are sorted by their quality value and the highest is selected as current locale. * */ export function computePreferredLocale(request, locales) { const acceptHeader = request.headers.get("Accept-Language"); let result = undefined; if (acceptHeader) { const browserLocaleList = sortAndFilterLocales(parseLocale(acceptHeader), locales); const firstResult = browserLocaleList.at(0); if (firstResult && firstResult.locale !== "*") { outer: for (const currentLocale of locales) { if (typeof currentLocale === "string") { if (normalizeTheLocale(currentLocale) === normalizeTheLocale(firstResult.locale)) { result = currentLocale; break; } } else { for (const currentCode of currentLocale.codes) { if (normalizeTheLocale(currentCode) === normalizeTheLocale(firstResult.locale)) { result = currentCode; break outer; } } } } } } return result; } export function computePreferredLocaleList(request, locales) { const acceptHeader = request.headers.get("Accept-Language"); let result = []; if (acceptHeader) { const browserLocaleList = sortAndFilterLocales(parseLocale(acceptHeader), locales); // SAFETY: bang operator is safe because checked by the previous condition if (browserLocaleList.length === 1 && browserLocaleList.at(0).locale === "*") { return getAllCodes(locales); } else if (browserLocaleList.length > 0) { for (const browserLocale of browserLocaleList) { for (const loopLocale of locales) { if (typeof loopLocale === "string") { if (normalizeTheLocale(loopLocale) === normalizeTheLocale(browserLocale.locale)) { result.push(loopLocale); } } else { for (const code of loopLocale.codes) { if (normalizeTheLocale(code) === normalizeTheLocale(browserLocale.locale)) { result.push(code); } } } } } } } return result; } export function computeCurrentLocale(pathname, locales, defaultLocale) { // pages that use a locale param ([locale].astro or [locale]/index.astro) // and getStaticPaths make [locale].html the pathname during SSG // which will not match a configured locale without removing .html // as we do in normalizeThePath for (const segment of pathname.split("/").map(normalizeThePath)) { for (const locale of locales) { if (typeof locale === "string") { // we skip ta locale that isn't present in the current segment if (!segment.includes(locale)) continue; if (normalizeTheLocale(locale) === normalizeTheLocale(segment)) { return locale; } } else { if (locale.path === segment) { return locale.codes.at(0); } else { for (const code of locale.codes) { if (normalizeTheLocale(code) === normalizeTheLocale(segment)) { return code; } } } } } } // If we didn't exit, it's probably because we don't have any code/locale in the URL. // We use the default locale. for (const locale of locales) { if (typeof locale === "string") { if (locale === defaultLocale) { return locale; } } else { if (locale.path === defaultLocale) { return locale.codes.at(0); } } } } /** * Check if any of the route's resolved param values match a configured locale. * This handles dynamic routes like `[locale]` or `[...path]` where the locale * isn't in a static segment of the route pathname. */ export function computeCurrentLocaleFromParams(params, locales) { // Precompute lookup maps for O(1) matching instead of nested loops. // normalizedCode -> original locale string or first code for object locales const byNormalizedCode = new Map(); // path -> first code (for object locales) const byPath = new Map(); for (const locale of locales) { if (typeof locale === "string") { byNormalizedCode.set(normalizeTheLocale(locale), locale); } else { byPath.set(locale.path, locale.codes[0]); for (const code of locale.codes) { byNormalizedCode.set(normalizeTheLocale(code), code); } } } for (const value of Object.values(params)) { if (!value) continue; const pathMatch = byPath.get(value); if (pathMatch) return pathMatch; const codeMatch = byNormalizedCode.get(normalizeTheLocale(value)); if (codeMatch) return codeMatch; } }