Source

Locale.js

/*
 * Locale.js - Locale specifier definition
 *
 * Copyright © 2012-2015, 2018,2021 JEDLSoft
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

var ilib = require("./ilib.js");
var JSUtils = require("./JSUtils.js");

/**
 * @class
 * Create a new locale instance. Locales are specified either with a specifier string
 * that follows the BCP-47 convention (roughly: "language-region-script-variant") or
 * with 4 parameters that specify the language, region, variant, and script individually.<p>
 *
 * The language is given as an ISO 639-1 two-letter, lower-case language code. You
 * can find a full list of these codes at
 * <a href="http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes">http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes</a><p>
 *
 * The region is given as an ISO 3166-1 two-letter, upper-case region code. You can
 * find a full list of these codes at
 * <a href="http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2">http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2</a>.<p>
 *
 * The variant is any string that does not contain a dash which further differentiates
 * locales from each other.<p>
 *
 * The script is given as the ISO 15924 four-letter script code. In some locales,
 * text may be validly written in more than one script. For example, Serbian is often
 * written in both Latin and Cyrillic, though not usually mixed together. You can find a
 * full list of these codes at
 * <a href="http://en.wikipedia.org/wiki/ISO_15924#List_of_codes">http://en.wikipedia.org/wiki/ISO_15924#List_of_codes</a>.<p>
 *
 * As an example in ilib, the script can be used in the date formatter. Dates formatted
 * in Serbian could have day-of-week names or month names written in the Latin
 * or Cyrillic script. Often one script is default such that sr-SR-Latn is the same
 * as sr-SR so the script code "Latn" can be left off of the locale spec.<p>
 *
 * Each part is optional, and an empty string in the specifier before or after a
 * dash or as a parameter to the constructor denotes an unspecified value. In this
 * case, many of the ilib functions will treat the locale as generic. For example
 * the locale "en-" is equivalent to "en" and to "en--" and denotes a locale
 * of "English" with an unspecified region and variant, which typically matches
 * any region or variant.<p>
 *
 * Without any arguments to the constructor, this function returns the locale of
 * the host Javascript engine.<p>
 *
 *
 * @constructor
 * @param {?string|Locale=} language the ISO 639 2-letter code for the language, or a full
 * locale spec in BCP-47 format, or another Locale instance to copy from
 * @param {string=} region the ISO 3166 2-letter code for the region
 * @param {string=} variant the name of the variant of this locale, if any
 * @param {string=} script the ISO 15924 code of the script for this locale, if any
 */
var Locale = function(language, region, variant, script) {
    if (typeof(region) === 'undefined' && typeof(variant) === 'undefined' && typeof(script) === 'undefined') {
        var spec = language || ilib.getLocale();
        if (typeof(spec) === 'string') {
            var parts = spec.split(/[-_]/g);
            for ( var i = 0; i < parts.length; i++ ) {
                if (Locale._isLanguageCode(parts[i])) {
                    /**
                     * @private
                     * @type {string|undefined}
                     */
                    this.language = parts[i];
                } else if (Locale._isRegionCode(parts[i])) {
                    /**
                     * @private
                     * @type {string|undefined}
                     */
                    this.region = parts[i];
                } else if (Locale._isScriptCode(parts[i])) {
                    /**
                     * @private
                     * @type {string|undefined}
                     */
                    this.script = parts[i];
                } else {
                    /**
                     * @private
                     * @type {string|undefined}
                     */
                    this.variant = parts[i];
                }
            }
            this.language = this.language || undefined;
            this.region = this.region || undefined;
            this.script = this.script || undefined;
            this.variant = this.variant || undefined;
        } else if (typeof(spec) === 'object') {
            this.language = spec.language || undefined;
            this.region = spec.region || undefined;
            this.script = spec.script || undefined;
            this.variant = spec.variant || undefined;
        }
    } else {
        if (language && typeof(language) === "string") {
            language = language.trim();
            this.language = language.length > 0 ? language.toLowerCase() : undefined;
        } else {
            this.language = undefined;
        }
        if (region && typeof(region) === "string") {
            region = region.trim();
            this.region = region.length > 0 ? region.toUpperCase() : undefined;
        } else {
            this.region = undefined;
        }
        if (variant && typeof(variant) === "string") {
            variant = variant.trim();
            this.variant = variant.length > 0 ? variant : undefined;
        } else {
            this.variant = undefined;
        }
        if (script && typeof(script) === "string") {
            script = script.trim();
            this.script = script.length > 0 ? script : undefined;
        } else {
            this.script = undefined;
        }
    }
    this._genSpec();
};

