changeset 2466:474448e88c24

Merge main.
author Karol 'grzywacz' Nowak <grzywacz@sul.uni.lodz.pl>
date Fri, 20 Jul 2007 01:56:41 +0200
parents 3c5b329afe4b (diff) dd138b728c1f (current diff)
children 31b3d03bd1ea
files MoinMoin/userprefs/prefs.py
diffstat 11 files changed, 400 insertions(+), 37 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/action/_tests/test_attachfile.py	Fri Jul 20 01:56:41 2007 +0200
@@ -0,0 +1,28 @@
+# -*- coding: iso-8859-1 -*-
+"""
+    MoinMoin - tests of AttachFile action
+
+    @copyright: 2007 by Karol Nowak <grywacz@gmail.com>
+    @license: GNU GPL, see COPYING for details.
+"""
+
+from MoinMoin.action.AttachFile import add_attachment, exists
+from MoinMoin.PageEditor import PageEditor
+from MoinMoin._tests.common import gain_superuser_rights
+
+def test_add_attachment(request):
+    """Test if add_attachment() works"""
+
+    gain_superuser_rights(request)
+    pagename = "AutoCreatedSillyPageToTestAttachments"
+    filename = "AutoCreatedSillyAttachment"
+
+    editor = PageEditor(request, pagename)
+    editor.deletePage()
+    editor.saveText("Test text!", 0)
+
+    print "First of all, no exceptions should be raised!"
+    add_attachment(request, pagename, filename, "Test content", True)
+
+    print "The save attachment should actually exist!"
+    assert exists(request, pagename, filename)
--- a/MoinMoin/events/__init__.py	Fri Jul 20 01:40:17 2007 +0200
+++ b/MoinMoin/events/__init__.py	Fri Jul 20 01:56:41 2007 +0200
@@ -23,6 +23,9 @@
 logger.setLevel(logging.DEBUG)
 logger.addHandler(logging.StreamHandler())
 
+# Dummy pseudo-getText function used in event descriptions,
+# so that they get into .po files
+_ = lambda x: x
 
 class Event(object):
     """A class handling information common to all events."""
@@ -38,7 +41,7 @@
 
 class PageChangedEvent(PageEvent):
 
-    description = u"""Page has been modified (edit, creation, deletion)"""
+    description = _(u"""Page has been modified (edit, creation, deletion)""")
     req_superuser = False
 
     def __init__(self, request, page, comment, trivial):
@@ -50,10 +53,10 @@
 
 class PageRenamedEvent(PageEvent):
 
-    description = u"""Page has been renamed"""
+    description = _(u"""Page has been renamed""")
     req_superuser = False
 
-    def __init__(self, request, page, old_page, comment):
+    def __init__(self, request, page, old_page, comment=""):
         PageEvent.__init__(self, request)
         self.page = page
         self.old_page = old_page
@@ -62,7 +65,7 @@
 
 class PageDeletedEvent(PageEvent):
 
-    description = u"""Page has been deleted"""
+    description = _(u"""Page has been deleted""")
     req_superuser = False
 
     def __init__(self, request, page, comment):
@@ -73,7 +76,7 @@
 
 class PageCopiedEvent(PageEvent):
 
-    description = u"""Page has been copied"""
+    description = _(u"""Page has been copied""")
     req_superuser = False
 
     def __init__(self, request, page, old_page, comment):
@@ -85,7 +88,7 @@
 
 class FileAttachedEvent(PageEvent):
 
-    description = u"""A new attachment has been added"""
+    description = _(u"""A new attachment has been added""")
     req_superuser = False
 
     def __init__(self, request, pagename, name, size):
@@ -98,7 +101,7 @@
 
 class PageRevertedEvent(PageEvent):
 
-    description = u"""A page has been reverted to a previous state"""
+    description = _(u"""A page has been reverted to a previous state""")
     req_superuser = False
 
     def __init__(self, request, pagename, previous, current):
@@ -110,7 +113,7 @@
 
 class SubscribedToPageEvent(PageEvent):
 
