Source: core/localizer.js

    /*global require,exports */

/**
 * @module montage/core/localizer
 * @requires montage/core/core
 * @requires montage/core/logger
 * @requires montage/core/deserializer
 * @requires montage/core/promise
 * @requires montage/core/messageformat
 * @requires montage/core/messageformat-locale
 */
var Montage = require("./core").Montage,
    MessageFormat = require("./messageformat"),
    rootComponent = require("../ui/component").__root__,
    logger = require("./logger").logger("localizer"),
    Serializer = require("./serialization/serializer/montage-serializer").MontageSerializer,
    Deserializer = require("./serialization/deserializer/montage-deserializer").MontageDeserializer,
    Promise = require("./promise").Promise,
    Bindings = require("./core").Bindings,
    FrbBindings = require("frb/bindings"),
    stringify = require("frb/stringify"),
    expand = require("frb/expand"),
    Map = require("collections/map"),
    Scope = require("frb/scope");

// Add all locales to MessageFormat object
MessageFormat.locale = require("./messageformat-locale");

var KEY_KEY = "key",
    DEFAULT_MESSAGE_KEY = "default",
    LOCALE_STORAGE_KEY = "montage_locale",

    // directory name that the locales are stored under
    LOCALES_DIRECTORY = "locale",
    // filename (without extension) of the file that contains the messages
    MESSAGES_FILENAME = "messages",
    // filename of the manifest file
    MANIFEST_FILENAME = "manifest.json";

var EMPTY_STRING_FUNCTION = function () { return ""; };

// This is not a strict match for the grammar in
// http://tools.ietf.org/html/rfc5646, but it's good enough for our purposes.
var reLanguageTagValidator = /^[a-zA-Z]+(?:-[a-zA-Z0-9]+)*$/;

var defaultLocalizer;

/**
 * @class Localizer
 * @extends Montage
 */
var Localizer = exports.Localizer = Montage.specialize( /** @lends Localizer.prototype # */ {

    /**
     * @function
     * @param {string} [locale] The RFC-5646 language tag this localizer should use. Defaults to defaultLocalizer.locale
     * @returns {Localizer} The Localizer object it was called on.
     */
    initWithLocale: {
        value: function (locale) {
            var defaultLocaleStored;

            if (this.storesLocale && typeof window !== "undefined" && window.localStorage) {
                defaultLocaleStored = window.localStorage.getItem(LOCALE_STORAGE_KEY);
            }

            var locateCandidate = locale || defaultLocaleStored || window.navigator.userLanguage || window.navigator.language || Localizer.defaultLocale,
                defaultLocale = this.callDelegateMethod("localizerWillUseLocale", this, locateCandidate);

            this.locale = defaultLocale || locateCandidate;
            this._isInitialized = true;

            this.loadMessages();

            return this;
        }
    },


    initWithLocaleAndDelegate: {
        value: function (locale, delegate) {
            this.delegate = delegate;

            return this.initWithLocale(locale);
        }
    },


    storesLocale: {
        value: false
    },


    component: {
        value: null
    },


    _delegate: {
        value: null
    },

    /**
     * Delegate to get the default locale or the locale messages.
     * Should implement a `getDefaultLocale` method that returns
     * a language-tag string that can be passed to {@link locale}
     * Should implement a `localizerWillLoadMessages` method that returns
     * a Promise that will return a "messages" object with a combination of keys/messages.
     *
     * @example
     * myDelegate.localizerWillLoadMessages = function () {
     *     return Promise(function (resolve, reject){
     *          return {
     *              language_key: "English",
     *              hello_key: "Hello"
     *          };
     *     });
     * }
     *
     * @type Object
     * @default null
     */
    delegate: {
        set: function (delegate) {
            if (!this._delegate && delegate && typeof delegate === "object") {
                this._delegate = delegate;
            }
        },
        get: function () {
            return this._delegate;
        }
    },

    _isInitialized: {
        value: false
    },

    isInitialized: {
        get: function () {
            return this._isInitialized;
        }
    },

    /**
     * The MessageFormat object to use.
     * @type {MessageFormat}
     * @default null
     */
    messageFormat: {
        serializable: false,
        value: null
    },

    _messages: {
        value: null
    },

    /**
     * A map from keys to messages.
     * @type Object
     * @default null
     */
    messages: {
        get: function () {
            return this._messages;
        },
        set: function (value) {
            if (this._messages !== value) {
                if (value !== undefined && value !== null && typeof value !== "object") {
                    throw new TypeError(value, " is not an object");
                }

                this._messages = value;
            }
        }
    },

    /**
     * A promise for the messages property
     * @type Promise
     * @default null
     */
    messagesPromise: {
        serializable: false,
        value: null
    },

    _locale: {
        value: null
    },

    /**
     * A RFC-5646 language-tag specifying the locale of this localizer.
     * Setting the locale will create a new {@link MessageFormat} object with
     * the new locale.
     * @type {string}
     * @default null
     */
    locale: {
        get: function () {
            return this._locale;
        },
        set: function (value) {
            if (!reLanguageTagValidator.test(value)) {
                throw new TypeError("Language tag '" + value + "' is not valid. It must match http://tools.ietf.org/html/rfc5646 (alphanumeric characters separated by hyphens)");
            }

            if (this._locale !== value) {
                var previousLocale = this._locale;

                this._locale = value;
                this.messageFormat = new MessageFormat(value);

                if (previousLocale && previousLocale !== this._locale) {
                    var self = this;

                    this.loadMessages().then(function () {
                        self._dispatchLocaleChange(previousLocale);
                    });
                }

                if (this.storesLocale && typeof window !== "undefined" && window.localStorage) {
                    // If possible, save locale
                    try {
                        window.localStorage.setItem(LOCALE_STORAGE_KEY, value);
                    } catch (e) {
                        // LocalStorage quota might have been exceeded
                        // iOS Safari emits a quota exceeded error from private mode always
                    }
                }
            }
        }
    },

    _availableLocales: {
        value: null
    },

    /**
     * A promise for the locales available in this package. Resolves to an
     * array of strings, each containing a locale tag.
     * @type Promise
     * @default null
     *
     * @returns {Promise.<Array.<String>>}
     */
    availableLocales: {
        get: function () {
            if (this._availableLocales) {
                return this._availableLocales;
            }

            this._availableLocales = this.callDelegateMethod("localizerWillPromiseAvailableLocales", this);

            if (!this._availableLocales) {
                this._availableLocales = this._manifest.get("files").get(LOCALES_DIRECTORY).get("files").then(function (locales) {
                    return Object.keys(locales);
                });
            }

            return this._availableLocales;
        }
    },

    _require: {
        value: global.require
    },

    /**
     * The require function to use in {@link loadMessages}.
     * By default this is set to the global require, meaning that messages
     * will be loaded from the root of the application. To load messages
     * from the root of your package set this to the require function from
     * any class in the package.
     * @type Function
     * @default global require | null
     */
    require: {
        serializable: false,
        get: function () {
            return this._require;
        },
        set: function (value) {
            if (this._require !== value) {
                this.__manifest = null;
                this._require = value;
            }
        }
    },

    __manifest: {
        value: null
    },

    /**
     * Promise for the manifest
     * @private
     * @type Promise
     * @default null
     */
    _manifest: {
        depends: ["require"],
        get: function () {
            var messageRequire = this.require;

            if (messageRequire.packageDescription.manifest === true) {
                if (!this.__manifest) {
                    this.__manifest = messageRequire.async(MANIFEST_FILENAME);
                }
                return this.__manifest;
            } else {
                return Promise.reject(new Error(
                    "Package has no manifest. " + messageRequire.location +
                    "package.json must contain \"manifest\": true and " +
                    messageRequire.location+MANIFEST_FILENAME+" must exist"
                ));
            }
        }
    },

    loadMessagesTimeout: {
        value: 5000
    },

    /**
     * Load messages for the locale
     * @function
     * @param {?number|boolean} [timeout=5000] Number of milliseconds to wait
     * before failing. Set to false for no timeout.
     * @param {Function} [callback] Called on successful loading of messages.
     * Using the returned promise is recomended.
     * @returns {Promise} A promise for the messages.
     */
    loadMessages: {
        value: function (timeout, callback) {
            if (!this.require) {
                throw new Error("Cannot load messages as", this, "require is not set");
            }

            if (typeof timeout !== "number") {
                timeout = this.loadMessagesTimeout;
            }

            this.messages = null;

            var self = this,
                promise = this.callDelegateMethod("localizerWillLoadMessages",this);

            //A delegate may have set a different timeout:
            if(this.hasOwnProperty("loadMessagesTimeout")) {
                timeout = this.loadMessagesTimeout;
            }

            if (promise) {
                promise = promise.timeout(timeout);

            } else {
                promise = this._manifest.timeout(timeout).then(function (manifest) {
                    return self._loadMessageFiles(manifest.files);
                });
            }

            this.messagesPromise = promise.then(function (localesMessages) {
                return self._collapseMessages(localesMessages);
            },function(error) {
                console.error("Could not load messages for '" + self.locale + "': " + error);
                throw error;
            }).then(function (messages) {
                if (typeof callback === "function") {
                    callback(messages);
                }

                return messages;
            });

            return this.messagesPromise;
        }
    },

    /**
     * Load the locale appropriate message files from the given manifest
     * structure.
     * @function
     * @param {Object} files An object mapping directory (locale) names to
     * @returns {Promise} A promise that will be resolved with an array
     * containing the content of message files appropriate to this locale.
     * Suitable for passing into {@link _collapseMessages}.
     * @private
     */
    _loadMessageFiles: {
        value: function (files) {
            var messageRequire = this.require;

            if (!files) {
                return Promise.reject(new Error(
                    messageRequire.location + MANIFEST_FILENAME +
                    " does not contain a 'files' property"
                ));
            }

            var availableLocales, localesMessagesP = [], fallbackLocale, localeFiles, filename;

            if (!(LOCALES_DIRECTORY in files)) {
                return Promise.reject(new Error(
                    "Package does not contain a '" + LOCALES_DIRECTORY + "' directory"
                ));
            }

            availableLocales = files[LOCALES_DIRECTORY].files;
            fallbackLocale = this._locale;

            // Fallback through the language tags, loading any available
            // message files
            while (fallbackLocale !== "") {
                if (availableLocales.hasOwnProperty(fallbackLocale)) {
                    localeFiles = availableLocales[fallbackLocale].files;

                    // Look for Javascript or JSON message files, with the
                    // compiled JS files taking precedence
                    if ((filename = MESSAGES_FILENAME + ".js") in localeFiles ||
                        (filename = MESSAGES_FILENAME + ".json") in localeFiles
                    ) {
                        // Require the message file
                        localesMessagesP.push(messageRequire.async(LOCALES_DIRECTORY + "/" + fallbackLocale + "/" + filename));
                    } else if(logger.isDebug) {
                        logger.debug(this, "Warning: '" + LOCALES_DIRECTORY + "/" + fallbackLocale +
                            "/' does not contain '" + MESSAGES_FILENAME + ".json' or '" + MESSAGES_FILENAME + ".js'");
                    }

                }
                // Strip the last language tag off of the locale
                fallbackLocale = fallbackLocale.substring(0, fallbackLocale.lastIndexOf("-"));
            }

            if (!localesMessagesP.length) {
                return Promise.reject(new Error("Could not find any " + MESSAGES_FILENAME + ".json or " + MESSAGES_FILENAME + ".js files"));
            }

            var promise = Promise.all(localesMessagesP);
            if (logger.isDebug) {
                var self = this;
                promise = promise.then(function (localesMessages) {
                    logger.debug(self, "loaded " + localesMessages.length + " message files");
                    return localesMessages;
                });
            }
            return promise;
        }
    },

    /**
     * Collapse an array of message objects into one, earlier elements taking
     * precedence over later ones.
     * @function
     * @param {Array<Object>} localesMessages
     * @returns {Object} An object mapping messages keys to the messages
     * @example
     * [{hi: "Good-day"}, {hi: "Hello", bye: "Bye"}]
     * // results in
     * {hi: "Good-day", bye: "Bye"}
     * @private
     */
    _collapseMessages: {
        value: function (localesMessages) {
            var messages = {};

            // Go through each set of messages, adding any keys that haven't
            // already been set
            for (var i = 0, l = localesMessages.length; i < l; i++) {
                var localeMessages = localesMessages[i];
                for (var key in localeMessages) {
                    if (localeMessages.hasOwnProperty(key)) {
                        if (!(key in messages)) {
                            messages[key] = localeMessages[key];
                        }
                    }
                }
            }
            this.messages = messages;
            return messages;
        }
    },

    // Caches the compiled functions from strings
    _compiledMessageCache: {
        value: Object.create(null)
    },

    /**
     * Localize a key and return a message function.
     *
     * If the message is a precompiled function then this is returned
     * directly. Otherwise the message string is compiled to a function with
     * <a href="https://github.com/SlexAxton/messageformat.js#readme">
     * messageformat.js</a>. The resulting function takes an object mapping
     * from variables in the message to their values.
     *
     * Be aware that if the messages have not loaded yet this method
     * will not return the localized string. See `localize` for
     * method that works whether the messages have loaded or not.
     *
     * @function
     * @param {string} key The key to the string in the {@link messages} object.
     * @param {string} defaultMessage The value to use if key does not exist.
     * @returns {Function} A function that accepts an object mapping variables
     * in the message string to values.
     */
    localizeSync: {
        value: function (key, defaultMessage) {
            var message, type, compiled;

            if (!key && !defaultMessage) {
                throw new Error("Key or default message must be truthy, not " + key + " and " + defaultMessage);
            }

            if (this._messages && key in this._messages) {
                message = this._messages[key];
                type = typeof message;

                if (type === "function") {
                    return message;
                } else if (type === "object") {
                    if (!("message" in message)) {
                        throw new Error(message, "does not contain a 'message' property");
                    }

                    message = message.message;
                }
            } else {
                message = defaultMessage;
            }

            if (!message) {
                console.warn("No message or default message for key '"+ key +"'");
                // Give back something so there's at least something for the UI
                message = key;
            }

            if (message in this._compiledMessageCache) {
                return this._compiledMessageCache[message];
            }

            var ast = this.messageFormat.parse(message);
            // if we have a simple string then create a very simple function,
            // and set it as its own toString so that it behaves a bit like
            // a string
            if (ast.program && ast.program.statements && ast.program.statements.length === 1 && ast.program.statements[0].type === "string") {
                compiled = function () { return message; };
                compiled.toString = compiled;
            } else {
                /* jshint evil:true */
                compiled = (new Function('MessageFormat', 'return ' + this.messageFormat.precompile(ast))(MessageFormat));
                /* jshint evil:false */
            }

            this._compiledMessageCache[message] = compiled;
            return compiled;
        }
    },

    /**
     * Async version of {@link localize}.
     *
     * Waits for the localizer to get messages before localizing the key.
     * Use either the callback or the promise.
     *
     * ```js
     * defaultLocalizer.localize("hello_name", "Hello, {name}!").then(function (hi) {
     *     console.log(hi({name: "World"})); // => "Hello, World!""
     *     console.log(hi()); // => Error: MessageFormat: No data passed to function.
     * });
     * ```
     *
     * If the message for the key is "simple", i.e. it does not contain any
     * variables, then the function will implement a custom `toString`
     * function that also returns the message. This means that you can use
     * the function like a string. Example:
     *
     * ```js
     * defaultLocalizer.localize("hello", "Hello").then(function (hi) {
     *     // Concatenating an object to a string calls its toString
     *     myObject.hello = "" + hi;
     *     var y = "The greeting '" + hi + "' is used in this locale";
     *     // textContent only accepts strings and so calls toString
     *     button.textContent = hi;
     *     // and use as a function also works.
     *     var z = hi();
     * });
     * ```
     *
     * @function
     * @param {string} key The key to the string in the {@link messages}
     * object.
     * @param {string} defaultMessage The value to use if key does not exist.
     * @param {string} [defaultOnFail=true] Whether to use the default messages
     * if the messages fail to load.
     * @param {Function} [callback] Passed the message function.
     * @returns {Promise} A promise that is resolved with the message function.
    */
    localize: {
        value: function (key, defaultMessage, defaultOnFail, callback) {
            defaultOnFail = (typeof defaultOnFail === "undefined") ? true : defaultOnFail;

            var self = this,
                promise;

            if (!this._isInitialized) {
                this.initWithLocale();

                promise = this.messagesPromise.then(function () {
                    return (self.localize(key, defaultMessage, defaultOnFail, callback));
                });

            } else if (!this.messagesPromise) {
                promise = Promise.resolve(this.localizeSync(key, defaultMessage));
                promise.then(callback);

            } else {
                var l = function () {
                    var messageFn = self.localizeSync(key, defaultMessage);

                    if (typeof callback === "function") {
                        callback(messageFn);
                    }

                    return messageFn;
                };

                if (defaultOnFail) {
                    // Try and localize the message, no matter what the outcome
                    promise = this.messagesPromise.then(l, l);

                } else {
                    promise = this.messagesPromise.then(l);
                }
            }

            return promise;
        }
    },

    /**
     * Reset the saved locale back to default by using the steps above.
     * @function
     * @returns {string} the reset locale
     */
    reset: {
        value: function () {
            if (this.storesLocale && typeof window !== "undefined" && window.localStorage) {
                window.localStorage.removeItem(LOCALE_STORAGE_KEY);
            }

            this.initWithLocale();

            return this._locale;
        }
    },

    _dispatchLocaleChangeAsNeeded: {
        value: function (previousLocale, component) {
            if (component && (component.localizer === null || component.localizer === void 0 || component.localizer === this)) {
                if (typeof component.localizerDidChangeLocale === "function") {
                    component.localizerDidChangeLocale(this, previousLocale, this._locale);
                }

                return true;
            }

            return false;
        }
    },


    _dispatchLocaleChange: {
        value: function (previousLocale, _component) {
            if (!_component) {
                _component = this.component || rootComponent;

                if (!this._dispatchLocaleChangeAsNeeded(previousLocale, _component)) {
                    return;
                }
            }

            // Get the private `_childComponents` in order to avoid to create an empty array,
            // indeed childComponents is set lazily.
            var childComponents = _component._childComponents;

            if (childComponents) {
                var child;

                for (var i = 0; i < childComponents.length; i++) {
                    child = childComponents[i];

                    if (this._dispatchLocaleChangeAsNeeded(previousLocale, child)) {
                        if (child._childComponents) {
                            this._dispatchLocaleChange(previousLocale, child);
                        }
                    }
                }
            }
        }
    }

}, {

    defaultLocalizer: {
        value: function () {
            if (!defaultLocalizer) {
                defaultLocalizer = new Localizer();
                defaultLocalizer.storesLocale = true;
            }

            return defaultLocalizer;
        }
    },

    defaultLocalizerWithDelegate: {
        value: function (delegate) {
            this.defaultLocalizer();
            defaultLocalizer.delegate = delegate;

            return defaultLocalizer;
        }
    },

    defaultLocale: {
        value: "en"
    }

});