// from http://en.wikipedia.org/wiki/ISO_3166-1
Locale.a2toa3regmap = {
    "AF": "AFG",
    "AX": "ALA",
    "AL": "ALB",
    "DZ": "DZA",
    "AS": "ASM",
    "AD": "AND",
    "AO": "AGO",
    "AI": "AIA",
    "AQ": "ATA",
    "AG": "ATG",
    "AR": "ARG",
    "AM": "ARM",
    "AW": "ABW",
    "AU": "AUS",
    "AT": "AUT",
    "AZ": "AZE",
    "BS": "BHS",
    "BH": "BHR",
    "BD": "BGD",
    "BB": "BRB",
    "BY": "BLR",
    "BE": "BEL",
    "BZ": "BLZ",
    "BJ": "BEN",
    "BM": "BMU",
    "BT": "BTN",
    "BO": "BOL",
    "BQ": "BES",
    "BA": "BIH",
    "BW": "BWA",
    "BV": "BVT",
    "BR": "BRA",
    "IO": "IOT",
    "BN": "BRN",
    "BG": "BGR",
    "BF": "BFA",
    "BI": "BDI",
    "KH": "KHM",
    "CM": "CMR",
    "CA": "CAN",
    "CV": "CPV",
    "KY": "CYM",
    "CF": "CAF",
    "TD": "TCD",
    "CL": "CHL",
    "CN": "CHN",
    "CX": "CXR",
    "CC": "CCK",
    "CO": "COL",
    "KM": "COM",
    "CG": "COG",
    "CD": "COD",
    "CK": "COK",
    "CR": "CRI",
    "CI": "CIV",
    "HR": "HRV",
    "CU": "CUB",
    "CW": "CUW",
    "CY": "CYP",
    "CZ": "CZE",
    "DK": "DNK",
    "DJ": "DJI",
    "DM": "DMA",
    "DO": "DOM",
    "EC": "ECU",
    "EG": "EGY",
    "SV": "SLV",
    "GQ": "GNQ",
    "ER": "ERI",
    "EE": "EST",
    "ET": "ETH",
    "FK": "FLK",
    "FO": "FRO",
    "FJ": "FJI",
    "FI": "FIN",
    "FR": "FRA",
    "GF": "GUF",
    "PF": "PYF",
    "TF": "ATF",
    "GA": "GAB",
    "GM": "GMB",
    "GE": "GEO",
    "DE": "DEU",
    "GH": "GHA",
    "GI": "GIB",
    "GR": "GRC",
    "GL": "GRL",
    "GD": "GRD",
    "GP": "GLP",
    "GU": "GUM",
    "GT": "GTM",
    "GG": "GGY",
    "GN": "GIN",
    "GW": "GNB",
    "GY": "GUY",
    "HT": "HTI",
    "HM": "HMD",
    "VA": "VAT",
    "HN": "HND",
    "HK": "HKG",
    "HU": "HUN",
    "IS": "ISL",
    "IN": "IND",
    "ID": "IDN",
    "IR": "IRN",
    "IQ": "IRQ",
    "IE": "IRL",
    "IM": "IMN",
    "IL": "ISR",
    "IT": "ITA",
    "JM": "JAM",
    "JP": "JPN",
    "JE": "JEY",
    "JO": "JOR",
    "KZ": "KAZ",
    "KE": "KEN",
    "KI": "KIR",
    "KP": "PRK",
    "KR": "KOR",
    "KW": "KWT",
    "KG": "KGZ",
    "LA": "LAO",
    "LV": "LVA",
    "LB": "LBN",
    "LS": "LSO",
    "LR": "LBR",
    "LY": "LBY",
    "LI": "LIE",
    "LT": "LTU",
    "LU": "LUX",
    "MO": "MAC",
    "MK": "MKD",
    "MG": "MDG",
    "MW": "MWI",
    "MY": "MYS",
    "MV": "MDV",
    "ML": "MLI",
    "MT": "MLT",
    "MH": "MHL",
    "MQ": "MTQ",
    "MR": "MRT",
    "MU": "MUS",
    "YT": "MYT",
    "MX": "MEX",
    "FM": "FSM",
    "MD": "MDA",
    "MC": "MCO",
    "MN": "MNG",
    "ME": "MNE",
    "MS": "MSR",
    "MA": "MAR",
    "MZ": "MOZ",
    "MM": "MMR",
    "NA": "NAM",
    "NR": "NRU",
    "NP": "NPL",
    "NL": "NLD",
    "NC": "NCL",
    "NZ": "NZL",
    "NI": "NIC",
    "NE": "NER",
    "NG": "NGA",
    "NU": "NIU",
    "NF": "NFK",
    "MP": "MNP",
    "NO": "NOR",
    "OM": "OMN",
    "PK": "PAK",
    "PW": "PLW",
    "PS": "PSE",
    "PA": "PAN",
    "PG": "PNG",
    "PY": "PRY",
    "PE": "PER",
    "PH": "PHL",
    "PN": "PCN",
    "PL": "POL",
    "PT": "PRT",
    "PR": "PRI",
    "QA": "QAT",
    "RE": "REU",
    "RO": "ROU",
    "RU": "RUS",
    "RW": "RWA",
    "BL": "BLM",
    "SH": "SHN",
    "KN": "KNA",
    "LC": "LCA",
    "MF": "MAF",
    "PM": "SPM",
    "VC": "VCT",
    "WS": "WSM",
    "SM": "SMR",
    "ST": "STP",
    "SA": "SAU",
    "SN": "SEN",
    "RS": "SRB",
    "SC": "SYC",
    "SL": "SLE",
    "SG": "SGP",
    "SX": "SXM",
    "SK": "SVK",
    "SI": "SVN",
    "SB": "SLB",
    "SO": "SOM",
    "ZA": "ZAF",
    "GS": "SGS",
    "SS": "SSD",
    "ES": "ESP",
    "LK": "LKA",
    "SD": "SDN",
    "SR": "SUR",
    "SJ": "SJM",
    "SZ": "SWZ",
    "SE": "SWE",
    "CH": "CHE",
    "SY": "SYR",
    "TW": "TWN",
    "TJ": "TJK",
    "TZ": "TZA",
    "TH": "THA",
    "TL": "TLS",
    "TG": "TGO",
    "TK": "TKL",
    "TO": "TON",
    "TT": "TTO",
    "TN": "TUN",
    "TR": "TUR",
    "TM": "TKM",
    "TC": "TCA",
    "TV": "TUV",
    "UG": "UGA",
    "UA": "UKR",
    "AE": "ARE",
    "GB": "GBR",
    "US": "USA",
    "UM": "UMI",
    "UY": "URY",
    "UZ": "UZB",
    "VU": "VUT",
    "VE": "VEN",
    "VN": "VNM",
    "VG": "VGB",
    "VI": "VIR",
    "WF": "WLF",
    "EH": "ESH",
    "YE": "YEM",
    "ZM": "ZMB",
    "ZW": "ZWE"
};