-    description = u"""An user has subscribed to a page"""
+    description = _(u"""An user has subscribed to a page""")
     req_superuser = True
 
     def __init__(self, request, pagename, username):
@@ -140,7 +143,7 @@
 class UserCreatedEvent(Event):
     """ Sent when a new user has been created """
 
-    description = u"""A new account has been created"""
+    description = _(u"""A new account has been created""")
     req_superuser = True
 
     def __init__(self, request, user):
@@ -233,3 +236,6 @@
             subscribable_events[ev.__name__] = {'desc': ev.description,
                                                  'superuser': ev.req_superuser}
     return subscribable_events
+
+# Get rid of the dummy getText so that it doesn't get imported with *
+del _
--- a/MoinMoin/i18n/__init__.py	Fri Jul 20 01:40:17 2007 +0200
+++ b/MoinMoin/i18n/__init__.py	Fri Jul 20 01:56:41 2007 +0200
@@ -39,13 +39,13 @@
 
 translations = {}
 
-def po_filename(request, language, domain):
+def po_filename(request, language, domain, i18n_dir='i18n'):
     """ we use MoinMoin/i18n/<language>[.<domain>].mo as filename for the PO file.
 
         TODO: later, when we have a farm scope plugin dir, we can also load
               language data from there.
     """
-    return os.path.join(request.cfg.moinmoin_dir, 'i18n', "%s.%s.po" % (language, domain))
+    return os.path.join(request.cfg.moinmoin_dir, i18n_dir, "%s.%s.po" % (language, domain))
 
 def i18n_init(request):
     """ this is called early from request initialization and makes sure we
@@ -87,6 +87,30 @@
                 pass
     request.clock.stop('i18n_init')
 
+def bot_translations(request):
+    """Return translations to be used by notification bot
+
+    This is called by XML RPC code.
+
+    @return: a dict (indexed by language) of dicts of translated strings (indexed by original ones)
+    """
+    translations = {}
+    po_dir = os.path.join('i18n', 'jabberbot')
+    encoding = 'utf-8'
+
+    for lang_file in glob.glob(po_filename(request, i18n_dir=po_dir, language='*', domain='JabberBot')):
+        language, domain, ext = os.path.basename(lang_file).split('.')
+        t = Translation(language, domain)
+        f = file(lang_file)
+        t.load_po(f)
+        f.close()
+        t.loadLanguage(request, trans_dir=po_dir)
+        translations[language] = {}
+
+        for key, text in t.raw.items():
+            translations[language][key] = text
+
+    return translations
 
 class Translation(object):
     """ This class represents a translation. Usually this is a translation
@@ -161,10 +185,10 @@
         text = text.strip()
         return text
 
-    def loadLanguage(self, request):
+    def loadLanguage(self, request, trans_dir="i18n"):
         request.clock.start('loadLanguage')
         cache = caching.CacheEntry(request, arena='i18n', key=self.language, scope='farm', use_pickle=True)
-        langfilename = po_filename(request, self.language, self.domain)
+        langfilename = po_filename(request, self.language, self.domain, i18n_dir=trans_dir)
         needsupdate = cache.needsUpdate(langfilename)
         if debug:
             request.log("i18n: langfilename %s needsupdate %d" % (langfilename, needsupdate))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/i18n/jabberbot/pl.JabberBot.po	Fri Jul 20 01:56:41 2007 +0200
@@ -0,0 +1,172 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Jabber Bot\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2007-07-17 03:14+0200\n"
+"PO-Revision-Date: 2007-07-19 01:53+0100\n"
+"Last-Translator: Karol Nowak <grywacz@gmail.com>\n"
+"Language-Team: Polish Language Team <grywacz@gmail.com>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Language: Polski\n"
+"X-Language-in-English: Polish\n"
+"X-Direction: ltr\n"
+"X-Poedit-Language: Polish\n"
+"X-Poedit-Country: POLAND\n"
+"X-Poedit-SourceCharset: utf-8\n"
+
+#, python-format
+msgid ""
+"Here's the page \"%(pagename)s\" that you've requested:\n"
+"\n"
+"%(data)s"
+msgstr ""
+"Oto strona \"%(pagename)s\", której żądałeś:\n"
+"\n"
+"%(data)s"
+
+#, python-format
+msgid ""
+"That's the list of pages accesible to you:\n"
+"\n"
+"%s"
+msgstr ""
+"Oto list stron, do których masz dostęp:\n"
+"\n"
+"%s"
+
+#, python-format
+msgid ""
+"Following detailed information on page \"%(pagename)s\" is available::\n"
+"\n"
+"%(data)s"
+msgstr ""
+"Oto dostępne szczegółowe informacje o stronie \"%(pagename)s\":\n"
+"\n"
+"%(data)s"
+
+msgid "Submit this form to perform a wiki search"
+msgstr "Wyślij ten formularz, by przeprowadzić wyszukiwanie"
+
+msgid "Title search"
+msgstr "Szukaj w tytułach"
+
+msgid "Full-text search"
+msgstr "Szukaj w tekście"
+
+msgid "Wiki search"
+msgstr "Przeszukiwanie wiki"
+
+msgid "Search text"
+msgstr "Szukany tekst"
+
+msgid "Please specify the search criteria."
+msgstr "Proszę podać kryteria wyszukiwania."
+
+msgid "This command requires a client supporting Data Forms"
+msgstr "Ta komenda wymaga klienta obsługującego Data Forms"
+
+msgid ""
+"The \"help\" command prints a short, helpful message about a given topic or function.\n"
+"\n"
+"Usage: help [topic_or_function]"
+msgstr ""
+"Komenda \"help\" wyświetla krótką pomoc na wybrany temat.\n"
+"\n"
+"Sposób użycia: help [temat_lub_funkcja]"
+
+msgid "The \"ping\" command returns a \"pong\" message as soon as it's received."
+msgstr "Komenda \"ping\" odpowiada \"pong\", gdy tylko zostanie odebrana."
+
+msgid "searchform - perform a wiki search using a form"
+msgstr "searchform - przeszukaj wiki korzystając z formularza"
+
+#, python-format
+msgid ""
+"%(command)s - %(description)s\n"
+"\n"
+"Usage: %(command)s %(params)s"
+msgstr ""
+"%(command)s - %(description)s\n"
+"\n"
+"Sposób użycia: %(command)s %(params)s"
+
+#, python-format
+msgid "Unknown command \"%s\" "
+msgstr "Nieznana komenda \"%s\""
+
+#, python-format
+msgid ""
+"You've specified a wrong parameter list. The call should look like:\n"
+"\n"
+"%(command)s %(params)s"
+msgstr ""
+"Podałeś błędną listę parametrów. Powinna ona wyglądać tak:\n"
+"\n"
+"%(command)s %(params)s"
+
+#, python-format
+msgid ""
+"Hello there! I'm a MoinMoin Notification Bot. Available commands:\n"
+"\n"
+"%(internal)s\n"
+"%(xmlrpc)s"
+msgstr ""
+"Witaj! Jestem botem-powiadamiaczem MoinMoin. Udostępniam takie komendy:\n"
+"\n"
+"%(internal)s\n"
+"%(xmlrpc)s"
+
+msgid "You must set a (long) secret string!"
+msgstr "Musisz ustawić (długie) sekretne hasło!"
+
+msgid "Error"
+msgstr "Błąd"
+
+#, python-format
+msgid ""
+"Your request has failed. The reason is:\n"
+"%s"
+msgstr ""
+"Twoje polecenie nie mogło zostać wypełniona. Przyczyna to:\n"
+"%s"
+
+#, python-format
+msgid ""
+"A serious error occured while processing your request:\n"
+"%s"
+msgstr ""
+"Podczas przetwarzania Twojego polecenia wystąpił poważny błąd:\n"
+"%s"
+
+msgid "An internal error has occured, please contact the administrator."
+msgstr "Wystąpił błąd wewnętrzny, proszę skontaktować się z administratorem."
+
+msgid "Credentials check failed, you may be unable to see all information."
+msgstr "Uwierzytelnienie nie powiodło się, część informacji może być dla Ciebie niedostępna."
+
+msgid "This command may take a while to complete, please be patient..."
+msgstr "Wykonanie tego polecenia może chwilę potrwać, proszę uzbroić się w cierpliwość..."
+
+#, python-format
+msgid ""
+"Last author: %(author)s\n"
+"Last modification: %(modification)s\n"
+"Current version: %(version)s"
+msgstr ""
+"Ostatni autor: %(author)s\n"
+"Ostatnia zmiana: %(modification)s\n"
+"Obecna wersja: %(version)s"
+
+msgid "You must set a (long) secret string"
+msgstr "Musisz ustawić (długie) sekretne hasło"
+
+msgid "You are not allowed to use this bot!"
+msgstr "Nie masz uprawnień do użycia tego bota!"
+
--- a/MoinMoin/userprefs/prefs.py	Fri Jul 20 01:40:17 2007 +0200
+++ b/MoinMoin/userprefs/prefs.py	Fri Jul 20 01:56:41 2007 +0200
@@ -103,7 +103,7 @@
 
         if not 'jid' in theuser.auth_attribs:
             # try to get the jid
-            jid = wikiutil.clean_input(form.get('jid', "")[0]).strip()
+            jid = wikiutil.clean_input(form.get('jid', [""])[0]).strip()
 
             jid_changed = theuser.jid != jid
             previous_jid = theuser.jid
--- a/MoinMoin/xmlrpc/__init__.py	Fri Jul 20 01:40:17 2007 +0200
+++ b/MoinMoin/xmlrpc/__init__.py	Fri Jul 20 01:56:41 2007 +0200
@@ -591,6 +591,22 @@
             userdata = dict(u.persistent_items())
         return userdata
 
+    def xmlrpc_getUserLanguageByJID(self, jid):
+        """ Returns user's language given his/her Jabber ID
+
+        It makes no sense to consider this a secret, right? Therefore
+        an authentication token is not required. We return a default
+        of "en" if user is not found.
+
+        TODO: surge protection? Do we fear account enumeration?
+        """
+        retval = "en"
+        u = user.get_by_jabber_id(self.request, jid)
+        if u:
+            retval = u.language
+
+        return retval
+
     # authorization methods
 
     def _cleanup_stale_tokens(self, request):
@@ -942,6 +958,15 @@
         AttachFile._addLogEntry(self.request, 'ATTNEW', pagename, filename)
         return xmlrpclib.Boolean(1)
 
+
+    def xmlrpc_getBotTranslations(self):
+        """ Return translations to be used by notification bot
+
+        @return: a dict (indexed by language) of dicts of translated strings (indexed by original ones)
+        """
+        from MoinMoin.i18n import bot_translations
+        return bot_translations(self.request)
+
     # XXX END WARNING XXX
 
 
--- a/jabberbot/commands.py	Fri Jul 20 01:40:17 2007 +0200
+++ b/jabberbot/commands.py	Fri Jul 20 01:56:41 2007 +0200
@@ -104,3 +104,13 @@
         BaseDataCommand.__init__(self, jid)
         self.term = term
         self.search_type = search_type
