[Zope-Checkins] CVS: Zope3/lib/python/Zope/I18n/Views/Browser - BaseTranslationServiceView.py:1.1 ExportImport.py:1.1 Synchronize.py:1.1 exportImport.pt:1.1 synchronize.pt:1.1 translateMessage.pt:1.1 Translate.py:1.3 browser.zcml:1.3 translate.pt:1.10

Stephan Richter srichter@cbu.edu
Sun, 16 Jun 2002 14:25:15 -0400


Update of /cvs-repository/Zope3/lib/python/Zope/I18n/Views/Browser
In directory cvs.zope.org:/tmp/cvs-serv23163/Views/Browser

Modified Files:
	Translate.py browser.zcml translate.pt 
Added Files:
	BaseTranslationServiceView.py ExportImport.py Synchronize.py 
	exportImport.pt synchronize.pt translateMessage.pt 
Log Message:
Commit part 1:

I have done quiet some work this weekend on refining and refactoring the 
Translation Service and it should be ready for EP now:

- Refactored the interfaces into more standard read and write interfaces.

- Implemented an Import and Export feature using geyyexy files.

- Implemented Message Synchronisation mechanism via XML-RPC.

- Improved the overall look and feel of the LocalTranslation Service.

- Added an icon for the Translation Service.


=== Added File Zope3/lib/python/Zope/I18n/Views/Browser/BaseTranslationServiceView.py ===
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
# All Rights Reserved.
# 
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
# 
##############################################################################
"""Synchronize with Foreign Translation Services

$Id: BaseTranslationServiceView.py,v 1.1 2002/06/16 18:25:13 srichter Exp $
"""

from Zope.Publisher.Browser.BrowserView import BrowserView
from Zope.I18n.ITranslationService import ITranslationService


class BaseTranslationServiceView(BrowserView):
    
    __used_for__ = ITranslationService


    def getAllLanguages(self):
        """Get all available languages from the Translation Service."""
        return self.context.getAllLanguages()


    def getAllDomains(self):
        return self.context.getAllDomains()


=== Added File Zope3/lib/python/Zope/I18n/Views/Browser/ExportImport.py ===
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
# All Rights Reserved.
# 
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
# 
##############################################################################
"""Message Export/Import View

$Id: ExportImport.py,v 1.1 2002/06/16 18:25:13 srichter Exp $
"""

from Zope.ComponentArchitecture import getAdapter

from Zope.App.PageTemplate import ViewPageTemplateFile
from Zope.I18n.IMessageExportFilter import IMessageExportFilter
from Zope.I18n.IMessageImportFilter import IMessageImportFilter

from BaseTranslationServiceView import BaseTranslationServiceView

class ExportImport(BaseTranslationServiceView):
    """ """
    
    exportImportForm = ViewPageTemplateFile('exportImport.pt')


    def exportMessages(self, domains, languages):
        self.request.getResponse().setHeader('content-type',
                                             'application/x-gettext')
        filter = getAdapter(self.context, IMessageExportFilter)
        return filter.exportMessages(domains, languages)
        
    
    def importMessages(self, domains, languages, file):
        filter = getAdapter(self.context, IMessageImportFilter)
        filter.importMessages(domains, languages, file)
        return self.request.getResponse().redirect(self.request.URL[-1])

    


=== Added File Zope3/lib/python/Zope/I18n/Views/Browser/Synchronize.py ===
##############################################################################
#
# Copyright (c) 2001, 2002 Zope Corporation and Contributors.
# All Rights Reserved.
# 
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
# 
##############################################################################
"""Synchronize with Foreign Translation Services

$Id: Synchronize.py,v 1.1 2002/06/16 18:25:13 srichter Exp $
"""
import xmlrpclib, httplib, urllib
from base64 import encodestring

from Zope.App.PageTemplate import ViewPageTemplateFile
from BaseTranslationServiceView import BaseTranslationServiceView