Locale.a1toa3langmap = {
    "ab": "abk",
    "aa": "aar",
    "af": "afr",
    "ak": "aka",
    "sq": "sqi",
    "am": "amh",
    "ar": "ara",
    "an": "arg",
    "hy": "hye",
    "as": "asm",
    "av": "ava",
    "ae": "ave",
    "ay": "aym",
    "az": "aze",
    "bm": "bam",
    "ba": "bak",
    "eu": "eus",
    "be": "bel",
    "bn": "ben",
    "bh": "bih",
    "bi": "bis",
    "bs": "bos",
    "br": "bre",
    "bg": "bul",
    "my": "mya",
    "ca": "cat",
    "ch": "cha",
    "ce": "che",
    "ny": "nya",
    "zh": "zho",
    "cv": "chv",
    "kw": "cor",
    "co": "cos",
    "cr": "cre",
    "hr": "hrv",
    "cs": "ces",
    "da": "dan",
    "dv": "div",
    "nl": "nld",
    "dz": "dzo",
    "en": "eng",
    "eo": "epo",
    "et": "est",
    "ee": "ewe",
    "fo": "fao",
    "fj": "fij",
    "fi": "fin",
    "fr": "fra",
    "ff": "ful",
    "gl": "glg",
    "ka": "kat",
    "de": "deu",
    "el": "ell",
    "gn": "grn",
    "gu": "guj",
    "ht": "hat",
    "ha": "hau",
    "he": "heb",
    "hz": "her",
    "hi": "hin",
    "ho": "hmo",
    "hu": "hun",
    "ia": "ina",
    "id": "ind",
    "ie": "ile",
    "ga": "gle",
    "ig": "ibo",
    "ik": "ipk",
    "io": "ido",
    "is": "isl",
    "it": "ita",
    "iu": "iku",
    "ja": "jpn",
    "jv": "jav",
    "kl": "kal",
    "kn": "kan",
    "kr": "kau",
    "ks": "kas",
    "kk": "kaz",
    "km": "khm",
    "ki": "kik",
    "rw": "kin",
    "ky": "kir",
    "kv": "kom",
    "kg": "kon",
    "ko": "kor",
    "ku": "kur",
    "kj": "kua",
    "la": "lat",
    "lb": "ltz",
    "lg": "lug",
    "li": "lim",
    "ln": "lin",
    "lo": "lao",
    "lt": "lit",
    "lu": "lub",
    "lv": "lav",
    "gv": "glv",
    "mk": "mkd",
    "mg": "mlg",
    "ms": "msa",
    "ml": "mal",
    "mt": "mlt",
    "mi": "mri",
    "mr": "mar",
    "mh": "mah",
    "mn": "mon",
    "na": "nau",
    "nv": "nav",
    "nb": "nob",
    "nd": "nde",
    "ne": "nep",
    "ng": "ndo",
    "nn": "nno",
    "no": "nor",
    "ii": "iii",
    "nr": "nbl",
    "oc": "oci",
    "oj": "oji",
    "cu": "chu",
    "om": "orm",
    "or": "ori",
    "os": "oss",
    "pa": "pan",
    "pi": "pli",
    "fa": "fas",
    "pl": "pol",
    "ps": "pus",
    "pt": "por",
    "qu": "que",
    "rm": "roh",
    "rn": "run",
    "ro": "ron",
    "ru": "rus",
    "sa": "san",
    "sc": "srd",
    "sd": "snd",
    "se": "sme",
    "sm": "smo",
    "sg": "sag",
    "sr": "srp",
    "gd": "gla",
    "sn": "sna",
    "si": "sin",
    "sk": "slk",
    "sl": "slv",
    "so": "som",
    "st": "sot",
    "es": "spa",
    "su": "sun",
    "sw": "swa",
    "ss": "ssw",
    "sv": "swe",
    "ta": "tam",
    "te": "tel",
    "tg": "tgk",
    "th": "tha",
    "ti": "tir",
    "bo": "bod",
    "tk": "tuk",
    "tl": "tgl",
    "tn": "tsn",
    "to": "ton",
    "tr": "tur",
    "ts": "tso",
    "tt": "tat",
    "tw": "twi",
    "ty": "tah",
    "ug": "uig",
    "uk": "ukr",
    "ur": "urd",
    "uz": "uzb",
    "ve": "ven",
    "vi": "vie",
    "vo": "vol",
    "wa": "wln",
    "cy": "cym",
    "wo": "wol",
    "fy": "fry",
    "xh": "xho",
    "yi": "yid",
    "yo": "yor",
    "za": "zha",
    "zu": "zul"
};

