<commit_before>/*global define*/
; (function (global, factory) {
    if (typeof exports === 'object' && typeof module !== 'undefined') {
        var VeloxScriptLoader = require("velox-loader") ;
        module.exports = factory(VeloxScriptLoader) ;
    } else if (typeof define === 'function' && define.amd) {
        define(['VeloxScriptLoader', 'VeloxWebView'], factory);
    } else {
        global.VeloxWebView.registerExtension(factory(global.veloxScriptLoader, global));
    }
}(this, (function (VeloxScriptLoader) { 'use strict';

    var I18NEXT_VERSION = "11.3.1" ;
    var I18NEXT_XHR_VERSION = "1.5.1";
    var I18NEXT_BROWSER_DETECT_VERSION = "2.2.0";
    var LANG_NUMBERS_VERSION = "1.0.0" ;

    var I18NEXT_LIB = [
        {
            name: "i18next",
            type: "js",
            version: I18NEXT_VERSION,
            cdn: "https://unpkg.com/i18next@$VERSION/i18next.js",
            bowerPath: "i18next/i18next.min.js",
            npmPath: "i18next/i18next.min.js"
        },
        {
            name: "i18next-xhr-backend",
            type: "js",
            version: I18NEXT_XHR_VERSION,
            cdn: "https://unpkg.com/i18next-xhr-backend@$VERSION/i18nextXHRBackend.js",
            bowerPath: "i18next-xhr-backend/i18nextXHRBackend.min.js",
            npmPath: "i18next-xhr-backend/i18nextXHRBackend.min.js"
        },
        {
            name: "i18next-browser-languagedetector",
            type: "js",
            version: I18NEXT_BROWSER_DETECT_VERSION,
            cdn: "https://unpkg.com/i18next-browser-languagedetector@$VERSION/i18nextBrowserLanguageDetector.js",
            bowerPath: "i18next-browser-languagedetector/i18nextBrowserLanguageDetector.min.js",
            npmPath: "i18next-browser-languagedetector/i18nextBrowserLanguageDetector.min.js",
        }
    ];

    var LANG_NUMBERS_LIB = {
        name: "lang-numbers",
        type: "json",
        version: LANG_NUMBERS_VERSION,
        cdn: "https://cdn.rawgit.com/aetna-softwares/velox-view/master/ext/lang/numbers.json",
        bowerPath: "velox-view/ext/lang/numbers.json",
        npmPath: "velox-view/ext/lang/numbers.json"
    };

    /**
     * object that contains i18next instance, by default try to get the global variable
     */
    var i18next = typeof(window)!=="undefined"?window.i18next:null ;

    /**
     * check if we did the initialization of i18next
     */
    var i18nextInitDone = false ;

    /**
     * list of listener waiting for i18next to finish
     */
    var initListeners = [] ;

    /**
     * i18next extension definition
     */
    var extension = {} ;

    extension.libs = [
        I18NEXT_LIB,
        LANG_NUMBERS_LIB,
    ] ;

    extension.name = "i18n" ;

    extension.prepare = function(params, cb){
        if(!i18nextInitDone) {
            //i18next is not inistialized
            console.debug("i18next is not initialized, initialization with default parameters") ;
            configureI18Next({}, cb) ;
        } else {
            cb() ;
        }
    } ;

    extension.init = function(){
        var view = this ;
        doInitView.bind(view)() ;
    } ;
    extension.extendsProto = {} ;

    /**
     * Translated the given translation key
     *
     * see https://www.i18next.com/api.html#t
     *
     * @param {string} trKey - The translation key
     * @param {*} [params] - translation parameters
     * @return {string} - The translated string in current language
     */
    extension.extendsProto.tr = function(trKey, params){
        return translate.apply(this, arguments) ;
    } ;

    var currentLocale = null;

    /**
     * Add extra data to localdata such as thousands and decimal delimiters
     *
     * Try to rely on toLocaleString browser if supported, if not supported, the numbro lib is loaded
     *
     * @param {function(Error)} callback called on finished
     */
    function fillLocales(callback){
        currentLocale = {lang : getLang(), delimiters: {}} ;
        if((99.99).toLocaleString('fr') === "99,99"){
            //the browser support locale string
            var localizedNumber = (1000.99).toLocaleString(currentLocale.lang) ;
            currentLocale.delimiters.thousands = localizedNumber[1] ;
            currentLocale.delimiters.decimal = localizedNumber[5] ;
            callback() ;
        }else{
            //the browser does not support locale string, load numbro lib
            VeloxScriptLoader.load(LANG_NUMBERS_LIB, function(err, result){
                if(err){ return callback(err); }
                var numbroLanguages = result[0][0] ;
                var numbroLangName = currentLocale.lang ;
                if(!numbroLanguages[numbroLangName]){ //lang code not found
                    if(numbroLanguages[numbroLangName+"-"+numbroLangName.toUpperCase()]){ //try with same as region code (ex : fr-FR)
                        numbroLangName = numbroLangName+"-"+numbroLangName.toUpperCase() ;
                    }else{
                        //search a lang code starting with our lang code
                        var foundStartWith = Object.keys(numbroLanguages).some(function(l){
                            if(l.indexOf(numbroLangName) === 0){
                                numbroLangName = l;
                                return true;
                            }
                        });
                        if(!foundStartWith){
                            //found nothing, fallback to en-US
                            numbroLangName = "en-US" ;
                        }
                    }
                }

                var langData = numbroLanguages[numbroLangName];
                currentLocale.delimiters.thousands = langData.delimiters.thousands ;
                currentLocale.delimiters.decimal = langData.delimiters.decimal ;
                callback() ;
            }) ;
        }
    }

    /**
     * Format a value to display
     *
     * @param {*} value - the value to format
     * @param {string} [type] - type of value
     */
    function format(value, type) {
        if (value === null || value === undefined) {
            if(["int", "integer", "number"].indexOf(type) !== -1){
                return "0";
            }else if(["double", "float"].indexOf(type) !== -1){
                return "0"+currentLocale.delimiters.decimal+"00";
            }else{
                return "";
            }
        }

        if(typeof(value) === "string" && /[0-3]{1}[0-9]{3}-[0-1]{1}[0-9]{1}-[0-3]{1}[0-9]{1}T[0-2]{1}[0-9]{1}:[0-5]{1}[0-9]{1}:[0-5]{1}[0-9]{1}.[0-9]{3}Z/.test(value)){
            //if is a date like "2017-07-24T22:00:00.000Z"
            value = new Date(value) ;
        }
        if(/[0-3]{1}[0-9]{3}-[0-1]{1}[0-9]{1}-[0-3]{1}[0-9]{1}/.test(value)){
            //if is a date like "2017-07-24"
            value = new Date(value) ;
        }
        if(value instanceof Date){
            if(!type){
                //try to guess if it is a date or a date time
                if(value.getHours() === 0 && value.getMinutes() === 0 && value.getSeconds() === 0 && value.getMilliseconds() === 0){
                    //the date is exactly midnight, assume it is date only data
                    type = "date";
                }else{
                    //the date has a date with time information, it is probably a data/time
                    type = "datetime";
                }
            }

            if(["datetime", "timestamp", "timestamptz"].indexOf(type) !== -1){
                if(value.toLocaleDateString){
                    return value.toLocaleDateString(currentLocale.lang)+" "+value.toLocaleTimeString(currentLocale.lang) ;
                }else{
                    return value.toDateString()+" "+value.toTimeString() ; //IE10...
                }
            }else{
                if(value.toLocaleDateString){
                    return value.toLocaleDateString(currentLocale.lang) ;
                }else{
                    return value.toDateString() ; //IE10...
                }
            }
        }

        if(typeof(value)==="string" && ["int", "integer", "number", "double", "float"].indexOf(type) !== -1){
            value = Number(value) ;
        }

        if(typeof(value)==="number"){
            if(["int", "integer", "number"].indexOf(type) !== -1){
                return Math.round(type).toFixed(0).replace(/,/g, currentLocale.delimiters.thousands);
            }else if(["double", "float"].indexOf(type) !== -1){
                var n = value.toFixed(2);
                n = n.substring(0, n.length-3).replace(/\B(?=(\d{3})+(?!\d))/g, currentLocale.delimiters.thousands)+
                    currentLocale.delimiters.decimal+
                    n.substring(n.length-2) ;
                return n;
            }else{
                return ""+value;
            }
        }
        return ""+value;
    }

    extension.extendsProto.format = format ;

    extension.extendsGlobal = {} ;

    extension.extendsGlobal.i18n = {} ;
    /**
     * Configure the i18next system.
     * options should contains any option of https://www.i18next.com/configuration-options.html
     * in more it can contains an i18n object to use. If not given, the global i18next object will be used
     * if it does not exists, i18next will be retrieved from CDN
     *
     * default options are :
     * {
     *       fallbackLng: 'en',
     *       backend: {
     *           loadPath: 'locales/{{lng}}.json',
     *       }
     *   }
     *
     * @param {object} options - Init option of i18next
     * @param {function(err)} callback - Called when configuration is done
     */
    extension.extendsGlobal.i18n.configure = function(options, callback){
        return configureI18Next(options, callback) ;
    } ;

    /**
     * change the current lang
     *
     * @param {string} lang - the lang code
     */
    extension.extendsGlobal.i18n.setLang = function(lang, callback){
        return setLang(lang, callback) ;
    } ;
    /**
     * get the current lang
     */
    extension.extendsGlobal.i18n.getLang = function(){
        return getLang() ;
    } ;

    /**
     *
     */
    extension.extendsGlobal.i18n.onLanguageChanged = function(listener){
        onLanguageChanged(listener) ;
    } ;

    extension.extendsGlobal.i18n.tr = extension.extendsProto.tr ;

    extension.extendsGlobal.tr = extension.extendsProto.tr ;

    extension.extendsGlobal.format = extension.extendsProto.format ;

    /**
     * init view translation
     *
     * @private
     */
    function doInitView(){
        var view = this ;
        i18next.on("languageChanged", function(){
            //when language change, reload the view translation
            doTranslateView.bind(view)() ;
        }) ;
        doTranslateView.bind(view)() ;
    }

    /**
     * translate in the view
     *
     * @private
     */
    function doTranslateView(){
        var elements = this.elementsHavingAttribute("data-i18n");
        var regexpAttr = /^\[(.*)](.*)$/;
        for(var i=0; i<elements.length; i++){
            var str = elements[i].getAttribute("data-i18n") ;
            var match ;
            if((match = regexpAttr.exec(str)) !== null) {
                var attr = match[1] ;
                var code = match[2] ;
                elements[i].setAttribute(attr, translate(code)) ;
            }else{
                elements[i].innerHTML = translate(str) ;
            }
        }
    }

    /**
     * configure i18next
     *
     * @private
     */
    function configureI18Next(options, callback) {
        if(options.i18next) {
            i18next = options.i18next ;
        }
        if(!i18next) {
            //no i18next object exists, load from CDN/bower
            console.debug("No i18next object given, we will load from CDN/bower. If you don't want this, include i18next "+I18NEXT_VERSION+
                " in your import scripts or give i18next object to VeloxWebView.i18n.configure function");

            if (!VeloxScriptLoader) {
                return callback("To have automatic script loading, you need to import VeloxScriptLoader");
            }

            VeloxScriptLoader.load(I18NEXT_LIB, function(err){
                if(err){ return callback(err); }
                i18next = window.i18next ;
                initI18Next(options, callback);
            }) ;
        } else {
            initI18Next(options, callback);
        }
    }

    /**
     * init i18next
     *
     * @private
     */
    function initI18Next(options, callback){
        var opts = {
            fallbackLng: 'en',
            backend: {
                loadPath: 'locales/{{lng}}.json',
            }
        } ;
        Object.keys(options).forEach(function(k){
            opts[k] = options[k] ;
        }) ;

        if(window.i18nextXHRBackend){
            i18next.use(window.i18nextXHRBackend);
        }
        if(window.i18nextBrowserLanguageDetector){
            i18next.use(window.i18nextBrowserLanguageDetector);
        }
        i18next.init(opts, function(err){
            i18nextInitDone = true ;
            console.debug("i18next init done") ;
            if(err){
                //just warn some files are missing, fallback will handle resolution to fallback lang
                console.warn("Some translation file are missing", err) ;
            }

            fillLocales(function(err){
                if(err){ return callback(err) ;}

                initListeners.forEach(function(l){
                    l() ;
                }) ;
                initListeners = [] ;

                callback() ;
            }) ;

        });
    }

    /**
     * Ensure the i18next is initialized
     * @param {function} callback called when i18n is done
     */
    function ensureInit(callback){
        if(i18next){ return callback() ;}
        initListeners.push(callback) ;
    }

    /**
     * change lang
     *
     * @private
     */
    function setLang(lang, callback){
        if(!callback){
            callback = function(){} ;
        }
        ensureInit(function(){
            i18next.changeLanguage(lang, callback);
        }) ;
    }

    /**
     * Get the current language
     */
    function getLang(){
        if(i18next){
            return i18next.language ;
        }else{
            return navigator.language || navigator.userLanguage ;
        }
    }

    /**
     * Add a listener on language change
     *
     * @param {function({string})} listener called when language changed, it receive the new language code
     */
    function onLanguageChanged(listener){
        ensureInit(function(){
            i18next.on("languageChanged", listener) ;
        }) ;

    }

    /**
     * do translation
     *
     * @private
     */
    function translate(str, params){
        if(!i18next){
            return console.error("i18next is not yet initialized");
        }
        return i18next.t.apply(i18next, arguments) ;
    }

    return extension ;

})));<commit_msg>fix detector order<commit_after>/*global define*/
; (function (global, factory) {
    if (typeof exports === 'object' && typeof module !== 'undefined') {
        var VeloxScriptLoader = require("velox-loader") ;
        module.exports = factory(VeloxScriptLoader) ;
    } else if (typeof define === 'function' && define.amd) {
        define(['VeloxScriptLoader', 'VeloxWebView'], factory);
    } else {
        global.VeloxWebView.registerExtension(factory(global.veloxScriptLoader, global));
    }
}(this, (function (VeloxScriptLoader) { 'use strict';

    var I18NEXT_VERSION = "11.3.1" ;
    var I18NEXT_XHR_VERSION = "1.5.1";
    var I18NEXT_BROWSER_DETECT_VERSION = "2.2.0";
    var LANG_NUMBERS_VERSION = "1.0.0" ;

    var I18NEXT_LIB = [
        {
            name: "i18next",
            type: "js",
            version: I18NEXT_VERSION,
            cdn: "https://unpkg.com/i18next@$VERSION/i18next.js",
            bowerPath: "i18next/i18next.min.js",
            npmPath: "i18next/i18next.min.js"
        },
        {
            name: "i18next-xhr-backend",
            type: "js",
            version: I18NEXT_XHR_VERSION,
            cdn: "https://unpkg.com/i18next-xhr-backend@$VERSION/i18nextXHRBackend.js",
            bowerPath: "i18next-xhr-backend/i18nextXHRBackend.min.js",
            npmPath: "i18next-xhr-backend/i18nextXHRBackend.min.js"
        },
        {
            name: "i18next-browser-languagedetector",
            type: "js",
            version: I18NEXT_BROWSER_DETECT_VERSION,
            cdn: "https://unpkg.com/i18next-browser-languagedetector@$VERSION/i18nextBrowserLanguageDetector.js",
            bowerPath: "i18next-browser-languagedetector/i18nextBrowserLanguageDetector.min.js",
            npmPath: "i18next-browser-languagedetector/i18nextBrowserLanguageDetector.min.js",
        }
    ];

    var LANG_NUMBERS_LIB = {
        name: "lang-numbers",
        type: "json",
        version: LANG_NUMBERS_VERSION,
        cdn: "https://cdn.rawgit.com/aetna-softwares/velox-view/master/ext/lang/numbers.json",
        bowerPath: "velox-view/ext/lang/numbers.json",
        npmPath: "velox-view/ext/lang/numbers.json"
    };

    /**
     * object that contains i18next instance, by default try to get the global variable
     */
    var i18next = typeof(window)!=="undefined"?window.i18next:null ;

    /**
     * check if we did the initialization of i18next
     */
    var i18nextInitDone = false ;

    /**
     * list of listener waiting for i18next to finish
     */
    var initListeners = [] ;

    /**
     * i18next extension definition
     */
    var extension = {} ;

    extension.libs = [
        I18NEXT_LIB,
        LANG_NUMBERS_LIB,
    ] ;

    extension.name = "i18n" ;

    extension.prepare = function(params, cb){
        if(!i18nextInitDone) {
            //i18next is not inistialized
            console.debug("i18next is not initialized, initialization with default parameters") ;
            configureI18Next({}, cb) ;
        } else {
            cb() ;
        }
    } ;

    extension.init = function(){
        var view = this ;
        doInitView.bind(view)() ;
    } ;
    extension.extendsProto = {} ;

    /**
     * Translated the given translation key
     *
     * see https://www.i18next.com/api.html#t
     *
     * @param {string} trKey - The translation key
     * @param {*} [params] - translation parameters
     * @return {string} - The translated string in current language
     */
    extension.extendsProto.tr = function(trKey, params){
        return translate.apply(this, arguments) ;
    } ;

    var currentLocale = null;

    /**
     * Add extra data to localdata such as thousands and decimal delimiters
     *
     * Try to rely on toLocaleString browser if supported, if not supported, the numbro lib is loaded
     *
     * @param {function(Error)} callback called on finished
     */
    function fillLocales(callback){
        currentLocale = {lang : getLang(), delimiters: {}} ;
        if((99.99).toLocaleString('fr') === "99,99"){
            //the browser support locale string
            var localizedNumber = (1000.99).toLocaleString(currentLocale.lang) ;
            currentLocale.delimiters.thousands = localizedNumber[1] ;
            currentLocale.delimiters.decimal = localizedNumber[5] ;
            callback() ;
        }else{
            //the browser does not support locale string, load numbro lib
            VeloxScriptLoader.load(LANG_NUMBERS_LIB, function(err, result){
                if(err){ return callback(err); }
                var numbroLanguages = result[0][0] ;
                var numbroLangName = currentLocale.lang ;
                if(!numbroLanguages[numbroLangName]){ //lang code not found
                    if(numbroLanguages[numbroLangName+"-"+numbroLangName.toUpperCase()]){ //try with same as region code (ex : fr-FR)
                        numbroLangName = numbroLangName+"-"+numbroLangName.toUpperCase() ;
                    }else{
                        //search a lang code starting with our lang code
                        var foundStartWith = Object.keys(numbroLanguages).some(function(l){
                            if(l.indexOf(numbroLangName) === 0){
                                numbroLangName = l;
                                return true;
                            }
                        });
                        if(!foundStartWith){
                            //found nothing, fallback to en-US
                            numbroLangName = "en-US" ;
                        }
                    }
                }

                var langData = numbroLanguages[numbroLangName];
                currentLocale.delimiters.thousands = langData.delimiters.thousands ;
                currentLocale.delimiters.decimal = langData.delimiters.decimal ;
                callback() ;
            }) ;
        }
    }

    /**
     * Format a value to display
     *
     * @param {*} value - the value to format
     * @param {string} [type] - type of value
     */
    function format(value, type) {
        if (value === null || value === undefined) {
            if(["int", "integer", "number"].indexOf(type) !== -1){
                return "0";
            }else if(["double", "float"].indexOf(type) !== -1){
                return "0"+currentLocale.delimiters.decimal+"00";
            }else{
                return "";
            }
        }

        if(typeof(value) === "string" && /[0-3]{1}[0-9]{3}-[0-1]{1}[0-9]{1}-[0-3]{1}[0-9]{1}T[0-2]{1}[0-9]{1}:[0-5]{1}[0-9]{1}:[0-5]{1}[0-9]{1}.[0-9]{3}Z/.test(value)){
            //if is a date like "2017-07-24T22:00:00.000Z"
            value = new Date(value) ;
        }
        if(/[0-3]{1}[0-9]{3}-[0-1]{1}[0-9]{1}-[0-3]{1}[0-9]{1}/.test(value)){
            //if is a date like "2017-07-24"
            value = new Date(value) ;
        }
        if(value instanceof Date){
            if(!type){
                //try to guess if it is a date or a date time
                if(value.getHours() === 0 && value.getMinutes() === 0 && value.getSeconds() === 0 && value.getMilliseconds() === 0){
                    //the date is exactly midnight, assume it is date only data
                    type = "date";
                }else{
                    //the date has a date with time information, it is probably a data/time
                    type = "datetime";
                }
            }

            if(["datetime", "timestamp", "timestamptz"].indexOf(type) !== -1){
                if(value.toLocaleDateString){
                    return value.toLocaleDateString(currentLocale.lang)+" "+value.toLocaleTimeString(currentLocale.lang) ;
                }else{
                    return value.toDateString()+" "+value.toTimeString() ; //IE10...
                }
            }else{
                if(value.toLocaleDateString){
                    return value.toLocaleDateString(currentLocale.lang) ;
                }else{
                    return value.toDateString() ; //IE10...
                }
            }
        }

        if(typeof(value)==="string" && ["int", "integer", "number", "double", "float"].indexOf(type) !== -1){
            value = Number(value) ;
        }

        if(typeof(value)==="number"){
            if(["int", "integer", "number"].indexOf(type) !== -1){
                return Math.round(type).toFixed(0).replace(/,/g, currentLocale.delimiters.thousands);
            }else if(["double", "float"].indexOf(type) !== -1){
                var n = value.toFixed(2);
                n = n.substring(0, n.length-3).replace(/\B(?=(\d{3})+(?!\d))/g, currentLocale.delimiters.thousands)+
                    currentLocale.delimiters.decimal+
                    n.substring(n.length-2) ;
                return n;
            }else{
                return ""+value;
            }
        }
        return ""+value;
    }

    extension.extendsProto.format = format ;

    extension.extendsGlobal = {} ;

    extension.extendsGlobal.i18n = {} ;
    /**
     * Configure the i18next system.
     * options should contains any option of https://www.i18next.com/configuration-options.html
     * in more it can contains an i18n object to use. If not given, the global i18next object will be used
     * if it does not exists, i18next will be retrieved from CDN
     *
     * default options are :
     * {
     *       fallbackLng: 'en',
     *       backend: {
     *           loadPath: 'locales/{{lng}}.json',
     *       }
     *   }
     *
     * @param {object} options - Init option of i18next
     * @param {function(err)} callback - Called when configuration is done
     */
    extension.extendsGlobal.i18n.configure = function(options, callback){
        return configureI18Next(options, callback) ;
    } ;

    /**
     * change the current lang
     *
     * @param {string} lang - the lang code
     */
    extension.extendsGlobal.i18n.setLang = function(lang, callback){
        return setLang(lang, callback) ;
    } ;
    /**
     * get the current lang
     */
    extension.extendsGlobal.i18n.getLang = function(){
        return getLang() ;
    } ;

    /**
     *
     */
    extension.extendsGlobal.i18n.onLanguageChanged = function(listener){
        onLanguageChanged(listener) ;
    } ;

    extension.extendsGlobal.i18n.tr = extension.extendsProto.tr ;

    extension.extendsGlobal.tr = extension.extendsProto.tr ;

    extension.extendsGlobal.format = extension.extendsProto.format ;

    /**
     * init view translation
     *
     * @private
     */
    function doInitView(){
        var view = this ;
        i18next.on("languageChanged", function(){
            //when language change, reload the view translation
            doTranslateView.bind(view)() ;
        }) ;
        doTranslateView.bind(view)() ;
    }

    /**
     * translate in the view
     *
     * @private
     */
    function doTranslateView(){
        var elements = this.elementsHavingAttribute("data-i18n");
        var regexpAttr = /^\[(.*)](.*)$/;
        for(var i=0; i<elements.length; i++){
            var str = elements[i].getAttribute("data-i18n") ;
            var match ;
            if((match = regexpAttr.exec(str)) !== null) {
                var attr = match[1] ;
                var code = match[2] ;
                elements[i].setAttribute(attr, translate(code)) ;
            }else{
                elements[i].innerHTML = translate(str) ;
            }
        }
    }

    /**
     * configure i18next
     *
     * @private
     */
    function configureI18Next(options, callback) {
        if(options.i18next) {
            i18next = options.i18next ;
        }
        if(!i18next) {
            //no i18next object exists, load from CDN/bower
            console.debug("No i18next object given, we will load from CDN/bower. If you don't want this, include i18next "+I18NEXT_VERSION+
                " in your import scripts or give i18next object to VeloxWebView.i18n.configure function");

            if (!VeloxScriptLoader) {
                return callback("To have automatic script loading, you need to import VeloxScriptLoader");
            }

            VeloxScriptLoader.load(I18NEXT_LIB, function(err){
                if(err){ return callback(err); }
                i18next = window.i18next ;
                initI18Next(options, callback);
            }) ;
        } else {
            initI18Next(options, callback);
        }
    }

    /**
     * init i18next
     *
     * @private
     */
    function initI18Next(options, callback){
        var opts = {
            fallbackLng: 'en',
            detection: {
                order: ['querystring', 'navigator', 'htmlTag', 'path', 'subdomain'],
            },
            backend: {
                loadPath: 'locales/{{lng}}.json',
            }
        } ;
        Object.keys(options).forEach(function(k){
            opts[k] = options[k] ;
        }) ;

        if(window.i18nextXHRBackend){
            i18next.use(window.i18nextXHRBackend);
        }
        if(window.i18nextBrowserLanguageDetector){
            i18next.use(window.i18nextBrowserLanguageDetector);
        }
        i18next.init(opts, function(err){
            i18nextInitDone = true ;
            console.debug("i18next init done") ;
            if(err){
                //just warn some files are missing, fallback will handle resolution to fallback lang
                console.warn("Some translation file are missing", err) ;
            }

            fillLocales(function(err){
                if(err){ return callback(err) ;}

                initListeners.forEach(function(l){
                    l() ;
                }) ;
                initListeners = [] ;

                callback() ;
            }) ;

        });
    }

    /**
     * Ensure the i18next is initialized
     * @param {function} callback called when i18n is done
     */
    function ensureInit(callback){
        if(i18next){ return callback() ;}
        initListeners.push(callback) ;
    }

    /**
     * change lang
     *
     * @private
     */
    function setLang(lang, callback){
        if(!callback){
            callback = function(){} ;
        }
        ensureInit(function(){
            i18next.changeLanguage(lang, callback);
        }) ;
    }

    /**
     * Get the current language
     */
    function getLang(){
        if(i18next){
            return i18next.language ;
        }else{
            return navigator.language || navigator.userLanguage ;
        }
    }

    /**
     * Add a listener on language change
     *
     * @param {function({string})} listener called when language changed, it receive the new language code
     */
    function onLanguageChanged(listener){
        ensureInit(function(){
            i18next.on("languageChanged", listener) ;
        }) ;

    }

    /**
     * do translation
     *
     * @private
     */
    function translate(str, params){
        if(!i18next){
            return console.error("i18next is not yet initialized");
        }
        return i18next.t.apply(i18next, arguments) ;
    }

    return extension ;

})));<|endoftext|>