class Synchronize(BaseTranslationServiceView):

    synchronizeForm = ViewPageTemplateFile('synchronize.pt')

    messageStatus = ['Up to Date', 'New Remote', 'Out of Date', 'Newer Local',
                     'Does not exist']

    def __init__(self, context, request):
        super(Synchronize, self).__init__(context, request)

        self.sync_url = self.request.cookies.get('sync_url',
                                'http://localhost:8081/++etc++Services/ts/')
        self.sync_url = urllib.unquote(self.sync_url)
        self.sync_username = self.request.cookies.get('sync_username', 'admin')
        self.sync_password = self.request.cookies.get('sync_password', 'admin')
        self.sync_domains = filter(None, self.request.cookies.get(
            'sync_domains', '').split(','))
        self.sync_languages = filter(None, self.request.cookies.get(
            'sync_languages', '').split(','))


    def _connect(self):
        '''Connect to the remote server via XML-RPC HTTP; return status'''
        # make sure the URL contains the http:// prefix
        if not self.sync_url.startswith('http://'):
            url = 'http://' + self.sync_url
        else:
            url = self.sync_url
        url += '++view++methods/'

        # Now try to connect
        self._connection = xmlrpclib.Server(
            url, transport = BasicAuthTransport(self.sync_username,
                                                self.sync_password))

        # check whether the connection was made and the Master Babel Tower
        # exists
        try:
            self._connection.getAllDomains()
            return 1
        except:
            self._connection = None
            return 0


    def _disconnect(self):
        '''Disconnect from the sever; return None'''
        if hasattr(self, '_connection') and self._connection is not None:
            self._connection = None


    def _isConnected(self):
        '''Check whether we are currently connected to the server; return
        boolean'''
        if not hasattr(self, '_connection'):
            self._connection = None

        if not self._connection is None and self._connection.getAllDomains():
            return 1
        else:
            return 0


    def canConnect(self):
        '''Checks whether we can connect using this server and user data;
        return boolean'''
        if self._isConnected():
            return 1
        else:
            try:
                return self._connect()
            except:
                return 0
            

    def getAllDomains(self):
        connected = self._isConnected() 
        if not connected: connected = self._connect()

        if connected:

            return self._connection.getAllDomains()
        else:
            return []


    def getAllLanguages(self):
        connected = self._isConnected() 
        if not connected: connected = self._connect()

        if connected:
            return self._connection.getAllLanguages()
        else:
            return []

            

    def queryMessages(self):
        connected = self._isConnected() 
        if not connected: connected = self._connect()

        if connected:            
            fmsgs = self._connection.getMessagesFor(self.sync_domains,
                                                    self.sync_languages)
        else:
            fmdgs = []

        return self.context.getMessagesMapping(self.sync_domains,
                                               self.sync_languages,
                                               fmsgs)


    def getStatus(self, fmsg, lmsg, verbose=1):
        state = 0
        if fmsg is None:
            state = 4
        elif lmsg is None:
            state = 1
        elif fmsg['mod_time'] > lmsg['mod_time']:
            state = 2
        elif fmsg['mod_time'] < lmsg['mod_time']:
            state = 3
        elif fmsg['mod_time'] == lmsg['mod_time']:
            state = 0

        if verbose:
            return self.messageStatus[state]
        return state
            

    def saveSettings(self):
        self.sync_domains = self.request.form.get('sync_domains', [])
        self.sync_languages = self.request.form.get('sync_languages', [])
        self.request.getResponse().setCookie('sync_domains',
                                             ','.join(self.sync_domains))
        self.request.getResponse().setCookie('sync_languages',
                                             ','.join(self.sync_languages))
        self.request.getResponse().setCookie('sync_url',
                            urllib.quote(self.request['sync_url']).strip())
        self.request.getResponse().setCookie('sync_username',
                                             self.request['sync_username'])
        self.request.getResponse().setCookie('sync_password',
                                             self.request['sync_password'])

        return self.request.getResponse().redirect(self.request.URL[-1]+
                                                   '/@@synchronizeForm.html')

        
    def synchronize(self):
        mapping = self.queryMessages()
        self.context.synchronize(mapping)
        return self.request.getResponse().redirect(self.request.URL[-1]+
                                                   '/@@synchronizeForm.html')


    def synchronizeMessages(self):
        idents = []
        for id in self.request.form['message_ids']:
            msgid = self.request.form['update-msgid-'+id]
            domain = self.request.form['update-domain-'+id]
            language = self.request.form['update-language-'+id]
            idents.append((msgid, domain, language))

        mapping = self.queryMessages()
        new_mapping = {}
        for item in mapping.items():
            if item[0] in idents:
                new_mapping[item[0]] = item[1]

        self.context.synchronize(new_mapping)
        return self.request.getResponse().redirect(self.request.URL[-1]+
                                                   '/@@synchronizeForm.html')



