| Index: src/extensions/i18n/i18n-utils.js
|
| diff --git a/src/extensions/i18n/i18n-utils.js b/src/extensions/i18n/i18n-utils.js
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..545082ecbba4d6ba9755c9639234a28ba4889cf5
|
| --- /dev/null
|
| +++ b/src/extensions/i18n/i18n-utils.js
|
| @@ -0,0 +1,536 @@
|
| +// Copyright 2013 the V8 project authors. All rights reserved.
|
| +// Redistribution and use in source and binary forms, with or without
|
| +// modification, are permitted provided that the following conditions are
|
| +// met:
|
| +//
|
| +// * Redistributions of source code must retain the above copyright
|
| +// notice, this list of conditions and the following disclaimer.
|
| +// * Redistributions in binary form must reproduce the above
|
| +// copyright notice, this list of conditions and the following
|
| +// disclaimer in the documentation and/or other materials provided
|
| +// with the distribution.
|
| +// * Neither the name of Google Inc. nor the names of its
|
| +// contributors may be used to endorse or promote products derived
|
| +// from this software without specific prior written permission.
|
| +//
|
| +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
| +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
| +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
| +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
| +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
| +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
| +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
| +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
| +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
| +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
| +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
| +// limitations under the License.
|
| +
|
| +// ECMAScript 402 API implementation is broken into separate files for
|
| +// each service. The build system combines them together into one
|
| +// Intl namespace.
|
| +
|
| +/**
|
| + * Adds bound method to the prototype of the given object.
|
| + */
|
| +function addBoundMethod(obj, methodName, implementation, length) {
|
| + function getter() {
|
| + if (!this || typeof this !== 'object' ||
|
| + this.__initializedIntlObject === undefined) {
|
| + throw new TypeError('Method ' + methodName + ' called on a ' +
|
| + 'non-object or on a wrong type of object.');
|
| + }
|
| + var internalName = '__bound' + methodName + '__';
|
| + if (this[internalName] === undefined) {
|
| + var that = this;
|
| + var boundMethod;
|
| + if (length === undefined || length === 2) {
|
| + boundMethod = function(x, y) {
|
| + if (%_IsConstructCall()) {
|
| + throw new TypeError(ORDINARY_FUNCTION_CALLED_AS_CONSTRUCTOR);
|
| + }
|
| + return implementation(that, x, y);
|
| + }
|
| + } else if (length === 1) {
|
| + boundMethod = function(x) {
|
| + if (%_IsConstructCall()) {
|
| + throw new TypeError(ORDINARY_FUNCTION_CALLED_AS_CONSTRUCTOR);
|
| + }
|
| + return implementation(that, x);
|
| + }
|
| + } else {
|
| + boundMethod = function() {
|
| + if (%_IsConstructCall()) {
|
| + throw new TypeError(ORDINARY_FUNCTION_CALLED_AS_CONSTRUCTOR);
|
| + }
|
| + // DateTimeFormat.format needs to be 0 arg method, but can stil
|
| + // receive optional dateValue param. If one was provided, pass it
|
| + // along.
|
| + if (arguments.length > 0) {
|
| + return implementation(that, arguments[0]);
|
| + } else {
|
| + return implementation(that);
|
| + }
|
| + }
|
| + }
|
| + %FunctionSetName(boundMethod, internalName);
|
| + %FunctionRemovePrototype(boundMethod);
|
| + %SetNativeFlag(boundMethod);
|
| + this[internalName] = boundMethod;
|
| + }
|
| + return this[internalName];
|
| + }
|
| +
|
| + %FunctionSetName(getter, methodName);
|
| + %FunctionRemovePrototype(getter);
|
| + %SetNativeFlag(getter);
|
| +
|
| + Object.defineProperty(obj.prototype, methodName, {
|
| + get: getter,
|
| + enumerable: false,
|
| + configurable: true
|
| + });
|
| +}
|
| +
|
| +
|
| +/**
|
| + * Returns an intersection of locales and service supported locales.
|
| + * Parameter locales is treated as a priority list.
|
| + */
|
| +function supportedLocalesOf(service, locales, options) {
|
| + if (service.match(SERVICE_RE) === null) {
|
| + throw new Error('Internal error, wrong service type: ' + service);
|
| + }
|
| +
|
| + // Provide defaults if matcher was not specified.
|
| + if (options === undefined) {
|
| + options = {};
|
| + } else {
|
| + options = toObject(options);
|
| + }
|
| +
|
| + var matcher = options.localeMatcher;
|
| + if (matcher !== undefined) {
|
| + matcher = String(matcher);
|
| + if (matcher !== 'lookup' && matcher !== 'best fit') {
|
| + throw new RangeError('Illegal value for localeMatcher:' + matcher);
|
| + }
|
| + } else {
|
| + matcher = 'best fit';
|
| + }
|
| +
|
| + var requestedLocales = initializeLocaleList(locales);
|
| +
|
| + // Cache these, they don't ever change per service.
|
| + if (AVAILABLE_LOCALES[service] === undefined) {
|
| + AVAILABLE_LOCALES[service] = getAvailableLocalesOf(service);
|
| + }
|
| +
|
| + // Use either best fit or lookup algorithm to match locales.
|
| + if (matcher === 'best fit') {
|
| + return initializeLocaleList(bestFitSupportedLocalesOf(
|
| + requestedLocales, AVAILABLE_LOCALES[service]));
|
| + }
|
| +
|
| + return initializeLocaleList(lookupSupportedLocalesOf(
|
| + requestedLocales, AVAILABLE_LOCALES[service]));
|
| +}
|
| +
|
| +
|
| +/**
|
| + * Returns the subset of the provided BCP 47 language priority list for which
|
| + * this service has a matching locale when using the BCP 47 Lookup algorithm.
|
| + * Locales appear in the same order in the returned list as in the input list.
|
| + */
|
| +function lookupSupportedLocalesOf(requestedLocales, availableLocales) {
|
| + var matchedLocales = [];
|
| + for (var i = 0; i < requestedLocales.length; ++i) {
|
| + // Remove -u- extension.
|
| + var locale = requestedLocales[i].replace(UNICODE_EXTENSION_RE, '');
|
| + do {
|
| + if (availableLocales[locale] !== undefined) {
|
| + // Push requested locale not the resolved one.
|
| + matchedLocales.push(requestedLocales[i]);
|
| + break;
|
| + }
|
| + // Truncate locale if possible, if not break.
|
| + var pos = locale.lastIndexOf('-');
|
| + if (pos === -1) {
|
| + break;
|
| + }
|
| + locale = locale.substring(0, pos);
|
| + } while (true);
|
| + }
|
| +
|
| + return matchedLocales;
|
| +}
|
| +
|
| +
|
| +/**
|
| + * Returns the subset of the provided BCP 47 language priority list for which
|
| + * this service has a matching locale when using the implementation
|
| + * dependent algorithm.
|
| + * Locales appear in the same order in the returned list as in the input list.
|
| + */
|
| +function bestFitSupportedLocalesOf(requestedLocales, availableLocales) {
|
| + // TODO(cira): implement better best fit algorithm.
|
| + return lookupSupportedLocalesOf(requestedLocales, availableLocales);
|
| +}
|
| +
|
| +
|
| +/**
|
| + * Returns a getOption function that extracts property value for given
|
| + * options object. If property is missing it returns defaultValue. If value
|
| + * is out of range for that property it throws RangeError.
|
| + */
|
| +function getGetOption(options, caller) {
|
| + if (options === undefined) {
|
| + throw new Error('Internal ' + caller + ' error. ' +
|
| + 'Default options are missing.');
|
| + }
|
| +
|
| + var getOption = function getOption(property, type, values, defaultValue) {
|
| + if (options[property] !== undefined) {
|
| + var value = options[property];
|
| + switch (type) {
|
| + case 'boolean':
|
| + value = Boolean(value);
|
| + break;
|
| + case 'string':
|
| + value = String(value);
|
| + break;
|
| + case 'number':
|
| + value = Number(value);
|
| + break;
|
| + default:
|
| + throw new Error('Internal error. Wrong value type.');
|
| + }
|
| + if (values !== undefined && values.indexOf(value) === -1) {
|
| + throw new RangeError('Value ' + value + ' out of range for ' + caller +
|
| + ' options property ' + property);
|
| + }
|
| +
|
| + return value;
|
| + }
|
| +
|
| + return defaultValue;
|
| + }
|
| +
|
| + return getOption;
|
| +}
|
| +
|
| +
|
| +/**
|
| + * Compares a BCP 47 language priority list requestedLocales against the locales
|
| + * in availableLocales and determines the best available language to meet the
|
| + * request. 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. Independent of the locale matching algorithm, options
|
| + * specified through Unicode locale extension sequences are negotiated
|
| + * separately, taking the caller's relevant extension keys and locale data as
|
| + * well as client-provided options into consideration. Returns an object with
|
| + * a locale property whose value is the language tag of the selected locale,
|
| + * and properties for each key in relevantExtensionKeys providing the selected
|
| + * value for that key.
|
| + */
|
| +function resolveLocale(service, requestedLocales, options) {
|
| + requestedLocales = initializeLocaleList(requestedLocales);
|
| +
|
| + var getOption = getGetOption(options, service);
|
| + var matcher = getOption('localeMatcher', 'string',
|
| + ['lookup', 'best fit'], 'best fit');
|
| + var resolved;
|
| + if (matcher === 'lookup') {
|
| + resolved = lookupMatcher(service, requestedLocales);
|
| + } else {
|
| + resolved = bestFitMatcher(service, requestedLocales);
|
| + }
|
| +
|
| + return resolved;
|
| +}
|
| +
|
| +
|
| +/**
|
| + * Returns best matched supported locale and extension info using basic
|
| + * lookup algorithm.
|
| + */
|
| +function lookupMatcher(service, requestedLocales) {
|
| + if (service.match(SERVICE_RE) === null) {
|
| + throw new Error('Internal error, wrong service type: ' + service);
|
| + }
|
| +
|
| + // Cache these, they don't ever change per service.
|
| + if (AVAILABLE_LOCALES[service] === undefined) {
|
| + AVAILABLE_LOCALES[service] = getAvailableLocalesOf(service);
|
| + }
|
| +
|
| + for (var i = 0; i < requestedLocales.length; ++i) {
|
| + // Remove all extensions.
|
| + var locale = requestedLocales[i].replace(ANY_EXTENSION_RE, '');
|
| + do {
|
| + if (AVAILABLE_LOCALES[service][locale] !== undefined) {
|
| + // Return the resolved locale and extension.
|
| + var extensionMatch = requestedLocales[i].match(UNICODE_EXTENSION_RE);
|
| + var extension = (extensionMatch === null) ? '' : extensionMatch[0];
|
| + return {'locale': locale, 'extension': extension, 'position': i};
|
| + }
|
| + // Truncate locale if possible.
|
| + var pos = locale.lastIndexOf('-');
|
| + if (pos === -1) {
|
| + break;
|
| + }
|
| + locale = locale.substring(0, pos);
|
| + } while (true);
|
| + }
|
| +
|
| + // Didn't find a match, return default.
|
| + if (DEFAULT_ICU_LOCALE === undefined) {
|
| + DEFAULT_ICU_LOCALE = %GetDefaultICULocale();
|
| + }
|
| +
|
| + return {'locale': DEFAULT_ICU_LOCALE, 'extension': '', 'position': -1};
|
| +}
|
| +
|
| +
|
| +/**
|
| + * Returns best matched supported locale and extension info using
|
| + * implementation dependend algorithm.
|
| + */
|
| +function bestFitMatcher(service, requestedLocales) {
|
| + // TODO(cira): implement better best fit algorithm.
|
| + return lookupMatcher(service, requestedLocales);
|
| +}
|
| +
|
| +
|
| +/**
|
| + * Parses Unicode extension into key - value map.
|
| + * Returns empty object if the extension string is invalid.
|
| + * We are not concerned with the validity of the values at this point.
|
| + */
|
| +function parseExtension(extension) {
|
| + var extensionSplit = extension.split('-');
|
| +
|
| + // Assume ['', 'u', ...] input, but don't throw.
|
| + if (extensionSplit.length <= 2 ||
|
| + (extensionSplit[0] !== '' && extensionSplit[1] !== 'u')) {
|
| + return {};
|
| + }
|
| +
|
| + // Key is {2}alphanum, value is {3,8}alphanum.
|
| + // Some keys may not have explicit values (booleans).
|
| + var extensionMap = {};
|
| + var previousKey = undefined;
|
| + for (var i = 2; i < extensionSplit.length; ++i) {
|
| + var length = extensionSplit[i].length;
|
| + var element = extensionSplit[i];
|
| + if (length === 2) {
|
| + extensionMap[element] = undefined;
|
| + previousKey = element;
|
| + } else if (length >= 3 && length <=8 && previousKey !== undefined) {
|
| + extensionMap[previousKey] = element;
|
| + previousKey = undefined;
|
| + } else {
|
| + // There is a value that's too long, or that doesn't have a key.
|
| + return {};
|
| + }
|
| + }
|
| +
|
| + return extensionMap;
|
| +}
|
| +
|
| +
|
| +/**
|
| + * Converts parameter to an Object if possible.
|
| + */
|
| +function toObject(value) {
|
| + if (value === undefined || value === null) {
|
| + throw new TypeError('Value cannot be converted to an Object.');
|
| + }
|
| +
|
| + return Object(value);
|
| +}
|
| +
|
| +
|
| +/**
|
| + * Populates internalOptions object with boolean key-value pairs
|
| + * from extensionMap and options.
|
| + * Returns filtered extension (number and date format constructors use
|
| + * Unicode extensions for passing parameters to ICU).
|
| + * It's used for extension-option pairs only, e.g. kn-normalization, but not
|
| + * for 'sensitivity' since it doesn't have extension equivalent.
|
| + * Extensions like nu and ca don't have options equivalent, so we place
|
| + * undefined in the map.property to denote that.
|
| + */
|
| +function setOptions(inOptions, extensionMap, keyValues, getOption, outOptions) {
|
| + var extension = '';
|
| +
|
| + var updateExtension = function updateExtension(key, value) {
|
| + return '-' + key + '-' + String(value);
|
| + }
|
| +
|
| + var updateProperty = function updateProperty(property, type, value) {
|
| + if (type === 'boolean' && (typeof value === 'string')) {
|
| + value = (value === 'true') ? true : false;
|
| + }
|
| +
|
| + if (property !== undefined) {
|
| + defineWEProperty(outOptions, property, value);
|
| + }
|
| + }
|
| +
|
| + for (var key in keyValues) {
|
| + if (keyValues.hasOwnProperty(key)) {
|
| + var value = undefined;
|
| + var map = keyValues[key];
|
| + if (map.property !== undefined) {
|
| + // This may return true if user specifies numeric: 'false', since
|
| + // Boolean('nonempty') === true.
|
| + value = getOption(map.property, map.type, map.values);
|
| + }
|
| + if (value !== undefined) {
|
| + updateProperty(map.property, map.type, value);
|
| + extension += updateExtension(key, value);
|
| + continue;
|
| + }
|
| + // User options didn't have it, check Unicode extension.
|
| + // Here we want to convert strings 'true', 'false' into proper Boolean
|
| + // values (not a user error).
|
| + if (extensionMap.hasOwnProperty(key)) {
|
| + value = extensionMap[key];
|
| + if (value !== undefined) {
|
| + updateProperty(map.property, map.type, value);
|
| + extension += updateExtension(key, value);
|
| + } else if (map.type === 'boolean') {
|
| + // Boolean keys are allowed not to have values in Unicode extension.
|
| + // Those default to true.
|
| + updateProperty(map.property, map.type, true);
|
| + extension += updateExtension(key, true);
|
| + }
|
| + }
|
| + }
|
| + }
|
| +
|
| + return extension === ''? '' : '-u' + extension;
|
| +}
|
| +
|
| +
|
| +/**
|
| + * Converts all OwnProperties into
|
| + * configurable: false, writable: false, enumerable: true.
|
| + */
|
| +function freezeArray(array) {
|
| + array.forEach(function(element, index) {
|
| + Object.defineProperty(array, index, {value: element,
|
| + configurable: false,
|
| + writable: false,
|
| + enumerable: true});
|
| + });
|
| +
|
| + Object.defineProperty(array, 'length', {value: array.length,
|
| + writable: false});
|
| +
|
| + return array;
|
| +}
|
| +
|
| +
|
| +/**
|
| + * It's sometimes desireable to leave user requested locale instead of ICU
|
| + * supported one (zh-TW is equivalent to zh-Hant-TW, so we should keep shorter
|
| + * one, if that was what user requested).
|
| + * This function returns user specified tag if its maximized form matches ICU
|
| + * resolved locale. If not we return ICU result.
|
| + */
|
| +function getOptimalLanguageTag(original, resolved) {
|
| + // Returns Array<Object>, where each object has maximized and base properties.
|
| + // Maximized: zh -> zh-Hans-CN
|
| + // Base: zh-CN-u-ca-gregory -> zh-CN
|
| + // Take care of grandfathered or simple cases.
|
| + if (original === resolved) {
|
| + return original;
|
| + }
|
| +
|
| + var locales = %GetLanguageTagVariants([original, resolved]);
|
| + if (locales[0].maximized !== locales[1].maximized) {
|
| + return resolved;
|
| + }
|
| +
|
| + // Preserve extensions of resolved locale, but swap base tags with original.
|
| + var resolvedBase = new RegExp('^' + locales[1].base);
|
| + return resolved.replace(resolvedBase, locales[0].base);
|
| +}
|
| +
|
| +
|
| +/**
|
| + * Returns an Object that contains all of supported locales for a given
|
| + * service.
|
| + * In addition to the supported locales we add xx-ZZ locale for each xx-Yyyy-ZZ
|
| + * that is supported. This is required by the spec.
|
| + */
|
| +function getAvailableLocalesOf(service) {
|
| + var available = %AvailableLocalesOf(service);
|
| +
|
| + for (var i in available) {
|
| + if (available.hasOwnProperty(i)) {
|
| + var parts = i.match(/^([a-z]{2,3})-([A-Z][a-z]{3})-([A-Z]{2})$/);
|
| + if (parts !== null) {
|
| + // Build xx-ZZ. We don't care about the actual value,
|
| + // as long it's not undefined.
|
| + available[parts[1] + '-' + parts[3]] = null;
|
| + }
|
| + }
|
| + }
|
| +
|
| + return available;
|
| +}
|
| +
|
| +
|
| +/**
|
| + * Defines a property and sets writable and enumerable to true.
|
| + * Configurable is false by default.
|
| + */
|
| +function defineWEProperty(object, property, value) {
|
| + Object.defineProperty(object, property,
|
| + {value: value, writable: true, enumerable: true});
|
| +}
|
| +
|
| +
|
| +/**
|
| + * Adds property to an object if the value is not undefined.
|
| + * Sets configurable descriptor to false.
|
| + */
|
| +function addWEPropertyIfDefined(object, property, value) {
|
| + if (value !== undefined) {
|
| + defineWEProperty(object, property, value);
|
| + }
|
| +}
|
| +
|
| +
|
| +/**
|
| + * Defines a property and sets writable, enumerable and configurable to true.
|
| + */
|
| +function defineWECProperty(object, property, value) {
|
| + Object.defineProperty(object, property,
|
| + {value: value,
|
| + writable: true,
|
| + enumerable: true,
|
| + configurable: true});
|
| +}
|
| +
|
| +
|
| +/**
|
| + * Adds property to an object if the value is not undefined.
|
| + * Sets all descriptors to true.
|
| + */
|
| +function addWECPropertyIfDefined(object, property, value) {
|
| + if (value !== undefined) {
|
| + defineWECProperty(object, property, value);
|
| + }
|
| +}
|
| +
|
| +
|
| +/**
|
| + * Returns titlecased word, aMeRricA -> America.
|
| + */
|
| +function toTitleCaseWord(word) {
|
| + return word.substr(0, 1).toUpperCase() + word.substr(1).toLowerCase();
|
| +}
|
|
|