<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

(function speak_to_me() {

console.log("Speak To Me starting up...");

if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
    console.error("You need a browser with getUserMedia support to use Speak To Me, sorry!");
    return;
}

const LOCAL_TEST = false;

const stt_server_url = "http://10.252.24.90:9001/asr";

// Encapsulation of the popup we use to provide our UI.
const popup_markup =
`

  <button id="stm-stop">Stop</button>

`;

const SpeakToMePopup = {
    init: () => {
        console.log(`SpeakToMePopup init`);
        let popup = document.createElement("div");
        popup.innerHTML = popup_markup;
        document.body.appendChild(popup);
        this.popup = document.getElementById("stm-popup");
        this.list = document.getElementById("stm-list");
    },

    showAt: (x, y) => {
        console.log(`SpeakToMePopup showAt ${x},${y}`);
        this.list.classList.add("hidden");

        let style = this.popup.style;
        style.left = (x + window.scrollX) + "px";
        style.top = (y + window.scrollY) + "px";
        style.display = "block";
    },

    hide: () => {
        console.log(`SpeakToMePopup hide`);
        this.popup.style.display = "none";
    },

    // Returns a Promise that resolves once the "Stop" button is clicked.
    // TODO: replace with silence detection.
    wait_for_stop: () => {
        console.log(`SpeakToMePopup wait_for_stop`);
        return new Promise((resolve, reject) => {
            console.log(`SpeakToMePopup set popup stop listener`);
            let button = document.getElementById("stm-stop");
            button.classList.remove("hidden");
            button.addEventListener("click", function _mic_stop() {
                button.classList.add("hidden");
                button.removeEventListener("click", _mic_stop);
                resolve();
            });
        });
    },

    // Returns a Promise that resolves to the choosen text.
    choose_item: (data) => {
        console.log(`SpeakToMePopup choose_item`);
        return new Promise((resolve, reject) => {
            let html = "
";
            data.forEach(item => {
                html += `<li>${item.text}</li>`;
            });
            html += "
";
            let list = this.list;
            list.innerHTML = html;
            list.classList.remove("hidden");

            list.addEventListener("click", function _choose_item(e) {
                list.removeEventListener("click", _choose_item);
                if (e.target instanceof HTMLLIElement) {
                    resolve(e.target.textContent);
                }
            });
        });
    }
}

// The icon that we anchor to the currently focused input element.

// TODO: figure out why using a resource in the extensions with browser.extension.getURL() fails.
const mic_icon_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAQAAABLCVATAAABW0lEQVR4Ad2VJXQDMRyHU1Ljzeu6+tnKeTM7NmPUu9CYSQ38ewVXWd/5t7qaoRrz/kkuNyoz/b5cOF+vjMoS7tqY2ohuPG9EZevIW7Ph2AhuwA/BvFXrQ+vwj6F8RZE4USRf0VOc6DlP0RrEUzeiVYij4qIViKPiomWII1/REsRTadEixFNp0QLEk8vhO3WAu8z+RZzoQs2yRrP/mkHEzzhwYG6zf8LhH0dqlnrMHbFMIr+5bUT1mZs//NE8aD0bN0f+DCLWy0AS4y5z5GU35hhk69V/ByxmjnsziRrZDQXJoh7TZtpN5+TVbI0X1arUNqJMYSMUFGw8ydq4tTaCMofYSYiASUC/KpbETQLWfIjYUTahzSRMwOKUHBiUHMgWLMK0OYd/WLyDIQkfeIe7UG7BnSSAP/5KSIB6UH7B7bhLa2TbgQqLAYq4yYqK8IchX59i3BGdfzAoqsEI9//IsA+uNg0AAAAASUVORK5CYII=";

const SpeakToMeIcon = {
    init: () => {
        console.log(`SpeakToMeIcon init`);
        SpeakToMeIcon.icon = document.createElement("div");
        let mic = document.createElement("img");
        mic.src = mic_icon_url;
        SpeakToMeIcon.icon.appendChild(mic);
        SpeakToMeIcon.icon.classList.add("stm-icon");
        SpeakToMeIcon.icon.classList.add("hidden");
        document.body.appendChild(SpeakToMeIcon.icon);

        SpeakToMeIcon.icon.addEventListener("click", on_spm_icon_click);

        document.body.addEventListener("focusin", (event) => {
            let target = event.target;
            // TODO: refine input field detection.
            if (target instanceof HTMLInputElement &&
                ["text", "email"].indexOf(target.type) >= 0) {
                SpeakToMeIcon.anchor_to(target);
            }
        });
    },

    anchor_to: (target) => {
        console.log(`SpeakToMeIcon anchor_to ${target}`);
        let bcr = target.getBoundingClientRect();
        let icon = SpeakToMeIcon.icon;
        icon.style.left = (bcr.left + window.scrollX) + "px";
        icon.style.top = (bcr.top + window.scrollY) + "px";
        icon.classList.remove("hidden");
        SpeakToMeIcon._input_field = target;
    },

    get input_field() {
        console.log(`SpeakToMeIcon get::input_field ${SpeakToMeIcon._input_field}`);
        return SpeakToMeIcon._input_field;
    }
}

const on_spm_icon_click = (event) => {
    let constraints = { audio: true };
    let chunks = [];

    navigator.mediaDevices.getUserMedia(constraints)
    .then(function(stream) {
        let options = {
            audioBitsPerSecond : 16000,
            mimeType : "audio/ogg"
        }

        let mediaRecorder = new MediaRecorder(stream, options);

        SpeakToMePopup.showAt(event.clientX, event.clientY);

        SpeakToMePopup.wait_for_stop().then(() => {
            mediaRecorder.stop();
        });

        // TODO: Would be nice to have a wave or fft display.
        // visualize(stream);

        mediaRecorder.start();

        mediaRecorder.onstop = (e) => {
            // We stopped the recording, send the content to the STT server.
            mediaRecorder = null;
            let blob = new Blob(chunks, { "type" : "audio/ogg; codecs=opus" });
            chunks = [];

            if (LOCAL_TEST) {
                let json = JSON.parse('{"status":"ok","data":[{"confidence":0.907493,"text":"PLEASE ADD MILK TO MY SHOPPING LIST"},{"confidence":0.906263,"text":"PLEASE AT MILK TO MY SHOPPING LIST"},{"confidence":0.904414,"text":"PLEASE ET MILK TO MY SHOPPING LIST"}]}');
                if (json.status == "ok") {
                    display_options(json.data);
                }
                return;
            }

            fetch(stt_server_url, {
                method: "POST",
                body: blob
                })
            .then((response) => { return response.json(); })
            .then((json) => {
                console.log(`Got STT result: ${JSON.stringify(json)}`);
                if (json.status == "ok") {
                    display_options(json.data);
                }
            })
            .catch((error) => {
                console.error(`Fetch error: ${error}`);
            });
        }

        mediaRecorder.ondataavailable = (e) => {
            chunks.push(e.data);
        }
    })
    .catch(function(err) {
        console.log(`Recording error: ${err}`);
    });
}

const display_options = (items) => {
    // Filter the array for empty items and normalize the text.
    let data = items.filter((item) => { return item.text != ""; })
                    .map((item) => { return { confidence: item.confidence,
                                              text: item.text.toLowerCase()
                                             } });

    if (data.length == 0) {
        // TODO: display some failure notification to the user?
        SpeakToMePopup.hide();
        return;
    }

    // if the first result has a high enough confidence, just
    // use it directly.
    if (data[0].confidence > 0.90) {
        SpeakToMeIcon.input_field.value = data[0].text;
        SpeakToMePopup.hide();
        return;
    }

    SpeakToMePopup.choose_item(data).then((text) => {
        input_field.value = text;
        // Once a choice is made, close the popup.
        SpeakToMePopup.hide();
    });
}

SpeakToMePopup.init();
SpeakToMeIcon.init();

})();
<commit_msg>Fix the choice selection<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

