/** * Commonly used routines throughout ECMA-402 package. Also referred to in the standard as "Abstract Operations" */ define(["./List", "./Record", "requirejs-text/text!../cldr/config/availableLocales.json", "requirejs-text/text!../cldr/supplemental/aliases.json", "requirejs-text/text!../cldr/supplemental/localeAliases.json", "requirejs-text/text!../cldr/supplemental/parentLocales.json", "requirejs-text/text!../cldr/supplemental/likelySubtags.json", "requirejs-text/text!../cldr/supplemental/calendarPreferenceData.json", ], function (List, Record, availableLocalesJson, aliasesJson, localeAliasesJson, parentLocalesJson, likelySubtagsJson, calendarPreferenceDataJson) { var aliases = JSON.parse(aliasesJson).supplemental.metadata.alias; var localeAliases = JSON.parse(localeAliasesJson).supplemental.metadata.alias; var parentLocales = JSON.parse(parentLocalesJson).supplemental.parentLocales.parentLocale; var likelySubtags = JSON.parse(likelySubtagsJson).supplemental.likelySubtags; var calendarPreferenceData = JSON.parse(calendarPreferenceDataJson).supplemental.calendarPreferenceData; var common = { unicodeLocaleExtensions : /-u(-[a-z0-9]{2,8})+/g, /** * Utility function to convert identifier strings to upper case as defined in ECMA-402 Section 6.1 * * @param {String} identifier The string to be converted * @returns {String} The converted string * @private */ _toUpperCaseIdentifier : function (identifier) { var match = /[a-z]/g; return identifier.replace(match, function (m) { return m.toUpperCase(); }); // String }, /** * Utility function to convert identifier strings to lower case as defined in ECMA-402 Section 6.1 * * @param {String} identifier The string to be converted * @returns {String} The converted string * @private */ _toLowerCaseIdentifier : function (identifier) { var match = /[A-Z]/g; return identifier.replace(match, function (m) { return m.toLowerCase(); }); // String }, /** * Utility function to retrieve the appropriate region code given a locale identifier. * If just a language tag is given, then the likely subtags data from CLDR is checked * to find the most likely region code. * * @param {String} locale The locale identifier * @returns {String} The 2 letter region code * @private */ _getRegion : function (locale) { var region = "001"; var regionPos = locale.search(/(?:-)([A-Z]{2})(?=(-|$))/); if (regionPos >= 0) { region = locale.substr(regionPos + 1, 2); } else { var likelySubtag = likelySubtags[locale]; if (likelySubtag) { region = likelySubtag.substr(-2); } } return region; }, /** * Utility function to determine the supported calendars for a given region. * Calendar preference data from CLDR is used to determine which locales are used * in a given region. * * @param {String} region The 2 letter region code * @returns {String[]} An array containing the supported calendars for this region, in order of preference. * @private */ _getSupportedCalendars : function (region) { var supportedCalendars = [ "gregory", "buddhist", "hebrew", "japanese", "roc", "islamic", "islamic-civil", "islamic-tbla", "islamic-umalqura"]; var calendarPreferences = []; if (calendarPreferenceData[region]) { var prefs = calendarPreferenceData[region].toString().split(" "); prefs.forEach(function (pref) { var thisPref = pref.replace("gregorian", "gregory"); if (supportedCalendars.indexOf(thisPref) !== -1) { calendarPreferences.push(thisPref); } }); } /* Gregorian should always be supported */ if (calendarPreferences.indexOf("gregory") === -1) { calendarPreferences.push("gregory"); } return calendarPreferences; }, /** * IsStructurallyValidLanguageTag abstract operation as defined in ECMA-402 Section 6.2.2 * * @param {String} locale The language tag to check * @returns {Boolean} Returns true if the string is a structurally valid language tag. * @private */ isStructurallyValidLanguageTag : function (locale) { if (typeof locale !== "string") { return false; // Boolean } var identifier = this._toLowerCaseIdentifier(locale); var langtag = new RegExp( "^([a-z]{2,3}(-[a-z]{3}){0,3}|[a-z]{4,8})" + // language "(-[a-z]{4})?" + // script "(-([a-z]{2}|\\d{3}))?" + // territory "(-([a-z0-9]{5,8}|\\d[a-z0-9]{3}))*" + // variant "(-[a-wyz0-9](-[a-z0-9]{2,8})+)*(-x(-[a-z0-9]{1,8})+)?$"); // extension var privateuse = /x(-[a-z0-9]{1,8})+/; var grandfathered = new RegExp( "en-gb-oed|(i-(ami|bnn|default|enochian|hak|klingon|lux|mingo|navajo|pwn|tao|tay|tsu))|" + "sgn-((be-(fr|nl))|(ch-de))"); if (privateuse.test(identifier) || grandfathered.test(identifier)) { return true; // Boolean } /** * Utility function to determine whether the given element is a unique variant * within the context of a BCP 47 compliant language tag. * * @param {String} element The element tag * @returns {Boolean} Returns true if the given element is a unique variant. * @private */ function _isUniqueVariant(element) { var firstSingletonPosition = identifier.search(/-[a-z0-9]-/); if (firstSingletonPosition > 0) { return identifier.indexOf(element) > firstSingletonPosition || identifier.indexOf(element) === identifier.lastIndexOf(element, firstSingletonPosition); // Boolean } return identifier.indexOf(element) === identifier.lastIndexOf(element); // Boolean } /** * Utility function to determine whether the given element is a unique singleton * within the context of a BCP 47 compliant language tag. * * @param {String} element The element tag * @returns {Boolean} Returns true if the given element is a unique singleton. * @private */ function _isUniqueSingleton(element) { var firstXPosition = identifier.search(/-x-/); if (firstXPosition > 0) { return identifier.indexOf(element) === identifier.lastIndexOf(element, firstXPosition); } return identifier.indexOf(element) === identifier.lastIndexOf(element); // Boolean } if (langtag.test(identifier)) { // represents a well-formed BCP 47 language tag var varianttag = /-[a-z0-9]{5,8}|\d[a-z0-9]{3}/g; var variants = varianttag.exec(identifier); var singletontag = /-[a-wyz0-9]-/g; var singletons = singletontag.exec(identifier); var variantsOK = !variants || variants.every(_isUniqueVariant); // has no duplicate variant tags var singletonsOK = !singletons || singletons.every(_isUniqueSingleton); // has no duplicate // singleton tags return variantsOK && singletonsOK; } return false; // Boolean }, /** * CanonicalizeLanguageTag abstract operation as defined in ECMA-402 Section 6.2.3 * * @param {String} locale The structurally valid language tag to canonicalize * @returns {String} The canonical and case regularized form of the language tag. * @private */ CanonicalizeLanguageTag : function (locale) { var result = locale.toLowerCase(); var firstSingletonPosition = result.search(/(^|-)[a-z0-9]-/); var languageTag = /^([a-z]{2,3}(-[a-z]{3}){0,3}|[a-z]{4,8})(?=(-|$))/; var scriptTag = /(?:-)([a-z]{4})(?=(-|$))/; var regionTag = /(?:-)([a-z]{2})(?=(-|$))/g; var variantTag = /(?:-)([a-z0-9]{5,8}|\d[a-z0-9]{3})/; var extlangTag = /^([a-z]{2,3}(-[a-z]{3}))(?=(-|$))/; /* Canonicalize the Language Tag */ result = result.replace(languageTag, function (m) { var lookupAlias = aliases.languageAlias[m]; if (lookupAlias && lookupAlias._reason !== "macrolanguage") { m = lookupAlias._replacement ? lookupAlias._replacement : m; } return m; }); // String // Canonicalize the Script Tag result = result.replace(scriptTag, function (m) { if (firstSingletonPosition === -1 || result.indexOf(m) < firstSingletonPosition) { m = m.substring(0, 2).toUpperCase() + m.substring(2); var script = m.substring(1); var lookupAlias = aliases.scriptAlias[script]; if (lookupAlias) { m = lookupAlias._replacement ? "-" + lookupAlias._replacement : m; } } return m; }); // String // Canonicalize the Region Tag result = result.replace(regionTag, function (m) { if (firstSingletonPosition === -1 || result.indexOf(m) < firstSingletonPosition) { m = m.toUpperCase(); var region = m.substring(1); var lookupAlias = aliases.territoryAlias[region]; if (lookupAlias) { var repl = lookupAlias._replacement; if (repl.indexOf(" ") >= 0) { repl = repl.substring(0, repl.indexOf(" ")); } m = repl ? "-" + repl : m; } } return m; }); // String // Canonicalize the Variant Tag result = result.replace(variantTag, function (m) { // Variant tags are upper case in CLDR's data. var variant = common._toUpperCaseIdentifier(m.substring(1)); var lookupAlias = aliases.variantAlias[variant]; if (lookupAlias) { var repl = lookupAlias._replacement; m = repl ? "-" + common._toLowerCaseIdentifier(repl) : m; } return m; }); // String // Canonicalize any whole tag combinations or grandfathered tags result = result.replace(result, function (m) { var lookupAlias = aliases.languageAlias[m]; if (lookupAlias && lookupAlias._reason !== "macrolanguage") { m = lookupAlias._replacement ? lookupAlias._replacement : m; } return m; }); // String // Remove the prefix if an extlang tag exists if (extlangTag.test(result)) { result = result.replace(/^[a-z]{2,3}-/, ""); } return result; // String }, /** * DefaultLocale abstract operation as defined in ECMA-402 Section 6.2.4 * * @returns {String} 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. * @private */ DefaultLocale : function () { var result; var global = (function () {return this; })(); var navigator = global.navigator; if (navigator && this.isStructurallyValidLanguageTag(navigator.language)) { result = this.BestFitAvailableLocale(this.availableLocalesList, this .CanonicalizeLanguageTag(navigator.language)); } if (!result && navigator && this.isStructurallyValidLanguageTag(navigator.userLanguage)) { result = this.BestFitAvailableLocale(this.availableLocalesList, this .CanonicalizeLanguageTag(navigator.userLanguage)); } if (!result) { result = "root"; } return result; }, /** * IsWellFormedCurrencyCode abstract operation as defined in ECMA-402 Section 6.3.1 * * @param {String} currency The currency code to check * @returns {Boolean} Returns true if the string is a well formed currency code. * @private */ IsWellFormedCurrencyCode : function (currency) { var wellFormed = /^[A-Za-z]{3}$/; return wellFormed.test(currency.toString()); // Boolean }, /** * CanonicalizeLocaleList abstract operation as defined in ECMA-402 Section 9.2.1 * * @param {*} locales The list of locales to canonicalize * @returns {Object} The canonicalized list of locales, as a "List" object. * @private */ CanonicalizeLocaleList : function (locales) { if (locales === undefined) { return new List(); } if (locales === null) { throw new TypeError("Locale list can not be null"); } var seen = new List(); if (typeof locales === "string") { locales = new Array(locales); } var O = Object(locales); var lenValue = O.length; var len = lenValue >>> 0; // Convert to unsigned 32-bit integer for (var k = 0; k < len ; k++) { var Pk = k.toString(); var kPresent = Pk in O; if (kPresent) { var kValue = O[Pk]; if (typeof kValue !== "string" && typeof kValue !== "object") { throw new TypeError(kValue + " must be a string or an object."); } var tag = kValue.toString(); if (!this.isStructurallyValidLanguageTag(tag)) { throw new RangeError(tag + " is not a structurally valid language tag."); } tag = this.CanonicalizeLanguageTag(tag); if (seen.indexOf(tag) < 0) { seen.push(tag); } } } return seen; }, /** * BestAvailableLocale abstract operation as defined in ECMA-402 Section 9.2.2 * * @param {List} availableLocales The canonicalized list of available locales * @param {String} locale The locale identifier to check * @returns {String} The best available locale, using the fallback mechanism of RFC 4647, section 3.4. * @private */ BestAvailableLocale : function (availableLocales, locale) { var candidate = locale; while (true) { if (availableLocales.indexOf(candidate) >= 0) { return candidate; } var pos = candidate.lastIndexOf("-"); if (pos < 0) { return undefined; } if (pos >= 2 && candidate.charAt(pos - 2) === "-") { pos -= 2; } candidate = candidate.substring(0, pos); } }, /** * LookupMatcher abstract operation as defined in ECMA-402 Section 9.2.3 * * @param {List} availableLocales The canonicalized list of available locales * @param {List} requestedLocales The canonicalized list of requested locales * @returns {String} The best available locale identifier to meet the request * @private */ LookupMatcher : function (availableLocales, requestedLocales) { var i = 0; var len = requestedLocales.length; var availableLocale = null; var locale = null; var noExtensionsLocale = null; while (i < len && availableLocale === null) { locale = requestedLocales[i]; noExtensionsLocale = locale.replace(this.unicodeLocaleExtensions, ""); availableLocale = this.BestAvailableLocale(availableLocales, noExtensionsLocale); i++; } var result = new Record(); if (availableLocale) { result.set("locale", availableLocale); if (locale !== noExtensionsLocale) { result.set("extension", locale.match(this.unicodeLocaleExtensions)[0]); result.set("extensionIndex", locale.search(this.unicodeLocaleExtensions)); } } else { result.set("locale", this.DefaultLocale()); } return result; }, /** * BestFitAvailableLocale abstract operation. * * Algorithm is similar to BestAvailableLocale, as in Section 9.2.2 * except that the following additional operations are performed: * 1). CLDR macrolanguage replacements are done ( i.e. "cmn" becomes "zh" ) * 2). Known locale aliases, such as zh-TW = zh-Hant-TW, are resolved, * 3). Explicit parent locales from CLDR's supplemental data are also considered. * * @param {List} availableLocales The canonicalized list of available locales * @param {String} locale The locale identifier to check * @returns {String} The best fit available locale, using CLDR's locale fallback mechanism. * @private */ BestFitAvailableLocale : function (availableLocales, locale) { var candidate = locale; while (true) { var langtag = candidate.substring(0, candidate.indexOf("-")); var lookupAlias = aliases.languageAlias[langtag]; if (lookupAlias && lookupAlias._reason === "macrolanguage") { candidate = candidate.replace(langtag, lookupAlias._replacement); } lookupAlias = localeAliases.localeAlias[candidate]; if (lookupAlias) { candidate = lookupAlias._replacement; } if (availableLocales.indexOf(candidate) >= 0) { return candidate; } var parentLocale = parentLocales[candidate]; if (parentLocale) { candidate = parentLocale; } else { var pos = candidate.lastIndexOf("-"); if (pos < 0) { return undefined; } if (pos >= 2 && candidate.charAt(pos - 2) === "-") { pos -= 2; } candidate = candidate.substring(0, pos); } } }, /** * BestFitMatcher abstract operation as defined in ECMA-402 Section 9.2.4 * * @param {List} availableLocales The canonicalized list of available locales * @param {List} requestedLocales The canonicalized list of requested locales * @returns {String} The best available locale identifier to meet the request * @private */ BestFitMatcher : function (availableLocales, requestedLocales) { var i = 0; var len = requestedLocales.length; var availableLocale = null; var locale = null; var noExtensionsLocale = null; while (i < len && availableLocale === null) { locale = requestedLocales[i]; noExtensionsLocale = locale.replace(this.unicodeLocaleExtensions, ""); availableLocale = this.BestFitAvailableLocale(availableLocales, noExtensionsLocale); i++; } var result = new Record(); if (availableLocale) { result.set("locale", availableLocale); if (locale !== noExtensionsLocale) { result.set("extension", locale.match(this.unicodeLocaleExtensions)[0]); result.set("extensionIndex", locale.search(this.unicodeLocaleExtensions)); } } else { result.set("locale", this.DefaultLocale()); } return result; }, /** * ResolveLocale abstract operation as defined in ECMA-402 Section 9.2.5 * * Compares a BCP 47 language priority list requestedLocales against the locales in availableLocales * and determines the best available language to meet the request. * * @param {List} availableLocales The canonicalized list of available locales * @param {List} requestedLocales The canonicalized list of requested locales * @param {Record} options Locale matching options (for example, "lookup" vs. "best fit" algorithm) * @param {String[]} relevantExtensionKeys Array of relevant -u extension keys for the match * @param {Object} localeData Hash table containing the preloaded locale data. * @returns {Record} The locale information regarding best available locale that meets the request * @private */ /* jshint maxcomplexity: 14 */ ResolveLocale : function (availableLocales, requestedLocales, options, relevantExtensionKeys, localeData) { var matcher = options.localeMatcher; var r = matcher === "lookup" ? this.LookupMatcher(availableLocales, requestedLocales) : this .BestFitMatcher(availableLocales, requestedLocales); var foundLocale = r.locale; var extension = ""; var extensionSubtags = []; var extensionSubtagsLength = 0; var extensionIndex = 0; if (r.extension !== undefined) { extension = r.extension; extensionIndex = r.extensionIndex; extensionSubtags = extension.split("-"); extensionSubtagsLength = extensionSubtags.length; } var result = new Record(); result.set("dataLocale", foundLocale); var supportedExtension = "-u"; var i = 0; var len = relevantExtensionKeys.length; while (i < len) { var key = relevantExtensionKeys[String(i)]; var foundLocaleData = localeData[foundLocale]; var keyLocaleData = foundLocaleData[key]; var value = keyLocaleData["0"]; var supportedExtensionAddition = ""; if (typeof extensionSubtags !== "undefined") { var keyPos = extensionSubtags.indexOf(key); var valuePos; if (keyPos !== -1) { if (keyPos + 1 < extensionSubtagsLength && extensionSubtags[String(keyPos + 1)].length > 2) { var requestedValue = extensionSubtags[String(keyPos + 1)]; // fix for islamic-civil, islamic-umalqura & islamic-tbla calendars if (requestedValue === "islamic" && extensionSubtags[String(keyPos + 2)]) { requestedValue += "-" + extensionSubtags[String(keyPos + 2)]; } valuePos = keyLocaleData.indexOf(requestedValue); if (valuePos !== -1) { value = requestedValue; supportedExtensionAddition = "-" + key + "-" + value; } } else { valuePos = keyLocaleData.indexOf("true"); if (valuePos !== -1) { value = "true"; } } } } var optionsValue = options[key]; if (optionsValue !== undefined) { if (keyLocaleData.indexOf(optionsValue) !== -1) { if (optionsValue !== value) { value = optionsValue; supportedExtensionAddition = ""; } } } result.set(key, value); supportedExtension += supportedExtensionAddition; i++; } if (supportedExtension.length > 2) { var preExtension = foundLocale.substring(0, extensionIndex); var postExtension = foundLocale.substring(extensionIndex); foundLocale = preExtension + supportedExtension + postExtension; } result.set("locale", foundLocale); return result; }, /* jshint maxcomplexity: 10 */ /** * LookupSupportedLocales abstract operation as defined in ECMA-402 Section 9.2.6 * * 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. * * @param {List} availableLocales The canonicalized list of available locales * @param {List} requestedLocales The canonicalized list of requested locales * @returns {String[]} An array containing a list of matching locales * @private */ LookupSupportedLocales : function (availableLocales, requestedLocales) { var len = requestedLocales.length; var subset = new List(); var k = 0; while (k < len) { var locale = requestedLocales[k]; var noExtensionsLocale = locale.replace(this.unicodeLocaleExtensions, ""); var availableLocale = this.BestAvailableLocale(availableLocales, noExtensionsLocale); if (availableLocale !== undefined) { subset.push(locale); } k++; } var subsetArray = subset.toArray(); return subsetArray; }, /** * BestFitSupportedLocales abstract operation as defined in ECMA-402 Section 9.2.7 * * 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. * * @param {List} availableLocales The canonicalized list of available locales * @param {List} requestedLocales The canonicalized list of requested locales * @returns {String[]} An array containing a list of matching locales * @private */ BestFitSupportedLocales : function (availableLocales, requestedLocales) { var len = requestedLocales.length; var subset = new List(); var k = 0; while (k < len) { var locale = requestedLocales[k]; var noExtensionsLocale = locale.replace(this.unicodeLocaleExtensions, ""); var availableLocale = this.BestFitAvailableLocale(availableLocales, noExtensionsLocale); if (availableLocale !== undefined) { subset.push(locale); } k++; } var subsetArray = subset.toArray(); return subsetArray; }, /** * SupportedLocales abstract operation as defined in ECMA-402 Section 9.2.8 * * 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. * * @param {List} availableLocales The canonicalized list of available locales * @param {List} requestedLocales The canonicalized list of requested locales * @param {Object} options Specifies which lookup algorithm to use * @returns {String[]} An array containing a list of matching locales * @private */ SupportedLocales : function (availableLocales, requestedLocales, options) { var matcher; var subset; if (options !== undefined) { options = Object(options); matcher = options.localeMatcher; if (matcher !== undefined) { matcher = String(matcher); if (matcher !== "lookup" && matcher !== "best fit") { throw new RangeError("Matching algorithm must be 'lookup' or 'best fit'."); } } } if (matcher === undefined || matcher === "best fit") { subset = this.BestFitSupportedLocales(availableLocales, requestedLocales); } else { subset = this.LookupSupportedLocales(availableLocales, requestedLocales); } for (var P in Object.getOwnPropertyNames(subset)) { var desc = Object.getOwnPropertyDescriptor(subset, P); if (desc !== undefined) { desc.writable = false; desc.configurable = false; Object.defineProperty(subset, P, desc); } } Object.defineProperty(subset, "length", { writable : false, configurable : false }); return subset; }, /** * GetOption abstract operation as defined in ECMA-402 Section 9.2.9 * * Extracts the value of the 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. * * @param {Object} options The object containing the options to search * @param {String} property The property to retrieve * @param {String} type The type of the resulting option value * @param {Array} values The list of allowed values * @param {*} fallback The fallback value * @returns {*} The resulting value as described above. * @private */ GetOption : function (options, property, type, values, fallback) { var value = options[property]; if (value !== undefined) { if (type === "boolean") { value = Boolean(value); } if (type === "string") { value = String(value); } if (values !== undefined) { for (var v in values) { if (values[v] === value) { return value; } } throw new RangeError("The specified value " + value + " for property " + property + " is invalid."); } return value; } return fallback; }, /** * GetNumberOption abstract operation as defined in ECMA-402 Section 9.2.10 * * Extracts the value of the named property 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. * * @param {Object} options The object containing the options to search * @param {String} property The property to retrieve * @param {Number} minimum The minimum numeric value for this option * @param {Number} maximum The maximum numeric value for this option * @param {*} fallback The fallback value * @returns {Number} The resulting value as described above. * @private */ GetNumberOption : function (options, property, minimum, maximum, fallback) { var value = options[property]; if (value !== undefined) { value = Number(value); if (isNaN(value) || value < minimum || value > maximum) { throw new RangeError("The specified number value " + value + " is not in the allowed range"); } return Math.floor(value); } return fallback; } }; common.availableLocalesList = common.CanonicalizeLocaleList(JSON.parse(availableLocalesJson).availableLocales); return common; });