// the list below is originally from https://unicode.org/iso15924/iso15924-codes.html
Locale.iso15924 = [
    "Adlm",
    "Afak",
    "Aghb",
    "Ahom",
    "Arab",
    "Aran",
    "Armi",
    "Armn",
    "Avst",
    "Bali",
    "Bamu",
    "Bass",
    "Batk",
    "Beng",
    "Bhks",
    "Blis",
    "Bopo",
    "Brah",
    "Brai",
    "Bugi",
    "Buhd",
    "Cakm",
    "Cans",
    "Cari",
    "Cham",
    "Cher",
    "Chrs",
    "Cirt",
    "Copt",
    "Cpmn",
    "Cprt",
    "Cyrl",
    "Cyrs",
    "Deva",
    "Diak",
    "Dogr",
    "Dsrt",
    "Dupl",
    "Egyd",
    "Egyh",
    "Egyp",
    "Elba",
    "Elym",
    "Ethi",
    "Geok",
    "Geor",
    "Glag",
    "Gong",
    "Gonm",
    "Goth",
    "Gran",
    "Grek",
    "Gujr",
    "Guru",
    "Hanb",
    "Hang",
    "Hani",
    "Hano",
    "Hans",
    "Hant",
    "Hatr",
    "Hebr",
    "Hira",
    "Hluw",
    "Hmng",
    "Hmnp",
    "Hrkt",
    "Hung",
    "Inds",
    "Ital",
    "Jamo",
    "Java",
    "Jpan",
    "Jurc",
    "Kali",
    "Kana",
    "Khar",
    "Khmr",
    "Khoj",
    "Kitl",
    "Kits",
    "Knda",
    "Kore",
    "Kpel",
    "Kthi",
    "Lana",
    "Laoo",
    "Latf",
    "Latg",
    "Latn",
    "Leke",
    "Lepc",
    "Limb",
    "Lina",
    "Linb",
    "Lisu",
    "Loma",
    "Lyci",
    "Lydi",
    "Mahj",
    "Maka",
    "Mand",
    "Mani",
    "Marc",
    "Maya",
    "Medf",
    "Mend",
    "Merc",
    "Mero",
    "Mlym",
    "Modi",
    "Mong",
    "Moon",
    "Mroo",
    "Mtei",
    "Mult",
    "Mymr",
    "Nand",
    "Narb",
    "Nbat",
    "Newa",
    "Nkdb",
    "Nkgb",
    "Nkoo",
    "Nshu",
    "Ogam",
    "Olck",
    "Orkh",
    "Orya",
    "Osge",
    "Osma",
    "Palm",
    "Pauc",
    "Perm",
    "Phag",
    "Phli",
    "Phlp",
    "Phlv",
    "Phnx",
    "Plrd",
    "Piqd",
    "Prti",
    "Qaaa",
    "Qabx",
    "Rjng",
    "Rohg",
    "Roro",
    "Runr",
    "Samr",
    "Sara",
    "Sarb",
    "Saur",
    "Sgnw",
    "Shaw",
    "Shrd",
    "Shui",
    "Sidd",
    "Sind",
    "Sinh",
    "Sogd",
    "Sogo",
    "Sora",
    "Soyo",
    "Sund",
    "Sylo",
    "Syrc",
    "Syre",
    "Syrj",
    "Syrn",
    "Tagb",
    "Takr",
    "Tale",
    "Talu",
    "Taml",
    "Tang",
    "Tavt",
    "Telu",
    "Teng",
    "Tfng",
    "Tglg",
    "Thaa",
    "Thai",
    "Tibt",
    "Tirh",
    "Toto",
    "Ugar",
    "Vaii",
    "Visp",
    "Wara",
    "Wcho",
    "Wole",
    "Xpeo",
    "Xsux",
    "Yezi",
    "Yiii",
    "Zanb",
    "Zinh",
    "Zmth",
    "Zsye",
    "Zsym",
    "Zxxx",
    "Zyyy",
    "Zzzz",
];

