[Zope-Checkins] CVS: Zope3/lib/python/Zope/I18n - GettextExportFilter.py:1.1 GettextImportFilter.py:1.1 IMessageExportFilter.py:1.1 IMessageImportFilter.py:1.1 i18n_service.gif:1.1 GettextMessageCatalog.py:1.3 IMessageCatalog.py:1.5 ITranslationService.py:1.4 MessageCatalog.py:1.6 TranslationService.py:1.8 i18n.zcml:1.9 IEditableTranslationService.py:NONE

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


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

Modified Files:
	GettextMessageCatalog.py IMessageCatalog.py 
	ITranslationService.py MessageCatalog.py TranslationService.py 
	i18n.zcml 
Added Files:
	GettextExportFilter.py GettextImportFilter.py 
	IMessageExportFilter.py IMessageImportFilter.py 
	i18n_service.gif 
Removed Files:
	IEditableTranslationService.py 
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/GettextExportFilter.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.
# 
##############################################################################
"""Translation Service Message Export Filter 

$Id: GettextExportFilter.py,v 1.1 2002/06/16 18:25:13 srichter Exp $
"""
import time
from types import StringTypes

from Zope.I18n.IMessageExportFilter import IMessageExportFilter
from Zope.I18n.ITranslationService import IWriteTranslationService


class GettextExportFilter:

    __implements__ =  IMessageExportFilter
    __used_for__ = IWriteTranslationService


    def __init__(self, service):
        self.service = service

    ############################################################
    # Implementation methods for interface
    # Zope.I18n.IMessageExportFilter.IMessageExportFilter

    def exportMessages(self, domains, languages):
        'See Zope.I18n.IMessageExportFilter.IMessageExportFilter'

        if isinstance(domains, StringTypes):
            domain = domains
        elif len(domains) == 1:
            domain = domains[0]
        else:
            raise TypeError, \
                  'Only one domain at a time is supported for gettext export.'

        if isinstance(languages, StringTypes):
            language = languages
        elif len(languages) == 1:
            language = languages[0]
        else:
            raise TypeError, \
                'Only one language at a time is supported for gettext export.'

        dt = time.time()
        dt = time.localtime(dt)
        dt = time.strftime('%Y/%m/%d %H:%M', dt)
        output = _file_header %(dt, language.encode('UTF-8'),
                                domain.encode('UTF-8'))
        service = self.service

        for msgid in service.getMessageIdsOfDomain(domain):
            msgstr = service.translate(domain, msgid,
                                       target_language=language)
            msgstr = msgstr.encode('UTF-8')
            msgid = msgid.encode('UTF-8')
            output += _msg_template %(msgid, msgstr)

        return output        

    #
    ############################################################



_file_header = '''
msgid ""
msgstr ""
"Project-Id-Version: Zope 3\\n"
"PO-Revision-Date: %s\\n"
"Last-Translator: Zope 3 Gettext Export Filter\\n"
"Zope-Language: %s\\n"
"Zope-Domain: %s\\n" 
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
'''

_msg_template = '''
msgid "%s"
msgstr "%s"
'''


=== Added File Zope3/lib/python/Zope/I18n/GettextImportFilter.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.
# 
##############################################################################
"""Translation Service Message Import Filter 

$Id: GettextImportFilter.py,v 1.1 2002/06/16 18:25:13 srichter Exp $
"""
import time, re
from types import StringTypes

from Zope.I18n.IMessageImportFilter import IMessageImportFilter
from Zope.I18n.ITranslationService import IWriteTranslationService