(function speak_to_me() {

console.log("Speak To Me starting up...");

if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
    console.error("You need a browser with getUserMedia support to use Speak To Me, sorry!");
    return;
}

const LOCAL_TEST = false;

const stt_server_url = "http://10.252.24.90:9001/asr";

// Encapsulation of the popup we use to provide our UI.
const popup_markup =
`

  <button id="stm-stop">Stop</button>

`;

const SpeakToMePopup = {
    init: () => {
        console.log(`SpeakToMePopup init`);
        let popup = document.createElement("div");
        popup.innerHTML = popup_markup;
        document.body.appendChild(popup);
        this.popup = document.getElementById("stm-popup");
        this.list = document.getElementById("stm-list");
    },

    showAt: (x, y) => {
        console.log(`SpeakToMePopup showAt ${x},${y}`);
        this.list.classList.add("hidden");

        let style = this.popup.style;
        style.left = (x + window.scrollX) + "px";
        style.top = (y + window.scrollY) + "px";
        style.display = "block";
    },

    hide: () => {
        console.log(`SpeakToMePopup hide`);
        this.popup.style.display = "none";
    },

    // Returns a Promise that resolves once the "Stop" button is clicked.
    // TODO: replace with silence detection.
    wait_for_stop: () => {
        console.log(`SpeakToMePopup wait_for_stop`);
        return new Promise((resolve, reject) => {
            console.log(`SpeakToMePopup set popup stop listener`);
            let button = document.getElementById("stm-stop");
            button.classList.remove("hidden");
            button.addEventListener("click", function _mic_stop() {
                button.classList.add("hidden");
                button.removeEventListener("click", _mic_stop);
                resolve();
            });
        });
    },

    // Returns a Promise that resolves to the choosen text.
    choose_item: (data) => {
        console.log(`SpeakToMePopup choose_item`);
        return new Promise((resolve, reject) => {
            let html = "
";
            data.forEach(item => {
                html += `<li>${item.text}</li>`;
            });
            html += "
";
            let list = this.list;
            list.innerHTML = html;
            list.classList.remove("hidden");

            list.addEventListener("click", function _choose_item(e) {
                list.removeEventListener("click", _choose_item);
                if (e.target instanceof HTMLLIElement) {
                    resolve(e.target.textContent);
                }
            });
        });
    }
}

// The icon that we anchor to the currently focused input element.

// TODO: figure out why using a resource in the extensions with browser.extension.getURL() fails.
const mic_icon_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAQAAABLCVATAAABW0lEQVR4Ad2VJXQDMRyHU1Ljzeu6+tnKeTM7NmPUu9CYSQ38ewVXWd/5t7qaoRrz/kkuNyoz/b5cOF+vjMoS7tqY2ohuPG9EZevIW7Ph2AhuwA/BvFXrQ+vwj6F8RZE4USRf0VOc6DlP0RrEUzeiVYij4qIViKPiomWII1/REsRTadEixFNp0QLEk8vhO3WAu8z+RZzoQs2yRrP/mkHEzzhwYG6zf8LhH0dqlnrMHbFMIr+5bUT1mZs//NE8aD0bN0f+DCLWy0AS4y5z5GU35hhk69V/ByxmjnsziRrZDQXJoh7TZtpN5+TVbI0X1arUNqJMYSMUFGw8ydq4tTaCMofYSYiASUC/KpbETQLWfIjYUTahzSRMwOKUHBiUHMgWLMK0OYd/WLyDIQkfeIe7UG7BnSSAP/5KSIB6UH7B7bhLa2TbgQqLAYq4yYqK8IchX59i3BGdfzAoqsEI9//IsA+uNg0AAAAASUVORK5CYII=";

const SpeakToMeIcon = {
    init: () => {
        console.log(`SpeakToMeIcon init`);
        SpeakToMeIcon.icon = document.createElement("div");
        let mic = document.createElement("img");
        mic.src = mic_icon_url;
        SpeakToMeIcon.icon.appendChild(mic);
        SpeakToMeIcon.icon.classList.add("stm-icon");
        SpeakToMeIcon.icon.classList.add("hidden");
        document.body.appendChild(SpeakToMeIcon.icon);

        SpeakToMeIcon.icon.addEventListener("click", on_spm_icon_click);

        document.body.addEventListener("focusin", (event) => {
            let target = event.target;
            // TODO: refine input field detection.
            if (target instanceof HTMLInputElement &&
                ["text", "email"].indexOf(target.type) >= 0) {
                SpeakToMeIcon.anchor_to(target);
            }
        });
    },

    anchor_to: (target) => {
        console.log(`SpeakToMeIcon anchor_to ${target}`);
        let bcr = target.getBoundingClientRect();
        let icon = SpeakToMeIcon.icon;
        icon.style.left = (bcr.left + window.scrollX) + "px";
        icon.style.top = (bcr.top + window.scrollY) + "px";
        icon.classList.remove("hidden");
        SpeakToMeIcon._input_field = target;
    },

    get input_field() {
        console.log(`SpeakToMeIcon get::input_field ${SpeakToMeIcon._input_field}`);
        return SpeakToMeIcon._input_field;
    }
}

const on_spm_icon_click = (event) => {
    let constraints = { audio: true };
    let chunks = [];

    navigator.mediaDevices.getUserMedia(constraints)
    .then(function(stream) {
        let options = {
            audioBitsPerSecond : 16000,
            mimeType : "audio/ogg"
        }

        let mediaRecorder = new MediaRecorder(stream, options);

        SpeakToMePopup.showAt(event.clientX, event.clientY);

        SpeakToMePopup.wait_for_stop().then(() => {
            mediaRecorder.stop();
        });

        // TODO: Would be nice to have a wave or fft display.
        // visualize(stream);

        mediaRecorder.start();

        mediaRecorder.onstop = (e) => {
            // We stopped the recording, send the content to the STT server.
            mediaRecorder = null;
            let blob = new Blob(chunks, { "type" : "audio/ogg; codecs=opus" });
            chunks = [];

            if (LOCAL_TEST) {
                let json = JSON.parse('{"status":"ok","data":[{"confidence":0.807493,"text":"PLEASE ADD MILK TO MY SHOPPING LIST"},{"confidence":0.906263,"text":"PLEASE AT MILK TO MY SHOPPING LIST"},{"confidence":0.904414,"text":"PLEASE ET MILK TO MY SHOPPING LIST"}]}');
                if (json.status == "ok") {
                    display_options(json.data);
                }
                return;
            }

            fetch(stt_server_url, {
                method: "POST",
                body: blob
                })
            .then((response) => { return response.json(); })
            .then((json) => {
                console.log(`Got STT result: ${JSON.stringify(json)}`);
                if (json.status == "ok") {
                    display_options(json.data);
                }
            })
            .catch((error) => {
                console.error(`Fetch error: ${error}`);
            });
        }

        mediaRecorder.ondataavailable = (e) => {
            chunks.push(e.data);
        }
    })
    .catch(function(err) {
        console.log(`Recording error: ${err}`);
    });
}

const display_options = (items) => {
    // Filter the array for empty items and normalize the text.
    let data = items.filter((item) => { return item.text != ""; })
                    .map((item) => { return { confidence: item.confidence,
                                              text: item.text.toLowerCase()
                                             } });

    if (data.length == 0) {
        // TODO: display some failure notification to the user?
        SpeakToMePopup.hide();
        return;
    }

    // if the first result has a high enough confidence, just
    // use it directly.
    if (data[0].confidence > 0.90) {
        SpeakToMeIcon.input_field.value = data[0].text;
        SpeakToMePopup.hide();
        return;
    }

    SpeakToMePopup.choose_item(data).then((text) => {
        SpeakToMeIcon.input_field.value = text;
        // Once a choice is made, close the popup.
        SpeakToMePopup.hide();
    });
}

SpeakToMePopup.init();
SpeakToMeIcon.init();

})();
<|endoftext|>


