/** This file is part of Reactor.
 *  Copyright (c) 2020-2025 Kedron Holdings LLC, All Rights Reserved.
 *  Reactor is not public domain or open source. Distribution or derivative works are expressly prohibited.
 */

/** The "process" global does not exist outside of nodejs (i.e. in browsers) currently,
 *  but we're declaring it global here anyway because the code here, which is intended
 *  to be identical to that on the nodejs/server side, uses it (conditionally, based on
 *  its actual availability). Declaring it below doesn't make it exist, it just quiets
 *  eslint from reporting an error we're already handling explicitly.
 */
/* global process */

export const version = 25141;

const default_short_months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ];
const default_full_months = [ 'January', 'February', 'March',     'April',   'May',      'June',
                              'July',    'August',   'September', 'October', 'November', 'December' ];
const default_long_weekdays = [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ];
const default_short_weekdays = [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ];

import m from '../../localeData.js';  /* Loads language data based on browser accept-language header */
console.log("localeData.js", m);
const localeData = m;

/** Function map assists strftime() formatting. */
const f_map = {
    // a: abbreviated day of week
    a: { pad: ' ', impl: (d) => ( getWeekdayAbbrevNames() )[ d.getDay() ] },
    // A: full day of week
    A: { pad: ' ', impl: (d) => ( getWeekdayFullNames() )[ d.getDay() ] },
    // b: abbreviated month name
    b: { pad: ' ', impl: (d) => ( getMonthAbbrevNames() )[ d.getMonth() ] },
    // B: full month name
    B: { pad: ' ', impl: (d) => ( getMonthFullNames() )[ d.getMonth() ] },
    // c: preferred representation in current locale (macro???)
    // Century number, two-digit
    C: { len: 2, pad: '0', impl: (d) => Math.floor( d.getFullYear() / 100 ) },
    // d: day of month 1-31, two-digit
    d: { len: 2, pad: '0', impl: (d) => d.getDate() },
    // D: %m/%d/%y expansion, see below
    // e: day of month, 1-31 space padded
    e: { len: 2, pad: ' ', impl: (d) => d.getDate() },
    // E: format? missing expansion???
    // f: milliseconds
    f: { len: 3, pad: '0', impl: (d) => d.getTime() % 1000 },
    // F: %Y-%m-%d (ISO 8601 date format) expansion, see below
    // G: ISO 8601 week-based year with century as decimal -- see man page
    // g: like G but without century
    // H: two-digit hours 0-24
    H: { len: 2, pad: '0', impl: (d) => d.getHours() },
    // h: synonym for %b
    // I: two-digit hours 01-12
    I: { len: 2, pad: '0', impl: (d) => {
        let h = d.getHours();
        if (h > 12) h -= 12;
        if (0 === h) h = 12;
        return h;
    }},
    // j: day of year 1 to 366 (Julian), 3 digits with leading 0s
    j: { len: 3, pad: '0', impl: (d) => ( Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()) - Date.UTC(d.getFullYear(), 0, 0) ) / 86400000 },
    // k: hour 0-24, space padded
    k: { len: 2, pad: ' ', impl: (d) => d.getHours() },
    // l: hour 1-12, space padded
    l: { len: 2, pad: ' ', impl: (d) => {
        let h = d.getHours();
        if (h > 12) h -= 12;
        if (0 === h) h = 12;
        return h;
    }},
    // m: month 01-12
    m: { len: 2, pad: '0', impl: (d) => d.getMonth() + 1 },
    // M: minutes 00-59
    M: { len: 2, pad: '0', impl: (d) => d.getMinutes() },
    // n: newline, not implemented (intentionally)
    // O: alt format?
    // p: AM or PM
    p: { case: 'U', impl: (d) => ( d.getHours() >= 12 ? ( localeData.time_meridiem.post || 'pm' ) :
        ( localeData.time_meridiem.ante || 'am' ) ).toLocaleUpperCase( localeData.locale ) },
    // P: am or pm
    P: { case: 'l', impl: (d) => ( d.getHours() >= 12 ? ( localeData.time_meridiem.post || 'pm' ) :
        ( localeData.time_meridiem.ante || 'am' ) ).toLocaleLowerCase( localeData.locale ) },
    // r: %I:%M:%S %p expansion, see below
    // R: %H:%M expansion, see below
    // s: Unix Epoch (w/fractional ms)
    s: (d) => Math.floor( d.getTime() / 1000 ),
    // S: seconds 00-59
    S: { len: 2, pad: '0', impl: (d) => d.getSeconds() },
    // t: tab, not implemented (intentionally)
    // T: %H:%M:%S expansion, see below
    // u: Day of week, 1=Mon, 7=Sun
    u: { pad: '0', impl: (d) => {
        let dow = d.getDay();
        return dow > 0 ? dow : 7;
    }},
    // U: week number of the current year as decimal, 00-53, starts with first Sunday as day 1 of week 1
    // V: ISO 8601 week number, 01-53 where week 1 is first week that has at least four days in new year
    // w: Day of week, 0=Sun, 6=Sat
    w: { pad: '0', impl: (d) => d.getDay() },
    // W: week number of current year as decimal, 00-53, week 1 starts first Monday of year
    // x: preferred (locale-specific) date representation
    x: (d) => d.toLocaleDateString( localeData.locale ),
    // X: preferred (locale-specific) time representation
    X: (d) => d.toLocaleTimeString( localeData.locale ),
    // y: two-digit year (00-99)
    y: { len: 2, pad: '0', impl: (d) => d.getFullYear() % 100 },
    // Y: four-digit year
    Y: { len: 4, pad: '0', impl: (d) => d.getFullYear() },
    // z: +-HHMM timezone offset from GMT
    z: (d) => {
        let z = -d.getTimezoneOffset();
        let h = Math.floor( z / 60 );
        return (h < 0 ? '-' : '+') + fill( Math.abs(h), '0', 2 ) + fill( z % 60, '0', 2 );
    },
    // Z: time zone abbreviation/name
    Z: () => Intl.DateTimeFormat( localeData.locale ).resolvedOptions().timeZone ||
        ( "undefined" !== typeof process ? process.env.TZ : '?' ),
    // +: date and time in *nix date(1) format
    // %: literal %
    '%': () => '%'
};