class GettextImportFilter:

    __implements__ =  IMessageImportFilter
    __used_for__ = IWriteTranslationService


    def __init__(self, service):
        self.service = service

    ############################################################
    # Implementation methods for interface
    # Zope.I18n.IMessageImportFilter.IMessageImportFilter

    def importMessages(self, domains, languages, file):
        'See Zope.I18n.IMessageImportFilter.IMessageImportFilter'

        if isinstance(domains, StringTypes):
            domain = domains
        elif len(domains) == 1:
            domain = domains[0]
        else:
            raise TypeError, \
                  'Only one domain at a time is supported for gettext export.'

        if isinstance(languages, StringTypes):
            language = languages
        elif len(languages) == 1:
            language = languages[0]
        else:
            raise TypeError, \
                'Only one language at a time is supported for gettext export.'

        result = parseGetText(file.readlines())[3]
        headers = parserHeaders(''.join(result[('',)][1]))
        del result[('',)]
        charset = extractCharset(headers['content-type'])
        service = self.service
        for msg in result.items():
            msgid = unicode(''.join(msg[0]), charset)
            msgid = msgid.replace('\\n', '\n')
            msgstr = unicode(''.join(msg[1][1]), charset)
            msgstr = msgstr.replace('\\n', '\n')
            service.addMessage(domain, msgid, msgstr, language)

    #
    ############################################################


def extractCharset(header):
    charset = header.split('charset=')[-1]
    return charset.lower()


def parserHeaders(headers_text):
    headers = {}
    for line in headers_text.split('\\n'):
        name = line.split(':')[0]
        value = ''.join(line.split(':')[1:])
        headers[name.lower()] = value

    return headers


def parseGetText(content):
    # The regular expressions
    com = re.compile('^#.*')
    msgid = re.compile(r'^ *msgid *"(.*?[^\\]*)"')
    msgstr = re.compile(r'^ *msgstr *"(.*?[^\\]*)"') 
    re_str = re.compile(r'^ *"(.*?[^\\])"') 
    blank = re.compile(r'^\s*$')

    trans = {}
    pointer = 0
    state = 0
    COM, MSGID, MSGSTR = [], [], []
    while pointer < len(content):
        line = content[pointer]
        #print 'STATE:', state
        #print 'LINE:', line, content[pointer].strip()
        if state == 0:
            COM, MSGID, MSGSTR = [], [], []
            if com.match(line):
                COM.append(strip(line))
                state = 1
                pointer = pointer + 1
            elif msgid.match(line):
                MSGID.append(msgid.match(line).group(1))
                state = 2
                pointer = pointer + 1
            elif blank.match(line):
                pointer = pointer + 1
            else:
                raise 'ParseError', 'state 0, line %d\n' % (pointer + 1)
        elif state == 1:
            if com.match(line):
                COM.append(strip(line))
                state = 1
                pointer = pointer + 1
            elif msgid.match(line):
                MSGID.append(msgid.match(line).group(1))
                state = 2
                pointer = pointer + 1
            elif blank.match(line):
                pointer = pointer + 1
            else:
                raise 'ParseError', 'state 1, line %d\n' % (pointer + 1)

        elif state == 2:
            if com.match(line):
                COM.append(strip(line))
                state = 2
                pointer = pointer + 1
            elif re_str.match(line):
                MSGID.append(re_str.match(line).group(1))
                state = 2
                pointer = pointer + 1
            elif msgstr.match(line):
                MSGSTR.append(msgstr.match(line).group(1))
                state = 3
                pointer = pointer + 1
            elif blank.match(line):
                pointer = pointer + 1
            else:
                raise 'ParseError', 'state 2, line %d\n' % (pointer + 1)

        elif state == 3:
            if com.match(line) or msgid.match(line):
                # print "\nEn", language, "detected", MSGID
                trans[tuple(MSGID)] = (COM, MSGSTR)
                state = 0
            elif re_str.match(line):
                MSGSTR.append(re_str.match(line).group(1))
                state = 3
                pointer = pointer + 1
            elif blank.match(line):
                pointer = pointer + 1
            else:
                raise 'ParseError', 'state 3, line %d\n' % (pointer + 1)

    # the last also goes in
    if tuple(MSGID):
        trans[tuple(MSGID)] = (COM, MSGSTR)

    return COM, MSGID, MSGSTR, trans


=== Added File Zope3/lib/python/Zope/I18n/IMessageExportFilter.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.
# 
##############################################################################
"""Translation Service Message Export Filter Interface

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

from Interface import Interface

class IMessageExportFilter(Interface):
    """The Export Filter for Translation Service Messages.

       Classes implementing this interface should usually be Adaptors, as
       they adapt the IEditableTranslationService interface."""


    def exportMessages(domains, languages):
        """Export all messages that are defined in the specified domains and
           languages.

           Note that some implementations might limit to only one domain and
           one language. A good example for that is a GettextFile.
        """


=== Added File Zope3/lib/python/Zope/I18n/IMessageImportFilter.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.
# 
##############################################################################
"""Translation Service Message Import Filter Interface

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

