michael@0: // Copyright 2011-2012 Norbert Lindenberg. All rights reserved. michael@0: // Copyright 2012-2013 Mozilla Corporation. All rights reserved. michael@0: // This code is governed by the BSD license found in the LICENSE file. michael@0: michael@0: /** michael@0: * This file contains shared functions for the tests in the conformance test michael@0: * suite for the ECMAScript Internationalization API. michael@0: * @author Norbert Lindenberg michael@0: */ michael@0: michael@0: michael@0: /** michael@0: * @description Calls the provided function for every service constructor in michael@0: * the Intl object, until f returns a falsy value. It returns the result of the michael@0: * last call to f, mapped to a boolean. michael@0: * @param {Function} f the function to call for each service constructor in michael@0: * the Intl object. michael@0: * @param {Function} Constructor the constructor object to test with. michael@0: * @result {Boolean} whether the test succeeded. michael@0: */ michael@0: function testWithIntlConstructors(f) { michael@0: var constructors = ["Collator", "NumberFormat", "DateTimeFormat"]; michael@0: return constructors.every(function (constructor) { michael@0: var Constructor = Intl[constructor]; michael@0: var result; michael@0: try { michael@0: result = f(Constructor); michael@0: } catch (e) { michael@0: e.message += " (Testing with " + constructor + ".)"; michael@0: throw e; michael@0: } michael@0: return result; michael@0: }); michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Returns the name of the given constructor object, which must be one of michael@0: * Intl.Collator, Intl.NumberFormat, or Intl.DateTimeFormat. michael@0: * @param {object} Constructor a constructor michael@0: * @return {string} the name of the constructor michael@0: */ michael@0: function getConstructorName(Constructor) { michael@0: switch (Constructor) { michael@0: case Intl.Collator: michael@0: return "Collator"; michael@0: case Intl.NumberFormat: michael@0: return "NumberFormat"; michael@0: case Intl.DateTimeFormat: michael@0: return "DateTimeFormat"; michael@0: default: michael@0: $ERROR("test internal error: unknown Constructor"); michael@0: } michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Taints a named data property of the given object by installing michael@0: * a setter that throws an exception. michael@0: * @param {object} obj the object whose data property to taint michael@0: * @param {string} property the property to taint michael@0: */ michael@0: function taintDataProperty(obj, property) { michael@0: Object.defineProperty(obj, property, { michael@0: set: function(value) { michael@0: $ERROR("Client code can adversely affect behavior: setter for " + property + "."); michael@0: }, michael@0: enumerable: false, michael@0: configurable: true michael@0: }); michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Taints a named method of the given object by replacing it with a function michael@0: * that throws an exception. michael@0: * @param {object} obj the object whose method to taint michael@0: * @param {string} property the name of the method to taint michael@0: */ michael@0: function taintMethod(obj, property) { michael@0: Object.defineProperty(obj, property, { michael@0: value: function() { michael@0: $ERROR("Client code can adversely affect behavior: method " + property + "."); michael@0: }, michael@0: writable: true, michael@0: enumerable: false, michael@0: configurable: true michael@0: }); michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Taints the given properties (and similarly named properties) by installing michael@0: * setters on Object.prototype that throw exceptions. michael@0: * @param {Array} properties an array of property names to taint michael@0: */ michael@0: function taintProperties(properties) { michael@0: properties.forEach(function (property) { michael@0: var adaptedProperties = [property, "__" + property, "_" + property, property + "_", property + "__"]; michael@0: adaptedProperties.forEach(function (property) { michael@0: taintDataProperty(Object.prototype, property); michael@0: }); michael@0: }); michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Taints the Array object by creating a setter for the property "0" and michael@0: * replacing some key methods with functions that throw exceptions. michael@0: */ michael@0: function taintArray() { michael@0: taintDataProperty(Array.prototype, "0"); michael@0: taintMethod(Array.prototype, "indexOf"); michael@0: taintMethod(Array.prototype, "join"); michael@0: taintMethod(Array.prototype, "push"); michael@0: taintMethod(Array.prototype, "slice"); michael@0: taintMethod(Array.prototype, "sort"); michael@0: } michael@0: michael@0: michael@0: // auxiliary data for getLocaleSupportInfo michael@0: var languages = ["zh", "es", "en", "hi", "ur", "ar", "ja", "pa"]; michael@0: var scripts = ["Latn", "Hans", "Deva", "Arab", "Jpan", "Hant"]; michael@0: var countries = ["CN", "IN", "US", "PK", "JP", "TW", "HK", "SG"]; michael@0: var localeSupportInfo = {}; michael@0: michael@0: michael@0: /** michael@0: * Gets locale support info for the given constructor object, which must be one michael@0: * of Intl.Collator, Intl.NumberFormat, Intl.DateTimeFormat. michael@0: * @param {object} Constructor the constructor for which to get locale support info michael@0: * @return {object} locale support info with the following properties: michael@0: * supported: array of fully supported language tags michael@0: * byFallback: array of language tags that are supported through fallbacks michael@0: * unsupported: array of unsupported language tags michael@0: */ michael@0: function getLocaleSupportInfo(Constructor) { michael@0: var constructorName = getConstructorName(Constructor); michael@0: if (localeSupportInfo[constructorName] !== undefined) { michael@0: return localeSupportInfo[constructorName]; michael@0: } michael@0: michael@0: var allTags = []; michael@0: var i, j, k; michael@0: var language, script, country; michael@0: for (i = 0; i < languages.length; i++) { michael@0: language = languages[i]; michael@0: allTags.push(language); michael@0: for (j = 0; j < scripts.length; j++) { michael@0: script = scripts[j]; michael@0: allTags.push(language + "-" + script); michael@0: for (k = 0; k < countries.length; k++) { michael@0: country = countries[k]; michael@0: allTags.push(language + "-" + script + "-" + country); michael@0: } michael@0: } michael@0: for (k = 0; k < countries.length; k++) { michael@0: country = countries[k]; michael@0: allTags.push(language + "-" + country); michael@0: } michael@0: } michael@0: michael@0: var supported = []; michael@0: var byFallback = []; michael@0: var unsupported = []; michael@0: for (i = 0; i < allTags.length; i++) { michael@0: var request = allTags[i]; michael@0: var result = new Constructor([request], {localeMatcher: "lookup"}).resolvedOptions().locale; michael@0: if (request === result) { michael@0: supported.push(request); michael@0: } else if (request.indexOf(result) === 0) { michael@0: byFallback.push(request); michael@0: } else { michael@0: unsupported.push(request); michael@0: } michael@0: } michael@0: michael@0: localeSupportInfo[constructorName] = { michael@0: supported: supported, michael@0: byFallback: byFallback, michael@0: unsupported: unsupported michael@0: }; michael@0: michael@0: return localeSupportInfo[constructorName]; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * @description Tests whether locale is a String value representing a michael@0: * structurally valid and canonicalized BCP 47 language tag, as defined in michael@0: * sections 6.2.2 and 6.2.3 of the ECMAScript Internationalization API michael@0: * Specification. michael@0: * @param {String} locale the string to be tested. michael@0: * @result {Boolean} whether the test succeeded. michael@0: */ michael@0: function isCanonicalizedStructurallyValidLanguageTag(locale) { michael@0: michael@0: /** michael@0: * Regular expression defining BCP 47 language tags. michael@0: * michael@0: * Spec: RFC 5646 section 2.1. michael@0: */ michael@0: var alpha = "[a-zA-Z]", michael@0: digit = "[0-9]", michael@0: alphanum = "(" + alpha + "|" + digit + ")", michael@0: regular = "(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)", michael@0: irregular = "(en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)", michael@0: grandfathered = "(" + irregular + "|" + regular + ")", michael@0: privateuse = "(x(-[a-z0-9]{1,8})+)", michael@0: singleton = "(" + digit + "|[A-WY-Za-wy-z])", michael@0: extension = "(" + singleton + "(-" + alphanum + "{2,8})+)", michael@0: variant = "(" + alphanum + "{5,8}|(" + digit + alphanum + "{3}))", michael@0: region = "(" + alpha + "{2}|" + digit + "{3})", michael@0: script = "(" + alpha + "{4})", michael@0: extlang = "(" + alpha + "{3}(-" + alpha + "{3}){0,2})", michael@0: language = "(" + alpha + "{2,3}(-" + extlang + ")?|" + alpha + "{4}|" + alpha + "{5,8})", michael@0: langtag = language + "(-" + script + ")?(-" + region + ")?(-" + variant + ")*(-" + extension + ")*(-" + privateuse + ")?", michael@0: languageTag = "^(" + langtag + "|" + privateuse + "|" + grandfathered + ")$", michael@0: languageTagRE = new RegExp(languageTag, "i"); michael@0: var duplicateSingleton = "-" + singleton + "-(.*-)?\\1(?!" + alphanum + ")", michael@0: duplicateSingletonRE = new RegExp(duplicateSingleton, "i"), michael@0: duplicateVariant = "(" + alphanum + "{2,8}-)+" + variant + "-(" + alphanum + "{2,8}-)*\\3(?!" + alphanum + ")", michael@0: duplicateVariantRE = new RegExp(duplicateVariant, "i"); michael@0: michael@0: michael@0: /** michael@0: * Verifies that the given string is a well-formed BCP 47 language tag michael@0: * with no duplicate variant or singleton subtags. michael@0: * michael@0: * Spec: ECMAScript Internationalization API Specification, draft, 6.2.2. michael@0: */ michael@0: function isStructurallyValidLanguageTag(locale) { michael@0: if (!languageTagRE.test(locale)) { michael@0: return false; michael@0: } michael@0: locale = locale.split(/-x-/)[0]; michael@0: return !duplicateSingletonRE.test(locale) && !duplicateVariantRE.test(locale); michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Mappings from complete tags to preferred values. michael@0: * michael@0: * Spec: IANA Language Subtag Registry. michael@0: */ michael@0: var __tagMappings = { michael@0: // property names must be in lower case; values in canonical form michael@0: michael@0: // grandfathered tags from IANA language subtag registry, file date 2011-08-25 michael@0: "art-lojban": "jbo", michael@0: "cel-gaulish": "cel-gaulish", michael@0: "en-gb-oed": "en-GB-oed", michael@0: "i-ami": "ami", michael@0: "i-bnn": "bnn", michael@0: "i-default": "i-default", michael@0: "i-enochian": "i-enochian", michael@0: "i-hak": "hak", michael@0: "i-klingon": "tlh", michael@0: "i-lux": "lb", michael@0: "i-mingo": "i-mingo", michael@0: "i-navajo": "nv", michael@0: "i-pwn": "pwn", michael@0: "i-tao": "tao", michael@0: "i-tay": "tay", michael@0: "i-tsu": "tsu", michael@0: "no-bok": "nb", michael@0: "no-nyn": "nn", michael@0: "sgn-be-fr": "sfb", michael@0: "sgn-be-nl": "vgt", michael@0: "sgn-ch-de": "sgg", michael@0: "zh-guoyu": "cmn", michael@0: "zh-hakka": "hak", michael@0: "zh-min": "zh-min", michael@0: "zh-min-nan": "nan", michael@0: "zh-xiang": "hsn", michael@0: // deprecated redundant tags from IANA language subtag registry, file date 2011-08-25 michael@0: "sgn-br": "bzs", michael@0: "sgn-co": "csn", michael@0: "sgn-de": "gsg", michael@0: "sgn-dk": "dsl", michael@0: "sgn-es": "ssp", michael@0: "sgn-fr": "fsl", michael@0: "sgn-gb": "bfi", michael@0: "sgn-gr": "gss", michael@0: "sgn-ie": "isg", michael@0: "sgn-it": "ise", michael@0: "sgn-jp": "jsl", michael@0: "sgn-mx": "mfs", michael@0: "sgn-ni": "ncs", michael@0: "sgn-nl": "dse", michael@0: "sgn-no": "nsl", michael@0: "sgn-pt": "psr", michael@0: "sgn-se": "swl", michael@0: "sgn-us": "ase", michael@0: "sgn-za": "sfs", michael@0: "zh-cmn": "cmn", michael@0: "zh-cmn-hans": "cmn-Hans", michael@0: "zh-cmn-hant": "cmn-Hant", michael@0: "zh-gan": "gan", michael@0: "zh-wuu": "wuu", michael@0: "zh-yue": "yue", michael@0: // deprecated variant with prefix from IANA language subtag registry, file date 2011-08-25 michael@0: "ja-latn-hepburn-heploc": "ja-Latn-alalc97" michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * Mappings from non-extlang subtags to preferred values. michael@0: * michael@0: * Spec: IANA Language Subtag Registry. michael@0: */ michael@0: var __subtagMappings = { michael@0: // property names and values must be in canonical case michael@0: // language subtags with Preferred-Value mappings from IANA language subtag registry, file date 2011-08-25 michael@0: "in": "id", michael@0: "iw": "he", michael@0: "ji": "yi", michael@0: "jw": "jv", michael@0: "mo": "ro", michael@0: "ayx": "nun", michael@0: "cjr": "mom", michael@0: "cmk": "xch", michael@0: "drh": "khk", michael@0: "drw": "prs", michael@0: "gav": "dev", michael@0: "mst": "mry", michael@0: "myt": "mry", michael@0: "tie": "ras", michael@0: "tkk": "twm", michael@0: "tnf": "prs", michael@0: // region subtags with Preferred-Value mappings from IANA language subtag registry, file date 2011-08-25 michael@0: "BU": "MM", michael@0: "DD": "DE", michael@0: "FX": "FR", michael@0: "TP": "TL", michael@0: "YD": "YE", michael@0: "ZR": "CD" michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * Mappings from extlang subtags to preferred values. michael@0: * michael@0: * Spec: IANA Language Subtag Registry. michael@0: */ michael@0: var __extlangMappings = { michael@0: // extlang subtags with Preferred-Value mappings from IANA language subtag registry, file date 2011-08-25 michael@0: // values are arrays with [0] the replacement value, [1] (if present) the prefix to be removed michael@0: "aao": ["aao", "ar"], michael@0: "abh": ["abh", "ar"], michael@0: "abv": ["abv", "ar"], michael@0: "acm": ["acm", "ar"], michael@0: "acq": ["acq", "ar"], michael@0: "acw": ["acw", "ar"], michael@0: "acx": ["acx", "ar"], michael@0: "acy": ["acy", "ar"], michael@0: "adf": ["adf", "ar"], michael@0: "ads": ["ads", "sgn"], michael@0: "aeb": ["aeb", "ar"], michael@0: "aec": ["aec", "ar"], michael@0: "aed": ["aed", "sgn"], michael@0: "aen": ["aen", "sgn"], michael@0: "afb": ["afb", "ar"], michael@0: "afg": ["afg", "sgn"], michael@0: "ajp": ["ajp", "ar"], michael@0: "apc": ["apc", "ar"], michael@0: "apd": ["apd", "ar"], michael@0: "arb": ["arb", "ar"], michael@0: "arq": ["arq", "ar"], michael@0: "ars": ["ars", "ar"], michael@0: "ary": ["ary", "ar"], michael@0: "arz": ["arz", "ar"], michael@0: "ase": ["ase", "sgn"], michael@0: "asf": ["asf", "sgn"], michael@0: "asp": ["asp", "sgn"], michael@0: "asq": ["asq", "sgn"], michael@0: "asw": ["asw", "sgn"], michael@0: "auz": ["auz", "ar"], michael@0: "avl": ["avl", "ar"], michael@0: "ayh": ["ayh", "ar"], michael@0: "ayl": ["ayl", "ar"], michael@0: "ayn": ["ayn", "ar"], michael@0: "ayp": ["ayp", "ar"], michael@0: "bbz": ["bbz", "ar"], michael@0: "bfi": ["bfi", "sgn"], michael@0: "bfk": ["bfk", "sgn"], michael@0: "bjn": ["bjn", "ms"], michael@0: "bog": ["bog", "sgn"], michael@0: "bqn": ["bqn", "sgn"], michael@0: "bqy": ["bqy", "sgn"], michael@0: "btj": ["btj", "ms"], michael@0: "bve": ["bve", "ms"], michael@0: "bvl": ["bvl", "sgn"], michael@0: "bvu": ["bvu", "ms"], michael@0: "bzs": ["bzs", "sgn"], michael@0: "cdo": ["cdo", "zh"], michael@0: "cds": ["cds", "sgn"], michael@0: "cjy": ["cjy", "zh"], michael@0: "cmn": ["cmn", "zh"], michael@0: "coa": ["coa", "ms"], michael@0: "cpx": ["cpx", "zh"], michael@0: "csc": ["csc", "sgn"], michael@0: "csd": ["csd", "sgn"], michael@0: "cse": ["cse", "sgn"], michael@0: "csf": ["csf", "sgn"], michael@0: "csg": ["csg", "sgn"], michael@0: "csl": ["csl", "sgn"], michael@0: "csn": ["csn", "sgn"], michael@0: "csq": ["csq", "sgn"], michael@0: "csr": ["csr", "sgn"], michael@0: "czh": ["czh", "zh"], michael@0: "czo": ["czo", "zh"], michael@0: "doq": ["doq", "sgn"], michael@0: "dse": ["dse", "sgn"], michael@0: "dsl": ["dsl", "sgn"], michael@0: "dup": ["dup", "ms"], michael@0: "ecs": ["ecs", "sgn"], michael@0: "esl": ["esl", "sgn"], michael@0: "esn": ["esn", "sgn"], michael@0: "eso": ["eso", "sgn"], michael@0: "eth": ["eth", "sgn"], michael@0: "fcs": ["fcs", "sgn"], michael@0: "fse": ["fse", "sgn"], michael@0: "fsl": ["fsl", "sgn"], michael@0: "fss": ["fss", "sgn"], michael@0: "gan": ["gan", "zh"], michael@0: "gom": ["gom", "kok"], michael@0: "gse": ["gse", "sgn"], michael@0: "gsg": ["gsg", "sgn"], michael@0: "gsm": ["gsm", "sgn"], michael@0: "gss": ["gss", "sgn"], michael@0: "gus": ["gus", "sgn"], michael@0: "hab": ["hab", "sgn"], michael@0: "haf": ["haf", "sgn"], michael@0: "hak": ["hak", "zh"], michael@0: "hds": ["hds", "sgn"], michael@0: "hji": ["hji", "ms"], michael@0: "hks": ["hks", "sgn"], michael@0: "hos": ["hos", "sgn"], michael@0: "hps": ["hps", "sgn"], michael@0: "hsh": ["hsh", "sgn"], michael@0: "hsl": ["hsl", "sgn"], michael@0: "hsn": ["hsn", "zh"], michael@0: "icl": ["icl", "sgn"], michael@0: "ils": ["ils", "sgn"], michael@0: "inl": ["inl", "sgn"], michael@0: "ins": ["ins", "sgn"], michael@0: "ise": ["ise", "sgn"], michael@0: "isg": ["isg", "sgn"], michael@0: "isr": ["isr", "sgn"], michael@0: "jak": ["jak", "ms"], michael@0: "jax": ["jax", "ms"], michael@0: "jcs": ["jcs", "sgn"], michael@0: "jhs": ["jhs", "sgn"], michael@0: "jls": ["jls", "sgn"], michael@0: "jos": ["jos", "sgn"], michael@0: "jsl": ["jsl", "sgn"], michael@0: "jus": ["jus", "sgn"], michael@0: "kgi": ["kgi", "sgn"], michael@0: "knn": ["knn", "kok"], michael@0: "kvb": ["kvb", "ms"], michael@0: "kvk": ["kvk", "sgn"], michael@0: "kvr": ["kvr", "ms"], michael@0: "kxd": ["kxd", "ms"], michael@0: "lbs": ["lbs", "sgn"], michael@0: "lce": ["lce", "ms"], michael@0: "lcf": ["lcf", "ms"], michael@0: "liw": ["liw", "ms"], michael@0: "lls": ["lls", "sgn"], michael@0: "lsg": ["lsg", "sgn"], michael@0: "lsl": ["lsl", "sgn"], michael@0: "lso": ["lso", "sgn"], michael@0: "lsp": ["lsp", "sgn"], michael@0: "lst": ["lst", "sgn"], michael@0: "lsy": ["lsy", "sgn"], michael@0: "ltg": ["ltg", "lv"], michael@0: "lvs": ["lvs", "lv"], michael@0: "lzh": ["lzh", "zh"], michael@0: "max": ["max", "ms"], michael@0: "mdl": ["mdl", "sgn"], michael@0: "meo": ["meo", "ms"], michael@0: "mfa": ["mfa", "ms"], michael@0: "mfb": ["mfb", "ms"], michael@0: "mfs": ["mfs", "sgn"], michael@0: "min": ["min", "ms"], michael@0: "mnp": ["mnp", "zh"], michael@0: "mqg": ["mqg", "ms"], michael@0: "mre": ["mre", "sgn"], michael@0: "msd": ["msd", "sgn"], michael@0: "msi": ["msi", "ms"], michael@0: "msr": ["msr", "sgn"], michael@0: "mui": ["mui", "ms"], michael@0: "mzc": ["mzc", "sgn"], michael@0: "mzg": ["mzg", "sgn"], michael@0: "mzy": ["mzy", "sgn"], michael@0: "nan": ["nan", "zh"], michael@0: "nbs": ["nbs", "sgn"], michael@0: "ncs": ["ncs", "sgn"], michael@0: "nsi": ["nsi", "sgn"], michael@0: "nsl": ["nsl", "sgn"], michael@0: "nsp": ["nsp", "sgn"], michael@0: "nsr": ["nsr", "sgn"], michael@0: "nzs": ["nzs", "sgn"], michael@0: "okl": ["okl", "sgn"], michael@0: "orn": ["orn", "ms"], michael@0: "ors": ["ors", "ms"], michael@0: "pel": ["pel", "ms"], michael@0: "pga": ["pga", "ar"], michael@0: "pks": ["pks", "sgn"], michael@0: "prl": ["prl", "sgn"], michael@0: "prz": ["prz", "sgn"], michael@0: "psc": ["psc", "sgn"], michael@0: "psd": ["psd", "sgn"], michael@0: "pse": ["pse", "ms"], michael@0: "psg": ["psg", "sgn"], michael@0: "psl": ["psl", "sgn"], michael@0: "pso": ["pso", "sgn"], michael@0: "psp": ["psp", "sgn"], michael@0: "psr": ["psr", "sgn"], michael@0: "pys": ["pys", "sgn"], michael@0: "rms": ["rms", "sgn"], michael@0: "rsi": ["rsi", "sgn"], michael@0: "rsl": ["rsl", "sgn"], michael@0: "sdl": ["sdl", "sgn"], michael@0: "sfb": ["sfb", "sgn"], michael@0: "sfs": ["sfs", "sgn"], michael@0: "sgg": ["sgg", "sgn"], michael@0: "sgx": ["sgx", "sgn"], michael@0: "shu": ["shu", "ar"], michael@0: "slf": ["slf", "sgn"], michael@0: "sls": ["sls", "sgn"], michael@0: "sqs": ["sqs", "sgn"], michael@0: "ssh": ["ssh", "ar"], michael@0: "ssp": ["ssp", "sgn"], michael@0: "ssr": ["ssr", "sgn"], michael@0: "svk": ["svk", "sgn"], michael@0: "swc": ["swc", "sw"], michael@0: "swh": ["swh", "sw"], michael@0: "swl": ["swl", "sgn"], michael@0: "syy": ["syy", "sgn"], michael@0: "tmw": ["tmw", "ms"], michael@0: "tse": ["tse", "sgn"], michael@0: "tsm": ["tsm", "sgn"], michael@0: "tsq": ["tsq", "sgn"], michael@0: "tss": ["tss", "sgn"], michael@0: "tsy": ["tsy", "sgn"], michael@0: "tza": ["tza", "sgn"], michael@0: "ugn": ["ugn", "sgn"], michael@0: "ugy": ["ugy", "sgn"], michael@0: "ukl": ["ukl", "sgn"], michael@0: "uks": ["uks", "sgn"], michael@0: "urk": ["urk", "ms"], michael@0: "uzn": ["uzn", "uz"], michael@0: "uzs": ["uzs", "uz"], michael@0: "vgt": ["vgt", "sgn"], michael@0: "vkk": ["vkk", "ms"], michael@0: "vkt": ["vkt", "ms"], michael@0: "vsi": ["vsi", "sgn"], michael@0: "vsl": ["vsl", "sgn"], michael@0: "vsv": ["vsv", "sgn"], michael@0: "wuu": ["wuu", "zh"], michael@0: "xki": ["xki", "sgn"], michael@0: "xml": ["xml", "sgn"], michael@0: "xmm": ["xmm", "ms"], michael@0: "xms": ["xms", "sgn"], michael@0: "yds": ["yds", "sgn"], michael@0: "ysl": ["ysl", "sgn"], michael@0: "yue": ["yue", "zh"], michael@0: "zib": ["zib", "sgn"], michael@0: "zlm": ["zlm", "ms"], michael@0: "zmi": ["zmi", "ms"], michael@0: "zsl": ["zsl", "sgn"], michael@0: "zsm": ["zsm", "ms"] michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * Canonicalizes the given well-formed BCP 47 language tag, including regularized case of subtags. michael@0: * michael@0: * Spec: ECMAScript Internationalization API Specification, draft, 6.2.3. michael@0: * Spec: RFC 5646, section 4.5. michael@0: */ michael@0: function canonicalizeLanguageTag(locale) { michael@0: michael@0: // start with lower case for easier processing, and because most subtags will need to be lower case anyway michael@0: locale = locale.toLowerCase(); michael@0: michael@0: // handle mappings for complete tags michael@0: if (__tagMappings.hasOwnProperty(locale)) { michael@0: return __tagMappings[locale]; michael@0: } michael@0: michael@0: var subtags = locale.split("-"); michael@0: var i = 0; michael@0: michael@0: // handle standard part: all subtags before first singleton or "x" michael@0: while (i < subtags.length) { michael@0: var subtag = subtags[i]; michael@0: if (subtag.length === 1 && (i > 0 || subtag === "x")) { michael@0: break; michael@0: } else if (i !== 0 && subtag.length === 2) { michael@0: subtag = subtag.toUpperCase(); michael@0: } else if (subtag.length === 4) { michael@0: subtag = subtag[0].toUpperCase() + subtag.substring(1).toLowerCase(); michael@0: } michael@0: if (__subtagMappings.hasOwnProperty(subtag)) { michael@0: subtag = __subtagMappings[subtag]; michael@0: } else if (__extlangMappings.hasOwnProperty(subtag)) { michael@0: subtag = __extlangMappings[subtag][0]; michael@0: if (i === 1 && __extlangMappings[subtag][1] === subtags[0]) { michael@0: subtags.shift(); michael@0: i--; michael@0: } michael@0: } michael@0: subtags[i] = subtag; michael@0: i++; michael@0: } michael@0: var normal = subtags.slice(0, i).join("-"); michael@0: michael@0: // handle extensions michael@0: var extensions = []; michael@0: while (i < subtags.length && subtags[i] !== "x") { michael@0: var extensionStart = i; michael@0: i++; michael@0: while (i < subtags.length && subtags[i].length > 1) { michael@0: i++; michael@0: } michael@0: var extension = subtags.slice(extensionStart, i).join("-"); michael@0: extensions.push(extension); michael@0: } michael@0: extensions.sort(); michael@0: michael@0: // handle private use michael@0: var privateUse; michael@0: if (i < subtags.length) { michael@0: privateUse = subtags.slice(i).join("-"); michael@0: } michael@0: michael@0: // put everything back together michael@0: var canonical = normal; michael@0: if (extensions.length > 0) { michael@0: canonical += "-" + extensions.join("-"); michael@0: } michael@0: if (privateUse !== undefined) { michael@0: if (canonical.length > 0) { michael@0: canonical += "-" + privateUse; michael@0: } else { michael@0: canonical = privateUse; michael@0: } michael@0: } michael@0: michael@0: return canonical; michael@0: } michael@0: michael@0: return typeof locale === "string" && isStructurallyValidLanguageTag(locale) && michael@0: canonicalizeLanguageTag(locale) === locale; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Tests whether the named options property is correctly handled by the given constructor. michael@0: * @param {object} Constructor the constructor to test. michael@0: * @param {string} property the name of the options property to test. michael@0: * @param {string} type the type that values of the property are expected to have michael@0: * @param {Array} [values] an array of allowed values for the property. Not needed for boolean. michael@0: * @param {any} fallback the fallback value that the property assumes if not provided. michael@0: * @param {object} testOptions additional options: michael@0: * @param {boolean} isOptional whether support for this property is optional for implementations. michael@0: * @param {boolean} noReturn whether the resulting value of the property is not returned. michael@0: * @param {boolean} isILD whether the resulting value of the property is implementation and locale dependent. michael@0: * @param {object} extra additional option to pass along, properties are value -> {option: value}. michael@0: * @return {boolean} whether the test succeeded. michael@0: */ michael@0: function testOption(Constructor, property, type, values, fallback, testOptions) { michael@0: var isOptional = testOptions !== undefined && testOptions.isOptional === true; michael@0: var noReturn = testOptions !== undefined && testOptions.noReturn === true; michael@0: var isILD = testOptions !== undefined && testOptions.isILD === true; michael@0: michael@0: function addExtraOptions(options, value, testOptions) { michael@0: if (testOptions !== undefined && testOptions.extra !== undefined) { michael@0: var extra; michael@0: if (value !== undefined && testOptions.extra[value] !== undefined) { michael@0: extra = testOptions.extra[value]; michael@0: } else if (testOptions.extra.any !== undefined) { michael@0: extra = testOptions.extra.any; michael@0: } michael@0: if (extra !== undefined) { michael@0: Object.getOwnPropertyNames(extra).forEach(function (prop) { michael@0: options[prop] = extra[prop]; michael@0: }); michael@0: } michael@0: } michael@0: } michael@0: michael@0: var testValues, options, obj, expected, actual, error; michael@0: michael@0: // test that the specified values are accepted. Also add values that convert to specified values. michael@0: if (type === "boolean") { michael@0: if (values === undefined) { michael@0: values = [true, false]; michael@0: } michael@0: testValues = values.slice(0); michael@0: testValues.push(888); michael@0: testValues.push(0); michael@0: } else if (type === "string") { michael@0: testValues = values.slice(0); michael@0: testValues.push({toString: function () { return values[0]; }}); michael@0: } michael@0: testValues.forEach(function (value) { michael@0: options = {}; michael@0: options[property] = value; michael@0: addExtraOptions(options, value, testOptions); michael@0: obj = new Constructor(undefined, options); michael@0: if (noReturn) { michael@0: if (obj.resolvedOptions().hasOwnProperty(property)) { michael@0: $ERROR("Option property " + property + " is returned, but shouldn't be."); michael@0: } michael@0: } else { michael@0: actual = obj.resolvedOptions()[property]; michael@0: if (isILD) { michael@0: if (actual !== undefined && values.indexOf(actual) === -1) { michael@0: $ERROR("Invalid value " + actual + " returned for property " + property + "."); michael@0: } michael@0: } else { michael@0: if (type === "boolean") { michael@0: expected = Boolean(value); michael@0: } else if (type === "string") { michael@0: expected = String(value); michael@0: } michael@0: if (actual !== expected && !(isOptional && actual === undefined)) { michael@0: $ERROR("Option value " + value + " for property " + property + michael@0: " was not accepted; got " + actual + " instead."); michael@0: } michael@0: } michael@0: } michael@0: }); michael@0: michael@0: // test that invalid values are rejected michael@0: if (type === "string") { michael@0: var invalidValues = ["invalidValue", -1, null]; michael@0: // assume that we won't have values in caseless scripts michael@0: if (values[0].toUpperCase() !== values[0]) { michael@0: invalidValues.push(values[0].toUpperCase()); michael@0: } else { michael@0: invalidValues.push(values[0].toLowerCase()); michael@0: } michael@0: invalidValues.forEach(function (value) { michael@0: options = {}; michael@0: options[property] = value; michael@0: addExtraOptions(options, value, testOptions); michael@0: error = undefined; michael@0: try { michael@0: obj = new Constructor(undefined, options); michael@0: } catch (e) { michael@0: error = e; michael@0: } michael@0: if (error === undefined) { michael@0: $ERROR("Invalid option value " + value + " for property " + property + " was not rejected."); michael@0: } else if (error.name !== "RangeError") { michael@0: $ERROR("Invalid option value " + value + " for property " + property + " was rejected with wrong error " + error.name + "."); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: // test that fallback value or another valid value is used if no options value is provided michael@0: if (!noReturn) { michael@0: options = {}; michael@0: addExtraOptions(options, undefined, testOptions); michael@0: obj = new Constructor(undefined, options); michael@0: actual = obj.resolvedOptions()[property]; michael@0: if (!(isOptional && actual === undefined)) { michael@0: if (fallback !== undefined) { michael@0: if (actual !== fallback) { michael@0: $ERROR("Option fallback value " + fallback + " for property " + property + michael@0: " was not used; got " + actual + " instead."); michael@0: } michael@0: } else { michael@0: if (values.indexOf(actual) === -1 && !(isILD && actual === undefined)) { michael@0: $ERROR("Invalid value " + actual + " returned for property " + property + "."); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Tests whether the named property of the given object has a valid value michael@0: * and the default attributes of the properties of an object literal. michael@0: * @param {Object} obj the object to be tested. michael@0: * @param {string} property the name of the property michael@0: * @param {Function|Array} valid either a function that tests value for validity and returns a boolean, michael@0: * an array of valid values. michael@0: * @exception if the property has an invalid value. michael@0: */ michael@0: function testProperty(obj, property, valid) { michael@0: var desc = Object.getOwnPropertyDescriptor(obj, property); michael@0: if (!desc.writable) { michael@0: $ERROR("Property " + property + " must be writable."); michael@0: } michael@0: if (!desc.enumerable) { michael@0: $ERROR("Property " + property + " must be enumerable."); michael@0: } michael@0: if (!desc.configurable) { michael@0: $ERROR("Property " + property + " must be configurable."); michael@0: } michael@0: var value = desc.value; michael@0: var isValid = (typeof valid === "function") ? valid(value) : (valid.indexOf(value) !== -1); michael@0: if (!isValid) { michael@0: $ERROR("Property value " + value + " is not allowed for property " + property + "."); michael@0: } michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Tests whether the named property of the given object, if present at all, has a valid value michael@0: * and the default attributes of the properties of an object literal. michael@0: * @param {Object} obj the object to be tested. michael@0: * @param {string} property the name of the property michael@0: * @param {Function|Array} valid either a function that tests value for validity and returns a boolean, michael@0: * an array of valid values. michael@0: * @exception if the property is present and has an invalid value. michael@0: */ michael@0: function mayHaveProperty(obj, property, valid) { michael@0: if (obj.hasOwnProperty(property)) { michael@0: testProperty(obj, property, valid); michael@0: } michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Tests whether the given object has the named property with a valid value michael@0: * and the default attributes of the properties of an object literal. michael@0: * @param {Object} obj the object to be tested. michael@0: * @param {string} property the name of the property michael@0: * @param {Function|Array} valid either a function that tests value for validity and returns a boolean, michael@0: * an array of valid values. michael@0: * @exception if the property is missing or has an invalid value. michael@0: */ michael@0: function mustHaveProperty(obj, property, valid) { michael@0: if (!obj.hasOwnProperty(property)) { michael@0: $ERROR("Object is missing property " + property + "."); michael@0: } michael@0: testProperty(obj, property, valid); michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Tests whether the given object does not have the named property. michael@0: * @param {Object} obj the object to be tested. michael@0: * @param {string} property the name of the property michael@0: * @exception if the property is present. michael@0: */ michael@0: function mustNotHaveProperty(obj, property) { michael@0: if (obj.hasOwnProperty(property)) { michael@0: $ERROR("Object has property it mustn't have: " + property + "."); michael@0: } michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Properties of the RegExp constructor that may be affected by use of regular michael@0: * expressions, and the default values of these properties. Properties are from michael@0: * https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Deprecated_and_obsolete_features#RegExp_Properties michael@0: */ michael@0: var regExpProperties = ["$1", "$2", "$3", "$4", "$5", "$6", "$7", "$8", "$9", michael@0: "$_", "$*", "$&", "$+", "$`", "$'", michael@0: "input", "lastMatch", "lastParen", "leftContext", "rightContext" michael@0: ]; michael@0: michael@0: var regExpPropertiesDefaultValues = (function () { michael@0: var values = Object.create(null); michael@0: regExpProperties.forEach(function (property) { michael@0: values[property] = RegExp[property]; michael@0: }); michael@0: return values; michael@0: }()); michael@0: michael@0: michael@0: /** michael@0: * Tests that executing the provided function (which may use regular expressions michael@0: * in its implementation) does not create or modify unwanted properties on the michael@0: * RegExp constructor. michael@0: */ michael@0: function testForUnwantedRegExpChanges(testFunc) { michael@0: regExpProperties.forEach(function (property) { michael@0: RegExp[property] = regExpPropertiesDefaultValues[property]; michael@0: }); michael@0: testFunc(); michael@0: regExpProperties.forEach(function (property) { michael@0: if (RegExp[property] !== regExpPropertiesDefaultValues[property]) { michael@0: $ERROR("RegExp has unexpected property " + property + " with value " + michael@0: RegExp[property] + "."); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Tests whether name is a valid BCP 47 numbering system name michael@0: * and not excluded from use in the ECMAScript Internationalization API. michael@0: * @param {string} name the name to be tested. michael@0: * @return {boolean} whether name is a valid BCP 47 numbering system name and michael@0: * allowed for use in the ECMAScript Internationalization API. michael@0: */ michael@0: michael@0: function isValidNumberingSystem(name) { michael@0: michael@0: // source: CLDR file common/bcp47/number.xml; version CLDR 21. michael@0: var numberingSystems = [ michael@0: "arab", michael@0: "arabext", michael@0: "armn", michael@0: "armnlow", michael@0: "bali", michael@0: "beng", michael@0: "brah", michael@0: "cakm", michael@0: "cham", michael@0: "deva", michael@0: "ethi", michael@0: "finance", michael@0: "fullwide", michael@0: "geor", michael@0: "grek", michael@0: "greklow", michael@0: "gujr", michael@0: "guru", michael@0: "hanidec", michael@0: "hans", michael@0: "hansfin", michael@0: "hant", michael@0: "hantfin", michael@0: "hebr", michael@0: "java", michael@0: "jpan", michael@0: "jpanfin", michael@0: "kali", michael@0: "khmr", michael@0: "knda", michael@0: "osma", michael@0: "lana", michael@0: "lanatham", michael@0: "laoo", michael@0: "latn", michael@0: "lepc", michael@0: "limb", michael@0: "mlym", michael@0: "mong", michael@0: "mtei", michael@0: "mymr", michael@0: "mymrshan", michael@0: "native", michael@0: "nkoo", michael@0: "olck", michael@0: "orya", michael@0: "roman", michael@0: "romanlow", michael@0: "saur", michael@0: "shrd", michael@0: "sora", michael@0: "sund", michael@0: "talu", michael@0: "takr", michael@0: "taml", michael@0: "tamldec", michael@0: "telu", michael@0: "thai", michael@0: "tibt", michael@0: "traditio", michael@0: "vaii" michael@0: ]; michael@0: michael@0: var excluded = [ michael@0: "finance", michael@0: "native", michael@0: "traditio" michael@0: ]; michael@0: michael@0: michael@0: return numberingSystems.indexOf(name) !== -1 && excluded.indexOf(name) === -1; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Provides the digits of numbering systems with simple digit mappings, michael@0: * as specified in 11.3.2. michael@0: */ michael@0: michael@0: var numberingSystemDigits = { michael@0: arab: "٠١٢٣٤٥٦٧٨٩", michael@0: arabext: "۰۱۲۳۴۵۶۷۸۹", michael@0: beng: "০১২৩৪৫৬৭৮৯", michael@0: deva: "०१२३४५६७८९", michael@0: fullwide: "0123456789", michael@0: gujr: "૦૧૨૩૪૫૬૭૮૯", michael@0: guru: "੦੧੨੩੪੫੬੭੮੯", michael@0: hanidec: "〇一二三四五六七八九", michael@0: khmr: "០១២៣៤៥៦៧៨៩", michael@0: knda: "೦೧೨೩೪೫೬೭೮೯", michael@0: laoo: "໐໑໒໓໔໕໖໗໘໙", michael@0: latn: "0123456789", michael@0: mlym: "൦൧൨൩൪൫൬൭൮൯", michael@0: mong: "᠐᠑᠒᠓᠔᠕᠖᠗᠘᠙", michael@0: mymr: "၀၁၂၃၄၅၆၇၈၉", michael@0: orya: "୦୧୨୩୪୫୬୭୮୯", michael@0: tamldec: "௦௧௨௩௪௫௬௭௮௯", michael@0: telu: "౦౧౨౩౪౫౬౭౮౯", michael@0: thai: "๐๑๒๓๔๕๖๗๘๙", michael@0: tibt: "༠༡༢༣༤༥༦༧༨༩" michael@0: }; michael@0: michael@0: michael@0: /** michael@0: * Tests that number formatting is handled correctly. The function checks that the michael@0: * digit sequences in formatted output are as specified, converted to the michael@0: * selected numbering system, and embedded in consistent localized patterns. michael@0: * @param {Array} locales the locales to be tested. michael@0: * @param {Array} numberingSystems the numbering systems to be tested. michael@0: * @param {Object} options the options to pass to Intl.NumberFormat. Options michael@0: * must include {useGrouping: false}, and must cause 1.1 to be formatted michael@0: * pre- and post-decimal digits. michael@0: * @param {Object} testData maps input data (in ES5 9.3.1 format) to expected output strings michael@0: * in unlocalized format with Western digits. michael@0: */ michael@0: michael@0: function testNumberFormat(locales, numberingSystems, options, testData) { michael@0: locales.forEach(function (locale) { michael@0: numberingSystems.forEach(function (numbering) { michael@0: var digits = numberingSystemDigits[numbering]; michael@0: var format = new Intl.NumberFormat([locale + "-u-nu-" + numbering], options); michael@0: michael@0: function getPatternParts(positive) { michael@0: var n = positive ? 1.1 : -1.1; michael@0: var formatted = format.format(n); michael@0: var oneoneRE = "([^" + digits + "]*)[" + digits + "]+([^" + digits + "]+)[" + digits + "]+([^" + digits + "]*)"; michael@0: var match = formatted.match(new RegExp(oneoneRE)); michael@0: if (match === null) { michael@0: $ERROR("Unexpected formatted " + n + " for " + michael@0: format.resolvedOptions().locale + " and options " + michael@0: JSON.stringify(options) + ": " + formatted); michael@0: } michael@0: return match; michael@0: } michael@0: michael@0: function toNumbering(raw) { michael@0: return raw.replace(/[0-9]/g, function (digit) { michael@0: return digits[digit.charCodeAt(0) - "0".charCodeAt(0)]; michael@0: }); michael@0: } michael@0: michael@0: function buildExpected(raw, patternParts) { michael@0: var period = raw.indexOf("."); michael@0: if (period === -1) { michael@0: return patternParts[1] + toNumbering(raw) + patternParts[3]; michael@0: } else { michael@0: return patternParts[1] + michael@0: toNumbering(raw.substring(0, period)) + michael@0: patternParts[2] + michael@0: toNumbering(raw.substring(period + 1)) + michael@0: patternParts[3]; michael@0: } michael@0: } michael@0: michael@0: if (format.resolvedOptions().numberingSystem === numbering) { michael@0: // figure out prefixes, infixes, suffixes for positive and negative values michael@0: var posPatternParts = getPatternParts(true); michael@0: var negPatternParts = getPatternParts(false); michael@0: michael@0: Object.getOwnPropertyNames(testData).forEach(function (input) { michael@0: var rawExpected = testData[input]; michael@0: var patternParts; michael@0: if (rawExpected[0] === "-") { michael@0: patternParts = negPatternParts; michael@0: rawExpected = rawExpected.substring(1); michael@0: } else { michael@0: patternParts = posPatternParts; michael@0: } michael@0: var expected = buildExpected(rawExpected, patternParts); michael@0: var actual = format.format(input); michael@0: if (actual !== expected) { michael@0: $ERROR("Formatted value for " + input + ", " + michael@0: format.resolvedOptions().locale + " and options " + michael@0: JSON.stringify(options) + " is " + actual + "; expected " + expected + "."); michael@0: } michael@0: }); michael@0: } michael@0: }); michael@0: }); michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Return the components of date-time formats. michael@0: * @return {Array} an array with all date-time components. michael@0: */ michael@0: michael@0: function getDateTimeComponents() { michael@0: return ["weekday", "era", "year", "month", "day", "hour", "minute", "second", "timeZoneName"]; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Return the valid values for the given date-time component, as specified michael@0: * by the table in section 12.1.1. michael@0: * @param {string} component a date-time component. michael@0: * @return {Array} an array with the valid values for the component. michael@0: */ michael@0: michael@0: function getDateTimeComponentValues(component) { michael@0: michael@0: var components = { michael@0: weekday: ["narrow", "short", "long"], michael@0: era: ["narrow", "short", "long"], michael@0: year: ["2-digit", "numeric"], michael@0: month: ["2-digit", "numeric", "narrow", "short", "long"], michael@0: day: ["2-digit", "numeric"], michael@0: hour: ["2-digit", "numeric"], michael@0: minute: ["2-digit", "numeric"], michael@0: second: ["2-digit", "numeric"], michael@0: timeZoneName: ["short", "long"] michael@0: }; michael@0: michael@0: var result = components[component]; michael@0: if (result === undefined) { michael@0: $ERROR("Internal error: No values defined for date-time component " + component + "."); michael@0: } michael@0: return result; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Tests that the given value is valid for the given date-time component. michael@0: * @param {string} component a date-time component. michael@0: * @param {string} value the value to be tested. michael@0: * @return {boolean} true if the test succeeds. michael@0: * @exception if the test fails. michael@0: */ michael@0: michael@0: function testValidDateTimeComponentValue(component, value) { michael@0: if (getDateTimeComponentValues(component).indexOf(value) === -1) { michael@0: $ERROR("Invalid value " + value + " for date-time component " + component + "."); michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Verifies that the actual array matches the expected one in length, elements, michael@0: * and element order. michael@0: * @param {Array} expected the expected array. michael@0: * @param {Array} actual the actual array. michael@0: * @return {boolean} true if the test succeeds. michael@0: * @exception if the test fails. michael@0: */ michael@0: function testArraysAreSame(expected, actual) { michael@0: for (i = 0; i < Math.max(actual.length, expected.length); i++) { michael@0: if (actual[i] !== expected[i]) { michael@0: $ERROR("Result array element at index " + i + " should be \"" + michael@0: expected[i] + "\" but is \"" + actual[i] + "\"."); michael@0: } michael@0: } michael@0: return true; michael@0: } michael@0: