intl-relative-time-format
Version:
A fully spec-compliant polyfill for 'Intl.RelativeTimeFormat'
1,295 lines (969 loc) • 48.7 kB
JavaScript
const SUPPORTS_RELATIVE_TIME_FORMAT = "RelativeTimeFormat" in Intl;
const LOCALE_MATCHER = ["lookup", "best fit"];
const STYLE = ["long", "short", "narrow"];
const NUMERIC = ["always", "auto"];
const VALID_SINGULAR_RELATIVE_TIME_UNIT_VALUES = ["second", "minute", "hour", "day", "week", "month", "quarter", "year"];
/**
* Sanitizes a RelativeTimeUnit into a SingularRelativeTimeUnit
*
* http://tc39.github.io/proposal-intl-relative-time/#sec-singularrelativetimeunit
* @param {RelativeTimeUnit} unit
* @return {SingularRelativeTimeUnit}
*/
function singularRelativeTimeUnit(unit) {
// Assert: Type(unit) is String.
if (typeof unit !== "string") {
throw new TypeError(`unit: '${unit}' must be a string`);
} // If unit is "seconds", return "second".
if (unit === "seconds") return "second"; // If unit is "minutes", return "minute".
if (unit === "minutes") return "minute"; // If unit is "hours", return "hour".
if (unit === "hours") return "hour"; // If unit is "days", return "day".
if (unit === "days") return "day"; // If unit is "weeks", return "week".
if (unit === "weeks") return "week"; // If unit is "months", return "month".
if (unit === "months") return "month"; // If unit is "quarters", return "quarter".
if (unit === "quarters") return "quarter"; // If unit is "years", return "year".
if (unit === "years") return "year"; // If unit is not one of "second", "minute", "hour", "day", "week", "month", "quarter", or "year", throw a RangeError exception.
if (!VALID_SINGULAR_RELATIVE_TIME_UNIT_VALUES.some(validUnit => validUnit === unit)) {
throw new RangeError(`Unit: '${unit}' must be one of: ${VALID_SINGULAR_RELATIVE_TIME_UNIT_VALUES.map(val => `"${val}"`).join(", ")}`);
} // Return unit.
return unit;
}
/* tslint:disable:use-primitive-type no-construct no-any */
/**
* The abstract operation ToObject converts argument to a value of type Object.
*
* https://tc39.github.io/ecma262/#sec-toobject
* @param {T} argument
* @return {T extends boolean ? Boolean : T extends number ? Number : T extends string ? String : T extends symbol ? symbol : T}
*/
function toObject(argument) {
if (argument == null) {
throw new TypeError(`Argument ${argument} cannot be converted to an Object`);
}
if (typeof argument === "boolean") {
return new Boolean(argument);
}
if (typeof argument === "number") {
return new Number(argument);
}
if (typeof argument === "string") {
return new String(argument);
}
if (typeof argument === "symbol") {
return new Object(argument);
}
return argument;
}
/**
* The abstract operation ToString converts argument to a value of type String
* https://tc39.es/ecma262/#sec-tostring
* @param {*} argument
* @returns {boolean}
*/
function toString(argument) {
return argument + "";
}
/**
* A Regular Expression that matches Unicode extension sequences
* @type {RegExp}
*/
const UNICODE_EXTENSION_SEQUENCE_REGEXP = /-u(?:-[0-9a-z]{2,8})+/gi;
/**
* Removes all Unicode characters from the given string
* @param {string} str
* @return {string}
*/
function removeUnicodeExtensionSequences(str) {
return str.replace(UNICODE_EXTENSION_SEQUENCE_REGEXP, "");
}
/**
* The abstract operation UnicodeExtensionValue is called with extension, which must be a Unicode locale extension sequence,
* and String key. This operation returns the type subtags for key.
* @param {string} extension
* @param {string} key
* @returns {string?}
*/
function unicodeExtensionValue(extension, key) {
// Assert: The number of elements in key is 2.
if (key.length !== 2) {
throw new TypeError(`Could not get UnicodeExtensionValue: The given key: '${key}' must have a length of 2`);
} // Let size be the number of elements in extension.
const size = key.length; // Let searchValue be the concatenation of "-", key, and "-".
let searchValue = `-${key}-`; // Let pos be Call(%StringProto_indexOf%, extension, « searchValue »).
let pos = String.prototype.indexOf.call(extension, searchValue); // If pos ≠ -1, then
if (pos !== -1) {
// Let start be pos + 4.
const start = pos + 4; // Let end be start.
let end = start; // Let k be start.
let k = start; // Let done be false.
let done = false; // Repeat, while done is false
while (!done) {
// Let e be Call(%StringProto_indexOf%, extension, « "-", k »).
const e = String.prototype.indexOf.call(extension, "-", k); // If e = -1, let len be size - k; else let len be e - k.
const len = e === -1 ? size - k : e - k; // If len = 2, then
if (len === 2) {
// Let done be true.
done = true;
} // Else if e = -1, then
else if (e === -1) {
// Let end be size.
end = size; // Let done be true.
done = true;
} // Else,
else {
// Let end be e.
end = e; // Let k be e + 1.
k = e + 1;
}
} // Return the String value equal to the substring of extension consisting of
// the code units at indices start (inclusive) through end (exclusive).
return extension.slice(start, end);
} // Let searchValue be the concatenation of "-" and key.
searchValue = `-${key}`; // Let pos be Call(%StringProto_indexOf%, extension, « searchValue »).
pos = String.prototype.indexOf.call(extension, searchValue); // If pos ≠ -1 and pos + 3 = size, then
if (pos !== -1 && pos + 3 === size) {
// Return the empty String.
return "";
} // Return undefined.
return undefined;
}
/**
* The BestAvailableLocale abstract operation compares the provided argument locale,
* which must be a String value with a structurally valid and canonicalized BCP 47 language tag,
* against the locales in availableLocales and returns either the longest non-empty prefix of locale
* that is an element of availableLocales, or undefined if there is no such element. It uses the fallback
* mechanism of RFC 4647, section 3.4.
*
* https://tc39.github.io/ecma402/#sec-bestavailablelocale
* @param {Locales} availableLocales
* @param {Locale} locale
* @return {string}
*/
function bestAvailableLocale(availableLocales, locale) {
// Let candidate be locale.
let candidate = locale; // Repeat
while (true) {
// If availableLocales contains an element equal to candidate, return candidate.
if (availableLocales.includes(candidate)) {
return candidate;
} // Let pos be the character index of the last occurrence of "-" (U+002D) within candidate.
let pos = candidate.lastIndexOf("-"); // If that character does not occur, return undefined.
if (pos === -1) return undefined; // If pos ≥ 2 and the character "-" occurs at index pos-2 of candidate, decrease pos by 2.
if (pos >= 2 && candidate.charAt(pos - 2) === "-") {
pos -= 2;
} // Let candidate be the substring of candidate from position 0, inclusive, to position pos, exclusive.
candidate = candidate.slice(0, pos);
}
}
/**
* Must represent the structurally valid (6.2.2) and canonicalized (6.2.3) BCP 47 language tag for the host environment's current locale.
*
* https://tc39.github.io/ecma402/#sec-defaultlocale
* @type {Locale?}
*/
let _defaultLocale;
/**
* Sets the default locale
* @param {Locale} locale
*/
function setDefaultLocale(locale) {
_defaultLocale = locale;
}
/**
* The DefaultLocale abstract operation returns a String value representing the structurally valid (6.2.2) and canonicalized (6.2.3) BCP 47 language tag for the host environment's current locale.
* https://tc39.github.io/ecma402/#sec-defaultlocale
* @return {Locale | undefined}
*/
function getDefaultLocale() {
return _defaultLocale;
}
/**
* Retrieves the default locale if it is set, and throws otherwise
* @return {Locale}
*/
function ensureDefaultLocale() {
if (_defaultLocale == null) {
throw new ReferenceError(`Could not determine locale: No default locale has been configured`);
}
return _defaultLocale;
}
/**
* The LookupMatcher abstract operation compares requestedLocales, which must be a List as returned by CanonicalizeLocaleList,
* against the locales in availableLocales and determines the best available language to meet the request.
*
* https://tc39.github.io/ecma402/#sec-lookupmatcher
* @param {MatcherOptions} options
* @return {MatcherResult}
*/
function lookupMatcher({
availableLocales,
requestedLocales
}) {
// Let result be a new Record.
const result = {}; // For each element locale of requestedLocales in List order, do
for (const locale of requestedLocales) {
// Let noExtensionsLocale be the String value that is locale with all Unicode locale extension sequences removed.
const noExtensionsLocale = removeUnicodeExtensionSequences(locale); // Let availableLocale be BestAvailableLocale(availableLocales, noExtensionsLocale).
const availableLocale = bestAvailableLocale(availableLocales, noExtensionsLocale); // If availableLocale is not undefined, then
if (availableLocale !== undefined) {
// Set result.[[locale]] to availableLocale.
result.locale = availableLocale; // If locale and noExtensionsLocale are not the same String value, then
if (locale !== noExtensionsLocale) {
// Let extension be the String value consisting of the first substring of local
// that is a Unicode locale extension sequence.
const extensionMatch = locale.match(UNICODE_EXTENSION_SEQUENCE_REGEXP); // Set result.[[extension]] to extension.
result.extension = extensionMatch == null ? "" : extensionMatch[0];
}
return result;
}
} // Let defLocale be DefaultLocale().
const defLocale = ensureDefaultLocale(); // Set result.[[locale]] to defLocale.
result.locale = defLocale; // Return result.
return result;
}
/**
* The BestFitMatcher abstract operation compares requestedLocales,
* which must be a List as returned by CanonicalizeLocaleList,
* against the locales in availableLocales and determines the best available language to meet the request.
* The algorithm is implementation dependent, but should produce results that a typical user of the requested
* locales would perceive as at least as good as those produced by the LookupMatcher abstract operation.
* RelativeTimeFormatOptions specified through Unicode locale extension sequences must be ignored by the algorithm.
* Information about such subsequences is returned separately. The abstract operation returns a record
* with a [[locale]] field, whose value is the language tag of the selected locale,
* which must be an element of availableLocales.
* If the language tag of the request locale that led to the selected locale contained a Unicode locale extension sequence,
* then the returned record also contains an [[extension]] field whose value is the first Unicode locale extension sequence
* within the request locale language tag.
*
* https://tc39.github.io/ecma402/#sec-bestfitmatcher
* @param {MatcherOptions} options
* @return {MatcherResult}
*/
function bestFitMatcher(options) {
return lookupMatcher(options);
}
/**
* Returns true if the given item is a record
* @param {T} item
* @return {item is T}
*/
function isRecord(item) {
return Object.prototype.toString.call(item) === "[object Object]";
}
/**
* Returns true if the given item is a List
* @param {T} item
* @return {item is T}
*/
function isList(item) {
return Array.isArray(item) || isRecord(item);
}
/**
* The internal comparison abstract operation SameValueNonNumber(x, y), where neither x nor y are Number values, produces true or false.
*
* https://tc39.github.io/ecma262/#sec-samevaluenonnumber
* @param {Exclude<*, number>} x
* @param {Exclude<*, number>} y
* @return {boolean}
*/
function sameValueNonNumber(x, y) {
// Assert: Type(x) is not Number.
if (typeof x === "number") {
throw new TypeError(`First argument 'x' must not be a number`);
} // Assert: Type(x) is the same as Type(y).
if (typeof x !== typeof y) {
throw new TypeError(`The given arguments must have the same type`);
} // If Type(x) is Undefined, return true.
if (x === undefined) return true; // If Type(x) is Null, return true.
if (x === null) return true; // If Type(x) is String, then
if (typeof x === "string") {
// If x and y are exactly the same sequence of code units
// (same length and same code units at corresponding indices), return true; otherwise, return false.
return x === y;
} // If Type(x) is Boolean, then
if (typeof x === "boolean") {
// If x and y are both true or both false, return true; otherwise, return false.
return x === y;
} // If Type(x) is Symbol, then
if (typeof x === "symbol") {
// If x and y are both the same Symbol value, return true; otherwise, return false.
return x.valueOf() === y.valueOf();
} // If x and y are the same Object value, return true. Otherwise, return false.
return x === y;
}
/**
* The internal comparison abstract operation SameValue(x, y), where x and y are ECMAScript language values, produces true or false.
*
* https://tc39.github.io/ecma262/#sec-samevalue
* @param {*} x
* @param {*} y
* @return {boolean}
*/
function sameValue(x, y) {
// If Type(x) is different from Type(y), return false.
if (typeof x !== typeof y) return false; // If Type(x) is Number, then
if (typeof x === "number") {
// If x is NaN and y is NaN, return true.
if (isNaN(x) && isNaN(y)) return true; // If x is +0 and y is -0, return false.
if (Object.is(x, 0) && Object.is(y, -0)) return false; // If x is the same Number value as y, return true.
if (x === y) return true; // Return false.
return false;
} // Return SameValueNonNumber(x, y).
return sameValueNonNumber(x, y);
}
/**
* The ResolveLocale abstract operation compares a BCP 47 language priority list
* requestedLocales against the locales in availableLocales and determines the best available language to meet the request.
* availableLocales, requestedLocales, and relevantExtensionKeys must be provided as List values,
* options and localeData as Records.
*
* https://tc39.github.io/ecma402/#sec-resolvelocale
* @param {Locales} availableLocales
* @param {Locales} requestedLocales
* @param {ResolveLocaleOptions} options
* @param {RelevantExtensionKey[]} relevantExtensionKeys
* @param {LocaleData} localeData
* @returns {ResolveLocaleResult}
*/
function resolveLocale(availableLocales, requestedLocales, options, relevantExtensionKeys, localeData) {
// Let matcher be options.[[localeMatcher]].
const matcher = options.localeMatcher; // If matcher is "lookup", then
// (a) Let r be LookupMatcher(availableLocales, requestedLocales).
// (b) Let r be BestFitMatcher(availableLocales, requestedLocales).
const r = matcher === "lookup" ? lookupMatcher({
availableLocales,
requestedLocales
}) : bestFitMatcher({
availableLocales,
requestedLocales
}); // Let foundLocale be r.[[locale]].
let foundLocale = r.locale; // Let result be a new Record.
const result = {}; // Set result.[[dataLocale]] to foundLocale.
result.dataLocale = foundLocale; // Let supportedExtension be "-u"
let supportedExtension = "-u"; // For each element key of relevantExtensionKeys in List order, do
for (const key of relevantExtensionKeys) {
// Let foundLocaleData be localeData.[[<foundLocale>]].
const foundLocaleData = localeData[foundLocale]; // Assert: Type(foundLocaleData) is Record.
if (!isRecord(foundLocaleData)) {
throw new TypeError(`LocaleData for locale: '${foundLocale}' must be an object`);
} // Let keyLocaleData be foundLocaleData.[[<key>]].
const keyLocaleData = foundLocaleData[key]; // Assert: Type(keyLocaleData) is List.
if (!isList(keyLocaleData)) {
throw new TypeError(`key: '${key}' in LocaleData for locale: '${foundLocale}' must be indexable`);
} // Let value be keyLocaleData[0].
let value = keyLocaleData[0]; // Assert: Type(value) is either String or Null.
if (typeof value !== "string" && value !== null) {
throw new TypeError(`value: '${value}' for key: '${key}' in LocaleData for locale: '${foundLocale}' must be a string or null`);
} // Let supportedExtensionAddition be "".
let supportedExtensionAddition = ""; // If r has an [[extension]] field, then
if ("extension" in r) {
// Let requestedValue be UnicodeExtensionValue(r.[[extension]], key).
const requestedValue = unicodeExtensionValue(r.extension, key); // If requestedValue is not undefined, then
if (requestedValue !== undefined) {
// If requestedValue is not the empty String, then
if (requestedValue !== "") {
// If keyLocaleData contains requestedValue, then
if (keyLocaleData.includes(requestedValue)) {
// Let value be requestedValue.
value = requestedValue; // Let supportedExtensionAddition be the concatenation of "-", key, "-", and value.
supportedExtensionAddition = `-${key}-${value}`;
}
} // Else if keyLocaleData contains "true", then
else if (keyLocaleData.includes("true")) {
// Let value be "true".
value = "true";
}
}
} // If options has a field [[<key>]], then
if ("key" in options) {
// Let optionsValue be options.[[<key>]].
const optionsValue = options.key; // Assert: Type(optionsValue) is either String, Undefined, or Null.
if (typeof optionsValue !== "string" && optionsValue != null) {
throw new TypeError(`options value: '${optionsValue}' must be a string, undefined, or null`);
} // If keyLocaleData contains optionsValue, then
if (optionsValue !== undefined && keyLocaleData.includes(optionsValue)) {
// If SameValue(optionsValue, value) is false, then
// tslint:disable-next-line:no-collapsible-if
if (!sameValue(optionsValue, value)) {
// Let value be optionsValue.
value = optionsValue; // Let supportedExtensionAddition be "".
supportedExtensionAddition = "";
}
}
} // Set result.[[<key>]] to value.
result[key] = value; // Append supportedExtensionAddition to supportedExtension.
supportedExtension += supportedExtensionAddition;
} // If the number of elements in supportedExtension is greater than 2, then
if (supportedExtension.length > 2) {
// Let privateIndex be Call(%StringProto_indexOf%, foundLocale, « "-x-" »).
const privateIndex = String.prototype.indexOf.call(foundLocale, "-x-"); // If privateIndex = -1, then
if (privateIndex === -1) {
// Let foundLocale be the concatenation of foundLocale and supportedExtension.
foundLocale = `${foundLocale}${supportedExtension}`;
} // Else,
else {
// Let preExtension be the substring of foundLocale from position 0, inclusive, to position privateIndex, exclusive.
const preExtension = foundLocale.slice(0, privateIndex); // Let postExtension be the substring of foundLocale from position privateIndex to the end of the string.
const postExtension = foundLocale.slice(privateIndex); // Let foundLocale be the concatenation of preExtension, supportedExtension, and postExtension.
foundLocale = `${preExtension}${supportedExtension}${postExtension}`;
} // Assert: IsStructurallyValidLanguageTag(foundLocale) is true.
// Let foundLocale be CanonicalizeLanguageTag(foundLocale).
// Intl.getCanonicalLocales will throw a TypeError if the locale isn't structurally valid
foundLocale = Intl.getCanonicalLocales(foundLocale)[0];
} // Set result.[[locale]] to foundLocale.
result.locale = foundLocale; // Return result.
return result;
}
/**
* The LookupSupportedLocales abstract operation returns the subset of the provided BCP 47 language priority list
* requestedLocales for which availableLocales has a matching locale when using the BCP 47 Lookup algorithm.
* Locales appear in the same order in the returned list as in requestedLocales.
*
* https://tc39.github.io/ecma402/#sec-bestfitsupportedlocales
* @param {Locales} availableLocales
* @param {Locales} requestedLocales
* @return {Locales}
*/
function lookupSupportedLocales(availableLocales, requestedLocales) {
// Let subset be a new empty List.
const subset = []; // For each element locale of requestedLocales in List order, do
for (const locale of requestedLocales) {
// Let noExtensionsLocale be the String value that is locale with all Unicode locale extension sequences removed.
const noExtensionsLocale = removeUnicodeExtensionSequences(locale); // Let availableLocale be BestAvailableLocale(availableLocales, noExtensionsLocale).
const availableLocale = bestAvailableLocale(availableLocales, noExtensionsLocale); // If availableLocale is not undefined, append locale to the end of subset.
if (availableLocale !== undefined) {
subset.push(locale);
}
}
return subset;
}
/**
* The BestFitSupportedLocales abstract operation returns the subset of the provided BCP 47 language priority list
* requestedLocales for which availableLocales has a matching locale when using the Best Fit Matcher algorithm.
* Locales appear in the same order in the returned list as in requestedLocales.
*
* https://tc39.github.io/ecma402/#sec-bestfitsupportedlocales
* @param {Locales} availableLocales
* @param {Locales} requestedLocales
* @return {Locales}
*/
function bestFitSupportedLocales(availableLocales, requestedLocales) {
return lookupSupportedLocales(availableLocales, requestedLocales);
}
/**
* The abstract operation IsPropertyKey determines if argument, which must be an ECMAScript language value, is a value that may be used as a property key.
* https://tc39.es/ecma262/#sec-ispropertykey
* @param {*} argument
* @returns {boolean}
*/
function isPropertyKey(argument) {
// If Type(argument) is String, return true.
if (typeof argument === "string") return true; // If Type(argument) is Symbol, return true.
if (typeof argument === "symbol") return true; // Return false.
return false;
}
/**
* The abstract operation Get is used to retrieve the value of a specific property of an object. The operation is called with arguments O and P where O is the object and P is the property key.
* https://tc39.es/ecma262/#sec-get-o-p
* @param {O} o
* @param {P} p
* @returns {O[P]}
*/
function get(o, p) {
// Assert: Type(O) is Object.
if (typeof o !== "object") {
throw new TypeError(`Given argument ${o} must be of type Object`);
} // Assert: IsPropertyKey(P) is true.
if (!isPropertyKey(p)) {
throw new TypeError(`Given argument ${p} must be a PropertyKey`);
}
return o[p];
}
/**
* The abstract operation ToBoolean converts argument to a value of type Boolean
* https://tc39.es/ecma262/#sec-toboolean
* @param {*} argument
* @returns {boolean}
*/
function toBoolean(argument) {
return Boolean(argument);
}
/**
* https://tc39.es/ecma402/#sec-getoption
* @param {Options} options
* @param {Property} property
* @param {Type} type
* @param {Values} values
* @param {Fallback} fallback
* @returns {Return}
*/
function getOption(options, property, type, values, fallback) {
// Let value be ? Get(options, property).
let value = get(options, property); // If value is not undefined, then
if (value !== undefined) {
// Assert: type is "boolean" or "string".
if (type !== "boolean" && type !== "string") {
throw new TypeError(`Expected type ${type} to be 'boolean' or 'string`);
} // If type is "boolean", then
if (type === "boolean") {
// Let value be ToBoolean(value).
value = toBoolean(value);
} // If type is "string", then
if (type === "string") {
// Let value be ? ToString(value).
value = toString(value);
} // If values is not undefined, then
if (values !== undefined) {
// If values does not contain an element equal to value, throw a RangeError exception.
// tslint:disable-next-line:no-collapsible-if
if (!values.includes(value)) {
throw new RangeError(`Value ${value} out of range for options property ${property}`);
}
} // Return value.
return value;
} // Else, return fallback.
else {
return fallback;
}
}
/**
* The SupportedLocales abstract operation returns the subset of the provided BCP 47 language priority list
* requestedLocales for which availableLocales has a matching locale. Two algorithms are available to match
* the locales: the Lookup algorithm described in RFC 4647 section 3.4, and an implementation dependent
* best-fit algorithm. Locales appear in the same order in the returned list as in requestedLocales.
*
* https://tc39.github.io/ecma402/#sec-supportedlocales
* @param {Locales} availableLocales
* @param {Locales} requestedLocales
* @param {SupportedLocalesOptions} [options]
* @return {Locales}
*/
function supportedLocales(availableLocales, requestedLocales, options) {
let matcher; // If options is not undefined, then
if (options !== undefined) {
// Let options be ? ToObject(options).
options = toObject(options); // Let matcher be ? GetOption(options, "localeMatcher", "string", « "lookup", "best fit" », "best fit").
matcher = getOption(options, "localeMatcher", "string", LOCALE_MATCHER, "best fit");
} // Else, let matcher be "best fit".
else {
matcher = "best fit";
} // If matcher is "best fit", then let supportedLocales be BestFitSupportedLocales(availableLocales, requestedLocales).
// Else let supportedLocales be LookupSupportedLocales(availableLocales, requestedLocales).
// Return CreateArrayFromList(supportedLocales).
return matcher === "best fit" ? bestFitSupportedLocales(availableLocales, requestedLocales) : lookupSupportedLocales(availableLocales, requestedLocales);
}
/**
* A WeakMap between RelativeTimeFormat instances and their internal slot members
* @type {WeakMap<RelativeTimeFormat, RelativeTimeFormatInstanceInternals>}
*/
const RELATIVE_TIME_FORMAT_INSTANCE_INTERNAL_MAP = new WeakMap();
/**
* Contains the internal static for RelativeTimeFormat
* @type {RelativeTimeFormatStaticInternals}
*/
const RELATIVE_TIME_FORMAT_STATIC_INTERNALS = {
/**
* The value of the [[RelevantExtensionKeys]] internal slot is « "nu" ».
* http://tc39.github.io/proposal-intl-relative-time/#sec-Intl.RelativeTimeFormat-internal-slots
*/
relevantExtensionKeys: ["nu"],
/**
* The value of the [[LocaleData]] internal slot is implementation defined within the constraints described in 9.1
* http://tc39.github.io/proposal-intl-relative-time/#sec-Intl.RelativeTimeFormat-internal-slots
*/
localeData: {},
/**
* The value of the [[AvailableLocales]] internal slot is implementation defined within the constraints described in 9.1
* http://tc39.github.io/proposal-intl-relative-time/#sec-Intl.RelativeTimeFormat-internal-slots
*/
availableLocales: []
};
/**
* Sets the value for a property in an internal slot for an instance of RelativeTimeFormat
* @param {RelativeTimeFormat} instance
* @param {T} property
* @param {RelativeTimeFormatInstanceInternals[T]} value
*/
function setInternalSlot(instance, property, value) {
let record = RELATIVE_TIME_FORMAT_INSTANCE_INTERNAL_MAP.get(instance);
if (record == null) {
record = Object.create(null);
RELATIVE_TIME_FORMAT_INSTANCE_INTERNAL_MAP.set(instance, record);
} // Update the property with the given value
record[property] = value;
}
/**
* Gets the value associated with the given property on the internal slots of the given instance of RelativeTimeFormat
* @param {RelativeTimeFormat} instance
* @param {T} property
* @return {RelativeTimeFormatInstanceInternals[T]}
*/
function getInternalSlot(instance, property) {
const record = RELATIVE_TIME_FORMAT_INSTANCE_INTERNAL_MAP.get(instance);
if (record == null) {
throw new ReferenceError(`No internal slots has been allocated for the given instance of RelativeTimeFormat`);
}
return record[property];
}
/**
* Returns true if the given property on the internal slots of the given instance of RelativeTimeFormat exists
* @param {RelativeTimeFormat} instance
* @param {T} property
* @return {RelativeTimeFormatInstanceInternals[T]}
*/
function hasInternalSlot(instance, property) {
const record = RELATIVE_TIME_FORMAT_INSTANCE_INTERNAL_MAP.get(instance);
return record != null && property in record;
}
/**
* When the ResolvePlural abstract operation is called with arguments pluralRules (which must be an object initialized as a PluralRules) and n (which must be a Number value), it returns a String value representing the plural form of n according to the effective locale and the options of pluralRules.
*
* https://tc39.github.io/ecma402/#sec-resolveplural
* @param {RelativeTimeFormat} relativeTimeFormat - needed to get internal slots
* @param {number} n
*/
function resolvePlural(relativeTimeFormat, n) {
// Assert: Type(pluralRules) is Object.
// Assert: pluralRules has an [[InitializedPluralRules]] internal slot.
if (!hasInternalSlot(relativeTimeFormat, "pluralRules")) {
throw new TypeError(`Given instance of of Intl.RelativeTimeFormat must have an [[InitializedPluralRules]] internal slot`);
} // Assert: Type(n) is Number.
if (typeof n !== "number") {
throw new TypeError(`Argument 'n' must be a number`);
} // If n is not a finite Number, then
if (!isFinite(n)) {
// Return "other".
return "other";
} // Let locale be pluralRules.[[Locale]].
// Let type be pluralRules.[[Type]].
const pluralRules = getInternalSlot(relativeTimeFormat, "pluralRules"); // Return ? PluralRuleSelect(locale, type, n, operands).
return pluralRules.select(n);
}
/**
* The MakePartsList abstract operation is called with arguments pattern,
* a pattern String, unit, a String, and parts, a List of Records representing a formatted Number.
*
* http://tc39.github.io/proposal-intl-relative-time/#sec-makepartslist
* @param {string} pattern
* @param {SingularRelativeTimeUnit} unit
* @param {Intl.NumberFormatPart[]} parts
* @returns {RelativeTimeFormatPart}
*/
function makePartsList(pattern, unit, parts) {
// Let result be a new empty List.
const result = []; // Let beginIndex be ! Call(%StringProto_indexOf%, pattern, « "{", 0 »).
let beginIndex = String.prototype.indexOf.call(pattern, "{", 0); // Let endIndex be 0.
let endIndex = 0; // Let nextIndex be 0.
let nextIndex = 0; // Let length be the number of elements in pattern.
const length = pattern.length; // Repeat, while beginIndex is an integer index into pattern
while (pattern[beginIndex] !== undefined) {
// Set endIndex to ! Call(%StringProto_indexOf%, pattern, « "}", beginIndex »).
endIndex = String.prototype.indexOf.call(pattern, "}", beginIndex); // Assert: endIndex is not -1, otherwise the pattern would be malformed.
if (endIndex === -1) {
throw new RangeError(`The pattern: '${pattern}' is malformed`);
} // If beginIndex is greater than nextIndex, then
if (beginIndex > nextIndex) {
// Let literal be a substring of pattern from position nextIndex, inclusive, to position beginIndex, exclusive.
const literal = pattern.slice(nextIndex, beginIndex); // Add new part Record { [[Type]]: "literal", [[Value]]: literal } as a new element of the list result.
result.push({
type: "literal",
value: literal
});
} // Let p be the substring of pattern from position beginIndex, exclusive, to position endIndex, exclusive.
const p = pattern.slice(beginIndex + 1, endIndex); // Assert: p is "0".
if (p !== "0") {
throw new TypeError(`Expected ${p} to be "0"`);
} // For each part in parts, do
for (const part of parts) {
// Add new part Record { [[Type]]: part.[[Type]], [[Value]]: part.[[Value]], [[Unit]]: unit } as a new element on the List result.
if (part.type === "literal") {
result.push({ ...part,
type: part.type
});
} else {
result.push({ ...part,
unit
});
}
} // Set nextIndex to endIndex + 1.
nextIndex = endIndex + 1; // Set beginIndex to Call(%StringProto_indexOf%, pattern, « "{", nextIndex »).
beginIndex = String.prototype.indexOf.call(pattern, "{", nextIndex);
} // If nextIndex is less than length, then
if (nextIndex < length) {
// Let literal be the substring of pattern from position nextIndex, exclusive, to position length, exclusive.
// CORRECTION: It should actually be from nextIndex, inclusive, to correctly partition text
const literal = pattern.slice(nextIndex, length); // Add new part Record { [[Type]]: "literal", [[Value]]: literal } as a new element of the list result.
result.push({
type: "literal",
value: literal
});
}
return result;
}
/**
* When the FormatRelativeTime abstract operation is called with arguments relativeTimeFormat,
* value, and unit it returns a String value representing value (interpreted as a time value as specified in ES2016, 20.3.1.1)
* according to the effective locale and the formatting options of relativeTimeFormat.
* @param {RelativeTimeFormat} relativeTimeFormat
* @param {number} value
* @param {RelativeTimeUnit} unit
* @returns {RelativeTimeFormatPart[]}
*/
function partitionRelativeTimePattern(relativeTimeFormat, value, unit) {
// Assert: relativeTimeFormat has an [[InitializedRelativeTimeFormat]] internal slot.
if (!hasInternalSlot(relativeTimeFormat, "initializedRelativeTimeFormat")) {
throw new TypeError(`Internal function called on incompatible receiver ${relativeTimeFormat.toString()}`);
} // Assert: Type(value) is Number.
if (typeof value !== "number") {
throw new TypeError(`Argument: 'value' must be a number`);
} // Assert: Type(unit) is String.
if (typeof unit !== "string") {
throw new TypeError(`Argument: 'unit' must be a string`);
} // If value is NaN, +∞, or -∞, throw a RangeError exception.
if (isNaN(value) || value === Infinity || value === -Infinity) {
throw new RangeError(`Value need to be finite number`);
} // Let unit be ? SingularRelativeTimeUnit(unit).
unit = singularRelativeTimeUnit(unit); // Let fields be relativeTimeFormat.[[Fields]].
const fields = getInternalSlot(relativeTimeFormat, "fields"); // Let style be relativeTimeFormat.[[Style]].
const style = getInternalSlot(relativeTimeFormat, "style"); // If style is equal to "short", then let entry be the string-concatenation of unit and "-short".
// Else if style is equal to "narrow", then let entry be the string-concatenation of unit and "-narrow".
// Else let entry be unit.
let entry = style === "short" ? `${unit}-short` : style === "narrow" ? `${unit}-narrow` : unit; // Let exists be ! HasProperty(fields, entry).
let exists = entry in fields; // If exists is false, then
if (!exists) {
// Let entry be unit.
entry = unit;
} // Let patterns be ! Get(fields, entry).
const patterns = fields[entry]; // Make sure that the patterns are defined
if (patterns == null) {
throw new TypeError(`Could not match entry: '${entry}' inside fields for locale: '${getInternalSlot(relativeTimeFormat, "locale")}'`);
} // Let numeric be relativeTimeFormat.[[Numeric]].
const numeric = getInternalSlot(relativeTimeFormat, "numeric"); // If numeric is equal to "auto", then
if (numeric === "auto") {
// Let exists be ! HasProperty(patterns, ! ToString(value)).
exists = toString(value) in patterns; // If exists is true, then
if (exists) {
// Let result be ! Get(patterns, ! ToString(value)).
const result = patterns[toString(value)]; // Return a List containing the Record { [[Type]]: "literal", [[Value]]: result }.
return [{
type: "literal",
value: result
}];
}
} // If value is -0 or if value is less than 0, then let tl be "past".
// Else let tl be "future".
const tl = Object.is(value, -0) || value < 0 ? "past" : "future"; // Let po be ! Get(patterns, tl).
const po = patterns[tl]; // Let fv be ! PartitionNumberPattern(relativeTimeFormat.[[NumberFormat]], value).
const fv = getInternalSlot(relativeTimeFormat, "numberFormat").formatToParts(Math.abs(value)); // Let pr be ! ResolvePlural(relativeTimeFormat.[[PluralRules]], value).
const pr = resolvePlural(relativeTimeFormat, value); // Let pattern be ! Get(po, pr).
const pattern = po[pr]; // Return ! MakePartsList(pattern, unit, fv).
return makePartsList(pattern, unit, fv);
}
/**
* The FormatRelativeTime abstract operation is called with arguments relativeTimeFormat
* (which must be an object initialized as a RelativeTimeFormat), value (which must be a Number value),
* and unit (which must be a String denoting the value unit) and performs the following steps:
*
* http://tc39.github.io/proposal-intl-relative-time/#sec-FormatRelativeTime
* @param {RelativeTimeFormat} relativeTimeFormat
* @param {number} value
* @param {RelativeTimeUnit} unit
* @return {string}
*/
function formatRelativeTime(relativeTimeFormat, value, unit) {
// Let parts be ? PartitionRelativeTimePattern(relativeTimeFormat, value, unit).
const parts = partitionRelativeTimePattern(relativeTimeFormat, value, unit); // Let result be an empty String.
let result = ""; // For each part in parts, do
for (const part of parts) {
// Set result to the string-concatenation of result and part.[[Value]].
result += part.value;
} // Return result.
return result;
}
/**
* The FormatRelativeTimeToParts abstract operation is called with arguments relativeTimeFormat
* (which must be an object initialized as a RelativeTimeFormat), value (which must be a Number value),
* and unit (which must be a String denoting the value unit)
*
* http://tc39.github.io/proposal-intl-relative-time/#sec-FormatRelativeTimeToParts
* @param {RelativeTimeFormat} relativeTimeFormat
* @param {number} value
* @param {RelativeTimeUnit} unit
* @return {RelativeTimeFormatPart[]}
*/
function formatRelativeTimeToParts(relativeTimeFormat, value, unit) {
return partitionRelativeTimePattern(relativeTimeFormat, value, unit);
}
/**
* The abstract operation ToNumber converts argument to a value of type Number
* https://tc39.es/ecma262/#sec-tonumber
* @param {*} argument
* @returns {boolean}
*/
function toNumber(argument) {
return Number(argument);
}
/**
* The RelativeTimeFormat constructor is the %RelativeTimeFormat% intrinsic object and a standard built-in property of the Intl object.
* Behaviour common to all service constructor properties of the Intl object is specified in 9.1.
*
* http://tc39.github.io/proposal-intl-relative-time/#sec-intl-relativetimeformat-constructor
*/
class RelativeTimeFormat {
// The spec states that the constructor must have a length of 0 and therefore be parameter-less
constructor() {
const locales = arguments[0];
let options = arguments[1]; // If NewTarget is undefined, throw a TypeError exception.
if (new.target === undefined) {
throw new TypeError(`Constructor Intl.RelativeTimeFormat requires 'new'`);
} // The following operations comes from the 'InitializeRelativeFormat' abstract operation (http://tc39.github.io/proposal-intl-relative-time/#sec-InitializeRelativeTimeFormat)
// Let requestedLocales be ? CanonicalizeLocaleList(locales).
const requestedLocales = Intl.getCanonicalLocales(locales); // If options is undefined, then (a) Let options be ObjectCreate(null).
// Else (b) Let options be ? ToObject(options).
options = options === undefined ? Object.create(null) : toObject(options); // Let opt be a new Record (that doesn't derive from Object.prototype).
const opt = Object.create(null); // Let matcher be ? GetOption(options, "localeMatcher", "string", «"lookup", "best fit"», "best fit").
const matcher = getOption(options, "localeMatcher", "string", LOCALE_MATCHER, "best fit"); // Set opt.[[LocaleMatcher]] to matcher.
opt.localeMatcher = matcher; // Let localeData be %RelativeTimeFormat%.[[LocaleData]].
const localeData = RELATIVE_TIME_FORMAT_STATIC_INTERNALS.localeData; // Let r be ResolveLocale(%RelativeTimeFormat%.[[AvailableLocales]], requestedLocales, opt, %RelativeTimeFormat%.[[RelevantExtensionKeys]], localeData).
const r = resolveLocale(RELATIVE_TIME_FORMAT_STATIC_INTERNALS.availableLocales, requestedLocales, opt, RELATIVE_TIME_FORMAT_STATIC_INTERNALS.relevantExtensionKeys, localeData); // Let locale be r.[[Locale]].
const locale = r.locale; // Set relativeTimeFormat.[[Locale]] to locale.
setInternalSlot(this, "locale", locale); // Set relativeTimeFormat.[[NumberingSystem]] to r_.[[nu]].
setInternalSlot(this, "numberingSystem", r.nu); // Let dataLocale be r.[[DataLocale]].
const dataLocale = r.dataLocale; // Let s be ? GetOption(options, "style", "string", «"long", "short", "narrow"», "long").
const s = getOption(options, "style", "string", STYLE, "long"); // Set relativeTimeFormat.[[Style]] to s.
setInternalSlot(this, "style", s); // Let numeric be ? GetOption(options, "numeric", "string", «"always", "auto"», "always").
const numeric = getOption(options, "numeric", "string", NUMERIC, "always"); // Set relativeTimeFormat.[[Numeric]] to numeric.
setInternalSlot(this, "numeric", numeric); // Let fields be ! Get(localeData, dataLocale).
const fields = localeData[dataLocale]; // Assert: fields is an object (see 1.3.3).
if (!(fields instanceof Object)) {
throw new TypeError(`Expected the LocaleDataEntry for locale: '${dataLocale}' to be an Object`);
} // Set relativeTimeFormat.[[Fields]] to fields.
setInternalSlot(this, "fields", fields); // Let relativeTimeFormat.[[NumberFormat]] be ! Construct(%NumberFormat%, « locale »).
setInternalSlot(this, "numberFormat", new Intl.NumberFormat(locale)); // Let relativeTimeFormat.[[PluralRules]] be ! Construct(%PluralRules%, « locale »).
// tslint:disable-next-line:no-any
setInternalSlot(this, "pluralRules", new Intl.PluralRules(locale)); // Intl.RelativeTimeFormat instances have an [[InitializedRelativeTimeFormat]] internal slot.
setInternalSlot(this, "initializedRelativeTimeFormat", this);
}
/**
* Returns an array containing those of the provided locales that are supported without having to fall back to the runtime's default locale.
* @param {Locale | Locales} locales
* @return {Locales}
*/
static supportedLocalesOf(locales) {
// The spec states that the 'length' value of supportedLocalesOf must be equal to 1,
// so we have to pull the options argument out of the method signature
const options = arguments[1]; // Let availableLocales be %RelativeTimeFormat%.[[AvailableLocales]].
const availableLocales = RELATIVE_TIME_FORMAT_STATIC_INTERNALS.availableLocales; // Let requestedLocales be ? CanonicalizeLocaleList(locales).
const requestedLocales = Intl.getCanonicalLocales(locales);
return supportedLocales(availableLocales, requestedLocales, options);
}
/**
* Adds locale data to the internal slot.
* This API exactly mimics that of the Intl polyfill (https://github.com/andyearnshaw/Intl.js)
* @private
* @internal
* @param {InputLocaleDataEntry} data
* @param {Locale} locale
*/
static __addLocaleData({
data,
locale
}) {
// Use the locale as the default one if none is configured
const defaultLocale = getDefaultLocale();
if (defaultLocale == null) {
setDefaultLocale(locale);
}
RELATIVE_TIME_FORMAT_STATIC_INTERNALS.localeData[locale] = data;
if (!RELATIVE_TIME_FORMAT_STATIC_INTERNALS.availableLocales.includes(locale)) {
RELATIVE_TIME_FORMAT_STATIC_INTERNALS.availableLocales.push(locale);
}
}
/**
* Method that formats a value and unit according to the locale and formatting options of this Intl.RelativeTimeFormat object.
* @param {number} value
* @param {RelativeTimeUnit} unit
* @return {string}
*/
format(value, unit) {
// Let relativeTimeFormat be the this value.
const relativeTimeFormat = this; // If Type(relativeTimeFormat) is not Object, throw a TypeError exception.
if (!(relativeTimeFormat instanceof Object)) {
throw new TypeError(`Method Intl.RelativeTimeFormat.prototype.format called on incompatible receiver ${this.toString()}`);
} // If relativeTimeFormat does not have an [[InitializedRelativeTimeFormat]] internal slot, throw a TypeError exception.
if (!hasInternalSlot(relativeTimeFormat, "initializedRelativeTimeFormat")) {
throw new TypeError(`Method Intl.RelativeTimeFormat.prototype.format called on incompatible receiver ${this.toString()}`);
} // Let value be ? ToNumber(value).
value = toNumber(value); // Let unit be ? ToString(unit).
unit = toString(unit); // Return ? FormatRelativeTime(relativeTimeFormat, value, unit).
return formatRelativeTime(relativeTimeFormat, value, unit);
}
/**
* A version of the 'format' method that returns an array of objects which represent "parts" of the object,
* separating the formatted number into its constituent parts and separating it from other surrounding text
* @param {number} value
* @param {RelativeTimeUnit} unit
* @return {RelativeTimeFormatPart[]}
*/
formatToParts(value, unit) {
// Let relativeTimeFormat be the this value.
const relativeTimeFormat = this; // If Type(relativeTimeFormat) is not Object, throw a TypeError exception.
if (!(relativeTimeFormat instanceof Object)) {
throw new TypeError(`Method Intl.RelativeTimeFormat.prototype.formatToParts called on incompatible receiver ${this.toString()}`);
} // If relativeTimeFormat does not have an [[InitializedRelativeTimeFormat]] internal slot, throw a TypeError exception.
if (!hasInternalSlot(relativeTimeFormat, "initializedRelativeTimeFormat")) {
throw new TypeError(`Method Intl.RelativeTimeFormat.prototype.formatToParts called on incompatible receiver ${this.toString()}`);
} // Let value be ? ToNumber(value).
value = toNumber(value); // Let unit be ? ToString(unit).
unit = toString(unit); // Return ? FormatRelativeTimeToParts(relativeTimeFormat, value, unit).
return formatRelativeTimeToParts(relativeTimeFormat, value, unit);
}
/**
* This method provides access to the locale and options computed during initialization of the object.
* @returns {ResolvedRelativeTimeFormatOptions}
*/
resolvedOptions() {
// Let relativeTimeFormat be the this value.
const relativeTimeFormat = this; // If Type(relativeTimeFormat) is not Object, throw a TypeError exception.
if (!(relativeTimeFormat instanceof Object)) {
throw new TypeError(`Method Intl.RelativeTimeFormat.prototype.resolvedOptions called on incompatible receiver ${this.toString()}`);
} // If relativeTimeFormat does not have an [[InitializedRelativeTimeFormat]] internal slot, throw a TypeError exception.
if (!hasInternalSlot(relativeTimeFormat, "initializedRelativeTimeFormat")) {
throw new TypeError(`Method Intl.RelativeTimeFormat.prototype.resolvedOptions called on incompatible receiver ${this.toString()}`);
}
const locale = getInternalSlot(this, "locale");
const style = getInternalSlot(this, "style");
const numeric = getInternalSlot(this, "numeric");
const numberingSystem = getInternalSlot(this, "numberingSystem");
return {
locale,
style,
numeric,
numberingSystem
};
}
}
/**
* The initial value of the @@toStringTag property is the string value "Intl.RelativeTimeFormat".
* This property has the attributes { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true }.
* @type {string}
*/
Object.defineProperty(RelativeTimeFormat.prototype, Symbol.toStringTag, {
writable: false,
enumerable: false,
value: "Intl.RelativeTimeFormat",
configurable: true
});
/**
* Patches Intl with Intl.RelativeTimeFormat
*/
function patch() {
if (typeof Intl === "undefined") {
throw new TypeError(`Could not define Intl.RelativeTimeFormat: Expected 'Intl' to exist. Remember to include polyfills for Intl.NumberFormat, Intl.getCanonicalLocales, and Intl.PluralRules before applying this polyfill`);
}
Intl.RelativeTimeFormat = RelativeTimeFormat;
}
if (!SUPPORTS_RELATIVE_TIME_FORMAT) {
patch();
}
//# sourceMappingURL=index.esm.js.map