/**
 * Tell whether or not the str does not start with a lower case ASCII char.
 * @private
 * @param {string} str the char to check
 * @return {boolean} true if the char is not a lower case ASCII char
 */
Locale._notLower = function(str) {
    // do this with ASCII only so we don't have to depend on the CType functions
    var ch = str.charCodeAt(0);
    return ch < 97 || ch > 122;
};

/**
 * Tell whether or not the str does not start with an upper case ASCII char.
 * @private
 * @param {string} str the char to check
 * @return {boolean} true if the char is a not an upper case ASCII char
 */
Locale._notUpper = function(str) {
    // do this with ASCII only so we don't have to depend on the CType functions
    var ch = str.charCodeAt(0);
    return ch < 65 || ch > 90;
};

/**
 * Tell whether or not the str does not start with a digit char.
 * @private
 * @param {string} str the char to check
 * @return {boolean} true if the char is a not an upper case ASCII char
 */
Locale._notDigit = function(str) {
    // do this with ASCII only so we don't have to depend on the CType functions
    var ch = str.charCodeAt(0);
    return ch < 48 || ch > 57;
};

/**
 * Tell whether or not the given string has the correct syntax to be
 * an ISO 639 language code.
 *
 * @private
 * @param {string} str the string to parse
 * @return {boolean} true if the string could syntactically be a language code.
 */
