UNPKG

intl

Version:

Polyfill the ECMA-402 Intl API (except collation)

603 lines (518 loc) 24.9 kB
// 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; }