from Interface import Interface

class IMessageImportFilter(Interface):
    """The Import Filter for Translation Service Messages.

       Classes implementing this interface should usually be Adaptors, as
       they adapt the IEditableTranslationService interface."""


    def importMessages(domains, languages, file):
        """Import all messages that are defined in the specified domains and
           languages.

           Note that some implementations might limit to only one domain and
           one language. A good example for that is a GettextFile.
        """


=== Added File Zope3/lib/python/Zope/I18n/i18n_service.gif ===
  <Binary-ish file>

=== Zope3/lib/python/Zope/I18n/GettextMessageCatalog.py 1.2 => 1.3 ===
 
 from gettext import GNUTranslations
-from Zope.I18n.IMessageCatalog import IMessageCatalog
+from Zope.I18n.IMessageCatalog import IReadMessageCatalog
 
 
 class GettextMessageCatalog:
     """ """
 
-    __implements__ =  IMessageCatalog
+    __implements__ =  IReadMessageCatalog
 
 
     def __init__(self, language, domain, path_to_file):
@@ -45,10 +45,10 @@
 
     ############################################################
     # Implementation methods for interface
-    # Zope.I18n.IMessageCatalog.
+    # Zope.I18n.IMessageCatalog.IReadMessageCatalog
 
     def getMessage(self, id):
-        'See Zope.I18n.IMessageCatalog.IMessageCatalog'
+        'See Zope.I18n.IMessageCatalog.IReadMessageCatalog'
         self._prepareTranslations()
         msg = self.__translation_object.ugettext(id)
         if msg == id:
@@ -56,7 +56,7 @@
         return msg
 
     def queryMessage(self, id, default=None):
-        'See Zope.I18n.IMessageCatalog.IMessageCatalog'
+        'See Zope.I18n.IMessageCatalog.IReadMessageCatalog'
         self._prepareTranslations()
         text = self.__translation_object.ugettext(id)
         if text != id:
@@ -66,15 +66,15 @@
         return default
 
     def getLanguage(self):
-        'See Zope.I18n.IMessageCatalog.IMessageCatalog'
+        'See Zope.I18n.IMessageCatalog.IReadMessageCatalog'
         return self._language
         
     def getDomain(self):
-        'See Zope.I18n.IMessageCatalog.IMessageCatalog'
+        'See Zope.I18n.IMessageCatalog.IReadMessageCatalog'
         return self._domain
 
     def getIdentifier(self):
-        'See Zope.I18n.IMessageCatalog.IMessageCatalog'
+        'See Zope.I18n.IMessageCatalog.IReadMessageCatalog'
         return self._path_to_file
         
     #


=== Zope3/lib/python/Zope/I18n/IMessageCatalog.py 1.4 => 1.5 ===
 
 
-class IMessageCatalog(Interface):
+class IReadMessageCatalog(Interface):
     """A catalog (mapping) of message ids to message text strings.
 
     This interface provides a method for translating a message or message id,
@@ -37,6 +37,9 @@
 
     When we refer to text here, we mean text that follows the standard Zope 3
     text representation.
+
+    Note: The IReadMessageCatalog is the absolut minimal version required for
+          the TranslationService to function. 
     """
 
     def getMessage(msgid):
@@ -54,17 +57,51 @@
         """
 
     def getLanguage():
-        """Return the language this message catalog is representing.
-        """
+        """Return the language this message catalog is representing."""
         
     def getDomain():
-        """Return the domain this message catalog is serving.
-        """
+        """Return the domain this message catalog is serving."""
 
     def getIdentifier():
         """Return a identifier for this message catalog. Note that this
-           identifier does not have to be unique as several message catalog
-           could serve the same domain and language.
+        identifier does not have to be unique as several message catalog
+        could serve the same domain and language.
 
-           Also, there are no restrictions on the form of the identifier.
+        Also, there are no restrictions on the form of the identifier.
         """
+
+
+class IWriteMessageCatalog(Interface):
+    """If this interfaces is implemented by a message catalog, then we will be
+    able to update our messages.
+
+    Note that not all methods here require write access, but they should
+    not be required for an IReadMEssageCatalog and are used for editing
+    only. Therefore this is the more suitable interface to put them.
+    """
+
+    def getFullMessage(msgid):
+        """Get the message data and meta data as a nice dictionary. More
+        advanced implementation might choose to return an object with
+        the data, but the object should then implement IEnumerableMapping.
+
+        An exception is raised if the message id is not found.
+        """
+
+    def setMessage(msgid, message, mod_time=None):
+        """Set a message to the catalog. If mod_time is None use the current
+           time instead as modification time."""
+
+    def deleteMessage(msgid):
+        """Delete a message from the catalog."""
+
+    def getMessageIds():
+        """Get a list of all the message ids."""
+
+    def getMessages():
+        """Get a list of all the messages."""
+
+
+class IMessageCatalog(IReadMessageCatalog, IWriteMessageCatalog):
+    """Most message catalogs should support this interface. 
+    """


=== Zope3/lib/python/Zope/I18n/ITranslationService.py 1.3 => 1.4 ===
 # 
 ##############################################################################
-"""
+"""Translation Service related Interfaces.
 
 $Id$
 """
@@ -19,7 +19,7 @@
 from Interface import Interface
 
 
-class ITranslationService(Interface):
+class IReadTranslationService(Interface):
     """The Translation Service
 
     This interface provides methods for translating text, including text with
@@ -60,15 +60,15 @@
                   context=None, target_language=None):
         """Return the translation for the message referred to by msgid.
 
-           However, the method does a little more than a vanilla translation.
-           The method also looks for a possible language to translate to.
-           After a translation it also replaces any $name variable variables
-           inside the post-translation string.
-           
-           Note: The TranslationService interface does not support simplified
-                 translation methods, since it is totally hidden by ZPT and in
-                 Python you should use a Domain object, since it supports all
-                 the simplifications.
+        However, the method does a little more than a vanilla translation.
+        The method also looks for a possible language to translate to.
+        After a translation it also replaces any $name variable variables
+        inside the post-translation string.
+        
+        Note: The TranslationService interface does not support simplified
+        translation methods, since it is totally hidden by ZPT and in
+        Python you should use a Domain object, since it supports all
+        the simplifications.
         """
 
     def getDomain(domain):
@@ -76,3 +76,107 @@
 
         The domain supports the IDomain interface
         """