/** Map of some predefined date/time formats to their components */
const f_repl = {
    c: "%a %d %b %Y %r %Z",     // *nix date(1) format
    D: "%m/%d/%y",
    F: "%Y-%m-%d",
    r: "%I:%M:%S %p",
    R: "%H:%M",
    T: "%H:%M:%S"
};

function fill( val, fillChar, wid ) {
    let res = String( val );
    fillChar = fillChar || "0";
    wid = wid || res.length;
    if ( wid > 0 && res.length < wid ) {
        /* Left pad */
        res = fillChar.repeat( wid - res.length ) + res;
    } else if ( wid < 0 && res.length < -wid ) {
        res += fillChar.repeat( (-wid) - res.length );
    }
    return res;
}

/**
 * charLength - Get actual character length of a string. This accounts for international characters
 * like Chinese.
 */
export function charLength( str ) {
    /** ... Iterates over characters, not code units, so:
     * localeLength(`A\ud87e\udc04Z`) returns 3 because string is A你Z (one Chinese char betw A and Z)
     * Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length?retiredLocale=id
     */
    return [...str].length;
}

/**
 * htmlEncode - Unicode-safe HTML encoding.
 */
const fixedHTMLEntities = { 34:'&quot;', 38:'&amp;', 39:'&apos;', 60:'&lt;', 62:'&gt;' };
export function htmlEncode( str ) {
    let res = "";
    [...str].forEach( c => {
        const codePoint = c.codePointAt(0);
        if ( fixedHTMLEntities[ codePoint ] ) {
            res += fixedHTMLEntities[ codePoint ];
        } else if ( codePoint >= 32 && codePoint < 127 ) {
            res += String.fromCodePoint( codePoint );
        } else {
            res += `&#x${codePoint.toString(16)};`;
        }
    });
    return res;
}

/**
 * charSubstring - Since String.substring() works on indexes and not characters, a substring of a
 * string containing Chinese or other multi-byte characters may produce an invalid result string
 * or altered meaning by cutting characters in mid-code. This is a character-aware version of
 * substring.
 */
export function charSubstring( str, start, end ) {
    let ch = [...str];
    end = undefined === end ? ch.length : end;
    if ( end < start ) {
        let t = end;
        end = start;
        start = t;
    }
    let ndel = ch.length - end;
    ch.splice( end, ndel ); /* Remove end to EOS */
    ch.splice( 0, start ); /* Remove BOS to start */
    return String(ch);
}

