intl
Version:
Polyfill the ECMA-402 Intl API (except collation)
603 lines (518 loc) • 24.9 kB
JavaScript
// Sect 9.2 Abstract Operations
// ============================
import {
List,
toObject,
arrIndexOf,
arrPush,
arrSlice,
Record,
hop,
defineProperty,
} from "./util.js";
import {
IsStructurallyValidLanguageTag,
CanonicalizeLanguageTag,
DefaultLocale,
} from "./6.locales-currencies-tz.js";
const expUnicodeExSeq = /-u(?:-[0-9a-z]{2,8})+/gi; // See `extension` below
export function /* 9.2.1 */CanonicalizeLocaleList (locales) {
// The abstract operation CanonicalizeLocaleList takes the following steps:
// 1. If locales is undefined, then a. Return a new empty List
if (locales === undefined)
return new List();
// 2. Let seen be a new empty List.
let seen = new List();
// 3. If locales is a String value, then
// a. Let locales be a new array created as if by the expression new
// Array(locales) where Array is the standard built-in constructor with
// that name and locales is the value of locales.
locales = typeof locales === 'string' ? [ locales ] : locales;
// 4. Let O be ToObject(locales).
let O = toObject(locales);
// 5. Let lenValue be the result of calling the [[Get]] internal method of
// O with the argument "length".
// 6. Let len be ToUint32(lenValue).
let len = O.length;
// 7. Let k be 0.
let k = 0;
// 8. Repeat, while k < len
while (k < len) {
// a. Let Pk be ToString(k).
let Pk = String(k);
// b. Let kPresent be the result of calling the [[HasProperty]] internal
// method of O with argument Pk.
let kPresent = Pk in O;
// c. If kPresent is true, then
if (kPresent) {
// i. Let kValue be the result of calling the [[Get]] internal
// method of O with argument Pk.
let kValue = O[Pk];
// ii. If the type of kValue is not String or Object, then throw a
// TypeError exception.
if (kValue === null || (typeof kValue !== 'string' && typeof kValue !== 'object'))
throw new TypeError('String or Object type expected');
// iii. Let tag be ToString(kValue).
let tag = String(kValue);
// iv. If the result of calling the abstract operation
// IsStructurallyValidLanguageTag (defined in 6.2.2), passing tag as
// the argument, is false, then throw a RangeError exception.
if (!IsStructurallyValidLanguageTag(tag))
throw new RangeError("'" + tag + "' is not a structurally valid language tag");
// v. Let tag be the result of calling the abstract operation
// CanonicalizeLanguageTag (defined in 6.2.3), passing tag as the
// argument.
tag = CanonicalizeLanguageTag(tag);
// vi. If tag is not an element of seen, then append tag as the last
// element of seen.
if (arrIndexOf.call(seen, tag) === -1)
arrPush.call(seen, tag);
}
// d. Increase k by 1.
k++;
}
// 9. Return seen.
return seen;
}
/**
* 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. The following steps are taken:
*/
export function /* 9.2.2 */BestAvailableLocale (availableLocales, locale) {
// 1. Let candidate be locale
let candidate = locale;
// 2. Repeat
while (candidate) {
// a. If availableLocales contains an element equal to candidate, then return
// candidate.
if (arrIndexOf.call(availableLocales, candidate) > -1)
return candidate;
// b. Let pos be the character index of the last occurrence of "-"
// (U+002D) within candidate. If that character does not occur, return
// undefined.
let pos = candidate.lastIndexOf('-');
if (pos < 0)
return;
// c. If pos ≥ 2 and the character "-" occurs at index pos-2 of candidate,
// then decrease pos by 2.
if (pos >= 2 && candidate.charAt(pos - 2) === '-')
pos -= 2;
// d. Let candidate be the substring of candidate from position 0, inclusive,
// to position pos, exclusive.
candidate = candidate.substring(0, pos);
}
}
/**
* 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. The following steps are taken:
*/
export function /* 9.2.3 */LookupMatcher (availableLocales, requestedLocales) {
// 1. Let i be 0.
let i = 0;
// 2. Let len be the number of elements in requestedLocales.
let len = requestedLocales.length;
// 3. Let availableLocale be undefined.
let availableLocale;
let locale, noExtensionsLocale;
// 4. Repeat while i < len and availableLocale is undefined:
while (i < len && !availableLocale) {
// a. Let locale be the element of requestedLocales at 0-origined list
// position i.
locale = requestedLocales[i];
// b. Let noExtensionsLocale be the String value that is locale with all
// Unicode locale extension sequences removed.
noExtensionsLocale = String(locale).replace(expUnicodeExSeq, '');
// c. Let availableLocale be the result of calling the
// BestAvailableLocale abstract operation (defined in 9.2.2) with
// arguments availableLocales and noExtensionsLocale.
availableLocale = BestAvailableLocale(availableLocales, noExtensionsLocale);
// d. Increase i by 1.
i++;
}
// 5. Let result be a new Record.
let result = new Record();
// 6. If availableLocale is not undefined, then
if (availableLocale !== undefined) {
// a. Set result.[[locale]] to availableLocale.
result['[[locale]]'] = availableLocale;
// b. If locale and noExtensionsLocale are not the same String value, then
if (String(locale) !== String(noExtensionsLocale)) {
// i. Let extension be the String value consisting of the first
// substring of locale that is a Unicode locale extension sequence.
let extension = locale.match(expUnicodeExSeq)[0];
// ii. Let extensionIndex be the character position of the initial
// "-" of the first Unicode locale extension sequence within locale.
let extensionIndex = locale.indexOf('-u-');
// iii. Set result.[[extension]] to extension.
result['[[extension]]'] = extension;
// iv. Set result.[[extensionIndex]] to extensionIndex.
result['[[extensionIndex]]'] = extensionIndex;
}
}
// 7. Else
else
// a. Set result.[[locale]] to the value returned by the DefaultLocale abstract
// operation (defined in 6.2.4).
result['[[locale]]'] = DefaultLocale();
// 8. 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. Options
* 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, and an [[extensionIndex]] field whose value
* is the index of the first Unicode locale extension sequence within the request
* locale language tag.
*/
export function /* 9.2.4 */BestFitMatcher (availableLocales, requestedLocales) {
return LookupMatcher(availableLocales, requestedLocales);
}
/**
* 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 and
* requestedLocales must be provided as List values, options as a Record.
*/
export function /* 9.2.5 */ResolveLocale (availableLocales, requestedLocales, options, relevantExtensionKeys, localeData) {
if (availableLocales.length === 0) {
throw new ReferenceError('No locale data has been provided for this object yet.');
}
// The following steps are taken:
// 1. Let matcher be the value of options.[[localeMatcher]].
let matcher = options['[[localeMatcher]]'];
let r;
// 2. If matcher is "lookup", then
if (matcher === 'lookup')
// a. Let r be the result of calling the LookupMatcher abstract operation
// (defined in 9.2.3) with arguments availableLocales and
// requestedLocales.
r = LookupMatcher(availableLocales, requestedLocales);
// 3. Else
else
// a. Let r be the result of calling the BestFitMatcher abstract
// operation (defined in 9.2.4) with arguments availableLocales and
// requestedLocales.
r = BestFitMatcher(availableLocales, requestedLocales);
// 4. Let foundLocale be the value of r.[[locale]].
let foundLocale = r['[[locale]]'];
let extensionSubtags, extensionSubtagsLength;
// 5. If r has an [[extension]] field, then
if (hop.call(r, '[[extension]]')) {
// a. Let extension be the value of r.[[extension]].
let extension = r['[[extension]]'];
// b. Let split be the standard built-in function object defined in ES5,
// 15.5.4.14.
let split = String.prototype.split;
// c. Let extensionSubtags be the result of calling the [[Call]] internal
// method of split with extension as the this value and an argument
// list containing the single item "-".
extensionSubtags = split.call(extension, '-');
// d. Let extensionSubtagsLength be the result of calling the [[Get]]
// internal method of extensionSubtags with argument "length".
extensionSubtagsLength = extensionSubtags.length;
}
// 6. Let result be a new Record.
let result = new Record();
// 7. Set result.[[dataLocale]] to foundLocale.
result['[[dataLocale]]'] = foundLocale;
// 8. Let supportedExtension be "-u".
let supportedExtension = '-u';
// 9. Let i be 0.
let i = 0;
// 10. Let len be the result of calling the [[Get]] internal method of
// relevantExtensionKeys with argument "length".
let len = relevantExtensionKeys.length;
// 11 Repeat while i < len:
while (i < len) {
// a. Let key be the result of calling the [[Get]] internal method of
// relevantExtensionKeys with argument ToString(i).
let key = relevantExtensionKeys[i];
// b. Let foundLocaleData be the result of calling the [[Get]] internal
// method of localeData with the argument foundLocale.
let foundLocaleData = localeData[foundLocale];
// c. Let keyLocaleData be the result of calling the [[Get]] internal
// method of foundLocaleData with the argument key.
let keyLocaleData = foundLocaleData[key];
// d. Let value be the result of calling the [[Get]] internal method of
// keyLocaleData with argument "0".
let value = keyLocaleData['0'];
// e. Let supportedExtensionAddition be "".
let supportedExtensionAddition = '';
// f. Let indexOf be the standard built-in function object defined in
// ES5, 15.4.4.14.
let indexOf = arrIndexOf;
// g. If extensionSubtags is not undefined, then
if (extensionSubtags !== undefined) {
// i. Let keyPos be the result of calling the [[Call]] internal
// method of indexOf with extensionSubtags as the this value and
// an argument list containing the single item key.
let keyPos = indexOf.call(extensionSubtags, key);
// ii. If keyPos ≠ -1, then
if (keyPos !== -1) {
// 1. If keyPos + 1 < extensionSubtagsLength and the length of the
// result of calling the [[Get]] internal method of
// extensionSubtags with argument ToString(keyPos +1) is greater
// than 2, then
if (keyPos + 1 < extensionSubtagsLength
&& extensionSubtags[keyPos + 1].length > 2) {
// a. Let requestedValue be the result of calling the [[Get]]
// internal method of extensionSubtags with argument
// ToString(keyPos + 1).
let requestedValue = extensionSubtags[keyPos + 1];
// b. Let valuePos be the result of calling the [[Call]]
// internal method of indexOf with keyLocaleData as the
// this value and an argument list containing the single
// item requestedValue.
let valuePos = indexOf.call(keyLocaleData, requestedValue);
// c. If valuePos ≠ -1, then
if (valuePos !== -1) {
// i. Let value be requestedValue.
value = requestedValue,
// ii. Let supportedExtensionAddition be the
// concatenation of "-", key, "-", and value.
supportedExtensionAddition = '-' + key + '-' + value;
}
}
// 2. Else
else {
// a. Let valuePos be the result of calling the [[Call]]
// internal method of indexOf with keyLocaleData as the this
// value and an argument list containing the single item
// "true".
let valuePos = indexOf(keyLocaleData, 'true');
// b. If valuePos ≠ -1, then
if (valuePos !== -1)
// i. Let value be "true".
value = 'true';
}
}
}
// h. If options has a field [[<key>]], then
if (hop.call(options, '[[' + key + ']]')) {
// i. Let optionsValue be the value of options.[[<key>]].
let optionsValue = options['[[' + key + ']]'];
// ii. If the result of calling the [[Call]] internal method of indexOf
// with keyLocaleData as the this value and an argument list
// containing the single item optionsValue is not -1, then
if (indexOf.call(keyLocaleData, optionsValue) !== -1) {
// 1. If optionsValue is not equal to value, then
if (optionsValue !== value) {
// a. Let value be optionsValue.
value = optionsValue;
// b. Let supportedExtensionAddition be "".
supportedExtensionAddition = '';
}
}
}
// i. Set result.[[<key>]] to value.
result['[[' + key + ']]'] = value;
// j. Append supportedExtensionAddition to supportedExtension.
supportedExtension += supportedExtensionAddition;
// k. Increase i by 1.
i++;
}
// 12. If the length of supportedExtension is greater than 2, then
if (supportedExtension.length > 2) {
// a.
let privateIndex = foundLocale.indexOf("-x-");
// b.
if (privateIndex === -1) {
// i.
foundLocale = foundLocale + supportedExtension;
}
// c.
else {
// i.
let preExtension = foundLocale.substring(0, privateIndex);
// ii.
let postExtension = foundLocale.substring(privateIndex);
// iii.
foundLocale = preExtension + supportedExtension + postExtension;
}
// d. asserting - skipping
// e.
foundLocale = CanonicalizeLanguageTag(foundLocale);
}
// 13. Set result.[[locale]] to foundLocale.
result['[[locale]]'] = foundLocale;
// 14. 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.
* The following steps are taken:
*/
export function /* 9.2.6 */LookupSupportedLocales (availableLocales, requestedLocales) {
// 1. Let len be the number of elements in requestedLocales.
let len = requestedLocales.length;
// 2. Let subset be a new empty List.
let subset = new List();
// 3. Let k be 0.
let k = 0;
// 4. Repeat while k < len
while (k < len) {
// a. Let locale be the element of requestedLocales at 0-origined list
// position k.
let locale = requestedLocales[k];
// b. Let noExtensionsLocale be the String value that is locale with all
// Unicode locale extension sequences removed.
let noExtensionsLocale = String(locale).replace(expUnicodeExSeq, '');
// c. Let availableLocale be the result of calling the
// BestAvailableLocale abstract operation (defined in 9.2.2) with
// arguments availableLocales and noExtensionsLocale.
let availableLocale = BestAvailableLocale(availableLocales, noExtensionsLocale);
// d. If availableLocale is not undefined, then append locale to the end of
// subset.
if (availableLocale !== undefined)
arrPush.call(subset, locale);
// e. Increment k by 1.
k++;
}
// 5. Let subsetArray be a new Array object whose elements are the same
// values in the same order as the elements of subset.
let subsetArray = arrSlice.call(subset);
// 6. Return subsetArray.
return subsetArray;
}
/**
* 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. The steps taken are implementation dependent.
*/
export function /*9.2.7 */BestFitSupportedLocales (availableLocales, requestedLocales) {
// ###TODO: implement this function as described by the specification###
return LookupSupportedLocales(availableLocales, requestedLocales);
}
/**
* 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. The following steps are taken:
*/
export function /*9.2.8 */SupportedLocales (availableLocales, requestedLocales, options) {
let matcher, subset;
// 1. If options is not undefined, then
if (options !== undefined) {
// a. Let options be ToObject(options).
options = new Record(toObject(options));
// b. Let matcher be the result of calling the [[Get]] internal method of
// options with argument "localeMatcher".
matcher = options.localeMatcher;
// c. If matcher is not undefined, then
if (matcher !== undefined) {
// i. Let matcher be ToString(matcher).
matcher = String(matcher);
// ii. If matcher is not "lookup" or "best fit", then throw a RangeError
// exception.
if (matcher !== 'lookup' && matcher !== 'best fit')
throw new RangeError('matcher should be "lookup" or "best fit"');
}
}
// 2. If matcher is undefined or "best fit", then
if (matcher === undefined || matcher === 'best fit')
// a. Let subset be the result of calling the BestFitSupportedLocales
// abstract operation (defined in 9.2.7) with arguments
// availableLocales and requestedLocales.
subset = BestFitSupportedLocales(availableLocales, requestedLocales);
// 3. Else
else
// a. Let subset be the result of calling the LookupSupportedLocales
// abstract operation (defined in 9.2.6) with arguments
// availableLocales and requestedLocales.
subset = LookupSupportedLocales(availableLocales, requestedLocales);
// 4. For each named own property name P of subset,
for (let P in subset) {
if (!hop.call(subset, P))
continue;
// a. Let desc be the result of calling the [[GetOwnProperty]] internal
// method of subset with P.
// b. Set desc.[[Writable]] to false.
// c. Set desc.[[Configurable]] to false.
// d. Call the [[DefineOwnProperty]] internal method of subset with P, desc,
// and true as arguments.
defineProperty(subset, P, {
writable: false, configurable: false, value: subset[P],
});
}
// "Freeze" the array so no new elements can be added
defineProperty(subset, 'length', { writable: false });
// 5. Return subset
return subset;
}
/**
* The GetOption abstract operation extracts the value of the property named
* property from the provided options object, converts it to the required type,
* checks whether it is one of a List of allowed values, and fills in a fallback
* value if necessary.
*/
export function /*9.2.9 */GetOption (options, property, type, values, fallback) {
// 1. Let value be the result of calling the [[Get]] internal method of
// options with argument property.
let value = options[property];
// 2. If value is not undefined, then
if (value !== undefined) {
// a. Assert: type is "boolean" or "string".
// b. If type is "boolean", then let value be ToBoolean(value).
// c. If type is "string", then let value be ToString(value).
value = type === 'boolean' ? Boolean(value)
: (type === 'string' ? String(value) : value);
// d. If values is not undefined, then
if (values !== undefined) {
// i. If values does not contain an element equal to value, then throw a
// RangeError exception.
if (arrIndexOf.call(values, value) === -1)
throw new RangeError("'" + value + "' is not an allowed value for `" + property +'`');
}
// e. Return value.
return value;
}
// Else return fallback.
return fallback;
}
/**
* The GetNumberOption abstract operation extracts a property value from the
* provided options object, converts it to a Number value, checks whether it is
* in the allowed range, and fills in a fallback value if necessary.
*/
export function /* 9.2.10 */GetNumberOption (options, property, minimum, maximum, fallback) {
// 1. Let value be the result of calling the [[Get]] internal method of
// options with argument property.
let value = options[property];
// 2. If value is not undefined, then
if (value !== undefined) {
// a. Let value be ToNumber(value).
value = Number(value);
// b. If value is NaN or less than minimum or greater than maximum, throw a
// RangeError exception.
if (isNaN(value) || value < minimum || value > maximum)
throw new RangeError('Value is not a number or outside accepted range');
// c. Return floor(value).
return Math.floor(value);
}
// 3. Else return fallback.
return fallback;
}