Explorar o código

Merge branch 'master' into gh-pages

JC Brand %!s(int64=12) %!d(string=hai) anos
pai
achega
6464acff5c

+ 19 - 29
CHANGES.rst

@@ -4,39 +4,29 @@ Changelog
 0.4 (Unreleased)
 0.4 (Unreleased)
 ----------------
 ----------------
 
 
-- CSS tweaks: fixed overflowing text in status message and chatrooms list.
-  [jcbrand]
-- Bugfix: Cannot join chatroom when clicking from a list of rooms.
-  [jcbrand]
-- Add better support for kicking or banning users from chatrooms.
-  [jcbrand]
+- CSS tweaks: fixed overflowing text in status message and chatrooms list. [jcbrand]
+- Bugfix: Couldn't join chatroom when clicking from a list of rooms. [jcbrand]
+- Add better support for kicking or banning users from chatrooms. [jcbrand]
+- Fixed alignment of chat messages in Firefox. [jcbrand]
+- More intelligent fetching of vCards. [jcbrand]
+- Fixed a race condition bug. Make sure that the roster is populated before sending initial presence. [jcbrand]
+- Reconnect automatically when the connection drops. [jcbrand]
+- Add support for internationalization. [jcbrand]
 
 
 0.3 (2013-05-21)
 0.3 (2013-05-21)
 ----------------
 ----------------
 
 
-- Add vCard support 
-  [jcbrand]
-- Remember custom status messages upon reload. 
-  [jcbrand] 
-- Remove jquery-ui dependency. 
-  [jcbrand]
-- Use backbone.localStorage to store the contacts roster, open chatboxes and
-  chat messages. 
-  [jcbrand]
-- Fixed user status handling, which wasn't 100% according to the spec. 
-  [jcbrand]
-- Separate messages according to day in chats. 
-  [jcbrand]
-- Add support for specifying the BOSH bind URL as configuration setting.
-  [jcbrand]
-- Improve the message counter to only increment when the window is not focused 
-  [witekdev]
-- Make fetching of list of chatrooms on a server a configuration option.
-  [jcbrand]
-- Use service discovery to show all available features on a room.
-  [jcbrand]
-- Multi-user chatrooms are now configurable.
-  [jcbrand]
+- Add vCard support [jcbrand]
+- Remember custom status messages upon reload. [jcbrand]
+- Remove jquery-ui dependency. [jcbrand]
+- Use backbone.localStorage to store the contacts roster, open chatboxes and chat messages. [jcbrand]
+- Fixed user status handling, which wasn't 100% according to the spec. [jcbrand]
+- Separate messages according to day in chats. [jcbrand]
+- Add support for specifying the BOSH bind URL as configuration setting. [jcbrand]
+- Improve the message counter to only increment when the window is not focused [witekdev]
+- Make fetching of list of chatrooms on a server a configuration option. [jcbrand]
+- Use service discovery to show all available features on a room. [jcbrand]
+- Multi-user chatrooms are now configurable. [jcbrand]
 
 
 
 
 0.2 (2013-03-28)
 0.2 (2013-03-28)

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 632 - 0
Libraries/jed.js


+ 11 - 0
Libraries/locales.js

@@ -0,0 +1,11 @@
+(function (root, factory) {
+    define("locales", [
+        'jed',
+        'af',
+        'en'
+        ], function (jed, af, en) {
+            root.locales = {};
+            root.locales.af = af;
+            root.locales.en = en;
+        });
+})(this);

+ 17 - 35
Makefile

@@ -11,30 +11,31 @@ ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) ./d
 # the i18n builder cannot share the environment and doctrees with the others
 # the i18n builder cannot share the environment and doctrees with the others
 I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) ./docs/source
 I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) ./docs/source
 
 
-.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+.PHONY: help clean html dirhtml singlehtml json htmlhelp devhelp epub latex latexpdf text changes linkcheck doctest gettext
 
 
 help:
 help:
 	@echo "Please use \`make <target>' where <target> is one of"
 	@echo "Please use \`make <target>' where <target> is one of"
-	@echo "  release    to make a new minified release"
-	@echo "  html       to make standalone HTML files"
+	@echo "  changes    to make an overview of all changed/added/deprecated items"
+	@echo "  devhelp    to make HTML files and a Devhelp project"
 	@echo "  dirhtml    to make HTML files named index.html in directories"
 	@echo "  dirhtml    to make HTML files named index.html in directories"
-	@echo "  singlehtml to make a single large HTML file"
-	@echo "  pickle     to make pickle files"
+	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
+	@echo "  epub       to export the documentation to epub"
+	@echo "  gettext    to make PO message catalogs of the documentation"
+	@echo "  html       to make standalone HTML files of the documentation"
+	@echo "  htmlhelp   to make HTML files and a HTML help project from the documentation"
+	@echo "  info       to make Texinfo files and run them through makeinfo"
 	@echo "  json       to make JSON files"
 	@echo "  json       to make JSON files"
-	@echo "  htmlhelp   to make HTML files and a HTML help project"
-	@echo "  qthelp     to make HTML files and a qthelp project"
-	@echo "  devhelp    to make HTML files and a Devhelp project"
-	@echo "  epub       to make an epub"
 	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
 	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
 	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
 	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
-	@echo "  text       to make text files"
-	@echo "  man        to make manual pages"
-	@echo "  texinfo    to make Texinfo files"
-	@echo "  info       to make Texinfo files and run them through makeinfo"
-	@echo "  gettext    to make PO message catalogs"
-	@echo "  changes    to make an overview of all changed/added/deprecated items"
 	@echo "  linkcheck  to check all external links for integrity"
 	@echo "  linkcheck  to check all external links for integrity"
-	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
+	@echo "  pot        generates a gettext POT file to be used for translations"
+	@echo "  release    to make a new minified release"
+	@echo "  singlehtml to make a single large HTML file"
+	@echo "  texinfo    to make Texinfo files"
+	@echo "  text       to make text files"
+
+pot: 
+	xgettext --keyword=__ --keyword=translate --from-code=UTF-8 --output=locale/converse.pot converse.js --package-name=Converse.js --copyright-holder="Jan-Carel Brand" --package-version=0.4 -c --language="python";
 
 
 release: 
 release: 
 	r.js -o build.js
 	r.js -o build.js
@@ -57,11 +58,6 @@ singlehtml:
 	@echo
 	@echo
 	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
 	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
 
 
-pickle:
-	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
-	@echo
-	@echo "Build finished; now you can process the pickle files."
-
 json:
 json:
 	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
 	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
 	@echo
 	@echo
@@ -73,15 +69,6 @@ htmlhelp:
 	@echo "Build finished; now you can run HTML Help Workshop with the" \
 	@echo "Build finished; now you can run HTML Help Workshop with the" \
 	      ".hhp project file in $(BUILDDIR)/htmlhelp."
 	      ".hhp project file in $(BUILDDIR)/htmlhelp."
 
 
-qthelp:
-	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
-	@echo
-	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
-	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
-	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/sphinx.qhcp"
-	@echo "To view the help file:"
-	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/sphinx.qhc"
-
 devhelp:
 devhelp:
 	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
 	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
 	@echo
 	@echo
@@ -114,11 +101,6 @@ text:
 	@echo
 	@echo
 	@echo "Build finished. The text files are in $(BUILDDIR)/text."
 	@echo "Build finished. The text files are in $(BUILDDIR)/text."
 
 
-man:
-	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
-	@echo
-	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
-
 texinfo:
 texinfo:
 	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
 	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
 	@echo
 	@echo

+ 4 - 0
build.js

