Explorar el Código

Merge branch 'master' into gh-pages

JC Brand hace 12 años
padre
commit
6464acff5c

+ 19 - 29
CHANGES.rst

@@ -4,39 +4,29 @@ Changelog
 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)
 ----------------
 
-- 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)

La diferencia del archivo ha sido suprimido porque es 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
 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:
 	@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 "  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 "  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 "  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 "  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: 
 	r.js -o build.js
@@ -57,11 +58,6 @@ singlehtml:
 	@echo
 	@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:
 	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
 	@echo
@@ -73,15 +69,6 @@ htmlhelp:
 	@echo "Build finished; now you can run HTML Help Workshop with the" \
 	      ".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:
 	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
 	@echo
@@ -114,11 +101,6 @@ text:
 	@echo
 	@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:
 	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
 	@echo

+ 4 - 0
build.js

@@ -2,6 +2,10 @@
     baseUrl: ".",
     paths: {
         "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",
         "tinysort": "Libraries/jquery.tinysort",
         "underscore": "Libraries/underscore",

+ 17 - 12
converse.css

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

+ 2428 - 2362
converse.js

@@ -14,6 +14,10 @@
     if (typeof define === 'function' && define.amd) {
         require.config({
             paths: {
+                "jed": "Libraries/jed",
+                "locales": "Libraries/locales",
+                "af": "locale/af/LC_MESSAGES/af",
+                "en": "locale/en/LC_MESSAGES/en",
                 "sjcl": "Libraries/sjcl",
                 "tinysort": "Libraries/jquery.tinysort",
                 "underscore": "Libraries/underscore",
@@ -48,6 +52,7 @@
         });
 
         define("converse", [
+            "locales",
             "localstorage",
             "tinysort",
             "sjcl",
@@ -74,450 +79,484 @@
     }
 }(this, function ($, _, console) {
     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 {
-                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'})
-                        .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('before').up()
                         .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 {
-                    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;
+                } 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'),
                     canvas = $('<canvas height="35px" width="35px" class="avatar"></canvas>'),
                     ctx = canvas.get(0).getContext('2d'),
@@ -528,2083 +567,2110 @@
                 };
                 img.src = img_src;
                 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>'+
-            '<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));
-            $('.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;
                 }
-                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 {
-                        $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 {
-                    $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 {
-                    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;
             }
-            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 {
-                        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'));
                     });
                 }
-            });
-            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;
-            } 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 {
-                    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
                             }));
-            // 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;
-                    $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');
-            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 {
-                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(
-                    $.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.chatboxesview = new this.ChatBoxesView({model: this.chatboxes});
         $('a.toggle-online-users').bind(

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
converse.min.css


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 55 - 0
converse.min.js


BIN
docs/doctrees/environment.pickle


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.
 
 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
 code.
 
@@ -47,7 +47,7 @@ An XMPP/Jabber server
 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
-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
 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
 ==========================
 
-Session support
+Session Support
 ---------------
 
 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
 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
 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
 be used.
 
-
-
 ============
 Minification
 ============
@@ -302,6 +300,91 @@ CSS can be minimized with Yahoo's yuicompressor tool:
     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

+ 64 - 5
docs/html/index.html

@@ -73,7 +73,7 @@
 </ul>
 </li>
 <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>
 </li>
 </ul>
@@ -98,6 +98,7 @@
 <li><a class="reference internal" href="#minifying-css" id="id21">Minifying CSS</a></li>
 </ul>
 </li>
+<li><a class="reference internal" href="#translations" id="id22">Translations</a></li>
 </ul>
 </div>
 <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
 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
-<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
 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
@@ -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
 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
-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
 servers that you can set up yourself on <a class="reference external" href="http://xmpp.org/xmpp-software/servers/">xmpp.org</a>.</p>
 </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">
 <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">
-<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
 authenticated in your website will also automatically be logged in on the chat server,
 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
 your website, where users authenticate once in your website and then stay
 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
 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>
@@ -311,6 +312,64 @@ manager, NPM.</p>
 <div class="highlight-python"><pre>yui-compressor --type=css converse.css -o converse.min.css</pre>
 </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>
 
 

La diferencia del archivo ha sido suprimido porque es 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
 be used.
 
-
-
 ============
 Minification
 ============
@@ -302,6 +300,91 @@ CSS can be minimized with Yahoo's yuicompressor tool:
     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

+ 1 - 0
index.html

@@ -62,6 +62,7 @@
         <li>Custom status messages</li>
         <li>Typing notifications</li>
         <li>Third person messages (/me )</li>
+        <li>i18n aware</li>
     </ul>
 
     <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': { 
             '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 vcard = $iq().c('vCard').c('FN').t(fullname);
                 callback(vcard.tree());

+ 0 - 6
spec/MainSpec.js

@@ -471,8 +471,6 @@
                         }).c('body').t(message).up()
                           .c('active', {'xmlns': 'http://jabber.org/protocol/chatstates'}).tree();
 
-                    spyOn(this, 'getVCard').andCallThrough();
-
                     // We don't already have an open chatbox for this user
                     expect(this.chatboxes.get(sender_jid)).not.toBeDefined();
 
@@ -483,10 +481,6 @@
                     }, converse));
                     waits(500);
                     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
                         var chatbox = this.chatboxes.get(sender_jid);
                         var chatboxview = this.chatboxesview.views[sender_jid];

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio