core_L18n.js

import { Events} from "./Events.js";

/**
 * L18n
 * Simple internationalization logic
 */
class L18n
{
    static LANG_EN = 'en';

    // List of valid 2-chared-county-codes
    static COUNTRY_CODES = [
    "AD", "AE", "AF", "AG", "AI", "AL", "AM", "AN", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ",
    "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS", "BT", "BV", "BW", "BY", "BZ",
    "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ",
    "DE", "DJ", "DK", "DM", "DO", "DZ",
    "EC", "EE", "EG", "EH", "EN", "ER", "ES", "ET",
    "FI", "FJ", "FK", "FM", "FO", "FR",
    "GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY",
    "HK", "HM", "HN", "HR", "HT", "HU",
    "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT",
    "JE", "JM", "JO", "JP",
    "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ",
    "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY",
    "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ",
    "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ",
    "OM",
    "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY",
    "QA",
    "RE", "RO", "RS", "RU", "RW",
    "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS", "ST", "SV", "SX", "SY", "SZ",
    "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW", "TZ",
    "UA", "UG", "UM", "US", "UY", "UZ",
    "VA", "VC", "VE", "VG", "VI", "VN", "VU",
    "WF", "WS", "YE", "YT",
    "ZA","ZM", "ZW" ];


    /**
     * Constructor
     * @param {App} appInstance - InfrontJS App reference
     */
    constructor( appInstance )
    {
        this.app = appInstance;
        this.defaultLanguage = L18n.LANG_EN;
        let cl = this.resolveBrowserLanguage();
        if ( null === cl )
        {
            cl = this.defaultLanguage.toLowerCase();
        }

        this.dictionary = {};
        this.setCurrentLanguage( cl );
    }

    /**
     * Resolve preferred browser language
     * @returns {*|null|string}
     */
    resolveBrowserLanguage()
    {
        // See: https://stackoverflow.com/questions/1043339/javascript-for-detecting-browser-language-preference/46514247#46514247
        var nav = typeof window !== "undefined" && window.navigator ? window.navigator : null,
            browserLanguagePropertyKeys = ['language', 'browserLanguage', 'systemLanguage', 'userLanguage'],
            i,
            language,
            len,
            shortLanguage = null;

        if ( !nav )
        {
            return null;
        }

        // support for HTML 5.1 "navigator.languages"
        if (Array.isArray(nav.languages)) {
            for (i = 0; i < nav.languages.length; i++) {
                language = nav.languages[i];
                len = language.length;
                if (!shortLanguage && len) {
                    shortLanguage = language;
                }
                if (language && len>2) {
                    return language.slice( 0, 2).toLowerCase();
                }
            }
        }

        // support for other well known properties in browsers
        for (i = 0; i < browserLanguagePropertyKeys.length; i++) {
            language = nav[browserLanguagePropertyKeys[i]];
            //skip this loop iteration if property is null/undefined.  IE11 fix.
            if (language == null) { continue; }
            len = language.length;
            if (!shortLanguage && len) {
                shortLanguage = language;
            }
            if (language && len > 2) {
                return language.slice( 0, 2).toLowerCase();
            }
        }

        return shortLanguage.toLowerCase();
    }

    /**
     * Export localization functions to window scope.
     */
    expose()
    {
        if ( typeof window !== "undefined" )
        {
            window[ '_lcs' ] = this.getLocale.bind( this );
            window[ '_lcn' ] = this.getNumber.bind( this );
            window[ '_lcd' ] = this.getDateTime.bind( this );
        }
    }

    /**
     * Sets dictionary
     * @param {object=}  [dict={ defaultLang : { "langCode" : "translation", ... }, ... } ] - Dictionary
     */
    setDictionary( dict = {} )
    {
        this.dictionary = dict;
    }

    /**
     *
     * Add given translation object to dictionary
     *
     * @param {string} langCode - Langugae code (2 chars)
     * @param {object} translationObject - The translation object, simple key-value object, e.g. { 'Hello' : 'Hallo' }
     */
    addTranslation( langCode, translationObject )
    {
        if ( langCode && langCode.length > 2 )
        {
            langCode = langCode.slice( 0, 2);
        }

        if ( !langCode || false === this._isValidLangCode( langCode ) )
        {
            throw new Error( 'Invalid langCode: ' + langCode )
        }

        for ( const [ key, value ] of Object.entries( translationObject ) )
        {
            if ( false === this.dictionary.hasOwnProperty( key ) )
            {
                this.dictionary[ key ] = {};
            }
            this.dictionary[ key ][ langCode ] = value;
        }
    }

    /**
     * Gets current translation by given key
     * @param {string} key - Translation key
     * @param {array=} [params=undefined] - Params array
     * @returns {string}
     */
    getLocale( key, params )
    {
        const defaultLanguage = this.defaultLanguage,
            language = this.currentLanguage,
            dictionary = this.dictionary;

        let langEntry = null;

        if ( dictionary.hasOwnProperty( key ) && dictionary[ key ].hasOwnProperty( language ) && typeof dictionary[ key ][ language ] === 'string' )
        {
            langEntry = dictionary[ key ][ language ];
        }
        else
        {
            langEntry = key;
        }

        return langEntry.replace(/{(\d+)}/g, function(match, number)
        {
            return typeof params[number] != 'undefined'
                ? params[number]
                : match
                ;
        });
    }

    /**
     * Get formatted number
     *
     * @param {Date} num - Number to format
     * @param {Object=} [opts=null] - NumberFormat options used if set. @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat|NumberFormat}
     * @returns {string}
     */
    getNumber( num = 0, opts = null )
    {
        if ( opts )
        {
            return Intl.NumberFormat( this.currentLanguage, opts ).format( num );
        }
        else
        {
            return this._nf.format( num );
        }
    }

    /**
     * Get formatted date time
     *
     * @param {Date} dt - Date object to format
     * @param {Object=} [opts=null] - DateTimeFormat options used if set. @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat|DateTimeFormat}
     * @returns {string}
     */
    getDateTime( dt, opts = null )
    {
        if ( opts )
        {
            return Intl.DateTimeFormat( this.currentLanguage, opts ).format( dt );
        }
        else
        {
            return this._dtf.format( dt );
        }
    }


    /**
     * Set current language
     * @param {string} langCode - 2 chared language code
     * @throws {Error} - Invalid language code
     */
    setCurrentLanguage( langCode )
    {
        if ( langCode && langCode.length > 2 )
        {
            langCode = langCode.slice( 0, 2);
        }

        if ( !langCode || false === this._isValidLangCode( langCode ) )
        {
            throw new Error( 'Invalid langCode: ' + langCode )
        }

        if ( this.app )
        {
            this.app.emit(
                Events.EVENT.BEFORE_LANGUAGE_SWITCH,
                {
                    currentLanguage: this.currentLanguage,
                    newLanguage: langCode.toLowerCase()
                }
            );
        }

        const oldLanguage = this.currentLanguage;
        this.currentLanguage = langCode.toLowerCase();
        this._nf = new Intl.NumberFormat(
            this.currentLanguage,
            {
                minimumFractionDigits : 2,
                maximumFractionDigits : 2
            }
        );
        this._dtf = new Intl.DateTimeFormat( this.currentLanguage );

        if ( this.app )
        {
            this.app.emit(
                Events.EVENT.AFTER_LANGUAGE_SWITCH,
                {
                    oldLanguage: oldLanguage,
                    currentLanguage: this.currentLanguage
                }
            );
        }
    }

    /**
     * Get current selected language
     * @returns {string} - Returns 2 chared language code
     */
    getCurrentLanguage()
    {
        return this.currentLanguage;
    }

    _isValidLangCode( langCode )
    {
        return -1 < L18n.COUNTRY_CODES.indexOf( langCode.toUpperCase() );
    }
}

export { L18n };