changeset 2461:f456dc2048d1

i18n should now work. First step, no caching yet.
author Karol 'grzywacz' Nowak <grzywacz@sul.uni.lodz.pl>
date Fri, 20 Jul 2007 00:24:21 +0200
parents 99b6222544c3
children 8ce2afa469a7
files MoinMoin/events/__init__.py MoinMoin/i18n/__init__.py MoinMoin/xmlrpc/__init__.py jabberbot/commands.py jabberbot/i18n.py jabberbot/main.py jabberbot/xmlrpcbot.py jabberbot/xmppbot.py
diffstat 8 files changed, 185 insertions(+), 28 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/events/__init__.py	Fri Jul 20 00:18:41 2007 +0200
+++ b/MoinMoin/events/__init__.py	Fri Jul 20 00:24:21 2007 +0200
@@ -53,7 +53,7 @@
     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
--- a/MoinMoin/i18n/__init__.py	Fri Jul 20 00:18:41 2007 +0200
+++ b/MoinMoin/i18n/__init__.py	Fri Jul 20 00:24:21 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))
--- a/MoinMoin/xmlrpc/__init__.py	Fri Jul 20 00:18:41 2007 +0200
+++ b/MoinMoin/xmlrpc/__init__.py	Fri Jul 20 00:24:21 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(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 00:18:41 2007 +0200
+++ b/jabberbot/commands.py	Fri Jul 20 00:24:21 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 00:18:41 2007 +0200
+++ b/jabberbot/i18n.py	Fri Jul 20 00:24:21 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 00:18:41 2007 +0200
+++ b/jabberbot/main.py	Fri Jul 20 00:24:21 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 00:18:41 2007 +0200
+++ b/jabberbot/xmlrpcbot.py	Fri Jul 20 00:24:21 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 00:18:41 2007 +0200
+++ b/jabberbot/xmppbot.py	Fri Jul 20 00:24:21 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"""