/**
 * The default localizer.
 * The locale of the defaultLocalizer is determined by following these steps:
 *
 * - If localStorage exists, use the value stored in "montage_locale"
 *   (LOCALE_STORAGE_KEY)
 * - Otherwise use the value of navigator.userLanguage (Internet Explorer)
 * - Otherwise use the value of navigator.language (other browsers)
 * - Otherwise fall back to "en"
 *
 * `defaultLocalizer.locale` can be set and if localStorage exists then the
 * value will be saved in
 *
 * "montage_locale" (LOCALE_STORAGE_KEY).
 */

Object.defineProperty(exports, "defaultLocalizer", {
    get: function () {
        return Localizer.defaultLocalizer();
    }
});


/** The localize function from {@link defaultLocalizer} provided for
 * convenience.
 *
 * @function
 * @see Localizer#localize
 */
var _localize;

Object.defineProperty(exports, "localize", {
    get: function () {
        if (!_localize) {
            var _defaultLocalizer = Localizer.defaultLocalizer();

            _localize = _defaultLocalizer.bind(_defaultLocalizer);
        }

        return _localize;
    }
});

/**
 * Tracks a message function and its data for changes in order to generate a
 * localized message.
 *
 * @class Message
 * @extends Montage
 */
var Message = exports.Message = Montage.specialize( /** @lends Message.prototype # */ {
    /**
     * @constructs Message
     */
    constructor: {
        value: function () {
            //todo: optimisation? set a flag on the localizer to listen when the locale change?
            this.addPathChangeListener("localizer.locale", this, "handleLocaleChange");
        }
    },

    /**
     * @function
     * @param {string|function} keyOrFunction A messageformat string or a
     * function that takes an object argument mapping variables to values and
     * returns a string. Usually the output of Localizer#localize.
     * @param {Object} data  Value for this data property.
     * @returns {Message} this.
     */
    init: {
        value: function (key, defaultMessage, data) {
            if (key) {
                this.key = key;
            }

            if (defaultMessage) {
                this.defaultMessage = defaultMessage;
            }

            if (data) {
                this.data = data;
            }

            return this;
        }
    },

    _localizer: {
        value: null
    },

    localizer: {
        get: function () {
            if (!this._localizer) {
                this._localizer = Localizer.defaultLocalizer();
            }

            return this._localizer;
        },
        set: function (value) {
            if (this._localizer === value) {
                return;
            }
            this._localizer = value;
            this._localize();
        }
    },

    _key: {
        value: null
    },

    /**
     * A key for the default localizer to get the message function from.
     * @type {string}
     * @default null
     */
    key: {
        get: function () {
            return this._key;
        },
        set: function (value) {
            if (this._key === value) {
                return;
            }

            this._key = value;
            this._localize();
        }
    },

    _defaultMessage: {
        value: null
    },

    defaultMessage: {
        get: function () {
            return this._defaultMessage;
        },
        set: function (value) {
            if (this._defaultMessage === value) {
                return;
            }

            this._defaultMessage = value;
            this._localize();
        }
    },

    handleLocaleChange: {
        value: function () {
            if (this._key && this._localizer && this._localizer.isInitialized) {
                this._localize();
            }
        }
    },

    _isLocalizeQueued: {
        value: false
    },

    _localize: {
        value: function () {
            if (this._isLocalizeQueued) {
                return;
            }
            this._isLocalizeQueued = true;

            var self = this;

            // Replace the _messageFunction promise with the real one.
            this._messageFunction = new Promise(function (resolve, reject) {
                // Set up a new promise now, so anyone accessing it in this tick
                // won't get the old one.
                setTimeout(function () {
                    self._isLocalizeQueued = false;

                        if (!self._key && !self._defaultMessage) {
                            // TODO: Revisit when components inside repetitions aren't uselessly instatiated.
                            // While it might seem like we should reject here, when
                            resolve(EMPTY_STRING_FUNCTION);
                            return;
                        }

                        resolve(self._localizer.localize(
                            self._key,
                            self._defaultMessage
                        ));
                    }, 0);
                });

            // Don't use fcall, so that if the `data` object is completely
            // changed we have the latest version.
            this.localized = this._messageFunction.then(function (fn) {
                return self._optimizedMessageCallBack(fn);
            });
        }
    },

    __messageFunction: {
        value: null
    },

    _messageFunction: {
        set: function (messageFunction) {
            this.__messageFunction = messageFunction;
        },
        get: function () {
            if (!this.__messageFunction) {
                this.__messageFunction = Promise.resolve(EMPTY_STRING_FUNCTION);
            }

            return this.__messageFunction;
        }
    },

    _data: {
        value: null
    },

    // Receives an object literal and creates a Map that tracks it.
    data: {
        get: function () {
            if (!this._data) {
                this._data = new Map();
                this._data.addMapChangeListener(this, "data");
            }

            return this._data;
        },
        set: function (data) {
            if (this._data === data) {
                return;
            }

            if (data) {
                // optimisation avoid to call the getter and remove/add listeners
                if (!this._data) {
                    this._data = new Map();

                } else {
                    this.data.removeMapChangeListener(this, "data");

                    if (this._data.length) {
                        this._data.clear();
                    }
                }

                for (var d in data) {
                    if (data.hasOwnProperty(d)) {
                        this.data.set(d, data[d]);
                    }
                }

                this._data.addMapChangeListener(this, "data");
                this.handleDataMapChange();
            }
        }
    },

    // TODO: Remove when possible to bind to promises
    __localizedResolved: {
        value: ""
    },

    _localizedDeferred: {
        value: Promise.resolve()
    },
    /**
        The message localized with all variables replaced.
        @type {string}
        @default ""
    */
    localized: {
        get: function() {
            this._localize();
            return this._localizedDeferred;
        },
        set: function (value) {
            if (value === this._localized) {
                return;
            }
            var self = this;

            // We create our own deferred so that if localized gets set in
            // succession without being resolved, we can replace the old
            // promises with the new one transparently.
            if(this._localizedDeferred) {
                value = Promise.resolve(value);
            }
            //this._localizedDeferred.resolve(deferred.promise);
            //value.then(deferred.resolve, deferred.reject);

            // TODO: Remove when possible to bind to promises
            value.then(function (message) {
                return (self.__localizedResolved = message);
            });

            this._localizedDeferred = value;
        }
    },

    /**
     * Whenever there is a change set the localized property.
     * @type {Function}
     * @private
     */
    handleDataMapChange: {
        value: function (event) {
            if (this._key) {
                var self = this;

                this.localized = this._messageFunction.then(function (fn) {
                    return self._optimizedMessageCallBack(fn);
                });
            }
        }
    },

    // Optimisation: avoid to create garbage and useless Map objects.
    _optimizedMessageCallBack: {
        value: function (fn) {
            if (this._data) {
                return fn(this.data.toObject());
            }

            return fn();
        }
    },

    serializeSelf: {
        value: function (serializer) {
            var result = {
                _bindingDescriptors: this._bindingDescriptors
            };

            // don't serialize the message function
            result.key = this._key;
            result.defaultMessage = this._defaultMessage;

            // only serialize localizer if it isn't the default one
            if (this._localizer !== Localizer.defaultLocalizer()) {
                result.localizer = this._localizer;
            }

            return result;
        }
    },

    serializeForLocalizations: {
        value: function (serializer) {
            var result = {},
                data,
                bindings;

            bindings = FrbBindings.getBindings(this);

            if (bindings && bindings.get("key")) {
                result[KEY_KEY] = {};
                this._serializeBinding(this, result[KEY_KEY], bindings.get("key"), serializer);
            } else {
                result[KEY_KEY] = this._key;
            }

            if (bindings && bindings.defaultMessage) {
                result[DEFAULT_MESSAGE_KEY] = {};
                this._serializeBinding(this, result[DEFAULT_MESSAGE_KEY], bindings.defaultMessage, serializer);
            } else {
                result[DEFAULT_MESSAGE_KEY] = this._defaultMessage;
            }

            var dataBindings = FrbBindings.getBindings(this.data);

            // NOTE: Can't use `Montage.getSerializablePropertyNames(this._data)`
            // because the properties we want to serialize are not defined
            // using `Montage.defineProperty`, and so don't have
            // `serializable: true` as part of the property descriptor.
            data = this.data.toObject();

            for (var p in data) {
                if (data.hasOwnProperty(p) &&
                    (!dataBindings || !dataBindings[".get('"+ p + "')"])
                ) {
                    result.data = result.data || {};
                    result.data[p] = data[p];
                }
            }

            var key, b,
                mapIter = dataBindings.keys();

            // Loop through bindings seperately in case the bound properties
            // haven't been set on the data object yet.
            while ((b = mapIter.next().value)) {
                // binding is in the form of "get('key')" because it's a map
                // but we want to serialize into an object literal instead.
                key = /\.get\('([^']+)'\)/.exec(b)[1];

                result.data = result.data || {};
                result.data[key] = {};
                this._serializeBinding(this.data, result.data[key], dataBindings.get(b), serializer);
            }


            // Loop through bindings seperately in case the bound properties
            // haven't been set on the data object yet.
            // for (b in dataBindings) {
            //     // binding is in the form of "get('key')" because it's a map
            //     // but we want to serialize into an object literal instead.
            //     var key = /\.get\('([^']+)'\)/.exec(b)[1];

            //     result.data = result.data || {};
            //     result.data[key] = {};
            //     this._serializeBinding(this.data, result.data[key], dataBindings[b], serializer);
            // }

            return result;
        }
    },

    _serializeBinding: {
        value: function (object, output, input, serializer) {
            //bindingsParameters = ["<-", "<->", "source", "revert", "convert", "trace", "serializable"];
            //
            //bindingsParameters.forEach(function (key) {
            //    if (key in binding) {
            //        data[key] = binding[key];
            //    }
            //});

            if (("serializable" in input) && !input.serializable) {
                return;
            }

            var scope,
                syntax = input.sourceSyntax;
                
            if (input.source !== object) {
                var reference = serializer.addObjectReference(input.source);
                scope = new Scope({
                    type: "component",
                    label: reference["@"]
                });
                scope.components = serializer;
                syntax = expand(syntax, scope);
            }

            scope = new Scope();
            scope.components = serializer;
            var sourcePath = stringify(syntax, scope);

            if (input.twoWay) {
                output["<->"] = sourcePath;
            } else {
                output["<-"] = sourcePath;
            }

            if (input.converter) {
                output.converter = input.converter;
            } else {
                output.convert = input.convert;
                output.revert = input.revert;
            }

            if (input.trace) {
                output.trace = true;
            }
        }
    }
});

