@remix-run/headers
Version:
A toolkit for working with HTTP headers in JavaScript
193 lines (192 loc) • 6.38 kB
JavaScript
import {} from "./header-value.js";
import { parseParams } from "./param-values.js";
import { isIterable } from "./utils.js";
/**
* The value of a `Accept-Language` HTTP header.
*
* [MDN `Accept-Language` Reference](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language)
*
* [HTTP/1.1 Specification](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5)
*/
export class AcceptLanguage {
#map;
/**
* @param init A string, iterable, or record to initialize the header
*/
constructor(init) {
this.#map = new Map();
if (init) {
if (typeof init === 'string') {
for (let piece of init.split(/\s*,\s*/)) {
let params = parseParams(piece);
if (params.length < 1)
continue;
let language = params[0][0];
let weight = 1;
for (let i = 1; i < params.length; i++) {
let [key, value] = params[i];
if (key === 'q') {
weight = Number(value);
break;
}
}
this.#map.set(language.toLowerCase(), weight);
}
}
else if (isIterable(init)) {
for (let value of init) {
if (Array.isArray(value)) {
this.#map.set(value[0].toLowerCase(), value[1]);
}
else {
this.#map.set(value.toLowerCase(), 1);
}
}
}
else {
for (let language of Object.getOwnPropertyNames(init)) {
this.#map.set(language.toLowerCase(), init[language]);
}
}
this.#sort();
}
}
#sort() {
this.#map = new Map([...this.#map].sort((a, b) => b[1] - a[1]));
}
/**
* An array of all languages in the header.
*/
get languages() {
return Array.from(this.#map.keys());
}
/**
* An array of all weights (q values) in the header.
*/
get weights() {
return Array.from(this.#map.values());
}
/**
* The number of languages in the header.
*/
get size() {
return this.#map.size;
}
/**
* Returns `true` if the header matches the given language (i.e. it is "acceptable").
*
* @param language The locale identifier of the language to check
* @return `true` if the language is acceptable, `false` otherwise
*/
accepts(language) {
return this.getWeight(language) > 0;
}
/**
* Gets the weight of a language with the given locale identifier. Performs wildcard and subtype
* matching, so `en` matches `en-US` and `en-GB`, and `*` matches all languages.
*
* @param language The locale identifier of the language to get
* @return The weight of the language, or `0` if it is not in the header
*/
getWeight(language) {
let [base, subtype] = language.toLowerCase().split('-');
for (let [key, value] of this) {
let [b, s] = key.split('-');
if ((b === base || b === '*' || base === '*') &&
(s === subtype || s === undefined || subtype === undefined)) {
return value;
}
}
return 0;
}
/**
* Returns the most preferred language from the given list of languages.
*
* @param languages The locale identifiers of the languages to choose from
* @return The most preferred language or `null` if none match
*/
getPreferred(languages) {
let sorted = languages
.map((language) => [language, this.getWeight(language)])
.sort((a, b) => b[1] - a[1]);
let first = sorted[0];
return first !== undefined && first[1] > 0 ? first[0] : null;
}
/**
* Gets the weight of a language with the given locale identifier. If it is not in the header
* verbatim, this returns `null`.
*
* @param language The locale identifier of the language to get
* @return The weight of the language, or `null` if it is not in the header
*/
get(language) {
return this.#map.get(language.toLowerCase()) ?? null;
}
/**
* Sets a language with the given weight.
*
* @param language The locale identifier of the language to set
* @param weight The weight of the language (default: `1`)
*/
set(language, weight = 1) {
this.#map.set(language.toLowerCase(), weight);
this.#sort();
}
/**
* Removes a language with the given locale identifier.
*
* @param language The locale identifier of the language to remove
*/
delete(language) {
this.#map.delete(language.toLowerCase());
}
/**
* Checks if the header contains a language with the given locale identifier.
*
* @param language The locale identifier of the language to check
* @return `true` if the language is in the header, `false` otherwise
*/
has(language) {
return this.#map.has(language.toLowerCase());
}
/**
* Removes all languages from the header.
*/
clear() {
this.#map.clear();
}
/**
* Returns an iterator of all language and weight pairs.
*
* @return An iterator of `[language, weight]` tuples
*/
entries() {
return this.#map.entries();
}
[Symbol.iterator]() {
return this.entries();
}
/**
* Invokes the callback for each language and weight pair.
*
* @param callback The function to call for each pair
* @param thisArg The value to use as `this` when calling the callback
*/
forEach(callback, thisArg) {
for (let [language, weight] of this) {
callback.call(thisArg, language, weight, this);
}
}
/**
* Returns the string representation of the header value.
*
* @return The header value as a string
*/
toString() {
let pairs = [];
for (let [language, weight] of this.#map) {
pairs.push(`${language}${weight === 1 ? '' : `;q=${weight}`}`);
}
return pairs.join(',');
}
}