class BasicAuthTransport(xmlrpclib.Transport):
    def __init__(self, username=None, password=None, verbose=0):
        self.username=username
        self.password=password
        self.verbose=verbose

    def request(self, host, handler, request_body, verbose=0):
        # issue XML-RPC request
        
        self.verbose = verbose

        h = httplib.HTTP(host)
        h.putrequest("POST", handler)

        # required by HTTP/1.1
        h.putheader("Host", host)

        # required by XML-RPC
        h.putheader("User-Agent", self.user_agent)
        h.putheader("Content-Type", "text/xml")
        h.putheader("Content-Length", str(len(request_body)))

        # basic auth
        if self.username is not None and self.password is not None:
            h.putheader("AUTHORIZATION", "Basic %s" % 
                        encodestring("%s:%s" % (self.username, self.password)
                                      ).replace("\012", ""))
        h.endheaders()

        if request_body:
            h.send(request_body)

        errcode, errmsg, headers = h.getreply()

        if errcode != 200:
            raise xmlrpclib.ProtocolError(
                host + handler,
                errcode, errmsg,
                headers
                )

        return self.parse_response(h.getfile()) 



=== Added File Zope3/lib/python/Zope/I18n/Views/Browser/exportImport.pt ===
<html metal:use-macro="views/standard_macros/page">
<head>
  <title>Translation Service - Translate</title>
</head>
<body>

<div metal:fill-slot="body">

<h3>Import and Export Messages</h3>

<p>Here you can export and import messages from your Translation Service.</p>

<form action="./" method="post" enctype="multipart/form-data"
      i18n:domain="Zope-I18n">
  <table cols="4" width="100%" border="0">
    <tr>
      <td width="25%">
        <div class="form-label" i18n:translate="">Select Languages:</div>
        <div>
          <select name="languages:list" size="3" style="width: 80%" multiple>
            <option value="" 
                    tal:attributes="value language"
                    tal:content="language"
                    tal:repeat="language view/getAllLanguages"></option>
          </select>
        </div>
      </td>
      <td width="25%">
        <div class="form-label" i18n:translate="">Select Domains:</div>
        <div>
          <select name="domains:list" size="3" style="width: 80%" multiple>
            <option value="" 
                    tal:attributes="value domain"
                    tal:content="domain"
                    tal:repeat="domain view/getAllDomains"></option>
          </select>
        </div>
      </td>
      <td width="25%" valign="top">
        <div class="form-label" i18n:translate="">Import File Name:</div>
        <div>
          <input type="file" name="file" size="20" value="">
        </div>
        <div>
          <input type="submit" name="@@import.html:method" value="Import">
          <input type="submit" name="@@export.html:method" value="Export">
        </div>
      </td>
    </tr>
  </table>
</form>

</div>

</body>
</html>


=== Added File Zope3/lib/python/Zope/I18n/Views/Browser/synchronize.pt ===
<html metal:use-macro="views/standard_macros/page">
  <head>
    <title>Translation Service - Synchronize</title>
  </head>
<body>

<div metal:fill-slot="body">

<style type="text/css">
  <!--
  .state0 {color: green;}
  .state1 {color: yellow;}
  .state2 {color: yellow;}
  .state3 {color:  red;}
  .state4 {color:  red;}
 -->
</style>

  <table cols="4" width="100%" border="0" cellspacing="0">
    <form action="./" method="post">
      <tr>
        <td width="30%">
          <div class="form-label">Server URL</div>
          <div>
            <input type="text" size="40" name="sync_url" value=""
                   tal:attributes="value view/sync_url" />
          </div>
          <div>Username</div>
          <div>
            <input type="text" size="40" name="sync_username" value=""
                   tal:attributes="value view/sync_username" />
          </div>
          <div>Password</div>
          <div>
            <input type="password" size="40" name="sync_password" value=""
                   tal:attributes="value view/sync_password" />
          </div>
        </td>
        <td width="25%">
          <div>Select Domains:</div>
          <div>
            <select name="sync_domains:list" size="6" style="width: 80%" 
                    multiple>
              <tal:block repeat="domain view/getAllDomains">
              <option value="" 
		      tal:attributes="value domain"
		      tal:content="domain"
                      tal:condition="python: domain not in
                                     view.sync_domains" ></option>
              <option value="" selected="1"
		      tal:attributes="value domain"
		      tal:content="domain"
                      tal:condition="python: domain in
                                     view.sync_domains" ></option>
              </tal:block>
            </select>
          </div>
        </td>
        <td width="25%">
          <div>Select Languages:</div>
          <div>
            <select name="sync_languages:list" size="6" style="width: 80%" 
                    multiple>
              <tal:block repeat="language view/getAllLanguages">
              <option value="" 
		      tal:attributes="value language"
		      tal:content="language"
                      tal:condition="python: language not in
                                     view.sync_languages" ></option>
              <option value="" selected="1"
		      tal:attributes="value language"
		      tal:content="language"
                      tal:condition="python: language in
                                     view.sync_languages" ></option>
              </tal:block>
            </select>
          </div>
        </td>
        <td width="20%">
          <center>
            <div><input type="submit" name="saveSettings.html:method" 
                        value="Save Settings"></div><br>
            <div><input type="submit" name="synchronize.html:method" 
                        value="Synchronize"></div>
          </center>
        </td>
      </tr>
    </form>
  </table>
  <br>

<form action="./"
      tal:condition="view/canConnect">
  <table cols="5" width="95%" border="0" cellpadding="2" cellspacing="0" 
         class="listing">
    <tr>
      <th width="16">&nbsp;</th>
      <th width="55%">Message Id</th>
      <th width="15%">Domain</th>
      <th width="10%">Language</th>
      <th width="15%">Status</th>
    </tr>
    <tal:block repeat="message python: view.queryMessages().items()">
      <tr tal:define="number repeat/message/number;
	              oddrow repeat/message/odd"
          tal:attributes="class python: oddrow and 'odd' or 'even'">
        <td width="16">
          <input type="hidden"
      	       tal:attributes="name python: 'update-msgid-%i' %number;
                                 value python: message[0][0]">
          <input type="hidden"
      	       tal:attributes="name python: 'update-domain-%i' %number;
                                 value python: message[0][1]">
          <input type="hidden"
      	       tal:attributes="name python: 'update-language-%i' %number;
                                 value python: message[0][2]">
          <input type="checkbox" name="message_ids:list"
      	       tal:attributes="value python: number">
        </td>
        <td tal:content="python: message[0][0]">Hello World!</td>
        <td tal:content="python: message[0][1]">default</td>
        <td tal:content="python: message[0][2]">en</td>
        <td>
          <b tal:content="python: view.getStatus(*message[1])"
             tal:attributes="class python:'state%i' %
                             view.getStatus(message[1][0], message[1][1], 0)"
            >status</b>
        </td>
      </tr>
    </tal:block>
  </table>
  <div><input type="submit" name="@@synchronizeMessages.html:method" 
              value="Update"></div>

</form>

<p tal:condition="python: not view.canConnect()">
No connection could be made to remote data source.
</p>

</div>

</body>
</html>

=== Added File Zope3/lib/python/Zope/I18n/Views/Browser/translateMessage.pt ===
<html metal:use-macro="views/standard_macros/page">
<head>
  <title>Translation Service - Translate</title>
</head>
<body>

<div metal:fill-slot="body">

<span i18n:domain="Zope-I18n">

<form action="./" method="post">
<input type="hidden" name="msg_domain" value=""
       tal:attributes="value request/domain" />
<input type="hidden" name="msg_id" value=""
       tal:attributes="value request/msgid" />
<table>
  <tr>
    <th i18n:translate="">Message Id</th>
    <td tal:content="request/msgid">Message Id of the message.</td>
  </tr>
  <tr tal:repeat="language view/getEditLanguages">
    <th tal:content="language">Language</th>
    <td>
      <textarea cols="80" rows="10" name=""
                tal:attributes="name string:msg_lang_${language}" 
                tal:content="python: view.getTranslation(request['domain'], 
                             request['msgid'], language)"
       >Translation of Message</textarea>
    </td>
  </tr>
</table>
<input class="form-element" type="submit" 
       name="@@editMessage.html:method" value="Edit Message"
       i18n:attributes="value">
</form>

</span>
</div>

</body>
</html>