+
+
+class IWriteTranslationService(Interface):
+    """This interface describes the methods that are necessary for an editable
+    Translation Service to work.
+    
+    For a translation service to be editable its 'messages' have to support
+    the following information: id, string, domain, language, date
+    
+    Most of the information will be natural, since they are required by the
+    translation service, but especially the date is not a necessary info 
+    (in fact, it is meta data) 
+    """
+    
+    def getMessageIdsOfDomain(domain, filter='%'):
+        """Get all the message ids of a particular domain."""
+
+    def getMessagesOfDomain(domain):
+        """Get all the messages of a particular domain."""
+        
+    def getMessage(msgid, domain, langauge):
+        """Get the full message of a particular domain and language."""
+
+    def getAllLanguages():
+        """Find all the languages that are available"""
+
+    def getAllDomains():
+        """Find all available domains."""
+
+    def getAvailableLanguages(domain):
+        """Find all the languages that are available for this domain"""
+
+    def getAvailableDomains(language):
+        """Find all available domains."""
+        
+    def addMessage(domain, msgid, msg, language, mod_time=None):
+        """Add a message to the translation service.
+
+        If mod_time is None, then the current time should be inserted.
+        """
+
+    def updateMessage(domain, msgid, msg, language, mod_time=None):
+        """Update a message in the translation service.
+
+        If mod_time is None, then the current time should be inserted.
+        """
+
+    def deleteMessage(domain, msgid, language):
+        """Delete a messahe in the translation service."""
+        
+    def addLanguage(language):
+        """Add Language to Translation Service"""
+
+    def addDomain(domain):
+        """Add Domain to Translation Service"""
+
+    def deleteLanguage(language):
+        """Delete a Domain from the Translation Service."""
+
+    def deleteDomain(domain):
+        """Delete a Domain from the Translation Service."""
+
+
+class ISyncTranslationService(Interface):
+    """This interface allows translation services to be synchronized. The
+       following four synchronization states can exist:
+
+       0 - uptodate: The two messages are in sync.
+                Default Action: Do nothing.
+
+       1 - new: The message exists on the foreign TS, but is locally unknown.
+                Default Action: Add the message to the local catalog.
+
+       2 - older: The local version of the message is older than the one on
+                the server.
+                Default Action: Update the local message.
+
+       3 - newer: The local version is newer than the foreign version.
+                Default Action: Do nothing.
+
+       4 - deleted: The message does not exist in the foreign TS.
+                Default Action: Delete local version of message/
+    """
+
+    def getMessagesMapping(domains, languages, foreign_messages):
+        """Creates a mapping of the passed foreign messages and the local ones.
+        Retruns a status report in a dictionary with keys of the form
+        (msgid, domain, language) and values being a tuple of:
+
+        foreign_mod_date, local_mod_date
+        """
+
+    def synchronize(messages_mapping):
+        """Update the local message catalogs based on the foreign data.
+        """
+
+
+class ITranslationService(IReadTranslationService, IWriteTranslationService,
+                          ISyncTranslationService):
+    """This is the common and full-features translation service. Almost all
+    translation service implementations will use this interface.
+
+    An exception to this is the GlobalMessageCatalog as it will be read-only.
+    """