@@ -2,6 +2,10 @@
     baseUrl: ".",
     baseUrl: ".",
     paths: {
     paths: {
         "jquery": "Libraries/require-jquery",
         "jquery": "Libraries/require-jquery",
+        "jed": "Libraries/jed",
+        "locales": "Libraries/locales",
+        "af": "locale/af/LC_MESSAGES/af",
+        "en": "locale/en/LC_MESSAGES/en",
         "sjcl": "Libraries/sjcl",
         "sjcl": "Libraries/sjcl",
         "tinysort": "Libraries/jquery.tinysort",
         "tinysort": "Libraries/jquery.tinysort",
         "underscore": "Libraries/underscore",
         "underscore": "Libraries/underscore",

+ 17 - 12
converse.css

@@ -174,24 +174,29 @@ ul.participant-list li.moderator {
     color:#666666;
     color:#666666;
 }
 }
 
 
+.chat-message-room,
+.chat-message-them,
 .chat-message-me {
 .chat-message-me {
     font-weight: bold;
     font-weight: bold;
-    color: #436976;
-}
-
-.chat-message-room {
-    font-weight: bold;
-    color: #4B7003;
-}
-
-.chat-message-them {
-    font-weight: bold;
-    color: #F62817;
     white-space: nowrap;
     white-space: nowrap;
     max-width: 100px;
     max-width: 100px;
     text-overflow: ellipsis;
     text-overflow: ellipsis;
     overflow: hidden;
     overflow: hidden;
     display: inline-block;
     display: inline-block;
+    float: left;
+    padding-right: 3px;
+}
+
+.chat-message-them {
+    color: #F62817;
+}
+
+.chat-message-me {
+    color: #436976;
+}
+
+.chat-message-room {
+    color: #4B7003;
 }
 }
 
 
 .chat-event, .chat-date, .chat-info {
 .chat-event, .chat-date, .chat-info {
@@ -263,7 +268,7 @@ div.chat-title {
 .chat-head-chatbox,
 .chat-head-chatbox,
 .chat-head-chatroom {
 .chat-head-chatroom {
     background: linear-gradient(top, rgba(206,220,231,1) 0%,rgba(89,106,114,1) 100%);
     background: linear-gradient(top, rgba(206,220,231,1) 0%,rgba(89,106,114,1) 100%);
-    height: 33px;
+    height: 35px;
     position: relative;
     position: relative;
 }
 }
 
 

+ 2428 - 2362
converse.js

@@ -14,6 +14,10 @@
     if (typeof define === 'function' && define.amd) {
     if (typeof define === 'function' && define.amd) {
         require.config({
         require.config({
             paths: {
             paths: {
+                "jed": "Libraries/jed",
+                "locales": "Libraries/locales",
+                "af": "locale/af/LC_MESSAGES/af",
+                "en": "locale/en/LC_MESSAGES/en",
                 "sjcl": "Libraries/sjcl",
                 "sjcl": "Libraries/sjcl",
                 "tinysort": "Libraries/jquery.tinysort",
                 "tinysort": "Libraries/jquery.tinysort",
                 "underscore": "Libraries/underscore",
                 "underscore": "Libraries/underscore",
@@ -48,6 +52,7 @@
         });
         });
 
 
         define("converse", [
         define("converse", [
+            "locales",
             "localstorage",
             "localstorage",
             "tinysort",
             "tinysort",
             "sjcl",
             "sjcl",
@@ -74,450 +79,484 @@
     }
     }
 }(this, function ($, _, console) {
 }(this, function ($, _, console) {
     var converse = {};
     var converse = {};
-    converse.msg_counter = 0;
-
-    var strinclude = function(str, needle){
-      if (needle === '') { return true; }
-      if (str === null) { return false; }
-      return String(str).indexOf(needle) !== -1;
-    };
-
-    converse.autoLink = function (text) {
-        // Convert URLs into hyperlinks
-        var re = /((http|https|ftp):\/\/[\w?=&.\/\-;#~%\-]+(?![\w\s?&.\/;#~%"=\-]*>))/g;
-        return text.replace(re, '<a target="_blank" href="$1">$1</a>');
-    };
-
-    converse.toISOString = function (date) {
-        var pad;
-        if (typeof date.toISOString !== 'undefined') {
-            return date.toISOString();
-        } else {
-            // IE <= 8 Doesn't have toISOStringMethod
-            pad = function (num) {
-                return (num < 10) ? '0' + num : '' + num;
-            };
-            return date.getUTCFullYear() + '-' +
-                pad(date.getUTCMonth() + 1) + '-' +
-                pad(date.getUTCDate()) + 'T' +
-                pad(date.getUTCHours()) + ':' +
-                pad(date.getUTCMinutes()) + ':' +
-                pad(date.getUTCSeconds()) + '.000Z';
-        }
-    };
-
-    converse.parseISO8601 = function (datestr) {
-        /* Parses string formatted as 2013-02-14T11:27:08.268Z to a Date obj.
-        */
-        var numericKeys = [1, 4, 5, 6, 7, 10, 11],
-            struct = /^\s*(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}\.?\d*)Z\s*$/.exec(datestr),
-            minutesOffset = 0,
-            i, k;
+    converse.initialize = function (settings) {
+        // Default values
+        this.bosh_service_url = ''; // The BOSH connection manager URL. Required if you are not prebinding.
+        this.animate = true;
+        this.auto_list_rooms = false;
+        this.auto_subscribe = false;
+        this.hide_muc_server = false;
+        this.prebind = false;
+        this.xhr_user_search = false;
+        this.i18n = locales.en;
+        _.extend(this, settings);
 
 
-        for (i = 0; (k = numericKeys[i]); ++i) {
-            struct[k] = +struct[k] || 0;
-        }
-        // allow undefined days and months
-        struct[2] = (+struct[2] || 1) - 1;
-        struct[3] = +struct[3] || 1;
-        if (struct[8] !== 'Z' && struct[9] !== undefined) {
-            minutesOffset = struct[10] * 60 + struct[11];
-
-            if (struct[9] === '+') {
-                minutesOffset = 0 - minutesOffset;
+        var __ = function (str) {
+            var t = converse.i18n.translate(str);
+            if (arguments.length>1) {
+                return t.fetch.apply(t, [].slice.call(arguments,1));
+            } else {
+                return t.fetch();
             }
             }
-        }
-        return new Date(Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7]));
-    };
+        };
+        this.msg_counter = 0;
+        this.autoLink = function (text) {
+            // Convert URLs into hyperlinks
+            var re = /((http|https|ftp):\/\/[\w?=&.\/\-;#~%\-]+(?![\w\s?&.\/;#~%"=\-]*>))/g;
+            return text.replace(re, '<a target="_blank" href="$1">$1</a>');
+        };
 
 
-    converse.updateMsgCounter = function () {
-        if (this.msg_counter > 0) {
-            if (document.title.search(/^Messages \(\d+\) /) == -1) {
-                document.title = "Messages (" + this.msg_counter + ") " + document.title;
+        this.toISOString = function (date) {
+            var pad;
+            if (typeof date.toISOString !== 'undefined') {
+                return date.toISOString();
             } else {
             } else {
-                document.title = document.title.replace(/^Messages \(\d+\) /, "Messages (" + this.msg_counter + ") ");
+                // IE <= 8 Doesn't have toISOStringMethod
+                pad = function (num) {
+                    return (num < 10) ? '0' + num : '' + num;
+                };
+                return date.getUTCFullYear() + '-' +
+                    pad(date.getUTCMonth() + 1) + '-' +
+                    pad(date.getUTCDate()) + 'T' +
+                    pad(date.getUTCHours()) + ':' +
+                    pad(date.getUTCMinutes()) + ':' +
+                    pad(date.getUTCSeconds()) + '.000Z';
             }
             }
-            window.blur();
-            window.focus();
-        } else if (document.title.search(/^Messages \(\d+\) /) != -1) {
-            document.title = document.title.replace(/^Messages \(\d+\) /, "");
-        }
-    };
+        };
 
 
-    converse.incrementMsgCounter = function () {
-        this.msg_counter += 1;
-        this.updateMsgCounter();
-    };
+        this.parseISO8601 = function (datestr) {
+            /* Parses string formatted as 2013-02-14T11:27:08.268Z to a Date obj.
+            */
+            var numericKeys = [1, 4, 5, 6, 7, 10, 11],
+                struct = /^\s*(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}\.?\d*)Z\s*$/.exec(datestr),
+                minutesOffset = 0,
+                i, k;
+
+            for (i = 0; (k = numericKeys[i]); ++i) {
+                struct[k] = +struct[k] || 0;
+            }
+            // allow undefined days and months
+            struct[2] = (+struct[2] || 1) - 1;
+            struct[3] = +struct[3] || 1;
+            if (struct[8] !== 'Z' && struct[9] !== undefined) {
+                minutesOffset = struct[10] * 60 + struct[11];
+
+                if (struct[9] === '+') {
+                    minutesOffset = 0 - minutesOffset;
+                }
+            }
+            return new Date(Date.UTC(struct[1], struct[2], struct[3], struct[4], struct[5] + minutesOffset, struct[6], struct[7]));
+        };
 
 
-    converse.clearMsgCounter = function () {
-        this.msg_counter = 0;
-        this.updateMsgCounter();
-    };
+        this.updateMsgCounter = function () {
+            if (this.msg_counter > 0) {
+                if (document.title.search(/^Messages \(\d+\) /) == -1) {
+                    document.title = "Messages (" + this.msg_counter + ") " + document.title;
+                } else {
+                    document.title = document.title.replace(/^Messages \(\d+\) /, "Messages (" + this.msg_counter + ") ");
+                }
+                window.blur();
+                window.focus();
+            } else if (document.title.search(/^Messages \(\d+\) /) != -1) {
+                document.title = document.title.replace(/^Messages \(\d+\) /, "");
+            }
+        };
 
 
-    converse.collections = {
-        /* FIXME: XEP-0136 specifies 'urn:xmpp:archive' but the mod_archive_odbc
-        *  add-on for ejabberd wants the URL below. This might break for other
-        *  Jabber servers.
-        */
-        'URI': 'http://www.xmpp.org/extensions/xep-0136.html#ns'
-    };
+        this.incrementMsgCounter = function () {
+            this.msg_counter += 1;
+            this.updateMsgCounter();
+        };
 
 
-    converse.collections.getLastCollection = function (jid, callback) {
-        var bare_jid = Strophe.getBareJidFromJid(jid),
-            iq = $iq({'type':'get'})
-                    .c('list', {'xmlns': this.URI,
-                                'with': bare_jid
-                                })
-                    .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
-                    .c('before').up()
-                    .c('max')
-                    .t('1');
-
-        converse.connection.sendIQ(iq,
-                    callback,
-                    function () {
-                        console.log('Error while retrieving collections');
-                    });
-    };
+        this.clearMsgCounter = function () {
+            this.msg_counter = 0;
+            this.updateMsgCounter();
+        };
+
+        this.collections = {
+            /* FIXME: XEP-0136 specifies 'urn:xmpp:archive' but the mod_archive_odbc
+            *  add-on for ejabberd wants the URL below. This might break for other
+            *  Jabber servers.
+            */
+            'URI': 'http://www.xmpp.org/extensions/xep-0136.html#ns'
+        };
 
 
-    converse.collections.getLastMessages = function (jid, callback) {
-        var that = this;
-        this.getLastCollection(jid, function (result) {
-            // Retrieve the last page of a collection (max 30 elements).
-            var $collection = $(result).find('chat'),
-                jid = $collection.attr('with'),
-                start = $collection.attr('start'),
+        this.collections.getLastCollection = function (jid, callback) {
+            var bare_jid = Strophe.getBareJidFromJid(jid),
                 iq = $iq({'type':'get'})
                 iq = $iq({'type':'get'})
-                        .c('retrieve', {'start': start,
-                                    'xmlns': that.URI,
-                                    'with': jid
+                        .c('list', {'xmlns': this.URI,
+                                    'with': bare_jid
                                     })
                                     })
                         .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
                         .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
+                        .c('before').up()
                         .c('max')
                         .c('max')
-                        .t('30');
-            converse.connection.sendIQ(iq, callback);
-        });
-    };
+                        .t('1');
+
+            converse.connection.sendIQ(iq,
+                        callback,
+                        function () {
+                            console.log('Error while retrieving collections');
+                        });
+        };
+
+        this.collections.getLastMessages = function (jid, callback) {
+            var that = this;
+            this.getLastCollection(jid, function (result) {
+                // Retrieve the last page of a collection (max 30 elements).
+                var $collection = $(result).find('chat'),
+                    jid = $collection.attr('with'),
+                    start = $collection.attr('start'),
+                    iq = $iq({'type':'get'})
+                            .c('retrieve', {'start': start,
+                                        'xmlns': that.URI,
+                                        'with': jid
+                                        })
+                            .c('set', {'xmlns': 'http://jabber.org/protocol/rsm'})
+                            .c('max')
+                            .t('30');
+                converse.connection.sendIQ(iq, callback);
+            });
+        };
 
 
-    converse.Message = Backbone.Model.extend();
+        this.Message = Backbone.Model.extend();
 
 
-    converse.Messages = Backbone.Collection.extend({
-        model: converse.Message
-    });
+        this.Messages = Backbone.Collection.extend({
+            model: converse.Message
+        });
 
 
-    converse.ChatBox = Backbone.Model.extend({
-        initialize: function () {
-            if (this.get('box_id') !== 'controlbox') {
-                this.messages = new converse.Messages();
-                this.messages.localStorage = new Backbone.LocalStorage(
-                    hex_sha1('converse.messages'+this.get('jid')));
-                this.set({
-                    'user_id' : Strophe.getNodeFromJid(this.get('jid')),
-                    'box_id' : hex_sha1(this.get('jid')),
-                    'fullname' : this.get('fullname'),
-                    'url': this.get('url'),
-                    'image_type': this.get('image_type'),
-                    'image_src': this.get('image_src')
-                });
-            }
-        },
-
-        messageReceived: function (message) {
-            var $message = $(message),
-                body = converse.autoLink($message.children('body').text()),
-                from = Strophe.getBareJidFromJid($message.attr('from')),
-                composing = $message.find('composing'),
-                delayed = $message.find('delay').length > 0,
-                fullname = (this.get('fullname')||'').split(' ')[0],
-                stamp, time, sender;
-
-            if (!body) {
-                if (composing.length) {
-                    this.messages.add({
-                        fullname: fullname,
-                        sender: 'them',
-                        delayed: delayed,
-                        time: converse.toISOString(new Date()),
-                        composing: composing.length
+        this.ChatBox = Backbone.Model.extend({
+            initialize: function () {
+                if (this.get('box_id') !== 'controlbox') {
+                    this.messages = new converse.Messages();
+                    this.messages.localStorage = new Backbone.LocalStorage(
+                        hex_sha1('converse.messages'+this.get('jid')));
+                    this.set({
+                        'user_id' : Strophe.getNodeFromJid(this.get('jid')),
+                        'box_id' : hex_sha1(this.get('jid')),
+                        'fullname' : this.get('fullname'),
+                        'url': this.get('url'),
+                        'image_type': this.get('image_type'),
+                        'image': this.get('image')
                     });
                     });
                 }
                 }
-            } else {
-                if (delayed) {
-                    stamp = $message.find('delay').attr('stamp');
-                    time = stamp;
+            },
+
+            messageReceived: function (message) {
+                var $message = $(message),
+                    body = converse.autoLink($message.children('body').text()),
+                    from = Strophe.getBareJidFromJid($message.attr('from')),
+                    composing = $message.find('composing'),
+                    delayed = $message.find('delay').length > 0,
+                    fullname = (this.get('fullname')||'').split(' ')[0],
+                    stamp, time, sender;
+
+                if (!body) {
+                    if (composing.length) {
+                        this.messages.add({
+                            fullname: fullname,
+                            sender: 'them',
+                            delayed: delayed,
+                            time: converse.toISOString(new Date()),
+                            composing: composing.length
+                        });
+                    }
                 } else {
                 } else {
-                    time = converse.toISOString(new Date());
+                    if (delayed) {
+                        stamp = $message.find('delay').attr('stamp');
+                        time = stamp;
+                    } else {
+                        time = converse.toISOString(new Date());
+                    }
+                    if (from == converse.bare_jid) {
+                        sender = 'me';
+                    } else {
+                        sender = 'them';
+                    }
+                    this.messages.create({
+                        fullname: fullname,
+                        sender: sender,
+                        delayed: delayed,
+                        time: time,
+                        message: body
+                    });
                 }
                 }
-                if (from == converse.bare_jid) {
-                    sender = 'me';
-                } else {
-                    sender = 'them';
-                }
-                this.messages.create({
-                    fullname: fullname,
-                    sender: sender,
-                    delayed: delayed,
-                    time: time,
-                    message: body
-                });
-            }
-        }
-    });
-
-    converse.ChatBoxView = Backbone.View.extend({
-        length: 200,
-        tagName: 'div',
-        className: 'chatbox',
-
-        events: {
-            'click .close-chatbox-button': 'closeChat',
-            'keypress textarea.chat-textarea': 'keyPressed'
-        },
-
-        message_template: _.template(
-                            '<div class="chat-message {{extra_classes}}">' +
-                                '<span class="chat-message-{{sender}}">{{time}} {{username}}:&nbsp;</span>' +
-                                '<span class="chat-message-content">{{message}}</span>' +
-                            '</div>'),
-
-        action_template: _.template(
-                            '<div class="chat-message {{extra_classes}}">' +
-                                '<span class="chat-message-{{sender}}">{{time}} **{{username}} </span>' +
-                                '<span class="chat-message-content">{{message}}</span>' +
-                            '</div>'),
-
-        new_day_template: _.template(
-                            '<time class="chat-date" datetime="{{isodate}}">{{datestring}}</time>'
-                            ),
-
-        appendMessage: function ($el, msg_dict) {
-            var this_date = converse.parseISO8601(msg_dict.time),
-                text = msg_dict.message,
-                match = text.match(/^\/(.*?)(?: (.*))?$/),
-                sender = msg_dict.sender,
-                template, username;
-            if ((match) && (match[1] === 'me')) {
-                text = text.replace(/^\/me/, '');
-                template = this.action_template;
-                username = msg_dict.fullname;
-            } else  {
-                template = this.message_template;
-                username = sender === 'me' && sender || msg_dict.fullname;
             }
             }
-            $el.find('div.chat-event').remove();
-            $el.append(
-                template({
-                    'sender': sender,
-                    'time': this_date.toLocaleTimeString().substring(0,4),
-                    'message': text,
-                    'username': username,
-                    'extra_classes': msg_dict.delayed && 'delayed' || ''
-                }));
-        },
-
-        insertStatusNotification: function (message, replace) {
-            var $chat_content = this.$el.find('.chat-content');
-            $chat_content.find('div.chat-event').remove().end()
-                .append($('<div class="chat-event"></div>').text(message));
-            this.scrollDown();
-        },
-
-        showMessage: function (message) {
-            var time = message.get('time'),
-                times = this.model.messages.pluck('time'),
-                this_date = converse.parseISO8601(time),
-                $chat_content = this.$el.find('.chat-content'),
-                previous_message, idx, prev_date, isodate, text, match;
-
-            // If this message is on a different day than the one received
-            // prior, then indicate it on the chatbox.
-            idx = _.indexOf(times, time)-1;
-            if (idx >= 0) {
-                previous_message = this.model.messages.at(idx);
-                prev_date = converse.parseISO8601(previous_message.get('time'));
-                isodate = new Date(this_date.getTime());
-                isodate.setUTCHours(0,0,0,0);
-                isodate = converse.toISOString(isodate);
-                if (this.isDifferentDay(prev_date, this_date)) {
-                    $chat_content.append(this.new_day_template({
-                        isodate: isodate,
-                        datestring: this_date.toString().substring(0,15)
+        });
+
+        this.ChatBoxView = Backbone.View.extend({
+            length: 200,
+            tagName: 'div',
+            className: 'chatbox',
+
+            events: {
+                'click .close-chatbox-button': 'closeChat',
+                'keypress textarea.chat-textarea': 'keyPressed'
+            },
+
+            message_template: _.template(
+                                '<div class="chat-message {{extra_classes}}">' +
+                                    '<span class="chat-message-{{sender}}">{{time}} {{username}}:&nbsp;</span>' +
+                                    '<span class="chat-message-content">{{message}}</span>' +
+                                '</div>'),
+
+            action_template: _.template(
+                                '<div class="chat-message {{extra_classes}}">' +
+                                    '<span class="chat-message-{{sender}}">{{time}} **{{username}} </span>' +
+                                    '<span class="chat-message-content">{{message}}</span>' +
+                                '</div>'),
+
+            new_day_template: _.template(
+                                '<time class="chat-date" datetime="{{isodate}}">{{datestring}}</time>'
+                                ),
+
+            appendMessage: function ($el, msg_dict) {
+                var this_date = converse.parseISO8601(msg_dict.time),
+                    text = msg_dict.message,
+                    match = text.match(/^\/(.*?)(?: (.*))?$/),
+                    sender = msg_dict.sender,
+                    template, username;
+                if ((match) && (match[1] === 'me')) {
+                    text = text.replace(/^\/me/, '');
+                    template = this.action_template;
+                    username = msg_dict.fullname;
+                } else  {
+                    template = this.message_template;
+                    username = sender === 'me' && sender || msg_dict.fullname;
+                }
+                $el.find('div.chat-event').remove();
+                $el.append(
+                    template({
+                        'sender': sender,
+                        'time': this_date.toLocaleTimeString().substring(0,4),
+                        'message': text,
+                        'username': username,
+                        'extra_classes': msg_dict.delayed && 'delayed' || ''
                     }));
                     }));
+            },
+
+            insertStatusNotification: function (message, replace) {
+                var $chat_content = this.$el.find('.chat-content');
+                $chat_content.find('div.chat-event').remove().end()
+                    .append($('<div class="chat-event"></div>').text(message));
+                this.scrollDown();
+            },
+
+            showMessage: function (message) {
+                var time = message.get('time'),
+                    times = this.model.messages.pluck('time'),
+                    this_date = converse.parseISO8601(time),
+                    $chat_content = this.$el.find('.chat-content'),
+                    previous_message, idx, prev_date, isodate, text, match;
+
+                // If this message is on a different day than the one received
+                // prior, then indicate it on the chatbox.
+                idx = _.indexOf(times, time)-1;
+                if (idx >= 0) {
+                    previous_message = this.model.messages.at(idx);
+                    prev_date = converse.parseISO8601(previous_message.get('time'));
+                    isodate = new Date(this_date.getTime());
+                    isodate.setUTCHours(0,0,0,0);
+                    isodate = converse.toISOString(isodate);
+                    if (this.isDifferentDay(prev_date, this_date)) {
+                        $chat_content.append(this.new_day_template({
+                            isodate: isodate,
+                            datestring: this_date.toString().substring(0,15)
+                        }));
+                    }
                 }
                 }
-            }
-            if (message.get('composing')) {
-                this.insertStatusNotification(message.get('fullname')+' '+'is typing');
-                return;
-            } else {
-                this.appendMessage($chat_content, _.clone(message.attributes));
-            }
-            if ((message.get('sender') != 'me') && (converse.windowState == 'blur')) {
-                converse.incrementMsgCounter();
-            }
-            this.scrollDown();
-        },
-
-        isDifferentDay: function (prev_date, next_date) {
-            return (
-                (next_date.getDate() != prev_date.getDate()) ||
-                (next_date.getFullYear() != prev_date.getFullYear()) ||
-                (next_date.getMonth() != prev_date.getMonth()));
-        },
-
-        addHelpMessages: function (msgs) {
-            var $chat_content = this.$el.find('.chat-content'), i,
-                msgs_length = msgs.length;
-            for (i=0; i<msgs_length; i++) {
-                $chat_content.append($('<div class="chat-info">'+msgs[i]+'</div>'));
-            }
-            this.scrollDown();
-        },
-
-        sendMessage: function (text) {
-            // TODO: Look in ChatPartners to see what resources we have for the recipient.
-            // if we have one resource, we sent to only that resources, if we have multiple
-            // we send to the bare jid.
-            var timestamp = (new Date()).getTime(),
-                bare_jid = this.model.get('jid'),
-                match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/),
-                msgs;
-
-            if (match) {
-                if (match[1] === "clear") {
-                    this.$el.find('.chat-content').empty();
-                    this.model.messages.reset();
+                if (message.get('composing')) {
+                    this.insertStatusNotification(message.get('fullname')+' '+'is typing');
                     return;
                     return;
+                } else {
+                    this.appendMessage($chat_content, _.clone(message.attributes));
                 }
                 }
-                else if (match[1] === "help") {
-                    msgs = [
-                        '<strong>/help</strong>: Show this menu',
-                        '<strong>/me</strong>: Write in the third person',
-                        '<strong>/clear</strong>: Remove messages'
-                        ];
-                    this.addHelpMessages(msgs);
-                    return;
+                if ((message.get('sender') != 'me') && (converse.windowState == 'blur')) {
+                    converse.incrementMsgCounter();
                 }
                 }
-            }
-            var message = $msg({from: converse.bare_jid, to: bare_jid, type: 'chat', id: timestamp})
-                .c('body').t(text).up()
-                .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'});
-            // Forward the message, so that other connected resources are also aware of it.
-            // TODO: Forward the message only to other connected resources (inside the browser)
-            var forwarded = $msg({to:converse.bare_jid, type:'chat', id:timestamp})
-                            .c('forwarded', {xmlns:'urn:xmpp:forward:0'})
-                            .c('delay', {xmns:'urn:xmpp:delay',stamp:timestamp}).up()
-                            .cnode(message.tree());
-            converse.connection.send(message);
-            converse.connection.send(forwarded);
-            // Add the new message
-            this.model.messages.create({
-                fullname: converse.xmppstatus.get('fullname')||converse.bare_jid,
-                sender: 'me',
-                time: converse.toISOString(new Date()),
-                message: text
-            });
-        },
+                this.scrollDown();
+            },
 
 
-        keyPressed: function (ev) {
-            var $textarea = $(ev.target),
-                message,
-                notify,
-                composing;
+            isDifferentDay: function (prev_date, next_date) {
+                return (
+                    (next_date.getDate() != prev_date.getDate()) ||
+                    (next_date.getFullYear() != prev_date.getFullYear()) ||
+                    (next_date.getMonth() != prev_date.getMonth()));
+            },
 
 
-            if(ev.keyCode == 13) {
-                ev.preventDefault();
-                message = $textarea.val();
-                $textarea.val('').focus();
-                if (message !== '') {
-                    if (this.model.get('chatroom')) {
-                        this.sendChatRoomMessage(message);
-                    } else {
-                        this.sendMessage(message);
+            addHelpMessages: function (msgs) {
+                var $chat_content = this.$el.find('.chat-content'), i,
+                    msgs_length = msgs.length;
+                for (i=0; i<msgs_length; i++) {
+                    $chat_content.append($('<div class="chat-info">'+msgs[i]+'</div>'));
+                }
+                this.scrollDown();
+            },
+
+            sendMessage: function (text) {
+                // TODO: Look in ChatPartners to see what resources we have for the recipient.
+                // if we have one resource, we sent to only that resources, if we have multiple
+                // we send to the bare jid.
+                var timestamp = (new Date()).getTime(),
+                    bare_jid = this.model.get('jid'),
+                    match = text.replace(/^\s*/, "").match(/^\/(.*)\s*$/),
+                    msgs;
+
+                if (match) {
+                    if (match[1] === "clear") {
+                        this.$el.find('.chat-content').empty();
+                        this.model.messages.reset();
+                        return;
+                    }
+                    else if (match[1] === "help") {
+                        msgs = [
+                            '<strong>/help</strong>:'+__('Show this menu')+'',
+                            '<strong>/me</strong>:'+__('Write in the third person')+'',
+                            '<strong>/clear</strong>:'+__('Remove messages')+''
+                            ];
+                        this.addHelpMessages(msgs);
+                        return;
                     }
                     }
                 }
                 }
-                this.$el.data('composing', false);
-
-            } else if (!this.model.get('chatroom')) {
-                // composing data is only for single user chat
-                composing = this.$el.data('composing');
-                if (!composing) {
-                    if (ev.keyCode != 47) {
-                        // We don't send composing messages if the message
-                        // starts with forward-slash.
-                        notify = $msg({'to':this.model.get('jid'), 'type': 'chat'})
-                                        .c('composing', {'xmlns':'http://jabber.org/protocol/chatstates'});
-                        converse.connection.send(notify);
+                var message = $msg({from: converse.bare_jid, to: bare_jid, type: 'chat', id: timestamp})
+                    .c('body').t(text).up()
+                    .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'});
+                // Forward the message, so that other connected resources are also aware of it.
+                // TODO: Forward the message only to other connected resources (inside the browser)
+                var forwarded = $msg({to:converse.bare_jid, type:'chat', id:timestamp})
+                                .c('forwarded', {xmlns:'urn:xmpp:forward:0'})
+                                .c('delay', {xmns:'urn:xmpp:delay',stamp:timestamp}).up()
+                                .cnode(message.tree());
+                converse.connection.send(message);
+                converse.connection.send(forwarded);
+                // Add the new message
+                this.model.messages.create({
+                    fullname: converse.xmppstatus.get('fullname')||converse.bare_jid,
+                    sender: 'me',
+                    time: converse.toISOString(new Date()),
+                    message: text
+                });
+            },
+
+            keyPressed: function (ev) {
+                var $textarea = $(ev.target),
+                    message, notify, composing;
+                if(ev.keyCode == 13) {
+                    ev.preventDefault();
+                    message = $textarea.val();
+                    $textarea.val('').focus();
+                    if (message !== '') {
+                        if (this.model.get('chatroom')) {
+                            this.sendChatRoomMessage(message);
+                        } else {
+                            this.sendMessage(message);
+                        }
+                    }
+                    this.$el.data('composing', false);
+                } else if (!this.model.get('chatroom')) {
+                    // composing data is only for single user chat
+                    composing = this.$el.data('composing');
+                    if (!composing) {
+                        if (ev.keyCode != 47) {
+                            // We don't send composing messages if the message
+                            // starts with forward-slash.
+                            notify = $msg({'to':this.model.get('jid'), 'type': 'chat'})
+                                            .c('composing', {'xmlns':'http://jabber.org/protocol/chatstates'});
+                            converse.connection.send(notify);
+                        }
+                        this.$el.data('composing', true);
                     }
                     }
-                    this.$el.data('composing', true);
                 }
                 }
-            }
-        },
-
-        onChange: function (item, changed) {
-            if (_.has(item.changed, 'chat_status')) {
-                var chat_status = item.get('chat_status'),
-                    fullname = item.get('fullname');
-                if (this.$el.is(':visible')) {
-                    if (chat_status === 'offline') {
-                        this.insertStatusNotification(fullname+' '+'has gone offline');
-                    } else if (chat_status === 'away') {
-                        this.insertStatusNotification(fullname+' '+'has gone away');
-                    } else if ((chat_status === 'dnd')) {
-                        this.insertStatusNotification(fullname+' '+'is busy');
-                    } else if (chat_status === 'online') {
-                        this.$el.find('div.chat-event').remove();
+            },
+
+            onChange: function (item, changed) {
+                if (_.has(item.changed, 'chat_status')) {
+                    var chat_status = item.get('chat_status'),
+                        fullname = item.get('fullname');
+                    if (this.$el.is(':visible')) {
+                        if (chat_status === 'offline') {
+                            this.insertStatusNotification(fullname+' '+'has gone offline');
+                        } else if (chat_status === 'away') {
+                            this.insertStatusNotification(fullname+' '+'has gone away');
+                        } else if ((chat_status === 'dnd')) {
+                            this.insertStatusNotification(fullname+' '+'is busy');
+                        } else if (chat_status === 'online') {
+                            this.$el.find('div.chat-event').remove();
+                        }
                     }
                     }
+                } if (_.has(item.changed, 'status')) {
+                    this.showStatusMessage(item.get('status'));
+                } if (_.has(item.changed, 'image')) {
+                    this.renderAvatar();
                 }
                 }
-            } if (_.has(item.changed, 'status')) {
-                this.showStatusMessage(item.get('status'));
-            }
-        },
+                // TODO check for changed fullname as well
+            },
 
 
-        showStatusMessage: function (msg) {
-            this.$el.find('p.user-custom-message').text(msg).attr('title', msg);
-        },
+            showStatusMessage: function (msg) {
+                this.$el.find('p.user-custom-message').text(msg).attr('title', msg);
+            },
 
 
-        closeChat: function () {
-            if (converse.connection) {
-                this.model.destroy();
-            } else {
-                this.model.trigger('hide');
-            }
-        },
-
-        initialize: function (){
-            this.model.messages.on('add', this.showMessage, this);
-            this.model.on('show', this.show, this);
-            this.model.on('destroy', this.hide, this);
-            this.model.on('change', this.onChange, this);
-
-            this.$el.appendTo(converse.chatboxesview.$el);
-            this.render().show().model.messages.fetch({add: true});
-            if (this.model.get('status')) {
-                this.showStatusMessage(this.model.get('status'));
-            }
-        },
-
-        template: _.template(
-            '<div class="chat-head chat-head-chatbox">' +
-                '<a class="close-chatbox-button">X</a>' +
-                '<a href="{{url}}" target="_blank" class="user">' +
-                    '<div class="chat-title"> {{ fullname }} </div>' +
-                '</a>' +
-                '<p class="user-custom-message"><p/>' +
-            '</div>' +
-            '<div class="chat-content"></div>' +
-            '<form class="sendXMPPMessage" action="" method="post">' +
-            '<textarea ' +
-                'type="text" ' +
-                'class="chat-textarea" ' +
-                'placeholder="Personal message"/>'+
-            '</form>'),
-
-        render: function () {
-            this.$el.attr('id', this.model.get('box_id'))
-                    .html(this.template(this.model.toJSON()));
-            if (this.model.get('image')) {
+            closeChat: function () {
+                if (converse.connection) {
+                    this.model.destroy();
+                } else {
+                    this.model.trigger('hide');
+                }
+            },
+
+            updateVCard: function () {
+                var jid = this.model.get('jid'),
+                    rosteritem = converse.roster.get(jid);
+                if ((rosteritem)&&(!rosteritem.get('vcard_updated'))) {
+                    converse.getVCard(
+                        jid,
+                        $.proxy(function (jid, fullname, image, image_type, url) {
+                            this.model.save({
+                                'fullname' : fullname || jid,
+                                'url': url,
+                                'image_type': image_type,
+                                'image': image,
+                                'vcard_updated': converse.toISOString(new Date())
+                            });
+                        }, this),
+                        $.proxy(function (stanza) {
+                            console.log("ChatBoxView.initialize: An error occured while fetching vcard");
+                        }, this)
+                    );
+                }
+            },
+
+            initialize: function (){
+                this.model.messages.on('add', this.showMessage, this);
+                this.model.on('show', this.show, this);
+                this.model.on('destroy', this.hide, this);
+                this.model.on('change', this.onChange, this);
+                this.updateVCard();
+                this.$el.appendTo(converse.chatboxesview.$el);
+                this.render().show().model.messages.fetch({add: true});
+                if (this.model.get('status')) {
+                    this.showStatusMessage(this.model.get('status'));
+                }
+            },
+
+            template: _.template(
+                '<div class="chat-head chat-head-chatbox">' +
+                    '<a class="close-chatbox-button">X</a>' +
+                    '<a href="{{url}}" target="_blank" class="user">' +
+                        '<div class="chat-title"> {{ fullname }} </div>' +
+                    '</a>' +
+                    '<p class="user-custom-message"><p/>' +
+                '</div>' +
+                '<div class="chat-content"></div>' +
+                '<form class="sendXMPPMessage" action="" method="post">' +
+                '<textarea ' +
+                    'type="text" ' +
+                    'class="chat-textarea" ' +
+                    'placeholder="'+__('Personal message')+'"/>'+
+                '</form>'),
+
+            renderAvatar: function () {
+                if (!this.model.get('image')) {
+                    return;
+                }
                 var img_src = 'data:'+this.model.get('image_type')+';base64,'+this.model.get('image'),
                 var img_src = 'data:'+this.model.get('image_type')+';base64,'+this.model.get('image'),
                     canvas = $('<canvas height="35px" width="35px" class="avatar"></canvas>'),
                     canvas = $('<canvas height="35px" width="35px" class="avatar"></canvas>'),
                     ctx = canvas.get(0).getContext('2d'),
                     ctx = canvas.get(0).getContext('2d'),
@@ -528,2083 +567,2110 @@
                 };
                 };
                 img.src = img_src;
                 img.src = img_src;
                 this.$el.find('.chat-title').before(canvas);
                 this.$el.find('.chat-title').before(canvas);
-            }
-            return this;
-        },
+            },
 
 
-        focus: function () {
-            this.$el.find('.chat-textarea').focus();
-            return this;
-        },
+            render: function () {
+                this.$el.attr('id', this.model.get('box_id'))
+                    .html(this.template(this.model.toJSON()));
+                this.renderAvatar();
+                return this;
+            },
 
 
-        hide: function () {
-            if (converse.animate) {
-                this.$el.hide('fast');
-            } else {
-                this.$el.hide();
-            }
-        },
+            focus: function () {
+                this.$el.find('.chat-textarea').focus();
+                return this;
+            },
 
 
-        show: function () {
-            if (this.$el.is(':visible') && this.$el.css('opacity') == "1") {
-                return this.focus();
-            }
-            if (converse.animate) {
-                this.$el.css({'opacity': 0, 'display': 'inline'}).animate({opacity: '1'}, 200);
-            } else {
-                this.$el.css({'opacity': 1, 'display': 'inline'});
-            }
-            if (converse.connection) {
-                // Without a connection, we haven't yet initialized
-                // localstorage
-                this.model.save();
+            hide: function () {
+                if (converse.animate) {
+                    this.$el.hide('fast');
+                } else {
+                    this.$el.hide();
+                }
+            },
+
+            show: function () {
+                if (this.$el.is(':visible') && this.$el.css('opacity') == "1") {
+                    return this.focus();
+                }
+                if (converse.animate) {
+                    this.$el.css({'opacity': 0, 'display': 'inline'}).animate({opacity: '1'}, 200);
+                } else {
+                    this.$el.css({'opacity': 1, 'display': 'inline'});
+                }
+                if (converse.connection) {
+                    // Without a connection, we haven't yet initialized
+                    // localstorage
+                    this.model.save();
+                }
+                return this;
+            },
+
+            scrollDown: function () {
+                var $content = this.$el.find('.chat-content');
+                $content.scrollTop($content[0].scrollHeight);
+                return this;
             }
             }
-            return this;
-        },
+        });
 
 
-        scrollDown: function () {
-            var $content = this.$el.find('.chat-content');
-            $content.scrollTop($content[0].scrollHeight);
-            return this;
-        }
-    });
-
-    converse.ContactsPanel = Backbone.View.extend({
-        tagName: 'div',
-        className: 'oc-chat-content',
-        id: 'users',
-        events: {
-            'click a.toggle-xmpp-contact-form': 'toggleContactForm',
-            'submit form.add-xmpp-contact': 'addContactFromForm',
-            'submit form.search-xmpp-contact': 'searchContacts',
-            'click a.subscribe-to-user': 'addContactFromList'
-        },
-
-        tab_template: _.template('<li><a class="s current" href="#users">Contacts</a></li>'),
-        template: _.template(
-            '<form class="set-xmpp-status" action="" method="post">'+
-                '<span id="xmpp-status-holder">'+
-                    '<select id="select-xmpp-status">'+
-                        '<option value="online">Online</option>'+
-                        '<option value="dnd">Busy</option>'+
-                        '<option value="away">Away</option>'+
-                        '<option value="offline">Offline</option>'+
-                    '</select>'+
-                '</span>'+
-            '</form>'+
-            '<dl class="add-converse-contact dropdown">' +
-                '<dt id="xmpp-contact-search" class="fancy-dropdown">' +
-                    '<a class="toggle-xmpp-contact-form" href="#" title="Click to add new chat contacts">Add a contact</a>' +
-                '</dt>' +
-                '<dd class="search-xmpp" style="display:none"><ul></ul></dd>' +
-            '</dl>'
-        ),
-
-        add_contact_template: _.template(
-            '<li>'+
-                '<form class="add-xmpp-contact">' +
-                    '<input type="text" name="identifier" class="username" placeholder="Contact username"/>' +
-                    '<button type="submit">Add</button>' +
-                '</form>'+
-            '<li>'
-        ),
-
-        search_contact_template: _.template(
-            '<li>'+
-                '<form class="search-xmpp-contact">' +
-                    '<input type="text" name="identifier" class="username" placeholder="Contact name"/>' +
-                    '<button type="submit">Search</button>' +
+        this.ContactsPanel = Backbone.View.extend({
+            tagName: 'div',
+            className: 'oc-chat-content',
+            id: 'users',
+            events: {
+                'click a.toggle-xmpp-contact-form': 'toggleContactForm',
+                'submit form.add-xmpp-contact': 'addContactFromForm',
+                'submit form.search-xmpp-contact': 'searchContacts',
+                'click a.subscribe-to-user': 'addContactFromList'
+            },
+
+            tab_template: _.template('<li><a class="s current" href="#users">'+__('Contacts')+'</a></li>'),
+            template: _.template(
+                '<form class="set-xmpp-status" action="" method="post">'+
+                    '<span id="xmpp-status-holder">'+
+                        '<select id="select-xmpp-status" style="display:none">'+
+                            '<option value="online">'+__('Online')+'</option>'+
+                            '<option value="dnd">'+__('Busy')+'</option>'+
+                            '<option value="away">'+__('Away')+'</option>'+
+                            '<option value="offline">'+__('Offline')+'</option>'+
+                        '</select>'+
+                    '</span>'+
                 '</form>'+
                 '</form>'+
-            '<li>'
-        ),
-
-        render: function () {
-            var markup;
-            this.$parent.find('#controlbox-tabs').append(this.tab_template());
-            this.$parent.find('#controlbox-panes').append(this.$el.html(this.template()));
-            if (converse.xhr_user_search) {
-                markup = this.search_contact_template();
-            } else {
-                markup = this.add_contact_template();
-            }
-            this.$el.find('.search-xmpp ul').append(markup);
-            return this;
-        },
+                '<dl class="add-converse-contact dropdown">' +
+                    '<dt id="xmpp-contact-search" class="fancy-dropdown">' +
+                        '<a class="toggle-xmpp-contact-form" href="#"'+
+                            'title="'+__('Click to add new chat contacts')+'">'+__('Add a contact')+'</a>' +
+                    '</dt>' +
+                    '<dd class="search-xmpp" style="display:none"><ul></ul></dd>' +
+                '</dl>'
+            ),
+
+            add_contact_template: _.template(
+                '<li>'+
+                    '<form class="add-xmpp-contact">' +
+                        '<input type="text" name="identifier" class="username" placeholder="'+__('Contact username')+'"/>' +
+                        '<button type="submit">'+__('Add')+'</button>' +
+                    '</form>'+
+                '<li>'
+            ),
+
+            search_contact_template: _.template(
+                '<li>'+
+                    '<form class="search-xmpp-contact">' +
+                        '<input type="text" name="identifier" class="username" placeholder="'+__('Contact name')+'"/>' +
+                        '<button type="submit">'+__('Search')+'</button>' +
+                    '</form>'+
+                '<li>'
+            ),
+
+            render: function () {
+                var markup;
+                this.$parent.find('#controlbox-tabs').append(this.tab_template());
+                this.$parent.find('#controlbox-panes').append(this.$el.html(this.template()));
+                if (converse.xhr_user_search) {
+                    markup = this.search_contact_template();
+                } else {
+                    markup = this.add_contact_template();
+                }
+                this.$el.find('.search-xmpp ul').append(markup);
+                this.$el.append(converse.rosterview.$el);
+                return this;
+            },
+
+            toggleContactForm: function (ev) {
+                ev.preventDefault();
+                this.$el.find('.search-xmpp').toggle('fast', function () {
+                    if ($(this).is(':visible')) {
+                        $(this).find('input.username').focus();
+                    }
+                });
+            },
+
+            searchContacts: function (ev) {
+                ev.preventDefault();
+                $.getJSON(portal_url + "/search-users?q=" + $(ev.target).find('input.username').val(), function (data) {
+                    var $ul= $('.search-xmpp ul');
+                    $ul.find('li.found-user').remove();
+                    $ul.find('li.chat-info').remove();
+                    if (!data.length) {
+                        $ul.append('<li class="chat-info">'+__('No users found')+'</li>');
+                    }
+
+                    $(data).each(function (idx, obj) {
+                        $ul.append(
+                            $('<li class="found-user"></li>')
+                            .append(
+                                $('<a class="subscribe-to-user" href="#" title="'+__('Click to add as a chat contact')+'"></a>')
+                                .attr('data-recipient', Strophe.escapeNode(obj.id)+'@'+converse.domain)
+                                .text(obj.fullname)
+                            )
+                        );
+                    });
+                });
+            },
 
 
-        toggleContactForm: function (ev) {
-            ev.preventDefault();
-            this.$el.find('.search-xmpp').toggle('fast', function () {
-                if ($(this).is(':visible')) {
-                    $(this).find('input.username').focus();
+            addContactFromForm: function (ev) {
+                ev.preventDefault();
+                var $input = $(ev.target).find('input');
+                var jid = $input.val();
+                if (! jid) {
+                    // this is not a valid JID
+                    $input.addClass('error');
+                    return;
                 }
                 }
-            });
-        },
-
-        searchContacts: function (ev) {
-            ev.preventDefault();
-            $.getJSON(portal_url + "/search-users?q=" + $(ev.target).find('input.username').val(), function (data) {
-                var $ul= $('.search-xmpp ul');
-                $ul.find('li.found-user').remove();
-                $ul.find('li.chat-info').remove();
-                if (!data.length) {
-                    $ul.append('<li class="chat-info">No users found</li>');
-                }
-
-                $(data).each(function (idx, obj) {
-                    $ul.append(
-                        $('<li class="found-user"></li>')
-                        .append(
-                            $('<a class="subscribe-to-user" href="#" title="Click to add as a chat contact"></a>')
-                            .attr('data-recipient', Strophe.escapeNode(obj.id)+'@'+converse.domain)
-                            .text(obj.fullname)
-                        )
-                    );
+                converse.getVCard(
+                    jid,
+                    $.proxy(function (jid, fullname, image, image_type, url) {
+                        this.addContact(jid, fullname);
+                    }, this),
+                    $.proxy(function (stanza) {
+                        console.log("An error occured while fetching vcard");
+                        if ($(stanza).find('error').attr('code') == '503') {
+                            // If we get service-unavailable, we continue to create
+                            // the user
+                            var jid = $(stanza).attr('from');
+                            this.addContact(jid, jid);
+                        }
+                    }, this));
+                $('.search-xmpp').hide();
+            },
+
+            addContactFromList: function (ev) {
+                ev.preventDefault();
+                var $target = $(ev.target),
+                    jid = $target.attr('data-recipient'),
+                    name = $target.text();
+                this.addContact(jid, name);
+                $target.parent().remove();
+                $('.search-xmpp').hide();
+            },
+
+            addContact: function (jid, name) {
+                converse.connection.roster.add(jid, name, [], function (iq) {
+                    converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
                 });
                 });
-            });
-        },
-
-        addContactFromForm: function (ev) {
-            ev.preventDefault();
-            var $input = $(ev.target).find('input');
-            var jid = $input.val();
-            if (! jid) {
-                // this is not a valid JID
-                $input.addClass('error');
-                return;
             }
             }
-            converse.getVCard(
-                jid,
-                $.proxy(function (jid, fullname, image, image_type, url) {
-                    this.addContact(jid, fullname);
-                }, this),
-                $.proxy(function (stanza) {
-                    console.log("An error occured while fetching vcard");
-                    if ($(stanza).find('error').attr('code') == '503') {
-                        // If we get service-unavailable, we continue to create
-                        // the user
-                        var jid = $(stanza).attr('from');
-                        this.addContact(jid, jid);
+        });
+
+        this.RoomsPanel = Backbone.View.extend({
+            tagName: 'div',
+            id: 'chatrooms',
+            events: {
+                'submit form.add-chatroom': 'createChatRoom',
+                'click input#show-rooms': 'showRooms',
+                'click a.open-room': 'createChatRoom',
+                'click a.room-info': 'showRoomInfo'
+            },
+            room_template: _.template(
+                '<dd class="available-chatroom">'+
+                '<a class="open-room" data-room-jid="{{jid}}"'+
+                'title="'+__('Click to open this room')+'" href="#">{{name}}</a>'+
+                '<a class="room-info" data-room-jid="{{jid}}"'+
+                'title="'+__('Show more information on this room')+'" href="#">&nbsp;</a>'+
+                '</dd>'),
+
+            room_description_template: _.template(
+                '<div class="room-info">'+
+                '<p class="room-info"><strong>'+__('Description:')+'</strong> {{desc}}</p>' +
+                '<p class="room-info"><strong>'+__('Occupants:')+'</strong> {{occ}}</p>' +
+                '<p class="room-info"><strong>'+__('Features:')+'</strong> <ul>'+
+                '{[ if (passwordprotected) { ]}' +
+                    '<li class="room-info locked">'+__('Requires authentication')+'</li>' +
+                '{[ } ]}' +
+                '{[ if (hidden) { ]}' +
+                    '<li class="room-info">'+__('Hidden')+'</li>' +
+                '{[ } ]}' +
+                '{[ if (membersonly) { ]}' +
+                    '<li class="room-info">'+__('Requires an invitation')+'</li>' +
+                '{[ } ]}' +
+                '{[ if (moderated) { ]}' +
+                    '<li class="room-info">'+__('Moderated')+'</li>' +
+                '{[ } ]}' +
+                '{[ if (nonanonymous) { ]}' +
+                    '<li class="room-info">'+__('Non-anonymous')+'</li>' +
+                '{[ } ]}' +
+                '{[ if (open) { ]}' +
+                    '<li class="room-info">'+__('Open room')+'</li>' +
+                '{[ } ]}' +
+                '{[ if (persistent) { ]}' +
+                    '<li class="room-info">'+__('Permanent room')+'</li>' +
+                '{[ } ]}' +
+                '{[ if (publicroom) { ]}' +
+                    '<li class="room-info">'+__('Public')+'</li>' +
+                '{[ } ]}' +
+                '{[ if (semianonymous) { ]}' +
+                    '<li class="room-info">'+__('Semi-anonymous')+'</li>' +
+                '{[ } ]}' +
+                '{[ if (temporary) { ]}' +
+                    '<li class="room-info">'+__('Temporary room')+'</li>' +
+                '{[ } ]}' +
+                '{[ if (unmoderated) { ]}' +
+                    '<li class="room-info">'+__('Unmoderated')+'</li>' +
+                '{[ } ]}' +
+                '</p>' +
+                '</div>'
+            ),
+
+            tab_template: _.template('<li><a class="s" href="#chatrooms">'+__('Rooms')+'</a></li>'),
+
+            template: _.template(
+                '<form class="add-chatroom" action="" method="post">'+
+                    '<input type="text" name="chatroom" class="new-chatroom-name" placeholder="'+__('Room name')+'"/>'+
+                    '<input type="text" name="nick" class="new-chatroom-nick" placeholder="'+__('Nickname')+'"/>'+
+                    '<input type="{{ server_input_type }}" name="server" class="new-chatroom-server" placeholder="'+__('Server')+'"/>'+
+                    '<input type="submit" name="join" value="'+__('Join')+'"/>'+
+                    '<input type="button" name="show" id="show-rooms" value="'+__('Show rooms')+'"/>'+
+                '</form>'+
+                '<dl id="available-chatrooms"></dl>'),
+
+            render: function () {
+                this.$parent.find('#controlbox-tabs').append(this.tab_template());
+                this.$parent.find('#controlbox-panes').append(
+                    this.$el.html(
+                        this.template({
+                            server_input_type: converse.hide_muc_server && 'hidden' || 'text'
+                        })
+                    ).hide());
+                return this;
+            },
+
+            initialize: function () {
+                this.on('update-rooms-list', function (ev) {
+                    this.updateRoomsList();
+                });
+                converse.xmppstatus.on("change", $.proxy(function (model) {
+                    if (!(_.has(model.changed, 'fullname'))) {
+                        return;
+                    }
+                    var $nick = this.$el.find('input.new-chatroom-nick');
+                    if (! $nick.is(':focus')) {
+                        $nick.val(model.get('fullname'));
                     }
                     }
                 }, this));
                 }, this));
-            $('.search-xmpp').hide();
-        },
-
-        addContactFromList: function (ev) {
-            ev.preventDefault();
-            var $target = $(ev.target),
-                jid = $target.attr('data-recipient'),
-                name = $target.text();
-            this.addContact(jid, name);
-            $target.parent().remove();
-            $('.search-xmpp').hide();
-        },
-
-        addContact: function (jid, name) {
-            converse.connection.roster.add(jid, name, [], function (iq) {
-                converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
-            });
-        }
-    });
-
-    converse.RoomsPanel = Backbone.View.extend({
-        tagName: 'div',
-        id: 'chatrooms',
-        events: {
-            'submit form.add-chatroom': 'createChatRoom',
-            'click input#show-rooms': 'showRooms',
-            'click a.open-room': 'createChatRoom',
-            'click a.room-info': 'showRoomInfo'
-        },
-        room_template: _.template(
-            '<dd class="available-chatroom">'+
-            '<a class="open-room" data-room-jid="{{jid}}" title="Click to open this room" href="#">{{name}}</a>'+
-            '<a class="room-info" data-room-jid="{{jid}}" title="Show more information on this room" href="#">&nbsp;</a>'+
-            '</dd>'),
-
-        room_description_template: _.template(
-            '<div class="room-info">'+
-            '<p class="room-info"><strong>Description:</strong> {{desc}}</p>' +
-            '<p class="room-info"><strong>Occupants:</strong> {{occ}}</p>' +
-            '<p class="room-info"><strong>Features:</strong> <ul>'+
-            '{[ if (passwordprotected) { ]}' +
-                '<li class="room-info locked">Requires authentication</li>' +
-            '{[ } ]}' +
-            '{[ if (hidden) { ]}' +
-                '<li class="room-info">Hidden</li>' +
-            '{[ } ]}' +
-            '{[ if (membersonly) { ]}' +
-                '<li class="room-info">Requires an invitation</li>' +
-            '{[ } ]}' +
-            '{[ if (moderated) { ]}' +
-                '<li class="room-info">Moderated</li>' +
-            '{[ } ]}' +
-            '{[ if (nonanonymous) { ]}' +
-                '<li class="room-info">Non-anonymous</li>' +
-            '{[ } ]}' +
-            '{[ if (open) { ]}' +
-                '<li class="room-info">Open room</li>' +
-            '{[ } ]}' +
-            '{[ if (persistent) { ]}' +
-                '<li class="room-info">Permanent room</li>' +
-            '{[ } ]}' +
-            '{[ if (publicroom) { ]}' +
-                '<li class="room-info">Public</li>' +
-            '{[ } ]}' +
-            '{[ if (semianonymous) { ]}' +
-                '<li class="room-info">Semi-anonymous</li>' +
-            '{[ } ]}' +
-            '{[ if (temporary) { ]}' +
-                '<li class="room-info">Temporary room</li>' +
-            '{[ } ]}' +
-            '{[ if (unmoderated) { ]}' +
-                '<li class="room-info">Unmoderated</li>' +
-            '{[ } ]}' +
-            '</p>' +
-            '</div>'
-        ),
-
-        tab_template: _.template('<li><a class="s" href="#chatrooms">Rooms</a></li>'),
-
-        template: _.template(
-            '<form class="add-chatroom" action="" method="post">'+
-                '<input type="text" name="chatroom" class="new-chatroom-name" placeholder="Room name"/>'+
-                '<input type="text" name="nick" class="new-chatroom-nick" placeholder="Nickname"/>'+
-                '<input type="{{ server_input_type }}" name="server" class="new-chatroom-server" placeholder="Server"/>'+
-                '<input type="submit" name="join" value="Join"/>'+
-                '<input type="button" name="show" id="show-rooms" value="Show rooms"/>'+
-            '</form>'+
-            '<dl id="available-chatrooms"></dl>'),
-
-        render: function () {
-            this.$parent.find('#controlbox-tabs').append(this.tab_template());
-            this.$parent.find('#controlbox-panes').append(
-                this.$el.html(
-                    this.template({
-                        server_input_type: converse.hide_muc_server && 'hidden' || 'text'
-                    })
-                ).hide());
-            return this;
-        },
-
-        initialize: function () {
-            this.on('update-rooms-list', function (ev) {
-                this.updateRoomsList();
-            });
-            converse.xmppstatus.on("change", $.proxy(function (model) {
-                if (!(_.has(model.changed, 'fullname'))) {
+            },
+
+            informNoRoomsFound: function () {
+                var $available_chatrooms = this.$el.find('#available-chatrooms');
+                // # For translators: %1$s is a variable and will be replaced with the XMPP server name
+                $available_chatrooms.html('<dt>'+__('No rooms on %1$s',this.muc_domain)+'</dt>');
+                $('input#show-rooms').show().siblings('span.spinner').remove();
+            },
+
+            updateRoomsList: function (domain) {
+                converse.connection.muc.listRooms(
+                    this.muc_domain,
+                    $.proxy(function (iq) { // Success
+                        var name, jid, i, fragment,
+                            that = this,
+                            $available_chatrooms = this.$el.find('#available-chatrooms');
+                        this.rooms = $(iq).find('query').find('item');
+                        if (this.rooms.length) {
+                            // # For translators: %1$s is a variable and will be
+                            // # replaced with the XMPP server name
+                            $available_chatrooms.html('<dt>'+__('Rooms on %1$s',this.muc_domain)+'</dt>');
+                            fragment = document.createDocumentFragment();
+                            for (i=0; i<this.rooms.length; i++) {
+                                name = Strophe.unescapeNode($(this.rooms[i]).attr('name')||$(this.rooms[i]).attr('jid'));
+                                jid = $(this.rooms[i]).attr('jid');
+                                fragment.appendChild($(this.room_template({
+                                    'name':name,
+                                    'jid':jid
+                                    }))[0]);
+                            }
+                            $available_chatrooms.append(fragment);
+                            $('input#show-rooms').show().siblings('span.spinner').remove();
+                        } else {
+                            this.informNoRoomsFound();
+                        }
+                        return true;
+                    }, this),
+                    $.proxy(function (iq) { // Failure
+                        this.informNoRoomsFound();
+                    }, this));
+            },
+
+            showRooms: function (ev) {
+                var $available_chatrooms = this.$el.find('#available-chatrooms');
+                var $server = this.$el.find('input.new-chatroom-server');
+                var server = $server.val();
+                if (!server) {
+                    $server.addClass('error');
                     return;
                     return;
                 }
                 }
-                var $nick = this.$el.find('input.new-chatroom-nick');
-                if (! $nick.is(':focus')) {
-                    $nick.val(model.get('fullname'));
+                this.$el.find('input.new-chatroom-name').removeClass('error');
+                $server.removeClass('error');
+                $available_chatrooms.empty();
+                $('input#show-rooms').hide().after('<span class="spinner"/>');
+                this.muc_domain = server;
+                this.updateRoomsList();
+            },
+
+            showRoomInfo: function (ev) {
+                var target = ev.target,
+                    $dd = $(target).parent('dd'),
+                    $div = $dd.find('div.room-info');
+                if ($div.length) {
+                    $div.remove();
+                } else {
+                    $dd.find('span.spinner').remove();
+                    $dd.append('<span class="spinner hor_centered"/>');
+                    converse.connection.disco.info(
+                        $(target).attr('data-room-jid'),
+                        null,
+                        $.proxy(function (stanza) {
+                            var $stanza = $(stanza);
+                            // All MUC features found here: http://xmpp.org/registrar/disco-features.html
+                            $dd.find('span.spinner').replaceWith(
+                                this.room_description_template({
+                                    'desc': $stanza.find('field[var="muc#roominfo_description"] value').text(),
+                                    'occ': $stanza.find('field[var="muc#roominfo_occupants"] value').text(),
+                                    'hidden': $stanza.find('feature[var="muc_hidden"]').length,
+                                    'membersonly': $stanza.find('feature[var="muc_membersonly"]').length,
+                                    'moderated': $stanza.find('feature[var="muc_moderated"]').length,
+                                    'nonanonymous': $stanza.find('feature[var="muc_nonanonymous"]').length,
+                                    'open': $stanza.find('feature[var="muc_open"]').length,
+                                    'passwordprotected': $stanza.find('feature[var="muc_passwordprotected"]').length,
+                                    'persistent': $stanza.find('feature[var="muc_persistent"]').length,
+                                    'publicroom': $stanza.find('feature[var="muc_public"]').length,
+                                    'semianonymous': $stanza.find('feature[var="muc_semianonymous"]').length,
+                                    'temporary': $stanza.find('feature[var="muc_temporary"]').length,
+                                    'unmoderated': $stanza.find('feature[var="muc_unmoderated"]').length
+                                }));
+                        }, this));
                 }
                 }
-            }, this));
-        },
-
-        updateRoomsList: function (domain) {
-            converse.connection.muc.listRooms(
-                this.muc_domain,
-                $.proxy(function (iq) { // Success
-                    var name, jid, i, fragment,
-                        that = this,
-                        $available_chatrooms = this.$el.find('#available-chatrooms');
-                    this.rooms = $(iq).find('query').find('item');
-                    if (this.rooms.length) {
-                        $available_chatrooms.html('<dt>Rooms on '+this.muc_domain+'</dt>');
-                        fragment = document.createDocumentFragment();
-                        for (i=0; i<this.rooms.length; i++) {
-                            name = Strophe.unescapeNode($(this.rooms[i]).attr('name')||$(this.rooms[i]).attr('jid'));
-                            jid = $(this.rooms[i]).attr('jid');
-                            fragment.appendChild($(this.room_template({
-                                'name':name,
-                                'jid':jid
-                                }))[0]);
-                        }
-                        $available_chatrooms.append(fragment);
-                        $('input#show-rooms').show().siblings('span.spinner').remove();
+            },
+
+            createChatRoom: function (ev) {
+                ev.preventDefault();
+                var name, $name,
+                    server, $server,
+                    jid,
+                    $nick = this.$el.find('input.new-chatroom-nick'),
+                    nick = $nick.val(),
+                    chatroom;
+
+                if (!nick) { $nick.addClass('error'); }
+                else { $nick.removeClass('error'); }
+
+                if (ev.type === 'click') {
+                    jid = $(ev.target).attr('data-room-jid');
+                } else {
+                    $name = this.$el.find('input.new-chatroom-name');
+                    $server= this.$el.find('input.new-chatroom-server');
+                    server = $server.val();
+                    name = $name.val().trim().toLowerCase();
+                    $name.val(''); // Clear the input
+                    if (name && server) {
+                        jid = Strophe.escapeNode(name) + '@' + server;
+                        $name.removeClass('error');
+                        $server.removeClass('error');
+                        this.muc_domain = server;
                     } else {
                     } else {
-                        $available_chatrooms.html('<dt>No rooms on '+this.muc_domain+'</dt>');
-                        $('input#show-rooms').show().siblings('span.spinner').remove();
+                        if (!name) { $name.addClass('error'); }
+                        if (!server) { $server.addClass('error'); }
+                        return;
                     }
                     }
-                    return true;
-                }, this),
-                $.proxy(function (iq) { // Failure
-                    var $available_chatrooms = this.$el.find('#available-chatrooms');
-                    $available_chatrooms.html('<dt>No rooms on '+this.muc_domain+'</dt>');
-                    $('input#show-rooms').show().siblings('span.spinner').remove();
-                }, this));
-        },
-
-        showRooms: function (ev) {
-            var $available_chatrooms = this.$el.find('#available-chatrooms');
-            var $server = this.$el.find('input.new-chatroom-server');
-            var server = $server.val();
-            if (!server) {
-                $server.addClass('error');
-                return;
+                }
+                if (!nick) { return; }
+                chatroom = converse.chatboxes.createChatBox({
+                    'id': jid,
+                    'jid': jid,
+                    'name': Strophe.unescapeNode(Strophe.getNodeFromJid(jid)),
+                    'nick': nick,
+                    'chatroom': true,
+                    'box_id' : hex_sha1(jid)
+                });
+                if (!chatroom.get('connected')) {
+                    converse.chatboxesview.views[jid].connect(null);
+                }
             }
             }
-            this.$el.find('input.new-chatroom-name').removeClass('error');
-            $server.removeClass('error');
-            $available_chatrooms.empty();
-            $('input#show-rooms').hide().after('<span class="spinner"/>');
-            this.muc_domain = server;
-            this.updateRoomsList();
-        },
-
-        showRoomInfo: function (ev) {
-            var target = ev.target,
-                $dd = $(target).parent('dd'),
-                $div = $dd.find('div.room-info');
-            if ($div.length) {
-                $div.remove();
-            } else {
-                $dd.find('span.spinner').remove();
-                $dd.append('<span class="spinner hor_centered"/>');
-                converse.connection.disco.info(
-                    $(target).attr('data-room-jid'),
-                    null,
-                    $.proxy(function (stanza) {
-                        var $stanza = $(stanza);
-                        // All MUC features found here: http://xmpp.org/registrar/disco-features.html
-                        $dd.find('span.spinner').replaceWith(
-                            this.room_description_template({
-                                'desc': $stanza.find('field[var="muc#roominfo_description"] value').text(),
-                                'occ': $stanza.find('field[var="muc#roominfo_occupants"] value').text(),
-                                'hidden': $stanza.find('feature[var="muc_hidden"]').length,
-                                'membersonly': $stanza.find('feature[var="muc_membersonly"]').length,
-                                'moderated': $stanza.find('feature[var="muc_moderated"]').length,
-                                'nonanonymous': $stanza.find('feature[var="muc_nonanonymous"]').length,
-                                'open': $stanza.find('feature[var="muc_open"]').length,
-                                'passwordprotected': $stanza.find('feature[var="muc_passwordprotected"]').length,
-                                'persistent': $stanza.find('feature[var="muc_persistent"]').length,
-                                'publicroom': $stanza.find('feature[var="muc_public"]').length,
-                                'semianonymous': $stanza.find('feature[var="muc_semianonymous"]').length,
-                                'temporary': $stanza.find('feature[var="muc_temporary"]').length,
-                                'unmoderated': $stanza.find('feature[var="muc_unmoderated"]').length
-                            }));
-                    }, this));
-            }
-        },
-
-        createChatRoom: function (ev) {
-            ev.preventDefault();
-            var name, $name,
-                server, $server,
-                jid,
-                $nick = this.$el.find('input.new-chatroom-nick'),
-                nick = $nick.val(),
-                chatroom;
-
-            if (!nick) { $nick.addClass('error'); }
-            else { $nick.removeClass('error'); }
-
-            if (ev.type === 'click') {
-                jid = $(ev.target).attr('data-room-jid');
-            } else {
-                $name = this.$el.find('input.new-chatroom-name');
-                $server= this.$el.find('input.new-chatroom-server');
-                server = $server.val();
-                name = $name.val().trim().toLowerCase();
-                $name.val(''); // Clear the input
-                if (name && server) {
-                    jid = Strophe.escapeNode(name) + '@' + server;
-                    $name.removeClass('error');
-                    $server.removeClass('error');
-                    this.muc_domain = server;
-                } else {
-                    if (!name) { $name.addClass('error'); }
-                    if (!server) { $server.addClass('error'); }
-                    return;
-                }
-            }
-            if (!nick) { return; }
-            chatroom = converse.chatboxes.createChatBox({
-                'id': jid,
-                'jid': jid,
-                'name': Strophe.unescapeNode(Strophe.getNodeFromJid(jid)),
-                'nick': nick,
-                'chatroom': true,
-                'box_id' : hex_sha1(jid)
-            });
-            if (!chatroom.get('connected')) {
-                converse.chatboxesview.views[jid].connect(null);
-            }
-        }
-    });
-
-    converse.ControlBoxView = converse.ChatBoxView.extend({
-        tagName: 'div',
-        className: 'chatbox',
-        id: 'controlbox',
-        events: {
-            'click a.close-chatbox-button': 'closeChat',
-            'click ul#controlbox-tabs li a': 'switchTab'
-        },
-
-        initialize: function () {
-            this.$el.appendTo(converse.chatboxesview.$el);
-            this.model.on('change', $.proxy(function (item, changed) {
-                var i;
-                if (_.has(item.changed, 'connected')) {
-                    this.render();
-                    converse.features.on('add', $.proxy(this.featureAdded, this));
-                    // Features could have been added before the controlbox was
-                    // initialized. Currently we're only interested in MUC
-                    var feature = converse.features.findWhere({'var': 'http://jabber.org/protocol/muc'});
-                    if (feature) {
-                        this.featureAdded(feature);
+        });
+
+        this.ControlBoxView = converse.ChatBoxView.extend({
+            tagName: 'div',
+            className: 'chatbox',
+            id: 'controlbox',
+            events: {
+                'click a.close-chatbox-button': 'closeChat',
+                'click ul#controlbox-tabs li a': 'switchTab'
+            },
+
+            initialize: function () {
+                this.$el.appendTo(converse.chatboxesview.$el);
+                this.model.on('change', $.proxy(function (item, changed) {
+                    var i;
+                    if (_.has(item.changed, 'connected')) {
+                        this.render();
+                        converse.features.on('add', $.proxy(this.featureAdded, this));
+                        // Features could have been added before the controlbox was
+                        // initialized. Currently we're only interested in MUC
+                        var feature = converse.features.findWhere({'var': 'http://jabber.org/protocol/muc'});
+                        if (feature) {
+                            this.featureAdded(feature);
+                        }
                     }
                     }
-                }
-                if (_.has(item.changed, 'visible')) {
-                    if (item.changed.visible === true) {
-                        this.show();
+                    if (_.has(item.changed, 'visible')) {
+                        if (item.changed.visible === true) {
+                            this.show();
+                        }
                     }
                     }
+                }, this));
+                this.model.on('show', this.show, this);
+                this.model.on('destroy', this.hide, this);
+                this.model.on('hide', this.hide, this);
+                if (this.model.get('visible')) {
+                    this.show();
                 }
                 }
-            }, this));
-            this.model.on('show', this.show, this);
-            this.model.on('destroy', this.hide, this);
-            this.model.on('hide', this.hide, this);
-            if (this.model.get('visible')) {
-                this.show();
-            }
-        },
+            },
 
 
-        featureAdded: function (feature) {
-            if (feature.get('var') == 'http://jabber.org/protocol/muc') {
-                this.roomspanel.muc_domain = feature.get('from');
-                var $server= this.$el.find('input.new-chatroom-server');
-                if (! $server.is(':focus')) {
-                    $server.val(this.roomspanel.muc_domain);
-                }
-                if (converse.auto_list_rooms) {
-                    this.roomspanel.trigger('update-rooms-list');
+            featureAdded: function (feature) {
+                if (feature.get('var') == 'http://jabber.org/protocol/muc') {
+                    this.roomspanel.muc_domain = feature.get('from');
+                    var $server= this.$el.find('input.new-chatroom-server');
+                    if (! $server.is(':focus')) {
+                        $server.val(this.roomspanel.muc_domain);
+                    }
+                    if (converse.auto_list_rooms) {
+                        this.roomspanel.trigger('update-rooms-list');
+                    }
                 }
                 }
-            }
-        },
-
-        template: _.template(
-            '<div class="chat-head oc-chat-head">'+
-                '<ul id="controlbox-tabs"></ul>'+
-                '<a class="close-chatbox-button">X</a>'+
-            '</div>'+
-            '<div id="controlbox-panes"></div>'
-        ),
-
-        switchTab: function (ev) {
-            ev.preventDefault();
-            var $tab = $(ev.target),
-                $sibling = $tab.parent().siblings('li').children('a'),
-                $tab_panel = $($tab.attr('href')),
-                $sibling_panel = $($sibling.attr('href'));
-
-            $sibling_panel.fadeOut('fast', function () {
-                $sibling.removeClass('current');
-                $tab.addClass('current');
-                $tab_panel.fadeIn('fast', function () {
+            },
+
+            template: _.template(
+                '<div class="chat-head oc-chat-head">'+
+                    '<ul id="controlbox-tabs"></ul>'+
+                    '<a class="close-chatbox-button">X</a>'+
+                '</div>'+
+                '<div id="controlbox-panes"></div>'
+            ),
+
+            switchTab: function (ev) {
+                ev.preventDefault();
+                var $tab = $(ev.target),
+                    $sibling = $tab.parent().siblings('li').children('a'),
+                    $tab_panel = $($tab.attr('href')),
+                    $sibling_panel = $($sibling.attr('href'));
+
+                $sibling_panel.fadeOut('fast', function () {
+                    $sibling.removeClass('current');
+                    $tab.addClass('current');
+                    $tab_panel.fadeIn('fast', function () {
+                    });
                 });
                 });
-            });
-        },
-
-        addHelpMessages: function (msgs) {
-            // Override addHelpMessages in ChatBoxView, for now do nothing.
-            return;
-        },
-
-        render: function () {
-            this.$el.html(this.template(this.model.toJSON()));
-            if ((!converse.prebind) && (!converse.connection)) {
-                // Add login panel if the user still has to authenticate
-                this.loginpanel = new converse.LoginPanel();
-                this.loginpanel.$parent = this.$el;
-                this.loginpanel.render();
-            } else {
-                this.contactspanel = new converse.ContactsPanel();
-                this.contactspanel.$parent = this.$el;
-                this.contactspanel.render();
-                this.roomspanel = new converse.RoomsPanel();
-                this.roomspanel.$parent = this.$el;
-                this.roomspanel.render();
-            }
-            return this;
-        }
-    });
-
-    converse.ChatRoomView = converse.ChatBoxView.extend({
-        length: 300,
-        tagName: 'div',
-        className: 'chatroom',
-        events: {
-            'click a.close-chatbox-button': 'closeChat',
-            'click a.configure-chatroom-button': 'configureChatRoom',
-            'keypress textarea.chat-textarea': 'keyPressed'
-        },
-        info_template: _.template('<div class="chat-info">{{message}}</div>'),
-
-        sendChatRoomMessage: function (body) {
-            var match = body.replace(/^\s*/, "").match(/^\/(.*?)(?: (.*))?$/) || [false],
-                $chat_content;
-            switch (match[1]) {
-                case 'msg':
-                    // TODO: Private messages
-                    break;
-                case 'clear':
-                    this.$el.find('.chat-content').empty();
-                    break;
-                case 'topic':
-                    converse.connection.muc.setTopic(this.model.get('jid'), match[2]);
-                    break;
-                case 'kick':
-                    converse.connection.muc.kick(this.model.get('jid'), match[2]);
-                    break;
-                case 'ban':
-                    converse.connection.muc.ban(this.model.get('jid'), match[2]);
-                    break;
-                case 'op':
-                    converse.connection.muc.op(this.model.get('jid'), match[2]);
-                    break;
-                case 'deop':
-                    converse.connection.muc.deop(this.model.get('jid'), match[2]);
-                    break;
-                case 'help':
-                    $chat_content = this.$el.find('.chat-content');
-                    msgs = [
-                        '<strong>/help</strong>: Show this menu',
-                        '<strong>/me</strong>: Write in the third person',
-                        '<strong>/topic</strong>: Set chatroom topic',
-                        '<strong>/kick</strong>: Kick user from chatroom',
-                        '<strong>/ban</strong>: Ban user from chatroom',
-                        '<strong>/clear</strong>: Remove messages'
-                        ];
-                    this.addHelpMessages(msgs);
-                    break;
-                default:
-                    this.last_msgid = converse.connection.muc.groupchat(this.model.get('jid'), body);
-                break;
-            }
-        },
-
-        template: _.template(
-            '<div class="chat-head chat-head-chatroom">' +
-                '<a class="close-chatbox-button">X</a>' +
-                '<a class="configure-chatroom-button" style="display:none">&nbsp;</a>' +
-                '<div class="chat-title"> {{ name }} </div>' +
-                '<p class="chatroom-topic"><p/>' +
-            '</div>' +
-            '<div class="chat-body">' +
-            '<img class="spinner centered" src="images/spinner.gif"/>' +
-            '</div>'),
-
-        chatarea_template: _.template(
-            '<div class="chat-area">' +
-                '<div class="chat-content"></div>' +
-                '<form class="sendXMPPMessage" action="" method="post">' +
-                    '<textarea type="text" class="chat-textarea" ' +
-                        'placeholder="Message"/>' +
-                '</form>' +
-            '</div>' +
-            '<div class="participants">' +
-                '<ul class="participant-list"></ul>' +
-            '</div>'
-        ),
-
-        render: function () {
-            this.$el.attr('id', this.model.get('box_id'))
-                    .html(this.template(this.model.toJSON()));
-            return this;
-        },
+            },
 
 
-        renderChatArea: function () {
-            if (!this.$el.find('.chat-area').length) { 
-                this.$el.find('.chat-body').empty().append(this.chatarea_template());
-            }
-            return this;
-        },
-
-        connect: function (password) {
-            if (_.has(converse.connection.muc.rooms, this.model.get('jid'))) {
-                // If the room exists, it already has event listeners, so we
-                // doing add them again.
-                converse.connection.muc.join(
-                    this.model.get('jid'), this.model.get('nick'), null, null, null, password);
-            } else {
-                converse.connection.muc.join(
-                    this.model.get('jid'),
-                    this.model.get('nick'),
-                    $.proxy(this.onChatRoomMessage, this),
-                    $.proxy(this.onChatRoomPresence, this),
-                    $.proxy(this.onChatRoomRoster, this),
-                    password);
-            }
-        },
-
-        initialize: function () {
-            this.connect(null);
-            this.model.messages.on('add', this.showMessage, this);
-            this.model.on('destroy', function (model, response, options) {
-                this.$el.hide('fast');
-                converse.connection.muc.leave(
-                    this.model.get('jid'),
-                    this.model.get('nick'),
-                    $.proxy(this.onLeave, this),
-                    undefined);
-            },
-            this);
-            this.$el.appendTo(converse.chatboxesview.$el);
-            this.render().show().model.messages.fetch({add: true});
-        },
-
-        onLeave: function () {
-            this.model.set('connected', false);
-        },
-
-        form_input_template: _.template('<label>{{label}}<input name="{{name}}" type="{{type}}" value="{{value}}"></label>'),
-        select_option_template: _.template('<option value="{{value}}">{{label}}</option>'),
-        form_select_template: _.template('<label>{{label}}<select name="{{name}}">{{options}}</select></label>'),
-        form_checkbox_template: _.template('<label>{{label}}<input name="{{name}}" type="{{type}}" {{checked}}"></label>'),
-
-        renderConfigurationForm: function (stanza) {
-            var $form= this.$el.find('form.chatroom-form'),
-                $stanza = $(stanza),
-                $fields = $stanza.find('field'),
-                title = $stanza.find('title').text(),
-                instructions = $stanza.find('instructions').text(),
-                i, j, options=[];
-            var input_types = {
-                'text-private': 'password',
-                'text-single': 'textline',
-                'boolean': 'checkbox',
-                'hidden': 'hidden',
-                'list-single': 'dropdown'
-            };
-            $form.find('img.spinner').remove();
-            $form.append($('<legend>').text(title));
-            if (instructions != title) {
-                $form.append($('<p>').text(instructions));
-            }
-            for (i=0; i<$fields.length; i++) {
-                $field = $($fields[i]);
-                if ($field.attr('type') == 'list-single') {
-                    options = [];
-                    $options = $field.find('option');
-                    for (j=0; j<$options.length; j++) {
-                        options.push(this.select_option_template({
-                            value: $($options[j]).find('value').text(),
-                            label: $($options[j]).attr('label')
-                        }));
-                    }
-                    $form.append(this.form_select_template({
-                        name: $field.attr('var'),
-                        label: $field.attr('label'),
-                        options: options.join('')
-                    }));
-                } else if ($field.attr('type') == 'boolean') {
-                    $form.append(this.form_checkbox_template({
-                        name: $field.attr('var'),
-                        type: input_types[$field.attr('type')],
-                        label: $field.attr('label') || '',
-                        checked: $field.find('value').text() === "1" && 'checked="1"' || ''
-                    }));
+            addHelpMessages: function (msgs) {
+                // Override addHelpMessages in ChatBoxView, for now do nothing.
+                return;
+            },
+
+            render: function () {
+                this.$el.html(this.template(this.model.toJSON()));
+                if ((!converse.prebind) && (!converse.connection)) {
+                    // Add login panel if the user still has to authenticate
+                    this.loginpanel = new converse.LoginPanel();
+                    this.loginpanel.$parent = this.$el;
+                    this.loginpanel.render();
                 } else {
                 } else {
-                    $form.append(this.form_input_template({
-                        name: $field.attr('var'),
-                        type: input_types[$field.attr('type')],
-                        label: $field.attr('label') || '',
-                        value: $field.find('value').text()
-                    }));
+                    this.contactspanel = new converse.ContactsPanel();
+                    this.contactspanel.$parent = this.$el;
+                    this.contactspanel.render();
+                    converse.xmppstatus = new converse.XMPPStatus();
+                    converse.xmppstatus.localStorage = new Backbone.LocalStorage(
+                        hex_sha1('converse.xmppstatus-'+converse.bare_jid));
+                    converse.xmppstatus.fetch({
+                        success: function (xmppstatus, resp) {
+                            if (!xmppstatus.get('fullname')) {
+                                converse.getVCard(
+                                    null, // No 'to' attr when getting one's own vCard
+                                    function (jid, fullname, image, image_type, url) {
+                                        converse.xmppstatus.save({'fullname': fullname});
+                                    }
+                                );
+                            }
+                        }
+                    });
+                    converse.xmppstatusview = new converse.XMPPStatusView({'model': converse.xmppstatus});
+                    converse.xmppstatusview.render();
+                    this.roomspanel = new converse.RoomsPanel();
+                    this.roomspanel.$parent = this.$el;
+                    this.roomspanel.render();
                 }
                 }
+                return this;
             }
             }
-            $form.append('<input type="submit" value="Save"/>');
-            $form.append('<input type="button" value="Cancel"/>');
-            $form.on('submit', $.proxy(this.saveConfiguration, this));
-            $form.find('input[type=button]').on('click', $.proxy(this.cancelConfiguration, this));
-        },
+        });
 
 
-        field_template: _.template('<field var="{{name}}"><value>{{value}}</value></field>'),
+        this.ChatRoomView = converse.ChatBoxView.extend({
+            length: 300,
+            tagName: 'div',
+            className: 'chatroom',
+            events: {
+                'click a.close-chatbox-button': 'closeChat',
+                'click a.configure-chatroom-button': 'configureChatRoom',
+                'keypress textarea.chat-textarea': 'keyPressed'
+            },
+            info_template: _.template('<div class="chat-info">{{message}}</div>'),
+
+            sendChatRoomMessage: function (body) {
+                var match = body.replace(/^\s*/, "").match(/^\/(.*?)(?: (.*))?$/) || [false],
+                    $chat_content;
+                switch (match[1]) {
+                    case 'msg':
+                        // TODO: Private messages
+                        break;
+                    case 'clear':
+                        this.$el.find('.chat-content').empty();
+                        break;
+                    case 'topic':
+                        converse.connection.muc.setTopic(this.model.get('jid'), match[2]);
+                        break;
+                    case 'kick':
+                        converse.connection.muc.kick(this.model.get('jid'), match[2]);
+                        break;
+                    case 'ban':
+                        converse.connection.muc.ban(this.model.get('jid'), match[2]);
+                        break;
+                    case 'op':
+                        converse.connection.muc.op(this.model.get('jid'), match[2]);
+                        break;
+                    case 'deop':
+                        converse.connection.muc.deop(this.model.get('jid'), match[2]);
+                        break;
+                    case 'help':
+                        $chat_content = this.$el.find('.chat-content');
+                        msgs = [
+                            '<strong>/help</strong>:'+__('Show this menu')+'',
+                            '<strong>/me</strong>:'+__('Write in the third person')+'',
+                            '<strong>/topic</strong>:'+__('Set chatroom topic')+'',
+                            '<strong>/kick</strong>:'+__('Kick user from chatroom')+'',
+                            '<strong>/ban</strong>:'+__('Ban user from chatroom')+'',
+                            '<strong>/clear</strong>:'+__('Remove messages')+''
+                            ];
+                        this.addHelpMessages(msgs);
+                        break;
+                    default:
+                        this.last_msgid = converse.connection.muc.groupchat(this.model.get('jid'), body);
+                    break;
+                }
+            },
 
 
-        saveConfiguration: function (ev) {
-            ev.preventDefault();
-            var that = this;
-            var $inputs = $(ev.target).find(':input:not([type=button]):not([type=submit])'),
-                count = $inputs.length,
-                configArray = [];
-            $inputs.each(function () {
-                var $input = $(this), value;
-                if ($input.is('[type=checkbox]')) {
-                    value = $input.is(':checked') && 1 || 0;
+            template: _.template(
+                '<div class="chat-head chat-head-chatroom">' +
+                    '<a class="close-chatbox-button">X</a>' +
+                    '<a class="configure-chatroom-button" style="display:none">&nbsp;</a>' +
+                    '<div class="chat-title"> {{ name }} </div>' +
+                    '<p class="chatroom-topic"><p/>' +
+                '</div>' +
+                '<div class="chat-body">' +
+                '<img class="spinner centered" src="images/spinner.gif"/>' +
+                '</div>'),
+
+            chatarea_template: _.template(
+                '<div class="chat-area">' +
+                    '<div class="chat-content"></div>' +
+                    '<form class="sendXMPPMessage" action="" method="post">' +
+                        '<textarea type="text" class="chat-textarea" ' +
+                            'placeholder="'+__('Message')+'"/>' +
+                    '</form>' +
+                '</div>' +
+                '<div class="participants">' +
+                    '<ul class="participant-list"></ul>' +
+                '</div>'
+            ),
+
+            render: function () {
+                this.$el.attr('id', this.model.get('box_id'))
+                        .html(this.template(this.model.toJSON()));
+                return this;
+            },
+
+            renderChatArea: function () {
+                if (!this.$el.find('.chat-area').length) {
+                    this.$el.find('.chat-body').empty().append(this.chatarea_template());
+                }
+                return this;
+            },
+
+            connect: function (password) {
+                if (_.has(converse.connection.muc.rooms, this.model.get('jid'))) {
+                    // If the room exists, it already has event listeners, so we
+                    // doing add them again.
+                    converse.connection.muc.join(
+                        this.model.get('jid'), this.model.get('nick'), null, null, null, password);
                 } else {
                 } else {
-                    value = $input.val();
-                }
-                var cnode = $(that.field_template({
-                    name: $input.attr('name'),
-                    value: value
-                }))[0];
-                configArray.push(cnode);
-                if (!--count) {
-                    converse.connection.muc.saveConfiguration(
-                        that.model.get('jid'),
-                        configArray,
-                        $.proxy(that.onConfigSaved, that),
-                        $.proxy(that.onErrorConfigSaved, that)
-                    );
+                    converse.connection.muc.join(
+                        this.model.get('jid'),
+                        this.model.get('nick'),
+                        $.proxy(this.onChatRoomMessage, this),
+                        $.proxy(this.onChatRoomPresence, this),
+                        $.proxy(this.onChatRoomRoster, this),
+                        password);
                 }
                 }
-            });
-            this.$el.find('div.chatroom-form-container').hide(
-                function () {
-                    $(this).remove();
-                    that.$el.find('.chat-area').show();
-                    that.$el.find('.participants').show();
-                });
-        },
+            },
 
 
-        onConfigSaved: function (stanza) {
-            // XXX
-        },
+            initialize: function () {
+                this.connect(null);
+                this.model.messages.on('add', this.showMessage, this);
+                this.model.on('destroy', function (model, response, options) {
+                    this.$el.hide('fast');
+                    converse.connection.muc.leave(
+                        this.model.get('jid'),
+                        this.model.get('nick'),
+                        $.proxy(this.onLeave, this),
+                        undefined);
+                },
+                this);
+                this.$el.appendTo(converse.chatboxesview.$el);
+                this.render().show().model.messages.fetch({add: true});
+            },
 
 
-        onErrorConfigSaved: function (stanza) {
-            this.insertStatusNotification("An error occurred while trying to save the form.");
-        },
+            onLeave: function () {
+                this.model.set('connected', false);
+            },
 
 
-        cancelConfiguration: function (ev) {
-            ev.preventDefault();
-            var that = this;
-            this.$el.find('div.chatroom-form-container').hide(
-                function () {
-                    $(this).remove();
-                    that.$el.find('.chat-area').show();
-                    that.$el.find('.participants').show();
+            form_input_template: _.template('<label>{{label}}<input name="{{name}}" type="{{type}}" value="{{value}}"></label>'),
+            select_option_template: _.template('<option value="{{value}}">{{label}}</option>'),
+            form_select_template: _.template('<label>{{label}}<select name="{{name}}">{{options}}</select></label>'),
+            form_checkbox_template: _.template('<label>{{label}}<input name="{{name}}" type="{{type}}" {{checked}}"></label>'),
+
+            renderConfigurationForm: function (stanza) {
+                var $form= this.$el.find('form.chatroom-form'),
+                    $stanza = $(stanza),
+                    $fields = $stanza.find('field'),
+                    title = $stanza.find('title').text(),
+                    instructions = $stanza.find('instructions').text(),
+                    i, j, options=[];
+                var input_types = {
+                    'text-private': 'password',
+                    'text-single': 'textline',
+                    'boolean': 'checkbox',
+                    'hidden': 'hidden',
+                    'list-single': 'dropdown'
+                };
+                $form.find('img.spinner').remove();
+                $form.append($('<legend>').text(title));
+                if (instructions != title) {
+                    $form.append($('<p>').text(instructions));
+                }
+                for (i=0; i<$fields.length; i++) {
+                    $field = $($fields[i]);
+                    if ($field.attr('type') == 'list-single') {
+                        options = [];
+                        $options = $field.find('option');
+                        for (j=0; j<$options.length; j++) {
+                            options.push(this.select_option_template({
+                                value: $($options[j]).find('value').text(),
+                                label: $($options[j]).attr('label')
+                            }));
+                        }
+                        $form.append(this.form_select_template({
+                            name: $field.attr('var'),
+                            label: $field.attr('label'),
+                            options: options.join('')
+                        }));
+                    } else if ($field.attr('type') == 'boolean') {
+                        $form.append(this.form_checkbox_template({
+                            name: $field.attr('var'),
+                            type: input_types[$field.attr('type')],
+                            label: $field.attr('label') || '',
+                            checked: $field.find('value').text() === "1" && 'checked="1"' || ''
+                        }));
+                    } else {
+                        $form.append(this.form_input_template({
+                            name: $field.attr('var'),
+                            type: input_types[$field.attr('type')],
+                            label: $field.attr('label') || '',
+                            value: $field.find('value').text()
+                        }));
+                    }
+                }
+                $form.append('<input type="submit" value="'+__('Save')+'"/>');
+                $form.append('<input type="button" value="'+__('Cancel')+'"/>');
+                $form.on('submit', $.proxy(this.saveConfiguration, this));
+                $form.find('input[type=button]').on('click', $.proxy(this.cancelConfiguration, this));
+            },
+
+            field_template: _.template('<field var="{{name}}"><value>{{value}}</value></field>'),
+
+            saveConfiguration: function (ev) {
+                ev.preventDefault();
+                var that = this;
+                var $inputs = $(ev.target).find(':input:not([type=button]):not([type=submit])'),
+                    count = $inputs.length,
+                    configArray = [];
+                $inputs.each(function () {
+                    var $input = $(this), value;
+                    if ($input.is('[type=checkbox]')) {
+                        value = $input.is(':checked') && 1 || 0;
+                    } else {
+                        value = $input.val();
+                    }
+                    var cnode = $(that.field_template({
+                        name: $input.attr('name'),
+                        value: value
+                    }))[0];
+                    configArray.push(cnode);
+                    if (!--count) {
+                        converse.connection.muc.saveConfiguration(
+                            that.model.get('jid'),
+                            configArray,
+                            $.proxy(that.onConfigSaved, that),
+                            $.proxy(that.onErrorConfigSaved, that)
+                        );
+                    }
                 });
                 });
-        },
+                this.$el.find('div.chatroom-form-container').hide(
+                    function () {
+                        $(this).remove();
+                        that.$el.find('.chat-area').show();
+                        that.$el.find('.participants').show();
+                    });
+            },
 
 
-        configureChatRoom: function (ev) {
-            ev.preventDefault();
-            if (this.$el.find('div.chatroom-form-container').length) {
-                return;
-            }
-            this.$el.find('.chat-area').hide();
-            this.$el.find('.participants').hide();
-            this.$el.find('.chat-body').append(
-                $('<div class="chatroom-form-container">'+
-                    '<form class="chatroom-form">'+
-                    '<img class="spinner centered" src="images/spinner.gif"/>'+
-                    '</form>'+
-                  '</div>'));
-            converse.connection.muc.configure(
-                this.model.get('jid'),
-                $.proxy(this.renderConfigurationForm, this)
-            );
-        },
-
-        submitPassword: function (ev) {
-            ev.preventDefault();
-            var password = this.$el.find('.chatroom-form').find('input[type=password]').val();
-            this.$el.find('.chatroom-form-container').replaceWith(
-                '<img class="spinner centered" src="images/spinner.gif"/>');
-            this.connect(password);
-        },
-
-        renderPasswordForm: function () {
-            this.$el.find('img.centered.spinner').remove();
-            this.$el.find('.chat-body').append(
-                $('<div class="chatroom-form-container">'+
-                    '<form class="chatroom-form">'+
-                        '<legend>This chat room requires a password</legend>' +
-                        '<label>Password: <input type="password" name="password"/></label>' +
-                        '<input type="submit"/>' +
-                    '</form>'+
-                  '</div>'));
-            this.$el.find('.chatroom-form').on('submit', $.proxy(this.submitPassword, this));
-        },
-
-        showDisconnectMessage: function (msg) {
-            this.$el.find('.chat-area').remove();
-            this.$el.find('.participants').remove();
-            this.$el.find('img.centered.spinner').remove();
-            this.$el.find('.chat-body').append($('<p>'+msg+'</p>'));
-        },
-
-        infoMessages: {
-            100: 'This room is not anonymous',
-            102: 'This room now shows unavailable members',
-            103: 'This room does not show unavailable members',
-            104: 'Non-privacy-related room configuration has changed',
-            170: 'Room logging is now enabled',
-            171: 'Room logging is now disabled',
-            172: 'This room is now non-anonymous',
-            173: 'This room is now semi-anonymous',
-            174: 'This room is now fully-anonymous',
-            201: 'A new room has been created',
-            210: 'Your nickname has been changed'
-        },
-
-        actionInfoMessages: {
-            301: ' has been banned',
-            307: ' has been kicked out',
-            321: " has been removed because of an affiliation change",
-            322: " has been removed for not being a member"
-        },
-
-        disconnectMessages: {
-            301: 'You have been banned from this room',
-            307: 'You have been kicked from this room',
-            321: "You have been removed from this room because of an affiliation change",
-            322: "You have been removed from this room because the room" +
-                "has changed to members-only and you're not a member",
-            332: "You have been removed from this room because the MUC " +
-                "(Multi-user chat) service is being shut down."
-        },
-
-        showStatusMessages: function ($el, is_self) {
-            /* Check for status codes and communicate their purpose to the user
-             * See: http://xmpp.org/registrar/mucstatus.html
-             */
-            var $chat_content = this.$el.find('.chat-content'),
-                $stats = $el.find('status'),
-                disconnect_msgs = [],
-                info_msgs = [],
-                action_msgs = [],
-                msgs, i;
-            for (i=0; i<$stats.length; i++) {
-                var stat = $stats[i].getAttribute('code');
-                if (is_self) {
-                    if (_.contains(_.keys(this.disconnectMessages), stat)) {
-                        disconnect_msgs.push(this.disconnectMessages[stat]);
+            onConfigSaved: function (stanza) {
+                // XXX
+            },
+
+            onErrorConfigSaved: function (stanza) {
+                this.insertStatusNotification(__("An error occurred while trying to save the form."));
+            },
+
+            cancelConfiguration: function (ev) {
+                ev.preventDefault();
+                var that = this;
+                this.$el.find('div.chatroom-form-container').hide(
+                    function () {
+                        $(this).remove();
+                        that.$el.find('.chat-area').show();
+                        that.$el.find('.participants').show();
+                    });
+            },
+
+            configureChatRoom: function (ev) {
+                ev.preventDefault();
+                if (this.$el.find('div.chatroom-form-container').length) {
+                    return;
+                }
+                this.$el.find('.chat-area').hide();
+                this.$el.find('.participants').hide();
+                this.$el.find('.chat-body').append(
+                    $('<div class="chatroom-form-container">'+
+                        '<form class="chatroom-form">'+
+                        '<img class="spinner centered" src="images/spinner.gif"/>'+
+                        '</form>'+
+                    '</div>'));
+                converse.connection.muc.configure(
+                    this.model.get('jid'),
+                    $.proxy(this.renderConfigurationForm, this)
+                );
+            },
+
+            submitPassword: function (ev) {
+                ev.preventDefault();
+                var password = this.$el.find('.chatroom-form').find('input[type=password]').val();
+                this.$el.find('.chatroom-form-container').replaceWith(
+                    '<img class="spinner centered" src="images/spinner.gif"/>');
+                this.connect(password);
+            },
+
+            renderPasswordForm: function () {
+                this.$el.find('img.centered.spinner').remove();
+                this.$el.find('.chat-body').append(
+                    $('<div class="chatroom-form-container">'+
+                        '<form class="chatroom-form">'+
+                            '<legend>'+__('This chatroom requires a password')+'</legend>' +
+                            '<label>'+__('Password: ')+'<input type="password" name="password"/></label>' +
+                            '<input type="submit" value="'+__('Submit')+'/>' +
+                        '</form>'+
+                    '</div>'));
+                this.$el.find('.chatroom-form').on('submit', $.proxy(this.submitPassword, this));
+            },
+
+            showDisconnectMessage: function (msg) {
+                this.$el.find('.chat-area').remove();
+                this.$el.find('.participants').remove();
+                this.$el.find('img.centered.spinner').remove();
+                this.$el.find('.chat-body').append($('<p>'+msg+'</p>'));
+            },
+
+            infoMessages: {
+                100: __('This room is not anonymous'),
+                102: __('This room now shows unavailable members'),
+                103: __('This room does not show unavailable members'),
+                104: __('Non-privacy-related room configuration has changed'),
+                170: __('Room logging is now enabled'),
+                171: __('Room logging is now disabled'),
+                172: __('This room is now non-anonymous'),
+                173: __('This room is now semi-anonymous'),
+                174: __('This room is now fully-anonymous'),
+                201: __('A new room has been created'),
+                210: __('Your nickname has been changed')
+            },
+
+            actionInfoMessages: {
+                // # For translations: %1$s will be replaced with the user's nickname
+                // # Don't translate "strong"
+                // # Example: <strong>jcbrand</strong> has been banned
+                301: converse.i18n.translate('<strong>%1$s</strong> has been banned'),
+                // # For translations: %1$s will be replaced with the user's nickname
+                // # Don't translate "strong"
+                // # Example: <strong>jcbrand</strong> has been kicked out
+                307: converse.i18n.translate('<strong>%1$s</strong> has been kicked out'),
+                // # For translations: %1$s will be replaced with the user's nickname
+                // # Don't translate "strong"
+                // # Example: <strong>jcbrand</strong> has been removed because of an affiliasion change
+                321: converse.i18n.translate("<strong>%1$s</strong> has been removed because of an affiliation change"),
+                // # For translations: %1$s will be replaced with the user's nickname
+                // # Don't translate "strong"
+                // # Example: <strong>jcbrand</strong> has been removed for not being a member
+                322: converse.i18n.translate("<strong>%1$s</strong> has been removed for not being a member")
+            },
+
+            disconnectMessages: {
+                301: __('You have been banned from this room'),
+                307: __('You have been kicked from this room'),
+                321: __("You have been removed from this room because of an affiliation change"),
+                322: __("You have been removed from this room because the room has changed to members-only and you're not a member"),
+                332: __("You have been removed from this room because the MUC (Multi-user chat) service is being shut down.")
+            },
+
+            showStatusMessages: function ($el, is_self) {
+                /* Check for status codes and communicate their purpose to the user
+                * See: http://xmpp.org/registrar/mucstatus.html
+                */
+                var $chat_content = this.$el.find('.chat-content'),
+                    $stats = $el.find('status'),
+                    disconnect_msgs = [],
+                    info_msgs = [],
+                    action_msgs = [],
+                    msgs, i;
+                for (i=0; i<$stats.length; i++) {
+                    var stat = $stats[i].getAttribute('code');
+                    if (is_self) {
+                        if (_.contains(_.keys(this.disconnectMessages), stat)) {
+                            disconnect_msgs.push(this.disconnectMessages[stat]);
+                        }
+                    } else {
+                        if (_.contains(_.keys(this.infoMessages), stat)) {
+                            info_msgs.push(this.infoMessages[stat]);
+                        } else if (_.contains(_.keys(this.actionInfoMessages), stat)) {
+                            action_msgs.push(
+                                this.actionInfoMessages[stat].fetch(
+                                    Strophe.unescapeNode(Strophe.getResourceFromJid($el.attr('from')))
+                            ));
+                        }
                     }
                     }
-                } else {
-                    if (_.contains(_.keys(this.infoMessages), stat)) {
-                        info_msgs.push(this.infoMessages[stat]);
-                    } else if (_.contains(_.keys(this.actionInfoMessages), stat)) {
-                        action_msgs.push(
-                            '<strong>'+
-                                Strophe.unescapeNode(Strophe.getResourceFromJid($el.attr('from')))+
-                            '</strong>'+
-                             this.actionInfoMessages[stat]);
+                }
+                if (disconnect_msgs.length > 0) {
+                    for (i=0; i<disconnect_msgs.length; i++) {
+                        this.showDisconnectMessage(disconnect_msgs[i]);
                     }
                     }
+                    this.model.set('connected', false);
+                    return;
                 }
                 }
-            }
-            if (disconnect_msgs.length > 0) {
-                for (i=0; i<disconnect_msgs.length; i++) {
-                    this.showDisconnectMessage(disconnect_msgs[i]);
+                this.renderChatArea();
+                for (i=0; i<info_msgs.length; i++) {
+                    $chat_content.append(this.info_template({message: info_msgs[i]}));
                 }
                 }
-                this.model.set('connected', false)
-                return;
-            } 
-            this.renderChatArea();
-            for (i=0; i<info_msgs.length; i++) {
-                $chat_content.append(this.info_template({message: info_msgs[i]}));
-            }
-            for (i=0; i<action_msgs.length; i++) {
-                $chat_content.append(this.info_template({message: action_msgs[i]}));
-            }
-            this.scrollDown();
-        },
-
-        showErrorMessage: function ($error, room) {
-            var $chat_content = this.$el.find('.chat-content');
-            // We didn't enter the room, so we must remove it from the MUC
-            // add-on
-            converse.connection.muc.removeRoom(room.name);
-            if ($error.attr('type') == 'auth') {
-                if ($error.find('not-authorized').length) {
-                    this.renderPasswordForm();
-                } else if ($error.find('registration-required').length) {
-                    this.showDisconnectMessage('You are not on the member list of this room');
-                } else if ($error.find('forbidden').length) {
-                    this.showDisconnectMessage('You have been banned from this room');
-                }
-            } else if ($error.attr('type') == 'modify') {
-                if ($error.find('jid-malformed').length) {
-                    this.showDisconnectMessage('No nickname was specified');
-                }
-            } else if ($error.attr('type') == 'cancel') {
-                if ($error.find('not-allowed').length) {
-                    this.showDisconnectMessage('You are not allowed to create new rooms');
-                } else if ($error.find('not-acceptable').length) {
-                    this.showDisconnectMessage("Your nickname doesn't conform to this room's policies");
-                } else if ($error.find('conflict').length) {
-                    this.showDisconnectMessage("Your nickname is already taken");
-                } else if ($error.find('item-not-found').length) {
-                    this.showDisconnectMessage("This room does not (yet) exist");
-                } else if ($error.find('service-unavailable').length) {
-                    this.showDisconnectMessage("This room has reached it's maximum number of occupants");
+                for (i=0; i<action_msgs.length; i++) {
+                    $chat_content.append(this.info_template({message: action_msgs[i]}));
                 }
                 }
-            }
-        },
-
-        onChatRoomPresence: function (presence, room) {
-            var nick = room.nick,
-                $presence = $(presence),
-                from = $presence.attr('from'),
-                is_self = ($presence.find("status[code='110']").length) || (from == room.name+'/'+Strophe.escapeNode(nick)),
-                $item;
-
-            if ($presence.attr('type') === 'error') {
-                this.model.set('connected', false)
-                this.showErrorMessage($presence.find('error'), room);
-            } else {
-                this.model.set('connected', true);
-                this.showStatusMessages($presence, is_self);
-                if (!this.model.get('connected')) {
-                    return true;
+                this.scrollDown();
+            },
+
+            showErrorMessage: function ($error, room) {
+                var $chat_content = this.$el.find('.chat-content');
+                // We didn't enter the room, so we must remove it from the MUC
+                // add-on
+                converse.connection.muc.removeRoom(room.name);
+                if ($error.attr('type') == 'auth') {
+                    if ($error.find('not-authorized').length) {
+                        this.renderPasswordForm();
+                    } else if ($error.find('registration-required').length) {
+                        this.showDisconnectMessage(__('You are not on the member list of this room'));
+                    } else if ($error.find('forbidden').length) {
+                        this.showDisconnectMessage(__('You have been banned from this room'));
+                    }
+                } else if ($error.attr('type') == 'modify') {
+                    if ($error.find('jid-malformed').length) {
+                        this.showDisconnectMessage(__('No nickname was specified'));
+                    }
+                } else if ($error.attr('type') == 'cancel') {
+                    if ($error.find('not-allowed').length) {
+                        this.showDisconnectMessage(__('You are not allowed to create new rooms'));
+                    } else if ($error.find('not-acceptable').length) {
+                        this.showDisconnectMessage(__("Your nickname doesn't conform to this room's policies"));
+                    } else if ($error.find('conflict').length) {
+                        this.showDisconnectMessage(__("Your nickname is already taken"));
+                    } else if ($error.find('item-not-found').length) {
+                        this.showDisconnectMessage(__("This room does not (yet) exist"));
+                    } else if ($error.find('service-unavailable').length) {
+                        this.showDisconnectMessage(__("This room has reached it's maximum number of occupants"));
+                    }
                 }
                 }
-                if ($presence.find("status[code='201']").length) {
-                    // This is a new chatroom. We create an instant
-                    // chatroom, and let the user manually set any
-                    // configuration setting.
-                    converse.connection.muc.createInstantRoom(room.name);
-                }
-                if (is_self) {
-                    $item = $presence.find('item');
-                    if ($item.length) {
-                        if ($item.attr('affiliation') == 'owner') {
-                            this.$el.find('a.configure-chatroom-button').show();
-                        }
+            },
+
+            onChatRoomPresence: function (presence, room) {
+                var nick = room.nick,
+                    $presence = $(presence),
+                    from = $presence.attr('from'),
+                    is_self = ($presence.find("status[code='110']").length) || (from == room.name+'/'+Strophe.escapeNode(nick)),
+                    $item;
+
+                if ($presence.attr('type') === 'error') {
+                    this.model.set('connected', false);
+                    this.showErrorMessage($presence.find('error'), room);
+                } else {
+                    this.model.set('connected', true);
+                    this.showStatusMessages($presence, is_self);
+                    if (!this.model.get('connected')) {
+                        return true;
                     }
                     }
-                    if ($presence.find("status[code='210']").length) {
-                        // check if server changed our nick
-                        this.model.set({'nick': Strophe.getResourceFromJid(from)});
+                    if ($presence.find("status[code='201']").length) {
+                        // This is a new chatroom. We create an instant
+                        // chatroom, and let the user manually set any
+                        // configuration setting.
+                        converse.connection.muc.createInstantRoom(room.name);
                     }
                     }
-                } 
-            }
-            return true;
-        },
-
-        onChatRoomMessage: function (message) {
-            var $message = $(message),
-                body = $message.children('body').text(),
-                jid = $message.attr('from'),
-                $chat_content = this.$el.find('.chat-content'),
-                resource = Strophe.getResourceFromJid(jid),
-                sender = resource && Strophe.unescapeNode(resource) || '',
-                delayed = $message.find('delay').length > 0,
-                subject = $message.children('subject').text(),
-                match, template, message_datetime, message_date, dates, isodate, stamp;
-
-            if (delayed) {
-                stamp = $message.find('delay').attr('stamp');
-                message_datetime = converse.parseISO8601(stamp);
-            } else {
-                message_datetime = new Date();
-            }
-            // If this message is on a different day than the one received
-            // prior, then indicate it on the chatbox.
-            dates = $chat_content.find("time").map(function(){return $(this).attr("datetime");}).get();
-            message_date = new Date(message_datetime.getTime());
-            message_date.setUTCHours(0,0,0,0);
-            isodate = converse.toISOString(message_date);
-            if (_.indexOf(dates, isodate) == -1) {
-                $chat_content.append(this.new_day_template({
-                    isodate: isodate,
-                    datestring: message_date.toString().substring(0,15)
-                }));
-            }
-            this.showStatusMessages($message);
-            if (subject) {
-                this.$el.find('.chatroom-topic').text(subject).attr('title', subject);
-                $chat_content.append(this.info_template({'message': 'Topic set by '+sender+' to: '+subject }));
-            }
-            if (!body) { return true; }
-            this.appendMessage($chat_content,
-                               {'message': body,
-                                'sender': sender === this.model.get('nick') && 'me' || 'room',
-                                'fullname': sender,
-                                'time': converse.toISOString(message_datetime)
-                               });
-            this.scrollDown();
-            return true;
-        },
-
-        occupant_template: _.template(
-            '<li class="{{role}}" '+
-                '{[ if (role === "moderator") { ]}' +
-                    'title="This user is a moderator"' +
-                '{[ } ]}'+
-                '{[ if (role === "participant") { ]}' +
-                    'title="This user can send messages in this room"' +
-                '{[ } ]}'+
-                '{[ if (role === "visitor") { ]}' +
-                    'title="This user can NOT send messages in this room"' +
-                '{[ } ]}'+
-            '>{{nick}}</li>'
-        ),
-
-        onChatRoomRoster: function (roster, room) {
-            this.renderChatArea();
-            var controlboxview = converse.chatboxesview.views.controlbox,
-                roster_size = _.size(roster),
-                $participant_list = this.$el.find('.participant-list'),
-                participants = [], keys = _.keys(roster), i;
-            this.$el.find('.participant-list').empty();
-            for (i=0; i<roster_size; i++) {
-                participants.push(
-                    this.occupant_template({
-                        role: roster[keys[i]].role,
-                        nick: Strophe.unescapeNode(keys[i])
+                    if (is_self) {
+                        $item = $presence.find('item');
+                        if ($item.length) {
+                            if ($item.attr('affiliation') == 'owner') {
+                                this.$el.find('a.configure-chatroom-button').show();
+                            }
+                        }
+                        if ($presence.find("status[code='210']").length) {
+                            // check if server changed our nick
+                            this.model.set({'nick': Strophe.getResourceFromJid(from)});
+                        }
+                    }
+                }
+                return true;
+            },
+
+            onChatRoomMessage: function (message) {
+                var $message = $(message),
+                    body = $message.children('body').text(),
+                    jid = $message.attr('from'),
+                    $chat_content = this.$el.find('.chat-content'),
+                    resource = Strophe.getResourceFromJid(jid),
+                    sender = resource && Strophe.unescapeNode(resource) || '',
+                    delayed = $message.find('delay').length > 0,
+                    subject = $message.children('subject').text(),
+                    match, template, message_datetime, message_date, dates, isodate, stamp;
+
+                if (delayed) {
+                    stamp = $message.find('delay').attr('stamp');
+                    message_datetime = converse.parseISO8601(stamp);
+                } else {
+                    message_datetime = new Date();
+                }
+                // If this message is on a different day than the one received
+                // prior, then indicate it on the chatbox.
+                dates = $chat_content.find("time").map(function(){return $(this).attr("datetime");}).get();
+                message_date = new Date(message_datetime.getTime());
+                message_date.setUTCHours(0,0,0,0);
+                isodate = converse.toISOString(message_date);
+                if (_.indexOf(dates, isodate) == -1) {
+                    $chat_content.append(this.new_day_template({
+                        isodate: isodate,
+                        datestring: message_date.toString().substring(0,15)
                     }));
                     }));
+                }
+                this.showStatusMessages($message);
+                if (subject) {
+                    this.$el.find('.chatroom-topic').text(subject).attr('title', subject);
+                    // # For translators: the %1$s and %2$s parts will get replaced by the user and topic text respectively
+                    // # Example: Topic set by JC Brand to: Hello World!
+                    $chat_content.append(this.info_template({'message': __('Topic set by %1$s to: %2$s', sender, subject)}));
+                }
+                if (!body) { return true; }
+                this.appendMessage($chat_content,
+                                {'message': body,
+                                    'sender': sender === this.model.get('nick') && 'me' || 'room',
+                                    'fullname': sender,
+                                    'time': converse.toISOString(message_datetime)
+                                });
+                this.scrollDown();
+                return true;
+            },
+
+            occupant_template: _.template(
+                '<li class="{{role}}" '+
+                    '{[ if (role === "moderator") { ]}' +
+                        'title="'+__('This user is a moderator')+'"' +
+                    '{[ } ]}'+
+                    '{[ if (role === "participant") { ]}' +
+                        'title="'+__('This user can send messages in this room')+'"' +
+                    '{[ } ]}'+
+                    '{[ if (role === "visitor") { ]}' +
+                        'title="'+__('This user can NOT send messages in this room')+'"' +
+                    '{[ } ]}'+
+                '>{{nick}}</li>'
+            ),
+
+            onChatRoomRoster: function (roster, room) {
+                this.renderChatArea();
+                var controlboxview = converse.chatboxesview.views.controlbox,
+                    roster_size = _.size(roster),
+                    $participant_list = this.$el.find('.participant-list'),
+                    participants = [], keys = _.keys(roster), i;
+                this.$el.find('.participant-list').empty();
+                for (i=0; i<roster_size; i++) {
+                    participants.push(
+                        this.occupant_template({
+                            role: roster[keys[i]].role,
+                            nick: Strophe.unescapeNode(keys[i])
+                        }));
+                }
+                $participant_list.append(participants.join(""));
+                return true;
             }
             }
-            $participant_list.append(participants.join(""));
-            return true;
-        }
-    });
+        });
 
 
-    converse.ChatBoxes = Backbone.Collection.extend({
-        model: converse.ChatBox,
+        this.ChatBoxes = Backbone.Collection.extend({
+            model: converse.ChatBox,
 
 
-        onConnected: function () {
-            this.localStorage = new Backbone.LocalStorage(
-                hex_sha1('converse.chatboxes-'+converse.bare_jid));
-            if (!this.get('controlbox')) {
-                this.add({
-                    id: 'controlbox',
-                    box_id: 'controlbox'
+            onConnected: function () {
+                this.localStorage = new Backbone.LocalStorage(
+                    hex_sha1('converse.chatboxes-'+converse.bare_jid));
+                if (!this.get('controlbox')) {
+                    this.add({
+                        id: 'controlbox',
+                        box_id: 'controlbox'
+                    });
+                } else {
+                    this.get('controlbox').save();
+                }
+                // This will make sure the Roster is set up
+                this.get('controlbox').set({connected:true});
+                // Get cached chatboxes from localstorage
+                this.fetch({
+                    add: true,
+                    success: $.proxy(function (collection, resp) {
+                        if (_.include(_.pluck(resp, 'id'), 'controlbox')) {
+                            // If the controlbox was saved in localstorage, it must be visible
+                            this.get('controlbox').set({visible:true}).save();
+                        }
+                    }, this)
                 });
                 });
-            } else {
-                this.get('controlbox').save();
-            }
-            // This will make sure the Roster is set up
-            this.get('controlbox').set({connected:true});
-            // Get cached chatboxes from localstorage
-            this.fetch({
-                add: true,
-                success: $.proxy(function (collection, resp) {
-                    if (_.include(_.pluck(resp, 'id'), 'controlbox')) {
-                        // If the controlbox was saved in localstorage, it must be visible
-                        this.get('controlbox').set({visible:true}).save();
-                    }
-                }, this)
-            });
-        },
+            },
 
 
-        createChatBox: function (attrs) {
-            var chatbox  = this.get(attrs.jid);
-            if (chatbox) {
-                chatbox.trigger('show');
-            } else {
-                chatbox = this.create(attrs);
-            }
-            return chatbox;
-        },
-
-        messageReceived: function (message) {
-            var partner_jid, $message = $(message),
-                message_from = $message.attr('from');
-            if (message_from == converse.connection.jid) {
-                // FIXME: Forwarded messages should be sent to specific resources,
-                // not broadcasted
-                return true;
-            }
-            var $forwarded = $message.children('forwarded');
-            if ($forwarded.length) {
-                $message = $forwarded.children('message');
-            }
-            var from = Strophe.getBareJidFromJid(message_from),
-                to = Strophe.getBareJidFromJid($message.attr('to')),
-                resource, chatbox;
-            if (from == converse.bare_jid) {
-                // I am the sender, so this must be a forwarded message...
-                partner_jid = to;
-                resource = Strophe.getResourceFromJid($message.attr('to'));
-            } else {
-                partner_jid = from;
-                resource = Strophe.getResourceFromJid(message_from);
-            }
-            chatbox = this.get(partner_jid);
-            if (!chatbox) {
-                converse.getVCard(
-                    partner_jid,
-                    $.proxy(function (jid, fullname, image, image_type, url) {
-                        var chatbox = this.create({
-                            'id': jid,
-                            'jid': jid,
-                            'fullname': fullname,
-                            'image_type': image_type,
-                            'image': image,
-                            'url': url
-                        });
-                        chatbox.messageReceived(message);
-                        converse.roster.addResource(partner_jid, resource);
-                    }, this),
-                    $.proxy(function () {
-                        // # FIXME
-                        console.log("An error occured while fetching vcard");
-                    }, this));
+            createChatBox: function (attrs) {
+                var chatbox  = this.get(attrs.jid);
+                if (chatbox) {
+                    chatbox.trigger('show');
+                } else {
+                    chatbox = this.create(attrs);
+                }
+                return chatbox;
+            },
+
+            messageReceived: function (message) {
+                var partner_jid, $message = $(message),
+                    message_from = $message.attr('from');
+                if (message_from == converse.connection.jid) {
+                    // FIXME: Forwarded messages should be sent to specific resources,
+                    // not broadcasted
+                    return true;
+                }
+                var $forwarded = $message.children('forwarded');
+                if ($forwarded.length) {
+                    $message = $forwarded.children('message');
+                }
+                var from = Strophe.getBareJidFromJid(message_from),
+                    to = Strophe.getBareJidFromJid($message.attr('to')),
+                    resource, chatbox, roster_item;
+                if (from == converse.bare_jid) {
+                    // I am the sender, so this must be a forwarded message...
+                    partner_jid = to;
+                    resource = Strophe.getResourceFromJid($message.attr('to'));
+                } else {
+                    partner_jid = from;
+                    resource = Strophe.getResourceFromJid(message_from);
+                }
+                chatbox = this.get(partner_jid);
+                roster_item = converse.roster.get(partner_jid);
+                if (!chatbox) {
+                    chatbox = this.create({
+                        'id': partner_jid,
+                        'jid': partner_jid,
+                        'fullname': roster_item.get('fullname') || jid,
+                        'image_type': roster_item.get('image_type'),
+                        'image': roster_item.get('image'),
+                        'url': roster_item.get('url')
+                    });
+                }
+                chatbox.messageReceived(message);
+                converse.roster.addResource(partner_jid, resource);
                 return true;
                 return true;
             }
             }
-            chatbox.messageReceived(message);
-            converse.roster.addResource(partner_jid, resource);
-            return true;
-        }
-    });
-
-    converse.ChatBoxesView = Backbone.View.extend({
-        el: '#collective-xmpp-chat-data',
-
-        initialize: function () {
-            // boxesviewinit
-            this.views = {};
-            this.model.on("add", function (item) {
-                var view = this.views[item.get('id')];
-                if (!view) {
-                    if (item.get('chatroom')) {
-                        view = new converse.ChatRoomView({'model': item});
-                    } else if (item.get('box_id') === 'controlbox') {
-                        view = new converse.ControlBoxView({model: item});
-                        view.render();
+        });
+
+        this.ChatBoxesView = Backbone.View.extend({
+            el: '#collective-xmpp-chat-data',
+
+            initialize: function () {
+                // boxesviewinit
+                this.views = {};
+                this.model.on("add", function (item) {
+                    var view = this.views[item.get('id')];
+                    if (!view) {
+                        if (item.get('chatroom')) {
+                            view = new converse.ChatRoomView({'model': item});
+                        } else if (item.get('box_id') === 'controlbox') {
+                            view = new converse.ControlBoxView({model: item});
+                            view.render();
+                        } else {
+                            view = new converse.ChatBoxView({model: item});
+                        }
+                        this.views[item.get('id')] = view;
                     } else {
                     } else {
-                        view = new converse.ChatBoxView({model: item});
-                    }
-                    this.views[item.get('id')] = view;
-                } else {
-                    view.model = item;
-                    view.initialize();
-                    if (item.get('id') !== 'controlbox') {
-                        // FIXME: Why is it necessary to again append chatboxes?
-                        view.$el.appendTo(this.$el);
+                        delete view.model; // Remove ref to old model to help garbage collection
+                        view.model = item;
+                        view.initialize();
+                        if (item.get('id') !== 'controlbox') {
+                            // FIXME: Why is it necessary to again append chatboxes?
+                            view.$el.appendTo(this.$el);
+                        }
                     }
                     }
+                }, this);
+            }
+        });
+
+        this.RosterItem = Backbone.Model.extend({
+            initialize: function (attributes, options) {
+                var jid = attributes.jid;
+                if (!attributes.fullname) {
+                    attributes.fullname = jid;
                 }
                 }
-            }, this);
-        }
-    });
+                var attrs = _.extend({
+                    'id': jid,
+                    'user_id': Strophe.getNodeFromJid(jid),
+                    'resources': [],
+                    'status': ''
+                }, attributes);
+                attrs.sorted = false;
+                attrs.chat_status = 'offline';
+                this.set(attrs);
+            }
+        });
+
+        this.RosterItemView = Backbone.View.extend({
+            tagName: 'dd',
 
 
-    converse.RosterItem = Backbone.Model.extend({
-        initialize: function (attributes, options) {
-            var jid = attributes.jid;
-            if (!attributes.fullname) {
-                attributes.fullname = jid;
+            events: {
+                "click .accept-xmpp-request": "acceptRequest",
+                "click .decline-xmpp-request": "declineRequest",
+                "click .open-chat": "openChat",
+                "click .remove-xmpp-contact": "removeContact"
+            },
+
+            openChat: function (ev) {
+                ev.preventDefault();
+                converse.chatboxes.createChatBox({
+                    'id': this.model.get('jid'),
+                    'jid': this.model.get('jid'),
+                    'fullname': this.model.get('fullname'),
+                    'image_type': this.model.get('image_type'),
+                    'image': this.model.get('image'),
+                    'url': this.model.get('url'),
+                    'status': this.model.get('status')
+                });
+            },
+
+            removeContact: function (ev) {
+                ev.preventDefault();
+                var result = confirm("Are you sure you want to remove this contact?");
+                if (result === true) {
+                    var bare_jid = this.model.get('jid');
+                    converse.connection.roster.remove(bare_jid, function (iq) {
+                        converse.connection.roster.unauthorize(bare_jid);
+                        converse.rosterview.model.remove(bare_jid);
+                    });
+                }
+            },
+
+            acceptRequest: function (ev) {
+                var jid = this.model.get('jid');
+                converse.connection.roster.authorize(jid);
+                converse.connection.roster.add(jid, this.model.get('fullname'), [], function (iq) {
+                    converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
+                });
+                ev.preventDefault();
+            },
+
+            declineRequest: function (ev) {
+                ev.preventDefault();
+                converse.connection.roster.unauthorize(this.model.get('jid'));
+                this.model.destroy();
+            },
+
+            template: _.template(
+                        '<a class="open-chat" title="'+__('Click to chat with this contact')+'" href="#">{{ fullname }}</a>' +
+                        '<a class="remove-xmpp-contact" title="'+__('Click to remove this contact')+'" href="#"></a>'),
+
+            pending_template: _.template(
+                        '<span>{{ fullname }}</span>' +
+                        '<a class="remove-xmpp-contact" title="'+__('Click to remove this contact')+'" href="#"></a>'),
+
+            request_template: _.template('<div>{{ fullname }}</div>' +
+                        '<button type="button" class="accept-xmpp-request">' +
+                        'Accept</button>' +
+                        '<button type="button" class="decline-xmpp-request">' +
+                        'Decline</button>' +
+                        ''),
+
+            render: function () {
+                var item = this.model,
+                    ask = item.get('ask'),
+                    subscription = item.get('subscription');
+                this.$el.addClass(item.get('chat_status'));
+
+                if (ask === 'subscribe') {
+                    this.$el.addClass('pending-xmpp-contact');
+                    this.$el.html(this.pending_template(item.toJSON()));
+                } else if (ask === 'request') {
+                    this.$el.addClass('requesting-xmpp-contact');
+                    this.$el.html(this.request_template(item.toJSON()));
+                    converse.showControlBox();
+                } else if (subscription === 'both' || subscription === 'to') {
+                    this.$el.addClass('current-xmpp-contact');
+                    this.$el.html(this.template(item.toJSON()));
+                }
+                return this;
+            },
+
+            initialize: function () {
+                this.options.model.on('change', function (item, changed) {
+                    if (_.has(item.changed, 'chat_status')) {
+                        this.$el.attr('class', item.changed.chat_status);
+                    }
+                }, this);
             }
             }
-            var attrs = _.extend({
-                'id': jid,
-                'user_id': Strophe.getNodeFromJid(jid),
-                'resources': [],
-                'status': ''
-            }, attributes);
-            attrs.sorted = false;
-            attrs.chat_status = 'offline';
-            this.set(attrs);
+        });
+
+        this.getVCard = function (jid, callback, errback) {
+            converse.connection.vcard.get($.proxy(function (iq) {
+                $vcard = $(iq).find('vCard');
+                var fullname = $vcard.find('FN').text(),
+                    img = $vcard.find('BINVAL').text(),
+                    img_type = $vcard.find('TYPE').text(),
+                    url = $vcard.find('URL').text();
+                var rosteritem = converse.roster.get(jid);
+                if (rosteritem) {
+                    rosteritem.save({
+                        'fullname': fullname || jid,
+                        'image_type': img_type,
+                        'image': img,
+                        'url': url,
+                        'vcard_updated': converse.toISOString(new Date())
+                    });
+                }
+                callback(jid, fullname, img, img_type, url);
+            }, this), jid, errback);
         }
         }
-    });
-
-    converse.RosterItemView = Backbone.View.extend({
-        tagName: 'dd',
-
-        events: {
-            "click .accept-xmpp-request": "acceptRequest",
-            "click .decline-xmpp-request": "declineRequest",
-            "click .open-chat": "openChat",
-            "click .remove-xmpp-contact": "removeContact"
-        },
-
-        openChat: function (ev) {
-            ev.preventDefault();
-            converse.chatboxes.createChatBox({
-                'id': this.model.get('jid'),
-                'jid': this.model.get('jid'),
-                'fullname': this.model.get('fullname'),
-                'image_type': this.model.get('image_type'),
-                'image': this.model.get('image'),
-                'url': this.model.get('url'),
-                'status': this.model.get('status')
-            });
-        },
-
-        removeContact: function (ev) {
-            ev.preventDefault();
-            var result = confirm("Are you sure you want to remove this contact?");
-            if (result === true) {
-                var bare_jid = this.model.get('jid');
-                converse.connection.roster.remove(bare_jid, function (iq) {
-                    converse.connection.roster.unauthorize(bare_jid);
-                    converse.rosterview.model.remove(bare_jid);
+
+        this.RosterItems = Backbone.Collection.extend({
+            model: converse.RosterItem,
+            comparator : function (rosteritem) {
+                var chat_status = rosteritem.get('chat_status'),
+                    rank = 4;
+                switch(chat_status) {
+                    case 'offline':
+                        rank = 0;
+                        break;
+                    case 'unavailable':
+                        rank = 1;
+                        break;
+                    case 'xa':
+                        rank = 2;
+                        break;
+                    case 'away':
+                        rank = 3;
+                        break;
+                    case 'dnd':
+                        rank = 4;
+                        break;
+                    case 'online':
+                        rank = 5;
+                        break;
+                }
+                return rank;
+            },
+
+            subscribeToSuggestedItems: function (msg) {
+                $(msg).find('item').each(function () {
+                    var $this = $(this),
+                        jid = $this.attr('jid'),
+                        action = $this.attr('action'),
+                        fullname = $this.attr('name');
+                    if (action === 'add') {
+                        converse.connection.roster.add(jid, fullname, [], function (iq) {
+                            converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
+                        });
+                    }
                 });
                 });
-            }
-        },
+                return true;
+            },
 
 
-        acceptRequest: function (ev) {
-            var jid = this.model.get('jid');
-            converse.connection.roster.authorize(jid);
-            converse.connection.roster.add(jid, this.model.get('fullname'), [], function (iq) {
-                converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
-            });
-            ev.preventDefault();
-        },
-
-        declineRequest: function (ev) {
-            ev.preventDefault();
-            converse.connection.roster.unauthorize(this.model.get('jid'));
-            this.model.destroy();
-        },
-
-        template: _.template(
-                    '<a class="open-chat" title="Click to chat with this contact" href="#">{{ fullname }}</a>' +
-                    '<a class="remove-xmpp-contact" title="Click to remove this contact" href="#"></a>'),
-
-        pending_template: _.template(
-                    '<span>{{ fullname }}</span>' +
-                    '<a class="remove-xmpp-contact" title="Click to remove this contact" href="#"></a>'),
-
-        request_template: _.template('<div>{{ fullname }}</div>' +
-                    '<button type="button" class="accept-xmpp-request">' +
-                    'Accept</button>' +
-                    '<button type="button" class="decline-xmpp-request">' +
-                    'Decline</button>' +
-                    ''),
-
-        render: function () {
-            var item = this.model,
-                ask = item.get('ask'),
-                subscription = item.get('subscription');
-            this.$el.addClass(item.get('chat_status'));
-
-            if (ask === 'subscribe') {
-                this.$el.addClass('pending-xmpp-contact');
-                this.$el.html(this.pending_template(item.toJSON()));
-            } else if (ask === 'request') {
-                this.$el.addClass('requesting-xmpp-contact');
-                this.$el.html(this.request_template(item.toJSON()));
-                converse.showControlBox();
-            } else if (subscription === 'both' || subscription === 'to') {
-                this.$el.addClass('current-xmpp-contact');
-                this.$el.html(this.template(item.toJSON()));
-            }
-            return this;
-        },
+            isSelf: function (jid) {
+                return (Strophe.getBareJidFromJid(jid) === Strophe.getBareJidFromJid(converse.connection.jid));
+            },
+
+            getItem: function (id) {
+                return Backbone.Collection.prototype.get.call(this, id);
+            },
+
+            addResource: function (bare_jid, resource) {
+                var item = this.getItem(bare_jid),
+                    resources;
+                if (item) {
+                    resources = item.get('resources');
+                    if (resources) {
+                        if (_.indexOf(resources, resource) == -1) {
+                            resources.push(resource);
+                            item.set({'resources': resources});
+                        }
+                    } else  {
+                        item.set({'resources': [resource]});
+                    }
+                }
+            },
 
 
-        initialize: function () {
-            this.options.model.on('change', function (item, changed) {
-                if (_.has(item.changed, 'chat_status')) {
-                    this.$el.attr('class', item.changed.chat_status);
+            removeResource: function (bare_jid, resource) {
+                var item = this.getItem(bare_jid),
+                    resources,
+                    idx;
+                if (item) {
+                    resources = item.get('resources');
+                    idx = _.indexOf(resources, resource);
+                    if (idx !== -1) {
+                        resources.splice(idx, 1);
+                        item.set({'resources': resources});
+                        return resources.length;
+                    }
                 }
                 }
-            }, this);
-        }
-    });
-
-    converse.getVCard = function (jid, callback, errback) {
-        converse.connection.vcard.get($.proxy(function (iq) {
-            $vcard = $(iq).find('vCard');
-            var fullname = $vcard.find('FN').text(),
-                img = $vcard.find('BINVAL').text(),
-                img_type = $vcard.find('TYPE').text(),
-                url = $vcard.find('URL').text();
-            callback(jid, fullname, img, img_type, url);
-        }, this), jid, errback);
-    }
+                return 0;
+            },
 
 
-    converse.RosterItems = Backbone.Collection.extend({
-        model: converse.RosterItem,
-        comparator : function (rosteritem) {
-            var chat_status = rosteritem.get('chat_status'),
-                rank = 4;
-            switch(chat_status) {
-                case 'offline':
-                    rank = 0;
-                    break;
-                case 'unavailable':
-                    rank = 1;
-                    break;
-                case 'xa':
-                    rank = 2;
-                    break;
-                case 'away':
-                    rank = 3;
-                    break;
-                case 'dnd':
-                    rank = 4;
-                    break;
-                case 'online':
-                    rank = 5;
-                    break;
-            }
-            return rank;
-        },
-
-        subscribeToSuggestedItems: function (msg) {
-            $(msg).find('item').each(function () {
-                var $this = $(this),
-                    jid = $this.attr('jid'),
-                    action = $this.attr('action'),
-                    fullname = $this.attr('name');
-                if (action === 'add') {
-                    converse.connection.roster.add(jid, fullname, [], function (iq) {
+            subscribeBack: function (jid) {
+                var bare_jid = Strophe.getBareJidFromJid(jid);
+                if (converse.connection.roster.findItem(bare_jid)) {
+                    converse.connection.roster.authorize(bare_jid);
+                    converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
+                } else {
+                    converse.connection.roster.add(jid, '', [], function (iq) {
+                        converse.connection.roster.authorize(bare_jid);
                         converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
                         converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
                     });
                     });
                 }
                 }
-            });
-            return true;
-        },
-
-        isSelf: function (jid) {
-            return (Strophe.getBareJidFromJid(jid) === Strophe.getBareJidFromJid(converse.connection.jid));
-        },
-
-        getItem: function (id) {
-            return Backbone.Collection.prototype.get.call(this, id);
-        },
-
-        addResource: function (bare_jid, resource) {
-            var item = this.getItem(bare_jid),
-                resources;
-            if (item) {
-                resources = item.get('resources');
-                if (resources) {
-                    if (_.indexOf(resources, resource) == -1) {
-                        resources.push(resource);
-                        item.set({'resources': resources});
-                    }
-                } else  {
-                    item.set({'resources': [resource]});
+            },
+
+            unsubscribe: function (jid) {
+                /* Upon receiving the presence stanza of type "unsubscribed",
+                * the user SHOULD acknowledge receipt of that subscription state
+                * notification by sending a presence stanza of type "unsubscribe"
+                * this step lets the user's server know that it MUST no longer
+                * send notification of the subscription state change to the user.
+                */
+                converse.xmppstatus.sendPresence('unsubscribe');
+                if (converse.connection.roster.findItem(jid)) {
+                    converse.connection.roster.remove(jid, function (iq) {
+                        converse.rosterview.model.remove(jid);
+                    });
                 }
                 }
-            }
-        },
-
-        removeResource: function (bare_jid, resource) {
-            var item = this.getItem(bare_jid),
-                resources,
-                idx;
-            if (item) {
-                resources = item.get('resources');
-                idx = _.indexOf(resources, resource);
-                if (idx !== -1) {
-                    resources.splice(idx, 1);
-                    item.set({'resources': resources});
-                    return resources.length;
+            },
+
+            getNumOnlineContacts: function () {
+                var count = 0,
+                    models = this.models,
+                    models_length = models.length,
+                    i;
+                for (i=0; i<models_length; i++) {
+                    if (_.indexOf(['offline', 'unavailable'], models[i].get('chat_status')) === -1) {
+                        count++;
+                    }
                 }
                 }
-            }
-            return 0;
-        },
-
-        subscribeBack: function (jid) {
-            var bare_jid = Strophe.getBareJidFromJid(jid);
-            if (converse.connection.roster.findItem(bare_jid)) {
-                converse.connection.roster.authorize(bare_jid);
-                converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
-            } else {
-                converse.connection.roster.add(jid, '', [], function (iq) {
-                    converse.connection.roster.authorize(bare_jid);
-                    converse.connection.roster.subscribe(jid, null, converse.xmppstatus.get('fullname'));
-                });
-            }
-        },
-
-        unsubscribe: function (jid) {
-            /* Upon receiving the presence stanza of type "unsubscribed",
-            * the user SHOULD acknowledge receipt of that subscription state
-            * notification by sending a presence stanza of type "unsubscribe"
-            * this step lets the user's server know that it MUST no longer
-            * send notification of the subscription state change to the user.
-            */
-            converse.xmppstatus.sendPresence('unsubscribe');
-            if (converse.connection.roster.findItem(jid)) {
-                converse.connection.roster.remove(jid, function (iq) {
-                    converse.rosterview.model.remove(jid);
-                });
-            }
-        },
+                return count;
+            },
 
 
-        getNumOnlineContacts: function () {
-            var count = 0,
-                models = this.models,
-                models_length = models.length,
-                i;
-            for (i=0; i<models_length; i++) {
-                if (_.indexOf(['offline', 'unavailable'], models[i].get('chat_status')) === -1) {
-                    count++;
+            cleanCache: function (items) {
+                /* The localstorage cache containing roster contacts might contain
+                * some contacts that aren't actually in our roster anymore. We
+                * therefore need to remove them now.
+                */
+                var id, i,
+                    roster_ids = [];
+                for (i=0; i < items.length; ++i) {
+                    roster_ids.push(items[i].jid);
                 }
                 }
-            }
-            return count;
-        },
-
-        cleanCache: function (items) {
-            /* The localstorage cache containing roster contacts might contain
-             * some contacts that aren't actually in our roster anymore. We
-             * therefore need to remove them now.
-             */
-            var id, i,
-                roster_ids = [];
-            for (i=0; i < items.length; ++i) {
-                roster_ids.push(items[i].jid);
-            }
-            for (i=0; i < this.models.length; ++i) {
-                id = this.models[i].get('id');
-                if (_.indexOf(roster_ids, id) === -1) {
-                    this.getItem(id).destroy();
+                for (i=0; i < this.models.length; ++i) {
+                    id = this.models[i].get('id');
+                    if (_.indexOf(roster_ids, id) === -1) {
+                        this.getItem(id).destroy();
+                    }
                 }
                 }
-            }
-        },
-
-        rosterHandler: function (items) {
-            this.cleanCache(items);
-            _.each(items, function (item, index, items) {
-                if (this.isSelf(item.jid)) { return; }
-                var model = this.getItem(item.jid);
-                if (!model) {
-                    is_last = false;
-                    if (index === (items.length-1)) { is_last = true; }
-                    this.create({
-                        jid: item.jid,
-                        subscription: item.subscription,
-                        ask: item.ask,
-                        fullname: item.name || item.jid,
-                        is_last: is_last
-                    });
-                } else {
-                    if ((item.subscription === 'none') && (item.ask === null)) {
-                        // This user is no longer in our roster
-                        model.destroy();
-                    } else if (model.get('subscription') !== item.subscription || model.get('ask') !== item.ask) {
-                        // only modify model attributes if they are different from the
-                        // ones that were already set when the rosterItem was added
-                        model.set({'subscription': item.subscription, 'ask': item.ask});
-                        model.save();
+            },
+
+            rosterHandler: function (items) {
+                this.cleanCache(items);
+                _.each(items, function (item, index, items) {
+                    if (this.isSelf(item.jid)) { return; }
+                    var model = this.getItem(item.jid);
+                    if (!model) {
+                        is_last = false;
+                        if (index === (items.length-1)) { is_last = true; }
+                        this.create({
+                            jid: item.jid,
+                            subscription: item.subscription,
+                            ask: item.ask,
+                            fullname: item.name || item.jid,
+                            is_last: is_last
+                        });
+                    } else {
+                        if ((item.subscription === 'none') && (item.ask === null)) {
+                            // This user is no longer in our roster
+                            model.destroy();
+                        } else if (model.get('subscription') !== item.subscription || model.get('ask') !== item.ask) {
+                            // only modify model attributes if they are different from the
+                            // ones that were already set when the rosterItem was added
+                            model.set({'subscription': item.subscription, 'ask': item.ask});
+                            model.save();
+                        }
+                    }
+                }, this);
+            },
+
+            presenceHandler: function (presence) {
+                var $presence = $(presence),
+                    presence_type = $presence.attr('type');
+                if (presence_type === 'error') {
+                    // TODO
+                    // error presence stanzas don't necessarily have a 'from' attr.
+                    return true;
+                }
+                var jid = $presence.attr('from'),
+                    bare_jid = Strophe.getBareJidFromJid(jid),
+                    resource = Strophe.getResourceFromJid(jid),
+                    $show = $presence.find('show'),
+                    chat_status = $show.text() || 'online',
+                    status_message = $presence.find('status'),
+                    item;
+
+                if (this.isSelf(bare_jid)) {
+                    if ((converse.connection.jid !== jid)&&(presence_type !== 'unavailabe')) {
+                        // Another resource has changed it's status, we'll update ours as well.
+                        // FIXME: We should ideally differentiate between converse.js using
+                        // resources and other resources (i.e Pidgin etc.)
+                        converse.xmppstatus.save({'status': chat_status});
                     }
                     }
+                    return true;
+                } else if (($presence.find('x').attr('xmlns') || '').indexOf(Strophe.NS.MUC) === 0) {
+                    return true; // Ignore MUC
                 }
                 }
-            }, this);
-        },
-
-        presenceHandler: function (presence) {
-            var $presence = $(presence),
-                jid = $presence.attr('from'),
-                bare_jid = Strophe.getBareJidFromJid(jid),
-                resource = Strophe.getResourceFromJid(jid),
-                presence_type = $presence.attr('type'),
-                $show = $presence.find('show'),
-                chat_status = $show.text() || 'online',
-                status_message = $presence.find('status'),
-                item;
-
-            if (this.isSelf(bare_jid)) {
-                if ((converse.connection.jid !== jid)&&(presence_type !== 'unavailabe')) {
-                    // Another resource has changed it's status, we'll update ours as well.
-                    // FIXME: We should ideally differentiate between converse.js using
-                    // resources and other resources (i.e Pidgin etc.)
-                    converse.xmppstatus.save({'status': chat_status});
+                item = this.getItem(bare_jid);
+                if (item && (status_message.text() != item.get('status'))) {
+                    item.save({'status': status_message.text()});
+                }
+                if ((presence_type === 'subscribed') || (presence_type === 'unsubscribe')) {
+                    return true;
+                } else if (presence_type === 'subscribe') {
+                    if (converse.auto_subscribe) {
+                        if ((!item) || (item.get('subscription') != 'to')) {
+                            this.subscribeBack(jid);
+                        } else {
+                            converse.connection.roster.authorize(bare_jid);
+                        }
+                    } else {
+                        if ((item) && (item.get('subscription') != 'none'))  {
+                            converse.connection.roster.authorize(bare_jid);
+                        } else {
+                            converse.getVCard(
+                                bare_jid,
+                                $.proxy(function (jid, fullname, img, img_type, url) {
+                                    this.add({
+                                        jid: bare_jid,
+                                        subscription: 'none',
+                                        ask: 'request',
+                                        fullname: fullname,
+                                        image: img,
+                                        image_type: img_type,
+                                        url: url,
+                                        is_last: true
+                                    });
+                                }, this),
+                                $.proxy(function (jid, fullname, img, img_type, url) {
+                                    console.log("Error while retrieving vcard");
+                                    this.add({jid: bare_jid, subscription: 'none', ask: 'request', fullname: jid, is_last: true});
+                                }, this)
+                            );
+                        }
+                    }
+                } else if (presence_type === 'unsubscribed') {
+                    this.unsubscribe(bare_jid);
+                } else if (presence_type === 'unavailable') {
+                    if (this.removeResource(bare_jid, resource) === 0) {
+                        if (item) {
+                            item.set({'chat_status': 'offline'});
+                        }
+                    }
+                } else if (item) {
+                    // presence_type is undefined
+                    this.addResource(bare_jid, resource);
+                    item.set({'chat_status': chat_status});
                 }
                 }
                 return true;
                 return true;
-            } else if (($presence.find('x').attr('xmlns') || '').indexOf(Strophe.NS.MUC) === 0) {
-                return true; // Ignore MUC
             }
             }
+        });
 
 
-            item = this.getItem(bare_jid);
-            if (item && (status_message.text() != item.get('status'))) {
-                item.save({'status': status_message.text()});
-            }
+        this.RosterView = Backbone.View.extend({
+            tagName: 'dl',
+            id: 'converse-roster',
+            rosteritemviews: {},
 
 
-            if ((presence_type === 'error') || (presence_type === 'subscribed') || (presence_type === 'unsubscribe')) {
-                return true;
-            } else if (presence_type === 'subscribe') {
-                if (converse.auto_subscribe) {
-                    if ((!item) || (item.get('subscription') != 'to')) {
-                        this.subscribeBack(jid);
-                    } else {
-                        converse.connection.roster.authorize(bare_jid);
+            removeRosterItem: function (item) {
+                var view = this.rosteritemviews[item.id];
+                if (view) {
+                    view.$el.remove();
+                    delete this.rosteritemviews[item.id];
+                    this.render();
+                }
+            },
+
+            initialize: function () {
+                this.model.on("add", function (item) {
+                    var view = new converse.RosterItemView({model: item});
+                    this.rosteritemviews[item.id] = view;
+                    this.render(item);
+                }, this);
+
+                this.model.on('change', function (item, changed) {
+                    if ((_.size(item.changed) === 1) && _.contains(_.keys(item.changed), 'sorted')) {
+                        return;
                     }
                     }
-                } else {
-                    if ((item) && (item.get('subscription') != 'none'))  {
-                        converse.connection.roster.authorize(bare_jid);
-                    } else {
-                        converse.getVCard(
-                            bare_jid,
-                            $.proxy(function (jid, fullname, img, img_type, url) {
-                                this.add({
-                                    jid: bare_jid,
-                                    subscription: 'none',
-                                    ask: 'request',
-                                    fullname: fullname,
-                                    image: img,
-                                    image_type: img_type,
-                                    url: url,
-                                    is_last: true
-                                });
-                            }, this),
-                            $.proxy(function (jid, fullname, img, img_type, url) {
-                                console.log("Error while retrieving vcard");
-                                this.add({jid: bare_jid, subscription: 'none', ask: 'request', fullname: jid, is_last: true});
-                            }, this)
-                        );
+                    this.updateChatBox(item, changed);
+                    this.render(item);
+                }, this);
+
+                this.model.on("remove", function (item) { this.removeRosterItem(item); }, this);
+                this.model.on("destroy", function (item) { this.removeRosterItem(item); }, this);
+
+                this.$el.hide().html(this.template());
+                this.model.fetch({add: true}); // Get the cached roster items from localstorage
+                // XXX: is this necessary? this.initialSort();
+            },
+
+            updateChatBox: function (item, changed) {
+                var chatbox = converse.chatboxes.get(item.get('jid')),
+                    changes = {};
+                if (!chatbox) { return; }
+                if (_.has(item.changed, 'chat_status')) {
+                    changes.chat_status = item.get('chat_status');
+                }
+                if (_.has(item.changed, 'status')) {
+                    changes.status = item.get('status');
+                }
+                chatbox.save(changes);
+            },
+
+            template: _.template('<dt id="xmpp-contact-requests">'+__('Contact requests')+'</dt>' +
+                                '<dt id="xmpp-contacts">'+__('My contacts')+'</dt>' +
+                                '<dt id="pending-xmpp-contacts">'+__('Pending contacts')+'</dt>'),
+
+            render: function (item) {
+                var $my_contacts = this.$el.find('#xmpp-contacts'),
+                    $contact_requests = this.$el.find('#xmpp-contact-requests'),
+                    $pending_contacts = this.$el.find('#pending-xmpp-contacts'),
+                    $count, presence_change;
+                if (item) {
+                    var jid = item.id,
+                        view = this.rosteritemviews[item.id],
+                        ask = item.get('ask'),
+                        subscription = item.get('subscription'),
+                        crit = {order:'asc'};
+
+                    if (ask === 'subscribe') {
+                        $pending_contacts.after(view.render().el);
+                        $pending_contacts.after($pending_contacts.siblings('dd.pending-xmpp-contact').tsort(crit));
+                    } else if (ask === 'request') {
+                        $contact_requests.after(view.render().el);
+                        $contact_requests.after($contact_requests.siblings('dd.requesting-xmpp-contact').tsort(crit));
+                    } else if (subscription === 'both' || subscription === 'to') {
+                        if (!item.get('sorted')) {
+                            // this attribute will be true only after all of the elements have been added on the page
+                            // at this point all offline
+                            $my_contacts.after(view.render().el);
+                        }
+                        else {
+                            // just by calling render will be enough to change the icon of the existing item without
+                            // having to reinsert it and the sort will come from the presence change
+                            view.render();
+                        }
+                    }
+                    presence_change = view.model.changed.chat_status;
+                    if (presence_change) {
+                        // resort all items only if the model has changed it's chat_status as this render
+                        // is also triggered when the resource is changed which always comes before the presence change
+                        // therefore we avoid resorting when the change doesn't affect the position of the item
+                        $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.offline').tsort('a', crit));
+                        $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.unavailable').tsort('a', crit));
+                        $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.away').tsort('a', crit));
+                        $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.dnd').tsort('a', crit));
+                        $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.online').tsort('a', crit));
+                    }
+
+                    if (item.get('is_last') && !item.get('sorted')) {
+                        // this will be true after all of the roster items have been added with the default
+                        // options where all of the items are offline and now we can show the rosterView
+                        item.set('sorted', true);
+                        this.initialSort();
+                        this.$el.show(function () {
+                            converse.xmppstatus.initStatus();
+                        });
                     }
                     }
                 }
                 }
-            } else if (presence_type === 'unsubscribed') {
-                this.unsubscribe(bare_jid);
-            } else if (presence_type === 'unavailable') {
-                if (this.removeResource(bare_jid, resource) === 0) {
-                    if (item) {
-                        item.set({'chat_status': 'offline'});
+                // Hide the headings if there are no contacts under them
+                _.each([$my_contacts, $contact_requests, $pending_contacts], function (h) {
+                    if (h.nextUntil('dt').length) {
+                        h.show();
+                    }
+                    else {
+                        h.hide();
                     }
                     }
+                });
+                $count = $('#online-count');
+                $count.text('('+this.model.getNumOnlineContacts()+')');
+                if (!$count.is(':visible')) {
+                    $count.show();
                 }
                 }
-            } else if (item) {
-                // presence_type is undefined
-                this.addResource(bare_jid, resource);
-                item.set({'chat_status': chat_status});
-            }
-            return true;
-        }
-    });
-
-    converse.RosterView = Backbone.View.extend({
-        tagName: 'dl',
-        id: 'converse-roster',
-        rosteritemviews: {},
-
-        removeRosterItem: function (item) {
-            var view = this.rosteritemviews[item.id];
-            if (view) {
-                view.$el.remove();
-                delete this.rosteritemviews[item.id];
-                this.render();
+                return this;
+            },
+
+            initialSort: function () {
+                var $my_contacts = this.$el.find('#xmpp-contacts'),
+                    crit = {order:'asc'};
+                $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.offline').tsort('a', crit));
+                $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.unavailable').tsort('a', crit));
             }
             }
-        },
+        });
 
 
-        initialize: function () {
-            this.model.on("add", function (item) {
-                var view = new converse.RosterItemView({model: item});
-                this.rosteritemviews[item.id] = view;
-                this.render(item);
-            }, this);
+        this.XMPPStatus = Backbone.Model.extend({
+            initialize: function () {
+                this.set({
+                    'status' : this.get('status'),
+                    'status_message' : this.get('status_message'),
+                    'fullname' : this.get('fullname')
+                });
+            },
 
 
-            this.model.on('change', function (item, changed) {
-                if ((_.size(item.changed) === 1) && _.contains(_.keys(item.changed), 'sorted')) {
-                    return;
+            initStatus: function () {
+                var stat = this.get('status');
+                if (stat === undefined) {
+                    stat = 'online';
+                    this.save({status: stat});
                 }
                 }
-                this.updateChatBox(item, changed);
-                this.render(item);
-            }, this);
-
-            this.model.on("remove", function (item) { this.removeRosterItem(item); }, this);
-            this.model.on("destroy", function (item) { this.removeRosterItem(item); }, this);
-
-            this.$el.hide().html(this.template());
-            this.model.fetch({add: true}); // Get the cached roster items from localstorage
-            this.initialSort();
-            this.$el.appendTo(converse.chatboxesview.views.controlbox.contactspanel.$el);
-        },
-
-        updateChatBox: function (item, changed) {
-            var chatbox = converse.chatboxes.get(item.get('jid')),
-                changes = {};
-            if (!chatbox) { return; }
-            if (_.has(item.changed, 'chat_status')) {
-                changes.chat_status = item.get('chat_status');
-            }
-            if (_.has(item.changed, 'status')) {
-                changes.status = item.get('status');
-            }
-            chatbox.save(changes);
-        },
-
-        template: _.template('<dt id="xmpp-contact-requests">Contact requests</dt>' +
-                            '<dt id="xmpp-contacts">My contacts</dt>' +
-                            '<dt id="pending-xmpp-contacts">Pending contacts</dt>'),
-
-        render: function (item) {
-            var $my_contacts = this.$el.find('#xmpp-contacts'),
-                $contact_requests = this.$el.find('#xmpp-contact-requests'),
-                $pending_contacts = this.$el.find('#pending-xmpp-contacts'),
-                $count, presence_change;
-            if (item) {
-                var jid = item.id,
-                    view = this.rosteritemviews[item.id],
-                    ask = item.get('ask'),
-                    subscription = item.get('subscription'),
-                    crit = {order:'asc'};
+                this.sendPresence(stat);
+            },
 
 
-                if (ask === 'subscribe') {
-                    $pending_contacts.after(view.render().el);
-                    $pending_contacts.after($pending_contacts.siblings('dd.pending-xmpp-contact').tsort(crit));
-                } else if (ask === 'request') {
-                    $contact_requests.after(view.render().el);
-                    $contact_requests.after($contact_requests.siblings('dd.requesting-xmpp-contact').tsort(crit));
-                } else if (subscription === 'both' || subscription === 'to') {
-                    if (!item.get('sorted')) {
-                        // this attribute will be true only after all of the elements have been added on the page
-                        // at this point all offline
-                        $my_contacts.after(view.render().el);
+            sendPresence: function (type) {
+                var status_message = this.get('status_message'),
+                    presence;
+                // Most of these presence types are actually not explicitly sent,
+                // but I add all of them here fore reference and future proofing.
+                if ((type === 'unavailable') ||
+                        (type === 'probe') ||
+                        (type === 'error') ||
+                        (type === 'unsubscribe') ||
+                        (type === 'unsubscribed') ||
+                        (type === 'subscribe') ||
+                        (type === 'subscribed')) {
+                    presence = $pres({'type':type});
+                } else {
+                    if (type === 'online') {
+                        presence = $pres();
+                    } else {
+                        presence = $pres().c('show').t(type).up();
                     }
                     }
-                    else {
-                        // just by calling render will be enough to change the icon of the existing item without
-                        // having to reinsert it and the sort will come from the presence change
-                        view.render();
+                    if (status_message) {
+                        presence.c('status').t(status_message);
                     }
                     }
                 }
                 }
-                presence_change = view.model.changed.chat_status;
-                if (presence_change) {
-                    // resort all items only if the model has changed it's chat_status as this render
-                    // is also triggered when the resource is changed which always comes before the presence change
-                    // therefore we avoid resorting when the change doesn't affect the position of the item
-                    $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.offline').tsort('a', crit));
-                    $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.unavailable').tsort('a', crit));
-                    $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.away').tsort('a', crit));
-                    $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.dnd').tsort('a', crit));
-                    $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.online').tsort('a', crit));
-                }
-
-                if (item.get('is_last') && !item.get('sorted')) {
-                    // this will be true after all of the roster items have been added with the default
-                    // options where all of the items are offline and now we can show the rosterView
-                    item.set('sorted', true);
-                    this.initialSort();
-                    this.$el.show();
-                }
+                converse.connection.send(presence);
+            },
+
+            setStatus: function (value) {
+                this.sendPresence(value);
+                this.save({'status': value});
+            },
+
+            setStatusMessage: function (status_message) {
+                converse.connection.send($pres().c('show').t(this.get('status')).up().c('status').t(status_message));
+                this.save({'status_message': status_message});
             }
             }
-            // Hide the headings if there are no contacts under them
-            _.each([$my_contacts, $contact_requests, $pending_contacts], function (h) {
-                if (h.nextUntil('dt').length) {
-                    h.show();
-                }
-                else {
-                    h.hide();
+        });
+
+        this.XMPPStatusView = Backbone.View.extend({
+            el: "span#xmpp-status-holder",
+
+            events: {
+                "click a.choose-xmpp-status": "toggleOptions",
+                "click #fancy-xmpp-status-select a.change-xmpp-status-message": "renderStatusChangeForm",
+                "submit #set-custom-xmpp-status": "setStatusMessage",
+                "click .dropdown dd ul li a": "setStatus"
+            },
+
+            toggleOptions: function (ev) {
+                ev.preventDefault();
+                $(ev.target).parent().parent().siblings('dd').find('ul').toggle('fast');
+            },
+
+            change_status_message_template: _.template(
+                '<form id="set-custom-xmpp-status">' +
+                    '<input type="text" class="custom-xmpp-status" {{ status_message }}"'+
+                        'placeholder="'+__('Custom status')+'"/>' +
+                    '<button type="submit">'+__('Save')+'</button>' +
+                '</form>'),
+
+            status_template: _.template(
+                '<div class="xmpp-status">' +
+                    '<a class="choose-xmpp-status {{ chat_status }}" data-value="{{status_message}}" href="#" title="'+__('Click to change your chat status')+'">' +
+                        '{{ status_message }}' +
+                    '</a>' +
+                    '<a class="change-xmpp-status-message" href="#" title="'+__('Click here to write a custom status message')+'"></a>' +
+                '</div>'),
+
+            renderStatusChangeForm: function (ev) {
+                ev.preventDefault();
+                var status_message = this.model.get('status') || 'offline';
+                var input = this.change_status_message_template({'status_message': status_message});
+                this.$el.find('.xmpp-status').replaceWith(input);
+                this.$el.find('.custom-xmpp-status').focus().focus();
+            },
+
+            setStatusMessage: function (ev) {
+                ev.preventDefault();
+                var status_message = $(ev.target).find('input').attr('value');
+                if (status_message === "") {
                 }
                 }
-            });
-            $count = $('#online-count');
-            $count.text('('+this.model.getNumOnlineContacts()+')');
-            if (!$count.is(':visible')) {
-                $count.show();
-            }
-            return this;
-        },
-
-        initialSort: function () {
-            var $my_contacts = this.$el.find('#xmpp-contacts'),
-                crit = {order:'asc'};
-            $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.offline').tsort('a', crit));
-            $my_contacts.after($my_contacts.siblings('dd.current-xmpp-contact.unavailable').tsort('a', crit));
-        }
-    });
-
-    converse.XMPPStatus = Backbone.Model.extend({
-        initialize: function () {
-            this.set({
-                'status' : this.get('status'),
-                'status_message' : this.get('status_message'),
-                'fullname' : this.get('fullname')
-            });
-        },
+                this.model.setStatusMessage(status_message);
+            },
 
 
-        initStatus: function () {
-            var stat = this.get('status');
-            if (stat === undefined) {
-                this.save({status: 'online'});
-            } else {
-                this.sendPresence(stat);
-            }
-        },
-
-        sendPresence: function (type) {
-            var status_message = this.get('status_message'),
-                presence;
-            // Most of these presence types are actually not explicitly sent,
-            // but I add all of them here fore reference and future proofing.
-            if ((type === 'unavailable') ||
-                    (type === 'probe') ||
-                    (type === 'error') ||
-                    (type === 'unsubscribe') ||
-                    (type === 'unsubscribed') ||
-                    (type === 'subscribe') ||
-                    (type === 'subscribed')) {
-                presence = $pres({'type':type});
-            } else {
-                if (type === 'online') {
-                    presence = $pres();
+            setStatus: function (ev) {
+                ev.preventDefault();
+                var $el = $(ev.target),
+                    value = $el.attr('data-value');
+                this.model.setStatus(value);
+                this.$el.find(".dropdown dd ul").hide();
+            },
+
+            getPrettyStatus: function (stat) {
+                if (stat === 'chat') {
+                    pretty_status = __('online');
+                } else if (stat === 'dnd') {
+                    pretty_status = __('busy');
+                } else if (stat === 'xa') {
+                    pretty_status = __('away for long');
+                } else if (stat === 'away') {
+                    pretty_status = __('away');
                 } else {
                 } else {
-                    presence = $pres().c('show').t(type).up();
+                    pretty_status = __(stat) || __('online'); // XXX: Is 'online' the right default choice here?
                 }
                 }
-                if (status_message) {
-                    presence.c('status').t(status_message);
+                return pretty_status;
+            },
+
+            updateStatusUI: function (model) {
+                if (!(_.has(model.changed, 'status')) && !(_.has(model.changed, 'status_message'))) {
+                    return;
                 }
                 }
-            }
-            converse.connection.send(presence);
-        },
+                var stat = model.get('status');
+                // # For translators: the %1$s part gets replaced with the status
+                // # Example, I am online
+                var status_message = model.get('status_message') || __("I am %1$s", this.getPrettyStatus(stat));
+                this.$el.find('#fancy-xmpp-status-select').html(
+                    this.status_template({
+                        'chat_status': stat,
+                        'status_message': status_message
+                    }));
+            },
 
 
-        setStatus: function (value) {
-            this.sendPresence(value);
-            this.save({'status': value});
-        },
+            choose_template: _.template(
+                '<dl id="target" class="dropdown">' +
+                    '<dt id="fancy-xmpp-status-select" class="fancy-dropdown"></dt>' +
+                    '<dd><ul></ul></dd>' +
+                '</dl>'),
 
 
-        setStatusMessage: function (status_message) {
-            converse.connection.send($pres().c('show').t(this.get('status')).up().c('status').t(status_message));
-            this.save({'status_message': status_message});
-        }
-    });
-
-    converse.XMPPStatusView = Backbone.View.extend({
-        el: "span#xmpp-status-holder",
-
-        events: {
-            "click a.choose-xmpp-status": "toggleOptions",
-            "click #fancy-xmpp-status-select a.change-xmpp-status-message": "renderStatusChangeForm",
-            "submit #set-custom-xmpp-status": "setStatusMessage",
-            "click .dropdown dd ul li a": "setStatus"
-        },
-
-        toggleOptions: function (ev) {
-            ev.preventDefault();
-            $(ev.target).parent().parent().siblings('dd').find('ul').toggle('fast');
-        },
-
-        change_status_message_template: _.template(
-            '<form id="set-custom-xmpp-status">' +
-                '<input type="text" class="custom-xmpp-status" {{ status_message }}" placeholder="Custom status"/>' +
-                '<button type="submit">Save</button>' +
-            '</form>'),
-
-        status_template: _.template(
-            '<div class="xmpp-status">' +
-                '<a class="choose-xmpp-status {{ chat_status }}" data-value="{{status_message}}" href="#" title="Click to change your chat status">' +
-                    '{{ status_message }}' +
-                '</a>' +
-                '<a class="change-xmpp-status-message" href="#" Title="Click here to write a custom status message"></a>' +
-            '</div>'),
-
-        renderStatusChangeForm: function (ev) {
-            ev.preventDefault();
-            var status_message = this.model.get('status') || 'offline';
-            var input = this.change_status_message_template({'status_message': status_message});
-            this.$el.find('.xmpp-status').replaceWith(input);
-            this.$el.find('.custom-xmpp-status').focus().focus();
-        },
-
-        setStatusMessage: function (ev) {
-            ev.preventDefault();
-            var status_message = $(ev.target).find('input').attr('value');
-            if (status_message === "") {
-            }
-            this.model.setStatusMessage(status_message);
-        },
-
-        setStatus: function (ev) {
-            ev.preventDefault();
-            var $el = $(ev.target),
-                value = $el.attr('data-value');
-            this.model.setStatus(value);
-            this.$el.find(".dropdown dd ul").hide();
-        },
-
-        getPrettyStatus: function (stat) {
-            if (stat === 'chat') {
-                pretty_status = 'online';
-            } else if (stat === 'dnd') {
-                pretty_status = 'busy';
-            } else if (stat === 'xa') {
-                pretty_status = 'away for long';
-            } else {
-                pretty_status = stat || 'online';
-            }
-            return pretty_status;
-        },
+            option_template: _.template(
+                '<li>' +
+                    '<a href="#" class="{{ value }}" data-value="{{ value }}">{{ text }}</a>' +
+                '</li>'),
 
 
-        updateStatusUI: function (model) {
-            if (!(_.has(model.changed, 'status')) && !(_.has(model.changed, 'status_message'))) {
-                return;
-            }
-            var stat = model.get('status'),
-                status_message = model.get('status_message') || "I am " + this.getPrettyStatus(stat);
-            this.$el.find('#fancy-xmpp-status-select').html(
-                this.status_template({
-                    'chat_status': stat,
-                    'status_message': status_message
-                }));
-        },
-
-        choose_template: _.template(
-            '<dl id="target" class="dropdown">' +
-                '<dt id="fancy-xmpp-status-select" class="fancy-dropdown"></dt>' +
-                '<dd><ul></ul></dd>' +
-            '</dl>'),
-
-        option_template: _.template(
-            '<li>' +
-                '<a href="#" class="{{ value }}" data-value="{{ value }}">{{ text }}</a>' +
-            '</li>'),
-
-        initialize: function () {
-            this.model.on("change", this.updateStatusUI, this);
-        },
-
-        render: function () {
-            // Replace the default dropdown with something nicer
-            var $select = this.$el.find('select#select-xmpp-status'),
-                chat_status = this.model.get('status') || 'offline',
-                options = $('option', $select),
-                $options_target,
-                options_list = [],
-                that = this;
-            this.$el.html(this.choose_template());
-            this.$el.find('#fancy-xmpp-status-select')
-                    .html(this.status_template({
-                            'status_message': "I am " + this.getPrettyStatus(chat_status),
+            initialize: function () {
+                this.model.on("change", this.updateStatusUI, this);
+            },
+
+            render: function () {
+                // Replace the default dropdown with something nicer
+                var $select = this.$el.find('select#select-xmpp-status'),
+                    chat_status = this.model.get('status') || 'offline',
+                    options = $('option', $select),
+                    $options_target,
+                    options_list = [],
+                    that = this;
+                this.$el.html(this.choose_template());
+                this.$el.find('#fancy-xmpp-status-select')
+                        .html(this.status_template({
+                            'status_message': __("I am %1$s", this.getPrettyStatus(chat_status)),
                             'chat_status': chat_status
                             'chat_status': chat_status
                             }));
                             }));
-            // iterate through all the <option> elements and add option values
-            options.each(function(){
-                options_list.push(that.option_template({'value': $(this).val(),
-                                                        'text': this.text
-                                                        }));
-            });
-            $options_target = this.$el.find("#target dd ul").hide();
-            $options_target.append(options_list.join(''));
-            $select.remove();
-            return this;
-        }
-    });
-
-    converse.Feature = Backbone.Model.extend();
-    converse.Features = Backbone.Collection.extend({
-        /* Service Discovery
-         * -----------------
-         * This collection stores Feature Models, representing features
-         * provided by available XMPP entities (e.g. servers)
-         * See XEP-0030 for more details: http://xmpp.org/extensions/xep-0030.html
-         * All features are shown here: http://xmpp.org/registrar/disco-features.html
-         */
-        model: converse.Feature,
-        initialize: function () {
-            this.localStorage = new Backbone.LocalStorage(
-                hex_sha1('converse.features'+converse.bare_jid));
-            if (this.localStorage.records.length === 0) {
-                // localStorage is empty, so we've likely never queried this
-                // domain for features yet
-                converse.connection.disco.info(converse.domain, null, $.proxy(this.onInfo, this));
-                converse.connection.disco.items(converse.domain, null, $.proxy(this.onItems, this));
-            } else {
-                this.fetch({add:true});
+                // iterate through all the <option> elements and add option values
+                options.each(function(){
+                    options_list.push(that.option_template({'value': $(this).val(),
+                                                            'text': this.text
+                                                            }));
+                });
+                $options_target = this.$el.find("#target dd ul").hide();
+                $options_target.append(options_list.join(''));
+                $select.remove();
+                return this;
             }
             }
-        },
-
-        onItems: function (stanza) {
-            $(stanza).find('query item').each($.proxy(function (idx, item) {
-                converse.connection.disco.info(
-                    $(item).attr('jid'),
-                    null,
-                    $.proxy(this.onInfo, this));
-            }, this));
-        },
+        });
 
 
-        onInfo: function (stanza) {
-            var $stanza = $(stanza);
-            if (($stanza.find('identity[category=server][type=im]').length === 0) &&
-                ($stanza.find('identity[category=conference][type=text]').length === 0)) {
-                // This isn't an IM server component
-                return;
+        this.Feature = Backbone.Model.extend();
+        this.Features = Backbone.Collection.extend({
+            /* Service Discovery
+            * -----------------
+            * This collection stores Feature Models, representing features
+            * provided by available XMPP entities (e.g. servers)
+            * See XEP-0030 for more details: http://xmpp.org/extensions/xep-0030.html
+            * All features are shown here: http://xmpp.org/registrar/disco-features.html
+            */
+            model: converse.Feature,
+            initialize: function () {
+                this.localStorage = new Backbone.LocalStorage(
+                    hex_sha1('converse.features'+converse.bare_jid));
+                if (this.localStorage.records.length === 0) {
+                    // localStorage is empty, so we've likely never queried this
+                    // domain for features yet
+                    converse.connection.disco.info(converse.domain, null, $.proxy(this.onInfo, this));
+                    converse.connection.disco.items(converse.domain, null, $.proxy(this.onItems, this));
+                } else {
+                    this.fetch({add:true});
+                }
+            },
+
+            onItems: function (stanza) {
+                $(stanza).find('query item').each($.proxy(function (idx, item) {
+                    converse.connection.disco.info(
+                        $(item).attr('jid'),
+                        null,
+                        $.proxy(this.onInfo, this));
+                }, this));
+            },
+
+            onInfo: function (stanza) {
+                var $stanza = $(stanza);
+                if (($stanza.find('identity[category=server][type=im]').length === 0) &&
+                    ($stanza.find('identity[category=conference][type=text]').length === 0)) {
+                    // This isn't an IM server component
+                    return;
+                }
+                $stanza.find('feature').each($.proxy(function (idx, feature) {
+                    this.create({
+                        'var': $(feature).attr('var'),
+                        'from': $stanza.attr('from')
+                    });
+                }, this));
             }
             }
-            $stanza.find('feature').each($.proxy(function (idx, feature) {
-                this.create({
-                    'var': $(feature).attr('var'),
-                    'from': $stanza.attr('from')
-                });
-            }, this));
-        }
-    });
-
-    converse.LoginPanel = Backbone.View.extend({
-        tagName: 'div',
-        id: "login-dialog",
-        events: {
-            'submit form#converse-login': 'authenticate'
-        },
-        tab_template: _.template(
-            '<li><a class="current" href="#login">Sign in</a></li>'),
-        template: _.template(
-            '<form id="converse-login">' +
-            '<label>XMPP/Jabber Username:</label>' +
-            '<input type="text" id="jid">' +
-            '<label>Password:</label>' +
-            '<input type="password" id="password">' +
-            '<input class="login-submit" type="submit" value="Log In">' +
-            '</form">'),
-
-        bosh_url_input: _.template(
-            '<label>BOSH Service URL:</label>' +
-            '<input type="text" id="bosh_service_url">'),
-
-        connect: function ($form, jid, password) {
-            var $button = $form.find('input[type=submit]'),
-                connection = new Strophe.Connection(converse.bosh_service_url);
-            $button.hide().after('<img class="spinner login-submit" src="images/spinner.gif"/>');
-            connection.connect(jid, password, $.proxy(function (status, message) {
-                if (status === Strophe.Status.CONNECTED) {
-                    console.log('Connected');
-                    converse.onConnected(connection);
-                } else if (status === Strophe.Status.DISCONNECTED) {
-                    $button.show().siblings('img').remove();
-                    converse.giveFeedback('Disconnected', 'error');
-                } else if (status === Strophe.Status.Error) {
-                    $button.show().siblings('img').remove();
-                    converse.giveFeedback('Error', 'error');
-                } else if (status === Strophe.Status.CONNECTING) {
-                    converse.giveFeedback('Connecting');
-                } else if (status === Strophe.Status.CONNFAIL) {
-                    $button.show().siblings('img').remove();
-                    converse.giveFeedback('Connection Failed', 'error');
-                } else if (status === Strophe.Status.AUTHENTICATING) {
-                    converse.giveFeedback('Authenticating');
-                } else if (status === Strophe.Status.AUTHFAIL) {
-                    $button.show().siblings('img').remove();
-                    converse.giveFeedback('Authentication Failed', 'error');
-                } else if (status === Strophe.Status.DISCONNECTING) {
-                    converse.giveFeedback('Disconnecting', 'error');
-                } else if (status === Strophe.Status.ATTACHED) {
-                    console.log('Attached');
+        });
+
+        this.LoginPanel = Backbone.View.extend({
+            tagName: 'div',
+            id: "login-dialog",
+            events: {
+                'submit form#converse-login': 'authenticate'
+            },
+            tab_template: _.template(
+                '<li><a class="current" href="#login">'+__('Sign in')+'</a></li>'),
+            template: _.template(
+                '<form id="converse-login">' +
+                '<label>'+__('XMPP/Jabber Username:')+'</label>' +
+                '<input type="text" id="jid">' +
+                '<label>'+__('Password:')+'</label>' +
+                '<input type="password" id="password">' +
+                '<input class="login-submit" type="submit" value="'+__('Log In')+'">' +
+                '</form">'),
+
+            bosh_url_input: _.template(
+                '<label>'+__('BOSH Service URL:')+'</label>' +
+                '<input type="text" id="bosh_service_url">'),
+
+            connect: function ($form, jid, password) {
+                var button = null,
+                    connection = new Strophe.Connection(converse.bosh_service_url);
+                if ($form) {
+                    $button = $form.find('input[type=submit]');
+                    $button.hide().after('<img class="spinner login-submit" src="images/spinner.gif"/>');
                 }
                 }
-            }, this));
-        },
-
-        authenticate: function (ev) {
-            ev.preventDefault();
-            var $form = $(ev.target),
-                $jid_input = $form.find('input#jid'),
-                jid = $jid_input.val(),
-                $pw_input = $form.find('input#password'),
-                password = $pw_input.val(),
-                $bsu_input = null,
-                errors = false;
-
-            if (! converse.bosh_service_url) {
-                $bsu_input = $form.find('input#bosh_service_url');
-                converse.bosh_service_url = $bsu_input.val();
-                if (! converse.bosh_service_url)  {
+                connection.connect(jid, password, $.proxy(function (status, message) {
+                    if (status === Strophe.Status.CONNECTED) {
+                        console.log(__('Connected'));
+                        converse.onConnected(connection);
+                    } else if (status === Strophe.Status.DISCONNECTED) {
+                        if ($button) { $button.show().siblings('img').remove(); }
+                        converse.giveFeedback(__('Disconnected'), 'error');
+                        this.connect(null, connection.jid, connection.pass);
+                    } else if (status === Strophe.Status.Error) {
+                        if ($button) { $button.show().siblings('img').remove(); }
+                        converse.giveFeedback(__('Error'), 'error');
+                    } else if (status === Strophe.Status.CONNECTING) {
+                        converse.giveFeedback(__('Connecting'));
+                    } else if (status === Strophe.Status.CONNFAIL) {
+                        if ($button) { $button.show().siblings('img').remove(); }
+                        converse.giveFeedback(__('Connection Failed'), 'error');
+                    } else if (status === Strophe.Status.AUTHENTICATING) {
+                        converse.giveFeedback(__('Authenticating'));
+                    } else if (status === Strophe.Status.AUTHFAIL) {
+                        if ($button) { $button.show().siblings('img').remove(); }
+                        converse.giveFeedback(__('Authentication Failed'), 'error');
+                    } else if (status === Strophe.Status.DISCONNECTING) {
+                        converse.giveFeedback(__('Disconnecting'), 'error');
+                    } else if (status === Strophe.Status.ATTACHED) {
+                        console.log(__('Attached'));
+                    }
+                }, this));
+            },
+
+            authenticate: function (ev) {
+                ev.preventDefault();
+                var $form = $(ev.target),
+                    $jid_input = $form.find('input#jid'),
+                    jid = $jid_input.val(),
+                    $pw_input = $form.find('input#password'),
+                    password = $pw_input.val(),
+                    $bsu_input = null,
+                    errors = false;
+
+                if (! converse.bosh_service_url) {
+                    $bsu_input = $form.find('input#bosh_service_url');
+                    converse.bosh_service_url = $bsu_input.val();
+                    if (! converse.bosh_service_url)  {
+                        errors = true;
+                        $bsu_input.addClass('error');
+                    }
+                }
+                if (! jid) {
                     errors = true;
                     errors = true;
-                    $bsu_input.addClass('error');
+                    $jid_input.addClass('error');
                 }
                 }
+                if (! password)  {
+                    errors = true;
+                    $pw_input.addClass('error');
+                }
+                if (errors) { return; }
+                this.connect($form, jid, password);
+            },
+
+            remove: function () {
+                this.$parent.find('#controlbox-tabs').empty();
+                this.$parent.find('#controlbox-panes').empty();
+            },
+
+            render: function () {
+                this.$parent.find('#controlbox-tabs').append(this.tab_template());
+                var template = this.template();
+                if (! this.bosh_url_input) {
+                    template.find('form').append(this.bosh_url_input);
+                }
+                this.$parent.find('#controlbox-panes').append(this.$el.html(template));
+                this.$el.find('input#jid').focus();
+                return this;
             }
             }
-            if (! jid) {
-                errors = true;
-                $jid_input.addClass('error');
-            }
-            if (! password)  {
-                errors = true;
-                $pw_input.addClass('error');
-            }
-            if (errors) { return; }
-            this.connect($form, jid, password);
-        },
-
-        remove: function () {
-            this.$parent.find('#controlbox-tabs').empty();
-            this.$parent.find('#controlbox-panes').empty();
-        },
-
-        render: function () {
-            this.$parent.find('#controlbox-tabs').append(this.tab_template());
-            var template = this.template();
-            if (! this.bosh_url_input) {
-                template.find('form').append(this.bosh_url_input);
-            }
-            this.$parent.find('#controlbox-panes').append(this.$el.html(template));
-            this.$el.find('input#jid').focus();
-            return this;
-        }
-    });
-
-    converse.showControlBox = function () {
-        var controlbox = this.chatboxes.get('controlbox');
-        if (!controlbox) {
-            this.chatboxes.add({
-                id: 'controlbox',
-                box_id: 'controlbox',
-                visible: true
-            });
-            if (this.connection) {
-                this.chatboxes.get('controlbox').save();
-            }
-        } else {
-            controlbox.trigger('show');
-        }
-    };
+        });
 
 
-    converse.toggleControlBox = function () {
-        if ($("div#controlbox").is(':visible')) {
+        this.showControlBox = function () {
             var controlbox = this.chatboxes.get('controlbox');
             var controlbox = this.chatboxes.get('controlbox');
-            if (this.connection) {
-                controlbox.destroy();
+            if (!controlbox) {
+                this.chatboxes.add({
+                    id: 'controlbox',
+                    box_id: 'controlbox',
+                    visible: true
+                });
+                if (this.connection) {
+                    this.chatboxes.get('controlbox').save();
+                }
             } else {
             } else {
-                controlbox.trigger('hide');
+                controlbox.trigger('show');
             }
             }
-        } else {
-            this.showControlBox();
-        }
-    };
-
-    converse.giveFeedback = function (message, klass) {
-        $('.conn-feedback').text(message);
-        $('.conn-feedback').attr('class', 'conn-feedback');
-        if (klass) {
-            $('.conn-feedback').addClass(klass);
-        }
-    };
+        };
 
 
-    converse.onConnected = function (connection) {
-        this.connection = connection;
-        this.connection.xmlInput = function (body) { console.log(body); };
-        this.connection.xmlOutput = function (body) { console.log(body); };
-        this.bare_jid = Strophe.getBareJidFromJid(this.connection.jid);
-        this.domain = Strophe.getDomainFromJid(this.connection.jid);
-        this.features = new this.Features();
-
-        // Set up the roster
-        this.roster = new this.RosterItems();
-        this.roster.localStorage = new Backbone.LocalStorage(
-            hex_sha1('converse.rosteritems-'+this.bare_jid));
-
-        this.xmppstatus = new this.XMPPStatus({id:1});
-        this.xmppstatus.localStorage = new Backbone.LocalStorage(
-            hex_sha1('converse.xmppstatus-'+this.bare_jid));
-
-        this.chatboxes.onConnected();
-        this.rosterview = new this.RosterView({'model':this.roster});
-
-        this.xmppstatusview = new this.XMPPStatusView({'model': this.xmppstatus}).render();
-        this.xmppstatus.fetch({
-            success: $.proxy(function (xmppstatus, resp) {
-                if (!xmppstatus.get('fullname')) {
-                    this.getVCard(
-                        null, // No 'to' attr when getting one's own vCard
-                        $.proxy(function (jid, fullname, image, image_type, url) {
-                            this.xmppstatus.save({'fullname': fullname});
-                        }, this));
+        this.toggleControlBox = function () {
+            if ($("div#controlbox").is(':visible')) {
+                var controlbox = this.chatboxes.get('controlbox');
+                if (this.connection) {
+                    controlbox.destroy();
+                } else {
+                    controlbox.trigger('hide');
                 }
                 }
-            }, this)
-        });
-
-        this.connection.addHandler(
-            $.proxy(this.roster.subscribeToSuggestedItems, this.roster),
-            'http://jabber.org/protocol/rosterx', 'message', null);
+            } else {
+                this.showControlBox();
+            }
+        };
 
 
-        this.connection.roster.registerCallback(
-            $.proxy(this.roster.rosterHandler, this.roster),
-            null, 'presence', null);
+        this.giveFeedback = function (message, klass) {
+            $('.conn-feedback').text(message);
+            $('.conn-feedback').attr('class', 'conn-feedback');
+            if (klass) {
+                $('.conn-feedback').addClass(klass);
+            }
+        };
 
 
-        this.connection.roster.get($.proxy(function () {
-            this.connection.addHandler(
-                    $.proxy(function (presence) {
-                        this.presenceHandler(presence);
-                        return true;
-                    }, this.roster), null, 'presence', null);
+        this.onConnected = function (connection) {
+            this.connection = connection;
+            this.connection.xmlInput = function (body) { console.log(body); };
+            this.connection.xmlOutput = function (body) { console.log(body); };
+            this.bare_jid = Strophe.getBareJidFromJid(this.connection.jid);
+            this.domain = Strophe.getDomainFromJid(this.connection.jid);
+            this.features = new this.Features();
+
+            // Set up the roster
+            this.roster = new this.RosterItems();
+            this.roster.localStorage = new Backbone.LocalStorage(
+                hex_sha1('converse.rosteritems-'+this.bare_jid));
+            this.connection.roster.registerCallback(
+                $.proxy(this.roster.rosterHandler, this.roster),
+                null, 'presence', null);
+            this.rosterview = new this.RosterView({'model':this.roster});
+
+            this.chatboxes.onConnected();
 
 
             this.connection.addHandler(
             this.connection.addHandler(
-                    $.proxy(function (message) {
-                        this.chatboxes.messageReceived(message);
-                        return true;
-                    }, this), null, 'message', 'chat');
-
-            this.xmppstatus.initStatus();
-        }, this));
+                $.proxy(this.roster.subscribeToSuggestedItems, this.roster),
+                'http://jabber.org/protocol/rosterx', 'message', null);
+
+            this.connection.roster.get($.proxy(function (a) {
+                this.connection.addHandler(
+                        $.proxy(function (presence) {
+                            this.presenceHandler(presence);
+                            return true;
+                        }, this.roster), null, 'presence', null);
+                this.connection.addHandler(
+                        $.proxy(function (message) {
+                            this.chatboxes.messageReceived(message);
+                            return true;
+                        }, this), null, 'message', 'chat');
+            }, this));
 
 
-        $(window).on("blur focus", $.proxy(function(e) {
-            if ((this.windowState != e.type) && (e.type == 'focus')) {
-                converse.clearMsgCounter();
-            }
-            this.windowState = e.type;
-        },this));
-        this.giveFeedback('Online Contacts');
-    };
+            $(window).on("blur focus", $.proxy(function(e) {
+                if ((this.windowState != e.type) && (e.type == 'focus')) {
+                    converse.clearMsgCounter();
+                }
+                this.windowState = e.type;
+            },this));
+            this.giveFeedback(__('Online Contacts'));
+        };
 
 
-    converse.initialize = function (settings) {
-        // Default values
-        this.bosh_service_url = ''; // The BOSH connection manager URL. Required if you are not prebinding.
-        this.animate = true;
-        this.auto_list_rooms = false;
-        this.auto_subscribe = false;
-        this.hide_muc_server = false;
-        this.prebind = false;
-        this.xhr_user_search = false;
-        _.extend(this, settings);
+        // This is the end of the initialize method.
         this.chatboxes = new this.ChatBoxes();
         this.chatboxes = new this.ChatBoxes();
         this.chatboxesview = new this.ChatBoxesView({model: this.chatboxes});
         this.chatboxesview = new this.ChatBoxesView({model: this.chatboxes});
         $('a.toggle-online-users').bind(
         $('a.toggle-online-users').bind(

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
converse.min.css


A diferenza do arquivo foi suprimida porque é demasiado grande
+ 55 - 0
converse.min.js


BIN=BIN
docs/doctrees/environment.pickle


BIN=BIN
docs/doctrees/index.doctree


+ 89 - 6
docs/html/_sources/index.txt

@@ -29,7 +29,7 @@ webchat experience and that you have control over the data. The latter being a
 requirement for many sites dealing with sensitive information.
 requirement for many sites dealing with sensitive information.
 
 
 You'll need to set up your own XMPP server and in order to have
 You'll need to set up your own XMPP server and in order to have
-`Session support`_ (i.e. single-signon functionality whereby users are authenticated once and stay
+`Session Support`_ (i.e. single-signon functionality whereby users are authenticated once and stay
 logged in to XMPP upon page reload) you will also have to add some server-side
 logged in to XMPP upon page reload) you will also have to add some server-side
 code.
 code.
 
 
@@ -47,7 +47,7 @@ An XMPP/Jabber server
 to connect to an XMPP/Jabber server (Jabber is really just a synonym for XMPP).
 to connect to an XMPP/Jabber server (Jabber is really just a synonym for XMPP).
 
 
 You can connect to public XMPP servers like ``jabber.org`` but if you want to
 You can connect to public XMPP servers like ``jabber.org`` but if you want to
-have `Session support`_ you'll have to set up your own XMPP server.
+have `Session Support`_ you'll have to set up your own XMPP server.
 
 
 You can find a list of public XMPP servers/providers on `xmpp.net`_ and a list of
 You can find a list of public XMPP servers/providers on `xmpp.net`_ and a list of
 servers that you can set up yourself on `xmpp.org`_.
 servers that you can set up yourself on `xmpp.org`_.
@@ -99,7 +99,7 @@ website. This will remove the need for any cross-domain XHR support.
 Server-side authentication
 Server-side authentication
 ==========================
 ==========================
 
 
-Session support
+Session Support
 ---------------
 ---------------
 
 
 It's possible to enable single-site login, whereby users already
 It's possible to enable single-site login, whereby users already
@@ -140,7 +140,7 @@ You'll most likely want to implement some kind of single-signon solution for
 your website, where users authenticate once in your website and then stay
 your website, where users authenticate once in your website and then stay
 logged into their XMPP session upon page reload.
 logged into their XMPP session upon page reload.
 
 
-For more info on this, read `Session support`_.
+For more info on this, read `Session Support`_.
 
 
 You might also want to have more fine-grained control of what gets included in
 You might also want to have more fine-grained control of what gets included in
 the minified Javascript file. Read `Configuration`_ and `Minification`_ for more info on how to do
 the minified Javascript file. Read `Configuration`_ and `Minification`_ for more info on how to do
@@ -260,8 +260,6 @@ There are two ways to add users.
 This setting enables the second mechanism, otherwise by default the first will
 This setting enables the second mechanism, otherwise by default the first will
 be used.
 be used.
 
 
-
-
 ============
 ============
 Minification
 Minification
 ============
 ============
@@ -302,6 +300,91 @@ CSS can be minimized with Yahoo's yuicompressor tool:
     yui-compressor --type=css converse.css -o converse.min.css
     yui-compressor --type=css converse.css -o converse.min.css
 
 
 
 
+============
+Translations
+============
+
+The gettext POT file located in ./locales/converse.pot is the template
+containing all translations and from which for each language an individual PO
+file is generated.
+
+The POT file contains all translateable strings extracted from converse.js.
+
+To make a user facing string translateable, wrap it in the double underscore helper
+function like so:
+
+::
+
+    __('This string will be translated at runtime');
+
+After adding the string, you'll need to regenerate the POT file, like so:
+
+::
+
+    make pot
+
+You can then create or update the PO file for a specific language by doing the following:
+
+::
+
+    msgmerge ./locales/af/LC_MESSAGES/converse.po ./locales/converse.pot -U
+
+This PO file is then what gets translated.
+
+If you've created a new PO file, please make sure to add the following
+attributes at the top of the file (under *Content-Transfer-Encoding*). They are
+required as configuration settings for Jed, the Javascript translations library
+that we're using.
+
+::
+
+    "domain: converse\n"
+    "lang: af\n",
+    "plural_forms: nplurals=2; plural=(n != 1);\n"
+    
+
+Unfortunately Jed cannot use the PO files directly. We have to generate from it 
+a file in JSON format and then put that in a .js file for the specific
+language.
+
+To generate JSON from a PO file, you'll need po2json for node.js. Run the
+following command to install it (npm being the node.js package manager):
+
+::
+
+    npm install po2json
+    
+You can then convert the translations into JSON format:
+
+::
+
+    po2json locales/af/LC_MESSAGES/converse.po locales/af/LC_MESSAGES/converse.json
+
+Now from converse.json paste the data as a value for the "locale_data" key in the
+object in the language's .js file.
+
+So, if you are for example translating into German (language code 'de'), you'll
+create or update the file ./locale/LC_MESSAGES/de.js with the following code:
+
+::
+
+    (function (root, factory) {
+        define("af", ['jed'], function () {
+            return factory(new Jed({
+                "domain": "converse",
+                "locale_data": {
+                    // Paste the JSON data from converse.json here
+                }
+            })
+        }
+    }(this, function (i18n) { 
+        return i18n; 
+    }));
+
+making sure to also paste the JSON data as value to the "locale_data" key.
+
+Congratulations, you've now succesfully added your translations. Sorry for all
+those hoops you had to jump through.
 
 
 
 
 .. _`conversejs.org`: http://conversejs.org
 .. _`conversejs.org`: http://conversejs.org

+ 64 - 5
docs/html/index.html

@@ -73,7 +73,7 @@
 </ul>
 </ul>
 </li>
 </li>
 <li><a class="reference internal" href="#server-side-authentication" id="id6">Server-side authentication</a><ul>
 <li><a class="reference internal" href="#server-side-authentication" id="id6">Server-side authentication</a><ul>
-<li><a class="reference internal" href="#session-support" id="id7">Session support</a></li>
+<li><a class="reference internal" href="#session-support" id="id7">Session Support</a></li>
 </ul>
 </ul>
 </li>
 </li>
 </ul>
 </ul>
@@ -98,6 +98,7 @@
 <li><a class="reference internal" href="#minifying-css" id="id21">Minifying CSS</a></li>
 <li><a class="reference internal" href="#minifying-css" id="id21">Minifying CSS</a></li>
 </ul>
 </ul>
 </li>
 </li>
+<li><a class="reference internal" href="#translations" id="id22">Translations</a></li>
 </ul>
 </ul>
 </div>
 </div>
 <div class="section" id="introduction">
 <div class="section" id="introduction">
@@ -113,7 +114,7 @@ properly configure and integrate it into your site.</p>
 webchat experience and that you have control over the data. The latter being a
 webchat experience and that you have control over the data. The latter being a
 requirement for many sites dealing with sensitive information.</p>
 requirement for many sites dealing with sensitive information.</p>
 <p>You&#8217;ll need to set up your own XMPP server and in order to have
 <p>You&#8217;ll need to set up your own XMPP server and in order to have
-<a class="reference internal" href="#session-support">Session support</a> (i.e. single-signon functionality whereby users are authenticated once and stay
+<a class="reference internal" href="#session-support">Session Support</a> (i.e. single-signon functionality whereby users are authenticated once and stay
 logged in to XMPP upon page reload) you will also have to add some server-side
 logged in to XMPP upon page reload) you will also have to add some server-side
 code.</p>
 code.</p>
 <p>The <a class="reference internal" href="#what-you-will-need">What you will need</a> section has more information on all these
 <p>The <a class="reference internal" href="#what-you-will-need">What you will need</a> section has more information on all these
@@ -126,7 +127,7 @@ requirements.</p>
 <p><em>Converse.js</em> implements <a class="reference external" href="https://en.wikipedia.org/wiki/Xmpp">XMPP</a> as its messaging protocol, and therefore needs
 <p><em>Converse.js</em> implements <a class="reference external" href="https://en.wikipedia.org/wiki/Xmpp">XMPP</a> as its messaging protocol, and therefore needs
 to connect to an XMPP/Jabber server (Jabber is really just a synonym for XMPP).</p>
 to connect to an XMPP/Jabber server (Jabber is really just a synonym for XMPP).</p>
 <p>You can connect to public XMPP servers like <tt class="docutils literal"><span class="pre">jabber.org</span></tt> but if you want to
 <p>You can connect to public XMPP servers like <tt class="docutils literal"><span class="pre">jabber.org</span></tt> but if you want to
-have <a class="reference internal" href="#session-support">Session support</a> you&#8217;ll have to set up your own XMPP server.</p>
+have <a class="reference internal" href="#session-support">Session Support</a> you&#8217;ll have to set up your own XMPP server.</p>
 <p>You can find a list of public XMPP servers/providers on <a class="reference external" href="http://xmpp.net">xmpp.net</a> and a list of
 <p>You can find a list of public XMPP servers/providers on <a class="reference external" href="http://xmpp.net">xmpp.net</a> and a list of
 servers that you can set up yourself on <a class="reference external" href="http://xmpp.org/xmpp-software/servers/">xmpp.org</a>.</p>
 servers that you can set up yourself on <a class="reference external" href="http://xmpp.org/xmpp-software/servers/">xmpp.org</a>.</p>
 </div>
 </div>
@@ -166,7 +167,7 @@ website. This will remove the need for any cross-domain XHR support.</p>
 <div class="section" id="server-side-authentication">
 <div class="section" id="server-side-authentication">
 <h2><a class="toc-backref" href="#id6">Server-side authentication</a><a class="headerlink" href="#server-side-authentication" title="Permalink to this headline">¶</a></h2>
 <h2><a class="toc-backref" href="#id6">Server-side authentication</a><a class="headerlink" href="#server-side-authentication" title="Permalink to this headline">¶</a></h2>
 <div class="section" id="session-support">
 <div class="section" id="session-support">
-<h3><a class="toc-backref" href="#id7">Session support</a><a class="headerlink" href="#session-support" title="Permalink to this headline">¶</a></h3>
+<h3><a class="toc-backref" href="#id7">Session Support</a><a class="headerlink" href="#session-support" title="Permalink to this headline">¶</a></h3>
 <p>It&#8217;s possible to enable single-site login, whereby users already
 <p>It&#8217;s possible to enable single-site login, whereby users already
 authenticated in your website will also automatically be logged in on the chat server,
 authenticated in your website will also automatically be logged in on the chat server,
 but this will require custom code on your server.</p>
 but this will require custom code on your server.</p>
@@ -196,7 +197,7 @@ practical.</p>
 <p>You&#8217;ll most likely want to implement some kind of single-signon solution for
 <p>You&#8217;ll most likely want to implement some kind of single-signon solution for
 your website, where users authenticate once in your website and then stay
 your website, where users authenticate once in your website and then stay
 logged into their XMPP session upon page reload.</p>
 logged into their XMPP session upon page reload.</p>
-<p>For more info on this, read <a class="reference internal" href="#session-support">Session support</a>.</p>
+<p>For more info on this, read <a class="reference internal" href="#session-support">Session Support</a>.</p>
 <p>You might also want to have more fine-grained control of what gets included in
 <p>You might also want to have more fine-grained control of what gets included in
 the minified Javascript file. Read <a class="reference internal" href="#configuration">Configuration</a> and <a class="reference internal" href="#minification">Minification</a> for more info on how to do
 the minified Javascript file. Read <a class="reference internal" href="#configuration">Configuration</a> and <a class="reference internal" href="#minification">Minification</a> for more info on how to do
 that.</p>
 that.</p>
@@ -311,6 +312,64 @@ manager, NPM.</p>
 <div class="highlight-python"><pre>yui-compressor --type=css converse.css -o converse.min.css</pre>
 <div class="highlight-python"><pre>yui-compressor --type=css converse.css -o converse.min.css</pre>
 </div>
 </div>
 </div>
 </div>
+</div>
+<div class="section" id="translations">
+<h1><a class="toc-backref" href="#id22">Translations</a><a class="headerlink" href="#translations" title="Permalink to this headline">¶</a></h1>
+<p>The gettext POT file located in ./locales/converse.pot is the template
+containing all translations and from which for each language an individual PO
+file is generated.</p>
+<p>The POT file contains all translateable strings extracted from converse.js.</p>
+<p>To make a user facing string translateable, wrap it in the double underscore helper
+function like so:</p>
+<div class="highlight-python"><div class="highlight"><pre><span class="n">__</span><span class="p">(</span><span class="s">&#39;This string will be translated at runtime&#39;</span><span class="p">);</span>
+</pre></div>
+</div>
+<p>After adding the string, you&#8217;ll need to regenerate the POT file, like so:</p>
+<div class="highlight-python"><pre>make pot</pre>
+</div>
+<p>You can then create or update the PO file for a specific language by doing the following:</p>
+<div class="highlight-python"><pre>msgmerge ./locales/af/LC_MESSAGES/converse.po ./locales/converse.pot -U</pre>
+</div>
+<p>This PO file is then what gets translated.</p>
+<p>If you&#8217;ve created a new PO file, please make sure to add the following
+attributes at the top of the file (under <em>Content-Transfer-Encoding</em>). They are
+required as configuration settings for Jed, the Javascript translations library
+that we&#8217;re using.</p>
+<div class="highlight-python"><div class="highlight"><pre><span class="s">&quot;domain: converse</span><span class="se">\n</span><span class="s">&quot;</span>
+<span class="s">&quot;lang: af</span><span class="se">\n</span><span class="s">&quot;</span><span class="p">,</span>
+<span class="s">&quot;plural_forms: nplurals=2; plural=(n != 1);</span><span class="se">\n</span><span class="s">&quot;</span>
+</pre></div>
+</div>
+<p>Unfortunately Jed cannot use the PO files directly. We have to generate from it
+a file in JSON format and then put that in a .js file for the specific
+language.</p>
+<p>To generate JSON from a PO file, you&#8217;ll need po2json for node.js. Run the
+following command to install it (npm being the node.js package manager):</p>
+<div class="highlight-python"><pre>npm install po2json</pre>
+</div>
+<p>You can then convert the translations into JSON format:</p>
+<div class="highlight-python"><pre>po2json locales/af/LC_MESSAGES/converse.po locales/af/LC_MESSAGES/converse.json</pre>
+</div>
+<p>Now from converse.json paste the data as a value for the &#8220;locale_data&#8221; key in the
+object in the language&#8217;s .js file.</p>
+<p>So, if you are for example translating into German (language code &#8216;de&#8217;), you&#8217;ll
+create or update the file ./locale/LC_MESSAGES/de.js with the following code:</p>
+<div class="highlight-python"><pre>(function (root, factory) {
+    define("af", ['jed'], function () {
+        return factory(new Jed({
+            "domain": "converse",
+            "locale_data": {
+                // Paste the JSON data from converse.json here
+            }
+        })
+    }
+}(this, function (i18n) {
+    return i18n;
+}));</pre>
+</div>
+<p>making sure to also paste the JSON data as value to the &#8220;locale_data&#8221; key.</p>
+<p>Congratulations, you&#8217;ve now succesfully added your translations. Sorry for all
+those hoops you had to jump through.</p>
 </div>
 </div>
 
 
 
 

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
docs/html/searchindex.js


+ 85 - 2
docs/source/index.rst

@@ -260,8 +260,6 @@ There are two ways to add users.
 This setting enables the second mechanism, otherwise by default the first will
 This setting enables the second mechanism, otherwise by default the first will
 be used.
 be used.
 
 
-
-
 ============
 ============
 Minification
 Minification
 ============
 ============
@@ -302,6 +300,91 @@ CSS can be minimized with Yahoo's yuicompressor tool:
     yui-compressor --type=css converse.css -o converse.min.css
     yui-compressor --type=css converse.css -o converse.min.css
 
 
 
 
+============
+Translations
+============
+
+The gettext POT file located in ./locales/converse.pot is the template
+containing all translations and from which for each language an individual PO
+file is generated.
+
+The POT file contains all translateable strings extracted from converse.js.
+
+To make a user facing string translateable, wrap it in the double underscore helper
+function like so:
+
+::
+
+    __('This string will be translated at runtime');
+
+After adding the string, you'll need to regenerate the POT file, like so:
+
+::
+
+    make pot
+
+You can then create or update the PO file for a specific language by doing the following:
+
+::
+
+    msgmerge ./locales/af/LC_MESSAGES/converse.po ./locales/converse.pot -U
+
+This PO file is then what gets translated.
+
+If you've created a new PO file, please make sure to add the following
+attributes at the top of the file (under *Content-Transfer-Encoding*). They are
+required as configuration settings for Jed, the Javascript translations library
+that we're using.
+
+::
+
+    "domain: converse\n"
+    "lang: af\n"
+    "plural_forms: nplurals=2; plural=(n != 1);\n"
+    
+
+Unfortunately Jed cannot use the PO files directly. We have to generate from it 
+a file in JSON format and then put that in a .js file for the specific
+language.
+
+To generate JSON from a PO file, you'll need po2json for node.js. Run the
+following command to install it (npm being the node.js package manager):
+
+::
+
+    npm install po2json
+    
+You can then convert the translations into JSON format:
+
+::
+
+    po2json locales/af/LC_MESSAGES/converse.po locales/af/LC_MESSAGES/converse.json
+
+Now from converse.json paste the data as a value for the "locale_data" key in the
+object in the language's .js file.
+
+So, if you are for example translating into German (language code 'de'), you'll
+create or update the file ./locale/LC_MESSAGES/de.js with the following code:
+
+::
+
+    (function (root, factory) {
+        define("af", ['jed'], function () {
+            return factory(new Jed({
+                "domain": "converse",
+                "locale_data": {
+                    // Paste the JSON data from converse.json here
+                }
+            })
+        }
+    }(this, function (i18n) { 
+        return i18n; 
+    }));
+
+making sure to also paste the JSON data as value to the "locale_data" key.
+
+Congratulations, you've now succesfully added your translations. Sorry for all
+those hoops you had to jump through.
 
 
 
 
 .. _`conversejs.org`: http://conversejs.org
 .. _`conversejs.org`: http://conversejs.org

+ 1 - 0
index.html

@@ -62,6 +62,7 @@
         <li>Custom status messages</li>
         <li>Custom status messages</li>
         <li>Typing notifications</li>
         <li>Typing notifications</li>
         <li>Third person messages (/me )</li>
         <li>Third person messages (/me )</li>
+        <li>i18n aware</li>
     </ul>
     </ul>
 
 
     <h2>Screencasts</h2>
     <h2>Screencasts</h2>

+ 459 - 0
locale/af/LC_MESSAGES/af.js

@@ -0,0 +1,459 @@
+(function (root, factory) {
+    define("af", ['jed'], function () {
+        var af = new Jed({
+            "domain": "converse",
+            "locale_data": {
+                "converse": {
+                    "": {
+                        "domain": "converse",
+                        "lang": "af",
+                        "plural_forms": "nplurals=2; plural=(n != 1);"
+                    },
+                    "Show this menu": [
+                        null,
+                        "Vertoon hierdie keuselys"
+                    ],
+                    "Write in the third person": [
+                        null,
+                        "Skryf in die derde persoon"
+                    ],
+                    "Remove messages": [
+                        null,
+                        "Verwyder boodskappe"
+                    ],
+                    "Personal message": [
+                        null,
+                        "Persoonlike boodskap"
+                    ],
+                    "Contacts": [
+                        null,
+                        "Kontakte"
+                    ],
+                    "Online": [
+                        null,
+                        "Aanlyn"
+                    ],
+                    "Busy": [
+                        null,
+                        "Besig"
+                    ],
+                    "Away": [
+                        null,
+                        "Weg"
+                    ],
+                    "Offline": [
+                        null,
+                        "Aflyn"
+                    ],
+                    "Click to add new chat contacts": [
+                        null,
+                        "Kliek om nuwe kletskontakte by te voeg"
+                    ],
+                    "Add a contact": [
+                        null,
+                        "Voeg 'n kontak by"
+                    ],
+                    "Contact username": [
+                        null,
+                        "Konak gebruikersnaam"
+                    ],
+                    "Add": [
+                        null,
+                        "Voeg by"
+                    ],
+                    "Contact name": [
+                        null,
+                        "Kontaknaam"
+                    ],
+                    "Search": [
+                        null,
+                        "Soek"
+                    ],
+                    "No users found": [
+                        null,
+                        "Geen gebruikers gevind"
+                    ],
+                    "Click to add as a chat contact": [
+                        null,
+                        "Kliek om as kletskontak by te voeg"
+                    ],
+                    "Click to open this room": [
+                        null,
+                        "Kliek om hierdie kletskamer te open"
+                    ],
+                    "Show more information on this room": [
+                        null,
+                        "Wys meer inligting aangaande hierdie kletskamer"
+                    ],
+                    "Description:": [
+                        null,
+                        "Beskrywing:"
+                    ],
+                    "Occupants:": [
+                        null,
+                        "Deelnemers:"
+                    ],
+                    "Features:": [
+                        null,
+                        "Eienskappe:"
+                    ],
+                    "Requires authentication": [
+                        null,
+                        "Benodig magtiging"
+                    ],
+                    "Hidden": [
+                        null,
+                        "Verskuil"
+                    ],
+                    "Requires an invitation": [
+                        null,
+                        "Benodig 'n uitnodiging"
+                    ],
+                    "Moderated": [
+                        null,
+                        "Gemodereer"
+                    ],
+                    "Non-anonymous": [
+                        null,
+                        "Nie-anoniem"
+                    ],
+                    "Open room": [
+                        null,
+                        "Oop kletskamer"
+                    ],
+                    "Permanent room": [
+                        null,
+                        "Permanente kamer"
+                    ],
+                    "Public": [
+                        null,
+                        "Publiek"
+                    ],
+                    "Semi-anonymous": [
+                        null,
+                        "Deels anoniem"
+                    ],
+                    "Temporary room": [
+                        null,
+                        "Tydelike kamer"
+                    ],
+                    "Unmoderated": [
+                        null,
+                        "Ongemodereer"
+                    ],
+                    "Rooms": [
+                        null,
+                        "Kamers"
+                    ],
+                    "Room name": [
+                        null,
+                        "Kamer naam"
+                    ],
+                    "Nickname": [
+                        null,
+                        "Bynaam"
+                    ],
+                    "Server": [
+                        null,
+                        "Bediener"
+                    ],
+                    "Join": [
+                        null,
+                        "Sluit aan"
+                    ],
+                    "Show rooms": [
+                        null,
+                        "Wys kamers"
+                    ],
+                    "No rooms on %1$s": [
+                        null,
+                        "Geen kamers op %1$s"
+                    ],
+                    "Rooms on %1$s": [
+                        null,
+                        "Kamers op %1$s"
+                    ],
+                    "Set chatroom topic": [
+                        null,
+                        "Stel kletskamer onderwerp"
+                    ],
+                    "Kick user from chatroom": [
+                        null,
+                        "Skop gebruiker uit die kletskamer"
+                    ],
+                    "Ban user from chatroom": [
+                        null,
+                        "Verban gebruiker vanuit die kletskamer"
+                    ],
+                    "Message": [
+                        null,
+                        "Boodskap"
+                    ],
+                    "Save": [
+                        null,
+                        "Stoor"
+                    ],
+                    "Cancel": [
+                        null,
+                        "Kanseleer"
+                    ],
+                    "An error occurred while trying to save the form.": [
+                        null,
+                        "A fout het voorgekom terwyl probeer is om die vorm te stoor."
+                    ],
+                    "This chatroom requires a password": [
+                        null,
+                        "Hiedie kletskamer benodig 'n wagwoord"
+                    ],
+                    "Password: ": [
+                        null,
+                        "Wagwoord:"
+                    ],
+                    "Submit": [
+                        null,
+                        "Dien in"
+                    ],
+                    "This room is not anonymous": [
+                        null,
+                        "Hierdie vertrek is nie anoniem nie"
+                    ],
+                    "This room now shows unavailable members": [
+                        null,
+                        "Hierdie vertrek wys nou onbeskikbare lede"
+                    ],
+                    "This room does not show unavailable members": [
+                        null,
+                        "Hierdie vertrek wys nie onbeskikbare lede nie"
+                    ],
+                    "Non-privacy-related room configuration has changed": [
+                        null,
+                        "Nie-privaatheidverwante kamer instellings het verander"
+                    ],
+                    "Room logging is now enabled": [
+                        null,
+                        "Kamer log is nou aangeskakel"
+                    ],
+                    "Room logging is now disabled": [
+                        null,
+                        "Kamer log is nou afgeskakel"
+                    ],
+                    "This room is now non-anonymous": [
+                        null,
+                        "Hiedie kamer is nou nie anoniem nie"
+                    ],
+                    "This room is now semi-anonymous": [
+                        null,
+                        "Hierdie kamer is nou gedeeltelik anoniem"
+                    ],
+                    "This room is now fully-anonymous": [
+                        null,
+                        "Hierdie kamer is nou ten volle anoniem"
+                    ],
+                    "A new room has been created": [
+                        null,
+                        "'n Nuwe kamer is geskep"
+                    ],
+                    "Your nickname has been changed": [
+                        null,
+                        "Jou bynaam is verander"
+                    ],
+                    "<strong>%1$s</strong> has been banned": [
+                        null,
+                        "<strong>%1$s</strong> is verban"
+                    ],
+                    "<strong>%1$s</strong> has been kicked out": [
+                        null,
+                        "<strong>%1$s</strong> is uitgeskop"
+                    ],
+                    "<strong>%1$s</strong> has been removed because of an affiliation change": [
+                        null,
+                        "<strong>%1$s</strong> is verwyder a.g.v 'n verandering van affiliasie"
+                    ],
+                    "<strong>%1$s</strong> has been removed for not being a member": [
+                        null,
+                        "<strong>%1$s</strong> is nie 'n lid nie, en dus verwyder"
+                    ],
+                    "You have been banned from this room": [
+                        null,
+                        "Jy is uit die kamer verban"
+                    ],
+                    "You have been kicked from this room": [
+                        null,
+                        "Jy is uit die kamer geskop"
+                    ],
+                    "You have been removed from this room because of an affiliation change": [
+                        null,
+                        "Jy is vanuit die kamer verwyder a.g.v 'n verandering van affiliasie"
+                    ],
+                    "You have been removed from this room because the room has changed to members-only and you're not a member": [
+                        null,
+                        "Jy is vanuit die kamer verwyder omdat die kamer nou slegs tot lede beperk word en jy nie 'n lid is nie."
+                    ],
+                    "You have been removed from this room because the MUC (Multi-user chat) service is being shut down.": [
+                        null,
+                        "Jy is van hierdie kamer verwyder aangesien die MUC (Multi-user chat) diens nou afgeskakel word."
+                    ],
+                    "You are not on the member list of this room": [
+                        null,
+                        "Jy is nie op die ledelys van hierdie kamer nie"
+                    ],
+                    "No nickname was specified": [
+                        null,
+                        "Geen bynaam verskaf nie"
+                    ],
+                    "You are not allowed to create new rooms": [
+                        null,
+                        "Jy word nie toegelaat om nog kamers te skep nie"
+                    ],
+                    "Your nickname doesn't conform to this room's policies": [
+                        null,
+                        "Jou bynaam voldoen nie aan die kamer se beleid nie"
+                    ],
+                    "Your nickname is already taken": [
+                        null,
+                        "Jou bynaam is reeds geneem"
+                    ],
+                    "This room does not (yet) exist": [
+                        null,
+                        "Hierdie kamer bestaan tans (nog) nie"
+                    ],
+                    "This room has reached it's maximum number of occupants": [
+                        null,
+                        "Hierdie kamer het sy maksimum aantal deelnemers bereik"
+                    ],
+                    "Topic set by %1$s to: %2$s": [
+                        null,
+                        "Onderwerp deur %1$s bygewerk na: %2$s"
+                    ],
+                    "This user is a moderator": [
+                        null,
+                        "Hierdie gebruiker is 'n moderator"
+                    ],
+                    "This user can send messages in this room": [
+                        null,
+                        "Hierdie gebruiker kan boodskappe na die kamer stuur"
+                    ],
+                    "This user can NOT send messages in this room": [
+                        null,
+                        "Hierdie gebruiker kan NIE boodskappe na die kamer stuur nie"
+                    ],
+                    "Click to chat with this contact": [
+                        null,
+                        "Kliek om met hierdie kontak te klets"
+                    ],
+                    "Click to remove this contact": [
+                        null,
+                        "Kliek om hierdie kontak te verwyder"
+                    ],
+                    "Contact requests": [
+                        null,
+                        "Kontak versoeke"
+                    ],
+                    "My contacts": [
+                        null,
+                        "My kontakte"
+                    ],
+                    "Pending contacts": [
+                        null,
+                        "Hangende kontakte"
+                    ],
+                    "Custom status": [
+                        null,
+                        "Doelgemaakte status"
+                    ],
+                    "Click to change your chat status": [
+                        null,
+                        "Kliek om jou klets-status te verander"
+                    ],
+                    "Click here to write a custom status message": [
+                        null,
+                        "Kliek hier om jou eie statusboodskap te skryf"
+                    ],
+                    "online": [
+                        null,
+                        "aanlyn"
+                    ],
+                    "busy": [
+                        null,
+                        "besig"
+                    ],
+                    "away for long": [
+                        null,
+                        "weg vir lank"
+                    ],
+                    "away": [
+                        null,
+                        "weg"
+                    ],
+                    "I am %1$s": [
+                        null,
+                        "Ek is %1$s"
+                    ],
+                    "Sign in": [
+                        null,
+                        "Teken in"
+                    ],
+                    "XMPP/Jabber Username:": [
+                        null,
+                        "XMPP/Jabber Gebruikersnaam:"
+                    ],
+                    "Password:": [
+                        null,
+                        "Wagwoord"
+                    ],
+                    "Log In": [
+                        null,
+                        "Meld aan"
+                    ],
+                    "BOSH Service URL:": [
+                        null,
+                        "BOSH bediener URL"
+                    ],
+                    "Connected": [
+                        null,
+                        "Verbind"
+                    ],
+                    "Disconnected": [
+                        null,
+                        "Ontkoppel"
+                    ],
+                    "Error": [
+                        null,
+                        "Fout"
+                    ],
+                    "Connecting": [
+                        null,
+                        "Verbind tans"
+                    ],
+                    "Connection Failed": [
+                        null,
+                        "Verbinding het gefaal"
+                    ],
+                    "Authenticating": [
+                        null,
+                        "Besig om te bekragtig"
+                    ],
+                    "Authentication Failed": [
+                        null,
+                        "Bekragtiging het gefaal"
+                    ],
+                    "Disconnecting": [
+                        null,
+                        "Besig om te ontkoppel"
+                    ],
+                    "Attached": [
+                        null,
+                        "Geheg"
+                    ],
+                    "Online Contacts": [
+                        null,
+                        "Kontakte aanlyn"
+                    ]
+                }
+            }
+        });
+        return factory(af);
+    });
+}(this, function (af) { 
+    return af; 
+}));

+ 488 - 0
locale/af/LC_MESSAGES/converse.po

@@ -0,0 +1,488 @@
+# Afrikaans translations for Converse.js package.
+# Copyright (C) 2013 Jan-Carel Brand
+# This file is distributed under the same license as the Converse.js package.
+# JC Brand <jc@opkode.com>, 2013.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Converse.js 0.4\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2013-06-01 23:02+0200\n"
+"PO-Revision-Date: 2013-06-01 23:03+0200\n"
+"Last-Translator: JC Brand <jc@opkode.com>\n"
+"Language-Team: Afrikaans\n"
+"Language: af\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=ASCII\n"
+"Content-Transfer-Encoding: 8bit\n"
+"domain: converse\n"
+"lang: af\n"
+"plural_forms: nplurals=2; plural=(n != 1);\n"
+
+# The last three values are needed by Jed (a Javascript translations library)
+#: converse.js:397 converse.js:1128
+msgid "Show this menu"
+msgstr "Vertoon hierdie keuselys"
+
+#: converse.js:398 converse.js:1129
+msgid "Write in the third person"
+msgstr "Skryf in die derde persoon"
+
+#: converse.js:399 converse.js:1133
+msgid "Remove messages"
+msgstr "Verwyder boodskappe"
+
+#: converse.js:539
+msgid "Personal message"
+msgstr "Persoonlike boodskap"
+
+#: converse.js:613
+msgid "Contacts"
+msgstr "Kontakte"
+
+#: converse.js:618
+msgid "Online"
+msgstr "Aanlyn"
+
+#: converse.js:619
+msgid "Busy"
+msgstr "Besig"
+
+#: converse.js:620
+msgid "Away"
+msgstr "Weg"
+
+#: converse.js:621
+msgid "Offline"
+msgstr "Aflyn"
+
+#: converse.js:628
+msgid "Click to add new chat contacts"
+msgstr "Kliek om nuwe kletskontakte by te voeg"
+
+#: converse.js:628
+msgid "Add a contact"
+msgstr "Voeg 'n kontak by"
+
+#: converse.js:637
+msgid "Contact username"
+msgstr "Konak gebruikersnaam"
+
+#: converse.js:638
+msgid "Add"
+msgstr "Voeg by"
+
+#: converse.js:646
+msgid "Contact name"
+msgstr "Kontaknaam"
+
+#: converse.js:647
+msgid "Search"
+msgstr "Soek"
+
+#: converse.js:682
+msgid "No users found"
+msgstr "Geen gebruikers gevind"
+
+#: converse.js:689
+msgid "Click to add as a chat contact"
+msgstr "Kliek om as kletskontak by te voeg"
+
+#: converse.js:753
+msgid "Click to open this room"
+msgstr "Kliek om hierdie kletskamer te open"
+
+#: converse.js:755
+msgid "Show more information on this room"
+msgstr "Wys meer inligting aangaande hierdie kletskamer"
+
+#: converse.js:760
+msgid "Description:"
+msgstr "Beskrywing:"
+
+#: converse.js:761
+msgid "Occupants:"
+msgstr "Deelnemers:"
+
+#: converse.js:762
+msgid "Features:"
+msgstr "Eienskappe:"
+
+#: converse.js:764
+msgid "Requires authentication"
+msgstr "Benodig magtiging"
+
+#: converse.js:767
+msgid "Hidden"
+msgstr "Verskuil"
+
+#: converse.js:770
+msgid "Requires an invitation"
+msgstr "Benodig 'n uitnodiging"
+
+#: converse.js:773
+msgid "Moderated"
+msgstr "Gemodereer"
+
+#: converse.js:776
+msgid "Non-anonymous"
+msgstr "Nie-anoniem"
+
+#: converse.js:779
+msgid "Open room"
+msgstr "Oop kletskamer"
+
+#: converse.js:782
+msgid "Permanent room"
+msgstr "Permanente kamer"
+
+#: converse.js:785
+msgid "Public"
+msgstr "Publiek"
+
+#: converse.js:788
+msgid "Semi-anonymous"
+msgstr "Deels anoniem"
+
+#: converse.js:791
+msgid "Temporary room"
+msgstr "Tydelike kamer"
+
+#: converse.js:794
+msgid "Unmoderated"
+msgstr "Ongemodereer"
+
+#: converse.js:800
+msgid "Rooms"
+msgstr "Kamers"
+
+#: converse.js:804
+msgid "Room name"
+msgstr "Kamer naam"
+
+#: converse.js:805
+msgid "Nickname"
+msgstr "Bynaam"
+
+#: converse.js:806
+msgid "Server"
+msgstr "Bediener"
+
+#: converse.js:807
+msgid "Join"
+msgstr "Sluit aan"
+
+#: converse.js:808
+msgid "Show rooms"
+msgstr "Wys kamers"
+
+#. For translators: %1$s is a variable and will be replaced with the XMPP server name
+#: converse.js:841
+msgid "No rooms on %1$s"
+msgstr "Geen kamers op %1$s"
+
+#. For translators: %1$s is a variable and will be
+#. replaced with the XMPP server name
+#: converse.js:856
+msgid "Rooms on %1$s"
+msgstr "Kamers op %1$s"
+
+#: converse.js:1130
+msgid "Set chatroom topic"
+msgstr "Stel kletskamer onderwerp"
+
+#: converse.js:1131
+msgid "Kick user from chatroom"
+msgstr "Skop gebruiker uit die kletskamer"
+
+#: converse.js:1132
+msgid "Ban user from chatroom"
+msgstr "Verban gebruiker vanuit die kletskamer"
+
+#: converse.js:1159
+msgid "Message"
+msgstr "Boodskap"
+
+#: converse.js:1273 converse.js:2318
+msgid "Save"
+msgstr "Stoor"
+
+#: converse.js:1274
+msgid "Cancel"
+msgstr "Kanseleer"
+
+#: converse.js:1321
+msgid "An error occurred while trying to save the form."
+msgstr "A fout het voorgekom terwyl probeer is om die vorm te stoor."
+
+#: converse.js:1367
+msgid "This chatroom requires a password"
+msgstr "Hiedie kletskamer benodig 'n wagwoord"
+
+#: converse.js:1368
+msgid "Password: "
+msgstr "Wagwoord:"
+
+#: converse.js:1369
+msgid "Submit"
+msgstr "Dien in"
+
+#: converse.js:1383
+msgid "This room is not anonymous"
+msgstr "Hierdie vertrek is nie anoniem nie"
+
+#: converse.js:1384
+msgid "This room now shows unavailable members"
+msgstr "Hierdie vertrek wys nou onbeskikbare lede"
+
+#: converse.js:1385
+msgid "This room does not show unavailable members"
+msgstr "Hierdie vertrek wys nie onbeskikbare lede nie"
+
+#: converse.js:1386
+msgid "Non-privacy-related room configuration has changed"
+msgstr "Nie-privaatheidverwante kamer instellings het verander"
+
+#: converse.js:1387
+msgid "Room logging is now enabled"
+msgstr "Kamer log is nou aangeskakel"
+
+#: converse.js:1388
+msgid "Room logging is now disabled"
+msgstr "Kamer log is nou afgeskakel"
+
+#: converse.js:1389
+msgid "This room is now non-anonymous"
+msgstr "Hiedie kamer is nou nie anoniem nie"
+
+#: converse.js:1390
+msgid "This room is now semi-anonymous"
+msgstr "Hierdie kamer is nou gedeeltelik anoniem"
+
+#: converse.js:1391
+msgid "This room is now fully-anonymous"
+msgstr "Hierdie kamer is nou ten volle anoniem"
+
+#: converse.js:1392
+msgid "A new room has been created"
+msgstr "'n Nuwe kamer is geskep"
+
+#: converse.js:1393
+msgid "Your nickname has been changed"
+msgstr "Jou bynaam is verander"
+
+#. For translations: %1$s will be replaced with the user's nickname
+#. Don't translate "strong"
+#. Example: <strong>jcbrand</strong> has been banned
+#: converse.js:1400
+msgid "<strong>%1$s</strong> has been banned"
+msgstr "<strong>%1$s</strong> is verban"
+
+#. For translations: %1$s will be replaced with the user's nickname
+#. Don't translate "strong"
+#. Example: <strong>jcbrand</strong> has been kicked out
+#: converse.js:1404
+msgid "<strong>%1$s</strong> has been kicked out"
+msgstr "<strong>%1$s</strong> is uitgeskop"
+
+#. For translations: %1$s will be replaced with the user's nickname
+#. Don't translate "strong"
+#. Example: <strong>jcbrand</strong> has been removed because of an affiliasion change
+#: converse.js:1408
+msgid "<strong>%1$s</strong> has been removed because of an affiliation change"
+msgstr "<strong>%1$s</strong> is verwyder a.g.v 'n verandering van affiliasie"
+
+#. For translations: %1$s will be replaced with the user's nickname
+#. Don't translate "strong"
+#. Example: <strong>jcbrand</strong> has been removed for not being a member
+#: converse.js:1412
+msgid "<strong>%1$s</strong> has been removed for not being a member"
+msgstr "<strong>%1$s</strong> is nie 'n lid nie, en dus verwyder"
+
+#: converse.js:1416 converse.js:1478
+msgid "You have been banned from this room"
+msgstr "Jy is uit die kamer verban"
+
+#: converse.js:1417
+msgid "You have been kicked from this room"
+msgstr "Jy is uit die kamer geskop"
+
+#: converse.js:1418
+msgid "You have been removed from this room because of an affiliation change"
+msgstr "Jy is vanuit die kamer verwyder a.g.v 'n verandering van affiliasie"
+
+#: converse.js:1419
+msgid ""
+"You have been removed from this room because the room has changed to members-"
+"only and you're not a member"
+msgstr ""
+"Jy is vanuit die kamer verwyder omdat die kamer nou slegs tot lede beperk "
+"word en jy nie 'n lid is nie."
+
+#: converse.js:1420
+msgid ""
+"You have been removed from this room because the MUC (Multi-user chat) "
+"service is being shut down."
+msgstr ""
+"Jy is van hierdie kamer verwyder aangesien die MUC (Multi-user chat) diens "
+"nou afgeskakel word."
+
+#: converse.js:1476
+msgid "You are not on the member list of this room"
+msgstr "Jy is nie op die ledelys van hierdie kamer nie"
+
+#: converse.js:1482
+msgid "No nickname was specified"
+msgstr "Geen bynaam verskaf nie"
+
+#: converse.js:1486
+msgid "You are not allowed to create new rooms"
+msgstr "Jy word nie toegelaat om nog kamers te skep nie"
+
+#: converse.js:1488
+msgid "Your nickname doesn't conform to this room's policies"
+msgstr "Jou bynaam voldoen nie aan die kamer se beleid nie"
+
+#: converse.js:1490
+msgid "Your nickname is already taken"
+msgstr "Jou bynaam is reeds geneem"
+
+#: converse.js:1492
+msgid "This room does not (yet) exist"
+msgstr "Hierdie kamer bestaan tans (nog) nie"
+
+#: converse.js:1494
+msgid "This room has reached it's maximum number of occupants"
+msgstr "Hierdie kamer het sy maksimum aantal deelnemers bereik"
+
+#. For translators: the %1$s and %2$s parts will get replaced by the user and topic text respectively
+#. Example: Topic set by JC Brand to: Hello World!
+#: converse.js:1571
+msgid "Topic set by %1$s to: %2$s"
+msgstr "Onderwerp deur %1$s bygewerk na: %2$s"
+
+#: converse.js:1587
+msgid "This user is a moderator"
+msgstr "Hierdie gebruiker is 'n moderator"
+
+#: converse.js:1590
+msgid "This user can send messages in this room"
+msgstr "Hierdie gebruiker kan boodskappe na die kamer stuur"
+
+#: converse.js:1593
+msgid "This user can NOT send messages in this room"
+msgstr "Hierdie gebruiker kan NIE boodskappe na die kamer stuur nie"
+
+#: converse.js:1796
+msgid "Click to chat with this contact"
+msgstr "Kliek om met hierdie kontak te klets"
+
+#: converse.js:1797 converse.js:1801
+msgid "Click to remove this contact"
+msgstr "Kliek om hierdie kontak te verwyder"
+
+#: converse.js:2163
+msgid "Contact requests"
+msgstr "Kontak versoeke"
+
+#: converse.js:2164
+msgid "My contacts"
+msgstr "My kontakte"
+
+#: converse.js:2165
+msgid "Pending contacts"
+msgstr "Hangende kontakte"
+
+#: converse.js:2317
+msgid "Custom status"
+msgstr "Doelgemaakte status"
+
+#: converse.js:2323
+msgid "Click to change your chat status"
+msgstr "Kliek om jou klets-status te verander"
+
+#: converse.js:2326
+msgid "Click here to write a custom status message"
+msgstr "Kliek hier om jou eie statusboodskap te skryf"
+
+#: converse.js:2355 converse.js:2363
+msgid "online"
+msgstr "aanlyn"
+
+#: converse.js:2357
+msgid "busy"
+msgstr "besig"
+
+#: converse.js:2359
+msgid "away for long"
+msgstr "weg vir lank"
+
+#: converse.js:2361
+msgid "away"
+msgstr "weg"
+
+#. For translators: the %1$s part gets replaced with the status
+#. Example, I am online
+#: converse.js:2375 converse.js:2409
+msgid "I am %1$s"
+msgstr "Ek is %1$s"
+
+#: converse.js:2480
+msgid "Sign in"
+msgstr "Teken in"
+
+#: converse.js:2483
+msgid "XMPP/Jabber Username:"
+msgstr "XMPP/Jabber Gebruikersnaam:"
+
+#: converse.js:2485
+msgid "Password:"
+msgstr "Wagwoord"
+
+#: converse.js:2487
+msgid "Log In"
+msgstr "Meld aan"
+
+#: converse.js:2491
+msgid "BOSH Service URL:"
+msgstr "BOSH bediener URL"
+
+#: converse.js:2503
+msgid "Connected"
+msgstr "Verbind"
+
+#: converse.js:2507
+msgid "Disconnected"
+msgstr "Ontkoppel"
+
+#: converse.js:2511
+msgid "Error"
+msgstr "Fout"
+
+#: converse.js:2513
+msgid "Connecting"
+msgstr "Verbind tans"
+
+#: converse.js:2516
+msgid "Connection Failed"
+msgstr "Verbinding het gefaal"
+
+#: converse.js:2518
+msgid "Authenticating"
+msgstr "Besig om te bekragtig"
+
+#: converse.js:2521
+msgid "Authentication Failed"
+msgstr "Bekragtiging het gefaal"
+
+#: converse.js:2523
+msgid "Disconnecting"
+msgstr "Besig om te ontkoppel"
+
+#: converse.js:2525
+msgid "Attached"
+msgstr "Geheg"
+
+#: converse.js:2656
+msgid "Online Contacts"
+msgstr "Kontakte aanlyn"

+ 481 - 0
locale/converse.pot

@@ -0,0 +1,481 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR Jan-Carel Brand
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Converse.js 0.4\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2013-06-01 23:03+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=CHARSET\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: converse.js:397 converse.js:1128
+msgid "Show this menu"
+msgstr ""
+
+#: converse.js:398 converse.js:1129
+msgid "Write in the third person"
+msgstr ""
+
+#: converse.js:399 converse.js:1133
+msgid "Remove messages"
+msgstr ""
+
+#: converse.js:539
+msgid "Personal message"
+msgstr ""
+
+#: converse.js:613
+msgid "Contacts"
+msgstr ""
+
+#: converse.js:618
+msgid "Online"
+msgstr ""
+
+#: converse.js:619
+msgid "Busy"
+msgstr ""
+
+#: converse.js:620
+msgid "Away"
+msgstr ""
+
+#: converse.js:621
+msgid "Offline"
+msgstr ""
+
+#: converse.js:628
+msgid "Click to add new chat contacts"
+msgstr ""
+
+#: converse.js:628
+msgid "Add a contact"
+msgstr ""
+
+#: converse.js:637
+msgid "Contact username"
+msgstr ""
+
+#: converse.js:638
+msgid "Add"
+msgstr ""
+
+#: converse.js:646
+msgid "Contact name"
+msgstr ""
+
+#: converse.js:647
+msgid "Search"
+msgstr ""
+
+#: converse.js:682
+msgid "No users found"
+msgstr ""
+
+#: converse.js:689
+msgid "Click to add as a chat contact"
+msgstr ""
+
+#: converse.js:753
+msgid "Click to open this room"
+msgstr ""
+
+#: converse.js:755
+msgid "Show more information on this room"
+msgstr ""
+
+#: converse.js:760
+msgid "Description:"
+msgstr ""
+
+#: converse.js:761
+msgid "Occupants:"
+msgstr ""
+
+#: converse.js:762
+msgid "Features:"
+msgstr ""
+
+#: converse.js:764
+msgid "Requires authentication"
+msgstr ""
+
+#: converse.js:767
+msgid "Hidden"
+msgstr ""
+
+#: converse.js:770
+msgid "Requires an invitation"
+msgstr ""
+
+#: converse.js:773
+msgid "Moderated"
+msgstr ""
+
+#: converse.js:776
+msgid "Non-anonymous"
+msgstr ""
+
+#: converse.js:779
+msgid "Open room"
+msgstr ""
+
+#: converse.js:782
+msgid "Permanent room"
+msgstr ""
+
+#: converse.js:785
+msgid "Public"
+msgstr ""
+
+#: converse.js:788
+msgid "Semi-anonymous"
+msgstr ""
+
+#: converse.js:791
+msgid "Temporary room"
+msgstr ""
+
+#: converse.js:794
+msgid "Unmoderated"
+msgstr ""
+
+#: converse.js:800
+msgid "Rooms"
+msgstr ""
+
+#: converse.js:804
+msgid "Room name"
+msgstr ""
+
+#: converse.js:805
+msgid "Nickname"
+msgstr ""
+
+#: converse.js:806
+msgid "Server"
+msgstr ""
+
+#: converse.js:807
+msgid "Join"
+msgstr ""
+
+#: converse.js:808
+msgid "Show rooms"
+msgstr ""
+
+#. For translators: %1$s is a variable and will be replaced with the XMPP server name
+#: converse.js:841
+msgid "No rooms on %1$s"
+msgstr ""
+
+#. For translators: %1$s is a variable and will be
+#. replaced with the XMPP server name
+#: converse.js:856
+msgid "Rooms on %1$s"
+msgstr ""
+
+#: converse.js:1130
+msgid "Set chatroom topic"
+msgstr ""
+
+#: converse.js:1131
+msgid "Kick user from chatroom"
+msgstr ""
+
+#: converse.js:1132
+msgid "Ban user from chatroom"
+msgstr ""
+
+#: converse.js:1159
+msgid "Message"
+msgstr ""
+
+#: converse.js:1273 converse.js:2318
+msgid "Save"
+msgstr ""
+
+#: converse.js:1274
+msgid "Cancel"
+msgstr ""
+
+#: converse.js:1321
+msgid "An error occurred while trying to save the form."
+msgstr ""
+
+#: converse.js:1367
+msgid "This chatroom requires a password"
+msgstr ""
+
+#: converse.js:1368
+msgid "Password: "
+msgstr ""
+
+#: converse.js:1369
+msgid "Submit"
+msgstr ""
+
+#: converse.js:1383
+msgid "This room is not anonymous"
+msgstr ""
+
+#: converse.js:1384
+msgid "This room now shows unavailable members"
+msgstr ""
+
+#: converse.js:1385
+msgid "This room does not show unavailable members"
+msgstr ""
+
+#: converse.js:1386
+msgid "Non-privacy-related room configuration has changed"
+msgstr ""
+
+#: converse.js:1387
+msgid "Room logging is now enabled"
+msgstr ""
+
+#: converse.js:1388
+msgid "Room logging is now disabled"
+msgstr ""
+
+#: converse.js:1389
+msgid "This room is now non-anonymous"
+msgstr ""
+
+#: converse.js:1390
+msgid "This room is now semi-anonymous"
+msgstr ""
+
+#: converse.js:1391
+msgid "This room is now fully-anonymous"
+msgstr ""
+
+#: converse.js:1392
+msgid "A new room has been created"
+msgstr ""
+
+#: converse.js:1393
+msgid "Your nickname has been changed"
+msgstr ""
+
+#. For translations: %1$s will be replaced with the user's nickname
+#. Don't translate "strong"
+#. Example: <strong>jcbrand</strong> has been banned
+#: converse.js:1400
+msgid "<strong>%1$s</strong> has been banned"
+msgstr ""
+
+#. For translations: %1$s will be replaced with the user's nickname
+#. Don't translate "strong"
+#. Example: <strong>jcbrand</strong> has been kicked out
+#: converse.js:1404
+msgid "<strong>%1$s</strong> has been kicked out"
+msgstr ""
+
+#. For translations: %1$s will be replaced with the user's nickname
+#. Don't translate "strong"
+#. Example: <strong>jcbrand</strong> has been removed because of an affiliasion change
+#: converse.js:1408
+msgid "<strong>%1$s</strong> has been removed because of an affiliation change"
+msgstr ""
+
+#. For translations: %1$s will be replaced with the user's nickname
+#. Don't translate "strong"
+#. Example: <strong>jcbrand</strong> has been removed for not being a member
+#: converse.js:1412
+msgid "<strong>%1$s</strong> has been removed for not being a member"
+msgstr ""
+
+#: converse.js:1416 converse.js:1478
+msgid "You have been banned from this room"
+msgstr ""
+
+#: converse.js:1417
+msgid "You have been kicked from this room"
+msgstr ""
+
+#: converse.js:1418
+msgid "You have been removed from this room because of an affiliation change"
+msgstr ""
+
+#: converse.js:1419
+msgid ""
+"You have been removed from this room because the room has changed to members-"
+"only and you're not a member"
+msgstr ""
+
+#: converse.js:1420
+msgid ""
+"You have been removed from this room because the MUC (Multi-user chat) "
+"service is being shut down."
+msgstr ""
+
+#: converse.js:1476
+msgid "You are not on the member list of this room"
+msgstr ""
+
+#: converse.js:1482
+msgid "No nickname was specified"
+msgstr ""
+
+#: converse.js:1486
+msgid "You are not allowed to create new rooms"
+msgstr ""
+
+#: converse.js:1488
+msgid "Your nickname doesn't conform to this room's policies"
+msgstr ""
+
+#: converse.js:1490
+msgid "Your nickname is already taken"
+msgstr ""
+
+#: converse.js:1492
+msgid "This room does not (yet) exist"
+msgstr ""
+
+#: converse.js:1494
+msgid "This room has reached it's maximum number of occupants"
+msgstr ""
+
+#. For translators: the %1$s and %2$s parts will get replaced by the user and topic text respectively
+#. Example: Topic set by JC Brand to: Hello World!
+#: converse.js:1571
+msgid "Topic set by %1$s to: %2$s"
+msgstr ""
+
+#: converse.js:1587
+msgid "This user is a moderator"
+msgstr ""
+
+#: converse.js:1590
+msgid "This user can send messages in this room"
+msgstr ""
+
+#: converse.js:1593
+msgid "This user can NOT send messages in this room"
+msgstr ""
+
+#: converse.js:1796
+msgid "Click to chat with this contact"
+msgstr ""
+
+#: converse.js:1797 converse.js:1801
+msgid "Click to remove this contact"
+msgstr ""
+
+#: converse.js:2163
+msgid "Contact requests"
+msgstr ""
+
+#: converse.js:2164
+msgid "My contacts"
+msgstr ""
+
+#: converse.js:2165
+msgid "Pending contacts"
+msgstr ""
+
+#: converse.js:2317
+msgid "Custom status"
+msgstr ""
+
+#: converse.js:2323
+msgid "Click to change your chat status"
+msgstr ""
+
+#: converse.js:2326
+msgid "Click here to write a custom status message"
+msgstr ""
+
+#: converse.js:2355 converse.js:2363
+msgid "online"
+msgstr ""
+
+#: converse.js:2357
+msgid "busy"
+msgstr ""
+
+#: converse.js:2359
+msgid "away for long"
+msgstr ""
+
+#: converse.js:2361
+msgid "away"
+msgstr ""
+
+#. For translators: the %1$s part gets replaced with the status
+#. Example, I am online
+#: converse.js:2375 converse.js:2409
+msgid "I am %1$s"
+msgstr ""
+
+#: converse.js:2480
+msgid "Sign in"
+msgstr ""
+
+#: converse.js:2483
+msgid "XMPP/Jabber Username:"
+msgstr ""
+
+#: converse.js:2485
+msgid "Password:"
+msgstr ""
+
+#: converse.js:2487
+msgid "Log In"
+msgstr ""
+
+#: converse.js:2491
+msgid "BOSH Service URL:"
+msgstr ""
+
+#: converse.js:2503
+msgid "Connected"
+msgstr ""
+
+#: converse.js:2507
+msgid "Disconnected"
+msgstr ""
+
+#: converse.js:2511
+msgid "Error"
+msgstr ""
+
+#: converse.js:2513
+msgid "Connecting"
+msgstr ""
+
+#: converse.js:2516
+msgid "Connection Failed"
+msgstr ""
+
+#: converse.js:2518
+msgid "Authenticating"
+msgstr ""
+
+#: converse.js:2521
+msgid "Authentication Failed"
+msgstr ""
+
+#: converse.js:2523
+msgid "Disconnecting"
+msgstr ""
+
+#: converse.js:2525
+msgid "Attached"
+msgstr ""
+
+#: converse.js:2656
+msgid "Online Contacts"
+msgstr ""

+ 442 - 0
locale/de/LC_MESSAGES/converse.po

@@ -0,0 +1,442 @@
+# German translations for Converse.js package.
+# Copyright (C) 2013 Jan-Carel Brand
+# This file is distributed under the same license as the Converse.js package.
+# JC Brand <jc@opkode.com>, 2013.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Converse.js 0.4\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2013-06-01 10:36+0200\n"
+"PO-Revision-Date: 2013-06-01 10:48+0200\n"
+"Last-Translator: JC Brand <jc@opkode.com>\n"
+"Language-Team: German\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=ASCII\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: converse.js:416 converse.js:1141
+msgid "Show this menu"
+msgstr ""
+
+#: converse.js:417 converse.js:1142
+msgid "Write in the third person"
+msgstr ""
+
+#: converse.js:418 converse.js:1146
+msgid "Remove messages"
+msgstr ""
+
+#: converse.js:558
+msgid "Personal message"
+msgstr ""
+
+#: converse.js:632
+msgid "Contacts"
+msgstr ""
+
+#: converse.js:637
+msgid "Online"
+msgstr ""
+
+#: converse.js:638
+msgid "Busy"
+msgstr ""
+
+#: converse.js:639
+msgid "Away"
+msgstr ""
+
+#: converse.js:640
+msgid "Offline"
+msgstr ""
+
+#: converse.js:647
+msgid "Click to add new chat contacts"
+msgstr ""
+
+#: converse.js:647
+msgid "Add a contact"
+msgstr ""
+
+#: converse.js:656
+msgid "Contact username"
+msgstr ""
+
+#: converse.js:657
+msgid "Add"
+msgstr ""
+
+#: converse.js:665
+msgid "Contact name"
+msgstr ""
+
+#: converse.js:666
+msgid "Search"
+msgstr ""
+
+#: converse.js:701
+msgid "No users found"
+msgstr ""
+
+#: converse.js:708
+msgid "Click to add as a chat contact"
+msgstr ""
+
+#: converse.js:772
+msgid "Click to open this room"
+msgstr ""
+
+#: converse.js:774
+msgid "Show more information on this room"
+msgstr ""
+
+#: converse.js:779
+msgid "Description:"
+msgstr ""
+
+#: converse.js:780
+msgid "Occupants:"
+msgstr ""
+
+#: converse.js:781
+msgid "Features:"
+msgstr ""
+
+#: converse.js:783
+msgid "Requires authentication"
+msgstr ""
+
+#: converse.js:786
+msgid "Hidden"
+msgstr ""
+
+#: converse.js:789
+msgid "Requires an invitation"
+msgstr ""
+
+#: converse.js:792
+msgid "Moderated"
+msgstr ""
+
+#: converse.js:795
+msgid "Non-anonymous"
+msgstr ""
+
+#: converse.js:798
+msgid "Open room"
+msgstr ""
+
+#: converse.js:801
+msgid "Permanent room"
+msgstr ""
+
+#: converse.js:804
+msgid "Public"
+msgstr ""
+
+#: converse.js:807
+msgid "Semi-anonymous"
+msgstr ""
+
+#: converse.js:810
+msgid "Temporary room"
+msgstr ""
+
+#: converse.js:813
+msgid "Unmoderated"
+msgstr ""
+
+#: converse.js:819
+msgid "Rooms"
+msgstr ""
+
+#: converse.js:823
+msgid "Room name"
+msgstr ""
+
+#: converse.js:824
+msgid "Nickname"
+msgstr ""
+
+#: converse.js:825
+msgid "Server"
+msgstr ""
+
+#: converse.js:826
+msgid "Join"
+msgstr ""
+
+#: converse.js:827
+msgid "Show rooms"
+msgstr ""
+
+#: converse.js:1143
+msgid "Set chatroom topic"
+msgstr ""
+
+#: converse.js:1144
+msgid "Kick user from chatroom"
+msgstr ""
+
+#: converse.js:1145
+msgid "Ban user from chatroom"
+msgstr ""
+
+#: converse.js:1172
+msgid "Message"
+msgstr ""
+
+#: converse.js:1286 converse.js:2321
+msgid "Save"
+msgstr ""
+
+#: converse.js:1287
+msgid "Cancel"
+msgstr ""
+
+#: converse.js:1334
+msgid "An error occurred while trying to save the form."
+msgstr ""
+
+#: converse.js:1380
+msgid "This chat room requires a password"
+msgstr ""
+
+#: converse.js:1381
+msgid "Password: "
+msgstr ""
+
+#: converse.js:1382
+msgid "Submit"
+msgstr ""
+
+#: converse.js:1396
+msgid "This room is not anonymous"
+msgstr ""
+
+#: converse.js:1397
+msgid "This room now shows unavailable members"
+msgstr ""
+
+#: converse.js:1398
+msgid "This room does not show unavailable members"
+msgstr ""
+
+#: converse.js:1399
+msgid "Non-privacy-related room configuration has changed"
+msgstr ""
+
+#: converse.js:1400
+msgid "Room logging is now enabled"
+msgstr ""
+
+#: converse.js:1401
+msgid "Room logging is now disabled"
+msgstr ""
+
+#: converse.js:1402
+msgid "This room is now non-anonymous"
+msgstr ""
+
+#: converse.js:1403
+msgid "This room is now semi-anonymous"
+msgstr ""
+
+#: converse.js:1404
+msgid "This room is now fully-anonymous"
+msgstr ""
+
+#: converse.js:1405
+msgid "A new room has been created"
+msgstr ""
+
+#: converse.js:1406
+msgid "Your nickname has been changed"
+msgstr ""
+
+#: converse.js:1410
+msgid " has been banned"
+msgstr ""
+
+#: converse.js:1411
+msgid " has been kicked out"
+msgstr ""
+
+#: converse.js:1412
+msgid " has been removed because of an affiliation change"
+msgstr ""
+
+#: converse.js:1413
+msgid " has been removed for not being a member"
+msgstr ""
+
+#: converse.js:1417 converse.js:1480
+msgid "You have been banned from this room"
+msgstr ""
+
+#: converse.js:1418
+msgid "You have been kicked from this room"
+msgstr ""
+
+#: converse.js:1419
+msgid "You have been removed from this room because of an affiliation change"
+msgstr ""
+
+#: converse.js:1420
+msgid ""
+"You have been removed from this room because the room has changed to members-"
+"only and you're not a member"
+msgstr ""
+
+#: converse.js:1421
+msgid ""
+"You have been removed from this room because the MUC (Multi-user chat) "
+"service is being shut down."
+msgstr ""
+
+#: converse.js:1478
+msgid "You are not on the member list of this room"
+msgstr ""
+
+#: converse.js:1484
+msgid "No nickname was specified"
+msgstr ""
+
+#: converse.js:1488
+msgid "You are not allowed to create new rooms"
+msgstr ""
+
+#: converse.js:1490
+msgid "Your nickname doesn't conform to this room's policies"
+msgstr ""
+
+#: converse.js:1492
+msgid "Your nickname is already taken"
+msgstr ""
+
+#: converse.js:1494
+msgid "This room does not (yet) exist"
+msgstr ""
+
+#: converse.js:1496
+msgid "This room has reached it's maximum number of occupants"
+msgstr ""
+
+#. For translators: the %1$s and %2$s parts will get replaced by the user and topic text respectively
+#. Example: Topic set by JC Brand to: Hello World!
+#: converse.js:1573
+msgid "Topic set by %1$s to: %2$s"
+msgstr ""
+
+#: converse.js:1589
+msgid "This user is a moderator"
+msgstr ""
+
+#: converse.js:1592
+msgid "This user can send messages in this room"
+msgstr ""
+
+#: converse.js:1595
+msgid "This user can NOT send messages in this room"
+msgstr ""
+
+#: converse.js:1799
+msgid "Click to chat with this contact"
+msgstr ""
+
+#: converse.js:1800 converse.js:1804
+msgid "Click to remove this contact"
+msgstr ""
+
+#: converse.js:2166
+msgid "Contact requests"
+msgstr ""
+
+#: converse.js:2167
+msgid "My contacts"
+msgstr ""
+
+#: converse.js:2168
+msgid "Pending contacts"
+msgstr ""
+
+#: converse.js:2320
+msgid "Custom status"
+msgstr ""
+
+#: converse.js:2326
+msgid "Click to change your chat status"
+msgstr ""
+
+#: converse.js:2329
+msgid "Click here to write a custom status message"
+msgstr ""
+
+#. For translators: the %1$s part gets replaced with the status
+#. Example, I am online
+#: converse.js:2376 converse.js:2410
+msgid "I am %1$s"
+msgstr ""
+
+#: converse.js:2481
+msgid "Sign in"
+msgstr ""
+
+#: converse.js:2484
+msgid "XMPP/Jabber Username:"
+msgstr ""
+
+#: converse.js:2486
+msgid "Password:"
+msgstr ""
+
+#: converse.js:2488
+msgid "Log In"
+msgstr ""
+
+#: converse.js:2492
+msgid "BOSH Service URL:"
+msgstr ""
+
+#: converse.js:2504
+msgid "Connected"
+msgstr ""
+
+#: converse.js:2508
+msgid "Disconnected"
+msgstr ""
+
+#: converse.js:2512
+msgid "Error"
+msgstr ""
+
+#: converse.js:2514
+msgid "Connecting"
+msgstr ""
+
+#: converse.js:2517
+msgid "Connection Failed"
+msgstr ""
+
+#: converse.js:2519
+msgid "Authenticating"
+msgstr ""
+
+#: converse.js:2522
+msgid "Authentication Failed"
+msgstr ""
+
+#: converse.js:2524
+msgid "Disconnecting"
+msgstr ""
+
+#: converse.js:2526
+msgid "Attached"
+msgstr ""
+
+#: converse.js:2657
+msgid "Online Contacts"
+msgstr ""

+ 19 - 0
locale/en/LC_MESSAGES/en.js

@@ -0,0 +1,19 @@
+(function (root, factory) {
+    define("en", ['jed'], function () {
+        var en = new Jed({
+            "domain": "converse",
+            "locale_data": {
+                "converse": {
+                    "": {
+                        "domain": "converse",
+                        "lang": "en",
+                        "plural_forms": "nplurals=2; plural=(n != 1);"
+                    }
+                }
+            }
+        });
+        return factory(en);
+    });
+}(this, function (en) { 
+    return en;
+}));

+ 442 - 0
locale/nl/LC_MESSAGES/converse.po

@@ -0,0 +1,442 @@
+# Dutch translations for Converse.js package.
+# Copyright (C) 2013 Jan-Carel Brand
+# This file is distributed under the same license as the Converse.js package.
+# JC Brand <jc@opkode.com>, 2013.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Converse.js 0.4\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2013-06-01 10:36+0200\n"
+"PO-Revision-Date: 2013-06-01 10:48+0200\n"
+"Last-Translator: JC Brand <jc@opkode.com>\n"
+"Language-Team: Dutch\n"
+"Language: nl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=ASCII\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: converse.js:416 converse.js:1141
+msgid "Show this menu"
+msgstr ""
+
+#: converse.js:417 converse.js:1142
+msgid "Write in the third person"
+msgstr ""
+
+#: converse.js:418 converse.js:1146
+msgid "Remove messages"
+msgstr ""
+
+#: converse.js:558
+msgid "Personal message"
+msgstr ""
+
+#: converse.js:632
+msgid "Contacts"
+msgstr ""
+
+#: converse.js:637
+msgid "Online"
+msgstr ""
+
+#: converse.js:638
+msgid "Busy"
+msgstr ""
+
+#: converse.js:639
+msgid "Away"
+msgstr ""
+
+#: converse.js:640
+msgid "Offline"
+msgstr ""
+
+#: converse.js:647
+msgid "Click to add new chat contacts"
+msgstr ""
+
+#: converse.js:647
+msgid "Add a contact"
+msgstr ""
+
+#: converse.js:656
+msgid "Contact username"
+msgstr ""
+
+#: converse.js:657
+msgid "Add"
+msgstr ""
+
+#: converse.js:665
+msgid "Contact name"
+msgstr ""
+
+#: converse.js:666
+msgid "Search"
+msgstr ""
+
+#: converse.js:701
+msgid "No users found"
+msgstr ""
+
+#: converse.js:708
+msgid "Click to add as a chat contact"
+msgstr ""
+
+#: converse.js:772
+msgid "Click to open this room"
+msgstr ""
+
+#: converse.js:774
+msgid "Show more information on this room"
+msgstr ""
+
+#: converse.js:779
+msgid "Description:"
+msgstr ""
+
+#: converse.js:780
+msgid "Occupants:"
+msgstr ""
+
+#: converse.js:781
+msgid "Features:"
+msgstr ""
+
+#: converse.js:783
+msgid "Requires authentication"
+msgstr ""
+
+#: converse.js:786
+msgid "Hidden"
+msgstr ""
+
+#: converse.js:789
+msgid "Requires an invitation"
+msgstr ""
+
+#: converse.js:792
+msgid "Moderated"
+msgstr ""
+
+#: converse.js:795
+msgid "Non-anonymous"
+msgstr ""
+
+#: converse.js:798
+msgid "Open room"
+msgstr ""
+
+#: converse.js:801
+msgid "Permanent room"
+msgstr ""
+
+#: converse.js:804
+msgid "Public"
+msgstr ""
+
+#: converse.js:807
+msgid "Semi-anonymous"
+msgstr ""
+
+#: converse.js:810
+msgid "Temporary room"
+msgstr ""
+
+#: converse.js:813
+msgid "Unmoderated"
+msgstr ""
+
+#: converse.js:819
+msgid "Rooms"
+msgstr ""
+
+#: converse.js:823
+msgid "Room name"
+msgstr ""
+
+#: converse.js:824
+msgid "Nickname"
+msgstr ""
+
+#: converse.js:825
+msgid "Server"
+msgstr ""
+
+#: converse.js:826
+msgid "Join"
+msgstr ""
+
+#: converse.js:827
+msgid "Show rooms"
+msgstr ""
+
+#: converse.js:1143
+msgid "Set chatroom topic"
+msgstr ""
+
+#: converse.js:1144
+msgid "Kick user from chatroom"
+msgstr ""
+
+#: converse.js:1145
+msgid "Ban user from chatroom"
+msgstr ""
+
+#: converse.js:1172
+msgid "Message"
+msgstr ""
+
+#: converse.js:1286 converse.js:2321
+msgid "Save"
+msgstr ""
+
+#: converse.js:1287
+msgid "Cancel"
+msgstr ""
+
+#: converse.js:1334
+msgid "An error occurred while trying to save the form."
+msgstr ""
+
+#: converse.js:1380
+msgid "This chat room requires a password"
+msgstr ""
+
+#: converse.js:1381
+msgid "Password: "
+msgstr ""
+
+#: converse.js:1382
+msgid "Submit"
+msgstr ""
+
+#: converse.js:1396
+msgid "This room is not anonymous"
+msgstr ""
+
+#: converse.js:1397
+msgid "This room now shows unavailable members"
+msgstr ""
+
+#: converse.js:1398
+msgid "This room does not show unavailable members"
+msgstr ""
+
+#: converse.js:1399
+msgid "Non-privacy-related room configuration has changed"
+msgstr ""
+
+#: converse.js:1400
+msgid "Room logging is now enabled"
+msgstr ""
+
+#: converse.js:1401
+msgid "Room logging is now disabled"
+msgstr ""
+
+#: converse.js:1402
+msgid "This room is now non-anonymous"
+msgstr ""
+
+#: converse.js:1403
+msgid "This room is now semi-anonymous"
+msgstr ""
+
+#: converse.js:1404
+msgid "This room is now fully-anonymous"
+msgstr ""
+
+#: converse.js:1405
+msgid "A new room has been created"
+msgstr ""
+
+#: converse.js:1406
+msgid "Your nickname has been changed"
+msgstr ""
+
+#: converse.js:1410
+msgid " has been banned"
+msgstr ""
+
+#: converse.js:1411
+msgid " has been kicked out"
+msgstr ""
+
+#: converse.js:1412
+msgid " has been removed because of an affiliation change"
+msgstr ""
+
+#: converse.js:1413
+msgid " has been removed for not being a member"
+msgstr ""
+
+#: converse.js:1417 converse.js:1480
+msgid "You have been banned from this room"
+msgstr ""
+
+#: converse.js:1418
+msgid "You have been kicked from this room"
+msgstr ""
+
+#: converse.js:1419
+msgid "You have been removed from this room because of an affiliation change"
+msgstr ""
+
+#: converse.js:1420
+msgid ""
+"You have been removed from this room because the room has changed to members-"
+"only and you're not a member"
+msgstr ""
+
+#: converse.js:1421
+msgid ""
+"You have been removed from this room because the MUC (Multi-user chat) "
+"service is being shut down."
+msgstr ""
+
+#: converse.js:1478
+msgid "You are not on the member list of this room"
+msgstr ""
+
+#: converse.js:1484
+msgid "No nickname was specified"
+msgstr ""
+
+#: converse.js:1488
+msgid "You are not allowed to create new rooms"
+msgstr ""
+
+#: converse.js:1490
+msgid "Your nickname doesn't conform to this room's policies"
+msgstr ""
+
+#: converse.js:1492
+msgid "Your nickname is already taken"
+msgstr ""
+
+#: converse.js:1494
+msgid "This room does not (yet) exist"
+msgstr ""
+
+#: converse.js:1496
+msgid "This room has reached it's maximum number of occupants"
+msgstr ""
+
+#. For translators: the %1$s and %2$s parts will get replaced by the user and topic text respectively
+#. Example: Topic set by JC Brand to: Hello World!
+#: converse.js:1573
+msgid "Topic set by %1$s to: %2$s"
+msgstr ""
+
+#: converse.js:1589
+msgid "This user is a moderator"
+msgstr ""
+
+#: converse.js:1592
+msgid "This user can send messages in this room"
+msgstr ""
+
+#: converse.js:1595
+msgid "This user can NOT send messages in this room"
+msgstr ""
+
+#: converse.js:1799
+msgid "Click to chat with this contact"
+msgstr ""
+
+#: converse.js:1800 converse.js:1804
+msgid "Click to remove this contact"
+msgstr ""
+
+#: converse.js:2166
+msgid "Contact requests"
+msgstr ""
+
+#: converse.js:2167
+msgid "My contacts"
+msgstr ""
+
+#: converse.js:2168
+msgid "Pending contacts"
+msgstr ""
+
+#: converse.js:2320
+msgid "Custom status"
+msgstr ""
+
+#: converse.js:2326
+msgid "Click to change your chat status"
+msgstr ""
+
+#: converse.js:2329
+msgid "Click here to write a custom status message"
+msgstr ""
+
+#. For translators: the %1$s part gets replaced with the status
+#. Example, I am online
+#: converse.js:2376 converse.js:2410
+msgid "I am %1$s"
+msgstr ""
+
+#: converse.js:2481
+msgid "Sign in"
+msgstr ""
+
+#: converse.js:2484
+msgid "XMPP/Jabber Username:"
+msgstr ""
+
+#: converse.js:2486
+msgid "Password:"
+msgstr ""
+
+#: converse.js:2488
+msgid "Log In"
+msgstr ""
+
+#: converse.js:2492
+msgid "BOSH Service URL:"
+msgstr ""
+
+#: converse.js:2504
+msgid "Connected"
+msgstr ""
+
+#: converse.js:2508
+msgid "Disconnected"
+msgstr ""
+
+#: converse.js:2512
+msgid "Error"
+msgstr ""
+
+#: converse.js:2514
+msgid "Connecting"
+msgstr ""
+
+#: converse.js:2517
+msgid "Connection Failed"
+msgstr ""
+
+#: converse.js:2519
+msgid "Authenticating"
+msgstr ""
+
+#: converse.js:2522
+msgid "Authentication Failed"
+msgstr ""
+
+#: converse.js:2524
+msgid "Disconnecting"
+msgstr ""
+
+#: converse.js:2526
+msgid "Attached"
+msgstr ""
+
+#: converse.js:2657
+msgid "Online Contacts"
+msgstr ""

+ 10 - 3
mock.js

@@ -28,9 +28,16 @@
         },
         },
         'vcard': { 
         'vcard': { 
             'get': function (callback, jid) {
             'get': function (callback, jid) {
-                var name = jid.split('@')[0].replace('.', ' ').split(' ');
-                var firstname = name[0].charAt(0).toUpperCase()+name[0].slice(1);
-                var lastname = name[1].charAt(0).toUpperCase()+name[1].slice(1);
+                var firstname, lastname;
+                if (!jid) {
+                    jid = 'dummy@localhost';
+                    firstname = 'Max';
+                    lastname = 'Mustermann';
+                } else {
+                    var name = jid.split('@')[0].replace('.', ' ').split(' ');
+                    firstname = name[0].charAt(0).toUpperCase()+name[0].slice(1);
+                    lastname = name[1].charAt(0).toUpperCase()+name[1].slice(1);
+                }
                 var fullname = firstname+' '+lastname;
                 var fullname = firstname+' '+lastname;
                 var vcard = $iq().c('vCard').c('FN').t(fullname);
                 var vcard = $iq().c('vCard').c('FN').t(fullname);
                 callback(vcard.tree());
                 callback(vcard.tree());

+ 0 - 6
spec/MainSpec.js

@@ -471,8 +471,6 @@
                         }).c('body').t(message).up()
                         }).c('body').t(message).up()
                           .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
                           .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
 
 
-                    spyOn(this, 'getVCard').andCallThrough();
-
                     // We don't already have an open chatbox for this user
                     // We don't already have an open chatbox for this user
                     expect(this.chatboxes.get(sender_jid)).not.toBeDefined();
                     expect(this.chatboxes.get(sender_jid)).not.toBeDefined();
 
 
@@ -483,10 +481,6 @@
                     }, converse));
                     }, converse));
                     waits(500);
                     waits(500);
                     runs($.proxy(function () {
                     runs($.proxy(function () {
-                        // Since we didn't already have an open chatbox, one
-                        // will asynchronously created inside a callback to
-                        // getVCard
-                        expect(this.getVCard).toHaveBeenCalled();
                         // Check that the chatbox and its view now exist
                         // Check that the chatbox and its view now exist
                         var chatbox = this.chatboxes.get(sender_jid);
                         var chatbox = this.chatboxes.get(sender_jid);
                         var chatboxview = this.chatboxesview.views[sender_jid];
                         var chatboxview = this.chatboxesview.views[sender_jid];

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio