Back to index

plone3  3.1.7
PlacelessTranslationService.py
Go to the documentation of this file.
00001 import sys, os, re, fnmatch
00002 import logging
00003 from stat import ST_MTIME
00004 
00005 from zope.component import getGlobalSiteManager
00006 from zope.component import queryUtility
00007 from zope.deprecation import deprecate
00008 from zope.i18n.gettextmessagecatalog import GettextMessageCatalog
00009 from zope.i18n.translationdomain import TranslationDomain
00010 from zope.i18n.interfaces import ITranslationDomain
00011 from zope.interface import implements
00012 from zope.publisher.interfaces.browser import IBrowserRequest
00013 
00014 import Globals
00015 from ExtensionClass import Base
00016 from Acquisition import aq_acquire
00017 from Acquisition import ImplicitAcquisitionWrapper
00018 from AccessControl import ClassSecurityInfo
00019 from AccessControl.Permissions import view, view_management_screens
00020 from Globals import InitializeClass
00021 from OFS.Folder import Folder
00022 
00023 from Products.PlacelessTranslationService.load import _checkLanguage
00024 from Products.PlacelessTranslationService.load import PTS_LANGUAGES
00025 from Products.PlacelessTranslationService.lazycatalog import \
00026     LazyGettextMessageCatalog
00027 from GettextMessageCatalog import BrokenMessageCatalog
00028 from GettextMessageCatalog import GettextMessageCatalog
00029 from GettextMessageCatalog import translationRegistry
00030 from GettextMessageCatalog import rtlRegistry
00031 from GettextMessageCatalog import getMessage
00032 from Negotiator import negotiator
00033 from Domain import Domain
00034 from interfaces import IPlacelessTranslationService
00035 from memoize import memoize
00036 from msgfmt import Msgfmt
00037 from msgfmt import PoSyntaxError
00038 from utils import log, Registry
00039 
00040 PTS_IS_RTL = '_pts_is_rtl'
00041 
00042 _marker = []
00043 
00044 # Setting up some regular expressions for finding interpolation variables in
00045 # the text.
00046 NAME_RE = r"[a-zA-Z][-a-zA-Z0-9_]*"
00047 _interp_regex = re.compile(r'(?<!\$)(\$(?:%(n)s|{%(n)s}))' %({'n': NAME_RE}))
00048 _get_var_regex = re.compile(r'%(n)s' %({'n': NAME_RE}))
00049 
00050 # Note that these fallbacks are used only to find a catalog.  If a particular
00051 # message in a catalog is not translated, tough luck, you get the msgid.
00052 LANGUAGE_FALLBACKS = list(os.environ.get('LANGUAGE_FALLBACKS', 'en').split(' '))
00053 
00054 catalogRegistry = Registry()
00055 registerCatalog = catalogRegistry.register
00056 fbcatalogRegistry = Registry()
00057 registerFBCatalog = fbcatalogRegistry.register
00058 
00059 class PTSWrapper(Base):
00060     """
00061     Wrap the persistent PTS since persistent
00062     objects can't be passed around threads.
00063     """
00064 
00065     security = ClassSecurityInfo()
00066 
00067     def __init__(self, service):
00068         # get path from service
00069         self._path=service.getPhysicalPath()
00070 
00071     security.declarePrivate('load')
00072     def load(self, context):
00073         # return the real service
00074         try: root = context.getPhysicalRoot()
00075         except: return None
00076         # traverse the service
00077         return root.unrestrictedTraverse(self._path, None)
00078 
00079     security.declareProtected(view, 'translate')
00080     def translate(self, domain, msgid, mapping=None, context=None,
00081                   target_language=None, default=None):
00082         """
00083         Translate a message using Unicode.
00084         """
00085         service = self.load(context)
00086         if not service:
00087             return default
00088         return service.translate(domain, msgid, mapping, context, target_language, default)
00089 
00090     security.declareProtected(view, 'utranslate')
00091     @deprecate("The utranslate method of the PTS is deprecated and will be "
00092                "removed in the next PTS release. Use the translate method "
00093                "instead.")
00094     def utranslate(self, domain, msgid, mapping=None, context=None,
00095                   target_language=None, default=None):
00096         """
00097         Translate a message using Unicode..
00098         """
00099         service = self.load(context)
00100         if not service:
00101             return default
00102         return service.utranslate(domain, msgid, mapping, context, target_language, default)
00103 
00104     security.declarePublic(view, 'getLanguageName')
00105     def getLanguageName(self, code, context):
00106         service = self.load(context)
00107         return service.getLanguageName(code)
00108 
00109     security.declarePublic(view, 'getLanguages')
00110     def getLanguages(self, context, domain=None):
00111         service = self.load(context)
00112         return service.getLanguages(domain)
00113 
00114     security.declarePrivate('negotiate_language')
00115     def negotiate_language(self, context, domain):
00116         service = self.load(context)
00117         return service.negotiate_language(context.REQUEST,domain)
00118 
00119     security.declarePublic('isRTL')
00120     @deprecate("The isRTL method of the PTS is deprecated and will be removed "
00121                "in the next PTS release. Use the information found in the "
00122                "Zope3 locale instead.")
00123     def isRTL(self, context, domain):
00124         service = self.load(context)
00125         # Default to LtR
00126         if service is None:
00127             return False
00128         return service.isRTL(context.REQUEST,domain)
00129 
00130     def __repr__(self):
00131         """
00132         Return a string representation
00133         """
00134         return "<PTSWrapper for %s>" % '/'.join(self._path)
00135 
00136 InitializeClass(PTSWrapper)
00137 
00138 class PlacelessTranslationService(Folder):
00139     """
00140     The Placeless Translation Service
00141     """
00142     implements(IPlacelessTranslationService)
00143 
00144     meta_type = title = 'Placeless Translation Service'
00145     icon = 'misc_/PlacelessTranslationService/PlacelessTranslationService.png'
00146     # major, minor, patchlevel, internal
00147     # internal is always 0 on releases
00148     # if you hack this internally, increment it
00149     # -3 for alpha, -2 for beta, -1 for release candidate
00150     # use an internal of > 99 to recreate the PTS at every startup
00151     # (development mode)
00152     _class_version = (1, 4, 13, 0)
00153     all_meta_types = ()
00154 
00155     security = ClassSecurityInfo()
00156 
00157     def __init__(self, default_domain='global', fallbacks=None):
00158         self._instance_version = self._class_version
00159         # XXX We haven't specified that ITranslationServices have a default
00160         # domain.  So far, we've required the domain argument to .translate()
00161         self._domain = default_domain
00162         # _catalogs maps (language, domain) to identifiers
00163         catalogRegistry = {}
00164         fbcatalogRegistry = {}
00165         # What languages to fallback to, if there is no catalog for the
00166         # requested language (no fallback on individual messages)
00167         if fallbacks is None:
00168             fallbacks = LANGUAGE_FALLBACKS
00169         self._fallbacks = fallbacks
00170 
00171     def _registerMessageCatalog(self, catalog):
00172         # dont register broken message catalogs
00173         if isinstance(catalog, BrokenMessageCatalog):
00174             return
00175 
00176         domain = catalog.getDomain()
00177         language = catalog.getLanguage()
00178         catalogRegistry.setdefault((language, domain), []).append(catalog.getIdentifier())
00179         for lang in catalog.getOtherLanguages():
00180             fbcatalogRegistry.setdefault((lang, domain), []).append(catalog.getIdentifier())
00181         self._p_changed = 1
00182 
00183     def _unregister_inner(self, catalog, clist):
00184         for key, combo in clist.items():
00185             try:
00186                 combo.remove(catalog.getIdentifier())
00187             except ValueError:
00188                 continue
00189             if not combo: # removed the last catalog for a
00190                           # language/domain combination
00191                 del clist[key]
00192 
00193     def _unregisterMessageCatalog(self, catalog):
00194         self._unregister_inner(catalog, catalogRegistry)
00195         self._unregister_inner(catalog, fbcatalogRegistry)
00196         self._p_changed = 1
00197 
00198     security.declarePrivate('calculatePoId')
00199     def calculatePoId(self, name, popath, language=None, domain=None):
00200         """Calulate the po id
00201         """
00202         # instance, software and global catalog path for i18n and locales
00203         iPath       = os.path.join(Globals.INSTANCE_HOME, 'Products') + os.sep
00204         sPath       = os.path.join(Globals.SOFTWARE_HOME, 'Products') + os.sep
00205         gci18nNPath = os.path.join(Globals.INSTANCE_HOME, 'i18n')
00206         gcLocPath   = os.path.join(Globals.INSTANCE_HOME, 'locales')
00207 
00208         # a global catalog is
00209         isGlobalCatalog = False
00210 
00211         # remove [isg]Path from the popath
00212         if popath.startswith(iPath):
00213             path = popath[len(iPath):]
00214         elif popath.startswith(sPath):
00215             path = popath[len(sPath):]
00216         elif popath.startswith(gci18nNPath):
00217             path = popath[len(gci18nNPath):]
00218             isGlobalCatalog = True
00219         elif popath.startswith(gcLocPath):
00220             path = popath[len(gcLocPath):]
00221             isGlobalCatalog = True
00222         else:
00223             # po file is located at a strange place calculate the name using
00224             # the position of the i18n/locales directory
00225             p = popath.split(os.sep)
00226             try:
00227                 idx = p.index('i18n')
00228             except ValueError:
00229                 try:
00230                     idx = p.index('locales')
00231                 except ValueError:
00232                     raise OSError('Invalid po path %s for %s. That should not happen' % (popath, name))
00233             path = os.path.join(p[idx-1],p[idx])
00234 
00235         # the po file name is GlobalCatalogs-$name or MyProducts.i18n-$name
00236         # or MyProducts.locales-$name
00237         if not isGlobalCatalog:
00238             p = path.split(os.sep)
00239             pre = '.'.join(p[:2])
00240         else:
00241             pre = 'GlobalCatalogs'
00242 
00243         if language and domain:
00244             return "%s-%s-%s.po" % (pre, language, domain)
00245         else:
00246             return '%s-%s' % (pre, name)
00247 
00248     def _load_catalog_file(self, name, popath, language=None, domain=None):
00249         """
00250         create catalog instances in ZODB
00251         """
00252         id = self.calculatePoId(name, popath, language=language, domain=domain)
00253 
00254         # validate id
00255         try:
00256             self._checkId(id, 1)
00257         except:
00258             id=name # fallback mode for borked paths
00259 
00260         # the po file path
00261         pofile = os.path.join(popath, name)
00262 
00263         ob = self._getOb(id, _marker)
00264         try:
00265             if isinstance(ob, BrokenMessageCatalog):
00266                 # remove broken catalog
00267                 self._delObject(id)
00268                 ob = _marker
00269         except:
00270             pass
00271         try:
00272             if ob is _marker:
00273                 self.addCatalog(GettextMessageCatalog(id, pofile, language, domain))
00274             else:
00275                 self.reloadCatalog(ob)
00276         except IOError:
00277             # io error probably cause of missing or not accessable
00278             try:
00279                 # remove false catalog from PTS instance
00280                 self._delObject(id)
00281             except:
00282                 pass
00283         except KeyboardInterrupt:
00284             raise
00285         except:
00286             exc=sys.exc_info()
00287             log('Message Catalog has errors', logging.WARNING, pofile, exc)
00288             self.addCatalog(BrokenMessageCatalog(id, pofile, exc))
00289 
00290     def _load_i18n_dir(self, basepath):
00291         """
00292         Loads an i18n directory (Zope3 PTS format)
00293         Format:
00294             Products/MyProduct/i18n/*.po
00295         The language and domain are stored in the po file
00296         """
00297         log('looking into ' + basepath, logging.DEBUG)
00298         if not os.path.isdir(basepath):
00299             log('it does not exist', logging.DEBUG)
00300             return
00301 
00302         # print deprecation warning for mo files
00303         depr_names = fnmatch.filter(os.listdir(basepath), '*.mo')
00304         if depr_names:
00305             import warnings
00306             warnings.warn(
00307                 'Compiled po files (*.mo) found in %s. '
00308                 'PlacelessTranslationService now compiles '
00309                 'mo files automatically. All mo files have '
00310                 'been ignored.' % basepath, DeprecationWarning, stacklevel=4)
00311 
00312         # load po files
00313         names = fnmatch.filter(os.listdir(basepath), '*.po')
00314         if not names:
00315             log('nothing found', logging.DEBUG)
00316             return
00317         for name in names:
00318             self._load_catalog_file(name, basepath)
00319 
00320         log('Initialized:', detail = repr(names) + (' from %s\n' % basepath))
00321 
00322     def _updateMoFile(self, name, msgpath, lang, domain):
00323         """
00324         Creates or updates a mo file in the locales folder. Returns True if a
00325         new file was created.
00326         """
00327         pofile = os.path.normpath(os.path.join(msgpath, name))
00328         mofile = os.path.normpath(os.path.join(msgpath, domain+'.mo'))
00329         create = False
00330         update = False
00331 
00332         try:
00333             po_mtime = os.stat(pofile)[ST_MTIME]
00334         except (IOError, OSError):
00335             po_mtime = 0
00336 
00337         if os.path.exists(mofile):
00338             # Update mo file?
00339             try:
00340                 mo_mtime = os.stat(mofile)[ST_MTIME]
00341             except (IOError, OSError):
00342                 mo_mtime = 0
00343 
00344             if po_mtime > mo_mtime:
00345                 # Update mo file
00346                 update = True
00347             else:
00348                 # Mo file is current
00349                 return
00350         else:
00351             # Create mo file
00352             create = True
00353 
00354         if create or update:
00355             try:
00356                 mo = Msgfmt(pofile, domain).getAsFile()
00357                 fd = open(mofile, 'wb')
00358                 fd.write(mo.read())
00359                 fd.close()
00360 
00361             except (IOError, OSError, PoSyntaxError):
00362                 log('Error while compiling %s' % pofile, logging.WARNING)
00363                 return
00364 
00365             if create:
00366                 return True
00367 
00368         return None
00369 
00370     def _load_locales_dir(self, basepath):
00371         """
00372         Loads an locales directory (Zope3 format)
00373         Format:
00374             Products/MyProduct/locales/${lang}/LC_MESSAGES/${domain}.po
00375         Where ${lang} and ${domain} are the language and the domain of the po
00376         file (e.g. locales/de/LC_MESSAGES/plone.po)
00377         """
00378         found=[]
00379         log('looking into ' + basepath, logging.DEBUG)
00380         if not os.path.isdir(basepath):
00381             log('it does not exist', logging.DEBUG)
00382             return
00383 
00384         for lang in os.listdir(basepath):
00385             langpath = os.path.join(basepath, lang)
00386             if not os.path.isdir(langpath):
00387                 # it's not a directory
00388                 continue
00389             if not _checkLanguage(lang):
00390                 return
00391             msgpath = os.path.join(langpath, 'LC_MESSAGES')
00392             if not os.path.isdir(msgpath):
00393                 # it doesn't contain a LC_MESSAGES directory
00394                 continue
00395             names = fnmatch.filter(os.listdir(msgpath), '*.po')
00396             for name in names:
00397                 domain = name[:-3]
00398                 found.append('%s:%s' % (lang, domain))
00399                 result = self._updateMoFile(name, msgpath, lang, domain)
00400                 if result:
00401                     # Newly created file, the Z3 domain might not exist
00402                     mofile = os.path.join(msgpath, domain + '.mo')
00403                     if queryUtility(ITranslationDomain, name=domain) is None:
00404                         ts_domain = TranslationDomain(domain)
00405                         sm = getGlobalSiteManager()
00406                         sm.registerUtility(ts_domain, ITranslationDomain, name=domain)
00407 
00408                     util = queryUtility(ITranslationDomain, name=domain)
00409                     if util is not None:
00410                         if PTS_LANGUAGES is not None:
00411                             # If we have restricted the available languages,
00412                             # use the speed and not memory optimized version
00413                             cat = GettextMessageCatalog(lang, domain, mofile)
00414                         else:
00415                             # Otherwise optimize for memory footprint
00416                             cat = LazyGettextMessageCatalog(lang, domain, mofile)
00417                         # Add message catalog
00418                         util.addCatalog(cat)
00419 
00420         if not found:
00421             log('nothing found', logging.DEBUG)
00422             return
00423         log('Initialized:', detail = repr(found) + (' from %s\n' % basepath))
00424 
00425     security.declareProtected(view_management_screens, 'manage_renameObject')
00426     def manage_renameObject(self, id, new_id, REQUEST=None):
00427         """
00428         wrap manage_renameObject to deal with registration
00429         """
00430         catalog = self._getOb(id)
00431         self._unregisterMessageCatalog(catalog)
00432         Folder.manage_renameObject(self, id, new_id, REQUEST=None)
00433         self._registerMessageCatalog(catalog)
00434 
00435     def _delObject(self, id, dp=1):
00436         catalog = self._getOb(id)
00437         Folder._delObject(self, id, dp)
00438         self._unregisterMessageCatalog(catalog)
00439 
00440     security.declarePrivate('reloadCatalog')
00441     def reloadCatalog(self, catalog):
00442         # trigger an exception if we don't know anything about it
00443         id=catalog.id
00444         self._getOb(id)
00445         self._unregisterMessageCatalog(catalog)
00446         catalog.reload()
00447         catalog=self._getOb(id)
00448         self._registerMessageCatalog(catalog)
00449 
00450     security.declarePrivate('addCatalog')
00451     def addCatalog(self, catalog):
00452         try:
00453             self._delObject(catalog.id)
00454         except:
00455             pass
00456         if not isinstance(catalog, BrokenMessageCatalog):
00457             lang = catalog.getLanguage()
00458             if not _checkLanguage(lang):
00459                 return
00460         self._setObject(catalog.id, catalog, set_owner=False)
00461         log('adding %s: %s' % (catalog.id, catalog.title))
00462         self._registerMessageCatalog(catalog)
00463 
00464     security.declarePrivate('getCatalogsForTranslation')
00465     @memoize
00466     def getCatalogsForTranslation(self, request, domain, target_language=None):
00467         if target_language is None:
00468             target_language = self.negotiate_language(request, domain)
00469 
00470         # get the catalogs for translations
00471         catalog_names = catalogRegistry.get((target_language, domain), ()) or \
00472                         fbcatalogRegistry.get((target_language, domain), ())
00473         catalog_names = list(catalog_names)
00474 
00475         # get fallback catalogs
00476         for language in self._fallbacks:
00477             fallback_catalog_names = catalogRegistry.get((language, domain),  ())
00478             if fallback_catalog_names:
00479                 for fallback_catalog_name in fallback_catalog_names:
00480                     if fallback_catalog_name not in catalog_names:
00481                         catalog_names.append(fallback_catalog_name)
00482 
00483         # move global catalogs to the beginning to allow overwriting
00484         # message ids by placing a po file in INSTANCE_HOME/i18n
00485         # use pos to keep the sort order
00486         pos=0
00487         for i in range(len(catalog_names)):
00488             catalog_name = catalog_names[i]
00489             if catalog_name.startswith('GlobalCatalogs-'):
00490                 del catalog_names[i]
00491                 catalog_names.insert(pos, catalog_name)
00492                 pos+=1
00493 
00494         # test for right to left language
00495         if not request.has_key(PTS_IS_RTL):
00496             request.set(PTS_IS_RTL, False)
00497         for name in catalog_names:
00498             if rtlRegistry.get(name):
00499                 request.set(PTS_IS_RTL, True)
00500                 break
00501 
00502         return [translationRegistry[name] for name in catalog_names]
00503 
00504     security.declarePrivate('setLanguageFallbacks')
00505     def setLanguageFallbacks(self, fallbacks=None):
00506         if fallbacks is None:
00507             fallbacks = LANGUAGE_FALLBACKS
00508         self._fallbacks = fallbacks
00509 
00510     security.declareProtected(view, 'getLanguageName')
00511     def getLanguageName(self, code):
00512         for (ccode, cdomain), cnames in catalogRegistry.items():
00513             if ccode == code:
00514                 for cname in cnames:
00515                     cat = self._getOb(cname)
00516                     if cat.name:
00517                         return cat.name
00518 
00519     security.declareProtected(view, 'getLanguages')
00520     def getLanguages(self, domain=None):
00521         """
00522         Get available languages
00523         """
00524         if domain is None:
00525             # no domain, so user wants 'em all
00526             langs = catalogRegistry.keys()
00527             # uniquify
00528             d = {}
00529             for l in langs:
00530                 d[l[0]] = 1
00531             l = d.keys()
00532         else:
00533             l = [k[0] for k in catalogRegistry.keys() if k[1] == domain]
00534         l.sort()
00535         return l
00536 
00537     security.declareProtected(view, 'isRTL')
00538     @deprecate("The isRTL method of the PTS is deprecated and will be removed "
00539                "in the next PTS release. Use the information found in the "
00540                "Zope3 locale instead.")
00541     def isRTL(self, context, domain):
00542         """get RTL settings
00543         """
00544         request = getattr(context, 'REQUEST', context)
00545         pts_is_rtl = request.get(PTS_IS_RTL, None)
00546         if pts_is_rtl is None:
00547             # call getCatalogsForTranslation to initialize the negotiator
00548             self.getCatalogsForTranslation(request, domain)
00549         return request.get(PTS_IS_RTL, False)
00550 
00551     security.declareProtected(view, 'utranslate')
00552     @deprecate("The utranslate method of the PTS is deprecated and will be "
00553                "removed in the next PTS release. Use the translate method "
00554                "instead.")
00555     def utranslate(self, domain, msgid, mapping=None, context=None,
00556                   target_language=None, default=None):
00557         """
00558         translate() using Unicode.
00559         """
00560         return self.translate(domain, msgid, mapping, context,
00561                   target_language, default)
00562 
00563     security.declareProtected(view, 'translate')
00564     def translate(self, domain, msgid, mapping=None, context=None,
00565                   target_language=None, default=None):
00566         """
00567         Translate a message using Unicode.
00568         """
00569         if not msgid:
00570             # refuse to translate an empty msgid
00571             return default
00572 
00573         # ZPT passes the object as context.  That's wrong according to spec.
00574         if not IBrowserRequest.providedBy(context):
00575             context = aq_acquire(context, 'REQUEST')
00576         text = msgid
00577 
00578         catalogs = self.getCatalogsForTranslation(context, domain, target_language)
00579         for catalog in catalogs:
00580             try:
00581                 text = getMessage(catalog, msgid, default)
00582             except KeyError:
00583                 # it's not in this catalog, try the next one
00584                 continue
00585             # found!
00586             break
00587         else:
00588             # Did the fallback fail? Sigh, use the default if it is not None.
00589             if default is not None:
00590                 text = default
00591 
00592         # Now we need to do the interpolation
00593         return self.interpolate(text, mapping)
00594 
00595     security.declarePrivate('negotiate_language')
00596     @memoize
00597     def negotiate_language(self, request, domain):
00598         langs = [m[0] for m in catalogRegistry.keys() if m[1] == domain] + \
00599                 [m[0] for m in fbcatalogRegistry.keys() if m[1] == domain]
00600         for fallback in self._fallbacks:
00601             if fallback not in langs:
00602                 langs.append(fallback)
00603         return negotiator.negotiate(langs, request, 'language')
00604 
00605     security.declareProtected(view, 'getDomain')
00606     def getDomain(self, domain):
00607         """
00608         return a domain instance
00609         """
00610         return Domain(domain, self)
00611 
00612     security.declarePrivate('interpolate')
00613     def interpolate(self, text, mapping):
00614         """
00615         Insert the data passed from mapping into the text
00616         """
00617         # If the mapping does not exist or is empty, make a
00618         # "raw translation" without interpolation.
00619         if not mapping:
00620             return text
00621 
00622         # Find all the spots we want to substitute
00623         to_replace = _interp_regex.findall(text)
00624 
00625         # Now substitute with the variables in mapping
00626         for string in to_replace:
00627             var = _get_var_regex.findall(string)[0]
00628             value = mapping.get(var, None)
00629             if value is None:
00630                 value = string
00631             try:
00632                 if not isinstance(value, basestring):
00633                     value = str(value)
00634                 if isinstance(text, unicode):
00635                     value = u'%s' % value
00636                 text = text.replace(string, value)
00637             except UnicodeDecodeError, msg:
00638                 log('Decoding problem in: %s %s' % (text, msg), logging.WARNING)
00639         return text
00640 
00641     security.declareProtected(view_management_screens, 'manage_main')
00642     def manage_main(self, REQUEST, *a, **kw):
00643         """
00644         Wrap Folder's manage_main to render international characters
00645         """
00646         # ugh, API cruft
00647         if REQUEST is self and a:
00648             REQUEST = a[0]
00649             a = a[1:]
00650         # wrap the special dtml method Folder.manage_main into a valid
00651         # acquisition context. Required for Zope 2.8+.
00652         try:
00653             r = Folder.manage_main(self, self, REQUEST, *a, **kw)
00654         except AttributeError:
00655             manage_main = ImplicitAcquisitionWrapper(Folder.manage_main, self)
00656             r = manage_main(self, self, REQUEST, *a, **kw)
00657         if isinstance(r, unicode):
00658             r = r.encode('utf-8')
00659         REQUEST.RESPONSE.setHeader('Content-type', 'text/html; charset=utf-8')
00660         return r
00661 
00662 InitializeClass(PlacelessTranslationService)