=== Zope3/lib/python/Zope/I18n/MessageCatalog.py 1.5 => 1.6 ===
 $Id$
 """
+import time
 
 from Persistence.BTrees.OOBTree import OOBTree
 from Persistence import Persistent
+from Zope.Proxy.ProxyIntrospection import removeAllProxies
 from Zope.ComponentArchitecture.IFactory import IFactory
 from Zope.App.Security.RegisteredObject import RegisteredObject
 from Zope.I18n.IMessageCatalog import IMessageCatalog
@@ -35,55 +37,85 @@
         self._domain = domain
         self._messages = OOBTree()
     
-    def setMessage(self, msgid, message):
-        """Set a message to the catalog."""
-        self._messages[msgid] = message
-
-    def deleteMessage(self, msgid):
-        """Set a message to the catalog."""
-        del self._messages[msgid]
-
-    def getMessageIds(self):
-        """Get a list of all the message ids."""
-        return list(self._messages.keys())
-        
 
     ############################################################
     # Implementation methods for interface
-    # Zope/ComponentArchitecture/IFactory.py
+    # Zope.I18n.IMessageCatalog.IMessageCatalog
 
-    def getInterfaces(self):
-        'See Zope.ComponentArchitecture.IFactory.IFactory'
-        return self.__implements__
-        
-    #
-    ############################################################
-
-    ############################################################
-    # Implementation methods for interface
-    # Zope.I18n.IMessageCatalog.
+    ######################################
+    # from: Zope.I18n.IMessageCatalog.IReadMessageCatalog
 
     def getMessage(self, id):
-        'See Zope.I18n.IMessageCatalog.IMessageCatalog'
-        return self._messages[id]
+        'See Zope.I18n.IMessageCatalog.IReadMessageCatalog'
+        return removeAllProxies(self._messages[id][0])
 
     def queryMessage(self, id, default=None):
-        'See Zope.I18n.IMessageCatalog.IMessageCatalog'
+        'See Zope.I18n.IMessageCatalog.IReadMessageCatalog'
         if default is None:
             default = id
-        return self._messages.get(id, default)
+        result = removeAllProxies(self._messages.get(id, default))
+        if result != default: result = result[0]
+        return result
 
     def getLanguage(self):
-        'See Zope.I18n.IMessageCatalog.IMessageCatalog'
+        'See Zope.I18n.IMessageCatalog.IReadMessageCatalog'
         return self._language
         
     def getDomain(self):
-        'See Zope.I18n.IMessageCatalog.IMessageCatalog'
+        'See Zope.I18n.IMessageCatalog.IReadMessageCatalog'
         return self._domain
 
     def getIdentifier(self):
-        'See Zope.I18n.IMessageCatalog.IMessageCatalog'
+        'See Zope.I18n.IMessageCatalog.IReadMessageCatalog'
         return (self._language, self._domain)
+        
+    ######################################
+    # from: Zope.I18n.IMessageCatalog.IWriteMessageCatalog
+
+    def getFullMessage(self, msgid):
+        'See Zope.I18n.IMessageCatalog.IWriteMessageCatalog'
+        message = removeAllProxies(self._messages[msgid])
+        return {'domain'   : self._domain,
+                'language' : self._language,
+                'msgid'    : msgid,
+                'msgstr'   : message[0],
+                'mod_time' : message[1]}
+
+    def setMessage(self, msgid, message, mod_time=None):
+        'See Zope.I18n.IMessageCatalog.IWriteMessageCatalog'
+        if mod_time is None:
+            mod_time = int(time.time())
+        self._messages[msgid] = (message, mod_time)
+        
+    def deleteMessage(self, msgid):
+        'See Zope.I18n.IMessageCatalog.IWriteMessageCatalog'
+        del self._messages[msgid]
+
+    def getMessageIds(self):
+        'See Zope.I18n.IMessageCatalog.IWriteMessageCatalog'
+        return list(self._messages.keys())
+
+    def getMessages(self):
+        'See Zope.I18n.IMessageCatalog.IWriteMessageCatalog'
+        messages = []
+        for message in self._messages.items():
+            messages.append({'domain'   : self._domain,
+                             'language' : self._language,
+                             'msgid'    : message[0],
+                             'msgstr'   : message[1][0],
+                             'mod_time' : message[1][1]})
+        return messages
+
+    #
+    ############################################################
+
+    ############################################################
+    # Implementation methods for interface
+    # Zope/ComponentArchitecture/IFactory.py
+
+    def getInterfaces(self):
+        'See Zope.ComponentArchitecture.IFactory.IFactory'
+        return self.__implements__
         
     #
     ############################################################


=== Zope3/lib/python/Zope/I18n/TranslationService.py 1.7 => 1.8 ===
 from Zope.I18n.IMessageCatalog import IMessageCatalog
 from Zope.I18n.ITranslationService import ITranslationService
-from Zope.I18n.IEditableTranslationService import IEditableTranslationService
 from Zope.I18n.SimpleTranslationService import SimpleTranslationService
 
 
@@ -43,7 +42,7 @@
 
 class TranslationService(BTreeContainer, SimpleTranslationService):
 
-    __implements__ =  ILocalTranslationService, IEditableTranslationService
+    __implements__ =  ILocalTranslationService
 
     def __init__(self, default_domain='global'):
         super(TranslationService, self).__init__()
@@ -95,7 +94,10 @@
 
     ############################################################
     # Implementation methods for interface
-    # Zope.I18n.ITranslationService.
+    # Zope.I18n.ITranslationService.ITranslationService
+
+    ######################################
+    # from: Zope.I18n.ITranslationService.IReadTranslationService
 
     def translate(self, domain, msgid, mapping=None, context=None,  
                   target_language=None):
@@ -124,16 +126,12 @@
         # Now we need to do the interpolation
         return self.interpolate(text, mapping)
 
-    # end Zope.I18n.ITranslationService
-    ############################################################
-
 
-    ############################################################
-    # Implementation methods for interface
-    # Zope.I18n.IEditableTranslationService.
+    ######################################
+    # from: Zope.I18n.ITranslationService.IWriteTranslationService
     
     def getMessageIdsOfDomain(self, domain, filter='%'):
-        'See IEditableTranslationService'
+        'See Zope.I18n.ITranslationService.IWriteTranslationService'
         filter = filter.replace('%', '.*')
         filter_re = re.compile(filter)
         
@@ -147,8 +145,27 @@
         return msgids.keys()
 
 
+    def getMessagesOfDomain(self, domain):
+        'See Zope.I18n.ITranslationService.IWriteTranslationService'
+        messages = []
+        languages = self.getAvailableLanguages(domain)
+        for language in languages:
+            for name in self._catalogs[(language, domain)]:
+                messages += self[name].getMessages()
+        return messages
+
+
+    def getMessage(self, msgid, domain, language):
+        'See Zope.I18n.ITranslationService.IWriteTranslationService'
+        for name in self._catalogs.get((language, domain), []):
+            try:
+                return self[name].getFullMessage(msgid)
+            except:
+                pass
+        return None
+
     def getAllLanguages(self):
-        'See IEditableTranslationService'
+        'See Zope.I18n.ITranslationService.IWriteTranslationService'
         languages = {}
         for key in self._catalogs.keys():
             languages[key[0]] = None
@@ -156,7 +173,7 @@
 
 
     def getAllDomains(self):
-        'See IEditableTranslationService'
+        'See Zope.I18n.ITranslationService.IWriteTranslationService'
         domains = {}
         for key in self._catalogs.keys():
             domains[key[1]] = None
@@ -164,7 +181,7 @@
 
 
     def getAvailableLanguages(self, domain):
-        'See IEditableTranslationService'
+        'See Zope.I18n.ITranslationService.IWriteTranslationService'
         identifiers = self._catalogs.keys()
         identifiers = filter(lambda x, d=domain: x[1] == d, identifiers)
         languages = map(lambda x: x[0], identifiers)
@@ -172,42 +189,42 @@
 
 
     def getAvailableDomains(self, language):
-        'See IEditableTranslationService'
+        'See Zope.I18n.ITranslationService.IWriteTranslationService'
         identifiers = self._catalogs.keys()
         identifiers = filter(lambda x, l=language: x[0] == l, identifiers)
         domains = map(lambda x: x[1], identifiers)
         return domains
         
 
-    def addMessage(self, domain, msg_id, msg, target_language):
-        'See IEditableTranslationService'
-        if not self._catalogs.has_key((target_language, domain)):
-            if target_language not in self.getAllLanguages():
-                self.addLanguage(target_language)
+    def addMessage(self, domain, msgid, msg, language, mod_time=None):
+        'See Zope.I18n.ITranslationService.IWriteTranslationService'
+        if not self._catalogs.has_key((language, domain)):
+            if language not in self.getAllLanguages():
+                self.addLanguage(language)
             if domain not in self.getAllDomains():
                 self.addDomain(domain)
             
-        catalog_name = self._catalogs[(target_language, domain)][0]
+        catalog_name = self._catalogs[(language, domain)][0]
         catalog = self[catalog_name]
-        catalog.setMessage(msg_id, msg)
+        catalog.setMessage(msgid, msg, mod_time)
 
 
-    def updateMessage(self, domain, msg_id, msg, target_language):
-        'See IEditableTranslationService'
-        catalog_name = self._catalogs[(target_language, domain)][0]
+    def updateMessage(self, domain, msgid, msg, language, mod_time=None):
+        'See Zope.I18n.ITranslationService.IWriteTranslationService'
+        catalog_name = self._catalogs[(language, domain)][0]
         catalog = self[catalog_name]
-        catalog.setMessage(msg_id, msg)
+        catalog.setMessage(msgid, msg, mod_time)
 
 
-    def deleteMessage(self, domain, msg_id, target_language):
-        'See IEditableTranslationService'
-        catalog_name = self._catalogs[(target_language, domain)][0]
+    def deleteMessage(self, domain, msgid, language):
+        'See Zope.I18n.ITranslationService.IWriteTranslationService'
+        catalog_name = self._catalogs[(language, domain)][0]
         catalog = self[catalog_name]
-        catalog.deleteMessage(msg_id)
+        catalog.deleteMessage(msgid)
 
 
     def addLanguage(self, language):
-        'See IEditableTranslationService'
+        'See Zope.I18n.ITranslationService.IWriteTranslationService'
         domains = self.getAllDomains()
         if not domains:
             domains = [self.default_domain]
@@ -218,7 +235,7 @@
 
 
     def addDomain(self, domain):
-        'See IEditableTranslationService'
+        'See Zope.I18n.ITranslationService.IWriteTranslationService'
         languages = self.getAllLanguages()
         if not languages:
             languages = ['en']
@@ -229,7 +246,7 @@
 
 
     def deleteLanguage(self, language):
-        'See IEditableTranslationService'
+        'See Zope.I18n.ITranslationService.IWriteTranslationService'
         domains = self.getAvailableDomains(language)
         for domain in domains:
             # Delete all catalogs from the data storage
@@ -240,7 +257,7 @@
             del self._catalogs[(language, domain)]
 
     def deleteDomain(self, domain):
-        'See IEditableTranslationService'
+        'See Zope.I18n.ITranslationService.IWriteTranslationService'
         languages = self.getAvailableLanguages(domain)
         for language in languages:
             # Delete all catalogs from the data storage
@@ -249,6 +266,51 @@
                     del self[name]
             # Now delete the specifc catalog registry for this lang/domain
             del self._catalogs[(language, domain)]
+
+
+    ######################################
+    # from: Zope.I18n.ITranslationService.ISyncTranslationService
+
+    def getMessagesMapping(self, domains, languages, foreign_messages):
+        'See Zope.I18n.ITranslationService.ISyncTranslationService'
+        mapping = {}
+        # Get all relevant local messages
+        local_messages = []
+        for domain in domains:
+            for language in languages:
+                for name in self._catalogs.get((language, domain), []):
+                    local_messages += self[name].getMessages()
+
+
+        for fmsg in foreign_messages:
+            ident = (fmsg['msgid'], fmsg['domain'], fmsg['language'])
+            mapping[ident] = (fmsg, self.getMessage(*ident))
+                
+        for lmsg in local_messages:
+            ident = (lmsg['msgid'], lmsg['domain'], lmsg['language'])
+            if ident not in mapping.keys(): 
+                mapping[ident] = (None, lmsg)
+
+        return mapping
+
+
+    def synchronize(self, messages_mapping):
+        'See Zope.I18n.ITranslationService.ISyncTranslationService'
+
+        for value in messages_mapping.values():
+            fmsg = value[0]
+            lmsg = value[1]
+            if fmsg is None:
+                self.deleteMessage(lmsg['domain'], lmsg['msgid'],
+                                   lmsg['language'])
+            elif lmsg is None:
+                self.addMessage(fmsg['domain'], fmsg['msgid'],
+                                fmsg['msgstr'], fmsg['language'],
+                                fmsg['mod_time'])
+            elif fmsg['mod_time'] > lmsg['mod_time']:
+                self.updateMessage(fmsg['domain'], fmsg['msgid'],
+                                   fmsg['msgstr'], fmsg['language'],
+                                   fmsg['mod_time'])
 
     #
     ############################################################


=== Zope3/lib/python/Zope/I18n/i18n.zcml 1.8 => 1.9 ===
                      interface="Zope.I18n.ITranslationService." 
                      />
-   <security:require permission="Zope.ManageServices"
-                     interface="Zope.I18n.IEditableTranslationService." 
-                     />
+
    <security:require permission="Zope.ManageServices"
                      interface="Zope.App.OFS.Container.IContainer." 
                      />
@@ -56,7 +54,9 @@
            permission="Zope.Public"
            component=".GlobalTranslationService.translationService" />
 
+<zmi:icon for=".ITranslationService." file="./i18n_service.gif" />
 
+<!-- Setup Message Catalogs -->
 <content class=".MessageCatalog.">
    <security:require permission="Zope.Security"
                      interface=".IMessageCatalog." 
@@ -69,6 +69,18 @@
 
 <factory component=".MessageCatalog." 
          id="Message Catalog"/>
+
+
+<!-- Setup Export and Import Filters -->
+<adapter factory=".GettextExportFilter."
+         for=".IEditableTranslationService."
+         provides=".IMessageExportFilter."
+         />
+
+<adapter factory=".GettextImportFilter."
+         for=".IEditableTranslationService."
+         provides=".IMessageImportFilter."
+         />
 
 
 <gts:registerTranslations directory="./locale" />

=== Removed File Zope3/lib/python/Zope/I18n/IEditableTranslationService.py ===