+
+class GetUserLanguage:
+    """Request user's language information from wiki"""
+
+    def __init__(self, jid):
+        """
+        @param jid: user's (bare) Jabber ID
+        """
+        self.jid = jid
+        self.language = None
--- a/jabberbot/i18n.py	Fri Jul 20 01:40:17 2007 +0200
+++ b/jabberbot/i18n.py	Fri Jul 20 01:56:41 2007 +0200
@@ -5,20 +5,61 @@
     @copyright: 2007 by Karol Nowak <grywacz@gmail.com>
     @license: GNU GPL, see COPYING for details.
 """
+import logging, xmlrpclib
 
 translations = None
 
+
 def getText(original, lang="en"):
+    """ Return a translation of text in the user's language.
+
+        @type original: unicode
+    """
+    if original == u"":
+        return u""
+
     global translations
-
     if not translations:
         init_i18n()
 
+    # get the matching entry in the mapping table
+    translated = original
     try:
         return translations[lang][original]
     except KeyError:
         return original
 
-def init_i18n():
+
+def init_i18n(config):
+    """Prepare i18n
+
+    @type config: jabberbot.config.BotConfig
+
+    """
     global translations
-    translations = {'en': {}}
+    translations = request_translations(config) or {'en': {}}
+
+
+def request_translations(config):
+    """Download translations from wiki using xml rpc
+
+    @type config: jabberbot.config.BotConfig
+
+    """
+
+    wiki = xmlrpclib.Server(config.wiki_url + "?action=xmlrpc2")
+    log = logging.getLogger("log")
+    log.debug("Initialising i18n...")
+
+    try:
+        translations =  wiki.getBotTranslations()
+        return translations
+    except xmlrpclib.Fault, fault:
+        log.error("XML RPC fault occured while getting translations: %s" % (str(fault), ))
+    except xmlrpclib.Error, error:
+        log.error("XML RPC error occured while getting translations: %s" % (str(error), ))
+    except Exception, exc:
+        log.error("Unexpected exception occured while getting translations: %s" % (str(exc), ))
+
+    log.error("Translations could not be downloaded, is wiki is accesible?")
+    return None
--- a/jabberbot/main.py	Fri Jul 20 01:40:17 2007 +0200
+++ b/jabberbot/main.py	Fri Jul 20 01:56:41 2007 +0200
@@ -10,6 +10,7 @@
 from Queue import Queue
 
 from jabberbot.config import BotConfig
+from jabberbot.i18n import init_i18n
 from jabberbot.xmppbot import XMPPBot
 from jabberbot.xmlrpcbot import XMLRPCServer, XMLRPCClient
 
@@ -29,6 +30,8 @@
     log.setLevel(logging.DEBUG)
     log.addHandler(logging.StreamHandler())
 
+    init_i18n(BotConfig)
+
     # TODO: actually accept options from the help string
     commands_from_xmpp = Queue()
     commands_to_xmpp = Queue()
--- a/jabberbot/xmlrpcbot.py	Fri Jul 20 01:40:17 2007 +0200
+++ b/jabberbot/xmlrpcbot.py	Fri Jul 20 01:56:41 2007 +0200
@@ -74,6 +74,8 @@
             self.get_page_list(command)
         elif isinstance(command, cmd.GetPageInfo):
             self.get_page_info(command)
+        elif isinstance(command, cmd.GetUserLanguage):
+            self.get_language_by_jid(command)
 
     def report_error(self, jid, text):
         report = cmd.NotificationCommand(jid, text, _("Error"), async=False)
@@ -222,6 +224,15 @@
 
     get_page_info = _xmlrpc_decorator(get_page_info)
 
+    def get_language_by_jid(self, command):
+        """Returns language of the a user identified by the given JID"""
+
+        server = xmlrpclib.ServerProxy(self.config.wiki_url + "?action=xmlrpc2")
+        language = server.getUserLanguageByJID(command.jid)
+
+        command.language = language or "en"
+        self.commands_out.put_nowait(command)
+
 
 class XMLRPCServer(Thread):
     """XMLRPC Server
--- a/jabberbot/xmppbot.py	Fri Jul 20 01:40:17 2007 +0200
+++ b/jabberbot/xmppbot.py	Fri Jul 20 01:56:41 2007 +0200
@@ -18,9 +18,8 @@
 import pyxmpp.jabber.dataforms as forms
 
 import jabberbot.commands as cmd
-from jabberbot.i18n import getText
+import jabberbot.i18n as i18n
 
-_ = getText
 
 class Contact:
     """Abstraction of a roster item / contact
@@ -28,9 +27,10 @@
     This class handles some logic related to keeping track of
     contact availability, status, etc."""
 