<commit_before>angular.module('pascalprecht.translate')

/**
 * @ngdoc property
 * @name pascalprecht.translate.TRANSLATE_MF_INTERPOLATION_CACHE
 * @requires TRANSLATE_MF_INTERPOLATION_CACHE
 *
 * @description
 * Uses MessageFormat.js to interpolate strings against some values.
 */
.constant('TRANSLATE_MF_INTERPOLATION_CACHE', '$translateMessageFormatInterpolation')

/**
 * @ngdoc object
 * @name pascalprecht.translate.$translateMessageFormatInterpolationProvider
 *
 * Configurations for $translateMessageFormatInterpolation
 */
.provider('$translateMessageFormatInterpolation', $translateMessageFormatInterpolationProvider);

function $translateMessageFormatInterpolationProvider() {

  'use strict';

  var configurer;

  /**
   * @ngdoc function
   * @name pascalprecht.translate.$translateMessageFormatInterpolationProvider#messageFormatConfigurer
   * @methodOf pascalprecht.translate.$translateMessageFormatInterpolationProvider
   *
   * @description
   * Defines an optional configurer for the MessageFormat instance.
   *
   * Note: This hook will be called whenever a new instance of MessageFormat will be created.
   *
   * @param {function} fn callback with the instance as argument
   */
  this.messageFormatConfigurer = function (fn) {
    configurer = fn;
  };

  /**
   * @ngdoc object
   * @name pascalprecht.translate.$translateMessageFormatInterpolation
   * @requires pascalprecht.translate.TRANSLATE_MF_INTERPOLATION_CACHE
   *
   * @description
   * Uses MessageFormat.js to interpolate strings against some values.
   *
   * Be aware to configure a proper sanitization strategy.
   *
   * See also:
   * * {@link pascalprecht.translate.$translateSanitization}
   * * {@link https://github.com/SlexAxton/messageformat.js}
   *
   * @return {object} $translateMessageFormatInterpolation Interpolator service
   */
  this.$get = function ($translateSanitization, $cacheFactory, TRANSLATE_MF_INTERPOLATION_CACHE) {
    return $translateMessageFormatInterpolation($translateSanitization, $cacheFactory, TRANSLATE_MF_INTERPOLATION_CACHE, configurer);
  };

}