var createMessageBinding = function (object, prop, key, defaultMessage, data, deserializer) {
    var message = new Message();

    if (data) {
        // optimisation
        var dataMap = message._data = new Map();

        var d, property, typeOfProperty;

        for (d in data) {
            if (data.hasOwnProperty(d)) {
                property = data[d];
                typeOfProperty = typeof property;

                if (typeOfProperty === "string") {
                    dataMap.set(d, property);

                } else if (typeOfProperty === "object") {
                    Bindings.defineBinding(dataMap, ".get('" + d + "')", property, {
                        components: deserializer
                    });
                }   
            }
        }

        dataMap.addMapChangeListener(message, "data");
    }

    if (typeof key === "object") {
        Bindings.defineBinding(message, "key", key, {
            components: deserializer
        });
    } else {
        message.key = key;
    }

    if (typeof defaultMessage === "object") {
        Bindings.defineBinding(message, "defaultMessage", defaultMessage, {
            components: deserializer
        });
    } else if (typeof defaultMessage === "string") {
        message.defaultMessage = defaultMessage;
    }

    Bindings.defineBinding(object, prop, {
        // TODO: Remove when possible to bind to promises and replace with
        // binding to "localized"
        "<-": "__localizedResolved",
        source: message,
        serializable: false
    });
};