=== Zope3/lib/python/Zope/I18n/Views/Browser/Translate.py 1.2 => 1.3 ===
 $Id$
 """
-from Zope.Publisher.Browser.BrowserView import BrowserView
 from Zope.App.PageTemplate import ViewPageTemplateFile
-from Zope.I18n.ITranslationService import ITranslationService
 
+from BaseTranslationServiceView import BaseTranslationServiceView
 
-class Translate(BrowserView):
-    """ """
+class Translate(BaseTranslationServiceView):
     
-    __used_for__ = ITranslationService
-
-
     index = ViewPageTemplateFile('translate.pt')
+    translateMessage = ViewPageTemplateFile('translateMessage.pt')
 
 
     def getMessages(self):
@@ -47,15 +43,6 @@
                                       target_language=target_lang)
     
 
-    def getAllLanguages(self):
-        """Get all available languages from the Translation Service."""
-        return self.context.getAllLanguages()
-
-
-    def getAllDomains(self):
-        return self.context.getAllDomains()
-        
-
     def getEditLanguages(self):
         '''get the languages that are selected for editing'''
         languages = self.request.cookies.get('edit_languages', '')
@@ -67,6 +54,18 @@
         domains = self.request.cookies.get('edit_domains', '')
         return filter(None, domains.split(','))
 
+
+    def editMessage(self):
+        """ """
+        domain = self.request['msg_domain']
+        msg_id = self.request['msg_id']
+        for language in self.getEditLanguages():
+            msg = self.request['msg_lang_%s' %language]
+            if msg != self.context.translate(domain, msg_id,
+                                             target_language=language):
+                self.context.updateMessage(domain, msg_id, msg, language)
+        return self.request.getResponse().redirect(self.request.URL[-1])
+        
 
     def editMessages(self):
         """ """


=== Zope3/lib/python/Zope/I18n/Views/Browser/browser.zcml 1.2 => 1.3 ===
 
      <browser:page name="index.html" attribute="index" />
+     <browser:page name="translateMessage.html" attribute="translateMessage" />
 
      <browser:page name="editMessages.html" attribute="editMessages" />
+     <browser:page name="editMessage.html" attribute="editMessage" />
 
      <browser:page name="deleteMessages.html" attribute="deleteMessages" />
 
@@ -36,8 +38,36 @@
 
   </browser:view>
 
+  <browser:view 
+     permission="Zope.ManageServices" 
+     for="Zope.I18n.ITranslationService."
+     factory="Zope.I18n.Views.Browser.ExportImport.">
+
+     <browser:page name="exportImportForm.html" attribute="exportImportForm" />
+
+     <browser:page name="export.html" attribute="exportMessages" />
+     <browser:page name="import.html" attribute="importMessages" />
+
+  </browser:view>
+
+  <browser:view 
+     permission="Zope.ManageServices" 
+     for="Zope.I18n.ITranslationService."
+     factory="Zope.I18n.Views.Browser.Synchronize.">
+
+     <browser:page name="synchronizeForm.html" attribute="synchronizeForm" />
+     <browser:page name="synchronize.html" attribute="synchronize" />
+     <browser:page name="synchronizeMessages.html" 
+                   attribute="synchronizeMessages" />
+     <browser:page name="saveSettings.html" attribute="saveSettings" />
+
+  </browser:view>
+
+
   <zmi:tabs for="Zope.I18n.ITranslationService.">
     <zmi:tab label="Translate" action="@@index.html"/>
+    <zmi:tab label="Import/Export" action="@@exportImportForm.html"/>
+    <zmi:tab label="Synchronize" action="@@synchronizeForm.html"/>
   </zmi:tabs>
 
 </zopeConfigure>


=== Zope3/lib/python/Zope/I18n/Views/Browser/translate.pt 1.9 => 1.10 ===
 
   <form action="./" method="post">
-  <table width="100%" cellspacing="0" cellpadding="3" border="0">
+  <table width="100%" cellspacing="0" cellpadding="3" border="0" 
+         class="listing">
     <tr class="list-header" align="left">
         <th width="16">&nbsp;</th>
         <th i18n:translate="">Message Id</th>
@@ -109,56 +110,65 @@
         <th tal:repeat="language python:view.getEditLanguages()"
             tal:content="language">de</th>
     </tr>    
-    <tr tal:repeat="message python: view.getMessages()">
-      <td>
-        <input type="hidden"
-	       tal:attributes="name python: 'edit-msg_id-%i' %message[2];
-                               value python: message[0]">
-        <input type="hidden"
-	       tal:attributes="name python: 'edit-domain-%i' %message[2];
-                               value python: message[1]">
-        <input type="checkbox" name="message_ids:list"
-	       tal:attributes="value python: message[2]">
-      </td>
-      <td>
-        <a href="editMessage?messageId="
-           tal:content="python: message[0]">message_id</a>
-      </td>
-      <td tal:content="python: message[1]">
-        default
-      </td>
-      <td tal:repeat="language python:view.getEditLanguages()">
-        <textarea cols="20" rows="2"
-	  tal:attributes="name python: 'edit-%s-%i' %(language, message[2])"
-          tal:content="python: view.getTranslation(message[1], 
-                               message[0], language)"></textarea>
-      </td>
-    </tr>
-    <tr><td colspan="3"
+    <tal:block repeat="message python: view.getMessages()">
+      <tr tal:define="oddrow repeat/message/odd"
+          tal:attributes="class python: oddrow and 'odd' or 'even'">
+      	<td>
+      	  <input type="hidden"
+                 tal:attributes="name python: 'edit-msg_id-%i' %message[2];
+                                 value python: message[0]">
+          <input type="hidden"
+                 tal:attributes="name python: 'edit-domain-%i' %message[2];
+                                 value python: message[1]">
+          <input type="checkbox" name="message_ids:list"
+                 tal:attributes="value python: message[2]">
+        </td>
+        <td>
+          <a href=""
+             tal:content="python: message[0]"
+             tal:attributes="
+              href python:'translateMessage.html?msgid=%s&domain=%s' %(
+                          message[0], message[1])">message_id</a>
+        </td>
+        <td tal:content="python: message[1]">
+          default
+        </td>
+        <td tal:repeat="language python:view.getEditLanguages()">
+          <textarea cols="20" rows="2"
+             tal:attributes="name python: 'edit-%s-%i' %(language, message[2])"
+            tal:content="python: view.getTranslation(message[1], 
+                                 message[0], language)"></textarea>
+        </td>
+      </tr>
+    </tal:block>
+    <tr><th colspan="3"
          tal:attributes="colspan python:len(view.getEditLanguages())+3">
-      <hr width="80%" align="center" size="2" noshade="1">
-    </td></tr>
+      Add new messages
+    </th></tr>
     
-    <tr tal:repeat="count python:range(5)">
-      <td width="16">&nbsp;</td>
-      <td>
-        <textarea cols="20" rows="2" name=""
-                  tal:attributes="name string:new-msg_id-${count}"></textarea> 
-      </td>
-      <td>
-        <select name=""
-                tal:attributes="name string:new-domain-${count}">
-          <option value=""
-    	          tal:repeat="domain python: view.getEditDomains()"
-                  tal:content="domain"
-		  tal:attributes="value domain">default</option>
-        </select>
-      </td>
-      <td tal:repeat="language python:view.getEditLanguages()">
-        <textarea cols="20" rows="2" name=""
-          tal:attributes="name string:new-${language}-${count}"></textarea> 
-      </td>
-    </tr>
+    <tal:block repeat="count python: range(5)">
+      <tr tal:define="oddrow repeat/count/odd"
+          tal:attributes="class python: oddrow and 'odd' or 'even'">
+      	<td width="16">&nbsp;</td>
+      	<td>
+      	  <textarea cols="20" rows="2" name=""
+      	        tal:attributes="name string:new-msg_id-${count}"></textarea> 
+      	</td>
+      	<td>
+      	  <select name=""
+      	          tal:attributes="name string:new-domain-${count}">
+      	    <option value=""
+      		          tal:repeat="domain python: view.getEditDomains()"
+      	            tal:content="domain"
+      			  tal:attributes="value domain">default</option>
+      	  </select>
+      	</td>
+      	<td tal:repeat="language python:view.getEditLanguages()">
+      	  <textarea cols="20" rows="2" name=""
+      	    tal:attributes="name string:new-${language}-${count}"></textarea> 
+      	</td>
+      </tr>
+    </tal:block>
   </table>
     
   <div>