function $translateMessageFormatInterpolation($translateSanitization, $cacheFactory, TRANSLATE_MF_INTERPOLATION_CACHE, messageFormatConfigurer) {

  'use strict';

  var $translateInterpolator = {},
      $cache = $cacheFactory.get(TRANSLATE_MF_INTERPOLATION_CACHE),
      // instantiate with default locale (which is 'en')
      $mf = new MessageFormat('en'),
      $identifier = 'messageformat';

  if (angular.isFunction(messageFormatConfigurer)) {
    messageFormatConfigurer($mf);
  }

  if (!$cache) {
    // create cache if it doesn't exist already
    $cache = $cacheFactory(TRANSLATE_MF_INTERPOLATION_CACHE);
  }

  $cache.put('en', $mf);

  /**
   * @ngdoc function
   * @name pascalprecht.translate.$translateMessageFormatInterpolation#setLocale
   * @methodOf pascalprecht.translate.$translateMessageFormatInterpolation
   *
   * @description
   * Sets current locale (this is currently not use in this interpolation).
   *
   * @param {string} locale Language key or locale.
   */
  $translateInterpolator.setLocale = function (locale) {
    $mf = $cache.get(locale);
    if (!$mf) {
      $mf = new MessageFormat(locale);
      if (angular.isFunction(messageFormatConfigurer)) {
        messageFormatConfigurer($mf);
      }
      $cache.put(locale, $mf);
    }
  };

  /**
   * @ngdoc function
   * @name pascalprecht.translate.$translateMessageFormatInterpolation#getInterpolationIdentifier
   * @methodOf pascalprecht.translate.$translateMessageFormatInterpolation
   *
   * @description
   * Returns an identifier for this interpolation service.
   *
   * @returns {string} $identifier
   */
  $translateInterpolator.getInterpolationIdentifier = function () {
    return $identifier;
  };

  /**
   * @deprecated will be removed in 3.0
   * @see {@link pascalprecht.translate.$translateSanitization}
   */
  $translateInterpolator.useSanitizeValueStrategy = function (value) {
    $translateSanitization.useStrategy(value);
    return this;
  };

  /**
   * @ngdoc function
   * @name pascalprecht.translate.$translateMessageFormatInterpolation#interpolate
   * @methodOf pascalprecht.translate.$translateMessageFormatInterpolation
   *
   * @description
   * Interpolates given string against given interpolate params using MessageFormat.js.
   *
   * @returns {string} interpolated string.
   */
  $translateInterpolator.interpolate = function (string, interpolationParams, context, sanitizeStrategy) {
    interpolationParams = interpolationParams || {};
    interpolationParams = $translateSanitization.sanitize(interpolationParams, 'params', sanitizeStrategy);

    var compiledFunction = $cache.get('mf:' + string);

    // if given string wasn't compiled yet, we do so now and never have to do it again
    if (!compiledFunction) {

      // Ensure explicit type if possible
      // MessageFormat checks the actual type (i.e. for amount based conditions)
      for (var key in interpolationParams) {
        if (interpolationParams.hasOwnProperty(key)) {
          // ensure number
          var number = parseInt(interpolationParams[key], 10);
          if (angular.isNumber(number) && ('' + number) === interpolationParams[key]) {
            interpolationParams[key] = number;
          }
        }
      }

      compiledFunction = $mf.compile(string);
      $cache.put('mf:' + string, compiledFunction);
    }

    var interpolatedText = compiledFunction(interpolationParams);
    return $translateSanitization.sanitize(interpolatedText, 'text', sanitizeStrategy);
  };

  return $translateInterpolator;
}

$translateMessageFormatInterpolation.displayName = '$translateMessageFormatInterpolation';
<commit_msg>docs(): fix missing ngdocs annotaton<commit_after>angular.module('pascalprecht.translate')

