Viewing File: /usr/local/cpanel/share/libraries/cjt2/src/util/locale.js

/*
# cjt/util/locale.js                                Copyright 2022 cPanel, L.L.C.
#                                                            All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited
*/

/* --------------------------*/
/* DEFINE GLOBALS FOR LINT
/*--------------------------*/
/* global define:false, console:false, global:false     */
/* --------------------------*/

/* eslint camelcase: 0 */

// TODO: Add tests for these

/**
 *
 * @module cjt/util/locale
 * @example
 *  define(
 *  [
 *      "cjt/util/locale",
 *  ],
 *  function(LOCALE) {
 *      LOCALE.numf(100.1);
 *  });
 *
 * @example
 *
 * The main use case is to get the current selected locale. If you need access to other locales, you
 * must set them up manually by doing the following in a module that brings in cjt2/util/locale. With
 * these you can generate new locales, modify existing locale, etc, though there should be very limited
 * need
 *
 *  define(
 *  [
 *      "cjt/util/locale",
 *  ],
 *  function(LOCALE) {
 *      var manager = LOCALE.get_manager();
 *      if (!manager.has_locale("fr")) {
 *          manager.generateClassFromCldr("fr", {
 *              misc_info: {
 *                  delimiters: {
 *                      quotation_start: "<<",
 *                      quotation_end:   ">>"
 *                  }
 *              }
 *          });
 *      }
 *  }
 *
 */