Locale._isLanguageCode = function(str) {
    if (typeof(str) === 'undefined' || str.length < 2 || str.length > 3) {
        return false;
    }

    for (var i = 0; i < str.length; i++) {
        if (Locale._notLower(str.charAt(i))) {
            return false;
        }
    }

    return true;
};

/**
 * Tell whether or not the given string has the correct syntax to be
 * an ISO 3166 2-letter region code or M.49 3-digit region code.
 *
 * @private
 * @param {string} str the string to parse
 * @return {boolean} true if the string could syntactically be a language code.
 */
Locale._isRegionCode = function (str) {
    var i;

    if (typeof(str) === 'undefined' || str.length < 2 || str.length > 3) {
        return false;
    }

    if (str.length === 2) {
        for (i = 0; i < str.length; i++) {
            if (Locale._notUpper(str.charAt(i))) {
                return false;
            }
        }
    } else {
        for (i = 0; i < str.length; i++) {
            if (Locale._notDigit(str.charAt(i))) {
                return false;
            }
        }
    }

    return true;
};

/**
 * Tell whether or not the given string has the correct syntax to be
 * an ISO 639 language code.
 *
 * @private
 * @param {string} str the string to parse
 * @return {boolean} true if the string could syntactically be a language code.
 */
Locale._isScriptCode = function(str) {
    if (typeof(str) === 'undefined' || str.length !== 4 || Locale._notUpper(str.charAt(0))) {
        return false;
    }

    for (var i = 1; i < 4; i++) {
        if (Locale._notLower(str.charAt(i))) {
            return false;
        }
    }

    return true;
};

/**
 * Return the ISO-3166 alpha3 equivalent region code for the given ISO 3166 alpha2
 * region code. If the given alpha2 code is not found, this function returns its
 * argument unchanged.
 * @static
 * @param {string|undefined} alpha2 the alpha2 code to map
 * @return {string|undefined} the alpha3 equivalent of the given alpha2 code, or the alpha2
 * parameter if the alpha2 value is not found
 */
Locale.regionAlpha2ToAlpha3 = function(alpha2) {
    return Locale.a2toa3regmap[alpha2] || alpha2;
};

/**
 * Return the ISO-639 alpha3 equivalent language code for the given ISO 639 alpha1
 * language code. If the given alpha1 code is not found, this function returns its
 * argument unchanged.
 * @static
 * @param {string|undefined} alpha1 the alpha1 code to map
 * @return {string|undefined} the alpha3 equivalent of the given alpha1 code, or the alpha1
 * parameter if the alpha1 value is not found
 */
Locale.languageAlpha1ToAlpha3 = function(alpha1) {
    return Locale.a1toa3langmap[alpha1] || alpha1;
};