/**
 * @ngdoc property
 * @name pascalprecht.translate.TRANSLATE_MF_INTERPOLATION_CACHE
 * @requires TRANSLATE_MF_INTERPOLATION_CACHE
 *
 * @description
 * Uses MessageFormat.js to interpolate strings against some values.
 */
.constant('TRANSLATE_MF_INTERPOLATION_CACHE', '$translateMessageFormatInterpolation')

/**
 * @ngdoc object
 * @name pascalprecht.translate.$translateMessageFormatInterpolationProvider
 *
 * @description
 * Configurations for $translateMessageFormatInterpolation
 */
.provider('$translateMessageFormatInterpolation', $translateMessageFormatInterpolationProvider);

function $translateMessageFormatInterpolationProvider() {

  'use strict';

  var configurer;

  /**
   * @ngdoc function
   * @name pascalprecht.translate.$translateMessageFormatInterpolationProvider#messageFormatConfigurer
   * @methodOf pascalprecht.translate.$translateMessageFormatInterpolationProvider
   *
   * @description
   * Defines an optional configurer for the MessageFormat instance.
   *
   * Note: This hook will be called whenever a new instance of MessageFormat will be created.
   *
   * @param {function} fn callback with the instance as argument
   */
  this.messageFormatConfigurer = function (fn) {
    configurer = fn;
  };

  /**
   * @ngdoc object
   * @name pascalprecht.translate.$translateMessageFormatInterpolation
   * @requires pascalprecht.translate.TRANSLATE_MF_INTERPOLATION_CACHE
   *
   * @description
   * Uses MessageFormat.js to interpolate strings against some values.
   *
   * Be aware to configure a proper sanitization strategy.
   *
   * See also:
   * * {@link pascalprecht.translate.$translateSanitization}
   * * {@link https://github.com/SlexAxton/messageformat.js}
   *
   * @return {object} $translateMessageFormatInterpolation Interpolator service
   */
  this.$get = function ($translateSanitization, $cacheFactory, TRANSLATE_MF_INTERPOLATION_CACHE) {
    return $translateMessageFormatInterpolation($translateSanitization, $cacheFactory, TRANSLATE_MF_INTERPOLATION_CACHE, configurer);
  };

}

function $translateMessageFormatInterpolation($translateSanitization, $cacheFactory, TRANSLATE_MF_INTERPOLATION_CACHE, messageFormatConfigurer) {

  'use strict';

  var $translateInterpolator = {},
      $cache = $cacheFactory.get(TRANSLATE_MF_INTERPOLATION_CACHE),
      // instantiate with default locale (which is 'en')
      $mf = new MessageFormat('en'),
      $identifier = 'messageformat';

  if (angular.isFunction(messageFormatConfigurer)) {
    messageFormatConfigurer($mf);
  }

  if (!$cache) {
    // create cache if it doesn't exist already
    $cache = $cacheFactory(TRANSLATE_MF_INTERPOLATION_CACHE);
  }

  $cache.put('en', $mf);

  /**
   * @ngdoc function
   * @name pascalprecht.translate.$translateMessageFormatInterpolation#setLocale
   * @methodOf pascalprecht.translate.$translateMessageFormatInterpolation
   *
   * @description
   * Sets current locale (this is currently not use in this interpolation).
   *
   * @param {string} locale Language key or locale.
   */
  $translateInterpolator.setLocale = function (locale) {
    $mf = $cache.get(locale);
    if (!$mf) {
      $mf = new MessageFormat(locale);
      if (angular.isFunction(messageFormatConfigurer)) {
        messageFormatConfigurer($mf);
      }
      $cache.put(locale, $mf);
    }
  };

  /**
   * @ngdoc function
   * @name pascalprecht.translate.$translateMessageFormatInterpolation#getInterpolationIdentifier
   * @methodOf pascalprecht.translate.$translateMessageFormatInterpolation
   *
   * @description
   * Returns an identifier for this interpolation service.
   *
   * @returns {string} $identifier
   */
  $translateInterpolator.getInterpolationIdentifier = function () {
    return $identifier;
  };

  /**
   * @deprecated will be removed in 3.0
   * @see {@link pascalprecht.translate.$translateSanitization}
   */
  $translateInterpolator.useSanitizeValueStrategy = function (value) {
    $translateSanitization.useStrategy(value);
    return this;
  };

  /**
   * @ngdoc function
   * @name pascalprecht.translate.$translateMessageFormatInterpolation#interpolate
   * @methodOf pascalprecht.translate.$translateMessageFormatInterpolation
   *
   * @description
   * Interpolates given string against given interpolate params using MessageFormat.js.
   *
   * @returns {string} interpolated string.
   */
  $translateInterpolator.interpolate = function (string, interpolationParams, context, sanitizeStrategy) {
    interpolationParams = interpolationParams || {};
    interpolationParams = $translateSanitization.sanitize(interpolationParams, 'params', sanitizeStrategy);

    var compiledFunction = $cache.get('mf:' + string);

    // if given string wasn't compiled yet, we do so now and never have to do it again
    if (!compiledFunction) {

      // Ensure explicit type if possible
      // MessageFormat checks the actual type (i.e. for amount based conditions)
      for (var key in interpolationParams) {
        if (interpolationParams.hasOwnProperty(key)) {
          // ensure number
          var number = parseInt(interpolationParams[key], 10);
          if (angular.isNumber(number) && ('' + number) === interpolationParams[key]) {
            interpolationParams[key] = number;
          }
        }
      }

      compiledFunction = $mf.compile(string);
      $cache.put('mf:' + string, compiledFunction);
    }

    var interpolatedText = compiledFunction(interpolationParams);
    return $translateSanitization.sanitize(interpolatedText, 'text', sanitizeStrategy);
  };

  return $translateInterpolator;
}

$translateMessageFormatInterpolation.displayName = '$translateMessageFormatInterpolation';
<|endoftext|>


<commit_before>"use strict";