Serializer.defineSerializationUnit("localizations", function (serializer, object) {
    var bindingDescriptors = FrbBindings.getBindings(object);

    if (bindingDescriptors) {
        var result;
        for (var prop in bindingDescriptors) {
            if (bindingDescriptors.hasOwnProperty(prop)) {
                var desc = bindingDescriptors[prop];
                if (Message.prototype.isPrototypeOf(desc.source)) {
                    if (!result) {
                        result = {};
                    }
                    var message = desc.source;
                    result[prop] = message.serializeForLocalizations(serializer);
                }
            }
        }
        return result;
    }
});

Deserializer.defineDeserializationUnit("localizations", function (deserializer, object, properties) {

    var desc,
        key,
        defaultMessage;

    for (var prop in properties) {
        if (properties.hasOwnProperty(prop)) {

            desc = properties[prop];
            if (!(KEY_KEY in desc)) {
                console.error("localized property '" + prop + "' must contain a key property (" + KEY_KEY + "), in ", properties[prop]);
                continue;
            }
            if(logger.isDebug && !(DEFAULT_MESSAGE_KEY in desc)) {
                logger.debug(this, "Warning: localized property '" + prop + "' does not contain a default message property (" + DEFAULT_MESSAGE_KEY + "), in ", object);
            }

            key = desc[KEY_KEY];
            defaultMessage = desc[DEFAULT_MESSAGE_KEY];

            createMessageBinding(object, prop, key, defaultMessage, desc.data, deserializer);
        }
    }
});