diff --git a/core/localizer.js b/core/localizer.js new file mode 100644 index 0000000000..6b7875ad34 --- /dev/null +++ b/core/localizer.js @@ -0,0 +1,1098 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ +/*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("montage").Montage, + MessageFormat = require("core/messageformat"), + logger = require("core/logger").logger("localizer"), + Serializer = require("core/serializer").Serializer, + Deserializer = require("core/deserializer").Deserializer, + Promise = require("core/promise").Promise, + deserializeBindingToBindingDescriptor = require("core/event/binding").deserializeBindingToBindingDescriptor; + +// Add all locales to MessageFormat object +MessageFormat.locale = require("core/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]+)*$/; + +/** + @class module:montage/core/localizer.Localizer + @extends module:montage/core/core.Montage +*/ +var Localizer = exports.Localizer = Montage.create(Montage, /** @lends module:montage/core/localizer.Localizer# */ { + + /** + Initialize the localizer. + + @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. + */ + init: { + value: function(locale) { + this.locale = locale || defaultLocalizer.locale; + + return this; + } + }, + + /** + Initialize the object + + @function + @param {String} locale The RFC-5646 language tag this localizer should use. + @param {Object} messages A map from keys to messages. Each message should either be a string or an object with a "message" property. + @returns {Localizer} The Localizer object it was called on. + */ + + initWithMessages: { + value: function(locale, messages) { + this.locale = locale; + this.messages = messages; + + return this; + } + }, + + /** + The MessageFormat object to use. + + @type {MessageFormat} + @default null + */ + messageFormat: { + serializable: false, + value: null + }, + + _messages: { + enumerable: false, + 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) { + // != ok checking for undefined as well + if (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: { + enumerable: false, + 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) { + this._locale = value; + this.messageFormat = new MessageFormat(value); + } + } + }, + + _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 + */ + availableLocales: { + get: function() { + if (this._availableLocales) { + return this._availableLocales; + } + + return this._availableLocales = this._manifest.get("files").get(LOCALES_DIRECTORY).get("files").then(function(locales) { + return Object.keys(locales); + }); + } + }, + + _require: { + value: (typeof global !== "undefined") ? global.require : (typeof window !== "undefined") ? window.require : null + }, + /** + 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) { + return this.__manifest; + } else { + return this.__manifest = messageRequire.async(MANIFEST_FILENAME); + } + } 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" + )); + } + } + }, + + /** + 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 (timeout === null) { + timeout = 5000; + } + this.messages = null; + + var self = this; + var messageRequire = this.require; + var promise = this._manifest; + + if (timeout) { + promise = promise.timeout(timeout); + } + + return this.messagesPromise = promise.get("files").then(function(files) { + return self._loadMessageFiles(files); + + }).then(function(localesMessages) { + return self._collapseMessages(localesMessages); + + }).fail(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; + + }); + } + }, + + /** + Load the locale appropriate message files from the given manifest + structure. + @private + @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}. + */ + _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. + @private + @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"} + */ + _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, len = localesMessages.length; i < len; i++) { + var localeMessages = localesMessages[i]; + for (var key in localeMessages) { + 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 + + messageformat.js. 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 { + compiled = (new Function('MessageFormat', 'return ' + this.messageFormat.precompile(ast))(MessageFormat)); + } + + 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.

+ +

+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:

+

+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) { + var listener, deferred, promise, self = this; + defaultOnFail = (typeof defaultOnFail === "undefined") ? true : defaultOnFail; + + if (!this.messagesPromise) { + promise = Promise.resolve(this.localizeSync(key, defaultMessage)); + promise.then(callback); + return promise; + } + + 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 + return this.messagesPromise.then(l, l); + } else { + return this.messagesPromise.then(l); + } + } + } + +}); + +/** + @class module:montage/core/localizer.DefaultLocalizer + @extends module:montage/core/localizer.Localizer +*/ +var DefaultLocalizer = Montage.create(Localizer, /** @lends module:montage/core/localizer.DefaultLocalizer# */ { + init: { + value: function() { + var defaultLocale = this.callDelegateMethod("getDefaultLocale"); + + if (!defaultLocale && typeof window !== "undefined") { + if (window.localStorage) { + defaultLocale = window.localStorage.getItem(LOCALE_STORAGE_KEY); + } + defaultLocale = defaultLocale || window.navigator.userLanguage || window.navigator.language; + } + + defaultLocale = defaultLocale || "en"; + this.locale = defaultLocale; + + this.loadMessages().done(); + + return this; + } + }, + + _delegate: { + enumerable: false, + value: null + }, + /** + Delegate to get the default locale. + + Should implement a getDefaultLocale method that returns + a language-tag string that can be passed to {@link locale} + @type Object + @default null + */ + delegate: { + get: function() { + return this._delegate; + }, + set: function(value) { + if (this._delegate !== value) { + this._delegate = value; + this.init(); + } + } + }, + + locale: { + get: function() { + return this._locale; + }, + set: function(value) { + try { + Object.getPropertyDescriptor(Localizer, "locale").set.call(this, value); + } catch (e) { + value = "en"; + Object.getPropertyDescriptor(Localizer, "locale").set.call(this, value); + } + + // If possible, save locale + if (typeof window !== "undefined" && window.localStorage) { + window.localStorage.setItem(LOCALE_STORAGE_KEY, value); + } + } + }, + + /** + Reset the saved locale back to default by using the steps above. + @function + @returns {String} the reset locale + */ + reset: { + value: function() { + if (typeof window !== "undefined" && window.localStorage) { + window.localStorage.removeItem(LOCALE_STORAGE_KEY); + } + this.init(); + return this._locale; + } + } +}); + +/** + The default localizer. + +

The locale of the defaultLocalizer is determined by following these steps:

+ +
    +
  1. If localStorage exists, use the value stored in "montage_locale" (LOCALE_STORAGE_KEY)
  2. +
  3. Otherwise use the value of navigator.userLanguage (Internet Explorer)
  4. +
  5. Otherwise use the value of navigator.language (other browsers)
  6. +
  7. Otherwise fall back to "en"
  8. +
+ +

defaultLocalizer.locale can be set and if localStorage exists then the value will be saved in + "montage_locale" (LOCALE_STORAGE_KEY).

+ + @type {module:montage/core/localizer.DefaultLocalizer} + @static +*/ +var defaultLocalizer = exports.defaultLocalizer = DefaultLocalizer.create().init(); + +/** + The localize function from {@link defaultLocalizer} provided for convenience. + + @function + @see module:montage/core/localizer.Localizer#localize +*/ +exports.localize = defaultLocalizer.localize.bind(defaultLocalizer); + +/** + Stores data needed for {@link Message}. + + When any of the properties in this object are set using setProperty (i.e. + through a binding) a change event will be dispatched. + + Really this is a proxy, and can be replaced by one once they become + readily available. + + @class module:montage/core/localizer.MessageData + @extends module:montage/core/core.Montage + @private +*/ +var MessageData = Montage.create(Montage, /** @lends module:montage/core/localizer.MessageVariables# */{ + /** + Initialize the object. + + @function + @param {Object} data Object with own properties to set on this object. + */ + init: { + enumerable: false, + value: function(data) { + for (var p in data) { + // adding this binding will call setProperty below which will + // add the PropertyChangeListener + Object.defineBinding(this, p, { + boundObject: data, + boundObjectPropertyPath: p, + oneway: false + }); + } + + return this; + } + }, + + /** + Watch any properties set through bindings for changes. + + @function + @private + */ + setProperty: { + enumerable: false, + value: function(path, value) { + // The same listener will only be registered once. It's faster to + // just register again than it would be to check for the existance + // of an existing listener. + this.addPropertyChangeListener(path, this); + + Object.setProperty.call(this, path, value); + } + }, + + handleChange: { + enumerable: false, + value: function(event) { + this.dispatchEventNamed("change", true, false); + } + } +}); +/** + Tracks a message function and its data for changes in order to generate a + localized message. + + @class module:montage/core/localizer.MessageLocalizer + @extends module:montage/core/core.Montage +*/ +var Message = exports.Message = Montage.create(Montage, /** @lends module:montage/core/localizer.MessageLocalizer# */ { + + didCreate: { + value: function() { + this._data = MessageData.create(); + this._data.addEventListener("change", this, false); + } + }, + + /** + Initialize the object. + + @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: defaultLocalizer + }, + localizer: { + get: function() { + 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(); + } + }, + + _isLocalizeQueued: { + value: false + }, + _localize: { + value: function() { + // Optimization: only run this function in the next tick. So that + // if the key, defaultKey and data are set individually we don't + // try and localize each time. + if (this._isLocalizeQueued) return; + this._isLocalizeQueued = true; + + var self = this; + // Set up a new promise now, so anyone accessing it in this tick + // won't get the old one. + var temp = Promise.defer(); + this._messageFunction = temp.promise; + + // 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 fn(self._data); + }); + + Promise.nextTick(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 + // repetitions get set up both the key and default message + // are null/undefined. By rejecting the developer would + // get an error whenever they use localization with a + // repetition. + // Instead we show a less severe "warning", so the issue + // is still surfaced + console.warn("Both key and default message are falsey for", + self, "If this is in a repetition this warning can be ignored"); + temp.resolve(EMPTY_STRING_FUNCTION); + return; + } + // Replace the _messageFunction promise with the real one. + temp.resolve(self._localizer.localizeSync( + self._key, + self._defaultMessage + )); + }); + } + }, + + _messageFunction: { + value: Promise.resolve(EMPTY_STRING_FUNCTION) + }, + + /** + The data needed for the message. Properties on this object can be + bound to. + + This object will be wrapped in a MessageData object to watch all + properties for changes so that the localized message can be updated. + + @type {MessageData} + @default null + */ + _data: { + value: null + }, + data: { + get: function() { + return this._data; + }, + set: function(value) { + if (this._data === value) { + return; + } + if (this._data) { + this._data.removeEventListener("change", this); + } + this._data = MessageData.create().init(value); + this._data.addEventListener("change", this, false); + this.handleChange(); + } + }, + + // TODO: Remove when possible to bind to promises + __localizedResolved: { + value: "" + }, + + _localizedDeferred: { + value: Promise.defer() + }, + /** + The message localized with all variables replaced. + @type {string} + @default "" + */ + localized: { + get: function() { + return this._localizedDeferred.promise; + }, + 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. + var deferred = Promise.defer(); + this._localizedDeferred.resolve(deferred.promise); + value.then(deferred.resolve, deferred.reject); + + // TODO: Remove when possible to bind to promises + deferred.promise.then(function (message) { + return self.__localizedResolved = message; + }).done(); + + this._localizedDeferred = deferred; + } + }, + + setProperty: { + enumerable: false, + value: function(path, value) { + // If a binding to a data property has been set directly on this + // object, instead of on the data object, install a listener for + // the data object + if (path.indexOf("data.") === 0) { + this._data.addPropertyChangeListener(path.substring(5), this._data); + } + Object.setProperty.call(this, path, value); + } + }, + + /** + * Whenever there is a change set the localized property. + * @type {Function} + * @private + */ + handleChange: { + value: function(event) { + this.localized = this._messageFunction.fcall(this._data); + } + }, + + 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 !== defaultLocalizer) { + result.localizer = this._localizer; + } + + return result; + } + }, + + serializeForLocalizations: { + value: function(serializer) { + var result = {}; + + var bindings = this._bindingDescriptors; + + if (bindings && bindings.key) { + result[KEY_KEY] = bindings.key; + } else { + result[KEY_KEY] = this._key; + } + + if (bindings && bindings.defaultMessage) { + result[DEFAULT_MESSAGE_KEY] = bindings.defaultMessage; + } else { + result[DEFAULT_MESSAGE_KEY] = this._defaultMessage; + } + + var dataBindings = this._data._bindingDescriptors; + + // 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. + for (var p in this._data) { + if (this._data.hasOwnProperty(p) && + (!dataBindings || !dataBindings[p]) + ) { + if (!result.data) result.data = {}; + result.data[p] = this._data[p]; + } + } + + // Loop through bindings seperately in case the bound properties + // haven't been set on the data object yet. + for (var b in dataBindings) { + if (!result.data) result.data = {}; + result.data[b] = dataBindings[b]; + } + + return result; + } + } + +}); + +var createMessageBinding = function(object, prop, key, defaultMessage, data, deserializer) { + var message = Message.create(); + + for (var d in data) { + if (typeof data[d] === "string") { + message.data[d] = data[d]; + } else { + deserializeBindingToBindingDescriptor(data[d], deserializer); + Object.defineBinding(message.data, d, data[d]); + } + } + + if (typeof key === "object") { + deserializeBindingToBindingDescriptor(key, deserializer); + Object.defineBinding(message, "key", key); + } else { + message.key = key; + } + + if (typeof defaultMessage === "object") { + deserializeBindingToBindingDescriptor(defaultMessage, deserializer); + Object.defineBinding(message, "defaultMessage", defaultMessage); + } else { + message.defaultMessage = defaultMessage; + } + + Object.defineBinding(object, prop, { + boundObject: message, + // TODO: Remove when possible to bind to promises and replace with + // binding to "localized" + boundObjectPropertyPath: "__localizedResolved", + oneway: true, + serializable: false + }); +}; + +Serializer.defineSerializationUnit("localizations", function(object) { + var bindingDescriptors = object._bindingDescriptors; + + if (bindingDescriptors) { + var result; + for (var prop in bindingDescriptors) { + var desc = bindingDescriptors[prop]; + if (Message.isPrototypeOf(desc.boundObject)) { + if (!result) { + result = {}; + } + var message = desc.boundObject; + result[prop] = message.serializeForLocalizations(); + } + } + return result; + } +}); + +Deserializer.defineDeserializationUnit("localizations", function(object, properties, deserializer) { + for (var prop in properties) { + var desc = properties[prop], + key, + defaultMessage; + + 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); + } +}); diff --git a/core/messageformat-locale.js b/core/messageformat-locale.js new file mode 100644 index 0000000000..0dfeacaef9 --- /dev/null +++ b/core/messageformat-locale.js @@ -0,0 +1,491 @@ +exports.am = function(n) { + if (n === 0 || n == 1) { + return 'one'; + } + return 'other'; +}; +exports.ar = function(n) { + if (n === 0) { + return 'zero'; + } + if (n == 1) { + return 'one'; + } + if (n == 2) { + return 'two'; + } + if ((n % 100) >= 3 && (n % 100) <= 10 && n == Math.floor(n)) { + return 'few'; + } + if ((n % 100) >= 11 && (n % 100) <= 99 && n == Math.floor(n)) { + return 'many'; + } + return 'other'; +}; +exports.bg = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.bn = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.br = function (n) { + if (n === 0) { + return 'zero'; + } + if (n == 1) { + return 'one'; + } + if (n == 2) { + return 'two'; + } + if (n == 3) { + return 'few'; + } + if (n == 6) { + return 'many'; + } + return 'other'; +}; +exports.ca = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.cs = function (n) { + if (n == 1) { + return 'one'; + } + if (n == 2 || n == 3 || n == 4) { + return 'few'; + } + return 'other'; +}; +exports.cy = function (n) { + if (n === 0) { + return 'zero'; + } + if (n == 1) { + return 'one'; + } + if (n == 2) { + return 'two'; + } + if (n == 3) { + return 'few'; + } + if (n == 6) { + return 'many'; + } + return 'other'; +}; +exports.da = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.de = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.el = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.en = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.es = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.et = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.eu = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.fa = function ( n ) { + return "other"; +}; +exports.fi = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.fil = function(n) { + if (n === 0 || n == 1) { + return 'one'; + } + return 'other'; +}; +exports.fr = function (n) { + if (n >= 0 && n < 2) { + return 'one'; + } + return 'other'; +}; +exports.ga = function (n) { + if (n == 1) { + return 'one'; + } + if (n == 2) { + return 'two'; + } + return 'other'; +}; +exports.gl = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.gsw = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.gu = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.he = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.hi = function(n) { + if (n === 0 || n == 1) { + return 'one'; + } + return 'other'; +}; +exports.hr = function (n) { + if ((n % 10) == 1 && (n % 100) != 11) { + return 'one'; + } + if ((n % 10) >= 2 && (n % 10) <= 4 && + ((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) { + return 'few'; + } + if ((n % 10) === 0 || ((n % 10) >= 5 && (n % 10) <= 9) || + ((n % 100) >= 11 && (n % 100) <= 14) && n == Math.floor(n)) { + return 'many'; + } + return 'other'; +}; +exports.hu = function(n) { + return 'other'; +}; +exports.id = function(n) { + return 'other'; +}; +exports["in"] = function(n) { + return 'other'; +}; +exports.is = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.it = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.iw = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.ja = function ( n ) { + return "other"; +}; +exports.kn = function ( n ) { + return "other"; +}; +exports.ko = function ( n ) { + return "other"; +}; +exports.lag = function (n) { + if (n === 0) { + return 'zero'; + } + if (n > 0 && n < 2) { + return 'one'; + } + return 'other'; +}; +exports.ln = function(n) { + if (n === 0 || n == 1) { + return 'one'; + } + return 'other'; +}; +exports.lt = function (n) { + if ((n % 10) == 1 && ((n % 100) < 11 || (n % 100) > 19)) { + return 'one'; + } + if ((n % 10) >= 2 && (n % 10) <= 9 && + ((n % 100) < 11 || (n % 100) > 19) && n == Math.floor(n)) { + return 'few'; + } + return 'other'; +}; +exports.lv = function (n) { + if (n === 0) { + return 'zero'; + } + if ((n % 10) == 1 && (n % 100) != 11) { + return 'one'; + } + return 'other'; +}; +exports.mk = function (n) { + if ((n % 10) == 1 && n != 11) { + return 'one'; + } + return 'other'; +}; +exports.ml = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.mo = function (n) { + if (n == 1) { + return 'one'; + } + if (n === 0 || n != 1 && (n % 100) >= 1 && + (n % 100) <= 19 && n == Math.floor(n)) { + return 'few'; + } + return 'other'; +}; +exports.mr = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.ms = function ( n ) { + return "other"; +}; +exports.mt = function (n) { + if (n == 1) { + return 'one'; + } + if (n === 0 || ((n % 100) >= 2 && (n % 100) <= 4 && n == Math.floor(n))) { + return 'few'; + } + if ((n % 100) >= 11 && (n % 100) <= 19 && n == Math.floor(n)) { + return 'many'; + } + return 'other'; +}; +exports.nl = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.no = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.or = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.pl = function (n) { + if (n == 1) { + return 'one'; + } + if ((n % 10) >= 2 && (n % 10) <= 4 && + ((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) { + return 'few'; + } + if ((n % 10) === 0 || n != 1 && (n % 10) == 1 || + ((n % 10) >= 5 && (n % 10) <= 9 || (n % 100) >= 12 && (n % 100) <= 14) && + n == Math.floor(n)) { + return 'many'; + } + return 'other'; +}; +exports.pt = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.ro = function (n) { + if (n == 1) { + return 'one'; + } + if (n === 0 || n != 1 && (n % 100) >= 1 && + (n % 100) <= 19 && n == Math.floor(n)) { + return 'few'; + } + return 'other'; +}; +exports.ru = function (n) { + if ((n % 10) == 1 && (n % 100) != 11) { + return 'one'; + } + if ((n % 10) >= 2 && (n % 10) <= 4 && + ((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) { + return 'few'; + } + if ((n % 10) === 0 || ((n % 10) >= 5 && (n % 10) <= 9) || + ((n % 100) >= 11 && (n % 100) <= 14) && n == Math.floor(n)) { + return 'many'; + } + return 'other'; +}; +exports.shi = function(n) { + if (n >= 0 && n <= 1) { + return 'one'; + } + if (n >= 2 && n <= 10 && n == Math.floor(n)) { + return 'few'; + } + return 'other'; +}; +exports.sk = function (n) { + if (n == 1) { + return 'one'; + } + if (n == 2 || n == 3 || n == 4) { + return 'few'; + } + return 'other'; +}; +exports.sl = function (n) { + if ((n % 100) == 1) { + return 'one'; + } + if ((n % 100) == 2) { + return 'two'; + } + if ((n % 100) == 3 || (n % 100) == 4) { + return 'few'; + } + return 'other'; +}; +exports.sq = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.sr = function (n) { + if ((n % 10) == 1 && (n % 100) != 11) { + return 'one'; + } + if ((n % 10) >= 2 && (n % 10) <= 4 && + ((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) { + return 'few'; + } + if ((n % 10) === 0 || ((n % 10) >= 5 && (n % 10) <= 9) || + ((n % 100) >= 11 && (n % 100) <= 14) && n == Math.floor(n)) { + return 'many'; + } + return 'other'; +}; +exports.sv = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.sw = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.ta = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.te = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.th = function ( n ) { + return "other"; +}; +exports.tl = function(n) { + if (n === 0 || n == 1) { + return 'one'; + } + return 'other'; +}; +exports.tr = function(n) { + return 'other'; +}; +exports.uk = function (n) { + if ((n % 10) == 1 && (n % 100) != 11) { + return 'one'; + } + if ((n % 10) >= 2 && (n % 10) <= 4 && + ((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) { + return 'few'; + } + if ((n % 10) === 0 || ((n % 10) >= 5 && (n % 10) <= 9) || + ((n % 100) >= 11 && (n % 100) <= 14) && n == Math.floor(n)) { + return 'many'; + } + return 'other'; +}; +exports.ur = function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; +}; +exports.vi = function ( n ) { + return "other"; +}; +exports.zh = function ( n ) { + return "other"; +}; diff --git a/core/messageformat.js b/core/messageformat.js new file mode 100644 index 0000000000..f22f9b7417 --- /dev/null +++ b/core/messageformat.js @@ -0,0 +1,1911 @@ +/** + * messageformat.js + * + * ICU PluralFormat + SelectFormat for JavaScript + * + * @author Alex Sexton - @SlexAxton + * @version 0.1.5 + * @license WTFPL + * @contributor_license Dojo CLA +*/ +(function ( root ) { + + // Create the contructor function + function MessageFormat ( locale, pluralFunc ) { + var fallbackLocale; + + if ( locale && pluralFunc ) { + MessageFormat.locale[ locale ] = pluralFunc; + } + + // Defaults + fallbackLocale = locale = locale || "en"; + pluralFunc = pluralFunc || MessageFormat.locale[ fallbackLocale = MessageFormat.Utils.getFallbackLocale( locale ) ]; + + if ( ! pluralFunc ) { + throw new Error( "Plural Function not found for locale: " + locale ); + } + + // Own Properties + this.pluralFunc = pluralFunc; + this.locale = locale; + this.fallbackLocale = fallbackLocale; + } + + // Set up the locales object. Add in english by default + MessageFormat.locale = { + "en" : function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; + } + }; + + // Build out our basic SafeString type + // more or less stolen from Handlebars by @wycats + MessageFormat.SafeString = function( string ) { + this.string = string; + }; + + MessageFormat.SafeString.prototype.toString = function () { + return this.string.toString(); + }; + + MessageFormat.Utils = { + numSub : function ( string, key, depth ) { + // make sure that it's not an escaped octothorpe + return string.replace( /^#|[^\\]#/g, function (m) { + var prefix = m && m.length === 2 ? m.charAt(0) : ''; + return prefix + '" + (function(){ var x = ' + + key+';\nif( isNaN(x) ){\nthrow new Error("MessageFormat: `"+lastkey_'+depth+'+"` isnt a number.");\n}\nreturn x;\n})() + "'; + }); + }, + escapeExpression : function (string) { + var escape = { + "\n": "\\n", + "\"": '\\"' + }, + badChars = /[\n"]/g, + possible = /[\n"]/, + escapeChar = function(chr) { + return escape[chr] || "&"; + }; + + // Don't escape SafeStrings, since they're already safe + if ( string instanceof MessageFormat.SafeString ) { + return string.toString(); + } + else if ( string === null || string === false ) { + return ""; + } + + if ( ! possible.test( string ) ) { + return string; + } + return string.replace( badChars, escapeChar ); + }, + getFallbackLocale: function( locale ) { + var tagSeparator = locale.indexOf("-") >= 0 ? "-" : "_"; + + // Lets just be friends, fallback through the language tags + while ( ! MessageFormat.locale.hasOwnProperty( locale ) ) { + locale = locale.substring(0, locale.lastIndexOf( tagSeparator )); + if (locale.length === 0) { + return null; + } + } + + return locale; + } + }; + + // This is generated and pulled in for browsers. + var mparser = (function(){ + /* Generated by PEG.js 0.6.2 (http://pegjs.majda.cz/). */ + + var result = { + /* + * Parses the input with a generated parser. If the parsing is successfull, + * returns a value explicitly or implicitly specified by the grammar from + * which the parser was generated (see |PEG.buildParser|). If the parsing is + * unsuccessful, throws |PEG.parser.SyntaxError| describing the error. + */ + parse: function(input, startRule) { + var parseFunctions = { + "_": parse__, + "char": parse_char, + "chars": parse_chars, + "digits": parse_digits, + "elementFormat": parse_elementFormat, + "hexDigit": parse_hexDigit, + "id": parse_id, + "messageFormatElement": parse_messageFormatElement, + "messageFormatPattern": parse_messageFormatPattern, + "messageFormatPatternRight": parse_messageFormatPatternRight, + "offsetPattern": parse_offsetPattern, + "pluralFormatPattern": parse_pluralFormatPattern, + "pluralForms": parse_pluralForms, + "pluralStyle": parse_pluralStyle, + "selectFormatPattern": parse_selectFormatPattern, + "selectStyle": parse_selectStyle, + "start": parse_start, + "string": parse_string, + "stringKey": parse_stringKey, + "whitespace": parse_whitespace + }; + + if (startRule !== undefined) { + if (parseFunctions[startRule] === undefined) { + throw new Error("Invalid rule name: " + quote(startRule) + "."); + } + } else { + startRule = "start"; + } + + var pos = 0; + var reportMatchFailures = true; + var rightmostMatchFailuresPos = 0; + var rightmostMatchFailuresExpected = []; + var cache = {}; + + function padLeft(input, padding, length) { + var result = input; + + var padLength = length - input.length; + for (var i = 0; i < padLength; i++) { + result = padding + result; + } + + return result; + } + + function escape(ch) { + var charCode = ch.charCodeAt(0); + + if (charCode <= 0xFF) { + var escapeChar = 'x'; + var length = 2; + } else { + var escapeChar = 'u'; + var length = 4; + } + + return '\\' + escapeChar + padLeft(charCode.toString(16).toUpperCase(), '0', length); + } + + function quote(s) { + /* + * ECMA-262, 5th ed., 7.8.4: All characters may appear literally in a + * string literal except for the closing quote character, backslash, + * carriage return, line separator, paragraph separator, and line feed. + * Any character may appear in the form of an escape sequence. + */ + return '"' + s + .replace(/\\/g, '\\\\') // backslash + .replace(/"/g, '\\"') // closing quote character + .replace(/\r/g, '\\r') // carriage return + .replace(/\n/g, '\\n') // line feed + .replace(/[\x80-\uFFFF]/g, escape) // non-ASCII characters + + '"'; + } + + function matchFailed(failure) { + if (pos < rightmostMatchFailuresPos) { + return; + } + + if (pos > rightmostMatchFailuresPos) { + rightmostMatchFailuresPos = pos; + rightmostMatchFailuresExpected = []; + } + + rightmostMatchFailuresExpected.push(failure); + } + + function parse_start() { + var cacheKey = 'start@' + pos; + var cachedResult = cache[cacheKey]; + if (cachedResult) { + pos = cachedResult.nextPos; + return cachedResult.result; + } + + + var savedPos0 = pos; + var result1 = parse_messageFormatPattern(); + var result2 = result1 !== null + ? (function(messageFormatPattern) { return { type: "program", program: messageFormatPattern }; })(result1) + : null; + if (result2 !== null) { + var result0 = result2; + } else { + var result0 = null; + pos = savedPos0; + } + + + + cache[cacheKey] = { + nextPos: pos, + result: result0 + }; + return result0; + } + + function parse_messageFormatPattern() { + var cacheKey = 'messageFormatPattern@' + pos; + var cachedResult = cache[cacheKey]; + if (cachedResult) { + pos = cachedResult.nextPos; + return cachedResult.result; + } + + + var savedPos0 = pos; + var savedPos1 = pos; + var result3 = parse_string(); + if (result3 !== null) { + var result4 = []; + var result5 = parse_messageFormatPatternRight(); + while (result5 !== null) { + result4.push(result5); + var result5 = parse_messageFormatPatternRight(); + } + if (result4 !== null) { + var result1 = [result3, result4]; + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + var result2 = result1 !== null + ? (function(s1, inner) { + var st = []; + if ( s1 && s1.val ) { + st.push( s1 ); + } + for( var i in inner ){ + if ( inner.hasOwnProperty( i ) ) { + st.push( inner[ i ] ); + } + } + return { type: 'messageFormatPattern', statements: st }; + })(result1[0], result1[1]) + : null; + if (result2 !== null) { + var result0 = result2; + } else { + var result0 = null; + pos = savedPos0; + } + + + + cache[cacheKey] = { + nextPos: pos, + result: result0 + }; + return result0; + } + + function parse_messageFormatPatternRight() { + var cacheKey = 'messageFormatPatternRight@' + pos; + var cachedResult = cache[cacheKey]; + if (cachedResult) { + pos = cachedResult.nextPos; + return cachedResult.result; + } + + + var savedPos0 = pos; + var savedPos1 = pos; + if (input.substr(pos, 1) === "{") { + var result3 = "{"; + pos += 1; + } else { + var result3 = null; + if (reportMatchFailures) { + matchFailed("\"{\""); + } + } + if (result3 !== null) { + var result4 = parse__(); + if (result4 !== null) { + var result5 = parse_messageFormatElement(); + if (result5 !== null) { + var result6 = parse__(); + if (result6 !== null) { + if (input.substr(pos, 1) === "}") { + var result7 = "}"; + pos += 1; + } else { + var result7 = null; + if (reportMatchFailures) { + matchFailed("\"}\""); + } + } + if (result7 !== null) { + var result8 = parse_string(); + if (result8 !== null) { + var result1 = [result3, result4, result5, result6, result7, result8]; + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + var result2 = result1 !== null + ? (function(mfe, s1) { + var res = []; + if ( mfe ) { + res.push(mfe); + } + if ( s1 && s1.val ) { + res.push( s1 ); + } + return { type: "messageFormatPatternRight", statements : res }; + })(result1[2], result1[5]) + : null; + if (result2 !== null) { + var result0 = result2; + } else { + var result0 = null; + pos = savedPos0; + } + + + + cache[cacheKey] = { + nextPos: pos, + result: result0 + }; + return result0; + } + + function parse_messageFormatElement() { + var cacheKey = 'messageFormatElement@' + pos; + var cachedResult = cache[cacheKey]; + if (cachedResult) { + pos = cachedResult.nextPos; + return cachedResult.result; + } + + + var savedPos0 = pos; + var savedPos1 = pos; + var result3 = parse_id(); + if (result3 !== null) { + var savedPos2 = pos; + if (input.substr(pos, 1) === ",") { + var result6 = ","; + pos += 1; + } else { + var result6 = null; + if (reportMatchFailures) { + matchFailed("\",\""); + } + } + if (result6 !== null) { + var result7 = parse_elementFormat(); + if (result7 !== null) { + var result5 = [result6, result7]; + } else { + var result5 = null; + pos = savedPos2; + } + } else { + var result5 = null; + pos = savedPos2; + } + var result4 = result5 !== null ? result5 : ''; + if (result4 !== null) { + var result1 = [result3, result4]; + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + var result2 = result1 !== null + ? (function(argIdx, efmt) { + var res = { + type: "messageFormatElement", + argumentIndex: argIdx + }; + if ( efmt && efmt.length ) { + res.elementFormat = efmt[1]; + } + else { + res.output = true; + } + return res; + })(result1[0], result1[1]) + : null; + if (result2 !== null) { + var result0 = result2; + } else { + var result0 = null; + pos = savedPos0; + } + + + + cache[cacheKey] = { + nextPos: pos, + result: result0 + }; + return result0; + } + + function parse_elementFormat() { + var cacheKey = 'elementFormat@' + pos; + var cachedResult = cache[cacheKey]; + if (cachedResult) { + pos = cachedResult.nextPos; + return cachedResult.result; + } + + + var savedPos2 = pos; + var savedPos3 = pos; + var result14 = parse__(); + if (result14 !== null) { + if (input.substr(pos, 6) === "plural") { + var result15 = "plural"; + pos += 6; + } else { + var result15 = null; + if (reportMatchFailures) { + matchFailed("\"plural\""); + } + } + if (result15 !== null) { + var result16 = parse__(); + if (result16 !== null) { + if (input.substr(pos, 1) === ",") { + var result17 = ","; + pos += 1; + } else { + var result17 = null; + if (reportMatchFailures) { + matchFailed("\",\""); + } + } + if (result17 !== null) { + var result18 = parse__(); + if (result18 !== null) { + var result19 = parse_pluralStyle(); + if (result19 !== null) { + var result20 = parse__(); + if (result20 !== null) { + var result12 = [result14, result15, result16, result17, result18, result19, result20]; + } else { + var result12 = null; + pos = savedPos3; + } + } else { + var result12 = null; + pos = savedPos3; + } + } else { + var result12 = null; + pos = savedPos3; + } + } else { + var result12 = null; + pos = savedPos3; + } + } else { + var result12 = null; + pos = savedPos3; + } + } else { + var result12 = null; + pos = savedPos3; + } + } else { + var result12 = null; + pos = savedPos3; + } + var result13 = result12 !== null + ? (function(t, s) { + return { + type : "elementFormat", + key : t, + val : s.val + }; + })(result12[1], result12[5]) + : null; + if (result13 !== null) { + var result11 = result13; + } else { + var result11 = null; + pos = savedPos2; + } + if (result11 !== null) { + var result0 = result11; + } else { + var savedPos0 = pos; + var savedPos1 = pos; + var result4 = parse__(); + if (result4 !== null) { + if (input.substr(pos, 6) === "select") { + var result5 = "select"; + pos += 6; + } else { + var result5 = null; + if (reportMatchFailures) { + matchFailed("\"select\""); + } + } + if (result5 !== null) { + var result6 = parse__(); + if (result6 !== null) { + if (input.substr(pos, 1) === ",") { + var result7 = ","; + pos += 1; + } else { + var result7 = null; + if (reportMatchFailures) { + matchFailed("\",\""); + } + } + if (result7 !== null) { + var result8 = parse__(); + if (result8 !== null) { + var result9 = parse_selectStyle(); + if (result9 !== null) { + var result10 = parse__(); + if (result10 !== null) { + var result2 = [result4, result5, result6, result7, result8, result9, result10]; + } else { + var result2 = null; + pos = savedPos1; + } + } else { + var result2 = null; + pos = savedPos1; + } + } else { + var result2 = null; + pos = savedPos1; + } + } else { + var result2 = null; + pos = savedPos1; + } + } else { + var result2 = null; + pos = savedPos1; + } + } else { + var result2 = null; + pos = savedPos1; + } + } else { + var result2 = null; + pos = savedPos1; + } + var result3 = result2 !== null + ? (function(t, s) { + return { + type : "elementFormat", + key : t, + val : s.val + }; + })(result2[1], result2[5]) + : null; + if (result3 !== null) { + var result1 = result3; + } else { + var result1 = null; + pos = savedPos0; + } + if (result1 !== null) { + var result0 = result1; + } else { + var result0 = null;; + }; + } + + + + cache[cacheKey] = { + nextPos: pos, + result: result0 + }; + return result0; + } + + function parse_pluralStyle() { + var cacheKey = 'pluralStyle@' + pos; + var cachedResult = cache[cacheKey]; + if (cachedResult) { + pos = cachedResult.nextPos; + return cachedResult.result; + } + + + var savedPos0 = pos; + var result1 = parse_pluralFormatPattern(); + var result2 = result1 !== null + ? (function(pfp) { + return { type: "pluralStyle", val: pfp }; + })(result1) + : null; + if (result2 !== null) { + var result0 = result2; + } else { + var result0 = null; + pos = savedPos0; + } + + + + cache[cacheKey] = { + nextPos: pos, + result: result0 + }; + return result0; + } + + function parse_selectStyle() { + var cacheKey = 'selectStyle@' + pos; + var cachedResult = cache[cacheKey]; + if (cachedResult) { + pos = cachedResult.nextPos; + return cachedResult.result; + } + + + var savedPos0 = pos; + var result1 = parse_selectFormatPattern(); + var result2 = result1 !== null + ? (function(sfp) { + return { type: "selectStyle", val: sfp }; + })(result1) + : null; + if (result2 !== null) { + var result0 = result2; + } else { + var result0 = null; + pos = savedPos0; + } + + + + cache[cacheKey] = { + nextPos: pos, + result: result0 + }; + return result0; + } + + function parse_pluralFormatPattern() { + var cacheKey = 'pluralFormatPattern@' + pos; + var cachedResult = cache[cacheKey]; + if (cachedResult) { + pos = cachedResult.nextPos; + return cachedResult.result; + } + + + var savedPos0 = pos; + var savedPos1 = pos; + var result6 = parse_offsetPattern(); + var result3 = result6 !== null ? result6 : ''; + if (result3 !== null) { + var result4 = []; + var result5 = parse_pluralForms(); + while (result5 !== null) { + result4.push(result5); + var result5 = parse_pluralForms(); + } + if (result4 !== null) { + var result1 = [result3, result4]; + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + var result2 = result1 !== null + ? (function(op, pf) { + var res = { + type: "pluralFormatPattern", + pluralForms: pf + }; + if ( op ) { + res.offset = op; + } + else { + res.offset = 0; + } + return res; + })(result1[0], result1[1]) + : null; + if (result2 !== null) { + var result0 = result2; + } else { + var result0 = null; + pos = savedPos0; + } + + + + cache[cacheKey] = { + nextPos: pos, + result: result0 + }; + return result0; + } + + function parse_offsetPattern() { + var cacheKey = 'offsetPattern@' + pos; + var cachedResult = cache[cacheKey]; + if (cachedResult) { + pos = cachedResult.nextPos; + return cachedResult.result; + } + + + var savedPos0 = pos; + var savedPos1 = pos; + var result3 = parse__(); + if (result3 !== null) { + if (input.substr(pos, 6) === "offset") { + var result4 = "offset"; + pos += 6; + } else { + var result4 = null; + if (reportMatchFailures) { + matchFailed("\"offset\""); + } + } + if (result4 !== null) { + var result5 = parse__(); + if (result5 !== null) { + if (input.substr(pos, 1) === ":") { + var result6 = ":"; + pos += 1; + } else { + var result6 = null; + if (reportMatchFailures) { + matchFailed("\":\""); + } + } + if (result6 !== null) { + var result7 = parse__(); + if (result7 !== null) { + var result8 = parse_digits(); + if (result8 !== null) { + var result9 = parse__(); + if (result9 !== null) { + var result1 = [result3, result4, result5, result6, result7, result8, result9]; + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + var result2 = result1 !== null + ? (function(d) { + return d; + })(result1[5]) + : null; + if (result2 !== null) { + var result0 = result2; + } else { + var result0 = null; + pos = savedPos0; + } + + + + cache[cacheKey] = { + nextPos: pos, + result: result0 + }; + return result0; + } + + function parse_selectFormatPattern() { + var cacheKey = 'selectFormatPattern@' + pos; + var cachedResult = cache[cacheKey]; + if (cachedResult) { + pos = cachedResult.nextPos; + return cachedResult.result; + } + + + var savedPos0 = pos; + var result1 = []; + var result3 = parse_pluralForms(); + while (result3 !== null) { + result1.push(result3); + var result3 = parse_pluralForms(); + } + var result2 = result1 !== null + ? (function(pf) { + return { + type: "selectFormatPattern", + pluralForms: pf + }; + })(result1) + : null; + if (result2 !== null) { + var result0 = result2; + } else { + var result0 = null; + pos = savedPos0; + } + + + + cache[cacheKey] = { + nextPos: pos, + result: result0 + }; + return result0; + } + + function parse_pluralForms() { + var cacheKey = 'pluralForms@' + pos; + var cachedResult = cache[cacheKey]; + if (cachedResult) { + pos = cachedResult.nextPos; + return cachedResult.result; + } + + + var savedPos0 = pos; + var savedPos1 = pos; + var result3 = parse__(); + if (result3 !== null) { + var result4 = parse_stringKey(); + if (result4 !== null) { + var result5 = parse__(); + if (result5 !== null) { + if (input.substr(pos, 1) === "{") { + var result6 = "{"; + pos += 1; + } else { + var result6 = null; + if (reportMatchFailures) { + matchFailed("\"{\""); + } + } + if (result6 !== null) { + var result7 = parse__(); + if (result7 !== null) { + var result8 = parse_messageFormatPattern(); + if (result8 !== null) { + var result9 = parse__(); + if (result9 !== null) { + if (input.substr(pos, 1) === "}") { + var result10 = "}"; + pos += 1; + } else { + var result10 = null; + if (reportMatchFailures) { + matchFailed("\"}\""); + } + } + if (result10 !== null) { + var result1 = [result3, result4, result5, result6, result7, result8, result9, result10]; + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + var result2 = result1 !== null + ? (function(k, mfp) { + return { + type: "pluralForms", + key: k, + val: mfp + }; + })(result1[1], result1[5]) + : null; + if (result2 !== null) { + var result0 = result2; + } else { + var result0 = null; + pos = savedPos0; + } + + + + cache[cacheKey] = { + nextPos: pos, + result: result0 + }; + return result0; + } + + function parse_stringKey() { + var cacheKey = 'stringKey@' + pos; + var cachedResult = cache[cacheKey]; + if (cachedResult) { + pos = cachedResult.nextPos; + return cachedResult.result; + } + + + var savedPos2 = pos; + var result7 = parse_id(); + var result8 = result7 !== null + ? (function(i) { + return i; + })(result7) + : null; + if (result8 !== null) { + var result6 = result8; + } else { + var result6 = null; + pos = savedPos2; + } + if (result6 !== null) { + var result0 = result6; + } else { + var savedPos0 = pos; + var savedPos1 = pos; + if (input.substr(pos, 1) === "=") { + var result4 = "="; + pos += 1; + } else { + var result4 = null; + if (reportMatchFailures) { + matchFailed("\"=\""); + } + } + if (result4 !== null) { + var result5 = parse_digits(); + if (result5 !== null) { + var result2 = [result4, result5]; + } else { + var result2 = null; + pos = savedPos1; + } + } else { + var result2 = null; + pos = savedPos1; + } + var result3 = result2 !== null + ? (function(d) { + return d; + })(result2[1]) + : null; + if (result3 !== null) { + var result1 = result3; + } else { + var result1 = null; + pos = savedPos0; + } + if (result1 !== null) { + var result0 = result1; + } else { + var result0 = null;; + }; + } + + + + cache[cacheKey] = { + nextPos: pos, + result: result0 + }; + return result0; + } + + function parse_string() { + var cacheKey = 'string@' + pos; + var cachedResult = cache[cacheKey]; + if (cachedResult) { + pos = cachedResult.nextPos; + return cachedResult.result; + } + + + var savedPos0 = pos; + var savedPos1 = pos; + var result3 = parse__(); + if (result3 !== null) { + var result4 = []; + var savedPos2 = pos; + var result6 = parse__(); + if (result6 !== null) { + var result7 = parse_chars(); + if (result7 !== null) { + var result8 = parse__(); + if (result8 !== null) { + var result5 = [result6, result7, result8]; + } else { + var result5 = null; + pos = savedPos2; + } + } else { + var result5 = null; + pos = savedPos2; + } + } else { + var result5 = null; + pos = savedPos2; + } + while (result5 !== null) { + result4.push(result5); + var savedPos2 = pos; + var result6 = parse__(); + if (result6 !== null) { + var result7 = parse_chars(); + if (result7 !== null) { + var result8 = parse__(); + if (result8 !== null) { + var result5 = [result6, result7, result8]; + } else { + var result5 = null; + pos = savedPos2; + } + } else { + var result5 = null; + pos = savedPos2; + } + } else { + var result5 = null; + pos = savedPos2; + } + } + if (result4 !== null) { + var result1 = [result3, result4]; + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + var result2 = result1 !== null + ? (function(ws, s) { + var tmp = []; + for( var i = 0; i < s.length; ++i ) { + for( var j = 0; j < s[ i ].length; ++j ) { + tmp.push(s[i][j]); + } + } + return { + type: "string", + val: ws + tmp.join('') + }; + })(result1[0], result1[1]) + : null; + if (result2 !== null) { + var result0 = result2; + } else { + var result0 = null; + pos = savedPos0; + } + + + + cache[cacheKey] = { + nextPos: pos, + result: result0 + }; + return result0; + } + + function parse_id() { + var cacheKey = 'id@' + pos; + var cachedResult = cache[cacheKey]; + if (cachedResult) { + pos = cachedResult.nextPos; + return cachedResult.result; + } + + + var savedPos0 = pos; + var savedPos1 = pos; + var result3 = parse__(); + if (result3 !== null) { + if (input.substr(pos).match(/^[a-zA-Z$_]/) !== null) { + var result4 = input.charAt(pos); + pos++; + } else { + var result4 = null; + if (reportMatchFailures) { + matchFailed("[a-zA-Z$_]"); + } + } + if (result4 !== null) { + var result5 = []; + if (input.substr(pos).match(/^[^ \n\r,.+={}]/) !== null) { + var result7 = input.charAt(pos); + pos++; + } else { + var result7 = null; + if (reportMatchFailures) { + matchFailed("[^ \\n\\r,.+={}]"); + } + } + while (result7 !== null) { + result5.push(result7); + if (input.substr(pos).match(/^[^ \n\r,.+={}]/) !== null) { + var result7 = input.charAt(pos); + pos++; + } else { + var result7 = null; + if (reportMatchFailures) { + matchFailed("[^ \\n\\r,.+={}]"); + } + } + } + if (result5 !== null) { + var result6 = parse__(); + if (result6 !== null) { + var result1 = [result3, result4, result5, result6]; + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + } else { + var result1 = null; + pos = savedPos1; + } + var result2 = result1 !== null + ? (function(s1, s2) { + return s1 + (s2 ? s2.join('') : ''); + })(result1[1], result1[2]) + : null; + if (result2 !== null) { + var result0 = result2; + } else { + var result0 = null; + pos = savedPos0; + } + + + + cache[cacheKey] = { + nextPos: pos, + result: result0 + }; + return result0; + } + + function parse_chars() { + var cacheKey = 'chars@' + pos; + var cachedResult = cache[cacheKey]; + if (cachedResult) { + pos = cachedResult.nextPos; + return cachedResult.result; + } + + + var savedPos0 = pos; + var result3 = parse_char(); + if (result3 !== null) { + var result1 = []; + while (result3 !== null) { + result1.push(result3); + var result3 = parse_char(); + } + } else { + var result1 = null; + } + var result2 = result1 !== null + ? (function(chars) { return chars.join(''); })(result1) + : null; + if (result2 !== null) { + var result0 = result2; + } else { + var result0 = null; + pos = savedPos0; + } + + + + cache[cacheKey] = { + nextPos: pos, + result: result0 + }; + return result0; + } + + function parse_char() { + var cacheKey = 'char@' + pos; + var cachedResult = cache[cacheKey]; + if (cachedResult) { + pos = cachedResult.nextPos; + return cachedResult.result; + } + + + var savedPos5 = pos; + if (input.substr(pos).match(/^[^{}\\\0- \n\r]/) !== null) { + var result19 = input.charAt(pos); + pos++; + } else { + var result19 = null; + if (reportMatchFailures) { + matchFailed("[^{}\\\\\\0- \\n\\r]"); + } + } + var result20 = result19 !== null + ? (function(x) { + return x; + })(result19) + : null; + if (result20 !== null) { + var result18 = result20; + } else { + var result18 = null; + pos = savedPos5; + } + if (result18 !== null) { + var result0 = result18; + } else { + var savedPos4 = pos; + if (input.substr(pos, 2) === "\\#") { + var result16 = "\\#"; + pos += 2; + } else { + var result16 = null; + if (reportMatchFailures) { + matchFailed("\"\\\\#\""); + } + } + var result17 = result16 !== null + ? (function() { + return "\\#"; + })() + : null; + if (result17 !== null) { + var result15 = result17; + } else { + var result15 = null; + pos = savedPos4; + } + if (result15 !== null) { + var result0 = result15; + } else { + var savedPos3 = pos; + if (input.substr(pos, 2) === "\\{") { + var result13 = "\\{"; + pos += 2; + } else { + var result13 = null; + if (reportMatchFailures) { + matchFailed("\"\\\\{\""); + } + } + var result14 = result13 !== null + ? (function() { + return "\u007B"; + })() + : null; + if (result14 !== null) { + var result12 = result14; + } else { + var result12 = null; + pos = savedPos3; + } + if (result12 !== null) { + var result0 = result12; + } else { + var savedPos2 = pos; + if (input.substr(pos, 2) === "\\}") { + var result10 = "\\}"; + pos += 2; + } else { + var result10 = null; + if (reportMatchFailures) { + matchFailed("\"\\\\}\""); + } + } + var result11 = result10 !== null + ? (function() { + return "\u007D"; + })() + : null; + if (result11 !== null) { + var result9 = result11; + } else { + var result9 = null; + pos = savedPos2; + } + if (result9 !== null) { + var result0 = result9; + } else { + var savedPos0 = pos; + var savedPos1 = pos; + if (input.substr(pos, 2) === "\\u") { + var result4 = "\\u"; + pos += 2; + } else { + var result4 = null; + if (reportMatchFailures) { + matchFailed("\"\\\\u\""); + } + } + if (result4 !== null) { + var result5 = parse_hexDigit(); + if (result5 !== null) { + var result6 = parse_hexDigit(); + if (result6 !== null) { + var result7 = parse_hexDigit(); + if (result7 !== null) { + var result8 = parse_hexDigit(); + if (result8 !== null) { + var result2 = [result4, result5, result6, result7, result8]; + } else { + var result2 = null; + pos = savedPos1; + } + } else { + var result2 = null; + pos = savedPos1; + } + } else { + var result2 = null; + pos = savedPos1; + } + } else { + var result2 = null; + pos = savedPos1; + } + } else { + var result2 = null; + pos = savedPos1; + } + var result3 = result2 !== null + ? (function(h1, h2, h3, h4) { + return String.fromCharCode(parseInt("0x" + h1 + h2 + h3 + h4)); + })(result2[1], result2[2], result2[3], result2[4]) + : null; + if (result3 !== null) { + var result1 = result3; + } else { + var result1 = null; + pos = savedPos0; + } + if (result1 !== null) { + var result0 = result1; + } else { + var result0 = null;; + }; + }; + }; + }; + } + + + + cache[cacheKey] = { + nextPos: pos, + result: result0 + }; + return result0; + } + + function parse_digits() { + var cacheKey = 'digits@' + pos; + var cachedResult = cache[cacheKey]; + if (cachedResult) { + pos = cachedResult.nextPos; + return cachedResult.result; + } + + + var savedPos0 = pos; + if (input.substr(pos).match(/^[0-9]/) !== null) { + var result3 = input.charAt(pos); + pos++; + } else { + var result3 = null; + if (reportMatchFailures) { + matchFailed("[0-9]"); + } + } + if (result3 !== null) { + var result1 = []; + while (result3 !== null) { + result1.push(result3); + if (input.substr(pos).match(/^[0-9]/) !== null) { + var result3 = input.charAt(pos); + pos++; + } else { + var result3 = null; + if (reportMatchFailures) { + matchFailed("[0-9]"); + } + } + } + } else { + var result1 = null; + } + var result2 = result1 !== null + ? (function(ds) { + return parseInt((ds.join('')), 10); + })(result1) + : null; + if (result2 !== null) { + var result0 = result2; + } else { + var result0 = null; + pos = savedPos0; + } + + + + cache[cacheKey] = { + nextPos: pos, + result: result0 + }; + return result0; + } + + function parse_hexDigit() { + var cacheKey = 'hexDigit@' + pos; + var cachedResult = cache[cacheKey]; + if (cachedResult) { + pos = cachedResult.nextPos; + return cachedResult.result; + } + + + if (input.substr(pos).match(/^[0-9a-fA-F]/) !== null) { + var result0 = input.charAt(pos); + pos++; + } else { + var result0 = null; + if (reportMatchFailures) { + matchFailed("[0-9a-fA-F]"); + } + } + + + + cache[cacheKey] = { + nextPos: pos, + result: result0 + }; + return result0; + } + + function parse__() { + var cacheKey = '_@' + pos; + var cachedResult = cache[cacheKey]; + if (cachedResult) { + pos = cachedResult.nextPos; + return cachedResult.result; + } + + var savedReportMatchFailures = reportMatchFailures; + reportMatchFailures = false; + var savedPos0 = pos; + var result1 = []; + var result3 = parse_whitespace(); + while (result3 !== null) { + result1.push(result3); + var result3 = parse_whitespace(); + } + var result2 = result1 !== null + ? (function(w) { return w.join(''); })(result1) + : null; + if (result2 !== null) { + var result0 = result2; + } else { + var result0 = null; + pos = savedPos0; + } + reportMatchFailures = savedReportMatchFailures; + if (reportMatchFailures && result0 === null) { + matchFailed("whitespace"); + } + + cache[cacheKey] = { + nextPos: pos, + result: result0 + }; + return result0; + } + + function parse_whitespace() { + var cacheKey = 'whitespace@' + pos; + var cachedResult = cache[cacheKey]; + if (cachedResult) { + pos = cachedResult.nextPos; + return cachedResult.result; + } + + + if (input.substr(pos).match(/^[ \n\r]/) !== null) { + var result0 = input.charAt(pos); + pos++; + } else { + var result0 = null; + if (reportMatchFailures) { + matchFailed("[ \\n\\r]"); + } + } + + + + cache[cacheKey] = { + nextPos: pos, + result: result0 + }; + return result0; + } + + function buildErrorMessage() { + function buildExpected(failuresExpected) { + failuresExpected.sort(); + + var lastFailure = null; + var failuresExpectedUnique = []; + for (var i = 0; i < failuresExpected.length; i++) { + if (failuresExpected[i] !== lastFailure) { + failuresExpectedUnique.push(failuresExpected[i]); + lastFailure = failuresExpected[i]; + } + } + + switch (failuresExpectedUnique.length) { + case 0: + return 'end of input'; + case 1: + return failuresExpectedUnique[0]; + default: + return failuresExpectedUnique.slice(0, failuresExpectedUnique.length - 1).join(', ') + + ' or ' + + failuresExpectedUnique[failuresExpectedUnique.length - 1]; + } + } + + var expected = buildExpected(rightmostMatchFailuresExpected); + var actualPos = Math.max(pos, rightmostMatchFailuresPos); + var actual = actualPos < input.length + ? quote(input.charAt(actualPos)) + : 'end of input'; + + return 'Expected ' + expected + ' but ' + actual + ' found.'; + } + + function computeErrorPosition() { + /* + * The first idea was to use |String.split| to break the input up to the + * error position along newlines and derive the line and column from + * there. However IE's |split| implementation is so broken that it was + * enough to prevent it. + */ + + var line = 1; + var column = 1; + var seenCR = false; + + for (var i = 0; i < rightmostMatchFailuresPos; i++) { + var ch = input.charAt(i); + if (ch === '\n') { + if (!seenCR) { line++; } + column = 1; + seenCR = false; + } else if (ch === '\r' | ch === '\u2028' || ch === '\u2029') { + line++; + column = 1; + seenCR = true; + } else { + column++; + seenCR = false; + } + } + + return { line: line, column: column }; + } + + + + var result = parseFunctions[startRule](); + + /* + * The parser is now in one of the following three states: + * + * 1. The parser successfully parsed the whole input. + * + * - |result !== null| + * - |pos === input.length| + * - |rightmostMatchFailuresExpected| may or may not contain something + * + * 2. The parser successfully parsed only a part of the input. + * + * - |result !== null| + * - |pos < input.length| + * - |rightmostMatchFailuresExpected| may or may not contain something + * + * 3. The parser did not successfully parse any part of the input. + * + * - |result === null| + * - |pos === 0| + * - |rightmostMatchFailuresExpected| contains at least one failure + * + * All code following this comment (including called functions) must + * handle these states. + */ + if (result === null || pos !== input.length) { + var errorPosition = computeErrorPosition(); + throw new this.SyntaxError( + buildErrorMessage(), + errorPosition.line, + errorPosition.column + ); + } + + return result; + }, + + /* Returns the parser source code. */ + toSource: function() { return this._source; } + }; + + /* Thrown when a parser encounters a syntax error. */ + + result.SyntaxError = function(message, line, column) { + this.name = 'SyntaxError'; + this.message = message; + this.line = line; + this.column = column; + }; + + result.SyntaxError.prototype = Error.prototype; + + return result; + })(); + + MessageFormat.prototype.parse = function () { + // Bind to itself so error handling works + return mparser.parse.apply( mparser, arguments ); + }; + + MessageFormat.prototype.precompile = function ( ast ) { + var self = this, + needOther = false, + fp = { + begin: 'function(d){\nvar r = "";\n', + end : "return r;\n}" + }; + + function interpMFP ( ast, data ) { + // Set some default data + data = data || {}; + var s = '', i, tmp, lastkeyname; + + switch ( ast.type ) { + case 'program': + return interpMFP( ast.program ); + case 'messageFormatPattern': + for ( i = 0; i < ast.statements.length; ++i ) { + s += interpMFP( ast.statements[i], data ); + } + return fp.begin + s + fp.end; + case 'messageFormatPatternRight': + for ( i = 0; i < ast.statements.length; ++i ) { + s += interpMFP( ast.statements[i], data ); + } + return s; + case 'messageFormatElement': + data.pf_count = data.pf_count || 0; + s += 'if(!d){\nthrow new Error("MessageFormat: No data passed to function.");\n}\n'; + if ( ast.output ) { + s += 'r += d["' + ast.argumentIndex + '"];\n'; + } + else { + lastkeyname = 'lastkey_'+(data.pf_count+1); + s += 'var '+lastkeyname+' = "'+ast.argumentIndex+'";\n'; + s += 'var k_'+(data.pf_count+1)+'=d['+lastkeyname+'];\n'; + s += interpMFP( ast.elementFormat, data ); + } + return s; + case 'elementFormat': + if ( ast.key === 'select' ) { + s += interpMFP( ast.val, data ); + s += 'r += (pf_' + + data.pf_count + + '[ k_' + (data.pf_count+1) + ' ] || pf_'+data.pf_count+'[ "other" ])( d );\n'; + } + else if ( ast.key === 'plural' ) { + s += interpMFP( ast.val, data ); + s += 'if ( pf_'+(data.pf_count)+'[ k_'+(data.pf_count+1)+' + "" ] ) {\n'; + s += 'r += pf_'+data.pf_count+'[ k_'+(data.pf_count+1)+' + "" ]( d ); \n'; + s += '}\nelse {\n'; + s += 'r += (pf_' + + data.pf_count + + '[ MessageFormat.locale["' + + self.fallbackLocale + + '"]( k_'+(data.pf_count+1)+' - off_'+(data.pf_count)+' ) ] || pf_'+data.pf_count+'[ "other" ] )( d );\n'; + s += '}\n'; + } + return s; + /* // Unreachable cases. + case 'pluralStyle': + case 'selectStyle':*/ + case 'pluralFormatPattern': + data.pf_count = data.pf_count || 0; + s += 'var off_'+data.pf_count+' = '+ast.offset+';\n'; + s += 'var pf_' + data.pf_count + ' = { \n'; + needOther = true; + // We're going to simultaneously check to make sure we hit the required 'other' option. + + for ( i = 0; i < ast.pluralForms.length; ++i ) { + if ( ast.pluralForms[ i ].key === 'other' ) { + needOther = false; + } + if ( tmp ) { + s += ',\n'; + } + else{ + tmp = 1; + } + s += '"' + ast.pluralForms[ i ].key + '" : ' + interpMFP( ast.pluralForms[ i ].val, + (function(){ var res = JSON.parse(JSON.stringify(data)); res.pf_count++; return res; })() ); + } + s += '\n};\n'; + if ( needOther ) { + throw new Error("No 'other' form found in pluralFormatPattern " + data.pf_count); + } + return s; + case 'selectFormatPattern': + + data.pf_count = data.pf_count || 0; + s += 'var off_'+data.pf_count+' = 0;\n'; + s += 'var pf_' + data.pf_count + ' = { \n'; + needOther = true; + + for ( i = 0; i < ast.pluralForms.length; ++i ) { + if ( ast.pluralForms[ i ].key === 'other' ) { + needOther = false; + } + if ( tmp ) { + s += ',\n'; + } + else{ + tmp = 1; + } + s += '"' + ast.pluralForms[ i ].key + '" : ' + interpMFP( ast.pluralForms[ i ].val, + (function(){ + var res = JSON.parse( JSON.stringify( data ) ); + res.pf_count++; + return res; + })() + ); + } + s += '\n};\n'; + if ( needOther ) { + throw new Error("No 'other' form found in selectFormatPattern " + data.pf_count); + } + return s; + /* // Unreachable + case 'pluralForms': + */ + case 'string': + return 'r += "' + MessageFormat.Utils.numSub( + MessageFormat.Utils.escapeExpression( ast.val ), + 'k_' + data.pf_count + ' - off_' + ( data.pf_count - 1 ), + data.pf_count + ) + '";\n'; + default: + throw new Error( 'Bad AST type: ' + ast.type ); + } + } + return interpMFP( ast ); + }; + + MessageFormat.prototype.compile = function ( message ) { + return (new Function( 'MessageFormat', + 'return ' + + this.precompile( + this.parse( message ) + ) + ))(MessageFormat); + }; + + + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = MessageFormat; + } + exports.MessageFormat = MessageFormat; + } + else if (typeof define === 'function' && define.amd) { + define(function() { + return MessageFormat; + }); + } + else { + root['MessageFormat'] = MessageFormat; + } + +})( this ); diff --git a/test/core/localizer-spec.js b/test/core/localizer-spec.js new file mode 100644 index 0000000000..8cb57c5fee --- /dev/null +++ b/test/core/localizer-spec.js @@ -0,0 +1,390 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ +/*global require,exports,describe,beforeEach,it,expect,waits,waitsFor,runs,spyOn */ +var Montage = require("montage").Montage, + Localizer = require("montage/core/localizer"), + Promise = require("montage/core/promise").Promise, + Deserializer = require("montage/core/deserializer").Deserializer; + +describe("core/localizer-spec", function() { + + describe("Message", function() { + var message; + beforeEach(function() { + message = Localizer.Message.create(); + }); + + it("has an init method that accepts key and default", function() { + message = Localizer.Message.create().init("hello", "Hello"); + return message.localized.then(function (localized) { + expect(localized).toBe("Hello"); + }); + }); + + it("has an init method that accepts a key, default and data", function() { + var object = { + name: "World" + }; + message = Localizer.Message.create().init("hello", "Hello, {name}", object); + return message.localized.then(function (localized) { + expect(localized).toBe("Hello, World"); + }); + + }); + + it("sets the localized property to the default message", function() { + message.key = "Hello"; + return message.localized.then(function (localized) { + expect(localized).toBe("Hello"); + }); + }); + + it("localizes the messages when message binding update", function() { + var def = { + key: "Hello, {name}" + }; + + message.data = { + name: "World" + }; + + Object.defineBinding(message, "key", { + boundObject: def, + boundObjectPropertyPath: "key" + }); + + return message.localized.then(function (localized) { + expect(localized).toBe("Hello, World"); + def.key = "Goodbye, {name}"; + + return message.localized; + }).then(function (localized) { + expect(localized).toBe("Goodbye, World"); + }); + }); + + it("localizes the messages when data bindings update", function() { + message.key = "Hello, {name}"; + + var object = { + name: "before" + }; + + Object.defineBinding(message, "data.name", { + boundObject: object, + boundObjectPropertyPath: "name" + }); + + return message.localized.then(function (localized) { + expect(localized).toBe("Hello, before"); + object.name = "after"; + + return message.localized; + }).then(function (localized) { + expect(localized).toBe("Hello, after"); + message.data.name = "later"; + + return message.localized; + }).then(function (localized) { + expect(localized).toBe("Hello, later"); + expect(object.name).toBe("later"); + }); + }); + + it("localizes the messages when other data bindings update", function() { + message.key = "Hello, {name}"; + + var otherObject = { + name: "before" + }; + + var object = {}; + + Object.defineBinding(object, "name", { + boundObject: otherObject, + boundObjectPropertyPath: "name" + }); + + Object.defineBinding(message, "data.name", { + boundObject: object, + boundObjectPropertyPath: "name" + }); + + return message.localized.then(function (localized) { + expect(localized).toBe("Hello, before"); + otherObject.name = "after"; + + return message.localized; + }).then(function (localized) { + expect(localized).toBe("Hello, after"); + }); + }); + + it("automatically localizes the messages when data property updates", function() { + message.key = "Hello, {name}"; + + message.data = { + name: "before" + }; + + return message.localized.then(function (localized) { + expect(localized).toBe("Hello, before"); + message.data.name = "after"; + + return message.localized; + }).then(function (localized) { + expect(localized).toBe("Hello, after"); + }); + }); + }); + + describe("Localizer", function(){ + var l; + beforeEach(function() { + l = Localizer.Localizer.create().init("en"); + }); + + it("can be created with a foreign language code", function() { + var l = Localizer.Localizer.create().init("no"); + expect(l.messageFormat).not.toBe(null); + }); + + describe("locale", function() { + it("can't be set to an invalid tag", function() { + var threw = false; + try { + l.locale = "123-en-US"; + } catch (e) { + threw = true; + } + expect(l.locale).not.toBe("123-en-US"); + expect(threw).toBe(true); + }); + }); + + describe("messages", function() { + it("can't be set to a non-object", function() { + var threw = false; + try { + l.messages = "hello"; + } catch (e) { threw = true; } + expect(l.messages).not.toBe("hello"); + expect(threw).toBe(true); + }); + it("can be set to an object", function() { + var input = {"hello": "ahoy!"}; + l.messages = input; + + expect(l.messages).toBe(input); + }); + }); + + describe("localize", function() { + beforeEach(function() { + l.messages = { + "hello_name": "Hei {name}!", + "hello_name_function": function(d){ + var r = ""; + r += "Hei "; + if(!d){ + throw new Error("MessageFormat: No data passed to function."); + } + r += d["name"]; + r += "!"; + return r; + }, + "love you": {"message": "Jeg elsker deg"}, + "wrong object": {"string": "nope"} + }; + }); + + it("returns a function with toString for simple messages", function() { + var x = l.localizeSync("love you"); + expect(x()).toBe("Jeg elsker deg"); + expect("" + x).toBe("Jeg elsker deg"); + }); + it("returns a function if it takes variables", function() { + var fn = l.localizeSync("hello_name"); + expect(typeof fn).toBe("function"); + expect(fn({name: "Ingrid"})).toBe("Hei Ingrid!"); + }); + it("caches the compiled functions", function() { + var fn = l.localizeSync("hello_name"); + var fn2 = l.localizeSync("hello_name"); + expect(fn).toBe(fn2); + }); + it("returns precompiled functions", function() { + var fn = l.localizeSync("hello_name_function"); + expect(typeof fn).toBe("function"); + expect(fn({name: "Ingrid"})).toBe("Hei Ingrid!"); + }); + it("uses the default if the key does not exist", function() { + expect(l.localizeSync("missing", "Missing key")()).toBe("Missing key"); + }); + it("returns the key if the key does not exist and no fallback is given", function() { + expect(l.localizeSync("missing")()).toBe("missing"); + }); + it("throws if the message object does not contain a 'message' property", function() { + var threw = false; + try { + l.localizeSync("wrong object"); + } catch (e) { + threw = true; + } + expect(threw).toBe(true); + }); + }); + + describe("loadMessages", function() { + it("fails when package.json has no manifest", function() { + return require.loadPackage(module.directory + "localizer/no-package-manifest/", {}).then(function(r){ + l.require = r; + return l.loadMessages(); + }).then(function(messages) { + return Promise.reject("expected messages not to load but got " + JSON.stringify(messages)); + }, function(err) { + return void 0; + }); + }); + it("fails when package has no manifest.json", function() { + return require.loadPackage(module.directory + "localizer/no-manifest/", {}).then(function(r){ + l.require = r; + return l.loadMessages(); + }).then(function(messages) { + return Promise.reject("expected messages not to load but got " + JSON.stringify(messages)); + }, function(err) { + return void 0; + }); + }); + it("fails when package has no manifest.json", function() { + return require.loadPackage(module.directory + "localizer/no-manifest-files/", {}).then(function(r){ + l.require = r; + return l.loadMessages(); + }).then(function(messages) { + return Promise.reject("expected messages not to load but got " + JSON.stringify(messages)); + }, function(err) { + return void 0; + }); + }); + + it("can load a simple messages.json (promise)", function() { + return require.loadPackage(module.directory + "localizer/simple/", {}).then(function(r){ + l.require = r; + return l.loadMessages(); + }).then(function(messages) { + expect(messages.hello).toBe("Hello, World!"); + }); + }); + + it("can load a simple messages.json (callback)", function() { + var deferred = Promise.defer(); + require.loadPackage(module.directory + "localizer/simple/", {}).then(function(r){ + l.require = r; + l.loadMessages(null, function(messages) { + expect(messages.hello).toBe("Hello, World!"); + deferred.resolve(); + }); + }); + return deferred.promise; + }); + + it("has a timeout", function() { + return require.loadPackage(module.directory + "localizer/simple/", {}).then(function(r){ + l.require = r; + return l.loadMessages(1); + }).then(function() { + return Promise.reject("expected a timeout"); + }, function(err) { + return void 0; + }); + }); + + it("loads non-English messages", function() { + var l = Localizer.Localizer.create().init("no"); + return require.loadPackage(module.directory + "localizer/fallback/", {}).then(function(r){ + l.require = r; + return l.loadMessages(); + }).then(function(messages) { + expect(messages.hello).toBe("Hei"); + }); + + }); + + it("loads the fallback messages", function() { + var l = Localizer.Localizer.create().init("no-x-compiled"); + return require.loadPackage(module.directory + "localizer/fallback/", {}).then(function(r){ + l.require = r; + return l.loadMessages(); + }).then(function(messages) { + expect(messages.hello).toBe("Hei"); + expect(typeof messages.welcome).toBe("function"); + var num_albums = l.localizeSync("num_albums"); + expect(num_albums({albums: 1})).toBe("1 fotoalbum"); + expect(num_albums({albums: 4})).toBe("4 fotoalbuma"); + }); + }); + }); + }); + + describe("defaultLocalizer", function() { + beforeEach(function() { + Localizer.defaultLocalizer.reset(); + }); + + describe("locale", function() { + it("defaults to navigator.language", function() { + expect(Localizer.defaultLocalizer.locale).toBe(window.navigator.language); + }); + it("saves the value to local storage", function() { + Localizer.defaultLocalizer.locale = "en-x-test"; + expect(Localizer.defaultLocalizer.locale).toBe("en-x-test"); + expect(window.localStorage.getItem("montage_locale")).toBe("en-x-test"); + }); + }); + + describe("delegate", function() { + it("is called to determine the default locale to use", function() { + var delegate = { + getDefaultLocale: function() { + return "en-x-delegate"; + } + }; + + spyOn(delegate, 'getDefaultLocale').andCallThrough(); + + Localizer.defaultLocalizer.delegate = delegate; + + expect(delegate.getDefaultLocale).toHaveBeenCalled(); + expect(Localizer.defaultLocalizer.locale).toBe("en-x-delegate"); + }); + }); + }); +}); diff --git a/test/core/localizer/fallback/fallback.html b/test/core/localizer/fallback/fallback.html new file mode 100644 index 0000000000..836006124a --- /dev/null +++ b/test/core/localizer/fallback/fallback.html @@ -0,0 +1,160 @@ + + + + + Localization test + + + + +
fail
+
fail
+
fail
+ + +
fail
+ + +

Numbers:

+
+
+
+ + + diff --git a/test/core/localizer/fallback/locale/en/messages.json b/test/core/localizer/fallback/locale/en/messages.json new file mode 100644 index 0000000000..b0ecc1048a --- /dev/null +++ b/test/core/localizer/fallback/locale/en/messages.json @@ -0,0 +1,19 @@ +{ + "hello": "Hello", + "num_albums": { + "message": "{albums, plural, one {1 album} other {# albums}}", + "description": "how many photo albums the user has" + }, + "welcome": { + "message": "Welcome to the site, {name}", + "description": "Message when the user signs in", + "variable_examples": { + "name": "Joe Bloggs" + } + }, + "photo_deleted": "{photo_name} was deleted", + + "horse": "{num, plural, one {1 horse} other {# horses}}", + "cow": "{num, plural, one {1 cow} other {# cows}}", + "sheep": "{num} sheep" +} diff --git a/test/core/localizer/fallback/locale/no-x-compiled/messages.js b/test/core/localizer/fallback/locale/no-x-compiled/messages.js new file mode 100644 index 0000000000..20eecab1c5 --- /dev/null +++ b/test/core/localizer/fallback/locale/no-x-compiled/messages.js @@ -0,0 +1,54 @@ +var MessageFormat = {locale: require("montage/core/messageformat-locale")}; +exports.num_albums = function(d){ +var r = ""; +if(!d){ +throw new Error("MessageFormat: No data passed to function."); +} +var lastkey_1 = "albums"; +var k_1=d[lastkey_1]; +var off_0 = 0; +var pf_0 = { +"one" : function(d){ +var r = ""; +r += "1 fotoalbum"; +return r; +}, +"other" : function(d){ +var r = ""; +r += "" + (function(){ var x = k_1 - off_0; +if( isNaN(x) ){ +throw new Error("MessageFormat: `"+lastkey_1+"` isnt a number."); +} +return x; +})() + " fotoalbuma"; +return r; +} +}; +if ( pf_0[ k_1 + "" ] ) { +r += pf_0[ k_1 + "" ]( d ); +} +else { +r += (pf_0[ MessageFormat.locale["no"]( k_1 - off_0 ) ] || pf_0[ "other" ] )( d ); +} +return r; +}; + +exports.welcome = function(d){ +var r = ""; +r += "Velkommen til nettstedet, "; +if(!d){ +throw new Error("MessageFormat: No data passed to function."); +} +r += d["name"]; +return r; +}; + +exports.photo_deleted = function(d){ +var r = ""; +if(!d){ +throw new Error("MessageFormat: No data passed to function."); +} +r += d["photo_name"]; +r += " ble slettet"; +return r; +}; diff --git a/test/core/localizer/fallback/locale/no/messages.json b/test/core/localizer/fallback/locale/no/messages.json new file mode 100644 index 0000000000..5310138a94 --- /dev/null +++ b/test/core/localizer/fallback/locale/no/messages.json @@ -0,0 +1,19 @@ +{ + "hello": "Hei", + "num_albums": { + "message": "{albums, plural, one {1 fotoalbum} other {# fotoalbuma}}", + "description": "how many photo albums the user has" + }, + "welcome": { + "message": "Velkommen til nettstedet, {name}", + "description": "Message when the user signs in", + "variable_examples": { + "name": "Joe Bloggs" + } + }, + "photo_deleted": "{photo_name} ble slettet", + + "horse": "{num, plural, one {1 hest} other {# hester}}", + "cow": "{num, plural, one {1 ku} other {# kyr}}", + "sheep": "{num, plural, one {1 sau} other {# sauer}}" +} diff --git a/test/core/localizer/fallback/manifest.json b/test/core/localizer/fallback/manifest.json new file mode 100644 index 0000000000..11c1dcd9de --- /dev/null +++ b/test/core/localizer/fallback/manifest.json @@ -0,0 +1,28 @@ +{ + "montage_manifest_version": 1, + "files": { + "locale": { + "directory": true, + "files": { + "en": { + "directory": true, + "files": { + "messages.json": null + } + }, + "no-x-compiled": { + "directory": true, + "files": { + "messages.js": null + } + }, + "no": { + "directory": true, + "files": { + "messages.json": null + } + } + } + } + } +} \ No newline at end of file diff --git a/test/core/localizer/fallback/package.json b/test/core/localizer/fallback/package.json new file mode 100644 index 0000000000..26abdd2c0b --- /dev/null +++ b/test/core/localizer/fallback/package.json @@ -0,0 +1,8 @@ +{ + "name": "simple-localization-test", + "mappings": { + "montage": "../../../../" + }, + "manifest": true +} + diff --git a/test/core/localizer/manifest.json b/test/core/localizer/manifest.json new file mode 100644 index 0000000000..be15637a04 --- /dev/null +++ b/test/core/localizer/manifest.json @@ -0,0 +1,28 @@ +{ + "montage_manifest_version": 1, + "files": { + "locale": { + "directory": true, + "files": { + "en": { + "directory": true, + "files": { + "messages.json": null + } + }, + "en-x-compiled": { + "directory": true, + "files": { + "messages.js": null + } + }, + "no": { + "directory": true, + "files": { + "messages.json": null + } + } + } + } + } +} \ No newline at end of file diff --git a/test/core/localizer/no-manifest-files/hello b/test/core/localizer/no-manifest-files/hello new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/core/localizer/no-manifest-files/manifest.json b/test/core/localizer/no-manifest-files/manifest.json new file mode 100644 index 0000000000..d50506c68c --- /dev/null +++ b/test/core/localizer/no-manifest-files/manifest.json @@ -0,0 +1,3 @@ +{ + "hello": null +} \ No newline at end of file diff --git a/test/core/localizer/no-manifest-files/package.json b/test/core/localizer/no-manifest-files/package.json new file mode 100644 index 0000000000..3da180bfd9 --- /dev/null +++ b/test/core/localizer/no-manifest-files/package.json @@ -0,0 +1,8 @@ +{ + "name": "simple-localization-test", + "mappings": { + "montage": "../../../" + }, + "manifest": true +} + diff --git a/test/core/localizer/no-manifest/package.json b/test/core/localizer/no-manifest/package.json new file mode 100644 index 0000000000..3da180bfd9 --- /dev/null +++ b/test/core/localizer/no-manifest/package.json @@ -0,0 +1,8 @@ +{ + "name": "simple-localization-test", + "mappings": { + "montage": "../../../" + }, + "manifest": true +} + diff --git a/test/core/localizer/no-package-manifest/package.json b/test/core/localizer/no-package-manifest/package.json new file mode 100644 index 0000000000..9e1f38a507 --- /dev/null +++ b/test/core/localizer/no-package-manifest/package.json @@ -0,0 +1,7 @@ +{ + "name": "simple-localization-test", + "mappings": { + "montage": "../../../" + } +} + diff --git a/test/core/localizer/serialization-spec.js b/test/core/localizer/serialization-spec.js new file mode 100644 index 0000000000..e471b97613 --- /dev/null +++ b/test/core/localizer/serialization-spec.js @@ -0,0 +1,288 @@ +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ +/*global require,exports,describe,beforeEach,it,expect,waits,waitsFor,runs */ +var Montage = require("montage").Montage, + Localizer = require("montage/core/localizer"), + Promise = require("montage/core/promise").Promise, + Serializer = require("montage/core/serializer").Serializer, + Deserializer = require("montage/core/deserializer").Deserializer, + TestPageLoader = require("support/testpageloader").TestPageLoader; + +var stripPP = function stripPrettyPrintting(str) { + return str.replace(/\n\s*/g, ""); +}; + +var testPage = TestPageLoader.queueTest("fallback", {directory: module.directory}, function() { + var test = testPage.test; + + function testDeserializer(object, callback) { + var deserializer = Deserializer.create(), + objects, latch; + + deserializer._require = require; + deserializer.initWithObject(object).deserialize(function(objs) { + latch = true; + objects = objs; + }); + + waitsFor(function() { return latch; }); + runs(function() { + callback(objects); + }); + } + + function testSerializer(object, callback) { + var serializer = Serializer.create().initWithRequire(require), + objects; + + testDeserializer(object, function(o) { + objects = o; + waits(10); // wait for messages to be resolved + runs(function() { + var serialization = serializer.serializeObject(objects.target); + callback(stripPP(serialization)); + }); + }); + } + + describe("core/localizer/serialization-spec", function() { + describe("Message", function() { + it("localizes the message", function() { + return test.message.localized.then(function (localized) { + expect(localized).toBe("Welcome to the site, World"); + }); + }); + + it("does not serialize the default localizer", function() { + testSerializer({ + target: { + prototype: "montage/core/localizer[Message]", + properties: { + key: "hello" + } + } + }, function(serialization) { + expect(serialization).not.toContain("localizer"); + }); + }); + + it("serializes an non-default localizer", function() { + testSerializer({ + localizer: { + prototype: "montage/core/localizer", + properties: { + locale: "en-x-test" + } + }, + target: { + prototype: "montage/core/localizer[Message]", + properties: { + key: "hello", + localizer: {"@": "localizer"} + } + } + }, function(serialization) { + expect(serialization).toBe('{"localizer":{"prototype":"montage/core/localizer","properties":{"locale":"en-x-test"}},"root":{"value":{"key":"hello","localizer":{"@":"localizer"}}}}'); + }); + }); + }); + + describe("localizations unit", function() { + + it("requires a key", function() { + expect(test.missingKey.value).toBe("Pass"); + }); + + it("localizes a string", function() { + expect(test.basic.value).toBe("Pass."); + }); + + it("localizes a string and uses available resources", function() { + expect(test.resources.value).toBe("Hello"); + }); + + it("creates a binding from the localizer to the object", function() { + expect(test.binding.value).toBe("Hello World"); + expect(test.binding._bindingDescriptors.value).toBeDefined(); + test.bindingInput.value = "Earth"; + waitsFor(function() { return test.binding.value !== "Hello World"; }); + runs(function() { + expect(test.binding.value).toBe("Hello Earth"); + }); + }); + + it("can localize two properties", function() { + expect(test.twoProperties.unpressedLabel).toBe("Off"); + expect(test.twoProperties.pressedLabel).toBe("On"); + }); + + it("accepts a binding for the default message", function() { + testDeserializer({ + source: { + value: {value: "Hello, {name}"} + }, + target: { + prototype: "montage", + localizations: { + "value": { + "key": "", // key is required + "default": {"<-": "@source.value"}, + "data": { + "name": "someone" + } + } + } + } + }, function(objects) { + waitsFor(function() { return objects.target.value !== ""; }); + runs(function() { + expect(objects.target.value).toBe("Hello, someone"); + objects.source.value = "Goodbye, {name}"; + }); + waitsFor(function() { return objects.target.value !== "Hello, someone"; }); + runs(function() { + expect(objects.target.value).toBe("Goodbye, someone"); + }); + }); + }); + + describe("serializer", function() { + var objects, + serializer; + + beforeEach(function() { + testDeserializer({ + source: { + value: {x: "Hello, {name}"} + }, + target: { + prototype: "montage", + localizations: { + "binding": { + "key": "", // key is required + "default": {"<-": "@source.value"}, + "data": { + "name": "someone" + } + }, + "message": { + "key": "", // key is required + "default": "Hello", + } + } + } + }, function(o) { + objects = o; + }); + + serializer = Serializer.create().initWithRequire(require); + }); + + it("doesn't create a localizations block when there are none", function() { + testSerializer({ + source: { + value: {value: "Hello", identifier: "source"} + }, + target: { + prototype: "montage", + bindings: { + "test": {"<-": "@source.value"} + } + } + }, function(serialization) { + expect(serialization).not.toContain("localizations"); + }); + }); + + it("serializes simple localization strings", function() { + testSerializer({ + target: { + prototype: "montage", + localizations: { + "message": { + "key": "hello", // key is required + "default": "Hello" + } + } + } + }, function(serialization) { + expect(serialization).toBe('{"root":{"prototype":"montage/core/core[Montage]","properties":{},"localizations":{"message":{"key":"hello","default":"Hello"}}}}'); + }); + }); + + it("serializes default message binding", function() { + testSerializer({ + source: { + value: {value: "Hello, {name}", identifier: "source"} + }, + target: { + prototype: "montage", + localizations: { + "binding": { + "key": "", // key is required + "default": {"<-": "@source.value"}, + "data": { + "name": "someone" + } + } + } + } + }, function(serialization) { + expect(serialization).toBe('{"root":{"prototype":"montage/core/core[Montage]","properties":{},"localizations":{"binding":{"key":"","default":{"<-":"@source.value"},"data":{"name":"someone"}}}},"source":{}}'); + }); + }); + + it("serializes data binding", function() { + testSerializer({ + source: { + value: {value: "World", identifier: "source"} + }, + target: { + prototype: "montage", + localizations: { + "binding": { + "key": "", // key is required + "default": "Hello, {name}", + "data": { + "name": {"<-": "@source.value"} + } + } + } + } + }, function(serialization) { + expect(serialization).toBe('{"root":{"prototype":"montage/core/core[Montage]","properties":{},"localizations":{"binding":{"key":"","default":"Hello, {name}","data":{"name":{"<-":"@source.value"}}}}},"source":{}}'); + }); + }); + }); + + }); + }); +}); diff --git a/test/core/localizer/simple/locale/en/messages.json b/test/core/localizer/simple/locale/en/messages.json new file mode 100644 index 0000000000..399b7b78b9 --- /dev/null +++ b/test/core/localizer/simple/locale/en/messages.json @@ -0,0 +1,3 @@ +{ + "hello": "Hello, World!" +} \ No newline at end of file diff --git a/test/core/localizer/simple/manifest.json b/test/core/localizer/simple/manifest.json new file mode 100644 index 0000000000..6217bc1518 --- /dev/null +++ b/test/core/localizer/simple/manifest.json @@ -0,0 +1,15 @@ +{ + "files": { + "locale": { + "directory": true, + "files": { + "en": { + "directory": true, + "files": { + "messages.json": null + } + } + } + } + } +} \ No newline at end of file diff --git a/test/core/localizer/simple/package.json b/test/core/localizer/simple/package.json new file mode 100644 index 0000000000..3da180bfd9 --- /dev/null +++ b/test/core/localizer/simple/package.json @@ -0,0 +1,8 @@ +{ + "name": "simple-localization-test", + "mappings": { + "montage": "../../../" + }, + "manifest": true +} + diff --git a/test/run.js b/test/run.js index c88c082438..e58e77d52a 100644 --- a/test/run.js +++ b/test/run.js @@ -68,6 +68,8 @@ if (spec) { "controllers/array-controller-spec", "core/core-spec", + "core/localizer-spec", + "core/localizer/serialization-spec", "core/selector-spec", "core/extras/function", diff --git a/test/support/testpageloader.js b/test/support/testpageloader.js index 2a187b5d9c..592980fa19 100644 --- a/test/support/testpageloader.js +++ b/test/support/testpageloader.js @@ -63,7 +63,7 @@ var TestPageLoader = exports.TestPageLoader = Montage.create(Montage, { } options.testName = testName; // FIXME Hack to get current directory - options.directory = this.queueTest.caller.caller.arguments[2].directory; + options.directory = options.directory || this.queueTest.caller.caller.arguments[2].directory; testPage.testQueue.push(options); return testPage; } diff --git a/tools/generate-manifest.js b/tools/generate-manifest.js new file mode 100755 index 0000000000..64d744cacd --- /dev/null +++ b/tools/generate-manifest.js @@ -0,0 +1,78 @@ +#!/usr/bin/env node +/* +Copyright (c) 2012, Motorola Mobility LLC. +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of Motorola Mobility LLC nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + */ + +var fs = require("fs"), + path = require("path"); + +var manifest = {files: {}}; + +function usage() { + console.log("Usage: generate-manifest [ directory... ]"); +} + +function main(dirPath, fileNames, files) { + for (var i = 0, len = fileNames.length; i < len; i++) { + var name = fileNames[i]; + if (name.lastIndexOf("/") === name.length - 1) { + name = name.substr(0, name.length - 1); + } + var filePath = path.join(dirPath, name); + + var stats = fs.statSync(filePath); + if (stats.isDirectory()) { + var newFiles = files[name] = { + directory: true, + files: {} + }; + + main(filePath, fs.readdirSync(filePath), newFiles.files); + } else { + files[name] = null; + } + } + + return files; +} + +var argv = process.argv; +if (argv.length > 2 && argv[2] === "--help") { + usage(); +} else { + var directories = argv.slice(2); + if (directories.length === 0) { + directories = fs.readdirSync("."); + } + + main("", directories, manifest.files); + fs.writeFileSync("manifest.json", JSON.stringify(manifest), "utf8"); + console.log("Wrote manfiest.json"); +} \ No newline at end of file diff --git a/tools/import-messageformat-locales.sh b/tools/import-messageformat-locales.sh new file mode 100755 index 0000000000..26133dbb32 --- /dev/null +++ b/tools/import-messageformat-locales.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# +# This file contains proprietary software owned by Motorola Mobility, Inc.
+# No rights, expressed or implied, whatsoever to this software are provided by Motorola Mobility, Inc. hereunder.
+# (c) Copyright 2012 Motorola Mobility, Inc. All Rights Reserved. +#
+ +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +MESSAGE_FORMAT_REPO="git://github.com/SlexAxton/messageformat.js.git" +MESSAGE_FORMAT_DIR="messageformat.js" +LOCALE_FILENAME="messageformat-locale.js" +DESTINATION="$DIR/../core" + +# remove left over directory if it exists +rm -rf $MESSAGE_FORMAT_DIR + +git clone "$MESSAGE_FORMAT_REPO" "$MESSAGE_FORMAT_DIR" +# put all the locale plural functions together +cat "$MESSAGE_FORMAT_DIR"/locale/* > "$LOCALE_FILENAME" +# make into a CommonJS module +sed -i "" "s/MessageFormat.locale/exports/g" "$LOCALE_FILENAME" +# move to the correct place +mv "$LOCALE_FILENAME" "$DESTINATION" + +# clean up +rm -rf "$MESSAGE_FORMAT_DIR" diff --git a/ui/component.js b/ui/component.js index 0efdd68efc..53f4926a49 100644 --- a/ui/component.js +++ b/ui/component.js @@ -1583,8 +1583,89 @@ var Component = exports.Component = Montage.create(Montage,/** @lends module:mon } composerList.splice(0, length); } + }, + + /** + The localizer for this component + @type {module:montage/core/localizer.Localizer} + @default null + */ + localizer: { + value: null + }, + + _waitForLocalizerMessages: { + value: false + }, + /** + Whether to wait for the localizer to load messages before drawing. + Make sure to set the localizer before setting to true. + @type Boolean + @default false + @example +// require localizer +var defaultLocalizer = localizer.defaultLocalizer, + _ = localizer.localize; + +exports.Main = Montage.create(Component, { + + didCreate: { + value: function() { + this.localizer = defaultLocalizer; + this.waitForLocalizerMessages = true; + } + }, + + // ... + + // no draw happens until the localizer's messages have been loaded + prepareForDraw: { + value: function() { + this._greeting = _("hello", "Hello {name}!"); + } + }, + draw: { + value: function() { + // this is for illustration only. This example is simple enough that + // you should use a localizations binding + this._element.textContent = this._greeting({name: this.name}); + } } +} + */ + waitForLocalizerMessages: { + enumerable: false, + get: function() { + return this._waitForLocalizerMessages; + }, + set: function(value) { + if (this._waitForLocalizerMessages !== value) { + if (value === true && !this.localizer.messages) { + if (!this.localizer) { + throw "Cannot wait for messages on localizer if it is not set"; + } + + this._waitForLocalizerMessages = true; + + var self = this; + logger.debug(this, "waiting for messages from localizer"); + this.canDrawGate.setField("messages", false); + + this.localizer.messagesPromise.then(function(messages) { + if (logger.isDebug) { + logger.debug(self, "got messages from localizer"); + } + self.canDrawGate.setField("messages", true); + }); + } else { + this._waitForLocalizerMessages = false; + this.canDrawGate.setField("messages", true); + } + } + } + }, + });