-    def __init__(self, jid, resource, priority, show):
+    def __init__(self, jid, resource, priority, show, language=None):
         self.jid = jid
         self.resources = {resource: {'show': show, 'priority': priority, 'forms': False}}
+        self.language = language
 
         # Queued messages, waiting for contact to change its "show"
         # status to something different than "dnd". The messages should
@@ -39,6 +39,7 @@
         # the next time she becomes "available".
         self.messages = []
 
+
     def add_resource(self, resource, show, priority):
         """Adds information about a connected resource
 
@@ -110,7 +111,6 @@
         res = ", ".join([name + " is " + res['show'] for name, res in self.resources.items()])
         return retval % (self.jid.as_utf8(), res, len(self.messages))
 
-
 class XMPPBot(Client, Thread):
     """A simple XMPP bot"""
 
@@ -165,6 +165,24 @@
                 while self.poll_commands(): pass
                 self.idle()
 
+    def getText(self, jid):
+        """Returns a getText function (_) for the given JID
+
+        @param jid: bare Jabber ID of the user we're going to communicate with
+        @type jid: str or pyxmpp.jid.JID
+
+        """
+        language = "en"
+        if isinstance(jid, str) or isinstance(jid, unicode):
+            jid = JID(jid).bare().as_utf8()
+        else:
+            jid = jid.bare().as_utf8()
+
+        if jid in self.contacts:
+            language = self.contacts[jid].language
+
+        return lambda text: i18n.getText(text, lang=language)
+
     def poll_commands(self):
         """Checks for new commands in the input queue and executes them
 
@@ -215,18 +233,23 @@
             self.remove_subscription(jid)
 
         elif isinstance(command, cmd.GetPage) or isinstance(command, cmd.GetPageHTML):
-            msg = _("""Here's the page "%(pagename)s" that you've requested:\n\n%(data)s""")
+            _ = self.getText(command.jid)
+            msg = _(u"""Here's the page "%(pagename)s" that you've requested:\n\n%(data)s""")
+
             self.send_message(command.jid, msg % {
                       'pagename': command.pagename,
                       'data': command.data,
             })
 
         elif isinstance(command, cmd.GetPageList):
-            msg = _("That's the list of pages accesible to you:\n\n%s")
-            pagelist = "\n".join(command.data)
+            _ = self.getText(command.jid)
+            msg = _("uThat's the list of pages accesible to you:\n\n%s")
+            pagelist = u"\n".join(command.data)
+
             self.send_message(command.jid, msg % (pagelist, ))
 
         elif isinstance(command, cmd.GetPageInfo):
+            _ = self.getText(command.jid)
             msg = _("""Following detailed information on page "%(pagename)s" \
 is available::\n\n%(data)s""")
 
@@ -235,6 +258,10 @@
                       'data': command.data,
             })
 
+        elif isinstance(command, cmd.GetUserLanguage):
+            if command.jid in self.contacts:
+                self.contacts[command.jid].language = command.language
+
     def ask_for_subscription(self, jid):
         """Sends a <presence/> stanza with type="subscribe"
 
@@ -274,13 +301,17 @@
         pass
 
     def send_search_form(self, jid):
-        help_form = _("Submit this form to perform a wiki search")
+        _ = self.getText(jid)
 
-        title_search = forms.Option("t", _("Title search"))
-        full_search = forms.Option("f", _("Full-text search"))
+        # This encode may look weird, but due to some pyxmpp oddness we have
+        # to provide an unicode string. Maybe this should be fixed upstream...
+        help_form = _("Submit this form to perform a wiki search").encode("utf-8")
+
+        title_search = forms.Option("t", _("Title search")) #.decode("utf-8"))
+        full_search = forms.Option("f", _("Full-text search"))#.decode("utf-8"))
 
         form = forms.Form(xmlnode_or_type="form", title=_("Wiki search"), instructions=help_form)
-        form.add_field(name="search_type", options=[title_search, full_search], field_type="list-single", label="Search type")
+        form.add_field(name="search_type", options=[title_search, full_search], field_type="list-single", label=_("Search type"))
         form.add_field(name="search", field_type="text-single", label=_("Search text"))
 
         message = Message(to_jid=jid, body=_("Please specify the search criteria."), subject=_("Wiki search"))
@@ -335,9 +366,9 @@
         elif self.is_xmlrpc(command[0]):
             response = self.handle_xmlrpc_command(sender, command)
         else:
-            response = self.reply_help()
+            response = self.reply_help(sender)
 
-        if not response == u"":
+        if response:
             self.send_message(sender, response)
 
     def handle_internal_command(self, sender, command):
@@ -348,13 +379,15 @@
         @type sender: pyxmpp.jid.JID
 
         """
