// Copyright (C) 2011 2012 Norbert Lindenberg. All rights reserved. // Copyright (C) 2012 2013 Mozilla Corporation. All rights reserved. // This code is governed by the BSD license found in the LICENSE file. /*--- description: | This file contains shared functions for the tests in the conformance test suite for the ECMAScript Internationalization API. author: Norbert Lindenberg ---*/ /** */ /** * @description Calls the provided function for every service constructor in * the Intl object. * @param {Function} f the function to call for each service constructor in * the Intl object. * @param {Function} Constructor the constructor object to test with. */ function testWithIntlConstructors(f) { var constructors = ["Collator", "NumberFormat", "DateTimeFormat"]; // Optionally supported Intl constructors. ["PluralRules"].forEach(function(constructor) { if (typeof Intl[constructor] === "function") { constructors[constructors.length] = constructor; } }); constructors.forEach(function (constructor) { var Constructor = Intl[constructor]; try { f(Constructor); } catch (e) { e.message += " (Testing with " + constructor + ".)"; throw e; } }); } /** * Taints a named data property of the given object by installing * a setter that throws an exception. * @param {object} obj the object whose data property to taint * @param {string} property the property to taint */ function taintDataProperty(obj, property) { Object.defineProperty(obj, property, { set: function(value) { $ERROR("Client code can adversely affect behavior: setter for " + property + "."); }, enumerable: false, configurable: true }); } /** * Taints a named method of the given object by replacing it with a function * that throws an exception. * @param {object} obj the object whose method to taint * @param {string} property the name of the method to taint */ function taintMethod(obj, property) { Object.defineProperty(obj, property, { value: function() { $ERROR("Client code can adversely affect behavior: method " + property + "."); }, writable: true, enumerable: false, configurable: true }); } /** * Taints the given properties (and similarly named properties) by installing * setters on Object.prototype that throw exceptions. * @param {Array} properties an array of property names to taint */ function taintProperties(properties) { properties.forEach(function (property) { var adaptedProperties = [property, "__" + property, "_" + property, property + "_", property + "__"]; adaptedProperties.forEach(function (property) { taintDataProperty(Object.prototype, property); }); }); } /** * Taints the Array object by creating a setter for the property "0" and * replacing some key methods with functions that throw exceptions. */ function taintArray() { taintDataProperty(Array.prototype, "0"); taintMethod(Array.prototype, "indexOf"); taintMethod(Array.prototype, "join"); taintMethod(Array.prototype, "push"); taintMethod(Array.prototype, "slice"); taintMethod(Array.prototype, "sort"); } /** * Gets locale support info for the given constructor object, which must be one * of Intl constructors. * @param {object} Constructor the constructor for which to get locale support info * @return {object} locale support info with the following properties: * supported: array of fully supported language tags * byFallback: array of language tags that are supported through fallbacks * unsupported: array of unsupported language tags */ function getLocaleSupportInfo(Constructor) { var languages = ["zh", "es", "en", "hi", "ur", "ar", "ja", "pa"]; var scripts = ["Latn", "Hans", "Deva", "Arab", "Jpan", "Hant", "Guru"]; var countries = ["CN", "IN", "US", "PK", "JP", "TW", "HK", "SG", "419"]; var allTags = []; var i, j, k; var language, script, country; for (i = 0; i < languages.length; i++) { language = languages[i]; allTags.push(language); for (j = 0; j < scripts.length; j++) { script = scripts[j]; allTags.push(language + "-" + script); for (k = 0; k < countries.length; k++) { country = countries[k]; allTags.push(language + "-" + script + "-" + country); } } for (k = 0; k < countries.length; k++) { country = countries[k]; allTags.push(language + "-" + country); } } var supported = []; var byFallback = []; var unsupported = []; for (i = 0; i < allTags.length; i++) { var request = allTags[i]; var result = new Constructor([request], {localeMatcher: "lookup"}).resolvedOptions().locale; if (request === result) { supported.push(request); } else if (request.indexOf(result) === 0) { byFallback.push(request); } else { unsupported.push(request); } } return { supported: supported, byFallback: byFallback, unsupported: unsupported }; } /** * Returns an array of strings for which IsStructurallyValidLanguageTag() returns false */ function getInvalidLanguageTags() { var invalidLanguageTags = [ "", // empty tag "i", // singleton alone "x", // private use without subtag "u", // extension singleton in first place "419", // region code in first place "u-nu-latn-cu-bob", // extension sequence without language "hans-cmn-cn", // "hans" could theoretically be a 4-letter language code, // but those can't be followed by extlang codes. "cmn-hans-cn-u-u", // duplicate singleton "cmn-hans-cn-t-u-ca-u", // duplicate singleton "de-gregory-gregory", // duplicate variant "*", // language range "de-*", // language range "中文", // non-ASCII letters "en-ß", // non-ASCII letters "ıd", // non-ASCII letters "es-Latn-latn", // two scripts "pl-PL-pl", // two regions "u-ca-gregory", // extension in first place "de-1996-1996", // duplicate numeric variant "pt-u-ca-gregory-u-nu-latn", // duplicate singleton subtag // Invalid tags starting with: https://github.com/tc39/ecma402/pull/289 "no-nyn", // regular grandfathered in BCP47, but invalid in UTS35 "i-klingon", // irregular grandfathered in BCP47, but invalid in UTS35 "zh-hak-CN", // language with extlang in BCP47, but invalid in UTS35 "sgn-ils", // language with extlang in BCP47, but invalid in UTS35 "x-foo", // privateuse-only in BCP47, but invalid in UTS35 "x-en-US-12345", // more privateuse-only variants. "x-12345-12345-en-US", "x-en-US-12345-12345", "x-en-u-foo", "x-en-u-foo-u-bar", "x-u-foo", // underscores in different parts of the language tag "de_DE", "DE_de", "cmn_Hans", "cmn-hans_cn", "es_419", "es-419-u-nu-latn-cu_bob", "i_klingon", "cmn-hans-cn-t-ca-u-ca-x_t-u", "enochian_enochian", "de-gregory_u-ca-gregory", "en\u0000", // null-terminator sequence " en", // leading whitespace "en ", // trailing whitespace "it-IT-Latn", // country before script tag "de-u", // incomplete Unicode extension sequences "de-u-", "de-u-ca-", "de-u-ca-gregory-", "si-x", // incomplete private-use tags "x-", "x-y-", ]; // make sure the data above is correct for (var i = 0; i < invalidLanguageTags.length; ++i) { var invalidTag = invalidLanguageTags[i]; assert( !isCanonicalizedStructurallyValidLanguageTag(invalidTag), "Test data \"" + invalidTag + "\" is a canonicalized and structurally valid language tag." ); } return invalidLanguageTags; } /** * @description Tests whether locale is a String value representing a * structurally valid and canonicalized BCP 47 language tag, as defined in * sections 6.2.2 and 6.2.3 of the ECMAScript Internationalization API * Specification. * @param {String} locale the string to be tested. * @result {Boolean} whether the test succeeded. */ function isCanonicalizedStructurallyValidLanguageTag(locale) { /** * Regular expression defining Unicode BCP 47 Locale Identifiers. * * Spec: https://unicode.org/reports/tr35/#Unicode_locale_identifier */ var alpha = "[a-z]", digit = "[0-9]", alphanum = "(" + alpha + "|" + digit + ")", variant = "(" + alphanum + "{5,8}|(" + digit + alphanum + "{3}))", region = "(" + alpha + "{2}|" + digit + "{3})", script = "(" + alpha + "{4})", language = "(" + alpha + "{2,3}|" + alpha + "{5,8})", privateuse = "(x(-[a-z0-9]{1,8})+)", singleton = "(" + digit + "|[a-wy-z])", attribute= "(" + alphanum + "{3,8})", keyword = "(" + alphanum + alpha + "(-" + alphanum + "{3,8})*)", unicode_locale_extensions = "(u((-" + keyword + ")+|((-" + attribute + ")+(-" + keyword + ")*)))", tlang = "(" + language + "(-" + script + ")?(-" + region + ")?(-" + variant + ")*)", tfield = "(" + alpha + digit + "(-" + alphanum + "{3,8})+)", transformed_extensions = "(t((-" + tlang + "(-" + tfield + ")*)|(-" + tfield + ")+))", other_singleton = "(" + digit + "|[a-sv-wy-z])", other_extensions = "(" + other_singleton + "(-" + alphanum + "{2,8})+)", extension = "(" + unicode_locale_extensions + "|" + transformed_extensions + "|" + other_extensions + ")", locale_id = language + "(-" + script + ")?(-" + region + ")?(-" + variant + ")*(-" + extension + ")*(-" + privateuse + ")?", languageTag = "^(" + locale_id + ")$", languageTagRE = new RegExp(languageTag, "i"); var duplicateSingleton = "-" + singleton + "-(.*-)?\\1(?!" + alphanum + ")", duplicateSingletonRE = new RegExp(duplicateSingleton, "i"), duplicateVariant = "(" + alphanum + "{2,8}-)+" + variant + "-(" + alphanum + "{2,8}-)*\\3(?!" + alphanum + ")", duplicateVariantRE = new RegExp(duplicateVariant, "i"); /** * Verifies that the given string is a well-formed Unicode BCP 47 Locale Identifier * with no duplicate variant or singleton subtags. * * Spec: ECMAScript Internationalization API Specification, draft, 6.2.2. */ function isStructurallyValidLanguageTag(locale) { if (!languageTagRE.test(locale)) { return false; } locale = locale.split(/-x-/)[0]; return !duplicateSingletonRE.test(locale) && !duplicateVariantRE.test(locale); } /** * Mappings from complete tags to preferred values. * * Spec: http://unicode.org/reports/tr35/#Identifiers * Version: CLDR, version 35 */ var __tagMappings = { // property names must be in lower case; values in canonical form "art-lojban": "jbo", "cel-gaulish": "xtg-x-cel-gaulish", "zh-guoyu": "zh", "zh-hakka": "hak", "zh-xiang": "hsn", }; /** * Mappings from language subtags to preferred values. * * Spec: http://unicode.org/reports/tr35/#Identifiers * Version: CLDR, version 35 */ var __languageMappings = { // property names and values must be in canonical case "aam": "aas", "aar": "aa", "abk": "ab", "adp": "dz", "afr": "af", "aju": "jrb", "aka": "ak", "alb": "sq", "als": "sq", "amh": "am", "ara": "ar", "arb": "ar", "arg": "an", "arm": "hy", "asm": "as", "aue": "ktz", "ava": "av", "ave": "ae", "aym": "ay", "ayr": "ay", "ayx": "nun", "aze": "az", "azj": "az", "bak": "ba", "bam": "bm", "baq": "eu", "bcc": "bal", "bcl": "bik", "bel": "be", "ben": "bn", "bgm": "bcg", "bh": "bho", "bih": "bho", "bis": "bi", "bjd": "drl", "bod": "bo", "bos": "bs", "bre": "br", "bul": "bg", "bur": "my", "bxk": "luy", "bxr": "bua", "cat": "ca", "ccq": "rki", "ces": "cs", "cha": "ch", "che": "ce", "chi": "zh", "chu": "cu", "chv": "cv", "cjr": "mom", "cka": "cmr", "cld": "syr", "cmk": "xch", "cmn": "zh", "cor": "kw", "cos": "co", "coy": "pij", "cqu": "quh", "cre": "cr", "cwd": "cr", "cym": "cy", "cze": "cs", "dan": "da", "deu": "de", "dgo": "doi", "dhd": "mwr", "dik": "din", "diq": "zza", "div": "dv", "drh": "mn", "dut": "nl", "dzo": "dz", "ekk": "et", "ell": "el", "emk": "man", "eng": "en", "epo": "eo", "esk": "ik", "est": "et", "eus": "eu", "ewe": "ee", "fao": "fo", "fas": "fa", "fat": "ak", "fij": "fj", "fin": "fi", "fra": "fr", "fre": "fr", "fry": "fy", "fuc": "ff", "ful": "ff", "gav": "dev", "gaz": "om", "gbo": "grb", "geo": "ka", "ger": "de", "gfx": "vaj", "ggn": "gvr", "gla": "gd", "gle": "ga", "glg": "gl", "glv": "gv", "gno": "gon", "gre": "el", "grn": "gn", "gti": "nyc", "gug": "gn", "guj": "gu", "guv": "duz", "gya": "gba", "hat": "ht", "hau": "ha", "hdn": "hai", "hea": "hmn", "heb": "he", "her": "hz", "him": "srx", "hin": "hi", "hmo": "ho", "hrr": "jal", "hrv": "hr", "hun": "hu", "hye": "hy", "ibi": "opa", "ibo": "ig", "ice": "is", "ido": "io", "iii": "ii", "ike": "iu", "iku": "iu", "ile": "ie", "ilw": "gal", "in": "id", "ina": "ia", "ind": "id", "ipk": "ik", "isl": "is", "ita": "it", "iw": "he", "jav": "jv", "jeg": "oyb", "ji": "yi", "jpn": "ja", "jw": "jv", "kal": "kl", "kan": "kn", "kas": "ks", "kat": "ka", "kau": "kr", "kaz": "kk", "kgc": "tdf", "kgh": "kml", "khk": "mn", "khm": "km", "kik": "ki", "kin": "rw", "kir": "ky", "kmr": "ku", "knc": "kr", "kng": "kg", "knn": "kok", "koj": "kwv", "kom": "kv", "kon": "kg", "kor": "ko", "kpv": "kv", "krm": "bmf", "ktr": "dtp", "kua": "kj", "kur": "ku", "kvs": "gdj", "kwq": "yam", "kxe": "tvd", "kzj": "dtp", "kzt": "dtp", "lao": "lo", "lat": "la", "lav": "lv", "lbk": "bnc", "lii": "raq", "lim": "li", "lin": "ln", "lit": "lt", "lmm": "rmx", "ltz": "lb", "lub": "lu", "lug": "lg", "lvs": "lv", "mac": "mk", "mah": "mh", "mal": "ml", "mao": "mi", "mar": "mr", "may": "ms", "meg": "cir", "mhr": "chm", "mkd": "mk", "mlg": "mg", "mlt": "mt", "mnk": "man", "mo": "ro", "mol": "ro", "mon": "mn", "mri": "mi", "msa": "ms", "mst": "mry", "mup": "raj", "mwj": "vaj", "mya": "my", "myt": "mry", "nad": "xny", "nau": "na", "nav": "nv", "nbl": "nr", "ncp": "kdz", "nde": "nd", "ndo": "ng", "nep": "ne", "nld": "nl", "nno": "nn", "nnx": "ngv", "no": "nb", "nob": "nb", "nor": "nb", "npi": "ne", "nts": "pij", "nya": "ny", "oci": "oc", "ojg": "oj", "oji": "oj", "ori": "or", "orm": "om", "ory": "or", "oss": "os", "oun": "vaj", "pan": "pa", "pbu": "ps", "pcr": "adx", "per": "fa", "pes": "fa", "pli": "pi", "plt": "mg", "pmc": "huw", "pmu": "phr", "pnb": "lah", "pol": "pl", "por": "pt", "ppa": "bfy", "ppr": "lcq", "pry": "prt", "pus": "ps", "puz": "pub", "que": "qu", "quz": "qu", "rmy": "rom", "roh": "rm", "ron": "ro", "rum": "ro", "run": "rn", "rus": "ru", "sag": "sg", "san": "sa", "sca": "hle", "scc": "sr", "scr": "hr", "sin": "si", "skk": "oyb", "slk": "sk", "slo": "sk", "slv": "sl", "sme": "se", "smo": "sm", "sna": "sn", "snd": "sd", "som": "so", "sot": "st", "spa": "es", "spy": "kln", "sqi": "sq", "src": "sc", "srd": "sc", "srp": "sr", "ssw": "ss", "sun": "su", "swa": "sw", "swe": "sv", "swh": "sw", "tah": "ty", "tam": "ta", "tat": "tt", "tdu": "dtp", "tel": "te", "tgk": "tg", "tgl": "fil", "tha": "th", "thc": "tpo", "thx": "oyb", "tib": "bo", "tie": "ras", "tir": "ti", "tkk": "twm", "tl": "fil", "tlw": "weo", "tmp": "tyj", "tne": "kak", "ton": "to", "tsf": "taj", "tsn": "tn", "tso": "ts", "ttq": "tmh", "tuk": "tk", "tur": "tr", "tw": "ak", "twi": "ak", "uig": "ug", "ukr": "uk", "umu": "del", "uok": "ema", "urd": "ur", "uzb": "uz", "uzn": "uz", "ven": "ve", "vie": "vi", "vol": "vo", "wel": "cy", "wln": "wa", "wol": "wo", "xba": "cax", "xho": "xh", "xia": "acn", "xkh": "waw", "xpe": "kpe", "xsj": "suj", "xsl": "den", "ybd": "rki", "ydd": "yi", "yid": "yi", "yma": "lrr", "ymt": "mtm", "yor": "yo", "yos": "zom", "yuu": "yug", "zai": "zap", "zha": "za", "zho": "zh", "zsm": "ms", "zul": "zu", "zyb": "za", } /** * Mappings from region subtags to preferred values. * * Spec: http://unicode.org/reports/tr35/#Identifiers * Version: CLDR, version 35 */ var __regionMappings = { // property names and values must be in canonical case "004": "AF", "008": "AL", "010": "AQ", "012": "DZ", "016": "AS", "020": "AD", "024": "AO", "028": "AG", "031": "AZ", "032": "AR", "036": "AU", "040": "AT", "044": "BS", "048": "BH", "050": "BD", "051": "AM", "052": "BB", "056": "BE", "060": "BM", "062": "034", "064": "BT", "068": "BO", "070": "BA", "072": "BW", "074": "BV", "076": "BR", "084": "BZ", "086": "IO", "090": "SB", "092": "VG", "096": "BN", "100": "BG", "104": "MM", "108": "BI", "112": "BY", "116": "KH", "120": "CM", "124": "CA", "132": "CV", "136": "KY", "140": "CF", "144": "LK", "148": "TD", "152": "CL", "156": "CN", "158": "TW", "162": "CX", "166": "CC", "170": "CO", "174": "KM", "175": "YT", "178": "CG", "180": "CD", "184": "CK", "188": "CR", "191": "HR", "192": "CU", "196": "CY", "203": "CZ", "204": "BJ", "208": "DK", "212": "DM", "214": "DO", "218": "EC", "222": "SV", "226": "GQ", "230": "ET", "231": "ET", "232": "ER", "233": "EE", "234": "FO", "238": "FK", "239": "GS", "242": "FJ", "246": "FI", "248": "AX", "249": "FR", "250": "FR", "254": "GF", "258": "PF", "260": "TF", "262": "DJ", "266": "GA", "268": "GE", "270": "GM", "275": "PS", "276": "DE", "278": "DE", "280": "DE", "288": "GH", "292": "GI", "296": "KI", "300": "GR", "304": "GL", "308": "GD", "312": "GP", "316": "GU", "320": "GT", "324": "GN", "328": "GY", "332": "HT", "334": "HM", "336": "VA", "340": "HN", "344": "HK", "348": "HU", "352": "IS", "356": "IN", "360": "ID", "364": "IR", "368": "IQ", "372": "IE", "376": "IL", "380": "IT", "384": "CI", "388": "JM", "392": "JP", "398": "KZ", "400": "JO", "404": "KE", "408": "KP", "410": "KR", "414": "KW", "417": "KG", "418": "LA", "422": "LB", "426": "LS", "428": "LV", "430": "LR", "434": "LY", "438": "LI", "440": "LT", "442": "LU", "446": "MO", "450": "MG", "454": "MW", "458": "MY", "462": "MV", "466": "ML", "470": "MT", "474": "MQ", "478": "MR", "480": "MU", "484": "MX", "492": "MC", "496": "MN", "498": "MD", "499": "ME", "500": "MS", "504": "MA", "508": "MZ", "512": "OM", "516": "NA", "520": "NR", "524": "NP", "528": "NL", "531": "CW", "533": "AW", "534": "SX", "535": "BQ", "540": "NC", "548": "VU", "554": "NZ", "558": "NI", "562": "NE", "566": "NG", "570": "NU", "574": "NF", "578": "NO", "580": "MP", "581": "UM", "583": "FM", "584": "MH", "585": "PW", "586": "PK", "591": "PA", "598": "PG", "600": "PY", "604": "PE", "608": "PH", "612": "PN", "616": "PL", "620": "PT", "624": "GW", "626": "TL", "630": "PR", "634": "QA", "638": "RE", "642": "RO", "643": "RU", "646": "RW", "652": "BL", "654": "SH", "659": "KN", "660": "AI", "662": "LC", "663": "MF", "666": "PM", "670": "VC", "674": "SM", "678": "ST", "682": "SA", "686": "SN", "688": "RS", "690": "SC", "694": "SL", "702": "SG", "703": "SK", "704": "VN", "705": "SI", "706": "SO", "710": "ZA", "716": "ZW", "720": "YE", "724": "ES", "728": "SS", "729": "SD", "732": "EH", "736": "SD", "740": "SR", "744": "SJ", "748": "SZ", "752": "SE", "756": "CH", "760": "SY", "762": "TJ", "764": "TH", "768": "TG", "772": "TK", "776": "TO", "780": "TT", "784": "AE", "788": "TN", "792": "TR", "795": "TM", "796": "TC", "798": "TV", "800": "UG", "804": "UA", "807": "MK", "818": "EG", "826": "GB", "830": "JE", "831": "GG", "832": "JE", "833": "IM", "834": "TZ", "840": "US", "850": "VI", "854": "BF", "858": "UY", "860": "UZ", "862": "VE", "876": "WF", "882": "WS", "886": "YE", "887": "YE", "891": "RS", "894": "ZM", "958": "AA", "959": "QM", "960": "QN", "962": "QP", "963": "QQ", "964": "QR", "965": "QS", "966": "QT", "967": "EU", "968": "QV", "969": "QW", "970": "QX", "971": "QY", "972": "QZ", "973": "XA", "974": "XB", "975": "XC", "976": "XD", "977": "XE", "978": "XF", "979": "XG", "980": "XH", "981": "XI", "982": "XJ", "983": "XK", "984": "XL", "985": "XM", "986": "XN", "987": "XO", "988": "XP", "989": "XQ", "990": "XR", "991": "XS", "992": "XT", "993": "XU", "994": "XV", "995": "XW", "996": "XX", "997": "XY", "998": "XZ", "999": "ZZ", "BU": "MM", "CS": "RS", "CT": "KI", "DD": "DE", "DY": "BJ", "FQ": "AQ", "FX": "FR", "HV": "BF", "JT": "UM", "MI": "UM", "NH": "VU", "NQ": "AQ", "PU": "UM", "PZ": "PA", "QU": "EU", "RH": "ZW", "TP": "TL", "UK": "GB", "VD": "VN", "WK": "UM", "YD": "YE", "YU": "RS", "ZR": "CD", }; /** * Canonicalizes the given well-formed BCP 47 language tag, including regularized case of subtags. * * Spec: ECMAScript Internationalization API Specification, draft, 6.2.3. * Spec: RFC 5646, section 4.5. */ function canonicalizeLanguageTag(locale) { // start with lower case for easier processing, and because most subtags will need to be lower case anyway locale = locale.toLowerCase(); // handle mappings for complete tags if (__tagMappings.hasOwnProperty(locale)) { return __tagMappings[locale]; } var subtags = locale.split("-"); var i = 0; // handle standard part: all subtags before first variant or singleton subtag var language; var script; var region; while (i < subtags.length) { var subtag = subtags[i]; if (i === 0) { language = subtag; } else if (subtag.length === 2 || subtag.length === 3) { region = subtag.toUpperCase(); } else if (subtag.length === 4 && !("0" <= subtag[0] && subtag[0] <= "9")) { script = subtag[0].toUpperCase() + subtag.substring(1).toLowerCase(); } else { break; } i++; } if (__languageMappings.hasOwnProperty(language)) { language = __languageMappings[language]; } else { // Language subtags with complex mappings, CLDR 35. switch (language) { case "cnr": language = "sr"; if (region === undefined) { region = "ME"; } break; case "drw": case "prs": case "tnf": language = "fa"; if (region === undefined) { region = "AF"; } break; case "hbs": case "sh": language = "sr"; if (script === undefined) { script = "Latn"; } break; case "swc": language = "sw"; if (region === undefined) { region = "CD"; } break; } } if (region !== undefined) { if (__regionMappings.hasOwnProperty(region)) { region = __regionMappings[region]; } else { // Region subtags with complex mappings, CLDR 35. switch (region) { case "172": if (language === "ab" || language === "ka" || language === "os" || (language === "und" && script === "Geor") || language === "xmf") { region = "GE"; } else if (language === "az" || language === "tkr" || language === "tly" || language === "ttt") { region = "AZ"; } else if (language === "be") { region = "BY"; } else if (language === "crh" || language === "got" || language === "ji" || language === "rue" || language === "uk" || (language === "und" && script === "Goth")) { region = "UA"; } else if (language === "gag") { region = "MD"; } else if (language === "hy" || (language === "und" && script === "Armn")) { region = "AM"; } else if (language === "kaa" || language === "sog" || (language === "und" && script === "Sogd") || (language === "und" && script === "Sogo") || language === "uz") { region = "UZ"; } else if (language === "kk" || (language === "ug" && script === "Cyrl")) { region = "KZ"; } else if (language === "ky") { region = "KG"; } else if (language === "tg") { region = "TJ"; } else if (language === "tk") { region = "TM"; } else { region = "RU"; } break; case "200": if (language === "sk") { region = "SK"; } else { region = "CZ"; } break; case "530": case "532": case "AN": if (language === "vic") { region = "SX"; } else { region = "CW"; } break; case "536": case "NT": if (language === "akk" || language === "ckb" || (language === "ku" && script === "Arab") || language === "mis" || language === "syr" || (language === "und" && script === "Xsux") || (language === "und" && script === "Hatr") || (language === "und" && script === "Syrc")) { region = "IQ"; } else { region = "SA"; } break; case "582": case "PC": if (language === "mh") { region = "MH"; } else if (language === "pau") { region = "PW"; } else { region = "FM"; } break; case "810": case "SU": if (language === "ab" || language === "ka" || language === "os" || language === "xmf" || (language === "und" && script === "Geor")) { region = "GE"; } else if (language === "az" || language === "tkr" || language === "tly" || language === "ttt") { region = "AZ"; } else if (language === "be") { region = "BY"; } else if (language === "crh" || language === "got" || language === "ji" || language === "rue" || language === "uk" || (language === "und" && script === "Goth")) { region = "UA"; } else if (language === "et" || language === "vro") { region = "EE"; } else if (language === "gag") { region = "MD"; } else if (language === "hy" || (language === "und" && script === "Armn")) { region = "AM"; } else if (language === "kaa" || language === "sog" || (language === "und" && script === "Sogd") || (language === "und" && script === "Sogo") || language === "uz") { region = "UZ"; } else if (language === "kk" || (language === "ug" && script === "Cyrl")) { region = "KZ"; } else if (language === "ky") { region = "KG"; } else if (language === "lt" || language === "sgs") { region = "LT"; } else if (language === "ltg" || language === "lv") { region = "LV"; } else if (language === "tg") { region = "TJ"; } else if (language === "tk") { region = "TM"; } else { region = "RU"; } break; case "890": if (language === "bs") { region = "BA"; } else if (language === "hr") { region = "HR"; } else if (language === "mk") { region = "MK"; } else if (language === "sl") { region = "SI"; } else { region = "RS"; } break; } } } // handle variants var variants = []; while (i < subtags.length && subtags[i].length > 1) { variants.push(subtags[i]); i += 1; } variants.sort(); // handle extensions var extensions = []; while (i < subtags.length && subtags[i] !== "x") { var extensionStart = i; i++; while (i < subtags.length && subtags[i].length > 1) { i++; } var extension = subtags.slice(extensionStart, i).join("-"); extensions.push(extension); } extensions.sort(); // handle private use var privateUse; if (i < subtags.length) { privateUse = subtags.slice(i).join("-"); } // put everything back together var canonical = language; if (script !== undefined) { canonical += "-" + script; } if (region !== undefined) { canonical += "-" + region; } if (variants.length > 0) { canonical += "-" + variants.join("-"); } if (extensions.length > 0) { canonical += "-" + extensions.join("-"); } if (privateUse !== undefined) { if (canonical.length > 0) { canonical += "-" + privateUse; } else { canonical = privateUse; } } return canonical; } return typeof locale === "string" && isStructurallyValidLanguageTag(locale) && canonicalizeLanguageTag(locale) === locale; } /** * Returns an array of error cases handled by CanonicalizeLocaleList(). */ function getInvalidLocaleArguments() { function CustomError() {} var topLevelErrors = [ // fails ToObject [null, TypeError], // fails Get [{ get length() { throw new CustomError(); } }, CustomError], // fail ToLength [{ length: Symbol.toPrimitive }, TypeError], [{ length: { get [Symbol.toPrimitive]() { throw new CustomError(); } } }, CustomError], [{ length: { [Symbol.toPrimitive]() { throw new CustomError(); } } }, CustomError], [{ length: { get valueOf() { throw new CustomError(); } } }, CustomError], [{ length: { valueOf() { throw new CustomError(); } } }, CustomError], [{ length: { get toString() { throw new CustomError(); } } }, CustomError], [{ length: { toString() { throw new CustomError(); } } }, CustomError], // fail type check [[undefined], TypeError], [[null], TypeError], [[true], TypeError], [[Symbol.toPrimitive], TypeError], [[1], TypeError], [[0.1], TypeError], [[NaN], TypeError], ]; var invalidLanguageTags = [ "", // empty tag "i", // singleton alone "x", // private use without subtag "u", // extension singleton in first place "419", // region code in first place "u-nu-latn-cu-bob", // extension sequence without language "hans-cmn-cn", // "hans" could theoretically be a 4-letter language code, // but those can't be followed by extlang codes. "abcdefghi", // overlong language "cmn-hans-cn-u-u", // duplicate singleton "cmn-hans-cn-t-u-ca-u", // duplicate singleton "de-gregory-gregory", // duplicate variant "*", // language range "de-*", // language range "中文", // non-ASCII letters "en-ß", // non-ASCII letters "ıd" // non-ASCII letters ]; return topLevelErrors.concat( invalidLanguageTags.map(tag => [tag, RangeError]), invalidLanguageTags.map(tag => [[tag], RangeError]), invalidLanguageTags.map(tag => [["en", tag], RangeError]), ) } /** * Tests whether the named options property is correctly handled by the given constructor. * @param {object} Constructor the constructor to test. * @param {string} property the name of the options property to test. * @param {string} type the type that values of the property are expected to have * @param {Array} [values] an array of allowed values for the property. Not needed for boolean. * @param {any} fallback the fallback value that the property assumes if not provided. * @param {object} testOptions additional options: * @param {boolean} isOptional whether support for this property is optional for implementations. * @param {boolean} noReturn whether the resulting value of the property is not returned. * @param {boolean} isILD whether the resulting value of the property is implementation and locale dependent. * @param {object} extra additional option to pass along, properties are value -> {option: value}. */ function testOption(Constructor, property, type, values, fallback, testOptions) { var isOptional = testOptions !== undefined && testOptions.isOptional === true; var noReturn = testOptions !== undefined && testOptions.noReturn === true; var isILD = testOptions !== undefined && testOptions.isILD === true; function addExtraOptions(options, value, testOptions) { if (testOptions !== undefined && testOptions.extra !== undefined) { var extra; if (value !== undefined && testOptions.extra[value] !== undefined) { extra = testOptions.extra[value]; } else if (testOptions.extra.any !== undefined) { extra = testOptions.extra.any; } if (extra !== undefined) { Object.getOwnPropertyNames(extra).forEach(function (prop) { options[prop] = extra[prop]; }); } } } var testValues, options, obj, expected, actual, error; // test that the specified values are accepted. Also add values that convert to specified values. if (type === "boolean") { if (values === undefined) { values = [true, false]; } testValues = values.slice(0); testValues.push(888); testValues.push(0); } else if (type === "string") { testValues = values.slice(0); testValues.push({toString: function () { return values[0]; }}); } testValues.forEach(function (value) { options = {}; options[property] = value; addExtraOptions(options, value, testOptions); obj = new Constructor(undefined, options); if (noReturn) { if (obj.resolvedOptions().hasOwnProperty(property)) { $ERROR("Option property " + property + " is returned, but shouldn't be."); } } else { actual = obj.resolvedOptions()[property]; if (isILD) { if (actual !== undefined && values.indexOf(actual) === -1) { $ERROR("Invalid value " + actual + " returned for property " + property + "."); } } else { if (type === "boolean") { expected = Boolean(value); } else if (type === "string") { expected = String(value); } if (actual !== expected && !(isOptional && actual === undefined)) { $ERROR("Option value " + value + " for property " + property + " was not accepted; got " + actual + " instead."); } } } }); // test that invalid values are rejected if (type === "string") { var invalidValues = ["invalidValue", -1, null]; // assume that we won't have values in caseless scripts if (values[0].toUpperCase() !== values[0]) { invalidValues.push(values[0].toUpperCase()); } else { invalidValues.push(values[0].toLowerCase()); } invalidValues.forEach(function (value) { options = {}; options[property] = value; addExtraOptions(options, value, testOptions); error = undefined; try { obj = new Constructor(undefined, options); } catch (e) { error = e; } if (error === undefined) { $ERROR("Invalid option value " + value + " for property " + property + " was not rejected."); } else if (error.name !== "RangeError") { $ERROR("Invalid option value " + value + " for property " + property + " was rejected with wrong error " + error.name + "."); } }); } // test that fallback value or another valid value is used if no options value is provided if (!noReturn) { options = {}; addExtraOptions(options, undefined, testOptions); obj = new Constructor(undefined, options); actual = obj.resolvedOptions()[property]; if (!(isOptional && actual === undefined)) { if (fallback !== undefined) { if (actual !== fallback) { $ERROR("Option fallback value " + fallback + " for property " + property + " was not used; got " + actual + " instead."); } } else { if (values.indexOf(actual) === -1 && !(isILD && actual === undefined)) { $ERROR("Invalid value " + actual + " returned for property " + property + "."); } } } } } /** * Properties of the RegExp constructor that may be affected by use of regular * expressions, and the default values of these properties. Properties are from * https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Deprecated_and_obsolete_features#RegExp_Properties */ var regExpProperties = ["$1", "$2", "$3", "$4", "$5", "$6", "$7", "$8", "$9", "$_", "$*", "$&", "$+", "$`", "$'", "input", "lastMatch", "lastParen", "leftContext", "rightContext" ]; var regExpPropertiesDefaultValues = (function () { var values = Object.create(null); regExpProperties.forEach(function (property) { values[property] = RegExp[property]; }); return values; }()); /** * Tests that executing the provided function (which may use regular expressions * in its implementation) does not create or modify unwanted properties on the * RegExp constructor. */ function testForUnwantedRegExpChanges(testFunc) { regExpProperties.forEach(function (property) { RegExp[property] = regExpPropertiesDefaultValues[property]; }); testFunc(); regExpProperties.forEach(function (property) { if (RegExp[property] !== regExpPropertiesDefaultValues[property]) { $ERROR("RegExp has unexpected property " + property + " with value " + RegExp[property] + "."); } }); } /** * Tests whether name is a valid BCP 47 numbering system name * and not excluded from use in the ECMAScript Internationalization API. * @param {string} name the name to be tested. * @return {boolean} whether name is a valid BCP 47 numbering system name and * allowed for use in the ECMAScript Internationalization API. */ function isValidNumberingSystem(name) { // source: CLDR file common/bcp47/number.xml; version CLDR 32. var numberingSystems = [ "adlm", "ahom", "arab", "arabext", "armn", "armnlow", "bali", "beng", "bhks", "brah", "cakm", "cham", "cyrl", "deva", "ethi", "finance", "fullwide", "geor", "gonm", "grek", "greklow", "gujr", "guru", "hanidays", "hanidec", "hans", "hansfin", "hant", "hantfin", "hebr", "hmng", "java", "jpan", "jpanfin", "kali", "khmr", "knda", "lana", "lanatham", "laoo", "latn", "lepc", "limb", "mathbold", "mathdbl", "mathmono", "mathsanb", "mathsans", "mlym", "modi", "mong", "mroo", "mtei", "mymr", "mymrshan", "mymrtlng", "native", "newa", "nkoo", "olck", "orya", "osma", "roman", "romanlow", "saur", "shrd", "sind", "sinh", "sora", "sund", "takr", "talu", "taml", "tamldec", "telu", "thai", "tirh", "tibt", "traditio", "vaii", "wara", ]; var excluded = [ "finance", "native", "traditio" ]; return numberingSystems.indexOf(name) !== -1 && excluded.indexOf(name) === -1; } /** * Provides the digits of numbering systems with simple digit mappings, * as specified in 11.3.2. */ var numberingSystemDigits = { arab: "٠١٢٣٤٥٦٧٨٩", arabext: "۰۱۲۳۴۵۶۷۸۹", bali: "\u1B50\u1B51\u1B52\u1B53\u1B54\u1B55\u1B56\u1B57\u1B58\u1B59", beng: "০১২৩৪৫৬৭৮৯", deva: "०१२३४५६७८९", fullwide: "0123456789", gujr: "૦૧૨૩૪૫૬૭૮૯", guru: "੦੧੨੩੪੫੬੭੮੯", hanidec: "〇一二三四五六七八九", khmr: "០១២៣៤៥៦៧៨៩", knda: "೦೧೨೩೪೫೬೭೮೯", laoo: "໐໑໒໓໔໕໖໗໘໙", latn: "0123456789", limb: "\u1946\u1947\u1948\u1949\u194A\u194B\u194C\u194D\u194E\u194F", mlym: "൦൧൨൩൪൫൬൭൮൯", mong: "᠐᠑᠒᠓᠔᠕᠖᠗᠘᠙", mymr: "၀၁၂၃၄၅၆၇၈၉", orya: "୦୧୨୩୪୫୬୭୮୯", tamldec: "௦௧௨௩௪௫௬௭௮௯", telu: "౦౧౨౩౪౫౬౭౮౯", thai: "๐๑๒๓๔๕๖๗๘๙", tibt: "༠༡༢༣༤༥༦༧༨༩" }; /** * Tests that number formatting is handled correctly. The function checks that the * digit sequences in formatted output are as specified, converted to the * selected numbering system, and embedded in consistent localized patterns. * @param {Array} locales the locales to be tested. * @param {Array} numberingSystems the numbering systems to be tested. * @param {Object} options the options to pass to Intl.NumberFormat. Options * must include {useGrouping: false}, and must cause 1.1 to be formatted * pre- and post-decimal digits. * @param {Object} testData maps input data (in ES5 9.3.1 format) to expected output strings * in unlocalized format with Western digits. */ function testNumberFormat(locales, numberingSystems, options, testData) { locales.forEach(function (locale) { numberingSystems.forEach(function (numbering) { var digits = numberingSystemDigits[numbering]; var format = new Intl.NumberFormat([locale + "-u-nu-" + numbering], options); function getPatternParts(positive) { var n = positive ? 1.1 : -1.1; var formatted = format.format(n); var oneoneRE = "([^" + digits + "]*)[" + digits + "]+([^" + digits + "]+)[" + digits + "]+([^" + digits + "]*)"; var match = formatted.match(new RegExp(oneoneRE)); if (match === null) { $ERROR("Unexpected formatted " + n + " for " + format.resolvedOptions().locale + " and options " + JSON.stringify(options) + ": " + formatted); } return match; } function toNumbering(raw) { return raw.replace(/[0-9]/g, function (digit) { return digits[digit.charCodeAt(0) - "0".charCodeAt(0)]; }); } function buildExpected(raw, patternParts) { var period = raw.indexOf("."); if (period === -1) { return patternParts[1] + toNumbering(raw) + patternParts[3]; } else { return patternParts[1] + toNumbering(raw.substring(0, period)) + patternParts[2] + toNumbering(raw.substring(period + 1)) + patternParts[3]; } } if (format.resolvedOptions().numberingSystem === numbering) { // figure out prefixes, infixes, suffixes for positive and negative values var posPatternParts = getPatternParts(true); var negPatternParts = getPatternParts(false); Object.getOwnPropertyNames(testData).forEach(function (input) { var rawExpected = testData[input]; var patternParts; if (rawExpected[0] === "-") { patternParts = negPatternParts; rawExpected = rawExpected.substring(1); } else { patternParts = posPatternParts; } var expected = buildExpected(rawExpected, patternParts); var actual = format.format(input); if (actual !== expected) { $ERROR("Formatted value for " + input + ", " + format.resolvedOptions().locale + " and options " + JSON.stringify(options) + " is " + actual + "; expected " + expected + "."); } }); } }); }); } /** * Return the components of date-time formats. * @return {Array} an array with all date-time components. */ function getDateTimeComponents() { return ["weekday", "era", "year", "month", "day", "hour", "minute", "second", "timeZoneName"]; } /** * Return the valid values for the given date-time component, as specified * by the table in section 12.1.1. * @param {string} component a date-time component. * @return {Array} an array with the valid values for the component. */ function getDateTimeComponentValues(component) { var components = { weekday: ["narrow", "short", "long"], era: ["narrow", "short", "long"], year: ["2-digit", "numeric"], month: ["2-digit", "numeric", "narrow", "short", "long"], day: ["2-digit", "numeric"], hour: ["2-digit", "numeric"], minute: ["2-digit", "numeric"], second: ["2-digit", "numeric"], timeZoneName: ["short", "long"] }; var result = components[component]; if (result === undefined) { $ERROR("Internal error: No values defined for date-time component " + component + "."); } return result; } /** * @description Tests whether timeZone is a String value representing a * structurally valid and canonicalized time zone name, as defined in * sections 6.4.1 and 6.4.2 of the ECMAScript Internationalization API * Specification. * @param {String} timeZone the string to be tested. * @result {Boolean} whether the test succeeded. */ function isCanonicalizedStructurallyValidTimeZoneName(timeZone) { /** * Regular expression defining IANA Time Zone names. * * Spec: IANA Time Zone Database, Theory file */ var fileNameComponent = "(?:[A-Za-z_]|\\.(?!\\.?(?:/|$)))[A-Za-z.\\-_]{0,13}"; var fileName = fileNameComponent + "(?:/" + fileNameComponent + ")*"; var etcName = "(?:Etc/)?GMT[+-]\\d{1,2}"; var systemVName = "SystemV/[A-Z]{3}\\d{1,2}(?:[A-Z]{3})?"; var legacyName = etcName + "|" + systemVName + "|CST6CDT|EST5EDT|MST7MDT|PST8PDT|NZ"; var zoneNamePattern = new RegExp("^(?:" + fileName + "|" + legacyName + ")$"); if (typeof timeZone !== "string") { return false; } // 6.4.2 CanonicalizeTimeZoneName (timeZone), step 3 if (timeZone === "UTC") { return true; } // 6.4.2 CanonicalizeTimeZoneName (timeZone), step 3 if (timeZone === "Etc/UTC" || timeZone === "Etc/GMT") { return false; } return zoneNamePattern.test(timeZone); }