Back to index

plone3  3.1.7
GettextMessageCatalog.py
Go to the documentation of this file.
00001 """A simple implementation of a Message Catalog.
00002 
00003 $Id: GettextMessageCatalog.py 61752 2008-03-31 16:07:04Z hannosch $
00004 """
00005 
00006 from gettext import GNUTranslations
00007 import os, sys, types, traceback
00008 import glob
00009 from stat import ST_MTIME
00010 
00011 from Acquisition import aq_parent, Implicit
00012 from DateTime import DateTime
00013 from AccessControl import ClassSecurityInfo
00014 from AccessControl.Permissions import view_management_screens
00015 from Globals import InitializeClass
00016 from Globals import INSTANCE_HOME
00017 from Globals import package_home
00018 import Globals
00019 from OFS.Traversable import Traversable
00020 from Persistence import Persistent
00021 from App.Management import Tabs
00022 
00023 import logging
00024 from utils import log, make_relative_location, Registry
00025 from msgfmt import Msgfmt
00026 
00027 from Products.PageTemplates.PageTemplateFile import PageTemplateFile
00028 
00029 def ptFile(id, *filename):
00030     if type(filename[0]) is types.DictType:
00031         filename = list(filename)
00032         filename[0] = package_home(filename[0])
00033     filename = os.path.join(*filename)
00034     if not os.path.splitext(filename)[1]:
00035         filename = filename + '.pt'
00036     return PageTemplateFile(filename, '', __name__=id)
00037 
00038 permission = 'View management screens'
00039 
00040 translationRegistry = Registry()
00041 registerTranslation = translationRegistry.register
00042 rtlRegistry = Registry()
00043 registerRTL = rtlRegistry.register
00044 
00045 def getMessage(catalog, id, orig_text=None):
00046     """get message from catalog
00047 
00048     returns the message according to the id 'id' from the catalog 'catalog' or
00049     raises a KeyError if no translation was found. The return type is always
00050     unicode
00051     """
00052     msg = catalog.gettext(id)
00053     if msg is id:
00054         raise KeyError
00055     if type(msg) is types.StringType:
00056         msg = unicode(msg, catalog._charset)
00057     return msg
00058 
00059 
00060 class BrokenMessageCatalog(Persistent, Implicit, Traversable, Tabs):
00061     """ broken message catalog """
00062     meta_type = title = 'Broken Gettext Message Catalog'
00063     icon='p_/broken'
00064 
00065     isPrincipiaFolderish = 0
00066     isTopLevelPrincipiaApplicationObject = 0
00067 
00068     security = ClassSecurityInfo()
00069     security.declareObjectProtected(view_management_screens)
00070 
00071     def __init__(self, id, pofile, error):
00072         self._pofile = make_relative_location(pofile)
00073         self.id = id
00074         self._mod_time = self._getModTime()
00075         self.error = traceback.format_exception(error[0],error[1],error[2])
00076 
00077     # modified time helper
00078     def _getModTime(self):
00079         """
00080         """
00081         try:
00082             mtime = os.stat(self._getPoFile())[ST_MTIME]
00083         except (IOError, OSError):
00084             mtime = 0
00085         return mtime
00086 
00087     def getIdentifier(self):
00088         """
00089         """
00090         return self.id
00091 
00092     def getId(self):
00093         """
00094         """
00095         return self.id
00096 
00097     security.declareProtected(view_management_screens, 'getError')
00098     def getError(self):
00099         """
00100         """
00101         return self.error
00102 
00103     def _getPoFile(self):
00104         """get absolute path of the po file as string
00105         """
00106         prefix, pofile = self._pofile
00107         if prefix == 'ZOPE_HOME':
00108             return os.path.join(ZOPE_HOME, pofile)
00109         elif prefix == 'INSTANCE_HOME':
00110             return os.path.join(INSTANCE_HOME, pofile)
00111         elif prefix == 'CLIENT_HOME':
00112             return os.path.join(CLIENT_HOME, pofile)
00113         else:
00114             return os.path.normpath(pofile)
00115 
00116     security.declareProtected(view_management_screens, 'Title')
00117     def Title(self):
00118         return self.title
00119 
00120     def get_size(self):
00121         """Get the size of the underlying file."""
00122         return os.path.getsize(self._getPoFile())
00123 
00124     def reload(self, REQUEST=None):
00125         """ Forcibly re-read the file """
00126         # get pts
00127         pts = aq_parent(self)
00128         name = self.getId()
00129         pofile = self._getPoFile()
00130         pts._delObject(name)
00131         try: pts.addCatalog(GettextMessageCatalog(name, pofile))
00132         except OSError:
00133             # XXX TODO
00134             # remove a catalog if it cannot be loaded from the old location
00135             raise
00136         except:
00137             exc=sys.exc_info()
00138             log('Message Catalog has errors', logging.WARNING, name, exc)
00139             pts.addCatalog(BrokenMessageCatalog(name, pofile, exc))
00140         self = pts._getOb(name)
00141         if hasattr(REQUEST, 'RESPONSE'):
00142             if not REQUEST.form.has_key('noredir'):
00143                 REQUEST.RESPONSE.redirect(self.absolute_url())
00144 
00145     security.declareProtected(view_management_screens, 'file_exists')
00146     def file_exists(self):
00147         try:
00148             file = open(self._getPoFile(), 'rb')
00149         except:
00150             return False
00151         return True
00152 
00153     def manage_afterAdd(self, item, container): pass
00154     def manage_beforeDelete(self, item, container): pass
00155     def manage_afterClone(self, item): pass
00156 
00157     manage_options = (
00158         {'label':'Info', 'action':''},
00159         )
00160 
00161     index_html = ptFile('index_html', globals(), 'www', 'catalog_broken')
00162 
00163 InitializeClass(BrokenMessageCatalog)
00164 
00165 class GettextMessageCatalog(Persistent, Implicit, Traversable, Tabs):
00166     """
00167     Message catalog that wraps a .po file in the filesystem and stores
00168     the compiled po file in the zodb
00169     """
00170     meta_type = title = 'Gettext Message Catalog'
00171     icon = 'misc_/PlacelessTranslationService/GettextMessageCatalog.png'
00172 
00173     isPrincipiaFolderish = 0
00174     isTopLevelPrincipiaApplicationObject = 0
00175 
00176     security = ClassSecurityInfo()
00177     security.declareObjectProtected(view_management_screens)
00178 
00179     def __init__(self, id, pofile, language=None, domain=None):
00180         """Initialize the message catalog"""
00181         self._pofile   = make_relative_location(pofile)
00182         self.id        = id
00183         self._mod_time = self._getModTime()
00184         self._language = language
00185         self._domain   = domain
00186         self._prepareTranslations(0)
00187 
00188     def _prepareTranslations(self, catch=1):
00189         """Try to generate the translation object
00190            if fails remove us from registry
00191         """
00192         try: self._doPrepareTranslations()
00193         except:
00194             if self.getId() in translationRegistry.keys():
00195                 del translationRegistry[self.getId()]
00196             if not catch: raise
00197             else: pass
00198 
00199     def _doPrepareTranslations(self):
00200         """Generate the translation object from a po file
00201         """
00202         self._updateFromFS()
00203         tro = None
00204         if getattr(self, '_v_tro', None) is None:
00205             self._v_tro = tro = translationRegistry.get(self.getId(), None)
00206         if tro is None:
00207             moFile = self._getMoFile()
00208             tro = GNUTranslations(moFile)
00209             if not self._language:
00210                 self._language = (tro._info.get('language-code', None) # new way
00211                                or tro._info.get('language', None)) # old way
00212             if not self._domain:
00213                 self._domain = tro._info.get('domain', None)
00214             if self._language is None or self._domain is None:
00215                 raise ValueError, 'potfile %s has no metadata, PTS needs a language and a message domain!' % os.path.join(*self._pofile)
00216             self._language = self._language.lower().replace('_', '-')
00217             self._other_languages = tro._info.get('x-is-fallback-for', '').split()
00218             self.preferred_encodings = tro._info.get('preferred-encodings', '').split()
00219             self.name = unicode(tro._info.get('language-name', ''), tro._charset)
00220             self.default_zope_data_encoding = tro._charset
00221 
00222             translationRegistry[self.getId()] = self._v_tro = tro
00223 
00224             # right to left support
00225             is_rtl = tro._info.get('x-is-rtl', 'no').strip().lower()
00226             if is_rtl in ('yes', 'y', 'true', '1'):
00227                 self._is_rtl = True
00228             elif is_rtl in ('no', 'n', 'false', '0'):
00229                 self._is_rtl = False
00230             else:
00231                 raise ValueError, 'Unsupported value for X-Is-RTL' % is_rtl
00232             rtlRegistry[self.getId()] = self.isRTL()
00233 
00234             if self.name:
00235                 self.title = '%s language (%s) for %s' % (self._language, self.name, self._domain)
00236             else:
00237                 self.title = '%s language for %s' % (self._language, self._domain)
00238 
00239     def filtered_manage_options(self, REQUEST=None):
00240         return self.manage_options
00241 
00242     def reload(self, REQUEST=None):
00243         """Forcibly re-read the file
00244         """
00245         if self.getId() in translationRegistry.keys():
00246             del translationRegistry[self.getId()]
00247         if hasattr(self, '_v_tro'):
00248             del self._v_tro
00249         name = self.getId()
00250         pts = aq_parent(self)
00251         pofile=self._getPoFile()
00252         try:
00253             self._prepareTranslations(0)
00254             log('reloading %s: %s' % (name, self.title), severity=logging.DEBUG)
00255         except:
00256             pts._delObject(name)
00257             exc=sys.exc_info()
00258             log('Message Catalog has errors', logging.WARNING, name, exc)
00259             pts.addCatalog(BrokenMessageCatalog(name, pofile, exc))
00260         self = pts._getOb(name)
00261         if hasattr(REQUEST, 'RESPONSE'):
00262             if not REQUEST.form.has_key('noredir'):
00263                 REQUEST.RESPONSE.redirect(self.absolute_url())
00264 
00265     security.declarePublic('queryMessage')
00266     def queryMessage(self, id, default=None):
00267         """Queries the catalog for a message
00268 
00269         If the message wasn't found the default value or the id is returned.
00270         """
00271         try:
00272             return getMessage(translationRegistry[self.getId()],id,default)
00273         except KeyError:
00274             if default is None:
00275                 default = id
00276             return default
00277 
00278     def getLanguage(self):
00279         """
00280         """
00281         return self._language
00282 
00283     def getLanguageName(self):
00284         """
00285         """
00286         return self.name or self._language
00287 
00288     def getOtherLanguages(self):
00289         """
00290         """
00291         return self._other_languages
00292 
00293     def getDomain(self):
00294         """
00295         """
00296         return self._domain
00297 
00298     def getIdentifier(self):
00299         """
00300         """
00301         return self.id
00302 
00303     def getId(self):
00304         """
00305         """
00306         return self.id
00307 
00308     def getInfo(self, name):
00309         """
00310         """
00311         self._prepareTranslations()
00312         return self._v_tro._info.get(name, None)
00313     
00314     def isRTL(self):
00315         """
00316         """
00317         return self._is_rtl
00318 
00319     security.declareProtected(view_management_screens, 'Title')
00320     def Title(self):
00321         return self.title
00322 
00323     def _getMoFile(self):
00324         """get compiled version of the po file as file object
00325         """
00326         useCache = True
00327         if useCache:
00328             hit, mof = cachedPoFile(self)
00329             return mof
00330         else:
00331             mo = Msgfmt(self._readFile(), self.getId())
00332             return mo.getAsFile()
00333 
00334     def _getPoFile(self):
00335         """get absolute path of the po file as string
00336         """
00337         prefix, pofile = self._pofile
00338         if prefix == 'ZOPE_HOME':
00339             return os.path.join(ZOPE_HOME, pofile)
00340         elif prefix == 'INSTANCE_HOME':
00341             return os.path.join(INSTANCE_HOME, pofile)
00342         elif prefix == 'CLIENT_HOME':
00343             return os.path.join(CLIENT_HOME, pofile)
00344         else:
00345             return os.path.normpath(pofile)
00346 
00347     def _readFile(self, reparse=False):
00348         """Read the data from the filesystem.
00349 
00350         """
00351         file = open(self._getPoFile(), 'rb')
00352         data = []
00353         try:
00354             # XXX need more checks here
00355             data = file.readlines()
00356         finally:
00357             file.close()
00358         return data
00359 
00360     def _updateFromFS(self):
00361         """Refresh our contents from the filesystem
00362 
00363         if the file is newer and we are running in debug mode.
00364         """
00365         if Globals.DevelopmentMode:
00366             mtime = self._getModTime()
00367             if mtime != self._mod_time:
00368                 self._mod_time = mtime
00369                 self.reload()
00370 
00371     def _getModTime(self):
00372         """
00373         """
00374         try:
00375             mtime = os.stat(self._getPoFile())[ST_MTIME]
00376         except (IOError, OSError):
00377             mtime = 0
00378         return mtime
00379 
00380     def get_size(self):
00381         """Get the size of the underlying file."""
00382         return os.path.getsize(self._getPoFile())
00383 
00384     def getModTime(self):
00385         """Return the last_modified date of the file we represent.
00386 
00387         Returns a DateTime instance.
00388         """
00389         self._updateFromFS()
00390         return DateTime(self._mod_time)
00391 
00392     def getObjectFSPath(self):
00393         """Return the path of the file we represent"""
00394         return self._getPoFile()
00395 
00396     # Zope/OFS integration
00397 
00398     def manage_afterAdd(self, item, container): pass
00399     def manage_beforeDelete(self, item, container): pass
00400     def manage_afterClone(self, item): pass
00401 
00402     manage_options = (
00403         {'label':'Info', 'action':''},
00404         {'label':'Test', 'action':'zmi_test'},
00405         )
00406 
00407     index_html = ptFile('index_html', globals(), 'www', 'catalog_info')
00408     zmi_test = ptFile('zmi_test', globals(), 'www', 'catalog_test')
00409 
00410     security.declareProtected(view_management_screens, 'file_exists')
00411     def file_exists(self):
00412         try:
00413             file = open(self._getPoFile(), 'rb')
00414         except:
00415             return False
00416         return True
00417 
00418     security.declareProtected(view_management_screens, 'getEncoding')
00419     def getEncoding(self):
00420         try:
00421             content_type = self.getHeader('content-type')
00422             enc = content_type.split(';')[1].strip()
00423             enc = enc.split('=')[1]
00424         except: enc='utf-8'
00425         return enc
00426 
00427     def getHeader(self, header):
00428         self._prepareTranslations()
00429         info = self._v_tro._info
00430         return info.get(header)
00431 
00432     security.declareProtected(view_management_screens, 'displayInfo')
00433     def displayInfo(self):
00434         self._prepareTranslations()
00435         try: info = self._v_tro._info
00436         except:
00437             # broken catalog probably
00438             info={}
00439         keys = info.keys()
00440         keys.sort()
00441         return [{'name': k, 'value': info[k]} for k in keys] + [
00442             {'name': 'full path', 'value': os.path.join(*self._pofile)},
00443             {'name': 'last modification', 'value': self.getModTime().ISO()}
00444             ]
00445 
00446 InitializeClass(GettextMessageCatalog)
00447 
00448 
00449 class MoFileCache(object):
00450     """Cache for mo files
00451     """
00452     
00453     def __init__(self, path):
00454         if not os.path.isdir(path):
00455             try:
00456                 os.makedirs(path)
00457             except (IOError, OSError):
00458                 log("No permission to create directory %s" % path, logging.INFO)
00459                 path = None
00460         self._path = path
00461         
00462     def storeMoFile(self, catalog):
00463         """compile and save to mo file for catalog to disk
00464         
00465         return value: mo file as file handler
00466         """
00467         f = self.getPath(catalog)
00468         mof = self.compilePo(catalog)
00469         moExists = os.path.exists(f)
00470         if (not moExists and os.access(self._path, os.W_OK)) \
00471           or (moExists and os.access(f, os.W_OK)):
00472             fd = open(f, 'wb')
00473             fd.write(mof.read()) # XXX efficient?
00474             fd.close()
00475         else:
00476             log("No permission to write file %s" % f, logging.INFO)
00477         mof.seek(0)
00478         return mof
00479         
00480     def retrieveMoFile(self, catalog):
00481         """Load a mo file file for a catalog from disk
00482         """
00483         f = self.getPath(catalog)
00484         if os.path.isfile(f):
00485             if os.access(f, os.R_OK):
00486                 return open(f, 'rb')
00487             else:
00488                 log("No permission to read file %s" % f, logging.INFO)
00489                 return None
00490         
00491     def getPath(self, catalog):
00492         """Get the mo file path (cache path + file name)
00493         """
00494         id = catalog.getId()
00495         if id.endswith('.po'):
00496             id = id[:-3]
00497         return os.path.join(self._path, '%s.mo' % id)
00498         
00499     def isCacheHit(self, catalog):
00500         """Cache hit?
00501         
00502         True: file exists and mod time is newer than mod time of the catalog
00503         False: file exists but mod time is older
00504         None: file doesn't exist
00505         """
00506         f = self.getPath(catalog)
00507         ca_mtime = catalog._getModTime()
00508         try:
00509             mo_mtime = os.stat(f)[ST_MTIME]
00510         except (IOError, OSError):
00511             mo_mtime = 0
00512         
00513         if mo_mtime == 0:
00514             return None
00515         elif ca_mtime == 0:
00516             return None
00517         elif mo_mtime > ca_mtime:
00518             return True
00519         else:
00520             return False
00521         
00522     def compilePo(self, catalog):
00523         """compile a po file to mo
00524         
00525         returns a file handler
00526         """
00527         mo = Msgfmt(catalog._readFile(), catalog.getId())
00528         return mo.getAsFile()
00529         
00530     def cachedPoFile(self, catalog):
00531         """Cache a po file (public api)
00532         
00533         Returns a file handler on a mo file
00534         """
00535         path = self._path
00536         if path is None:
00537             return None, self.compilePo(catalog)
00538         hit = self.isCacheHit(catalog)
00539         if hit:
00540             mof = self.retrieveMoFile(catalog)
00541             if mof is None:
00542                 mof = self.compilePo(catalog)
00543         else:
00544             mof = self.storeMoFile(catalog)
00545         return hit, mof
00546         
00547     def purgeCache(self):
00548         """Purge the cache and remove all compiled mo files
00549         """
00550         log("Purging mo file cache", logging.INFO)
00551         if not os.access(self._path, os.W_OK):
00552             log("No write permission on folder %s" % self._path, logging.INFO)
00553             return False
00554         pattern = os.path.join(self._path, '*.mo')
00555         for mo in glob.glob(pattern):
00556             if not os.access(mo, os.W_OK):
00557                 log("No write permission on file %s" % mo, logging.INFO)
00558                 continue
00559             try:
00560                 os.unlink(mo)
00561             except IOError:
00562                 log("Failed to unlink %s" % mo, logging.INFO)
00563 
00564 _moCache = MoFileCache(os.path.join(CLIENT_HOME, 'pts'))
00565 cachedPoFile = _moCache.cachedPoFile
00566 purgeMoFileCache = _moCache.purgeCache