define(["lodash"], function(_) {
    "use strict";

    /**
     * Utility module for building localized strings.
     *
     * @static
     * @public
     * @class locale
     */


    var DEFAULT_ELLIPSIS = {
        initial: "…{0}",
        medial: "{0}…{1}",
        "final": "{0}…"
    };

    var html_apos = _.escape("'");
    var html_quot = _.escape("\"");
    var html_amp  = _.escape("&");

    /**
     * JS getUTCDay() starts from Sunday, but CLDR starts from Monday.
     * @param  {[type]} the_date [description]
     * @return {[type]}          [description]
     */
    var get_cldr_day = function(the_date) {
        var num = the_date.getUTCDay() - 1;
        return (num < 0) ? 6 : num;
    };

    function Locale() {}
    Locale._locales = {};
    Locale._currentLocaleTag = "";

    /**
     * Get the current locale tag name
     * @return {String} Locale tag name in ISO ??? format.
     */
    Locale.getCurrentLocale = function() {
        return Locale._currentLocaleTag;
    };

    /**
     * Sets the current locale by its tag name.
     * @param {String} tag Locale tag name in ISO ??? format.
     */
    Locale.setCurrentLocale = function(tag) {
        if (tag in Locale._locales.keys()) {
            Locale._currentLocaleTag = tag;
            window.LOCALE = Locale._locales[tag];
        } else {
            // eslint-disable-next-line no-console
            console.log("Failed to locate the requested locale " + tag + " in the loaded locales.");
        }
        return window.LOCALE;
    };

    /**
     * Add a locale to the system
     * @param {String} tag        Locale tag name in ISO ??? format.
     * @param {Function} construc Constructor function for the CLDR instance for this locale.
     */
    Locale.add_locale = function(tag, construc) {
        Locale._locales[tag] = construc;
        Locale._currentLocaleTag = tag; // Assume the last one set is the current locale.
        construc.prototype._locale_tag = tag;
    };

    /**
     * Remove a local from the system
     * @param  {String} tag Locale tag name in ISO ??? format.
     */
    Locale.remove_locale = function(tag) {  // For testing
        return delete Locale._locales[tag];
    };

    /**
     * Remove all locales
     */
    Locale.clear_locales = function(tag) {  // For testing
        Locale._locales = {};
    };

    /**
     * Test if the locale generator or instance exists
     * @param  {String} tag Locale tag name in ISO format.
     */
    Locale.has_locale = function(tag) {
        return !!Locale._locales[tag];
    };

    /**
     * Get a handle to the specified locale, processing the arguments until one is found.
     * You can provide one or more instances tags to attempt. This method will search until
     * if finds one or will return the first one if it cant find any of the ones you passed,
     * or you didn't pass a tag.
     * @param {String...} tag name of the locale to lookup.
     * @return {Object} Locale object containing the CLDR knowledge and string management
     * logic for a given locale.
     */
    Locale.get_handle = function() {
        var cur_arg;
        var arg_count = arguments.length;
        for (var a = 0; a < arg_count; a++) {
            cur_arg = arguments[a];
            if ( cur_arg in Locale._locales ) {
                return new Locale._locales[cur_arg]();
            }
        }

        // We didn't find anything from the given arguments, so check _locales.
        // We can't trust JS's iteration order, so grab keys and take the first one.
        var locale_tags = Object.keys(Locale._locales);
        var loc = locale_tags.length ? locale_tags[0] : false;

        var loc_obj = (loc && Locale._locales[loc] ? new Locale._locales[loc]() : new Locale());
        loc_obj.get_manager = function() {
            return Locale;
        };
        return loc_obj;
    };

    // ymd_string_to_date will be smarter once case 52389 is done.
    // For now, we need the ymd order from the server.
    Locale.ymd = null;

    /**
     * Convert a YYMMDD string to a date object
     * @param  {String} str [description]
     * @return {Date}     [description]
     * @????
     */
    Locale.ymd_string_to_date = function(str) {
        var str_split = str.split(/\D+/);
        var ymd = this.ymd || "mdy";  // U.S. English;

        var day = str_split[ ymd.indexOf("d") ];
        var month = str_split[ ymd.indexOf("m") ];
        var year = str_split[ ymd.indexOf("y") ];

        // It seems unlikely that we'd care about ancient times.
        if ( year && (year.length < 4) ) {
            var deficit = 4 - year.length;
            year = String((new Date()).getFullYear()).substr(0, deficit) + year;
        }

        var date = new Date( year, month - 1, day );
        return isNaN(date.getTime()) ? undefined : date;
    };

    // temporary, until case 52389 is in
    Locale.date_template = null;

    /**
     * Convert a date into a YYMMDD string
     * @param {Date} date [description]
     * @return {String} [description]
     */
    Locale.to_ymd_string = function(date) {
        var template = Locale.date_template || "{month}/{day}/{year}";  // U.S. English
        return template.replace(/\{(?:month|day|year)\}/g, function(subst) {
            switch (subst) {
                case "{day}":
                    return date.getDate();
                case "{month}":
                    return date.getMonth() + 1;
                case "{year}":
                    return date.getFullYear();
            }
        } );
    };

    var bracket_re = /([^~\[\]]+|~.|\[|\]|~)/g;

    // cf. Locale::Maketext re DEL
    var faux_comma = "\x07";
    var faux_comma_re = new RegExp( faux_comma, "g" );

    // For outside a bracket group
    var tilde_chars = { "[": 1, "]": 1, "~": 1 };

    var underscore_digit_re = /^_(\d+)$/;

    var func_substitutions = {
        "#": "numf",
        "*": "quant"
    };

    // NOTE: There is no widely accepted consensus of exactly how to measure data
    // and which units to use for it. For example, some bodies define "B" to mean
    // bytes, while others don't. (NB: SI defines "B" to mean bels.) Some folks
    // use k for kilo; others use K. Some say kilo should be 1,024; others say
    // it's 1,000 (and "kibi" would be 1,024). What we do here is at least in
    // longstanding use at cPanel.
    var data_abbreviations = [ "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" ];

    // NOTE: args *must* be a list, not an Array object (as is permissible with
    // most other functions in this module).

    /**
     * Internal implementation
     * @param  {String} str Source string.
     * @return {String}     Interpolated string.
     */
    var _maketext = function(str /* , args list */) { // ## no extract maketext
        if (!str) {
            return;
        }

        str = this.LEXICON && this.LEXICON[str] || str;

        if ( str.indexOf("[") === -1 ) {
            return String(str);
        }

        var assembled = [];

        var pieces = str.match(bracket_re);
        var pieces_length = pieces.length;

        var in_group = false;
        var bracket_args = "";

        var p, cur_p, a;

        PIECE:
        for (p = 0; p < pieces_length; p++) {
            cur_p = pieces[p];
            if ((cur_p === "[")) {
                if (in_group) {
                    throw "Invalid maketext string: " + str; // ## no extract maketext
                }

                in_group = true;
            } else if ( cur_p === "]" ) {

                if (!in_group || !bracket_args) {
                    throw "Invalid maketext string: " + str; // ## no extract maketext
                }

                in_group = false;

                var real_args = bracket_args.split(",");
                var len = real_args.length;
                var func;

                if ( len === 1 ) {
                    var arg = real_args[0].match(underscore_digit_re);
                    if ( !arg ) {
                        throw "Invalid maketext string: " + str; // ## no extract maketext
                    }

                    var looked_up = arguments[arg[1]];
                    if ( typeof looked_up === "undefined" ) {
                        throw "Invalid argument \"" + arg[1] + "\" passed to maketext string: " + str; // ## no extract maketext
                    } else {
                        bracket_args = "";
                        assembled.push(looked_up);
                        continue PIECE;
                    }
                } else {
                    func = real_args.shift();
                    len -= 1;
                    func = func_substitutions[func] || func;

                    if ( typeof this[func] !== "function" ) {
                        throw "Invalid function \"" + func + "\" in maketext string: " + str; // ## no extract maketext
                    }
                }

                if ( bracket_args.indexOf(faux_comma) !== -1 ) {
                    for (a = 0; a < len; a++) {
                        real_args[a] = real_args[a].replace(faux_comma_re, ",");
                    }
                }

                var cur_arg, alen;

                for (a = 0; a < len; a++) {
                    cur_arg = real_args[a];
                    if ( cur_arg.charAt(0) === "_" ) {
                        if ( cur_arg === "_*" ) {
                            real_args.splice( a, 1 );
                            for (a = 1, alen = arguments.length; a < alen; a++) {
                                real_args.push( arguments[a] );
                            }
                        } else {
                            var arg_num = cur_arg.match(underscore_digit_re);
                            if ( arg_num ) {
                                if ( arg_num[1] in arguments ) {
                                    real_args[a] = arguments[arg_num[1]];
                                } else {
                                    throw "Invalid variable \"" + arg_num[1] + "\" in maketext string: " + str; // ## no extract maketext
                                }
                            } else {
                                throw "Invalid maketext string: " + str; // ## no extract maketext
                            }
                        }
                    }
                }

                bracket_args = "";
                assembled.push( this[func].apply(this, real_args) );
            } else if ( cur_p.charAt(0) === "~" ) {
                var real_char = cur_p.charAt(1) || "~";
                if ( in_group ) {
                    if (real_char === ",") {
                        bracket_args += faux_comma;
                    } else {
                        bracket_args += real_char;
                    }
                } else if ( real_char in tilde_chars ) {
                    assembled.push(real_char);
                } else {
                    assembled.push(cur_p);
                }
            } else if (in_group) {
                bracket_args += cur_p;
            } else {
                assembled.push(cur_p);
            }
        }

        if (in_group) {
            throw "Invalid maketext string: " + str; // ## no extract maketext
        }

        return assembled.join("");
    };

    // Most of what is here ports functionality from CPAN Locale::Maketext::Utils.
    Locale.prototype = Object.create({

        /**
         * Initialized with the value stored in window.LEXICON if there is anything.
         * @type {Object} - where each key is the untranslated string and the value is the translated version
         * of that string in the current users selected locale.
         */
        LEXICON: (typeof window === "undefined") ?
            global.LEXICON || (global.LEXICON = {}) :
            window.LEXICON || (window.LEXICON = {}),

        /**
         * Use this method to localize a static string. These strings are harvested normally.
         *
         * @method maketext                                                                                         // ## no extract maketext
         * @param {String} template Template to process.
         * @param {...*}   [args]   Optional replacement arguments for the template.
         * @return {String}
         */
        maketext: _maketext, // ## no extract maketext

        /**
         * Like maketext() but does not lookup the phrase in the lexicon and compiles the phrase exactly as given.  // ## no extract maketext
         *
         * @note  In the current implementation this works just like maketext, but will need to be modified once we // ## no extract maketext
         * start doing lexicon lookups.
         *
         * @method maketext                                                                                         // ## no extract maketext
         * @param {String} template Template to process.
         * @param {...*}   [args]   Optional replacement arguments for the template.
         * @return {String}
         */
        makethis: _maketext,                                                                                       // ## no extract maketext

        /**
         * Use this method instead of maketext if you are passing a variable that contains the maketext template.   // ## no extract maketext
         *
         * @method makevar
         * @param {String} template Template to process.
         * @param {...*}   [args]   Optional replacement arguments for the template.
         * @return {String}
         * @example
         *
         * var translatable = LOCALE.translatable;                                                                  // ## no extract maketext
         * var template = translatable("What is this [numf,_1] thing.");                                            // ## no extract maketext
         * ...
         * var localized = LOCALE.makevar(template)
         *
         * or
         *
         * var template = LOCALE.translatable("What is this [numf,_1] thing.");                                     // ## no extract maketext
         * ...
         * var localized = LOCALE.makevar(template)
         *
         */
        makevar: _maketext, // this is a marker method that is ignored in phrase harvesting, but is functionally equivalent to maketext otherwise. // ## no extract maketext

        /**
         * Marks the phrase as translatable for the harvester.                                                      // ## no extract maketext
         *
         * @method translatable                                                                                     // ## no extract maketext
         * @param  {String} str Translatable string
         * @return {Strung}     Same string, this is just a marker function for the harvester
         */
        translatable: function(str) { // ## no extract maketext
            return str;
        },

        _locale_tag: null,

        /**
         * Get the language tag for this locale.
         * @return {[type]} [description]
         */
        get_language_tag: function() {
            return this._locale_tag;
        },

        /**
         * Returns an instance of Intl.Collator for the current locale.
         * @return {[type]} [description]
         */
        getCollator: function getCollator() {
            if (!this._collator) {
                var localeTag = this.get_language_tag();

                try {
                    this._collator = new Intl.Collator(localeTag);
                } catch (e) {
                    // eslint-disable-next-line no-console
                    console.info("Failed to create collator for locale: " + localeTag + "; falling back to “en”", e);

                    this._collator = new Intl.Collator("en");
                }
            }

            return this._collator;
        },

        // These methods are locale-independent and should not need overrides.

        /**
         * Join a list with the provided separator.
         * @param  {[type]} sep  [description]
         * @param  {[type]} list [description]
         * @return {[type]}      [description]
         * @example
         *
         *  LOCALE.maketext("We have [join,,,_1].", ["fruit", "nuts", "raisins"]);  // ## no extract maketext
         */
        join: function(sep, list) {
            sep = String(sep);

            if ( typeof list === "object" ) {
                return list.join(sep);
            } else {
                var str = String(arguments[1]);
                for (var a = 2; a < arguments.length; a++) {
                    str += sep + arguments[a];
                }
                return str;
            }
        },

        /**
         * Boolean operator.
         * Perl has undef, but JavaScript has both null *and* undefined.
         * Let's treat null as undefined since JSON doesn't know what
         * undefined is, so serializers use null instead.
         * @param  {Boolean} condition  [description]
         * @param  {String} when_true  String to write when condition is true
         * @param  {String} when_false String to write when condition is false
         * @param  {String} [when_null]  String to write when condition is null or undefined
         * @return {String}
         * @example
         *
         *  LOCALE.maketext("We have [boolean,_1,a,no] banana.", bananas); // ## no extract maketext
         */
        "boolean": function(condition, when_true, when_false, when_null) {
            if (condition) {
                return "" + when_true;
            }

            if ( ((arguments.length > 3) && (condition === null || condition === undefined)) ) {
                return "" + when_null;
            }

            return "" + when_false;
        },

        /**
         * Comment operator
         * @return {String} Returns nothing
         * @example
         *
         *  LOCALE.maketext("We have only bananas. [comment,This does nothing]"); // ## no extract maketext
         */
        comment: function() {
            return "";
        },

        //

        /**
         * Output operator for bracket notation. Acts as "dispatch" function for the
         * output_* methods below.
         * @param  {String} sub_func Name of the output function to process
         * @param  {String} str      String to pass to the output function
         * @return {String}          Processed output.
         */
        output: function( sub_func, str ) {
            var that = this;

            var sub_args = Array.prototype.concat.apply([], arguments).slice(1);

            // Implementation of the chr() and amp() embeddable methods
            if ( sub_args && typeof sub_args[0] === "string" ) {
                sub_args[0] = sub_args[0].replace(/chr\((\d+|\S)\)/g, function(str, p1) {
                    return that.output_chr(p1);
                });
                sub_args[0] = sub_args[0].replace(/amp\(\)/g, function(str) {
                    return that.output_amp();
                });
            }

            var func_name = "output_" + sub_func;
            if ( typeof this[func_name] === "function" ) {
                return this[func_name].apply(this, sub_args);
            } else {
                if (window.console) {
                    window.console.warn("Locale output function \"" + sub_func + "\" is not implemented.");
                }
                return str;
            }
        },

        /**
         * Output an HTML safe apostrophe
         * @return {String}
         */
        output_apos: function() {
            return html_apos;
        },

        /**
         * Output an HTML safe quote mark
         * @return {String}
         */
        output_quot: function() {
            return html_quot;
        },

        // TODO: Implement embeddable methods described at
        // https://metacpan.org/pod/Locale::Maketext::Utils#asis()
        output_asis: String,
        asis: String,

        /**
         * Output the string wrapped in a <u> HTML tag
         * @param  {String} str
         * @return {String}
         */
        output_underline: function(str) {
            return "<u>" + str + "</u>";
        },

        /**
         * Output the string wrapped in a <strong> HTML tag
         * @param  {String} str
         * @return {String}
         */
        output_strong: function(str) {
            return "<strong>" + str + "</strong>";
        },

        /**
         * Output the string wrapped in a <em> HTML tag
         * @param  {String} str
         * @return {String}
         */
        output_em: function(str) {
            return "<em>" + str + "</em>";
        },

        /**
         * Output the string wrapped in a <abbr> HTML tag
         * @param  {String} abbr Abbreviation
         * @param  {String} full Full version of the abbreviation
         * @return {String}
         */
        output_abbr: function(abbr, full) {
            return "<abbr title=\"__FULL__\">".replace(/__FULL__/, full) + abbr + "</abbr>";
        },

        /**
         * Output the string wrapped in a <abbr> HTML tag with special markings
         * @param  {String} abbr Acronym
         * @param  {String} full Full version of the acronym
         * @return {String}
         */
        output_acronym: function(abbr, full) {

            // TODO: Is this still right with bootstrap???
            return this.output_abbr(abbr, full).replace(/^(<[a-z]+)/i, "$1 class=\"initialism\"");
        },

        /**
         * Output the string wrapped in a <span> HTML tag with the provided classes
         * @param {String} str String to embed in the span.
         * @param {String...} list of classes as arguments.
         * @return {[type]}     [description]
         */
        output_class: function(str) {
            var classes = Array.prototype.slice.call( arguments, 1 );
            return "<span class=\"" + classes.join(" ") + "\">" + str + "</span>";
        },

        /**
         * Output the requested character encoded as an HTML character.
         * @param  {Number} num Character code to output.
         * @return {String}
         */
        output_chr: function(num) {
            return isNaN(+num) ? String(num) : _.escape(String.fromCharCode(num));
        },

        /**
         * Output the HTML escaped version of an ampersand.
         * @return {String}
         */
        output_amp: function() {
            return html_amp;
        },

        /**
         * Output a url from the input. There are multiple forms possible:
         *  A) output_url( dest, text, [ config_obj ] )
         *  B) output_url( dest, text, [ key1, val1, [...] ] )
         *  C) output_url( dest, [ config_obj ] )
         *  D) output_url( dest, [ key1, val1, [...] ] )
         * @param  {String} dest Url to link the results too.
         * @return {String}
         */
        output_url: function(dest) {
            var
                args_length = arguments.length,
                config = arguments[args_length - 1],
                text,
                key,
                value,
                start_i,
                a,
                len;

            // object properties hash, form A or C
            if ( typeof config === "object" ) {
                text = (args_length === 3) ? arguments[1] : (config.html || dest);

                // Go ahead and clobber other stuff.
                if ( "_type" in config && config._type === "offsite" ) {
                    config["class"] = "offsite";
                    config.target = "_blank";
                    delete config._type;
                }
            } else {
                config = {};

                if (args_length % 2) {
                    start_i = 1;
                } else {
                    text = arguments[1];
                    start_i = 2;
                }
                a = start_i;
                len = arguments.length;
                while ( a < len ) {
                    key = arguments[a];
                    value = arguments[++a];
                    if (key === "_type" && value === "offsite") {
                        config.target = "_blank";
                        config["class"] = "offsite";
                    } else {
                        config[key] = value;
                    }
                    a++;
                }

                if (!text) {
                    text = config.html || dest;
                }
            }

            var html = "<a href=\"" + dest + "\"";
            if ( typeof config === "object" ) {
                for (key in config) {
                    if (config.hasOwnProperty(key)) {
                        html += " " + key + "=\"" + config[key] + "\"";
                    }
                }
            }
            html += ">" + text + "</a>";

            return html;
        },


        // Flattening argument lists in JS is much hairier than in Perl,
        // so this doesn't flatten array objects. Hopefully CLDR will soon
        // implement list_or; then we could deprecate this function.
        // cf. http://unicode.org/cldr/trac/ticket/4051
        list_separator: ", ",
        oxford_separator: ",",
        list_default_and: "&",


        /**
         * Maketext list operator
         * @param  {String} word Type of list to process. Examples are 'and' and 'or'.
         * @return {[type]}      [description]
         */
        list: function(word /* , [foo,bar,...] | foo, bar, ... */) {
            if (!word) {
                word = this.list_default_and;  // copying our Perl
            }
            var list_sep   = this.list_separator;
            var oxford_sep = this.oxford_separator;

            var the_list;
            if (typeof arguments[1] === "object" && arguments[1] instanceof Array) {
                the_list = arguments[1];
            } else {
                the_list = Array.prototype.concat.apply([], arguments).slice(1);
            }

            var len = the_list.length;

            if (!len) {
                return "";
            }

            if (len === 1) {
                return String(the_list[0]);
            } else if (len === 2) {
                return (the_list[0] + " " + word + " " + the_list[1]);
            } else {

                // Use slice() here to avoid altering the array
                // since it may have been passed in as an object.
                return (the_list.slice(0, -1).join(list_sep) + [oxford_sep, word, the_list.slice(-1)].join(" "));
            }
        },


        /**
         * Formats bytes with the specific decimal places.
         * This depends on locale-specific overrides of base functionality
         * but should not itself need an override.
         * @param  {[type]} bytes          [description]
         * @param  {[type]} decimal_places [description]
         * @return {[type]}                [description]
         */
        format_bytes: function(bytes, decimal_places) {

            if ( decimal_places === undefined ) {
                decimal_places = 2;
            }

            bytes = Number(bytes);

            var exponent = bytes && Math.min( Math.floor( Math.log(bytes) / Math.log(1024) ), data_abbreviations.length );
            if ( !exponent ) {

                // This is a special, internal-to-format_bytes, phrase: developers will not have to deal with this phrase directly.
                return this.maketext( "[quant,_1,%s byte,%s bytes]", bytes ); // the space between the '%s' and the 'b' is a non-break space (e.g. option-spacebar, not spacebar)
                // We do not use &nbsp; or \u00a0 since:
                //   * parsers would need to know how to interpolate them in order to work with the phrase in the context of the system
                //   * the non-breaking space character behaves as you'd expect its various representations to.
                // Should a second instance of this sort of thing happen we can revisit the idea of adding [comment] in the phrase itself or perhaps supporting an embedded call to [output,nbsp].
            } else {

                // We use \u00a0 here because it won't affect lookup since it is not
                // being used in a source phrase and we don't want to worry about
                // whether an entity is going to be interpreted or not.
                return this.numf(bytes / Math.pow(1024, exponent), decimal_places) + "\u00a0" + data_abbreviations[exponent - 1];
            }
        },

        // CLDR-informed functions

        /**
         * Maketext numerate operator
         * @param  {Number} num Quantity to numerate.
         * @return {String}
         */
        numerate: function(num) {
            if ( this.get_plural_form ) {   // from CPAN Locales
                var numerated = this.get_plural_form.apply(this, arguments)[0];
                if (numerated === undefined) {
                    numerated = arguments[arguments.length - 1];
                }
                return numerated;
            } else {

                // English-language logic, in the absence of CLDR
                // The -1 case here is debatable.
                // cf. http://unicode.org/cldr/trac/ticket/4049
                var abs = Math.abs(num);

                if (abs === 1) {
                    return "" + arguments[1];
                } else if (abs === 0) {
                    return "" + arguments[ arguments.length - 1 ];
                } else {
                    return "" + arguments[2];
                }
            }
        },

        /**
         * Maketext quant operator..
         * @param  {Number} num Quantity on which the output depends.
         * @return {[type]}     [description]
         */
        quant: function(num) {
            var numerated,
                is_special_zero,
                decimal_places = 3;

            if ( num instanceof Array ) {
                decimal_places = num[1];
                num = num[0];
            }

            if ( this.get_plural_form ) {

                // from CPAN Locales
                var gpf = this.get_plural_form.apply(this, arguments);
                numerated = gpf[0];

                // If there's a mismatch between the actual number of forms
                // (singular, plural, etc.) and the real number, this can be
                // undefined, which can break code.  We pick the rightmost, or
                // "most plural," form as a fallback.
                if (numerated === undefined) {
                    numerated = arguments[arguments.length - 1];
                }
                is_special_zero = gpf[1];
            } else {

                // no CLDR, fall back to English
                numerated = this.numerate.apply(this, arguments);

                // Check: num is 0, we gave a special_zero value, and that numerate() gave it
                is_special_zero = (parseInt(num, 10) === 0) &&
                    ( arguments.length > 3 ) &&
                    ( numerated === String(arguments[3]) )
                ;
            }
            var formatted = this.numf(num, decimal_places);

            if (numerated.indexOf("%s") !== -1) {
                return numerated.replace(/%s/g, formatted);
            }

            if (is_special_zero) {
                return numerated;
            }

            return this.is_rtl() ? (numerated + " " + formatted) : (formatted + " " + numerated);
        },

        _max_decimal_places: 6,

        /**
         * [numf description]
         * @param  {[type]} num            [description]
         * @param  {[type]} decimal_places [description]
         * @return {[type]}                [description]
         */
        numf: function(num, decimal_places) {
            if ( decimal_places === undefined ) {
                decimal_places = this._max_decimal_places;
            }

            // exponential -> don't know how to deal
            if (/e/.test(num)) {
                return String(num);
            }

            var cldr, decimal_format, decimal_group, decimal_decimal;
            try {
                cldr = this.get_cldr("misc_info").cldr_formats;
                decimal_format = cldr.decimal;
                decimal_group = cldr._decimal_format_group;
                decimal_decimal = cldr._decimal_format_decimal;
            } catch (e) {}

            // No CLDR, so fall back to hard-coded English values.
            if (!decimal_format || !decimal_group || !decimal_decimal) {
                decimal_format = "#,##0.###";
                decimal_group = ",";
                decimal_decimal = ".";
            }

            var is_negative = num < 0;
            num = Math.abs(num);

            // Trim the decimal part to 6 digits and round
            var whole = Math.floor(num);
            var normalized, fraction;
            if ( /(?!')\.(?!')/.test(num) ) {

                // This weirdness is necessary to avoid floating-point
                // errors that can crop up with large-ish numbers.

                // Convert to a simple fraction.
                fraction = String(num).replace(/^[^.]+/, "0");

                // Now round to the desired precision.
                fraction = Number(fraction).toFixed(decimal_places);

                // e.g., 1.9999 when only 3 decimal places are desired.
                if (/^1/.test(fraction)) {
                    whole++;
                    num = whole;
                    fraction = undefined;
                    normalized = num;
                } else {
                    fraction = fraction.replace(/^.*\./, "").replace(/0+$/, "");
                    normalized = Number( whole + "." + fraction );
                }
            } else {
                normalized = num;
            }

            var pattern_with_outside_symbols;
            if ( /(?!');(?!')/.test(decimal_format) ) {
                pattern_with_outside_symbols = decimal_format.split(/(?!');(?!')/)[ is_negative ? 1 : 0 ];
            } else {
                pattern_with_outside_symbols = (is_negative ? "-" : "") + decimal_format;
            }

            var inner_pattern = pattern_with_outside_symbols.match(/[0#].*[0#]/)[0];

            // Applying the integer part of the pattern is much easier if it's
            // done with the strings reversed.
            var pattern_split = inner_pattern.split(/(?!')\.(?!')/);
            var int_pattern_split = pattern_split[0].split("").reverse().join("").split(/(?!'),(?!')/);

            // If there is only one part of the int pattern, then set the "joiner"
            // to empty string. (http://unicode.org/cldr/trac/ticket/4094)
            var group_joiner;
            if (int_pattern_split.length === 1) {
                group_joiner = "";
            } else {

                // Most patterns look like #,##0.###, for which the leftmost # is
                // just a placeholder so we know where to put the group separator.
                int_pattern_split.pop();
                group_joiner = decimal_group;
            }

            var whole_reverse = String(whole).split("").reverse();
            var whole_assembled = [];  // reversed
            var pattern;
            var replacer = function(chr) {
                switch (chr) {
                    case "#":
                        return whole_reverse.shift() || "";
                    case "0":
                        return whole_reverse.shift() || "0";
                }
            };
            while ( whole_reverse.length ) {
                if ( int_pattern_split.length ) {
                    pattern = int_pattern_split.shift();
                }

                // Since this is reversed, we can just replace a character
                // at a time, in regular forward order. Make sure we leave quoted
                // stuff alone while paying attention to stuff *by* quoted stuff.
                var assemble_chunk = pattern
                    .replace(/(?!')[0#]|[0#](?!')/g, replacer )
                    .replace(/'([.,0#;¤%E])'$/, "")
                    .replace(/'([.,0#;¤%E])'/, "$1")
                ;

                whole_assembled.push(assemble_chunk);
            }

            var formatted_num = whole_assembled.join(group_joiner).split("").reverse().join("") + ( fraction ? decimal_decimal + fraction : "" );
            return pattern_with_outside_symbols.replace(/[0#].*[0#]/, formatted_num);
        },

        // This *may* be useful publicly.
        _quote: function(str) {
            var delimiters;
            try {
                delimiters = this.get_cldr("misc_info").delimiters;
            } catch (e) {
                delimiters = {
                    quotation_start: "“",
                    quotation_end: "”"
                };
            }
            return delimiters["quotation_start"] + str + delimiters["quotation_end"];
        },

        /**
         * Quotes each value and then returns a localized “and”-list of them.
         *
         * Accepts either a list of arguments or a single array of arguments.
         *
         * @return {String} The localized list of quoted items.
         */
        list_and_quoted: function() {
            return this._list_quoted("list_and", arguments);
        },

        /**
         * Quotes each value and then returns a localized “and”-list of them.
         *
         * Accepts either a list of arguments or a single array of arguments.
         *
         * @return {String} The localized list of quoted items.
         */
        list_or_quoted: function() {
            return this._list_quoted("list_or", arguments);
        },

        _list_quoted: function(join_fn, args) {
            var the_list;
            if (typeof (args[0]) === "object") {
                if (args[0] instanceof Array) {

                    // slice() so that we don’t change the caller’s data
                    the_list = args[0].slice();
                } else {
                    throw ( "Unrecognized list_and_quoted() argument: " + args[0].toString() );
                }
            } else {
                the_list = Array.prototype.slice.apply(args);
            }

            // Emulate Locales.pm _quote_get_list_items() list_quote_mode 'all'.
            // list_or(), currently not implemented in JS (no reason for it not to be), will need to behave the same
            if (the_list === undefined || the_list.length === 0) {

                the_list = [""]; // disambiguate no args
            }

            // The CJT1 code actually writes out the list_and() logic again.
            // There doesn’t seem to be a good reason for that … ??
            return this[join_fn](the_list.map( _.bind(this._quote, this) ) );
        },

        /**
         * [list_and description]
         * @return {[type]} [description]
         */
        list_and: function() {
            return this._list_join_cldr("list", arguments);
        },

        list_or: function() {
            return this._list_join_cldr("list_or", arguments);
        },


        _list_join_cldr: function(templates_name, args) {
            var the_list;
            if ( (typeof args[0] === "object") && args[0] instanceof Array ) {
                the_list = args[0];
            } else {
                the_list = args;
            }

            var cldr_list;
            var len = the_list.length;
            var pattern;
            var text;

            try {
                cldr_list = this.get_cldr("misc_info").cldr_formats[templates_name];
            } catch (e) {

                // Use hard-coded English below if we don't have CLDR.

                var conjunction = (templates_name === "list_or") ? "or" : "and";

                cldr_list = {
                    2: "{0} " + conjunction + " {1}",
                    start: "{0}, {1}",
                    middle: "{0}, {1}",
                    end: "{0}, " + conjunction + " {1}",
                };
            }

            var replacer = function(str, p1) {
                switch (p1) {
                    case "0":
                        return text;
                    case "1":
                        return the_list[i++];
                }
            };

            switch (len) {
                case 0:
                    return;
                case 1:
                    return String(the_list[0]);
                default:
                    if ( len === 2 ) {
                        text = cldr_list["2"];
                    } else {
                        text = cldr_list.start;
                    }

                    text = text.replace(/\{([01])\}/g, function(all, bit) {
                        return the_list[bit];
                    });
                    if (len === 2) {
                        return text;
                    }

                    var i = 2;
                    while ( i < len ) {
                        pattern = cldr_list[ (i === len - 1) ? "end" : "middle" ];

                        text = pattern.replace(/\{([01])\}/g, replacer );
                    }

                    return text;
            }
        },

        /**
         * [_apply_quote_types description]
         * @param  {[type]} quotee    [description]
         * @param  {[type]} starttype [description]
         * @param  {[type]} endtype   [description]
         * @return {[type]}           [description]
         */
        _apply_quote_types: function( quotee, starttype, endtype ) {
            if (quotee === undefined) {
                return;
            }

            var delimiters = this.get_cldr().misc_info.delimiters;

            return delimiters[starttype] + quotee + delimiters[endtype];
        },

        /**
         * [quote description]
         * @param  {[type]} quotee [description]
         * @return {[type]}        [description]
         */
        quote: function( quotee ) {
            return this._apply_quote_types( quotee, "quotation_start", "quotation_end" );
        },

        /**
         * [alt_quote description]
         * @param  {[type]} quotee [description]
         * @return {[type]}        [description]
         */
        alt_quote: function( quotee ) {
            return this._apply_quote_types( quotee, "alternate_quotation_start", "alternate_quotation_end" );
        },

        /**
         * [_quote_list description]
         * @param  {[type]} quote_method [description]
         * @param  {[type]} list_method  [description]
         * @param  {[type]} items_obj    [description]
         * @return {[type]}              [description]
         */
        _quote_list: function( quote_method, list_method, items_obj ) {
            if ( (typeof items_obj[0] === "object") && (items_obj[0] instanceof Array) ) {
                items_obj = items_obj[0];
            }

            var quoted = [];
            for (var i = items_obj.length - 1; i >= 0; i--) {
                quoted[i] = this[quote_method]( items_obj[i] );
            }

            return this[list_method]( quoted );
        },

        /**
         * [quote_list_and description]
         * @return {[type]} [description]
         */
        quote_list_and: function() {
            return this._quote_list( "quote", "list_and", arguments );
        },

        /**
         * [alt_quote_list_and description]
         * @return {[type]} [description]
         */
        alt_quote_list_and: function() {
            return this._quote_list( "alt_quote", "list_and", arguments );
        },

        /* NOT IMPLEMENTED, pending a list_or() implementation
        quote_list_or : function( items ) {
            return this._quote_list( "quote", "list_or", items );
        },

        alt_quote_list_or : function( items ) {
            return this._quote_list( "alt_quote", "list_or", items );
        },
        */

        /**
         * [local_datetime description]
         * @param  {[type]} my_date       [description]
         * @param  {[type]} format_string [description]
         * @return {[type]}               [description]
         */
        local_datetime: function( my_date, format_string ) {
            if (!this._cldr) {
                return this.datetime.apply(this, arguments);
            }

            if ( my_date instanceof Date ) {
                my_date = new Date(my_date);
            } else if ( /^-?\d+$/.test(my_date) ) {
                my_date = new Date(my_date * 1000);
            } else {
                my_date = new Date();
            }

            var tz_offset = my_date.getTimezoneOffset();

            my_date.setMinutes( my_date.getMinutes() - tz_offset );

            var non_utc = this.datetime( my_date, format_string );

            // This is really hackish...but should be safe.
            if ( non_utc.indexOf("UTC") > -1 ) {
                var hours = (tz_offset > 0) ? "-" : "+";
                hours += _.padStart(Math.floor(Math.abs(tz_offset) / 60).toString(), 2, "0");
                var minutes = _.padStart((tz_offset % 60).toString(), 2, "0");
                non_utc = non_utc.replace("UTC", "GMT" + hours + minutes);
            }

            return non_utc;
        },

        // time can be either epoch seconds or a JS Date object
        // format_string can match the regexp below or be a [ date, time ] suffix pair
        // (e.g., [ "medium", "short" ] -> "Aug 30, 2011 5:12 PM")

        /**
         * Format dates according to CLDR.
         * This logic should stay in sync with Cpanel::Date::Format in Perl.
         *
         * @param  {[type]} my_date       [description]
         * @param  {[type]} format_string [description]
         * @return {[type]}               [description]
         */
        datetime: function datetime( my_date, format_string ) {
            if ( !my_date && (my_date !== 0) ) {
                my_date = new Date();
            } else if ( !(my_date instanceof Date) ) {
                my_date = new Date(my_date * 1000);
            }

            var loc_strs = this.get_cldr("datetime");

            if ( !loc_strs ) {
                return my_date.toString();
            }

            if ( format_string ) {

                // Make sure we don't just grab any random CLDR datetime key.
                if ( /^(?:date|time|datetime|special)_format_/.test(format_string) ) {
                    format_string = loc_strs[format_string];
                }
            } else {
                format_string = loc_strs.date_format_long;
            }

            /**
             * [substituter description]
             * @return {[type]} [description]
             */
            var substituter = function() {

                // Check for quoted strings
                if (arguments[1]) {
                    return arguments[1].substr( 1, arguments[1].length - 2 );
                }

                // No quoted string, eh? OK, let’s check for a known pattern.
                var key = arguments[2];
                var xformed = ( function() {
                    switch (key) {
                        case "yy":
                            return Math.abs(my_date.getUTCFullYear()).toString().slice(-2);
                        case "y":
                        case "yyy":
                        case "yyyy":
                            return Math.abs(my_date.getUTCFullYear());
                        case "MMMMM":
                            return loc_strs.month_format_narrow[my_date.getUTCMonth()];
                        case "LLLLL":
                            return loc_strs.month_stand_alone_narrow[my_date.getUTCMonth()];
                        case "MMMM":
                            return loc_strs.month_format_wide[my_date.getUTCMonth()];
                        case "LLLL":
                            return loc_strs.month_stand_alone_wide[my_date.getUTCMonth()];
                        case "MMM":
                            return loc_strs.month_format_abbreviated[my_date.getUTCMonth()];
                        case "LLL":
                            return loc_strs.month_stand_alone_abbreviated[my_date.getUTCMonth()];
                        case "MM":
                        case "LL":
                            return _.padStart((my_date.getUTCMonth() + 1).toString(), 2, "0");
                        case "M":
                        case "L":
                            return my_date.getUTCMonth() + 1;
                        case "EEEE":
                            return loc_strs.day_format_wide[ get_cldr_day(my_date) ];
                        case "EEE":
                        case "EE":
                        case "E":
                            return loc_strs.day_format_abbreviated[ get_cldr_day(my_date) ];
                        case "EEEEE":
                            return loc_strs.day_format_narrow[ get_cldr_day(my_date) ];
                        case "cccc":
                            return loc_strs.day_stand_alone_wide[ get_cldr_day(my_date) ];
                        case "ccc":
                        case "cc":
                        case "c":
                            return loc_strs.day_stand_alone_abbreviated[ get_cldr_day(my_date) ];
                        case "ccccc":
                            return loc_strs.day_stand_alone_narrow[ get_cldr_day(my_date) ];
                        case "dd":
                            return _.padStart(my_date.getUTCDate().toString(), 2, "0");
                        case "d":
                            return my_date.getUTCDate();
                        case "h":
                        case "hh":
                            var twelve_hours = my_date.getUTCHours();
                            if ( twelve_hours > 12 ) {
                                twelve_hours -= 12;
                            }
                            if ( twelve_hours === 0 ) {
                                twelve_hours = 12;
                            }
                            return ( key === "hh" ) ? _.padStart(twelve_hours.toString(), 2, "0") : twelve_hours;
                        case "H":
                            return my_date.getUTCHours();
                        case "HH":
                            return _.padStart(my_date.getUTCHours().toString(), 2, "0");
                        case "m":
                            return my_date.getUTCMinutes();
                        case "mm":
                            return _.padStart(my_date.getUTCMinutes().toString(), 2, "0");
                        case "s":
                            return my_date.getUTCSeconds();
                        case "ss":
                            return _.padStart(my_date.getUTCSeconds().toString(), 2, "0");
                        case "a":
                            var hours = my_date.getUTCHours();
                            if (hours < 12) {
                                return loc_strs.am_pm_abbreviated[0];
                            } else if ( hours > 12 ) {
                                return loc_strs.am_pm_abbreviated[1];
                            }

                            // CLDR defines "noon", but CPAN DateTime::Locale doesn't have it.
                            return loc_strs.am_pm_abbreviated[1];
                        case "z":
                        case "zzzz":
                        case "v":
                        case "vvvv":
                            return "UTC";
                        case "G":
                        case "GG":
                        case "GGG":
                            return loc_strs.era_abbreviated[ my_date.getUTCFullYear() < 0 ? 0 : 1 ];
                        case "GGGGG":
                            return loc_strs.era_narrow[ my_date.getUTCFullYear() < 0 ? 0 : 1 ];
                        case "GGGG":
                            return loc_strs.era_wide[ my_date.getUTCFullYear() < 0 ? 0 : 1 ];
                    }

                    if (window.console) {
                        // eslint-disable-next-line no-console
                        console.warn("Unknown CLDR date/time pattern: " + key + " (" + format_string + ")" );
                    }
                    return key;
                } )();

                return xformed;
            };

            return format_string.replace(
                /('[^']+')|(([a-zA-Z])\3*)/g,
                substituter
            );
        },

        /**
         * [is_rtl description]
         * @return {Boolean} [description]
         */
        is_rtl: function() {
            try {
                return this.get_cldr("misc_info").orientation.characters === "right-to-left";
            } catch (e) {
                return false;
            }
        },

        /**
        * Shorten a string into one or two end fragments, using CLDR formatting.
        *
        * ex.: elide( "123456", 2 )    //"12…"
        * ex.: elide( "123456", 2, 2 ) //"12…56"
        * ex.: elide( "123456", 0, 2 ) //"…56"
        *
        * @param str     {String} The actual string to shorten.
        * @param start_length {Number} How many initial characters to put into the result.
        * @param end_length {Number} How many final characters to put into the result. (optional)
        * @return        {String} The processed string.
        */
        elide: function(str, start_length, end_length) {
            start_length = start_length || 0;
            end_length = end_length || 0;

            if (str.length <= (start_length + end_length)) {
                return str;
            }

            var template, substring0, substring1;
            if (start_length) {
                if (end_length) {
                    template = "medial";
                    substring0 = str.substr(0, start_length);
                    substring1 = str.substr( str.length - end_length );
                } else {
                    template = "final";
                    substring0 = str.substr(0, start_length);
                }
            } else if (end_length) {
                template = "initial";
                substring0 = str.substr( str.length - end_length );
            } else {
                return "";
            }

            try {
                template = this._cldr.misc_info.cldr_formats.ellipsis[template]; // JS reserved word
            } catch (e) {
                template = DEFAULT_ELLIPSIS[template];
            }

            if (substring1) {   // medial
                return template
                    .replace( "{0}", substring0 )
                    .replace( "{1}", substring1 )
                ;
            }

            return template.replace( "{0}", substring0 );
        },

        /**
         * [get_first_day_of_week description]
         * @return {[type]} [description]
         */
        get_first_day_of_week: function() {
            var fd = Number(this.get_cldr("datetime").first_day_of_week) + 1;
            return (fd === 8) ? 0 : fd;
        },

        /**
         * [set_cldr description]
         * @param {[type]} cldr [description]
         */
        set_cldr: function(cldr) {
            var cldr_obj = this._cldr;
            if ( !cldr_obj ) {
                cldr_obj = this._cldr = {};
            }
            for (var key in cldr) {
                if (cldr.hasOwnProperty(key)) {
                    cldr_obj[key] = cldr[key];
                }
            }
        },

        /**
         * [get_cldr description]
         * @param  {[type]} key [description]
         * @return {[type]}     [description]
         */
        get_cldr: function(key) {
            if ( !this._cldr ) {
                return;
            }

            if ((typeof key === "object") && (key instanceof Array)) {
                return key.map(this.get_cldr, this);
            } else {
                return key ? this._cldr[key] : this._cldr;
            }
        },

        /**
         * For testing. Don't "delete" since this will cause prototype traversal.
         * @return {[type]} [description]
         */
        reset_cldr: function() {
            this._cldr = undefined;
        },

        _cldr: null
    });

    // Annotate the class hierarchy for introspection.
    Locale.prototype.constructor = Locale;
    Locale.prototype.parent = Object;

    Locale.generateClassFromCldr = function(tag, cldr) {

        // Create a custom class for the locale generated from the CLDR data.
        var GeneratedLocale = function() {
            GeneratedLocale.prototype.parent.apply(this, arguments);
            this.set_cldr( { datetime: cldr.datetime_info } );
            this.set_cldr( { misc_info: cldr.misc_info } );
        };

        GeneratedLocale.prototype = new Locale();

        // Annotate the class hierarchy for introspection.
        GeneratedLocale.prototype.constructor = GeneratedLocale;
        GeneratedLocale.prototype.parent = Locale;

        // Mix in the the CLDR locale functions into the new class.
        for (var key in cldr.functions) {
            if ( cldr.functions.hasOwnProperty(key)) {
                GeneratedLocale.prototype[key] = cldr.functions[key];
            }
        }

        // Add the new locale class to the collection
        Locale.add_locale(tag, GeneratedLocale);
    };

    var tag;
    if (window.CJT2_loader && window.CJT2_loader.CLDR && window.CJT2_loader.current_locale) {
        tag = window.CJT2_loader.current_locale;
        Locale.generateClassFromCldr(tag, window.CJT2_loader.CLDR[tag]);
    }

    return Locale.get_handle(tag);
});
Back to Directory File Manager