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
JavaScript
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;
}
}