+        _ = self.getText(sender)
+
         if command[0] == "ping":
             return "pong"
         elif command[0] == "help":
             if len(command) == 1:
-                return self.reply_help()
+                return self.reply_help(sender)
             else:
-                return self.help_on(command[1])
+                return self.help_on(sender, command[1])
         elif command[0] == "searchform":
             jid = sender.bare().as_utf8()
             resource = sender.resource
@@ -365,7 +398,7 @@
                 self.send_message(sender, msg, u"Error")
         else:
             # For unknown command return a generic help message
-            return self.reply_help()
+            return self.reply_help(sender)
 
     def do_search(self, jid, term, search_type):
         """Performs a Wiki search of term
@@ -381,7 +414,7 @@
         search = cmd.Search(jid, term, search_type)
         self.from_commands.put_nowait(search)
 
-    def help_on(self, command):
+    def help_on(self, jid, command):
         """Returns a help message on a given topic
 
         @param command: a command to describe in a help message
@@ -389,6 +422,8 @@
         @return: a help message
 
         """
+        _ = self.getText(jid)
+
         if command == "help":
             return _("""The "help" command prints a short, helpful message \
 about a given topic or function.\n\nUsage: help [topic_or_function]""")
@@ -404,7 +439,7 @@
         else:
             if command in self.xmlrpc_commands:
                 classobj = self.xmlrpc_commands[command]
-                help_str = _("%(command)s - %(description)s\n\nUsage: %(command)s %(params)s")
+                help_str = _(u"%(command)s - %(description)s\n\nUsage: %(command)s %(params)s")
                 return help_str % {'command': command,
                                    'description': classobj.description,
                                    'params': classobj.parameter_list,
@@ -419,6 +454,7 @@
         @type command: list representing a command, name and parameters
 
         """
+        _ = self.getText(sender)
         command_class = self.xmlrpc_commands[command[0]]
 
         # Add sender's JID to the argument list
@@ -528,6 +564,11 @@
         else:
             self.contacts[bare_jid] = Contact(jid, jid.resource, priority, show)
             self.supports_dataforms(jid)
+
+            # Request user's language now. This is suboptimal, but caching
+            # should fix it in the future.
+            request = cmd.GetUserLanguage(bare_jid)
+            self.from_commands.put_nowait(request)
             self.log.debug(self.contacts[bare_jid])
 
         # Confirm that we've handled this stanza
@@ -575,18 +616,20 @@
         for command in contact.messages:
             self.handle_command(command, ignore_dnd)
 
-    def reply_help(self):
+    def reply_help(self, jid):
         """Constructs a generic help message
 
         It's sent in response to an uknown message or the "help" command.
 
         """
+        _ = self.getText(jid)
+
         msg = _("Hello there! I'm a MoinMoin Notification Bot. Available commands:\
 \n\n%(internal)s\n%(xmlrpc)s")
         internal = ", ".join(self.internal_commands)
         xmlrpc = ", ".join(self.xmlrpc_commands.keys())
 
-        return msg % (internal, xmlrpc)
+        return msg % {'internal': internal, 'xmlrpc': xmlrpc}
 
     def authenticated(self):
         """Called when authentication succeedes"""