export function strftime( fmt, dateObj ) {
    const mpat = /%([0_^#-]*)([0-9]*)([A-Za-z%])/;
    if ( "undefined" === typeof dateObj || null === dateObj )
        dateObj = new Date();
    else if ( "number" === typeof dateObj )
        dateObj = new Date( dateObj );
    else if ( "string" === typeof dateObj )
        dateObj = new Date( dateObj );

    // First, replace "macros" with actual patterns (e.g. %D becomes %m/%d/%y)
    const fp = Object.keys( f_repl );
    const r = new RegExp('%(' + fp.join('|') + ')');
    while ( true ) {
        const i = fmt.search( r );
        if (i < 0) break;
        let c = fmt.charAt(i+1);
        if (i === 0) {
            fmt = f_repl[c] + fmt.substr(2);
        } else {
            fmt = fmt.substr(0, i) + f_repl[c] + fmt.substr(i+2);
        }
    }

    // Now replace pattern elements
    let res = "";
    let p;
    while ( ( p = fmt.match( mpat ) ) ) {
        // console.log('matched!', p);
        let c = p[3];
        /* Establish default formatting options */
        let pad = '0', len = 0, modcase = false;
        let i = f_map[ c ];
        if ( "object" === typeof i ) {
            pad = i.pad || false;
            len = i.len || 0;
            modcase = i.case || false;
            i = i.impl;
        }
        /* Now establish overrides to formatting */
        const m = p[1].split( "" );
        m.forEach( c => {
            if ( "0" === c ) {
                pad = '0';
            } else if ( '_' === c ) {
                pad = ' ';
            } else if ( '-' === c ) {
                pad = false;
            } else if ( '^' === c ) {
                modcase = 'U';
            } else if ( '#' === c ) {
                modcase = 'S';
            }
        });
        if ( '' !== p[2] && ! isNaN( p[2] ) ) {
            len = parseInt( p[2] );
        }

        // Prepend anything before our substitution, then run the implementation for our substitution.
        res += fmt.substr( 0, p.index );
        if ( i ) {
            let v = i.call( this, dateObj );
            if ( 0 !== len && false !== pad ) {
                v = fill( v, pad, len );
            }
            if ( 'U' === modcase ) {
                v = v.toLocaleUpperCase( localeData.locale );
            } else if ( 'S' === modcase || 'l' === modcase ) {
                v = v.toLocaleLowerCase( localeData.locale );
            }
            res += v;
        } else {
            res += p[0]; // keep in string as-is if unrecognized
        }

        // Clip format to remainder. Move along. Move along.
        fmt = fmt.substr( p.index + p[0].length );
        // console.log('post sub, res is now ' + res + ', fmt is now ' + fmt);
    }
    res += fmt; // append whatever is left
    return res;
}

/**
 * Format a string using {} substitution formatting similar to python. The format token
 * is of the form [argIndex][:[flags][alignment][width][.precision][format], where:
 *      argIndex is the index of the argument (zero-based) to be inserted; if not given,
 *          the next argument is taken.
 *      flags (optional) control formatting of the value;
 *          # = for numeric formatting, the absolute value is used (never negative)
 *      alignment (optional) can be "<" "^" or ">" for left, center, or right alignment
 *          within the specified width (so it makes sense to specify width whenever using
 *          these alignment specifiers). The default alignment for d, e/E, f/F, and g/G
 *          formats is right; all others left.
 *      width (optional) width of the field in which the argument is displayed. If the
 *          argument is wider than the specified width, the field is grown, not truncated.
 *          If the width specification begins with a 0 (e.g. "04d"), the string is zero-
 *          padded; otherwise space padding is used to the width.
 *      precision (optional) the precision (number of decimals) shown on the value. This
 *          only applies to e/E, f/F, and g/G formats. If not specified, sufficient
 *          digits are shown to express the result, if possible. If specified, it must be
 *          in the range of 0-19.
 *      format (optional, "s" default) the format specification for the argument. The
 *          supported specifiers are:
 *              b - binary (argument must be integer)
 *              d - decimal (argument must be integer);
 *              e/E - exponential (scientific) format (e.g. 6.022e23). If "E", an upper-
 *                    case E is used before the exponent.
 *              f - fixed format (e.g. 1.2345, 186282, etc.).
 *              g/G - general format, switches between fixed and exponential automatically
 *                    based on magnitude and precision.
 *              o - octal (argument must be integer)
 *              s - string; this is the default format if no specifier at all is given.
 *              x/X - hexadecimal (argument must be integer), uses a-f (x) or A-F (X).
 *
 * If using this function for localization strings, do not use the {} without an index.
 * Localized data often changes word order, so specifying the index is more helpful in
 * making the translation. It should be obvious, then, that the order of parameters should
 * never change; add new parameters to the end, and use appropriate dummy/default arguments
 * for removals.
 */
export function format( cstr, ...args ) {
    let apos = 0;
    return cstr.replace( /{([^}]*)}/g, function( ...m ) {
        let cond, spec, indx = m[1];
        let width = false, prec = false, align = false, pad = ' ', absvalue = false,
            fixedWidth = false;
        let i = indx.indexOf( ':' );
        if ( i >= 0 ) {
            spec = indx.substring( i+1 );
            indx = indx.substring( 0, i );
            /* Look for ?<conditional> */
            i = spec.match( /^([^?]*)\?(.*)$/ );
            if ( i ) {
                spec = i[1];
                cond = i[2];
            } else {
                cond = false;
            }
        } else {
            spec = false;
        }
        /* Blank index means next arg -- NB feature should not be used for localized strings */
        let val;
        if ( "" !== indx ) {
            apos = parseInt( indx );
            if ( isNaN( apos ) || apos < 0 || apos >= args.length ) {
                apos = false;
            }
        } else if ( apos >= args.length ) {
            apos = false;
        }
        if ( false !== apos ) {
            val = args[ apos++ ];
        } else {
            val = m[0];
            spec = false;
            width = 0;
        }
        if ( cond ) {
            /* Check condition: ?[!][(<=?|==?|>=?|::?)]<string>, string may be regexp
             * If the condition is not matched, the empty string is returned for this
             * argument. Example: format( "{0:d} dog{0:'s'?!=1}", n ) returns
             * "0 dogs" when n=0, "1 dog" when n=1, "2 dogs" when n=2, etc.
             */
            let testval = val;
            if ( cond.startsWith( '#' ) ) {
                cond = cond.substring( 1 );
                testval = String( val ).length; // ??? use charLength()?
            } else if ( cond.startsWith( '@' ) ) {
                cond = cond.substring( 1 );
                testval = typeof val;
            }
            let match = false;
            let op = cond.match( /^(!?)(<=?|==?|>=?|::?|t(?:rue)?|f(?:alse)?|n(?:ull)?)(.*)/ );
            let invert = '' !== op[1];
            cond = op[3];
            op = op[2];
            switch ( op ) {
                case 't':
                case 'true':
                    match = true === testval;
                    break;
                case 'f':
                case 'false':
                    match = false === testval;
                    break;
                case 'n':
                case 'null':
                    match = null === testval || "undefined" === typeof testval;
                    break;
                case '>':
                    /* In these numeric comparisons, we're relying heavily on the fact that JS will coerce a string to
                     * a number (for testval), and a numeric comparison to NaN is always false.
                     */
                    match = !isNaN( testval ) && testval > parseFloat( cond );
                    break;
                case '>=':
                    match = !isNaN( testval ) && testval >= parseFloat( cond );
                    break;
                case '<':
                    match = !isNaN( testval ) && testval < parseFloat( cond );
                    break;
                case '<=':
                    match = !isNaN( testval ) && testval <= parseFloat( cond );
                    break;
                case '=':
                case '==':
                    match = !isNaN( testval ) && testval === parseFloat( cond );
                    break;
                case ':':
                case '::':
                    /* For strings, : case-insensitive string comparison, :: case sensitive */
                    if ( "string" !== typeof testval ) {
                        testval = String( testval );
                    }
                    if ( cond.startsWith( '/' ) || cond.startsWith( '~' ) ) {
                        /* Greedy match everything between seps, keep flags after */
                        let r = cond.match( /^(?<sep>.)(.*)\k<sep>(.*)$/ );
                        /* If case-insensitive, force 'i' flag */
                        if ( ':' === op && r[3].indexOf( 'i' ) < 0 ) {
                            r[3] += 'i';
                        }
                        /* If Unicode char class used, force 'u' flag */
                        if ( r[2].indexOf( "\\p{" ) >= 0 && r[3].indexOf( 'u' ) < 0 ) {
                            r[3] += 'u';
                        }
                        let re = new RegExp( r[2], r[3] );
                        match = re.test( testval );
                    } else {
                        /* Plain string */
                        match = 0 === testval.localeCompare( cond, undefined, { sensitivity: ':' === op ? 'base' : 'case' } );
                    }
                    break;
                default:
                    throw new Error( `Unsupported op "${op}" in "${cstr}" (${apos})` );
            }
            /* Return empty string if conditional does not succeed */
            match = invert ? !match : match;
            if ( !match ) {
                return "";
            }
        }
        if ( spec ) {
            /* Flags */
            if ( spec.startsWith( '#' ) ) {
                absvalue = true;
                spec = spec.substring( 1 );
            }
            /* Alignment */
            align = spec.match( /^([<^>])(.*)$/ );
            if ( align ) {
                spec = align[2];
                align = align[1];
            }
            /* Width; leading 0 means zero-padded */
            if ( spec.startsWith( '=' ) ) {
                fixedWidth = true;
                spec = spec.substring( 1 );
            }
            if ( spec.startsWith( '0' ) ) {
                pad = '0';
                spec = spec.substring( 1 );
            } else if ( spec.startsWith( '_' ) ) {
                pad = ' ';
                spec = spec.substring( 1 );
            }
            let k = spec.match( /^([0-9]+)(.*)$/ );
            if ( k ) {
                width = parseInt( k[1] );
                spec = k[2];
            }
            /* Precision */
            k = spec.match( /^\.([0-9]+)(.*)$/ );
            if ( k ) {
                prec = parseInt( k[1] );
                spec = k[2];
            }
            /* And finally, apply default spec, so {0} is treated like {0:s} */
            if ( spec.length < 1 ) {
                spec = 's';
            }
            /* If our spec is a quoted string, take it literally. This is mostly for use with conditionals as a way
             * of substituting the value for a constant. See the conditional example above.
             */
            let q = spec.match( /^(?<quot>["'`])(.*)\k<quot> *$/ );
            if ( q ) {
                val = q[2];
            } else {
                if ( !isNaN( val ) && absvalue ) {
                    val = Math.abs( val );
                }
                switch( spec ) {
                    case 'b':
                        if ( ! Number.isInteger( val ) ) {
                            throw new Error( `Argument ${val} not valid for ${spec} format in ${m[1]}` );
                        }
                        val = val.toString(2);
                        break;

                    case 'd':
                    case 'D':
                        if ( ! Number.isInteger( val ) ) {
                            throw new Error( `Argument ${val} not valid for ${spec} format in ${m[1]}` );
                        }
                        val = String(val);
                        break;

                    case 'o':
                        if ( ! Number.isInteger( val ) ) {
                            throw new Error( `Argument ${val} not valid for ${spec} format in ${m[1]}` );
                        }
                        val = val.toString(8);
                        break;

                    case 'x':
                    case 'X':
                        if ( ! Number.isInteger( val ) ) {
                            throw new Error( `Argument ${val} not valid for ${spec} format in ${m[1]}` );
                        }
                        val = val.toString(16);
                        if ( 'X' === spec ) {
                            val = val.toLocaleUpperCase( localeData.locale );
                        } else {
                            val = val.toLocaleLowerCase( localeData.locale );
                        }
                        break;

                    case 'f':
                        /* Fixed-point */
                        if ( false !== prec ) {
                            val = Number.parseFloat( val ).toFixed( prec );
                        } else {
                            val = Number.parseFloat( val ).toString();
                        }
                        if ( false === align ) {
                            align = 1;
                        }
                        break;

                    case 'e':
                    case 'E':
                        /* Exponential */
                        if ( false !== prec ) {
                            val = Number.parseFloat( val ).toExponential( prec );
                        } else {
                            val = Number.parseFloat( val ).toExponential();
                        }
                        if ( 'E' === spec ) {
                            val = val.toLocaleUpperCase( localeData.locale );
                        }
                        if ( false === align ) {
                            align = 1;
                        }
                        break;

                    case 'g':
                    case 'G':
                        /* General */
                        if ( false !== prec ) {
                            val = Number.parseFloat( val ).toPrecision( prec );
                        } else {
                            val = Number.parseFloat( val ).toPrecision();
                        }
                        if ( 'G' === spec ) {
                            val = val.toLocaleUpperCase( localeData.locale );
                        }
                        if ( false === align ) {
                            align = 1;
                        }
                        break;

                    case '%':
                        val = parseFloat( val );
                        if ( isNaN(val) ) {
                            val = "nan";
                        } else {
                            val = String( Math.floor( val * 100 + 0.5 ) ) + '%';
                        }
                        if ( false === align ) {
                            align = 1;
                        }
                        break;

                    case 'T':
                        /* Send the argument value through translator (recursion) */
                        val = _T( val );
                        break;

                    case 'q':
                        val = JSON.stringify( String( val ) );
                        break;

                    case 's':
                        if ( "string" !== typeof val ) {
                            val = String( val );
                        }
                        break;

                    default:
                        throw new Error( `Invalid format spec "${spec}" in "${m[1]}"` );
                }
            }
        }
        /* Padding and alignment */
        if ( fixedWidth && width ) {
            val = val.substring( 0, width ); // ??? maybe use charSubstring()?
        }
        if ( width && width > val.length ) {
            const n = width - val.length; // ??? maybe use charLength()?
            if ( '^' === align ) {
                const k = n / 2;
                val = pad.repeat(k) + val + pad.repeat(n-k);
            } else if ( '>' === align ) {
                val = pad.repeat(n) + val;
            } else {
                val = val + pad.repeat(n);
            }
        }
        return val;
    });
}

export function _T( txt, ...args ) {
    if ( ! Array.isArray( txt ) ) {
        txt = [ String( txt ) ];
    }
    // console.assert( txt.length > 0 );
    if ( ! localeData.strings[ txt[0] ] ) {
        // console.log( "i18n: missing", localeData.locale, "language string:", txt[0] );
    }
    for ( let t of txt ) {
        if ( localeData.strings[ t ] ) {
            return format( localeData.strings[ t ], ...args );
        }
    }
    console.trace( "Missing translation string", new Error( txt[0] ) );
    return format( txt.pop(), ...args );  // Last string is default
}

export function _H( txt, ...args ) {
    return _T( txt, ...args ).replace( /[\x00-x1f&<>"'/\x7f-\xff\u0100-\uffff]/,
        m => '&#x' + (m.codePointAt(0)).toString(16) + ';' );
}

export function _S( txt ) {
    if ( ! localeData.strings[ txt ] ) {
        console.log("i18n: missing language string:", txt);
    }
    return localeData.strings[ txt ] || txt;
}

/** Date/time formatting. Use the built-in locale-specific conversions unless there's an override.
 *  In that case, pass through to util for strftime().
 */

export function locale_date( d ) {
    d = d instanceof Date ? d.getTime() : parseInt( d );
    if ( localeData.date_format ) {
        return strftime( localeData.date_format, d );
    }
    return new Date( d ).toLocaleDateString( localeData.locale );
}

export function locale_time( d ) {
    d = d instanceof Date ? d.getTime() : parseInt( d );
    if ( localeData.time_format ) {
        return strftime( localeData.time_format, d );
    }
    return new Date( d ).toLocaleTimeString( localeData.locale );
}

export function locale_datetime( d ) {
    d = d instanceof Date ? d.getTime() : parseInt( d );
    if ( localeData.datetime_format ) {
        return strftime( localeData.datetime_format, d );
    }
    return locale_date( d ) + ' ' + locale_time( d );
}

export function getLocale() {
    return localeData.locale;
}

export function getLoadedLocale() {
    return localeData.locale_loaded || 'en-US';
}

export const _LD = localeData;

export function getMonthAbbrevNames() {
    return localeData.short_months || default_short_months;
}

export function getMonthFullNames() {
    return localeData.full_months || default_full_months;
}

export function getWeekdayAbbrevNames() {
    return localeData.short_weekdays || default_short_weekdays;
}

export function getWeekdayFullNames() {
    return localeData.long_weekdays || default_long_weekdays;
}