Locale.prototype = {
    /**
     * @private
     */
    _genSpec: function () {
        this.spec = this.language || "";

        if (this.script) {
            if (this.spec.length > 0) {
                this.spec += "-";
            }
            this.spec += this.script;
        }

        if (this.region) {
            if (this.spec.length > 0) {
                this.spec += "-";
            }
            this.spec += this.region;
        }

        if (this.variant) {
            if (this.spec.length > 0) {
                this.spec += "-";
            }
            this.spec += this.variant;
        }
    },

    /**
     * Return the ISO 639 language code for this locale.
     * @return {string|undefined} the language code for this locale
     */
    getLanguage: function() {
        return this.language;
    },

    /**
     * Return the language of this locale as an ISO-639-alpha3 language code
     * @return {string|undefined} the alpha3 language code of this locale
     */
    getLanguageAlpha3: function() {
        return Locale.languageAlpha1ToAlpha3(this.language);
    },

    /**
     * Return the ISO 3166 region code for this locale.
     * @return {string|undefined} the region code of this locale
     */
    getRegion: function() {
        return this.region;
    },

    /**
     * Return the region of this locale as an ISO-3166-alpha3 region code
     * @return {string|undefined} the alpha3 region code of this locale
     */
    getRegionAlpha3: function() {
        return Locale.regionAlpha2ToAlpha3(this.region);
    },

    /**
     * Return the ISO 15924 script code for this locale
     * @return {string|undefined} the script code of this locale
     */
    getScript: function () {
        return this.script;
    },

    /**
     * Return the variant code for this locale
     * @return {string|undefined} the variant code of this locale, if any
     */
    getVariant: function() {
        return this.variant;
    },

    /**
     * Return the whole locale specifier as a string.
     * @return {string} the locale specifier
     */
    getSpec: function() {
        if (!this.spec) this._genSpec();
        return this.spec;
    },

    /**
     * Return the language locale specifier. This includes the
     * language and the script if it is available. This can be
     * used to see whether the written language of two locales
     * match each other regardless of the region or variant.
     *
     * @return {string} the language locale specifier
     */
    getLangSpec: function() {
        var spec = this.language;
        if (spec && this.script) {
            spec += "-" + this.script;
        }
        return spec || "";
    },

    /**
     * Express this locale object as a string. Currently, this simply calls the getSpec
     * function to represent the locale as its specifier.
     *
     * @return {string} the locale specifier
     */
    toString: function() {
        return this.getSpec();
    },

    /**
     * Return true if the the other locale is exactly equal to the current one.
     * @return {boolean} whether or not the other locale is equal to the current one
     */
    equals: function(other) {
        return this.language === other.language &&
            this.region === other.region &&
            this.script === other.script &&
            this.variant === other.variant;
    },

    /**
     * Return true if the current locale is the special pseudo locale.
     * @return {boolean} true if the current locale is the special pseudo locale
     */
    isPseudo: function () {
        return JSUtils.indexOf(ilib.pseudoLocales, this.spec) > -1;
    },

    /**
     * Return true if the current locale uses a valid ISO codes for each component
     * of the locale that exists.
     * @return {boolean} true if the current locale has all valid components, and
     * false otherwise.
     */
    isValid: function() {
        if (!this.language && !this.script && !this.region) return false;

        return !!((!this.language || (Locale._isLanguageCode(this.language) && Locale.a1toa3langmap[this.language])) &&
            (!this.script || (Locale._isScriptCode(this.script) && Locale.iso15924.indexOf(this.script) > -1)) &&
            (!this.region || (Locale._isRegionCode(this.region) && Locale.a2toa3regmap[this.region])));
    }
};

// static functions
/**
 * @private
 */
Locale.locales = [];
// !macro localelist

/**
 * Return the list of available locales that this iLib file supports.
 * If this copy of ilib is pre-assembled with locale data, then the
 * list locales may be much smaller
 * than the list of all available locales in the iLib repository. The
 * assembly tool will automatically fill in the list for an assembled
 * copy of iLib. If this copy is being used with dynamically loaded
 * data, then you
 * can load any locale that iLib supports. You can form a locale with any
 * combination of a language and region tags that exist in the locale
 * data directory. Language tags are in the root of the locale data dir,
 * and region tags can be found underneath the "und" directory. (The
 * region tags are separated into a different dir because the region names
 * conflict with language names on file systems that are case-insensitive.)
 * If you have culled the locale data directory to limit the size of
 * your app, then this function should return only those files that actually exist
 * according to the ilibmanifest.json file in the root of that locale
 * data dir. Make sure your ilibmanifest.json file is up-to-date with
 * respect to the list of files that exist in the locale data dir.
 *
 * @param {boolean} sync if false, load the list of available files from disk
 * asynchronously, otherwise load them synchronously. (Default: true/synchronously)
 * @param {Function} onLoad a callback function to call if asynchronous
 * load was requested and the list of files have been loaded.
 * @return {Array.<string>} this is an array of locale specs for which
 * this iLib file has locale data for
 */
Locale.getAvailableLocales = function (sync, onLoad) {
    var locales = [];
    if (Locale.locales.length || typeof(ilib._load.listAvailableFiles) !== 'function') {
        locales = Locale.locales;
        if (onLoad && typeof(onLoad) === 'function') {
            onLoad(locales);
        }
    } else {
        if (typeof(sync) === 'undefined') {
            sync = true;
        }
        ilib._load.listAvailableFiles(sync, function(manifest) {
            if (manifest) {
                for (var dir in manifest) {
                    var filelist = manifest[dir];
                    for (var i = 0; i < filelist.length; i++) {
                        if (filelist[i].length > 15 && filelist[i].substr(-15) === "localeinfo.json") {
                            locales.push(filelist[i].substring(0,filelist[i].length-16).replace(/\//g, "-"));
                        }
                    }
                }
            }
            if (onLoad && typeof(onLoad) === 'function') {
                onLoad(locales);
            }
        });
    }
    return locales;
};

module.exports = Locale;