var PokemonFilter = new function () {
	var self = this;

	var app = window.App;

	var home = app.home;
	var pokedex = home.pokedex;
	var invPokedex = _.invert(home.pokedex); // for lookup by name

	// Pokemon that can't be caught - don't show buttons for them
	var UNOBTAINABLES = [
		+invPokedex['Mew'],
		+invPokedex['Mewtwo']
	];

	var TOTAL_POKEMON_COUNT = 150;

	//region Config

	self.debug = false;
	self.bubbles = false;
	self.enabled = true;
	self.blacklists = [];
	self.preset_n = 0;

	// This will be a pointer to the active blacklist save slot
	self.blacklist = [];

	this.successBubble = function(text, heading) {
		self.debug && console.log('[filter] ' + heading + ' ' + text);

		if (!self.bubbles) return;
		toastr.remove();
		app.success(text, heading);
	};

	this.errorBubble = function(text, heading) {
		self.debug && console.log('[filter] ' + heading, ' ' + text);

		if (!self.bubbles) return;
		toastr.remove();
		app.error(text, heading);
	};

	this.loadConfig = function () {
		var configStr = localStorage.getItem('pokemon_filter');

		// Defaults
		var config = {
			enabled: true,
			blacklists: [[], [], [], [], []],
			preset_n: 0,
			bubbles: true,
			debug: false
		};

		if (configStr) {
			try {
				config = JSON.parse(configStr);
			} catch (e) {
				console.warn("[filter] PokemonFilter config corrupted, cleaning up.");
			}
		}

		// Load the config
		self.blacklists = config.blacklists;
		if (_.isUndefined(self.blacklists) || !_.isArray(self.blacklists) || self.blacklists.length != 5) {
			self.blacklists = [[], [], [], [], []];
		}

		self.preset_n = config.preset_n || 0;
		self.enabled = !!config.enabled;
		self.bubbles = !!config.bubbles;
		self.debug = !!config.debug;

		// 'Open the slot'
		self.blacklist = self.blacklists[self.preset_n];

		self.debug && console.log("[filter] Config loaded: ", config);
	};

	this.persistConfig = function () {
		var config = {
			enabled: self.enabled,
			blacklists: self.blacklists,
			preset_n: self.preset_n,
			bubbles: self.bubbles,
			debug: self.debug
		};

		localStorage.setItem('pokemon_filter', JSON.stringify(config));

		self.debug && console.log("[filter] Saved config.");
	};

	//endregion

	/** Check if a Pokémon is blacklisted */
	this.isBlacklisted = function (id) {
		return self.enabled && (self.blacklist.indexOf(+id) != -1);
	};

	/** Start the extension */
	this.init = function () {
		if (_.isUndefined(app) || $('main').hasClass('error')) {
			console.warn('[filter] Either the site scripts changed, or it didn\'t load right.\nCannot init PokéVision Filter.');
			return;
		}

		self.loadConfig();

		self.installFilter();
		self.applyBlacklist();
		self.createFilterPane();

		console.info("[filter] Filter ready, good hunt!");
	};

	/**
	 * he extension is loaded after the page has already loaded some Pokemon.
	 * Let's get rid of the hidden ones
	 */
	this.applyBlacklist = function () {
		self.debug && console.log('[filter] Removing hidden Pokémon from the map.');

		_.each(home.pokemon, function (entry) {
			if (_.isUndefined(entry)) return;

			if (self.isBlacklisted(entry.pokemonId)) {
				// mark it expired - it'll get removed in updateMarkers()
				entry.expiration_time -= 10000;
			}
		});
		home.updateMarkers();
	};

	/** Select a save slot to use */
	this.selectActivePreset = function(n) {
		if (n < 0 || n >= self.blacklists.length) {
			console.errorBubble("[filter] Invalid slot number:", n);
			return;
		}

		self.blacklist = self.blacklists[n];
		self.preset_n = n;

		self.debug && console.log("[filter] Selected preset slot", n);

		$('.x-filter-pokemon').each(function(n, elem) {
			var id = $(elem).data('id');
			$(elem).toggleClass('filter-hidden', self.isBlacklisted(id));
		});

		self.persistConfig();
		self.applyBlacklist();

		self.successBubble('Click the map or refresh the page for it to have effect.', 'Filter preset changed');
	};

	/** Wrap the createMarker function in a filter */
	this.installFilter = function () {
		self.debug && console.log("[filter] Installing Pokémon filter...");

		home.createMarkerOrig = home.createMarker;
		home.createMarker = function (idx, pk) {
			if (self.isBlacklisted(pk.pokemonId)) {
				// Return a harmless do-nothing stub
				return {
					updateLabel: function () {
					}
				};
			}

			return home.createMarkerOrig(idx, pk);
		};
	};

	this.togglePokemon = function (id, state) {
		id = +id; // cast

		self.debug && console.log('[filter] Toggling pokemon:', id, '(' + pokedex[id] + ')');

		var wantShow = self.isBlacklisted(id);

		if (!_.isUndefined(state)) {
			wantShow = state;
		}

		var elem = $('#filter-pokemon-id-' + id);

		if (wantShow) {
			// un-hide
			delete self.blacklist[self.blacklist.indexOf(id)];
			elem.removeClass('filter-hidden');
		} else {
			self.blacklist.push(id);
			elem.addClass('filter-hidden');
		}

		// Hide / show
		self.applyBlacklist();
		self.persistConfig();
	};

	this.createFilterPane = function () {
		self.debug && console.log("[filter] Building user interface...");

		var sidebar = $('.home-sidebar');

		var filterDiv = $('

');

		// Top row with toggle buttons
		var toggleBox = $('
' +
			'<input type="checkbox" id="pf-enable">' +
			'Pokémon Filter' +
			'
');
		for (var i = 0; i < self.blacklists.length; i++) {
			toggleBox.append(''+(i+1)+'');
		}
		filterDiv.append(toggleBox);

		// Box with save slots
		var slotBox = $('
' +
			'Show all' +
			'Hide all' +
			'
');
		filterDiv.append(slotBox);

		// Box with the pokémon images
		for (var id = 1; id < TOTAL_POKEMON_COUNT + 1; id++) {
			if (UNOBTAINABLES.indexOf(id) != -1) continue;

			var img = document.createElement('img');
			img.src = 'https://ugc.pokevision.com/images/pokemon/' + id + '.png';
			img.title = pokedex[id] + ' - ' + id + '';
			img.classList.add('x-filter-pokemon');
			img.id = 'filter-pokemon-id-' + id;
			img.dataset.id = id;

			if (self.isBlacklisted(id)) {
				img.classList.add('filter-hidden');
			}

			filterDiv.append(img);
		}

		sidebar.prepend(filterDiv);

		// Checkbox for toggles

		filterDiv.append($('
' +
			'<input type="checkbox" id="pf-bubbles">' +
			'Info bubbles for filter actions' +
			'
'));

		filterDiv.append($('
' +
			'<input type="checkbox" id="pf-debug">' +
			'Debug logging' +
			'
'));

		// Preset toggles
		$('#pf-slot-'+self.preset_n).addClass('active');
		$('.pf-slot').on('click', function () {
			$('.pf-slot').removeClass('active');
			$(this).addClass('active');
			var n = $(this).data('n');
			self.selectActivePreset(n);
		});

		// Enable/disable toggle
		$('#pf-enable').attr('checked', self.enabled).on('change', function() {
			self.enabled = $(this).is(':checked');
			self.applyBlacklist();
			self.persistConfig();

			if (self.enabled) {
				self.successBubble(null, 'Filter enabled');
			} else {
				self.errorBubble(null, 'Filter disabled');
			}
		});

		// Toggle bubbles
		$('#pf-bubbles').attr('checked', self.enabled).on('change', function() {
			self.bubbles = $(this).is(':checked');
			self.persistConfig();

			toastr.remove();
			self.successBubble(null, 'Filter info bubbles enabled');
		});

		// Toggle debug
		$('#pf-debug').attr('checked', self.debug).on('change', function() {
			self.debug = $(this).is(':checked');
			self.persistConfig();

			if (self.debug) {
				self.successBubble(null, 'Filter debug enabled');
			} else {
				self.errorBubble(null, 'Filter debug disabled');
			}
		});

		// Toggle pokémon on click
		$('.x-filter-pokemon').on('click', function () {
			self.togglePokemon(this.dataset.id); // also applies and saves
		});

		// Hide & show buttons
		$('#pf-hide-all').on('click', function () {
			$('.x-filter-pokemon').addClass('filter-hidden');
			self.blacklist.length = 0;

			for (var i = 1; i < TOTAL_POKEMON_COUNT+1; i++) {
				self.blacklist.push(i);
			}

			self.applyBlacklist();
			self.persistConfig();

			self.successBubble('Enable the ones you want, then click the map or refresh the page.', 'All species hidden.')
		});

		$('#pf-show-all').on('click', function () {
			$('.x-filter-pokemon').removeClass('filter-hidden');
			self.blacklist.length = 0;
			self.applyBlacklist();
			self.persistConfig();

			self.successBubble('Refresh the page or click the map to load them.', 'All species visible.')
		});
	};
};

// Start up
PokemonFilter.init();
<commit_msg>fixed issue with junk being saved to localstorage<commit_after>"use strict";

var PokemonFilter = new function () {
	var self = this;

	var app = window.App;

	var home = app.home;
	var pokedex = home.pokedex;
	var invPokedex = _.invert(home.pokedex); // for lookup by name

	// Pokemon that can't be caught - don't show buttons for them
	var UNOBTAINABLES = [
		+invPokedex['Mew'],
		+invPokedex['Mewtwo']
	];

	var TOTAL_POKEMON_COUNT = 150;

	//region Config

	self.debug = false;
	self.bubbles = false;
	self.enabled = true;
	self.blacklists = [];
	self.preset_n = 0;

	// This will be a pointer to the active blacklist save slot
	self.blacklist = [];

	this.successBubble = function(text, heading) {
		self.debug && console.log('[filter] ' + heading + ' ' + text);

		if (!self.bubbles) return;
		toastr.remove();
		app.success(text, heading);
	};

	this.errorBubble = function(text, heading) {
		self.debug && console.log('[filter] ' + heading, ' ' + text);

		if (!self.bubbles) return;
		toastr.remove();
		app.error(text, heading);
	};

	this.loadConfig = function () {
		var configStr = localStorage.getItem('pokemon_filter');

		// Defaults
		var config = {
			enabled: true,
			blacklists: [[], [], [], [], []],
			preset_n: 0,
			bubbles: true,
			debug: false
		};

		if (configStr) {
			try {
				config = JSON.parse(configStr);
			} catch (e) {
				console.warn("[filter] PokemonFilter config corrupted, cleaning up.");
			}
		}

		// Load the config
		self.blacklists = config.blacklists;
		if (_.isUndefined(self.blacklists) || !_.isArray(self.blacklists) || self.blacklists.length != 5) {
			self.blacklists = [[], [], [], [], []];
		}

		self.preset_n = config.preset_n || 0;
		self.enabled = !!config.enabled;
		self.bubbles = !!config.bubbles;
		self.debug = !!config.debug;

		// 'Open the slot'
		self.blacklist = self.blacklists[self.preset_n];

		self.debug && console.log("[filter] Config loaded: ", config);
	};

	this.persistConfig = function () {
		// Remove nulls
		for (var i = 0; i < self.blacklists.length; i++) {
			self.blacklists[i] = _.filter(self.blacklists[i], function(x) {return !_.isUndefined(x) && !_.isNull(x)});
		}

		// Fix broken reference
		self.blacklist = self.blacklists[self.preset_n];

		var config = {
			enabled: self.enabled,
			blacklists: self.blacklists,
			preset_n: self.preset_n,
			bubbles: self.bubbles,
			debug: self.debug
		};

		localStorage.setItem('pokemon_filter', JSON.stringify(config));

		self.debug && console.log("[filter] Saved config.");
	};

	//endregion

	/** Check if a Pokémon is blacklisted */
	this.isBlacklisted = function (id) {
		return self.enabled && (self.blacklist.indexOf(+id) != -1);
	};

	/** Start the extension */
	this.init = function () {
		if (_.isUndefined(app) || $('main').hasClass('error')) {
			console.warn('[filter] Either the site scripts changed, or it didn\'t load right.\nCannot init PokéVision Filter.');
			return;
		}

		self.loadConfig();

		self.installFilter();
		self.applyBlacklist();
		self.createFilterPane();

		console.info("[filter] Filter ready, good hunt!");
	};

	/**
	 * he extension is loaded after the page has already loaded some Pokemon.
	 * Let's get rid of the hidden ones
	 */
	this.applyBlacklist = function () {
		self.debug && console.log('[filter] Removing hidden Pokémon from the map.');

		_.each(home.pokemon, function (entry) {
			if (_.isUndefined(entry)) return;

			if (self.isBlacklisted(entry.pokemonId)) {
				// mark it expired - it'll get removed in updateMarkers()
				entry.expiration_time -= 10000;
			}
		});
		home.updateMarkers();
	};

	/** Select a save slot to use */
	this.selectActivePreset = function(n) {
		if (n < 0 || n >= self.blacklists.length) {
			console.errorBubble("[filter] Invalid slot number:", n);
			return;
		}

		self.blacklist = self.blacklists[n];
		self.preset_n = n;

		self.debug && console.log("[filter] Selected preset slot", n);

		$('.x-filter-pokemon').each(function(n, elem) {
			var id = $(elem).data('id');
			$(elem).toggleClass('filter-hidden', self.isBlacklisted(id));
		});

		self.persistConfig();
		self.applyBlacklist();

		self.successBubble('Click the map or refresh the page for it to have effect.', 'Filter preset changed');
	};

	/** Wrap the createMarker function in a filter */
	this.installFilter = function () {
		self.debug && console.log("[filter] Installing Pokémon filter...");

		home.createMarkerOrig = home.createMarker;
		home.createMarker = function (idx, pk) {
			if (self.isBlacklisted(pk.pokemonId)) {
				// Return a harmless do-nothing stub
				return {
					updateLabel: function () {
					}
				};
			}

			return home.createMarkerOrig(idx, pk);
		};
	};

	this.togglePokemon = function (id, state) {
		id = +id; // cast

		self.debug && console.log('[filter] Toggling pokemon:', id, '(' + pokedex[id] + ')');

		var wantShow = self.isBlacklisted(id);

		if (!_.isUndefined(state)) {
			wantShow = state;
		}

		var elem = $('#filter-pokemon-id-' + id);

		if (wantShow) {
			// un-hide
			delete self.blacklist[self.blacklist.indexOf(id)];
			elem.removeClass('filter-hidden');
		} else {
			self.blacklist.push(id);
			elem.addClass('filter-hidden');
		}

		// Hide / show
		self.applyBlacklist();
		self.persistConfig();
	};

	this.createFilterPane = function () {
		self.debug && console.log("[filter] Building user interface...");

		var sidebar = $('.home-sidebar');

		var filterDiv = $('

');

		// Top row with toggle buttons
		var toggleBox = $('
' +
			'<input type="checkbox" id="pf-enable">' +
			'Pokémon Filter' +
			'
');
		for (var i = 0; i < self.blacklists.length; i++) {
			toggleBox.append(''+(i+1)+'');
		}
		filterDiv.append(toggleBox);

		// Box with save slots
		var slotBox = $('
' +
			'Show all' +
			'Hide all' +
			'
');
		filterDiv.append(slotBox);

		// Box with the pokémon images
		for (var id = 1; id < TOTAL_POKEMON_COUNT + 1; id++) {
			if (UNOBTAINABLES.indexOf(id) != -1) continue;

			var img = document.createElement('img');
			img.src = 'https://ugc.pokevision.com/images/pokemon/' + id + '.png';
			img.title = pokedex[id] + ' - ' + id + '';
			img.classList.add('x-filter-pokemon');
			img.id = 'filter-pokemon-id-' + id;
			img.dataset.id = id;

			if (self.isBlacklisted(id)) {
				img.classList.add('filter-hidden');
			}

			filterDiv.append(img);
		}

		sidebar.prepend(filterDiv);

		// Checkbox for toggles

		filterDiv.append($('
' +
			'<input type="checkbox" id="pf-bubbles">' +
			'Info bubbles for filter actions' +
			'
'));

		filterDiv.append($('
' +
			'<input type="checkbox" id="pf-debug">' +
			'Debug logging' +
			'
'));

		// Preset toggles
		$('#pf-slot-'+self.preset_n).addClass('active');
		$('.pf-slot').on('click', function () {
			$('.pf-slot').removeClass('active');
			$(this).addClass('active');
			var n = $(this).data('n');
			self.selectActivePreset(n);
		});

		// Enable/disable toggle
		$('#pf-enable').attr('checked', self.enabled).on('change', function() {
			self.enabled = $(this).is(':checked');
			self.applyBlacklist();
			self.persistConfig();

			if (self.enabled) {
				self.successBubble(null, 'Filter enabled');
			} else {
				self.errorBubble(null, 'Filter disabled');
			}
		});

		// Toggle bubbles
		$('#pf-bubbles').attr('checked', self.enabled).on('change', function() {
			self.bubbles = $(this).is(':checked');
			self.persistConfig();

			toastr.remove();
			self.successBubble(null, 'Filter info bubbles enabled');
		});

		// Toggle debug
		$('#pf-debug').attr('checked', self.debug).on('change', function() {
			self.debug = $(this).is(':checked');
			self.persistConfig();

			if (self.debug) {
				self.successBubble(null, 'Filter debug enabled');
			} else {
				self.errorBubble(null, 'Filter debug disabled');
			}
		});

		// Toggle pokémon on click
		$('.x-filter-pokemon').on('click', function () {
			self.togglePokemon(this.dataset.id); // also applies and saves
		});

		// Hide & show buttons
		$('#pf-hide-all').on('click', function () {
			$('.x-filter-pokemon').addClass('filter-hidden');
			self.blacklist.length = 0;

			for (var i = 1; i < TOTAL_POKEMON_COUNT+1; i++) {
				self.blacklist.push(i);
			}

			self.applyBlacklist();
			self.persistConfig();

			self.successBubble('Enable the ones you want, then click the map or refresh the page.', 'All species hidden.')
		});

		$('#pf-show-all').on('click', function () {
			$('.x-filter-pokemon').removeClass('filter-hidden');
			self.blacklist.length = 0;
			self.applyBlacklist();
			self.persistConfig();

			self.successBubble('Refresh the page or click the map to load them.', 'All species visible.')
		});
	};
};

// Start up
PokemonFilter.init();
<|endoftext|>


<commit_before>module.exports = {
  API_URL: process.env.SHIPIT_API_URL || 'https://api.shipit.staging.mozilla-releng.net',
  TREEHERDER_URL: 'https://treeherder.mozilla.org',
  TASKCLUSTER_TOOLS_URL: 'https://tools.taskcluster.net',
  AUTH0: {
    domain: 'auth.mozilla.auth0.com',
    clientID: 'FK1mJkHhwjulTYBGklxn8W4Fhd1pgT4t',
    redirectUri: new URL('/login', window.location).href,
    scope: 'full-user-credentials openid profile email',
  },
  PRODUCTS: [
    {
      product: 'firefox',
      prettyName: 'Firefox Desktop',
      appName: 'browser',
      branches: [
        {
          prettyName: 'Maple Beta',
          project: 'maple',
          branch: 'projects/maple',
          repo: 'https://hg.mozilla.org/projects/maple',
        },
      ],
      enablePartials: true,
      channel: 'beta',
    },
    {
      product: 'fennec',
      prettyName: 'Firefox Mobile',
      // TODO: The actual appName is `mobile/android` but it gets the version from `browser`.
      appName: 'browser',
      branches: [
        {
          prettyName: 'Maple Beta',
          project: 'maple',
          branch: 'projects/maple',
          repo: 'https://hg.mozilla.org/projects/maple',
        },
      ],
      enablePartials: false,
      channel: 'beta',
    },
    {
      product: 'devedition',
      prettyName: 'Firefox Developer Edition',
      appName: 'browser',
      branches: [
        {
          prettyName: 'Maple Beta',
          project: 'maple',
          branch: 'projects/maple',
          repo: 'https://hg.mozilla.org/projects/maple',
        },
      ],
      enablePartials: true,
      channel: 'beta',
    },
    {
      product: 'thunderbird',
      prettyName: 'Thunderbird',
      appName: 'mail',
      branches: [
        {
          prettyName: 'Try',
          project: 'try-comm-central',
          branch: 'try-comm-central',
          repo: 'https://hg.mozilla.org/try-comm-central',
        },
      ],
      // TODO
      enablePartials: false,
      channel: 'beta',
    },
  ],
};
<commit_msg>[shipit] Add try as a possible branch for staging releases. (#1475)<commit_after>module.exports = {
  API_URL: process.env.SHIPIT_API_URL || 'https://api.shipit.staging.mozilla-releng.net',
  TREEHERDER_URL: 'https://treeherder.mozilla.org',
  TASKCLUSTER_TOOLS_URL: 'https://tools.taskcluster.net',
  AUTH0: {
    domain: 'auth.mozilla.auth0.com',
    clientID: 'FK1mJkHhwjulTYBGklxn8W4Fhd1pgT4t',
    redirectUri: new URL('/login', window.location).href,
    scope: 'full-user-credentials openid profile email',
  },
  PRODUCTS: [
    {
      product: 'firefox',
      prettyName: 'Firefox Desktop',
      appName: 'browser',
      branches: [
        {
          prettyName: 'Maple Beta',
          project: 'maple',
          branch: 'projects/maple',
          repo: 'https://hg.mozilla.org/projects/maple',
        },
        {
          prettyName: 'Try',
          project: 'try',
          branch: 'try',
          repo: 'https://hg.mozilla.org/try',
        },
      ],
      enablePartials: true,
      channel: 'beta',
    },
    {
      product: 'fennec',
      prettyName: 'Firefox Mobile',
      // TODO: The actual appName is `mobile/android` but it gets the version from `browser`.
      appName: 'browser',
      branches: [
        {
          prettyName: 'Maple Beta',
          project: 'maple',
          branch: 'projects/maple',
          repo: 'https://hg.mozilla.org/projects/maple',
        },
        {
          prettyName: 'Try',
          project: 'try',
          branch: 'try',
          repo: 'https://hg.mozilla.org/try',
        },
      ],
      enablePartials: false,
      channel: 'beta',
    },
    {
      product: 'devedition',
      prettyName: 'Firefox Developer Edition',
      appName: 'browser',
      branches: [
        {
          prettyName: 'Maple Beta',
          project: 'maple',
          branch: 'projects/maple',
          repo: 'https://hg.mozilla.org/projects/maple',
        },
        {
          prettyName: 'Try',
          project: 'try',
          branch: 'try',
          repo: 'https://hg.mozilla.org/try',
        },
      ],
      enablePartials: true,
      channel: 'beta',
    },
    {
      product: 'thunderbird',
      prettyName: 'Thunderbird',
      appName: 'mail',
      branches: [
        {
          prettyName: 'Try',
          project: 'try-comm-central',
          branch: 'try-comm-central',
          repo: 'https://hg.mozilla.org/try-comm-central',
        },
      ],
      // TODO
      enablePartials: false,
      channel: 'beta',
    },
  ],
};
<|endoftext|>